-
Notifications
You must be signed in to change notification settings - Fork 12
/
transactionHistory.ts
290 lines (269 loc) · 9.02 KB
/
transactionHistory.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
import axios from "axios";
import { contentTypeHeaders, errMsgs, graphqlEndpoint, UtilEither} from "../utils";
import { Pool } from "pg";
/**
Everything else in this repo is using graphql, so why psql here?
Hasura and the rest of the GraphQL start are _slow_ for this sort of thing.
The psql query generated by Hasura for the equiv GraphQL does several layers
of lateral joins. On my machine, such queries can take as long as 41s to run.
This SQL is fast, averaging about 10ms (though, clearly, the time scales poorly
with the number of results, as you can see by the subqueries in the select clause.
As we anticipate cardano-graphql being able to handle this in the future, I have
left the interface to match what graphql would do. For posterity's sake, I have
also left the original GraphQL query in this file.
Beware! The GraphQL query never passed tests, and doesn't pull blockindex/tx_ordinal/tx_index.
**/
const askTransactionSqlQuery = `
with
hashes as (
select distinct hash
from (select "txHash" as hash
from "TransactionInput"
where "address" = ANY(($1)::varchar array)
union
select tx.hash as hash
from tx
join tx_out
on tx.id = tx_out.tx_id
where tx_out.address = ANY(($1)::varchar array)) hashes)
select tx.hash
, tx.fee
, tx.block_index as "txIndex"
, block.block_no as "blockNumber"
, block.hash as "blockHash"
, block.epoch_no as "blockEpochNo"
, block.slot_no as "blockSlotNo"
, block.epoch_slot_no as "blockSlotInEpoch"
, case when vrf_key is null then 'byron'
else 'shelley' end
as blockEra
, block.time at time zone 'UTC' as "includedAt"
, (select json_agg(( source_tx_out.address
, source_tx_out.value
, encode(source_tx.hash, 'hex')
, tx_in.tx_out_index) order by tx_in.id asc) as inAddrValPairs
FROM tx inadd_tx
JOIN tx_in
ON tx_in.tx_in_id = inadd_tx.id
JOIN tx_out source_tx_out
ON tx_in.tx_out_id = source_tx_out.tx_id AND tx_in.tx_out_index::smallint = source_tx_out.index::smallint
JOIN tx source_tx
ON source_tx_out.tx_id = source_tx.id
where inadd_tx.hash = tx.hash) as "inAddrValPairs"
, (select json_agg(("address", "value") order by "index" asc) as outAddrValPairs
from "TransactionOutput" hasura_to
where hasura_to."txHash" = tx.hash) as "outAddrValPairs"
, (select json_agg(("address", "amount") order by "Withdrawal"."id" asc)
from "Withdrawal"
where tx_id = tx.id) as withdrawals
, pool_meta_data.hash as metadata
from tx
join hashes
on hashes.hash = tx.hash
join block
on block.id = tx.block
left join pool_meta_data
on tx.id = pool_meta_data.registered_tx_id
where block.block_no <= $2
and block.block_no > $3
order by block.time asc, tx.block_index asc
limit $4;
`;
const graphQLQuery = `
query TxsHistory(
$addresses: [String]
$limit: Int
$afterBlockNum: Int
$untilBlockNum: Int
) {
transactions(
where: {
_and: [
{ block: { number: { _gte: $afterBlockNum, _lte: $untilBlockNum } } }
{
_or: [
{ inputs: { address: { _in: $addresses } } }
{ outputs: { address: { _in: $addresses } } }
]
}
]
}
limit: $limit
order_by: { includedAt: asc }
) {
hash
block {
number
hash
epochNo
slotNo
}
includedAt
inputs {
address
value
}
outputs {
address
value
}
}
}
`;
const MAX_INT = "2147483647";
export enum BlockEra { Byron = "byron"
, Shelley = "shelley"}
interface TransactionFrag {
hash: string;
fee: string;
ttl: string;
blockEra: BlockEra;
metadata: string;
block: BlockFrag;
includedAt: Date;
inputs: TransInputFrag[];
outputs: TransOutputFrag[]; // technically a TransactionOutput fragment
txIndex: number;
withdrawals: TransOutputFrag[];
}
interface BlockFrag {
number: number;
hash: string;
epochNo: number;
slotNo: number;
}
interface TransInputFrag {
address: string;
amount: string;
id: string;
index: number;
txHash: string;
}
interface TransOutputFrag {
address: string;
amount: string;
}
export const askTransactionHistory = async (
pool: Pool
, limit: number
, addresses: string[]
, afterNum: UtilEither<BlockNumByTxHashFrag>
, untilNum: UtilEither<number>) : Promise<UtilEither<TransactionFrag[]>> => {
const ret = await pool.query(askTransactionSqlQuery, [ addresses
, untilNum.kind === "ok" ? untilNum.value : 0
, afterNum.kind === "ok" ? afterNum.value.block.number : 0
, limit]);
const txs = ret.rows.map( (row: any):TransactionFrag => {
const inputs = row.inAddrValPairs.map( ( obj:any ): TransInputFrag => ({ address: obj.f1
, amount: obj.f2.toString()
, id: obj.f3.concat(obj.f4.toString())
, index: obj.f4
, txHash: obj.f3}));
const outputs = row.outAddrValPairs.map( ( obj:any ): TransOutputFrag => ({ address: obj.f1, amount: obj.f2.toString() }));
const withdrawals : TransOutputFrag[] = row.withdrawals ? row.withdrawals.map( ( obj:any ): TransOutputFrag => ({ address: obj.f1, amount: obj.f2.toString() })) : [];
const blockFrag : BlockFrag = { number: row.blockNumber
, hash: row.blockHash.toString("hex")
, epochNo: row.blockEpochNo
, slotNo: row.blockSlotInEpoch };
return { hash: row.hash.toString("hex")
, block: blockFrag
, fee: row.fee.toString()
, metadata: (row.metadata) ? row.metadata.toString("hex") : null
, includedAt: row.includedAt
, inputs: inputs
, outputs: outputs
, ttl: MAX_INT
, blockEra: row.blockEra === "byron" ? BlockEra.Byron : BlockEra.Shelley
, txIndex: row.txIndex
, withdrawals: withdrawals
};
});
return { kind: "ok", value: txs } ;
//if('data' in ret && 'data' in ret.data && 'transactions' in ret.data.data)
// return {'kind':'ok', value:ret.data.data.transactions};
//else
// return {'kind':'error', errMsg:'TxsHistory, could not understand graphql response'};
};
interface BlockNumByTxHashFrag {
block: BlockByTxHashFrag;
hash: string;
}
interface BlockByTxHashFrag {
hash: string;
number: number;
}
export const askBlockNumByTxHash = async (hash : string|undefined): Promise<UtilEither<BlockNumByTxHashFrag>> => {
if(!hash)
return {kind:"error", errMsg: errMsgs.noValue};
const query = `
query BlockNumByTxHash($hashId: Hash32HexString!) {
transactions(
where: {
hash: {
_eq: $hashId
}
}
) {
hash
block {
number
hash
}
}
}`;
let ret = null;
try {
ret = (await axios.post(graphqlEndpoint,
JSON.stringify({ "query": query, "variables": {"hashId":hash} }),
contentTypeHeaders));
} catch (err) {
return { kind: "error", errMsg: "askBlockNumByTxHash, unable to query graphql service: " + err };
}
if("data" in ret
&& "data" in ret.data
&& "transactions" in ret.data.data
&& Array.isArray(ret.data.data.transactions))
if( ret.data.data.transactions.length > 0
&& "block" in ret.data.data.transactions[0]
&& "hash" in ret.data.data.transactions[0].block
&& "number" in ret.data.data.transactions[0].block)
return {kind:"ok", value:ret.data.data.transactions[0]};
else
return { kind:"error", errMsg: errMsgs.noValue };
else
return {kind:"error", errMsg: "Did not understand graphql response"};
} ;
export const askBlockNumByHash = async (hash : string) : Promise<UtilEither<number>> => {
const query = `
query BlockNumByHash($id: Hash32HexString!) {
blocks(
where: {
hash: {
_eq: $id
}
}
) {
number
}
}
`;
let ret = null;
try {
ret = await axios.post(graphqlEndpoint,
JSON.stringify({ "query": query, "variables": {"id":hash} }),
contentTypeHeaders);
} catch (err) {
return { kind:"error", errMsg: "askBlockNumByHash, unable to query graphql service: " + err };
}
if("data" in ret
&& "data" in ret.data
&& "blocks" in ret.data.data
&& Array.isArray(ret.data.data.blocks))
if( ret.data.data.blocks.length > 0
&& "number" in ret.data.data.blocks[0])
return {kind:"ok", value:ret.data.data.blocks[0].number};
else
return { kind:"error", errMsg: errMsgs.noValue };
else
return {kind:"error", errMsg: "askBlockNumByHash, Did not understand graphql response"};
};