@fleet-sdk/blockchain-providers 0.5.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.
@@ -3,7 +3,9 @@ import type {
3
3
  BlockHeader,
4
4
  Box,
5
5
  BoxId,
6
+ DataInput,
6
7
  HexString,
8
+ ProverResult,
7
9
  SignedTransaction,
8
10
  TokenId,
9
11
  TransactionId,
@@ -14,6 +16,23 @@ import type { RequireAtLeastOne } from "type-fest";
14
16
 
15
17
  export type BoxSource = "blockchain" | "mempool" | "blockchain+mempool";
16
18
 
19
+ export type BoxWhere = {
20
+ /** Base16-encoded BoxId */
21
+ boxId?: BoxId;
22
+
23
+ /** Base16-encoded ErgoTree */
24
+ ergoTree?: HexString;
25
+
26
+ /** Base58-encoded address */
27
+ address?: ErgoAddress | Base58String;
28
+
29
+ /** Base16-encoded contract template hash */
30
+ templateHash?: HexString;
31
+
32
+ /** Base16-encoded TokenId */
33
+ tokenId?: TokenId;
34
+ };
35
+
17
36
  export type BoxQuery<W extends BoxWhere> = {
18
37
  /** The query to filter boxes. */
19
38
  where: RequireAtLeastOne<W>;
@@ -25,29 +44,69 @@ export type BoxQuery<W extends BoxWhere> = {
25
44
  from?: BoxSource;
26
45
  };
27
46
 
28
- export type HeaderQuery = { take: number };
47
+ export type UnconfirmedTransactionWhere = {
48
+ /** Base16-encoded TransactionId */
49
+ transactionId?: TransactionId;
29
50
 
30
- export type BoxWhere = {
31
- /** Base16-encoded BoxId */
32
- boxId?: BoxId;
51
+ /** Base58-encoded address */
52
+ address?: ErgoAddress | Base58String;
33
53
 
34
54
  /** Base16-encoded ErgoTree */
35
55
  ergoTree?: HexString;
56
+ };
57
+
58
+ export type ConfirmedTransactionWhere = {
59
+ /** Base16-encoded TransactionId */
60
+ transactionId?: TransactionId;
61
+
62
+ /** Base16-encoded HeaderID */
63
+ headerId?: HexString;
36
64
 
37
65
  /** Base58-encoded address */
38
66
  address?: ErgoAddress | Base58String;
39
67
 
40
- /** Base16-encoded contract template hash */
41
- templateHash?: HexString;
68
+ /** Base16-encoded ErgoTree */
69
+ ergoTree?: HexString;
42
70
 
43
- /** Base16-encoded TokenId */
44
- tokenId?: TokenId;
71
+ /** Min blockchain height */
72
+ minHeight?: number;
73
+
74
+ /** Only returns relevant outputs for the selected filter params */
75
+ onlyRelevantOutputs?: boolean;
76
+ };
77
+
78
+ export type TransactionQuery<W extends ConfirmedTransactionWhere> = {
79
+ /** The query to filter boxes. */
80
+ where: RequireAtLeastOne<W, keyof Omit<W, "minHeight" | "onlyRelevantOutputs">>;
45
81
  };
46
82
 
47
- export type ChainProviderBox = Box<bigint> & {
83
+ export type HeaderQuery = { take: number };
84
+
85
+ export type ChainProviderBox<T> = Omit<Box, "value" | "assets"> & {
86
+ value: T;
87
+ assets: { tokenId: TokenId; amount: T }[];
88
+ confirmed: boolean;
89
+ };
90
+
91
+ type TransactionOutput<T> = Omit<ChainProviderBox<T>, "confirmed">;
92
+ type TransactionInput<T> = TransactionOutput<T> & { spendingProof: ProverResult };
93
+
94
+ export type ChainProviderUnconfirmedTransaction<T> = {
95
+ transactionId: TransactionId;
96
+ inputs: TransactionInput<T>[];
97
+ dataInputs: DataInput[];
98
+ outputs: TransactionOutput<T>[];
48
99
  confirmed: boolean;
100
+ timestamp: number;
49
101
  };
50
102
 
103
+ export type ChainProviderConfirmedTransaction<T> =
104
+ ChainProviderUnconfirmedTransaction<T> & {
105
+ height: number;
106
+ index: number;
107
+ headerId: HexString;
108
+ };
109
+
51
110
  export type TransactionEvaluationError = {
52
111
  success: false;
53
112
  message: string;
@@ -74,16 +133,44 @@ export type TransactionReductionResult =
74
133
  * Represents a blockchain provider that can interact with the blockchain.
75
134
  * @template B The type of the box query used by the provider.
76
135
  */
77
- export interface IBlockchainProvider<B extends BoxWhere> {
136
+ export interface IBlockchainProvider<I> {
78
137
  /**
79
138
  * Get boxes.
80
139
  */
81
- getBoxes(query: BoxQuery<B>): Promise<ChainProviderBox[]>;
140
+ getBoxes(query: BoxQuery<BoxWhere>): Promise<ChainProviderBox<I>[]>;
82
141
 
83
142
  /**
84
143
  * Stream boxes.
85
144
  */
86
- streamBoxes(query: BoxQuery<B>): AsyncIterable<ChainProviderBox[]>;
145
+ streamBoxes(query: BoxQuery<BoxWhere>): AsyncIterable<ChainProviderBox<I>[]>;
146
+
147
+ /**
148
+ * Stream unconfirmed transactions
149
+ */
150
+ streamUnconfirmedTransactions(
151
+ query: TransactionQuery<UnconfirmedTransactionWhere>
152
+ ): AsyncIterable<ChainProviderUnconfirmedTransaction<I>[]>;
153
+
154
+ /**
155
+ * Get unconfirmed transactions
156
+ */
157
+ getUnconfirmedTransactions(
158
+ query: TransactionQuery<UnconfirmedTransactionWhere>
159
+ ): Promise<ChainProviderUnconfirmedTransaction<I>[]>;
160
+
161
+ /**
162
+ * Stream confirmed transactions
163
+ */
164
+ streamConfirmedTransactions(
165
+ query: TransactionQuery<ConfirmedTransactionWhere>
166
+ ): AsyncIterable<ChainProviderConfirmedTransaction<I>[]>;
167
+
168
+ /**
169
+ * Get confirmed transactions
170
+ */
171
+ getConfirmedTransactions(
172
+ query: TransactionQuery<ConfirmedTransactionWhere>
173
+ ): Promise<ChainProviderConfirmedTransaction<I>[]>;
87
174
 
88
175
  /**
89
176
  * Get headers.
@@ -93,16 +180,12 @@ export interface IBlockchainProvider<B extends BoxWhere> {
93
180
  /**
94
181
  * Check for transaction validity without broadcasting it to the network.
95
182
  */
96
- checkTransaction(
97
- transaction: SignedTransaction
98
- ): Promise<TransactionEvaluationResult>;
183
+ checkTransaction(transaction: SignedTransaction): Promise<TransactionEvaluationResult>;
99
184
 
100
185
  /**
101
186
  * Broadcast a transaction to the network.
102
187
  */
103
- submitTransaction(
104
- transaction: SignedTransaction
105
- ): Promise<TransactionEvaluationResult>;
188
+ submitTransaction(transaction: SignedTransaction): Promise<TransactionEvaluationResult>;
106
189
 
107
190
  /**
108
191
  * Evaluate a transaction and return Base16-encoded evaluation result.
@@ -1,12 +1,8 @@
1
- export const mockResponse = (data: string) => {
2
- return {
1
+ // export const mockSuccessResponse = (data: unknown) => resolveString(JSON.stringify(data));
2
+
3
+ export const resolveString = (data: string) =>
4
+ ({
3
5
  text: () => new Promise((resolve) => resolve(data))
4
- } as unknown as Response;
5
- };
6
+ }) as unknown as Response;
6
7
 
7
- export const mockChunkedResponse = (chunks: string[]) => {
8
- let i = 0;
9
- return {
10
- text: () => new Promise((resolve) => resolve(chunks[i++]))
11
- } as unknown as Response;
12
- };
8
+ export const resolveData = (data: unknown) => resolveString(JSON.stringify(data));
@@ -5,17 +5,15 @@ import {
5
5
  isEmpty,
6
6
  some
7
7
  } from "@fleet-sdk/common";
8
+ import type { FallbackRetryOptions, ParserLike } from "./networking";
9
+ import { request } from "./networking";
8
10
 
9
11
  const OP_NAME_REGEX = /(query|mutation)\s?([\w\-_]+)?/;
10
- export const DEFAULT_HEADERS: Headers = {
12
+ const DEFAULT_HEADERS = {
11
13
  "content-type": "application/json; charset=utf-8",
12
14
  accept: "application/graphql-response+json, application/json"
13
15
  };
14
16
 
15
- type Credentials = RequestCredentials;
16
- type Headers = HeadersInit;
17
- type Fetcher = typeof fetch;
18
-
19
17
  export type GraphQLVariables = Record<string, unknown> | null;
20
18
 
21
19
  export interface GraphQLError {
@@ -36,84 +34,80 @@ export type GraphQLResponse<T = unknown> =
36
34
  | GraphQLSuccessResponse<T>
37
35
  | GraphQLErrorResponse;
38
36
 
39
- export type GraphQLOperation<
37
+ export type GraphQLOperation<R extends GraphQLResponse, V extends GraphQLVariables> = (
38
+ variables?: V,
39
+ url?: string
40
+ ) => Promise<R>;
41
+
42
+ export type GraphQLRequiredUrlOperation<
40
43
  R extends GraphQLResponse,
41
44
  V extends GraphQLVariables
42
- > = (variables?: V) => Promise<R>;
43
-
44
- export interface ResponseParser {
45
- parse<T>(text: string): T;
46
- stringify<T>(value: T): string;
47
- }
45
+ > = (variables: V | undefined, url: string) => Promise<R>;
48
46
 
49
- export interface RequestParams {
47
+ interface RequestParams {
50
48
  operationName?: string | null;
51
49
  query: string;
52
50
  variables?: Record<string, unknown> | null;
53
51
  }
54
52
 
55
53
  export interface GraphQLRequestOptions {
56
- url: URL | string;
57
- headers?: Headers;
58
- parser?: ResponseParser;
59
- fetcher?: Fetcher;
60
- credentials?: Credentials;
54
+ url?: string;
55
+ parser?: ParserLike;
56
+ retry?: FallbackRetryOptions;
61
57
  throwOnNonNetworkErrors?: boolean;
58
+ httpOptions?: Omit<RequestInit, "body" | "method">;
62
59
  }
63
60
 
64
- export interface GraphQLThrowableOptions extends GraphQLRequestOptions {
65
- throwOnNonNetworkErrors: true;
66
- }
67
-
68
- export function createGqlOperation<
69
- R,
70
- V extends GraphQLVariables = GraphQLVariables
71
- >(
61
+ export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
72
62
  query: string,
73
- options: GraphQLThrowableOptions
63
+ options: GraphQLRequestOptions & { throwOnNonNetworkErrors: true }
74
64
  ): GraphQLOperation<GraphQLSuccessResponse<R>, V>;
75
- export function createGqlOperation<
76
- R,
77
- V extends GraphQLVariables = GraphQLVariables
78
- >(
65
+ export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
66
+ query: string,
67
+ options?: GraphQLRequestOptions & { url: undefined }
68
+ ): GraphQLRequiredUrlOperation<GraphQLResponse<R>, V>;
69
+ export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
70
+ query: string,
71
+ options: GraphQLRequestOptions & { url: undefined; throwOnNonNetworkErrors: true }
72
+ ): GraphQLRequiredUrlOperation<GraphQLSuccessResponse<R>, V>;
73
+ export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
79
74
  query: string,
80
75
  options: GraphQLRequestOptions
81
76
  ): GraphQLOperation<GraphQLResponse<R>, V>;
82
- export function createGqlOperation<
83
- R,
84
- V extends GraphQLVariables = GraphQLVariables
85
- >(
77
+ export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
86
78
  query: string,
87
- options: GraphQLRequestOptions
88
- ): GraphQLOperation<GraphQLResponse<R>, V> {
89
- return async (variables?: V): Promise<GraphQLResponse<R>> => {
90
- const response = await (options.fetcher ?? fetch)(options.url, {
91
- method: "POST",
92
- headers: ensureDefaults(options.headers, DEFAULT_HEADERS),
93
- credentials: options.credentials,
94
- body: (options.parser ?? JSON).stringify({
95
- operationName: getOpName(query),
96
- query,
97
- variables: variables ? clearUndefined(variables) : undefined
98
- } as RequestParams)
79
+ options?: GraphQLRequestOptions
80
+ ):
81
+ | GraphQLOperation<GraphQLResponse<R>, V>
82
+ | GraphQLRequiredUrlOperation<GraphQLResponse<R>, V> {
83
+ return async (variables?: V, url?: string): Promise<GraphQLResponse<R>> => {
84
+ url = url ?? options?.url;
85
+ if (!url) throw new Error("URL is required");
86
+
87
+ const response = await request<GraphQLResponse<R>>(url, {
88
+ ...options,
89
+ httpOptions: {
90
+ ...options?.httpOptions,
91
+ method: "POST",
92
+ headers: ensureDefaults(options?.httpOptions?.headers, DEFAULT_HEADERS),
93
+ body: (options?.parser ?? JSON).stringify({
94
+ operationName: getOpName(query),
95
+ query,
96
+ variables: variables ? clearUndefined(variables) : undefined
97
+ } as RequestParams)
98
+ }
99
99
  });
100
100
 
101
- const rawData = await response.text();
102
- const parsedData = (options.parser ?? JSON).parse(
103
- rawData
104
- ) as GraphQLResponse<R>;
105
-
106
101
  if (
107
- options.throwOnNonNetworkErrors &&
108
- some(parsedData.errors) &&
109
- isEmpty(parsedData.data)
102
+ options?.throwOnNonNetworkErrors &&
103
+ some(response.errors) &&
104
+ isEmpty(response.data)
110
105
  ) {
111
- throw new BlockchainProviderError(parsedData.errors[0].message, {
112
- cause: parsedData.errors
113
- });
106
+ const msg = response.errors[0].message;
107
+ throw new BlockchainProviderError(msg, { cause: response.errors });
114
108
  }
115
109
 
116
- return parsedData;
110
+ return response;
117
111
  };
118
112
  }
119
113
 
@@ -124,9 +118,3 @@ export function gql(query: TemplateStringsArray): string {
124
118
  export function getOpName(query: string): string | undefined {
125
119
  return OP_NAME_REGEX.exec(query)?.at(2);
126
120
  }
127
-
128
- export function isRequestParam(obj: unknown): obj is GraphQLRequestOptions {
129
- return (
130
- typeof obj === "object" && (obj as GraphQLRequestOptions).url !== undefined
131
- );
132
- }
@@ -0,0 +1,83 @@
1
+ import { some } from "@fleet-sdk/common";
2
+
3
+ export interface ParserLike {
4
+ parse<T>(text: string): T;
5
+ stringify<T>(value: T): string;
6
+ }
7
+
8
+ export type Route = { base: string; path: string; query?: Record<string, unknown> };
9
+ export type URLLike = string | Route;
10
+ export type FallbackRetryOptions = { fallbacks?: URLLike[] } & RetryOptions;
11
+
12
+ export type FetchOptions = {
13
+ parser?: ParserLike;
14
+ base?: string;
15
+ query?: Record<string, unknown>;
16
+ retry?: FallbackRetryOptions;
17
+ httpOptions?: RequestInit;
18
+ };
19
+
20
+ export async function request<T>(path: string, opt?: Partial<FetchOptions>): Promise<T> {
21
+ const url = buildURL(path, opt?.query, opt?.base);
22
+
23
+ let response: Response;
24
+ if (opt?.retry) {
25
+ const routes = some(opt.retry.fallbacks) ? [url, ...opt.retry.fallbacks] : [url];
26
+ const attempts = opt.retry.attempts;
27
+ response = await exponentialRetry(
28
+ (r) => fetch(resolveUrl(routes, attempts - r), opt.httpOptions),
29
+ opt.retry
30
+ );
31
+ } else {
32
+ response = await fetch(url, opt?.httpOptions);
33
+ }
34
+
35
+ return (opt?.parser || JSON).parse(await response.text());
36
+ }
37
+
38
+ function resolveUrl(routes: URLLike[], attempt: number) {
39
+ const route = routes[attempt % routes.length];
40
+ return typeof route === "string"
41
+ ? route
42
+ : buildURL(route.path, route.query, route.base).toString();
43
+ }
44
+
45
+ function buildURL(path: string, query?: Record<string, unknown>, base?: string) {
46
+ if (!base && !query) return path;
47
+
48
+ const url = new URL(path, base);
49
+ if (some(query)) {
50
+ for (const key in query) url.searchParams.append(key, String(query[key]));
51
+ }
52
+
53
+ return url.toString();
54
+ }
55
+
56
+ export type RetryOptions = {
57
+ attempts: number;
58
+ delay: number;
59
+ };
60
+
61
+ /**
62
+ * Retries an asynchronous operation a specified number of times with a delay
63
+ * growing exponentially between each attempt.
64
+ * @param operation - The asynchronous operation to retry.
65
+ * @param options - The retry options.
66
+ * @returns A promise that resolves to the result of the operation, or undefined
67
+ * if all attempts fail.
68
+ */
69
+ export async function exponentialRetry<T>(
70
+ operation: (remainingAttempts: number) => Promise<T>,
71
+ { attempts, delay }: RetryOptions
72
+ ): Promise<T> {
73
+ try {
74
+ return await operation(attempts);
75
+ } catch (e) {
76
+ if (attempts > 0) {
77
+ await new Promise((resolve) => setTimeout(resolve, delay));
78
+ return exponentialRetry(operation, { attempts: attempts - 1, delay: delay * 2 });
79
+ }
80
+
81
+ throw e;
82
+ }
83
+ }