@fleet-sdk/blockchain-providers 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +44 -12
- package/dist/index.d.ts +44 -12
- package/dist/index.js +176 -107
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +172 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/ergo-graphql/ergoGraphQLProvider.ts +149 -137
- package/src/index.ts +3 -0
- package/src/utils/graphql.ts +1 -5
- package/src/utils/networking.ts +0 -1
@@ -12,18 +12,20 @@ import type {
|
|
12
12
|
import {
|
13
13
|
type Base58String,
|
14
14
|
type BlockHeader,
|
15
|
-
ensureDefaults,
|
16
15
|
type HexString,
|
16
|
+
type SignedTransaction,
|
17
|
+
ensureDefaults,
|
17
18
|
isEmpty,
|
18
19
|
isUndefined,
|
19
20
|
NotSupportedError,
|
20
21
|
orderBy,
|
21
|
-
type SignedTransaction,
|
22
22
|
some,
|
23
23
|
uniq,
|
24
|
-
uniqBy
|
24
|
+
uniqBy,
|
25
|
+
chunk
|
25
26
|
} from "@fleet-sdk/common";
|
26
27
|
import { ErgoAddress } from "@fleet-sdk/core";
|
28
|
+
import { hex } from "@fleet-sdk/crypto";
|
27
29
|
import type {
|
28
30
|
BoxQuery,
|
29
31
|
BoxWhere,
|
@@ -39,12 +41,11 @@ import type {
|
|
39
41
|
UnconfirmedTransactionWhere
|
40
42
|
} from "../types/blockchainProvider";
|
41
43
|
import {
|
42
|
-
createGqlOperation,
|
43
44
|
type GraphQLOperation,
|
44
45
|
type GraphQLRequestOptions,
|
45
46
|
type GraphQLSuccessResponse,
|
46
47
|
type GraphQLVariables,
|
47
|
-
|
48
|
+
createGqlOperation
|
48
49
|
} from "../utils";
|
49
50
|
import {
|
50
51
|
ALL_BOXES_QUERY,
|
@@ -57,14 +58,9 @@ import {
|
|
57
58
|
UNCONF_TX_QUERY
|
58
59
|
} from "./queries";
|
59
60
|
|
60
|
-
type
|
61
|
-
type OP<R, V extends GraphQLVariables> = GraphQLOperation<GraphQLSuccessResponse<R>, V>;
|
62
|
-
type BiMapper<T> = (value: string) => T;
|
61
|
+
type SkipAndTake = { skip?: number; take?: number };
|
63
62
|
|
64
63
|
export type GraphQLBoxWhere = BoxWhere & {
|
65
|
-
/** Base16-encoded BoxIds */
|
66
|
-
boxIds?: HexString[];
|
67
|
-
|
68
64
|
/** Base16-encoded ErgoTrees */
|
69
65
|
ergoTrees?: HexString[];
|
70
66
|
|
@@ -73,13 +69,11 @@ export type GraphQLBoxWhere = BoxWhere & {
|
|
73
69
|
};
|
74
70
|
|
75
71
|
export type GraphQLConfirmedTransactionWhere = ConfirmedTransactionWhere & {
|
76
|
-
transactionIds?: HexString[];
|
77
72
|
addresses?: (Base58String | ErgoAddress)[];
|
78
73
|
ergoTrees?: HexString[];
|
79
74
|
};
|
80
75
|
|
81
76
|
export type GraphQLUnconfirmedTransactionWhere = UnconfirmedTransactionWhere & {
|
82
|
-
transactionIds?: HexString[];
|
83
77
|
addresses?: (Base58String | ErgoAddress)[];
|
84
78
|
ergoTrees?: HexString[];
|
85
79
|
};
|
@@ -87,7 +81,7 @@ export type GraphQLUnconfirmedTransactionWhere = UnconfirmedTransactionWhere & {
|
|
87
81
|
export type GraphQLBoxQuery = BoxQuery<GraphQLBoxWhere>;
|
88
82
|
export type ErgoGraphQLRequestOptions = Omit<
|
89
83
|
GraphQLRequestOptions,
|
90
|
-
"
|
84
|
+
"throwOnNonNetworkErrors"
|
91
85
|
>;
|
92
86
|
|
93
87
|
type ConfirmedBoxesResponse = { boxes: GQLBox[] };
|
@@ -100,7 +94,15 @@ type CheckTransactionResponse = { checkTransaction: string };
|
|
100
94
|
type TransactionSubmissionResponse = { submitTransaction: string };
|
101
95
|
type SignedTxArgsResp = { signedTransaction: SignedTransaction };
|
102
96
|
|
97
|
+
type GraphQLThrowableOptions = ErgoGraphQLRequestOptions & {
|
98
|
+
throwOnNonNetworkErrors: true;
|
99
|
+
};
|
100
|
+
|
101
|
+
type OP<R, V extends GraphQLVariables> = GraphQLOperation<GraphQLSuccessResponse<R>, V>;
|
102
|
+
type BiMapper<T> = (value: string) => T;
|
103
|
+
|
103
104
|
const PAGE_SIZE = 50;
|
105
|
+
const MAX_ARGS = 20;
|
104
106
|
|
105
107
|
export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
106
108
|
#options: GraphQLThrowableOptions;
|
@@ -116,15 +118,14 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
116
118
|
#getHeaders!: OP<BlockHeadersResponse, QueryBlockHeadersArgs>;
|
117
119
|
|
118
120
|
constructor(url: string);
|
119
|
-
constructor(
|
121
|
+
constructor(options: ErgoGraphQLRequestOptions);
|
120
122
|
constructor(optOrUrl: ErgoGraphQLRequestOptions | string) {
|
123
|
+
this.#biMapper = (value) => BigInt(value) as I;
|
121
124
|
this.#options = {
|
122
125
|
...(isRequestParam(optOrUrl) ? optOrUrl : { url: optOrUrl }),
|
123
126
|
throwOnNonNetworkErrors: true
|
124
127
|
};
|
125
128
|
|
126
|
-
this.#biMapper = (value) => BigInt(value) as I;
|
127
|
-
|
128
129
|
this.#getConfirmedBoxes = this.createOperation(CONF_BOXES_QUERY);
|
129
130
|
this.#getUnconfirmedBoxes = this.createOperation(UNCONF_BOXES_QUERY);
|
130
131
|
this.#getAllBoxes = this.createOperation(ALL_BOXES_QUERY);
|
@@ -137,10 +138,10 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
137
138
|
|
138
139
|
#fetchBoxes(args: QueryBoxesArgs, inclConf: boolean, inclUnconf: boolean) {
|
139
140
|
return inclConf && inclUnconf
|
140
|
-
? this.#getAllBoxes(args
|
141
|
+
? this.#getAllBoxes(args)
|
141
142
|
: inclUnconf
|
142
|
-
? this.#getUnconfirmedBoxes(args
|
143
|
-
: this.#getConfirmedBoxes(args
|
143
|
+
? this.#getUnconfirmedBoxes(args)
|
144
|
+
: this.#getConfirmedBoxes(args);
|
144
145
|
}
|
145
146
|
|
146
147
|
setUrl(url: string): ErgoGraphQLProvider<I> {
|
@@ -153,63 +154,68 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
153
154
|
return this as unknown as ErgoGraphQLProvider<M>;
|
154
155
|
}
|
155
156
|
|
156
|
-
async *streamBoxes(
|
157
|
+
async *streamBoxes(
|
158
|
+
query: GraphQLBoxQuery & SkipAndTake
|
159
|
+
): AsyncGenerator<ChainProviderBox<I>[]> {
|
157
160
|
if (isEmpty(query.where)) {
|
158
161
|
throw new Error("Cannot fetch unspent boxes without a where clause.");
|
159
162
|
}
|
160
163
|
|
161
164
|
const notBeingSpent = (box: GQLBox) => !box.beingSpent;
|
162
165
|
const returnedBoxIds = new Set<string>();
|
163
|
-
const {
|
164
|
-
const
|
166
|
+
const { from, take } = query;
|
167
|
+
const pageSize = take ?? PAGE_SIZE;
|
168
|
+
const queries = buildGqlBoxQueries(query);
|
169
|
+
const isMempoolAware = from !== "blockchain";
|
165
170
|
|
166
|
-
|
167
|
-
|
168
|
-
|
171
|
+
for (const query of queries) {
|
172
|
+
let inclChain = from !== "mempool";
|
173
|
+
let inclPool = from !== "blockchain";
|
169
174
|
|
170
|
-
|
171
|
-
|
172
|
-
|
175
|
+
while (inclChain || inclPool) {
|
176
|
+
const { data } = await this.#fetchBoxes(query, inclChain, inclPool);
|
177
|
+
let boxes: ChainProviderBox<I>[] = [];
|
173
178
|
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
+
if (inclChain && hasConfirmed(data)) {
|
180
|
+
if (some(data.boxes)) {
|
181
|
+
const confirmedBoxes = (
|
182
|
+
isMempoolAware ? data.boxes.filter(notBeingSpent) : data.boxes
|
183
|
+
).map((b) => mapConfirmedBox(b, this.#biMapper));
|
179
184
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
inclChain = data.boxes.length === PAGE_SIZE;
|
184
|
-
}
|
185
|
+
boxes = boxes.concat(confirmedBoxes);
|
186
|
+
}
|
185
187
|
|
186
|
-
|
187
|
-
if (some(data.mempool.boxes)) {
|
188
|
-
const mempoolBoxes = data.mempool.boxes
|
189
|
-
.filter(notBeingSpent)
|
190
|
-
.map((b) => mapUnconfirmedBox(b, this.#biMapper));
|
191
|
-
boxes = boxes.concat(mempoolBoxes);
|
188
|
+
inclChain = data.boxes.length === pageSize;
|
192
189
|
}
|
193
190
|
|
194
|
-
|
195
|
-
|
191
|
+
if (isMempoolAware && hasMempool(data)) {
|
192
|
+
if (some(data.mempool.boxes)) {
|
193
|
+
const mempoolBoxes = data.mempool.boxes
|
194
|
+
.filter(notBeingSpent)
|
195
|
+
.map((b) => mapUnconfirmedBox(b, this.#biMapper));
|
196
|
+
boxes = boxes.concat(mempoolBoxes);
|
197
|
+
}
|
196
198
|
|
197
|
-
|
198
|
-
// boxes can be moved from the mempool to the blockchain while streaming,
|
199
|
-
// so we need to filter out boxes that have already been returned.
|
200
|
-
if (boxes.some((box) => returnedBoxIds.has(box.boxId))) {
|
201
|
-
boxes = boxes.filter((b) => !returnedBoxIds.has(b.boxId));
|
199
|
+
inclPool = data.mempool.boxes.length === pageSize;
|
202
200
|
}
|
203
201
|
|
204
202
|
if (some(boxes)) {
|
205
|
-
boxes
|
206
|
-
|
207
|
-
|
203
|
+
// boxes can be moved from the mempool to the blockchain while streaming,
|
204
|
+
// so we need to filter out boxes that have already been returned.
|
205
|
+
if (boxes.some((box) => returnedBoxIds.has(box.boxId))) {
|
206
|
+
boxes = boxes.filter((b) => !returnedBoxIds.has(b.boxId));
|
207
|
+
}
|
208
|
+
|
209
|
+
if (some(boxes)) {
|
210
|
+
boxes = uniqBy(boxes, (box) => box.boxId);
|
211
|
+
for (const box of boxes) returnedBoxIds.add(box.boxId);
|
212
|
+
yield boxes;
|
213
|
+
}
|
208
214
|
}
|
209
|
-
}
|
210
215
|
|
211
|
-
|
212
|
-
|
216
|
+
if (inclChain || inclPool) query.skip += pageSize;
|
217
|
+
}
|
218
|
+
}
|
213
219
|
}
|
214
220
|
|
215
221
|
async getBoxes(query: GraphQLBoxQuery): Promise<ChainProviderBox<I>[]> {
|
@@ -219,21 +225,24 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
219
225
|
}
|
220
226
|
|
221
227
|
async *streamUnconfirmedTransactions(
|
222
|
-
query: TransactionQuery<GraphQLUnconfirmedTransactionWhere>
|
228
|
+
query: TransactionQuery<GraphQLUnconfirmedTransactionWhere> & SkipAndTake
|
223
229
|
): AsyncIterable<ChainProviderUnconfirmedTransaction<I>[]> {
|
224
|
-
const
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
230
|
+
const pageSize = query.take ?? PAGE_SIZE;
|
231
|
+
const queries = buildGqlUnconfirmedTxQueries(query);
|
232
|
+
|
233
|
+
for (const query of queries) {
|
234
|
+
let keepFetching = true;
|
235
|
+
while (keepFetching) {
|
236
|
+
const response = await this.#getUnconfirmedTransactions(query);
|
237
|
+
if (some(response.data?.mempool?.transactions)) {
|
238
|
+
yield response.data.mempool.transactions.map((t) =>
|
239
|
+
mapUnconfirmedTransaction(t, this.#biMapper)
|
240
|
+
);
|
241
|
+
}
|
234
242
|
|
235
|
-
|
236
|
-
|
243
|
+
keepFetching = response.data?.mempool?.transactions?.length === pageSize;
|
244
|
+
if (keepFetching) query.skip += pageSize;
|
245
|
+
}
|
237
246
|
}
|
238
247
|
}
|
239
248
|
|
@@ -249,21 +258,24 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
249
258
|
}
|
250
259
|
|
251
260
|
async *streamConfirmedTransactions(
|
252
|
-
query: TransactionQuery<GraphQLConfirmedTransactionWhere>
|
261
|
+
query: TransactionQuery<GraphQLConfirmedTransactionWhere> & SkipAndTake
|
253
262
|
): AsyncIterable<ChainProviderConfirmedTransaction<I>[]> {
|
254
|
-
const
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
263
|
+
const pageSize = query.take ?? PAGE_SIZE;
|
264
|
+
const queries = buildGqlConfirmedTxQueries(query);
|
265
|
+
|
266
|
+
for (const query of queries) {
|
267
|
+
let keepFetching = true;
|
268
|
+
while (keepFetching) {
|
269
|
+
const response = await this.#getConfirmedTransactions(query);
|
270
|
+
if (some(response.data?.transactions)) {
|
271
|
+
yield response.data.transactions.map((t) =>
|
272
|
+
mapConfirmedTransaction(t, this.#biMapper)
|
273
|
+
);
|
274
|
+
}
|
264
275
|
|
265
|
-
|
266
|
-
|
276
|
+
keepFetching = response.data?.transactions?.length === pageSize;
|
277
|
+
if (keepFetching) query.skip += pageSize;
|
278
|
+
}
|
267
279
|
}
|
268
280
|
}
|
269
281
|
|
@@ -279,15 +291,15 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
279
291
|
}
|
280
292
|
|
281
293
|
async getHeaders(query: HeaderQuery): Promise<BlockHeader[]> {
|
282
|
-
const response = await this.#getHeaders(query
|
294
|
+
const response = await this.#getHeaders(query);
|
283
295
|
|
284
296
|
return (
|
285
|
-
response.data?.blockHeaders.map((
|
286
|
-
...
|
287
|
-
id:
|
288
|
-
timestamp: Number(
|
289
|
-
nBits: Number(
|
290
|
-
votes:
|
297
|
+
response.data?.blockHeaders.map((h) => ({
|
298
|
+
...h,
|
299
|
+
id: h.headerId,
|
300
|
+
timestamp: Number(h.timestamp),
|
301
|
+
nBits: Number(h.nBits),
|
302
|
+
votes: hex.encode(Uint8Array.from(h.votes))
|
291
303
|
})) ?? []
|
292
304
|
);
|
293
305
|
}
|
@@ -306,10 +318,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
306
318
|
signedTransaction: SignedTransaction
|
307
319
|
): Promise<TransactionEvaluationResult> {
|
308
320
|
try {
|
309
|
-
const response = await this.#checkTransaction(
|
310
|
-
{ signedTransaction },
|
311
|
-
this.#options.url
|
312
|
-
);
|
321
|
+
const response = await this.#checkTransaction({ signedTransaction });
|
313
322
|
return { success: true, transactionId: response.data.checkTransaction };
|
314
323
|
} catch (e) {
|
315
324
|
return { success: false, message: (e as Error).message };
|
@@ -320,10 +329,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
320
329
|
signedTransaction: SignedTransaction
|
321
330
|
): Promise<TransactionEvaluationResult> {
|
322
331
|
try {
|
323
|
-
const response = await this.#sendTransaction(
|
324
|
-
{ signedTransaction },
|
325
|
-
this.#options.url
|
326
|
-
);
|
332
|
+
const response = await this.#sendTransaction({ signedTransaction });
|
327
333
|
return { success: true, transactionId: response.data.submitTransaction };
|
328
334
|
} catch (e) {
|
329
335
|
return { success: false, message: (e as Error).message };
|
@@ -335,58 +341,60 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
|
|
335
341
|
}
|
336
342
|
}
|
337
343
|
|
338
|
-
function
|
339
|
-
const
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
} satisfies QueryBoxesArgs;
|
348
|
-
|
349
|
-
const addresses = merge(where.addresses, where.address);
|
350
|
-
if (some(addresses)) {
|
351
|
-
const trees = addresses.map((address) =>
|
352
|
-
typeof address === "string"
|
353
|
-
? ErgoAddress.decode(address).ergoTree
|
354
|
-
: address.ergoTree
|
355
|
-
);
|
356
|
-
|
357
|
-
args.ergoTrees = uniq(some(args.ergoTrees) ? args.ergoTrees.concat(trees) : trees);
|
358
|
-
}
|
344
|
+
function buildGqlBoxQueries(query: GraphQLBoxQuery & SkipAndTake) {
|
345
|
+
const ergoTrees = uniq(
|
346
|
+
[
|
347
|
+
merge(query.where.ergoTrees, query.where.ergoTree) ?? [],
|
348
|
+
merge(query.where.addresses, query.where.address)?.map((a) =>
|
349
|
+
typeof a === "string" ? ErgoAddress.decode(a).ergoTree : a.ergoTree
|
350
|
+
) ?? []
|
351
|
+
].flat()
|
352
|
+
);
|
359
353
|
|
360
|
-
return
|
354
|
+
return chunk(ergoTrees, MAX_ARGS).map((chunk) => ({
|
355
|
+
spent: false,
|
356
|
+
boxIds: query.where.boxId ? [query.where.boxId] : undefined,
|
357
|
+
ergoTrees: chunk,
|
358
|
+
ergoTreeTemplateHash: query.where.templateHash,
|
359
|
+
tokenId: query.where.tokenId,
|
360
|
+
skip: query.skip ?? 0,
|
361
|
+
take: query.take ?? PAGE_SIZE
|
362
|
+
}));
|
361
363
|
}
|
362
364
|
|
363
|
-
function
|
365
|
+
function buildGqlUnconfirmedTxQueries(
|
366
|
+
query: TransactionQuery<GraphQLUnconfirmedTransactionWhere> & SkipAndTake
|
367
|
+
) {
|
364
368
|
const addresses = uniq(
|
365
369
|
[
|
366
|
-
merge(where.addresses, where.address)?.map((address): string =>
|
370
|
+
merge(query.where.addresses, query.where.address)?.map((address): string =>
|
367
371
|
typeof address === "string" ? address : address.encode()
|
368
372
|
) ?? [],
|
369
|
-
merge(where.ergoTrees, where.ergoTree)?.map((tree) =>
|
373
|
+
merge(query.where.ergoTrees, query.where.ergoTree)?.map((tree) =>
|
370
374
|
ErgoAddress.fromErgoTree(tree).encode()
|
371
375
|
) ?? []
|
372
376
|
].flat()
|
373
377
|
);
|
374
378
|
|
375
|
-
return {
|
376
|
-
addresses:
|
377
|
-
transactionIds:
|
378
|
-
skip: 0,
|
379
|
-
take: PAGE_SIZE
|
380
|
-
};
|
379
|
+
return chunk(addresses, MAX_ARGS).map((chunk) => ({
|
380
|
+
addresses: chunk.length ? chunk : undefined,
|
381
|
+
transactionIds: query.where.transactionId ? [query.where.transactionId] : undefined,
|
382
|
+
skip: query.skip ?? 0,
|
383
|
+
take: query.take ?? PAGE_SIZE
|
384
|
+
}));
|
381
385
|
}
|
382
386
|
|
383
|
-
function
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
387
|
+
function buildGqlConfirmedTxQueries(
|
388
|
+
query: TransactionQuery<GraphQLConfirmedTransactionWhere> & SkipAndTake
|
389
|
+
) {
|
390
|
+
return buildGqlUnconfirmedTxQueries(
|
391
|
+
query as TransactionQuery<GraphQLUnconfirmedTransactionWhere>
|
392
|
+
).map((q) => ({
|
393
|
+
...q,
|
394
|
+
headerId: query.where.headerId,
|
395
|
+
minHeight: query.where.minHeight,
|
396
|
+
onlyRelevantOutputs: query.where.onlyRelevantOutputs
|
397
|
+
}));
|
390
398
|
}
|
391
399
|
|
392
400
|
function merge<T>(array?: T[], el?: T) {
|
@@ -481,3 +489,7 @@ function mapConfirmedTransaction<T>(
|
|
481
489
|
confirmed: true
|
482
490
|
};
|
483
491
|
}
|
492
|
+
|
493
|
+
export function isRequestParam(obj: unknown): obj is ErgoGraphQLRequestOptions {
|
494
|
+
return typeof obj === "object" && (obj as ErgoGraphQLRequestOptions).url !== undefined;
|
495
|
+
}
|
package/src/index.ts
CHANGED
package/src/utils/graphql.ts
CHANGED
@@ -9,7 +9,7 @@ import type { FallbackRetryOptions, ParserLike } from "./networking";
|
|
9
9
|
import { request } from "./networking";
|
10
10
|
|
11
11
|
const OP_NAME_REGEX = /(query|mutation)\s?([\w\-_]+)?/;
|
12
|
-
|
12
|
+
const DEFAULT_HEADERS = {
|
13
13
|
"content-type": "application/json; charset=utf-8",
|
14
14
|
accept: "application/graphql-response+json, application/json"
|
15
15
|
};
|
@@ -118,7 +118,3 @@ export function gql(query: TemplateStringsArray): string {
|
|
118
118
|
export function getOpName(query: string): string | undefined {
|
119
119
|
return OP_NAME_REGEX.exec(query)?.at(2);
|
120
120
|
}
|
121
|
-
|
122
|
-
export function isRequestParam(obj: unknown): obj is GraphQLRequestOptions {
|
123
|
-
return typeof obj === "object" && (obj as GraphQLRequestOptions).url !== undefined;
|
124
|
-
}
|