@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.
@@ -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
- isRequestParam
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 GraphQLThrowableOptions = GraphQLRequestOptions & { throwOnNonNetworkErrors: true };
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
- "throwOnNonNetworkError"
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(url: ErgoGraphQLRequestOptions);
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, this.#options.url)
141
+ ? this.#getAllBoxes(args)
141
142
  : inclUnconf
142
- ? this.#getUnconfirmedBoxes(args, this.#options.url)
143
- : this.#getConfirmedBoxes(args, this.#options.url);
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(query: GraphQLBoxQuery): AsyncGenerator<ChainProviderBox<I>[]> {
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 { where, from } = query;
164
- const args = buildGqlBoxQueryArgs(where);
166
+ const { from, take } = query;
167
+ const pageSize = take ?? PAGE_SIZE;
168
+ const queries = buildGqlBoxQueries(query);
169
+ const isMempoolAware = from !== "blockchain";
165
170
 
166
- let inclChain = from !== "mempool";
167
- let inclPool = from !== "blockchain";
168
- const isMempoolAware = inclPool;
171
+ for (const query of queries) {
172
+ let inclChain = from !== "mempool";
173
+ let inclPool = from !== "blockchain";
169
174
 
170
- do {
171
- const { data } = await this.#fetchBoxes(args, inclChain, inclPool);
172
- let boxes: ChainProviderBox<I>[] = [];
175
+ while (inclChain || inclPool) {
176
+ const { data } = await this.#fetchBoxes(query, inclChain, inclPool);
177
+ let boxes: ChainProviderBox<I>[] = [];
173
178
 
174
- if (inclChain && hasConfirmed(data)) {
175
- if (some(data.boxes)) {
176
- const confirmedBoxes = (
177
- isMempoolAware ? data.boxes.filter(notBeingSpent) : data.boxes
178
- ).map((b) => mapConfirmedBox(b, this.#biMapper));
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
- boxes = boxes.concat(confirmedBoxes);
181
- }
182
-
183
- inclChain = data.boxes.length === PAGE_SIZE;
184
- }
185
+ boxes = boxes.concat(confirmedBoxes);
186
+ }
185
187
 
186
- if (isMempoolAware && hasMempool(data)) {
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
- inclPool = data.mempool.boxes.length === PAGE_SIZE;
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
- if (some(boxes)) {
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 = uniqBy(boxes, (box) => box.boxId);
206
- for (const box of boxes) returnedBoxIds.add(box.boxId);
207
- yield boxes;
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
- if (inclChain || inclPool) args.skip += PAGE_SIZE;
212
- } while (inclChain || inclPool);
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 args = buildGqlUnconfirmedTxQueryArgs(query.where);
225
-
226
- let keepFetching = true;
227
- while (keepFetching) {
228
- const response = await this.#getUnconfirmedTransactions(args);
229
- if (some(response.data?.mempool?.transactions)) {
230
- yield response.data.mempool.transactions.map((t) =>
231
- mapUnconfirmedTransaction(t, this.#biMapper)
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
- keepFetching = response.data?.mempool?.transactions?.length === PAGE_SIZE;
236
- if (keepFetching) args.skip += PAGE_SIZE;
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 args = buildGqlConfirmedTxQueryArgs(query.where);
255
-
256
- let keepFetching = true;
257
- while (keepFetching) {
258
- const response = await this.#getConfirmedTransactions(args);
259
- if (some(response.data?.transactions)) {
260
- yield response.data.transactions.map((t) =>
261
- mapConfirmedTransaction(t, this.#biMapper)
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
- keepFetching = response.data?.transactions?.length === PAGE_SIZE;
266
- if (keepFetching) args.skip += PAGE_SIZE;
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, this.#options.url);
294
+ const response = await this.#getHeaders(query);
283
295
 
284
296
  return (
285
- response.data?.blockHeaders.map((header) => ({
286
- ...header,
287
- id: header.headerId,
288
- timestamp: Number(header.timestamp),
289
- nBits: Number(header.nBits),
290
- votes: header.votes.join("")
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 buildGqlBoxQueryArgs(where: GraphQLBoxWhere) {
339
- const args = {
340
- spent: false,
341
- boxIds: merge(where.boxIds, where.boxId),
342
- ergoTrees: merge(where.ergoTrees, where.ergoTree),
343
- ergoTreeTemplateHash: where.templateHash,
344
- tokenId: where.tokenId,
345
- skip: 0,
346
- take: PAGE_SIZE
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 args;
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 buildGqlUnconfirmedTxQueryArgs(where: GraphQLConfirmedTransactionWhere) {
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: addresses.length ? addresses : undefined,
377
- transactionIds: merge(where.transactionIds, where.transactionId),
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 buildGqlConfirmedTxQueryArgs(where: GraphQLConfirmedTransactionWhere) {
384
- return {
385
- ...buildGqlUnconfirmedTxQueryArgs(where),
386
- headerId: where.headerId,
387
- minHeight: where.minHeight,
388
- onlyRelevantOutputs: where.onlyRelevantOutputs
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
@@ -1 +1,4 @@
1
1
  export * from "./ergo-graphql/ergoGraphQLProvider";
2
+ export * from "./types/blockchainProvider";
3
+ export * from "./utils/networking";
4
+ export * from "./utils/graphql";
@@ -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
- export const DEFAULT_HEADERS = {
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
- }
@@ -1,5 +1,4 @@
1
1
  import { some } from "@fleet-sdk/common";
2
- import { isEmpty } from "packages/common/src";
3
2
 
4
3
  export interface ParserLike {
5
4
  parse<T>(text: string): T;