@fleet-sdk/blockchain-providers 0.4.1 → 0.6.0
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 +19 -0
- package/dist/index.d.mts +124 -33
- package/dist/index.d.ts +124 -33
- package/dist/index.js +254 -75
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +254 -75
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/ergo-graphql/ergoGraphQLProvider.ts +296 -93
- package/src/ergo-graphql/queries.ts +21 -9
- package/src/index.ts +1 -1
- package/src/types/blockchainProvider.ts +111 -18
- package/src/utils/_tests.ts +7 -7
- package/src/utils/graphql.test-d.ts +15 -7
- package/src/utils/graphql.ts +54 -40
- package/src/utils/networking.ts +84 -0
- package/src/ergo-graphql/index.ts +0 -1
- package/src/types/index.ts +0 -1
@@ -1,19 +1,38 @@
|
|
1
|
-
import {
|
1
|
+
import type {
|
2
2
|
Base58String,
|
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,
|
10
12
|
UnsignedTransaction
|
11
13
|
} from "@fleet-sdk/common";
|
12
|
-
import { ErgoAddress } from "@fleet-sdk/core";
|
13
|
-
import { RequireAtLeastOne } from "type-fest";
|
14
|
+
import type { ErgoAddress } from "@fleet-sdk/core";
|
15
|
+
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
|
47
|
+
export type UnconfirmedTransactionWhere = {
|
48
|
+
/** Base16-encoded TransactionId */
|
49
|
+
transactionId?: TransactionId;
|
29
50
|
|
30
|
-
|
31
|
-
|
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
|
41
|
-
|
68
|
+
/** Base16-encoded ErgoTree */
|
69
|
+
ergoTree?: HexString;
|
42
70
|
|
43
|
-
/**
|
44
|
-
|
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
|
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;
|
@@ -63,23 +122,55 @@ export type TransactionReductionSuccess = {
|
|
63
122
|
reducedTransaction: HexString;
|
64
123
|
};
|
65
124
|
|
66
|
-
export type TransactionEvaluationResult =
|
67
|
-
|
125
|
+
export type TransactionEvaluationResult =
|
126
|
+
| TransactionEvaluationError
|
127
|
+
| TransactionEvaluationSuccess;
|
128
|
+
export type TransactionReductionResult =
|
129
|
+
| TransactionEvaluationError
|
130
|
+
| TransactionReductionSuccess;
|
68
131
|
|
69
132
|
/**
|
70
133
|
* Represents a blockchain provider that can interact with the blockchain.
|
71
134
|
* @template B The type of the box query used by the provider.
|
72
135
|
*/
|
73
|
-
export interface IBlockchainProvider<
|
136
|
+
export interface IBlockchainProvider<I> {
|
74
137
|
/**
|
75
138
|
* Get boxes.
|
76
139
|
*/
|
77
|
-
getBoxes(query: BoxQuery<
|
140
|
+
getBoxes(query: BoxQuery<BoxWhere>): Promise<ChainProviderBox<I>[]>;
|
78
141
|
|
79
142
|
/**
|
80
143
|
* Stream boxes.
|
81
144
|
*/
|
82
|
-
streamBoxes(query: BoxQuery<
|
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>[]>;
|
83
174
|
|
84
175
|
/**
|
85
176
|
* Get headers.
|
@@ -99,5 +190,7 @@ export interface IBlockchainProvider<B extends BoxWhere> {
|
|
99
190
|
/**
|
100
191
|
* Evaluate a transaction and return Base16-encoded evaluation result.
|
101
192
|
*/
|
102
|
-
reduceTransaction(
|
193
|
+
reduceTransaction(
|
194
|
+
transaction: UnsignedTransaction
|
195
|
+
): Promise<TransactionReductionResult>;
|
103
196
|
}
|
package/src/utils/_tests.ts
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
export const
|
2
|
-
return { text: () => new Promise((resolve) => resolve(data)) } as unknown as Response;
|
3
|
-
};
|
1
|
+
// export const mockSuccessResponse = (data: unknown) => resolveString(JSON.stringify(data));
|
4
2
|
|
5
|
-
export const
|
6
|
-
|
7
|
-
|
8
|
-
};
|
3
|
+
export const resolveString = (data: string) =>
|
4
|
+
({
|
5
|
+
text: () => new Promise((resolve) => resolve(data))
|
6
|
+
}) as unknown as Response;
|
7
|
+
|
8
|
+
export const resolveData = (data: unknown) => resolveString(JSON.stringify(data));
|
@@ -1,10 +1,10 @@
|
|
1
1
|
import { describe, expectTypeOf, it } from "vitest";
|
2
2
|
import {
|
3
3
|
createGqlOperation,
|
4
|
-
GraphQLOperation,
|
5
|
-
GraphQLResponse,
|
6
|
-
GraphQLSuccessResponse,
|
7
|
-
GraphQLVariables
|
4
|
+
type GraphQLOperation,
|
5
|
+
type GraphQLResponse,
|
6
|
+
type GraphQLSuccessResponse,
|
7
|
+
type GraphQLVariables
|
8
8
|
} from "./graphql";
|
9
9
|
|
10
10
|
describe("createGqlOperation() types", () => {
|
@@ -12,12 +12,20 @@ describe("createGqlOperation() types", () => {
|
|
12
12
|
const url = "https://gql.example.com/";
|
13
13
|
|
14
14
|
it("Should infer the correct type when throwOnNonNetworkErrors is set to true", () => {
|
15
|
-
const throwable = createGqlOperation(query, {
|
15
|
+
const throwable = createGqlOperation(query, {
|
16
|
+
throwOnNonNetworkErrors: true,
|
17
|
+
url
|
18
|
+
});
|
16
19
|
expectTypeOf(throwable).toMatchTypeOf<
|
17
20
|
GraphQLOperation<GraphQLSuccessResponse, GraphQLVariables>
|
18
21
|
>();
|
19
22
|
|
20
|
-
const notThrowable = createGqlOperation(query, {
|
21
|
-
|
23
|
+
const notThrowable = createGqlOperation(query, {
|
24
|
+
throwOnNonNetworkErrors: false,
|
25
|
+
url
|
26
|
+
});
|
27
|
+
expectTypeOf(notThrowable).toMatchTypeOf<
|
28
|
+
GraphQLOperation<GraphQLResponse, GraphQLVariables>
|
29
|
+
>();
|
22
30
|
});
|
23
31
|
});
|
package/src/utils/graphql.ts
CHANGED
@@ -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
|
12
|
+
export 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 {
|
@@ -32,68 +30,84 @@ export interface GraphQLErrorResponse {
|
|
32
30
|
errors: GraphQLError[];
|
33
31
|
}
|
34
32
|
|
35
|
-
export type GraphQLResponse<T = unknown> =
|
33
|
+
export type GraphQLResponse<T = unknown> =
|
34
|
+
| GraphQLSuccessResponse<T>
|
35
|
+
| GraphQLErrorResponse;
|
36
36
|
|
37
37
|
export type GraphQLOperation<R extends GraphQLResponse, V extends GraphQLVariables> = (
|
38
|
-
variables?: V
|
38
|
+
variables?: V,
|
39
|
+
url?: string
|
39
40
|
) => Promise<R>;
|
40
41
|
|
41
|
-
export
|
42
|
-
|
43
|
-
|
44
|
-
|
42
|
+
export type GraphQLRequiredUrlOperation<
|
43
|
+
R extends GraphQLResponse,
|
44
|
+
V extends GraphQLVariables
|
45
|
+
> = (variables: V | undefined, url: string) => Promise<R>;
|
45
46
|
|
46
|
-
|
47
|
+
interface RequestParams {
|
47
48
|
operationName?: string | null;
|
48
49
|
query: string;
|
49
50
|
variables?: Record<string, unknown> | null;
|
50
51
|
}
|
51
52
|
|
52
53
|
export interface GraphQLRequestOptions {
|
53
|
-
url
|
54
|
-
|
55
|
-
|
56
|
-
fetcher?: Fetcher;
|
57
|
-
credentials?: Credentials;
|
54
|
+
url?: string;
|
55
|
+
parser?: ParserLike;
|
56
|
+
retry?: FallbackRetryOptions;
|
58
57
|
throwOnNonNetworkErrors?: boolean;
|
59
|
-
|
60
|
-
|
61
|
-
export interface GraphQLThrowableOptions extends GraphQLRequestOptions {
|
62
|
-
throwOnNonNetworkErrors: true;
|
58
|
+
httpOptions?: Omit<RequestInit, "body" | "method">;
|
63
59
|
}
|
64
60
|
|
65
61
|
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
66
62
|
query: string,
|
67
|
-
options:
|
63
|
+
options: GraphQLRequestOptions & { throwOnNonNetworkErrors: true }
|
68
64
|
): GraphQLOperation<GraphQLSuccessResponse<R>, V>;
|
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>;
|
69
73
|
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
70
74
|
query: string,
|
71
75
|
options: GraphQLRequestOptions
|
72
76
|
): GraphQLOperation<GraphQLResponse<R>, V>;
|
73
77
|
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
74
78
|
query: string,
|
75
|
-
options
|
76
|
-
):
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
+
}
|
87
99
|
});
|
88
100
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
101
|
+
if (
|
102
|
+
options?.throwOnNonNetworkErrors &&
|
103
|
+
some(response.errors) &&
|
104
|
+
isEmpty(response.data)
|
105
|
+
) {
|
106
|
+
const msg = response.errors[0].message;
|
107
|
+
throw new BlockchainProviderError(msg, { cause: response.errors });
|
94
108
|
}
|
95
109
|
|
96
|
-
return
|
110
|
+
return response;
|
97
111
|
};
|
98
112
|
}
|
99
113
|
|
@@ -0,0 +1,84 @@
|
|
1
|
+
import { some } from "@fleet-sdk/common";
|
2
|
+
import { isEmpty } from "packages/common/src";
|
3
|
+
|
4
|
+
export interface ParserLike {
|
5
|
+
parse<T>(text: string): T;
|
6
|
+
stringify<T>(value: T): string;
|
7
|
+
}
|
8
|
+
|
9
|
+
export type Route = { base: string; path: string; query?: Record<string, unknown> };
|
10
|
+
export type URLLike = string | Route;
|
11
|
+
export type FallbackRetryOptions = { fallbacks?: URLLike[] } & RetryOptions;
|
12
|
+
|
13
|
+
export type FetchOptions = {
|
14
|
+
parser?: ParserLike;
|
15
|
+
base?: string;
|
16
|
+
query?: Record<string, unknown>;
|
17
|
+
retry?: FallbackRetryOptions;
|
18
|
+
httpOptions?: RequestInit;
|
19
|
+
};
|
20
|
+
|
21
|
+
export async function request<T>(path: string, opt?: Partial<FetchOptions>): Promise<T> {
|
22
|
+
const url = buildURL(path, opt?.query, opt?.base);
|
23
|
+
|
24
|
+
let response: Response;
|
25
|
+
if (opt?.retry) {
|
26
|
+
const routes = some(opt.retry.fallbacks) ? [url, ...opt.retry.fallbacks] : [url];
|
27
|
+
const attempts = opt.retry.attempts;
|
28
|
+
response = await exponentialRetry(
|
29
|
+
(r) => fetch(resolveUrl(routes, attempts - r), opt.httpOptions),
|
30
|
+
opt.retry
|
31
|
+
);
|
32
|
+
} else {
|
33
|
+
response = await fetch(url, opt?.httpOptions);
|
34
|
+
}
|
35
|
+
|
36
|
+
return (opt?.parser || JSON).parse(await response.text());
|
37
|
+
}
|
38
|
+
|
39
|
+
function resolveUrl(routes: URLLike[], attempt: number) {
|
40
|
+
const route = routes[attempt % routes.length];
|
41
|
+
return typeof route === "string"
|
42
|
+
? route
|
43
|
+
: buildURL(route.path, route.query, route.base).toString();
|
44
|
+
}
|
45
|
+
|
46
|
+
function buildURL(path: string, query?: Record<string, unknown>, base?: string) {
|
47
|
+
if (!base && !query) return path;
|
48
|
+
|
49
|
+
const url = new URL(path, base);
|
50
|
+
if (some(query)) {
|
51
|
+
for (const key in query) url.searchParams.append(key, String(query[key]));
|
52
|
+
}
|
53
|
+
|
54
|
+
return url.toString();
|
55
|
+
}
|
56
|
+
|
57
|
+
export type RetryOptions = {
|
58
|
+
attempts: number;
|
59
|
+
delay: number;
|
60
|
+
};
|
61
|
+
|
62
|
+
/**
|
63
|
+
* Retries an asynchronous operation a specified number of times with a delay
|
64
|
+
* growing exponentially between each attempt.
|
65
|
+
* @param operation - The asynchronous operation to retry.
|
66
|
+
* @param options - The retry options.
|
67
|
+
* @returns A promise that resolves to the result of the operation, or undefined
|
68
|
+
* if all attempts fail.
|
69
|
+
*/
|
70
|
+
export async function exponentialRetry<T>(
|
71
|
+
operation: (remainingAttempts: number) => Promise<T>,
|
72
|
+
{ attempts, delay }: RetryOptions
|
73
|
+
): Promise<T> {
|
74
|
+
try {
|
75
|
+
return await operation(attempts);
|
76
|
+
} catch (e) {
|
77
|
+
if (attempts > 0) {
|
78
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
79
|
+
return exponentialRetry(operation, { attempts: attempts - 1, delay: delay * 2 });
|
80
|
+
}
|
81
|
+
|
82
|
+
throw e;
|
83
|
+
}
|
84
|
+
}
|
@@ -1 +0,0 @@
|
|
1
|
-
export * from "./ergoGraphQLProvider";
|
package/src/types/index.ts
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
export * from "./blockchainProvider";
|