@fleet-sdk/blockchain-providers 0.3.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 +15 -0
- package/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/index.cjs.js +215 -0
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.mts +281 -0
- package/dist/index.d.ts +281 -0
- package/dist/index.esm.js +213 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +47 -0
- package/src/ergo-graphql/ergoGraphQLProvider.ts +280 -0
- package/src/ergo-graphql/index.ts +1 -0
- package/src/ergo-graphql/queries.ts +12 -0
- package/src/index.ts +1 -0
- package/src/types/blockchainProvider.ts +103 -0
- package/src/types/index.ts +1 -0
- package/src/utils/_tests.ts +8 -0
- package/src/utils/graphql.test-d.ts +23 -0
- package/src/utils/graphql.ts +110 -0
- package/src/utils/index.ts +2 -0
@@ -0,0 +1,280 @@
|
|
1
|
+
import {
|
2
|
+
Box,
|
3
|
+
QueryBoxesArgs as BoxesArgs,
|
4
|
+
Header,
|
5
|
+
QueryBlockHeadersArgs as HeadersArgs
|
6
|
+
} from "@ergo-graphql/types";
|
7
|
+
import {
|
8
|
+
Base58String,
|
9
|
+
BlockHeader,
|
10
|
+
ensureDefaults,
|
11
|
+
HexString,
|
12
|
+
isEmpty,
|
13
|
+
isUndefined,
|
14
|
+
NotSupportedError,
|
15
|
+
orderBy,
|
16
|
+
SignedTransaction,
|
17
|
+
some,
|
18
|
+
uniq,
|
19
|
+
uniqBy
|
20
|
+
} from "@fleet-sdk/common";
|
21
|
+
import { ErgoAddress } from "@fleet-sdk/core";
|
22
|
+
import {
|
23
|
+
BoxQuery,
|
24
|
+
BoxWhere,
|
25
|
+
ChainProviderBox,
|
26
|
+
HeaderQuery,
|
27
|
+
IBlockchainProvider,
|
28
|
+
TransactionEvaluationResult,
|
29
|
+
TransactionReductionResult
|
30
|
+
} from "../types";
|
31
|
+
import {
|
32
|
+
createGqlOperation,
|
33
|
+
GraphQLOperation,
|
34
|
+
GraphQLRequestOptions,
|
35
|
+
GraphQLSuccessResponse,
|
36
|
+
GraphQLThrowableOptions,
|
37
|
+
GraphQLVariables,
|
38
|
+
isRequestParam
|
39
|
+
} from "../utils";
|
40
|
+
import {
|
41
|
+
ALL_BOXES_QUERY,
|
42
|
+
CHECK_TX_MUTATION,
|
43
|
+
CONF_BOXES_QUERY,
|
44
|
+
HEADERS_QUERY,
|
45
|
+
SEND_TX_MUTATION,
|
46
|
+
UNCONF_BOXES_QUERY
|
47
|
+
} from "./queries";
|
48
|
+
|
49
|
+
export type GraphQLBoxWhere = BoxWhere & {
|
50
|
+
/** Base16-encoded BoxIds */
|
51
|
+
boxIds?: HexString[];
|
52
|
+
|
53
|
+
/** Base16-encoded ErgoTrees */
|
54
|
+
ergoTrees?: HexString[];
|
55
|
+
|
56
|
+
/** Base58-encoded addresses or `ErgoAddress` objects */
|
57
|
+
addresses?: (Base58String | ErgoAddress)[];
|
58
|
+
};
|
59
|
+
|
60
|
+
export type GraphQLBoxQuery = BoxQuery<GraphQLBoxWhere>;
|
61
|
+
export type ErgoGraphQLRequestOptions = Omit<GraphQLRequestOptions, "throwOnNonNetworkError">;
|
62
|
+
|
63
|
+
type ConfBoxesResp = { boxes: Box[] };
|
64
|
+
type UnconfBoxesResp = { mempool: { boxes: Box[] } };
|
65
|
+
type AllBoxesResp = ConfBoxesResp & UnconfBoxesResp;
|
66
|
+
type HeadersResp = { blockHeaders: Header[] };
|
67
|
+
type CheckTxResp = { checkTransaction: string };
|
68
|
+
type SendTxResp = { submitTransaction: string };
|
69
|
+
type SignedTxArgsResp = { signedTransaction: SignedTransaction };
|
70
|
+
|
71
|
+
const PAGE_SIZE = 50;
|
72
|
+
|
73
|
+
export class ErgoGraphQLProvider implements IBlockchainProvider<BoxWhere> {
|
74
|
+
#options: GraphQLThrowableOptions;
|
75
|
+
|
76
|
+
#getConfBoxes;
|
77
|
+
#getUnconfBoxes;
|
78
|
+
#getAllBoxes;
|
79
|
+
#getHeaders;
|
80
|
+
#checkTx;
|
81
|
+
#sendTx;
|
82
|
+
|
83
|
+
constructor(url: string | URL);
|
84
|
+
constructor(url: ErgoGraphQLRequestOptions);
|
85
|
+
constructor(optOrUrl: ErgoGraphQLRequestOptions | string | URL) {
|
86
|
+
this.#options = {
|
87
|
+
...(isRequestParam(optOrUrl) ? optOrUrl : { url: optOrUrl }),
|
88
|
+
throwOnNonNetworkErrors: true
|
89
|
+
};
|
90
|
+
|
91
|
+
this.#getConfBoxes = this.createOperation<ConfBoxesResp, BoxesArgs>(CONF_BOXES_QUERY);
|
92
|
+
this.#getUnconfBoxes = this.createOperation<UnconfBoxesResp, BoxesArgs>(UNCONF_BOXES_QUERY);
|
93
|
+
this.#getAllBoxes = this.createOperation<AllBoxesResp, BoxesArgs>(ALL_BOXES_QUERY);
|
94
|
+
this.#getHeaders = this.createOperation<HeadersResp, HeadersArgs>(HEADERS_QUERY);
|
95
|
+
this.#checkTx = this.createOperation<CheckTxResp, SignedTxArgsResp>(CHECK_TX_MUTATION);
|
96
|
+
this.#sendTx = this.createOperation<SendTxResp, SignedTxArgsResp>(SEND_TX_MUTATION);
|
97
|
+
}
|
98
|
+
|
99
|
+
#fetchBoxes(args: BoxesArgs, inclConf: boolean, inclUnconf: boolean) {
|
100
|
+
if (inclConf && inclUnconf) {
|
101
|
+
return this.#getAllBoxes(args);
|
102
|
+
} else if (inclUnconf) {
|
103
|
+
return this.#getUnconfBoxes(args);
|
104
|
+
} else {
|
105
|
+
return this.#getConfBoxes(args);
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
async *streamBoxes(query: GraphQLBoxQuery): AsyncGenerator<ChainProviderBox[]> {
|
110
|
+
if (isEmpty(query.where)) {
|
111
|
+
throw new Error("Cannot fetch unspent boxes without a where clause.");
|
112
|
+
}
|
113
|
+
|
114
|
+
const notBeingSpent = (box: Box) => !box.beingSpent;
|
115
|
+
const returnedBoxIds = new Set<string>();
|
116
|
+
const { where, from } = query;
|
117
|
+
const args = buildGqlBoxQueryArgs(where);
|
118
|
+
|
119
|
+
let fetchFromChain = from !== "mempool";
|
120
|
+
let fetchFromMempool = from !== "blockchain";
|
121
|
+
const isMempoolAware = fetchFromMempool;
|
122
|
+
|
123
|
+
do {
|
124
|
+
const response = await this.#fetchBoxes(args, fetchFromChain, fetchFromMempool);
|
125
|
+
|
126
|
+
const { data } = response;
|
127
|
+
let boxes: ChainProviderBox[] = [];
|
128
|
+
|
129
|
+
if (fetchFromChain && hasConfirmed(data)) {
|
130
|
+
if (some(data.boxes)) {
|
131
|
+
const confirmedBoxes = (
|
132
|
+
isMempoolAware ? data.boxes.filter(notBeingSpent) : data.boxes
|
133
|
+
).map(asConfirmed(true));
|
134
|
+
|
135
|
+
boxes = boxes.concat(confirmedBoxes);
|
136
|
+
}
|
137
|
+
|
138
|
+
fetchFromChain = data.boxes.length === PAGE_SIZE;
|
139
|
+
}
|
140
|
+
|
141
|
+
if (isMempoolAware && hasMempool(data)) {
|
142
|
+
if (some(data.mempool.boxes)) {
|
143
|
+
const mempoolBoxes = data.mempool.boxes.filter(notBeingSpent).map(asConfirmed(false));
|
144
|
+
boxes = boxes.concat(mempoolBoxes);
|
145
|
+
}
|
146
|
+
|
147
|
+
fetchFromMempool = data.mempool.boxes.length === PAGE_SIZE;
|
148
|
+
}
|
149
|
+
|
150
|
+
if (some(boxes)) {
|
151
|
+
// boxes can be moved from the mempool to the blockchain while streaming,
|
152
|
+
// so we need to filter out boxes that have already been returned.
|
153
|
+
if (boxes.some((box) => returnedBoxIds.has(box.boxId))) {
|
154
|
+
boxes = boxes.filter((b) => !returnedBoxIds.has(b.boxId));
|
155
|
+
}
|
156
|
+
|
157
|
+
if (some(boxes)) {
|
158
|
+
boxes = uniqBy(boxes, (box) => box.boxId);
|
159
|
+
boxes.forEach((box) => returnedBoxIds.add(box.boxId));
|
160
|
+
|
161
|
+
yield boxes;
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
if (fetchFromChain || fetchFromMempool) args.skip += PAGE_SIZE;
|
166
|
+
} while (fetchFromChain || fetchFromMempool);
|
167
|
+
}
|
168
|
+
|
169
|
+
async getBoxes(query: GraphQLBoxQuery): Promise<ChainProviderBox[]> {
|
170
|
+
let boxes: ChainProviderBox[] = [];
|
171
|
+
for await (const chunk of this.streamBoxes(query)) {
|
172
|
+
boxes = boxes.concat(chunk);
|
173
|
+
}
|
174
|
+
|
175
|
+
return orderBy(boxes, (box) => box.creationHeight);
|
176
|
+
}
|
177
|
+
|
178
|
+
async getHeaders(query: HeaderQuery): Promise<BlockHeader[]> {
|
179
|
+
const response = await this.#getHeaders(query);
|
180
|
+
|
181
|
+
return (
|
182
|
+
response.data?.blockHeaders.map((header) => ({
|
183
|
+
...header,
|
184
|
+
id: header.headerId,
|
185
|
+
timestamp: Number(header.timestamp),
|
186
|
+
nBits: Number(header.nBits),
|
187
|
+
votes: header.votes.join("")
|
188
|
+
})) ?? []
|
189
|
+
);
|
190
|
+
}
|
191
|
+
|
192
|
+
createOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
193
|
+
query: string,
|
194
|
+
options?: Partial<ErgoGraphQLRequestOptions>
|
195
|
+
): GraphQLOperation<GraphQLSuccessResponse<R>, V> {
|
196
|
+
const opt = ensureDefaults(options, this.#options);
|
197
|
+
opt.throwOnNonNetworkErrors = true;
|
198
|
+
|
199
|
+
return createGqlOperation(query, opt);
|
200
|
+
}
|
201
|
+
|
202
|
+
async checkTransaction(
|
203
|
+
signedTransaction: SignedTransaction
|
204
|
+
): Promise<TransactionEvaluationResult> {
|
205
|
+
try {
|
206
|
+
const response = await this.#checkTx({ signedTransaction });
|
207
|
+
|
208
|
+
return { success: true, transactionId: response.data.checkTransaction };
|
209
|
+
} catch (e) {
|
210
|
+
return { success: false, message: (e as Error).message };
|
211
|
+
}
|
212
|
+
}
|
213
|
+
|
214
|
+
async submitTransaction(
|
215
|
+
signedTransaction: SignedTransaction
|
216
|
+
): Promise<TransactionEvaluationResult> {
|
217
|
+
try {
|
218
|
+
const response = await this.#sendTx({ signedTransaction });
|
219
|
+
|
220
|
+
return { success: true, transactionId: response.data.submitTransaction };
|
221
|
+
} catch (e) {
|
222
|
+
return { success: false, message: (e as Error).message };
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
reduceTransaction(): Promise<TransactionReductionResult> {
|
227
|
+
throw new NotSupportedError("Transaction reducing is not supported by ergo-graphql.");
|
228
|
+
}
|
229
|
+
}
|
230
|
+
|
231
|
+
function buildGqlBoxQueryArgs(where: GraphQLBoxWhere) {
|
232
|
+
const args = {
|
233
|
+
spent: false,
|
234
|
+
boxIds: merge(where.boxIds, where.boxId),
|
235
|
+
ergoTrees: merge(where.ergoTrees, where.ergoTree),
|
236
|
+
ergoTreeTemplateHash: where.templateHash,
|
237
|
+
tokenId: where.tokenId,
|
238
|
+
skip: 0,
|
239
|
+
take: PAGE_SIZE
|
240
|
+
} satisfies BoxesArgs;
|
241
|
+
|
242
|
+
const addresses = merge(where.addresses, where.address);
|
243
|
+
if (some(addresses)) {
|
244
|
+
const trees = addresses.map((address) =>
|
245
|
+
typeof address === "string" ? ErgoAddress.fromBase58(address).ergoTree : address.ergoTree
|
246
|
+
);
|
247
|
+
|
248
|
+
args.ergoTrees = uniq(some(args.ergoTrees) ? args.ergoTrees.concat(trees) : trees);
|
249
|
+
}
|
250
|
+
|
251
|
+
return args;
|
252
|
+
}
|
253
|
+
|
254
|
+
function merge<T>(array?: T[], el?: T) {
|
255
|
+
if (isEmpty(array) && isUndefined(el)) return;
|
256
|
+
|
257
|
+
const set = new Set<T>(array ?? []);
|
258
|
+
if (!isUndefined(el)) set.add(el);
|
259
|
+
return Array.from(set.values());
|
260
|
+
}
|
261
|
+
|
262
|
+
function hasMempool(data: AllBoxesResp | ConfBoxesResp | UnconfBoxesResp): data is UnconfBoxesResp {
|
263
|
+
return !!(data as UnconfBoxesResp)?.mempool?.boxes;
|
264
|
+
}
|
265
|
+
|
266
|
+
function hasConfirmed(data: AllBoxesResp | ConfBoxesResp | UnconfBoxesResp): data is ConfBoxesResp {
|
267
|
+
return !!(data as ConfBoxesResp)?.boxes;
|
268
|
+
}
|
269
|
+
|
270
|
+
function asConfirmed(confirmed: boolean) {
|
271
|
+
return (box: Box): ChainProviderBox => ({
|
272
|
+
...box,
|
273
|
+
value: BigInt(box.value),
|
274
|
+
assets: box.assets.map((asset) => ({
|
275
|
+
tokenId: asset.tokenId,
|
276
|
+
amount: BigInt(asset.amount)
|
277
|
+
})),
|
278
|
+
confirmed
|
279
|
+
});
|
280
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./ergoGraphQLProvider";
|
@@ -0,0 +1,12 @@
|
|
1
|
+
const B = [
|
2
|
+
`query boxes($spent: Boolean! $boxIds: [String!] $ergoTrees: [String!] $ergoTreeTemplateHash: String $tokenId: String $skip: Int $take: Int)`,
|
3
|
+
`boxIds: $boxIds ergoTrees: $ergoTrees ergoTreeTemplateHash: $ergoTreeTemplateHash tokenId: $tokenId skip: $skip take: $take`,
|
4
|
+
`boxId transactionId index value creationHeight ergoTree assets { tokenId amount } additionalRegisters beingSpent`
|
5
|
+
];
|
6
|
+
|
7
|
+
export const CONF_BOXES_QUERY = `${B[0]} { boxes(spent: $spent ${B[1]}) { ${B[2]} } }`;
|
8
|
+
export const UNCONF_BOXES_QUERY = `${B[0]} { mempool { boxes(${B[1]}) { ${B[2]} } } }`;
|
9
|
+
export const ALL_BOXES_QUERY = `${B[0]} { boxes(spent: $spent ${B[1]}) { ${B[2]} } mempool { boxes(${B[1]}) { ${B[2]} } } }`;
|
10
|
+
export const HEADERS_QUERY = `query blockHeaders($take: Int) { blockHeaders(take: $take) {headerId timestamp version adProofsRoot stateRoot transactionsRoot nBits extensionHash powSolutions height difficulty parentId votes } }`;
|
11
|
+
export const CHECK_TX_MUTATION = `mutation checkTransaction($signedTransaction: SignedTransaction!) { checkTransaction(signedTransaction: $signedTransaction) }`;
|
12
|
+
export const SEND_TX_MUTATION = `mutation submitTransaction($signedTransaction: SignedTransaction!) { submitTransaction(signedTransaction: $signedTransaction) }`;
|
package/src/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./ergo-graphql";
|
@@ -0,0 +1,103 @@
|
|
1
|
+
import {
|
2
|
+
Base58String,
|
3
|
+
BlockHeader,
|
4
|
+
Box,
|
5
|
+
BoxId,
|
6
|
+
HexString,
|
7
|
+
SignedTransaction,
|
8
|
+
TokenId,
|
9
|
+
TransactionId,
|
10
|
+
UnsignedTransaction
|
11
|
+
} from "@fleet-sdk/common";
|
12
|
+
import { ErgoAddress } from "@fleet-sdk/core";
|
13
|
+
import { RequireAtLeastOne } from "type-fest";
|
14
|
+
|
15
|
+
export type BoxSource = "blockchain" | "mempool" | "blockchain+mempool";
|
16
|
+
|
17
|
+
export type BoxQuery<W extends BoxWhere> = {
|
18
|
+
/** The query to filter boxes. */
|
19
|
+
where: RequireAtLeastOne<W>;
|
20
|
+
|
21
|
+
/**
|
22
|
+
* The source of boxes to query.
|
23
|
+
* @default "blockchain+mempool"
|
24
|
+
*/
|
25
|
+
from?: BoxSource;
|
26
|
+
};
|
27
|
+
|
28
|
+
export type HeaderQuery = { take: number };
|
29
|
+
|
30
|
+
export type BoxWhere = {
|
31
|
+
/** Base16-encoded BoxId */
|
32
|
+
boxId?: BoxId;
|
33
|
+
|
34
|
+
/** Base16-encoded ErgoTree */
|
35
|
+
ergoTree?: HexString;
|
36
|
+
|
37
|
+
/** Base58-encoded address */
|
38
|
+
address?: ErgoAddress | Base58String;
|
39
|
+
|
40
|
+
/** Base16-encoded contract template hash */
|
41
|
+
templateHash?: HexString;
|
42
|
+
|
43
|
+
/** Base16-encoded TokenId */
|
44
|
+
tokenId?: TokenId;
|
45
|
+
};
|
46
|
+
|
47
|
+
export type ChainProviderBox = Box<bigint> & {
|
48
|
+
confirmed: boolean;
|
49
|
+
};
|
50
|
+
|
51
|
+
export type TransactionEvaluationError = {
|
52
|
+
success: false;
|
53
|
+
message: string;
|
54
|
+
};
|
55
|
+
|
56
|
+
export type TransactionEvaluationSuccess = {
|
57
|
+
success: true;
|
58
|
+
transactionId: TransactionId;
|
59
|
+
};
|
60
|
+
|
61
|
+
export type TransactionReductionSuccess = {
|
62
|
+
success: true;
|
63
|
+
reducedTransaction: HexString;
|
64
|
+
};
|
65
|
+
|
66
|
+
export type TransactionEvaluationResult = TransactionEvaluationError | TransactionEvaluationSuccess;
|
67
|
+
export type TransactionReductionResult = TransactionEvaluationError | TransactionReductionSuccess;
|
68
|
+
|
69
|
+
/**
|
70
|
+
* Represents a blockchain provider that can interact with the blockchain.
|
71
|
+
* @template B The type of the box query used by the provider.
|
72
|
+
*/
|
73
|
+
export interface IBlockchainProvider<B extends BoxWhere> {
|
74
|
+
/**
|
75
|
+
* Get boxes.
|
76
|
+
*/
|
77
|
+
getBoxes(query: BoxQuery<B>): Promise<ChainProviderBox[]>;
|
78
|
+
|
79
|
+
/**
|
80
|
+
* Stream boxes.
|
81
|
+
*/
|
82
|
+
streamBoxes(query: BoxQuery<B>): AsyncIterable<ChainProviderBox[]>;
|
83
|
+
|
84
|
+
/**
|
85
|
+
* Get headers.
|
86
|
+
*/
|
87
|
+
getHeaders(query: HeaderQuery): Promise<BlockHeader[]>;
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Check for transaction validity without broadcasting it to the network.
|
91
|
+
*/
|
92
|
+
checkTransaction(transaction: SignedTransaction): Promise<TransactionEvaluationResult>;
|
93
|
+
|
94
|
+
/**
|
95
|
+
* Broadcast a transaction to the network.
|
96
|
+
*/
|
97
|
+
submitTransaction(transaction: SignedTransaction): Promise<TransactionEvaluationResult>;
|
98
|
+
|
99
|
+
/**
|
100
|
+
* Evaluate a transaction and return Base16-encoded evaluation result.
|
101
|
+
*/
|
102
|
+
reduceTransaction(transaction: UnsignedTransaction): Promise<TransactionReductionResult>;
|
103
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from "./blockchainProvider";
|
@@ -0,0 +1,8 @@
|
|
1
|
+
export const mockResponse = (data: string) => {
|
2
|
+
return { text: () => new Promise((resolve) => resolve(data)) } as unknown as Response;
|
3
|
+
};
|
4
|
+
|
5
|
+
export const mockChunkedResponse = (chunks: string[]) => {
|
6
|
+
let i = 0;
|
7
|
+
return { text: () => new Promise((resolve) => resolve(chunks[i++])) } as unknown as Response;
|
8
|
+
};
|
@@ -0,0 +1,23 @@
|
|
1
|
+
import { describe, expectTypeOf, it } from "vitest";
|
2
|
+
import {
|
3
|
+
createGqlOperation,
|
4
|
+
GraphQLOperation,
|
5
|
+
GraphQLResponse,
|
6
|
+
GraphQLSuccessResponse,
|
7
|
+
GraphQLVariables
|
8
|
+
} from "./graphql";
|
9
|
+
|
10
|
+
describe("createGqlOperation() types", () => {
|
11
|
+
const query = "query test { state { height } }";
|
12
|
+
const url = "https://gql.example.com/";
|
13
|
+
|
14
|
+
it("Should infer the correct type when throwOnNonNetworkErrors is set to true", () => {
|
15
|
+
const throwable = createGqlOperation(query, { throwOnNonNetworkErrors: true, url });
|
16
|
+
expectTypeOf(throwable).toMatchTypeOf<
|
17
|
+
GraphQLOperation<GraphQLSuccessResponse, GraphQLVariables>
|
18
|
+
>();
|
19
|
+
|
20
|
+
const notThrowable = createGqlOperation(query, { throwOnNonNetworkErrors: false, url });
|
21
|
+
expectTypeOf(notThrowable).toMatchTypeOf<GraphQLOperation<GraphQLResponse, GraphQLVariables>>();
|
22
|
+
});
|
23
|
+
});
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import {
|
2
|
+
BlockchainProviderError,
|
3
|
+
clearUndefined,
|
4
|
+
ensureDefaults,
|
5
|
+
isEmpty,
|
6
|
+
some
|
7
|
+
} from "@fleet-sdk/common";
|
8
|
+
|
9
|
+
const OP_NAME_REGEX = /(query|mutation)\s?([\w\-_]+)?/;
|
10
|
+
export const DEFAULT_HEADERS: Headers = {
|
11
|
+
"content-type": "application/json; charset=utf-8",
|
12
|
+
accept: "application/graphql-response+json, application/json"
|
13
|
+
};
|
14
|
+
|
15
|
+
type Credentials = RequestCredentials;
|
16
|
+
type Headers = HeadersInit;
|
17
|
+
type Fetcher = typeof fetch;
|
18
|
+
|
19
|
+
export type GraphQLVariables = Record<string, unknown> | null;
|
20
|
+
|
21
|
+
export interface GraphQLError {
|
22
|
+
message: string;
|
23
|
+
}
|
24
|
+
|
25
|
+
export interface GraphQLSuccessResponse<T = unknown> {
|
26
|
+
data: T;
|
27
|
+
errors: null;
|
28
|
+
}
|
29
|
+
|
30
|
+
export interface GraphQLErrorResponse {
|
31
|
+
data: null;
|
32
|
+
errors: GraphQLError[];
|
33
|
+
}
|
34
|
+
|
35
|
+
export type GraphQLResponse<T = unknown> = GraphQLSuccessResponse<T> | GraphQLErrorResponse;
|
36
|
+
|
37
|
+
export type GraphQLOperation<R extends GraphQLResponse, V extends GraphQLVariables> = (
|
38
|
+
variables?: V
|
39
|
+
) => Promise<R>;
|
40
|
+
|
41
|
+
export interface ResponseParser {
|
42
|
+
parse<T>(text: string): T;
|
43
|
+
stringify<T>(value: T): string;
|
44
|
+
}
|
45
|
+
|
46
|
+
export interface RequestParams {
|
47
|
+
operationName?: string | null;
|
48
|
+
query: string;
|
49
|
+
variables?: Record<string, unknown> | null;
|
50
|
+
}
|
51
|
+
|
52
|
+
export interface GraphQLRequestOptions {
|
53
|
+
url: URL | string;
|
54
|
+
headers?: Headers;
|
55
|
+
parser?: ResponseParser;
|
56
|
+
fetcher?: Fetcher;
|
57
|
+
credentials?: Credentials;
|
58
|
+
throwOnNonNetworkErrors?: boolean;
|
59
|
+
}
|
60
|
+
|
61
|
+
export interface GraphQLThrowableOptions extends GraphQLRequestOptions {
|
62
|
+
throwOnNonNetworkErrors: true;
|
63
|
+
}
|
64
|
+
|
65
|
+
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
66
|
+
query: string,
|
67
|
+
options: GraphQLThrowableOptions
|
68
|
+
): GraphQLOperation<GraphQLSuccessResponse<R>, V>;
|
69
|
+
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
70
|
+
query: string,
|
71
|
+
options: GraphQLRequestOptions
|
72
|
+
): GraphQLOperation<GraphQLResponse<R>, V>;
|
73
|
+
export function createGqlOperation<R, V extends GraphQLVariables = GraphQLVariables>(
|
74
|
+
query: string,
|
75
|
+
options: GraphQLRequestOptions
|
76
|
+
): GraphQLOperation<GraphQLResponse<R>, V> {
|
77
|
+
return async (variables?: V): Promise<GraphQLResponse<R>> => {
|
78
|
+
const response = await (options.fetcher ?? fetch)(options.url, {
|
79
|
+
method: "POST",
|
80
|
+
headers: ensureDefaults(options.headers, DEFAULT_HEADERS),
|
81
|
+
credentials: options.credentials,
|
82
|
+
body: (options.parser ?? JSON).stringify({
|
83
|
+
operationName: getOpName(query),
|
84
|
+
query,
|
85
|
+
variables: variables ? clearUndefined(variables) : undefined
|
86
|
+
} as RequestParams)
|
87
|
+
});
|
88
|
+
|
89
|
+
const rawData = await response.text();
|
90
|
+
const parsedData = (options.parser ?? JSON).parse(rawData) as GraphQLResponse<R>;
|
91
|
+
|
92
|
+
if (options.throwOnNonNetworkErrors && some(parsedData.errors) && isEmpty(parsedData.data)) {
|
93
|
+
throw new BlockchainProviderError(parsedData.errors[0].message, { cause: parsedData.errors });
|
94
|
+
}
|
95
|
+
|
96
|
+
return parsedData;
|
97
|
+
};
|
98
|
+
}
|
99
|
+
|
100
|
+
export function gql(query: TemplateStringsArray): string {
|
101
|
+
return query[0];
|
102
|
+
}
|
103
|
+
|
104
|
+
export function getOpName(query: string): string | undefined {
|
105
|
+
return OP_NAME_REGEX.exec(query)?.at(2);
|
106
|
+
}
|
107
|
+
|
108
|
+
export function isRequestParam(obj: unknown): obj is GraphQLRequestOptions {
|
109
|
+
return typeof obj === "object" && (obj as GraphQLRequestOptions).url !== undefined;
|
110
|
+
}
|