@fleet-sdk/blockchain-providers 0.6.0 → 0.6.1

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,7 @@ 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;
63
-
64
61
  export type GraphQLBoxWhere = BoxWhere & {
65
- /** Base16-encoded BoxIds */
66
- boxIds?: HexString[];
67
-
68
62
  /** Base16-encoded ErgoTrees */
69
63
  ergoTrees?: HexString[];
70
64
 
@@ -73,13 +67,11 @@ export type GraphQLBoxWhere = BoxWhere & {
73
67
  };
74
68
 
75
69
  export type GraphQLConfirmedTransactionWhere = ConfirmedTransactionWhere & {
76
- transactionIds?: HexString[];
77
70
  addresses?: (Base58String | ErgoAddress)[];
78
71
  ergoTrees?: HexString[];
79
72
  };
80
73
 
81
74
  export type GraphQLUnconfirmedTransactionWhere = UnconfirmedTransactionWhere & {
82
- transactionIds?: HexString[];
83
75
  addresses?: (Base58String | ErgoAddress)[];
84
76
  ergoTrees?: HexString[];
85
77
  };
@@ -87,7 +79,7 @@ export type GraphQLUnconfirmedTransactionWhere = UnconfirmedTransactionWhere & {
87
79
  export type GraphQLBoxQuery = BoxQuery<GraphQLBoxWhere>;
88
80
  export type ErgoGraphQLRequestOptions = Omit<
89
81
  GraphQLRequestOptions,
90
- "throwOnNonNetworkError"
82
+ "throwOnNonNetworkErrors"
91
83
  >;
92
84
 
93
85
  type ConfirmedBoxesResponse = { boxes: GQLBox[] };
@@ -100,7 +92,15 @@ type CheckTransactionResponse = { checkTransaction: string };
100
92
  type TransactionSubmissionResponse = { submitTransaction: string };
101
93
  type SignedTxArgsResp = { signedTransaction: SignedTransaction };
102
94
 
95
+ type GraphQLThrowableOptions = ErgoGraphQLRequestOptions & {
96
+ throwOnNonNetworkErrors: true;
97
+ };
98
+
99
+ type OP<R, V extends GraphQLVariables> = GraphQLOperation<GraphQLSuccessResponse<R>, V>;
100
+ type BiMapper<T> = (value: string) => T;
101
+
103
102
  const PAGE_SIZE = 50;
103
+ const MAX_ARGS = 20;
104
104
 
105
105
  export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
106
106
  #options: GraphQLThrowableOptions;
@@ -116,15 +116,14 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
116
116
  #getHeaders!: OP<BlockHeadersResponse, QueryBlockHeadersArgs>;
117
117
 
118
118
  constructor(url: string);
119
- constructor(url: ErgoGraphQLRequestOptions);
119
+ constructor(options: ErgoGraphQLRequestOptions);
120
120
  constructor(optOrUrl: ErgoGraphQLRequestOptions | string) {
121
+ this.#biMapper = (value) => BigInt(value) as I;
121
122
  this.#options = {
122
123
  ...(isRequestParam(optOrUrl) ? optOrUrl : { url: optOrUrl }),
123
124
  throwOnNonNetworkErrors: true
124
125
  };
125
126
 
126
- this.#biMapper = (value) => BigInt(value) as I;
127
-
128
127
  this.#getConfirmedBoxes = this.createOperation(CONF_BOXES_QUERY);
129
128
  this.#getUnconfirmedBoxes = this.createOperation(UNCONF_BOXES_QUERY);
130
129
  this.#getAllBoxes = this.createOperation(ALL_BOXES_QUERY);
@@ -137,10 +136,10 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
137
136
 
138
137
  #fetchBoxes(args: QueryBoxesArgs, inclConf: boolean, inclUnconf: boolean) {
139
138
  return inclConf && inclUnconf
140
- ? this.#getAllBoxes(args, this.#options.url)
139
+ ? this.#getAllBoxes(args)
141
140
  : inclUnconf
142
- ? this.#getUnconfirmedBoxes(args, this.#options.url)
143
- : this.#getConfirmedBoxes(args, this.#options.url);
141
+ ? this.#getUnconfirmedBoxes(args)
142
+ : this.#getConfirmedBoxes(args);
144
143
  }
145
144
 
146
145
  setUrl(url: string): ErgoGraphQLProvider<I> {
@@ -161,55 +160,57 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
161
160
  const notBeingSpent = (box: GQLBox) => !box.beingSpent;
162
161
  const returnedBoxIds = new Set<string>();
163
162
  const { where, from } = query;
164
- const args = buildGqlBoxQueryArgs(where);
163
+ const queries = buildGqlBoxQueries(where);
164
+ const isMempoolAware = from !== "blockchain";
165
165
 
166
- let inclChain = from !== "mempool";
167
- let inclPool = from !== "blockchain";
168
- const isMempoolAware = inclPool;
166
+ for (const query of queries) {
167
+ let inclChain = from !== "mempool";
168
+ let inclPool = from !== "blockchain";
169
169
 
170
- do {
171
- const { data } = await this.#fetchBoxes(args, inclChain, inclPool);
172
- let boxes: ChainProviderBox<I>[] = [];
170
+ while (inclChain || inclPool) {
171
+ const { data } = await this.#fetchBoxes(query, inclChain, inclPool);
172
+ let boxes: ChainProviderBox<I>[] = [];
173
173
 
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));
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
179
 
180
- boxes = boxes.concat(confirmedBoxes);
181
- }
180
+ boxes = boxes.concat(confirmedBoxes);
181
+ }
182
182
 
183
- inclChain = data.boxes.length === PAGE_SIZE;
184
- }
185
-
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);
183
+ inclChain = data.boxes.length === PAGE_SIZE;
192
184
  }
193
185
 
194
- inclPool = data.mempool.boxes.length === PAGE_SIZE;
195
- }
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);
192
+ }
196
193
 
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));
194
+ inclPool = data.mempool.boxes.length === PAGE_SIZE;
202
195
  }
203
196
 
204
197
  if (some(boxes)) {
205
- boxes = uniqBy(boxes, (box) => box.boxId);
206
- for (const box of boxes) returnedBoxIds.add(box.boxId);
207
- yield 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));
202
+ }
203
+
204
+ if (some(boxes)) {
205
+ boxes = uniqBy(boxes, (box) => box.boxId);
206
+ for (const box of boxes) returnedBoxIds.add(box.boxId);
207
+ yield boxes;
208
+ }
208
209
  }
209
- }
210
210
 
211
- if (inclChain || inclPool) args.skip += PAGE_SIZE;
212
- } while (inclChain || inclPool);
211
+ if (inclChain || inclPool) query.skip += PAGE_SIZE;
212
+ }
213
+ }
213
214
  }
214
215
 
215
216
  async getBoxes(query: GraphQLBoxQuery): Promise<ChainProviderBox<I>[]> {
@@ -221,19 +222,21 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
221
222
  async *streamUnconfirmedTransactions(
222
223
  query: TransactionQuery<GraphQLUnconfirmedTransactionWhere>
223
224
  ): 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
- }
225
+ const queries = buildGqlUnconfirmedTxQueries(query.where);
226
+
227
+ for (const query of queries) {
228
+ let keepFetching = true;
229
+ while (keepFetching) {
230
+ const response = await this.#getUnconfirmedTransactions(query);
231
+ if (some(response.data?.mempool?.transactions)) {
232
+ yield response.data.mempool.transactions.map((t) =>
233
+ mapUnconfirmedTransaction(t, this.#biMapper)
234
+ );
235
+ }
234
236
 
235
- keepFetching = response.data?.mempool?.transactions?.length === PAGE_SIZE;
236
- if (keepFetching) args.skip += PAGE_SIZE;
237
+ keepFetching = response.data?.mempool?.transactions?.length === PAGE_SIZE;
238
+ if (keepFetching) query.skip += PAGE_SIZE;
239
+ }
237
240
  }
238
241
  }
239
242
 
@@ -251,19 +254,21 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
251
254
  async *streamConfirmedTransactions(
252
255
  query: TransactionQuery<GraphQLConfirmedTransactionWhere>
253
256
  ): 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
- }
257
+ const queries = buildGqlConfirmedTxQueries(query.where);
258
+
259
+ for (const query of queries) {
260
+ let keepFetching = true;
261
+ while (keepFetching) {
262
+ const response = await this.#getConfirmedTransactions(query);
263
+ if (some(response.data?.transactions)) {
264
+ yield response.data.transactions.map((t) =>
265
+ mapConfirmedTransaction(t, this.#biMapper)
266
+ );
267
+ }
264
268
 
265
- keepFetching = response.data?.transactions?.length === PAGE_SIZE;
266
- if (keepFetching) args.skip += PAGE_SIZE;
269
+ keepFetching = response.data?.transactions?.length === PAGE_SIZE;
270
+ if (keepFetching) query.skip += PAGE_SIZE;
271
+ }
267
272
  }
268
273
  }
269
274
 
@@ -279,15 +284,15 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
279
284
  }
280
285
 
281
286
  async getHeaders(query: HeaderQuery): Promise<BlockHeader[]> {
282
- const response = await this.#getHeaders(query, this.#options.url);
287
+ const response = await this.#getHeaders(query);
283
288
 
284
289
  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("")
290
+ response.data?.blockHeaders.map((h) => ({
291
+ ...h,
292
+ id: h.headerId,
293
+ timestamp: Number(h.timestamp),
294
+ nBits: Number(h.nBits),
295
+ votes: hex.encode(Uint8Array.from(h.votes))
291
296
  })) ?? []
292
297
  );
293
298
  }
@@ -306,10 +311,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
306
311
  signedTransaction: SignedTransaction
307
312
  ): Promise<TransactionEvaluationResult> {
308
313
  try {
309
- const response = await this.#checkTransaction(
310
- { signedTransaction },
311
- this.#options.url
312
- );
314
+ const response = await this.#checkTransaction({ signedTransaction });
313
315
  return { success: true, transactionId: response.data.checkTransaction };
314
316
  } catch (e) {
315
317
  return { success: false, message: (e as Error).message };
@@ -320,10 +322,7 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
320
322
  signedTransaction: SignedTransaction
321
323
  ): Promise<TransactionEvaluationResult> {
322
324
  try {
323
- const response = await this.#sendTransaction(
324
- { signedTransaction },
325
- this.#options.url
326
- );
325
+ const response = await this.#sendTransaction({ signedTransaction });
327
326
  return { success: true, transactionId: response.data.submitTransaction };
328
327
  } catch (e) {
329
328
  return { success: false, message: (e as Error).message };
@@ -335,32 +334,28 @@ export class ErgoGraphQLProvider<I = bigint> implements IBlockchainProvider<I> {
335
334
  }
336
335
  }
337
336
 
338
- function buildGqlBoxQueryArgs(where: GraphQLBoxWhere) {
339
- const args = {
337
+ function buildGqlBoxQueries(where: GraphQLBoxWhere) {
338
+ const ergoTrees = uniq(
339
+ [
340
+ merge(where.ergoTrees, where.ergoTree) ?? [],
341
+ merge(where.addresses, where.address)?.map((a) =>
342
+ typeof a === "string" ? ErgoAddress.decode(a).ergoTree : a.ergoTree
343
+ ) ?? []
344
+ ].flat()
345
+ );
346
+
347
+ return chunk(ergoTrees, MAX_ARGS).map((chunk) => ({
340
348
  spent: false,
341
- boxIds: merge(where.boxIds, where.boxId),
342
- ergoTrees: merge(where.ergoTrees, where.ergoTree),
349
+ boxIds: where.boxId ? [where.boxId] : undefined,
350
+ ergoTrees: chunk,
343
351
  ergoTreeTemplateHash: where.templateHash,
344
352
  tokenId: where.tokenId,
345
353
  skip: 0,
346
354
  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
- }
359
-
360
- return args;
355
+ }));
361
356
  }
362
357
 
363
- function buildGqlUnconfirmedTxQueryArgs(where: GraphQLConfirmedTransactionWhere) {
358
+ function buildGqlUnconfirmedTxQueries(where: GraphQLConfirmedTransactionWhere) {
364
359
  const addresses = uniq(
365
360
  [
366
361
  merge(where.addresses, where.address)?.map((address): string =>
@@ -372,21 +367,21 @@ function buildGqlUnconfirmedTxQueryArgs(where: GraphQLConfirmedTransactionWhere)
372
367
  ].flat()
373
368
  );
374
369
 
375
- return {
376
- addresses: addresses.length ? addresses : undefined,
377
- transactionIds: merge(where.transactionIds, where.transactionId),
370
+ return chunk(addresses, MAX_ARGS).map((chunk) => ({
371
+ addresses: chunk.length ? chunk : undefined,
372
+ transactionIds: where.transactionId ? [where.transactionId] : undefined,
378
373
  skip: 0,
379
374
  take: PAGE_SIZE
380
- };
375
+ }));
381
376
  }
382
377
 
383
- function buildGqlConfirmedTxQueryArgs(where: GraphQLConfirmedTransactionWhere) {
384
- return {
385
- ...buildGqlUnconfirmedTxQueryArgs(where),
378
+ function buildGqlConfirmedTxQueries(where: GraphQLConfirmedTransactionWhere) {
379
+ return buildGqlUnconfirmedTxQueries(where).map((query) => ({
380
+ ...query,
386
381
  headerId: where.headerId,
387
382
  minHeight: where.minHeight,
388
383
  onlyRelevantOutputs: where.onlyRelevantOutputs
389
- };
384
+ }));
390
385
  }
391
386
 
392
387
  function merge<T>(array?: T[], el?: T) {
@@ -481,3 +476,7 @@ function mapConfirmedTransaction<T>(
481
476
  confirmed: true
482
477
  };
483
478
  }
479
+
480
+ export function isRequestParam(obj: unknown): obj is ErgoGraphQLRequestOptions {
481
+ return typeof obj === "object" && (obj as ErgoGraphQLRequestOptions).url !== undefined;
482
+ }
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;