@qubic.ts/sdk 0.1.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/package.json +38 -0
- package/src/assets.test.ts +114 -0
- package/src/assets.ts +175 -0
- package/src/bob/client.test.ts +109 -0
- package/src/bob/client.ts +393 -0
- package/src/bob/log-stream.test.ts +55 -0
- package/src/bob/log-stream.ts +241 -0
- package/src/browser.ts +43 -0
- package/src/contracts.test.ts +90 -0
- package/src/contracts.ts +140 -0
- package/src/errors.ts +15 -0
- package/src/http.ts +1 -0
- package/src/index.ts +141 -0
- package/src/node.ts +1 -0
- package/src/retry.ts +61 -0
- package/src/rpc/client.test.ts +322 -0
- package/src/rpc/client.ts +688 -0
- package/src/sdk.test.ts +34 -0
- package/src/sdk.ts +113 -0
- package/src/tick.test.ts +69 -0
- package/src/tick.ts +47 -0
- package/src/transactions.queue.test.ts +102 -0
- package/src/transactions.test.ts +149 -0
- package/src/transactions.ts +234 -0
- package/src/transfers.test.ts +59 -0
- package/src/transfers.ts +132 -0
- package/src/tx/confirm.test.ts +149 -0
- package/src/tx/confirm.ts +147 -0
- package/src/tx/tx-queue.test.ts +146 -0
- package/src/tx/tx-queue.ts +214 -0
- package/src/tx/tx.ts +36 -0
- package/src/vault/types.ts +131 -0
- package/src/vault-browser.test.ts +77 -0
- package/src/vault-browser.ts +449 -0
- package/src/vault-cli.test.ts +63 -0
- package/src/vault-cli.ts +123 -0
- package/src/vault.test.ts +97 -0
- package/src/vault.ts +439 -0
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
import { SdkError } from "../errors.js";
|
|
2
|
+
import type { FetchLike } from "../http.js";
|
|
3
|
+
import { normalizeRetryConfig, type RetryConfig, withRetry } from "../retry.js";
|
|
4
|
+
|
|
5
|
+
export type RpcClientConfig = Readonly<{
|
|
6
|
+
/**
|
|
7
|
+
* RPC base URL (recommended: `https://rpc.qubic.org`).
|
|
8
|
+
* You may also pass a baseUrl that already includes `/live/v1` or `/query/v1`;
|
|
9
|
+
* those suffixes are stripped automatically.
|
|
10
|
+
*/
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
fetch?: FetchLike;
|
|
13
|
+
headers?: Readonly<Record<string, string>>;
|
|
14
|
+
retry?: RetryConfig;
|
|
15
|
+
onRequest?: (info: Readonly<{ url: string; method: string; body?: unknown }>) => void;
|
|
16
|
+
onResponse?: (
|
|
17
|
+
info: Readonly<{
|
|
18
|
+
url: string;
|
|
19
|
+
method: string;
|
|
20
|
+
status: number;
|
|
21
|
+
ok: boolean;
|
|
22
|
+
durationMs: number;
|
|
23
|
+
}>,
|
|
24
|
+
) => void;
|
|
25
|
+
onError?: (error: RpcError) => void;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export class RpcError extends SdkError {
|
|
29
|
+
override name = "RpcError";
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
code: string,
|
|
33
|
+
message: string,
|
|
34
|
+
readonly details: Readonly<{
|
|
35
|
+
url: string;
|
|
36
|
+
method: string;
|
|
37
|
+
status?: number;
|
|
38
|
+
statusText?: string;
|
|
39
|
+
bodyText?: string;
|
|
40
|
+
}>,
|
|
41
|
+
cause?: unknown,
|
|
42
|
+
) {
|
|
43
|
+
super(code, message, details, cause);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type TickInfo = Readonly<{
|
|
48
|
+
tick: bigint;
|
|
49
|
+
duration: bigint;
|
|
50
|
+
epoch: bigint;
|
|
51
|
+
initialTick: bigint;
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
export type LiveBalance = Readonly<{
|
|
55
|
+
id: string;
|
|
56
|
+
balance: bigint;
|
|
57
|
+
validForTick: bigint;
|
|
58
|
+
latestIncomingTransferTick: bigint;
|
|
59
|
+
latestOutgoingTransferTick: bigint;
|
|
60
|
+
incomingAmount: bigint;
|
|
61
|
+
outgoingAmount: bigint;
|
|
62
|
+
numberOfIncomingTransfers: bigint;
|
|
63
|
+
numberOfOutgoingTransfers: bigint;
|
|
64
|
+
}>;
|
|
65
|
+
|
|
66
|
+
export type BroadcastTransactionResult = Readonly<{
|
|
67
|
+
peersBroadcasted: number;
|
|
68
|
+
encodedTransaction: string;
|
|
69
|
+
transactionId: string;
|
|
70
|
+
}>;
|
|
71
|
+
|
|
72
|
+
export type LastProcessedTick = Readonly<{
|
|
73
|
+
tickNumber: bigint;
|
|
74
|
+
epoch: bigint;
|
|
75
|
+
intervalInitialTick: bigint;
|
|
76
|
+
}>;
|
|
77
|
+
|
|
78
|
+
export type ProcessedTickInterval = Readonly<{
|
|
79
|
+
epoch: bigint;
|
|
80
|
+
firstTick: bigint;
|
|
81
|
+
lastTick: bigint;
|
|
82
|
+
}>;
|
|
83
|
+
|
|
84
|
+
export type ComputorList = Readonly<{
|
|
85
|
+
epoch: bigint;
|
|
86
|
+
tickNumber: bigint;
|
|
87
|
+
identities: readonly string[];
|
|
88
|
+
signature: string;
|
|
89
|
+
}>;
|
|
90
|
+
|
|
91
|
+
export type QueryTransaction = Readonly<{
|
|
92
|
+
hash: string;
|
|
93
|
+
amount: bigint;
|
|
94
|
+
source: string;
|
|
95
|
+
destination: string;
|
|
96
|
+
tickNumber: bigint;
|
|
97
|
+
timestamp: bigint;
|
|
98
|
+
inputType: bigint;
|
|
99
|
+
inputSize: bigint;
|
|
100
|
+
inputData: string;
|
|
101
|
+
signature: string;
|
|
102
|
+
moneyFlew?: boolean;
|
|
103
|
+
}>;
|
|
104
|
+
|
|
105
|
+
export type Hits = Readonly<{ total: bigint; from: bigint; size: bigint }>;
|
|
106
|
+
|
|
107
|
+
export type Range = Readonly<{
|
|
108
|
+
gt?: string;
|
|
109
|
+
gte?: string;
|
|
110
|
+
lt?: string;
|
|
111
|
+
lte?: string;
|
|
112
|
+
}>;
|
|
113
|
+
|
|
114
|
+
export type Pagination = Readonly<{ offset?: bigint; size?: bigint }>;
|
|
115
|
+
|
|
116
|
+
export type TransactionsForIdentityRequest = Readonly<{
|
|
117
|
+
identity: string;
|
|
118
|
+
filters?: Readonly<Record<string, string>>;
|
|
119
|
+
ranges?: Readonly<Record<string, Range>>;
|
|
120
|
+
pagination?: Pagination;
|
|
121
|
+
}>;
|
|
122
|
+
|
|
123
|
+
export type TransactionsForIdentityPagingInput = TransactionsForIdentityRequest &
|
|
124
|
+
Readonly<{
|
|
125
|
+
pageSize?: bigint | number;
|
|
126
|
+
limit?: bigint | number;
|
|
127
|
+
offset?: bigint | number;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
export type TransactionsForIdentityResponse = Readonly<{
|
|
131
|
+
validForTick: bigint;
|
|
132
|
+
hits: Hits;
|
|
133
|
+
transactions: readonly QueryTransaction[];
|
|
134
|
+
}>;
|
|
135
|
+
|
|
136
|
+
export type TickData = Readonly<{
|
|
137
|
+
tickNumber: bigint;
|
|
138
|
+
epoch: bigint;
|
|
139
|
+
computorIndex: bigint;
|
|
140
|
+
timestamp: bigint;
|
|
141
|
+
varStruct: string;
|
|
142
|
+
timeLock: string;
|
|
143
|
+
transactionHashes: readonly string[];
|
|
144
|
+
contractFees: readonly bigint[];
|
|
145
|
+
signature: string;
|
|
146
|
+
}>;
|
|
147
|
+
|
|
148
|
+
export type RpcClient = Readonly<{
|
|
149
|
+
live: Readonly<{
|
|
150
|
+
tickInfo(): Promise<TickInfo>;
|
|
151
|
+
balance(identity: string): Promise<LiveBalance>;
|
|
152
|
+
broadcastTransaction(tx: Uint8Array | string): Promise<BroadcastTransactionResult>;
|
|
153
|
+
querySmartContract(input: {
|
|
154
|
+
contractIndex: bigint | number;
|
|
155
|
+
inputType: bigint | number;
|
|
156
|
+
input: Uint8Array | string;
|
|
157
|
+
}): Promise<{ responseBytes: Uint8Array; responseBase64: string }>;
|
|
158
|
+
}>;
|
|
159
|
+
query: Readonly<{
|
|
160
|
+
getLastProcessedTick(): Promise<LastProcessedTick>;
|
|
161
|
+
getTransactionByHash(hash: string): Promise<QueryTransaction>;
|
|
162
|
+
getTransactionsForIdentity(
|
|
163
|
+
input: TransactionsForIdentityRequest,
|
|
164
|
+
): Promise<TransactionsForIdentityResponse>;
|
|
165
|
+
getTransactionsForIdentityPages(
|
|
166
|
+
input: TransactionsForIdentityPagingInput,
|
|
167
|
+
): AsyncGenerator<TransactionsForIdentityResponse, void, void>;
|
|
168
|
+
getTransactionsForIdentityAll(
|
|
169
|
+
input: TransactionsForIdentityPagingInput,
|
|
170
|
+
): Promise<readonly QueryTransaction[]>;
|
|
171
|
+
getTransactionsForTick(tickNumber: bigint | number): Promise<readonly QueryTransaction[]>;
|
|
172
|
+
getTickData(tickNumber: bigint | number): Promise<TickData>;
|
|
173
|
+
getProcessedTickIntervals(): Promise<readonly ProcessedTickInterval[]>;
|
|
174
|
+
getComputorListsForEpoch(epoch: bigint | number): Promise<readonly ComputorList[]>;
|
|
175
|
+
}>;
|
|
176
|
+
}>;
|
|
177
|
+
|
|
178
|
+
export function createRpcClient(config: RpcClientConfig = {}): RpcClient {
|
|
179
|
+
const baseUrl = normalizeRpcBaseUrl(config.baseUrl ?? "https://rpc.qubic.org");
|
|
180
|
+
const base = new URL(ensureTrailingSlash(baseUrl));
|
|
181
|
+
const doFetch = config.fetch ?? fetch;
|
|
182
|
+
const retryConfig = normalizeRetryConfig(config.retry);
|
|
183
|
+
|
|
184
|
+
const requestJson = async (method: string, url: URL, body?: unknown): Promise<unknown> => {
|
|
185
|
+
return withRetry(
|
|
186
|
+
retryConfig,
|
|
187
|
+
method,
|
|
188
|
+
async () => {
|
|
189
|
+
const start = Date.now();
|
|
190
|
+
const headers: Record<string, string> = {
|
|
191
|
+
accept: "application/json",
|
|
192
|
+
...config.headers,
|
|
193
|
+
};
|
|
194
|
+
let bodyText: string | undefined;
|
|
195
|
+
if (body !== undefined) {
|
|
196
|
+
headers["content-type"] = "application/json";
|
|
197
|
+
bodyText = JSON.stringify(body);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
config.onRequest?.({ url: url.toString(), method, body });
|
|
202
|
+
const res = await doFetch(url, {
|
|
203
|
+
method,
|
|
204
|
+
headers,
|
|
205
|
+
body: bodyText,
|
|
206
|
+
});
|
|
207
|
+
config.onResponse?.({
|
|
208
|
+
url: url.toString(),
|
|
209
|
+
method,
|
|
210
|
+
status: res.status,
|
|
211
|
+
ok: res.ok,
|
|
212
|
+
durationMs: Date.now() - start,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const text = await res.text();
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
const error = new RpcError(
|
|
218
|
+
"rpc_request_failed",
|
|
219
|
+
`RPC request failed: ${res.status} ${res.statusText}`,
|
|
220
|
+
{
|
|
221
|
+
url: url.toString(),
|
|
222
|
+
method,
|
|
223
|
+
status: res.status,
|
|
224
|
+
statusText: res.statusText,
|
|
225
|
+
bodyText: text || undefined,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
config.onError?.(error);
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (text.length === 0) return null;
|
|
233
|
+
try {
|
|
234
|
+
return JSON.parse(text) as unknown;
|
|
235
|
+
} catch {
|
|
236
|
+
const error = new RpcError("rpc_invalid_json", "RPC response was not valid JSON", {
|
|
237
|
+
url: url.toString(),
|
|
238
|
+
method,
|
|
239
|
+
status: res.status,
|
|
240
|
+
statusText: res.statusText,
|
|
241
|
+
bodyText: text || undefined,
|
|
242
|
+
});
|
|
243
|
+
config.onError?.(error);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error instanceof RpcError) throw error;
|
|
248
|
+
const wrapped = new RpcError(
|
|
249
|
+
"rpc_fetch_error",
|
|
250
|
+
"RPC fetch failed",
|
|
251
|
+
{
|
|
252
|
+
url: url.toString(),
|
|
253
|
+
method,
|
|
254
|
+
},
|
|
255
|
+
error,
|
|
256
|
+
);
|
|
257
|
+
config.onError?.(wrapped);
|
|
258
|
+
throw wrapped;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
(error) => shouldRetryRpc(error, retryConfig),
|
|
262
|
+
);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const live = {
|
|
266
|
+
async tickInfo(): Promise<TickInfo> {
|
|
267
|
+
const url = new URL("live/v1/tick-info", base);
|
|
268
|
+
const json = await requestJson("GET", url);
|
|
269
|
+
const obj = expectObject(json);
|
|
270
|
+
const tickInfo = expectObject(obj.tickInfo);
|
|
271
|
+
return {
|
|
272
|
+
tick: parseJsonInteger(tickInfo.tick, "tickInfo.tick"),
|
|
273
|
+
duration: parseJsonInteger(tickInfo.duration, "tickInfo.duration"),
|
|
274
|
+
epoch: parseJsonInteger(tickInfo.epoch, "tickInfo.epoch"),
|
|
275
|
+
initialTick: parseJsonInteger(tickInfo.initialTick, "tickInfo.initialTick"),
|
|
276
|
+
};
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async balance(identity: string): Promise<LiveBalance> {
|
|
280
|
+
const url = new URL(`live/v1/balances/${encodeURIComponent(identity)}`, base);
|
|
281
|
+
const json = await requestJson("GET", url);
|
|
282
|
+
const obj = expectObject(json);
|
|
283
|
+
const bal = expectObject(obj.balance);
|
|
284
|
+
return {
|
|
285
|
+
id: expectString(bal.id, "balance.id"),
|
|
286
|
+
balance: parseJsonBigintString(bal.balance, "balance.balance"),
|
|
287
|
+
validForTick: parseJsonInteger(bal.validForTick, "balance.validForTick"),
|
|
288
|
+
latestIncomingTransferTick: parseJsonInteger(
|
|
289
|
+
bal.latestIncomingTransferTick,
|
|
290
|
+
"balance.latestIncomingTransferTick",
|
|
291
|
+
),
|
|
292
|
+
latestOutgoingTransferTick: parseJsonInteger(
|
|
293
|
+
bal.latestOutgoingTransferTick,
|
|
294
|
+
"balance.latestOutgoingTransferTick",
|
|
295
|
+
),
|
|
296
|
+
incomingAmount: parseJsonBigintString(bal.incomingAmount, "balance.incomingAmount"),
|
|
297
|
+
outgoingAmount: parseJsonBigintString(bal.outgoingAmount, "balance.outgoingAmount"),
|
|
298
|
+
numberOfIncomingTransfers: parseJsonInteger(
|
|
299
|
+
bal.numberOfIncomingTransfers,
|
|
300
|
+
"balance.numberOfIncomingTransfers",
|
|
301
|
+
),
|
|
302
|
+
numberOfOutgoingTransfers: parseJsonInteger(
|
|
303
|
+
bal.numberOfOutgoingTransfers,
|
|
304
|
+
"balance.numberOfOutgoingTransfers",
|
|
305
|
+
),
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
async broadcastTransaction(tx: Uint8Array | string): Promise<BroadcastTransactionResult> {
|
|
310
|
+
const encodedTransaction = typeof tx === "string" ? tx : encodeBase64(tx);
|
|
311
|
+
const url = new URL("live/v1/broadcast-transaction", base);
|
|
312
|
+
const json = await requestJson("POST", url, { encodedTransaction });
|
|
313
|
+
const obj = expectObject(json);
|
|
314
|
+
return {
|
|
315
|
+
peersBroadcasted: expectInt32(obj.peersBroadcasted, "peersBroadcasted"),
|
|
316
|
+
encodedTransaction: expectString(obj.encodedTransaction, "encodedTransaction"),
|
|
317
|
+
transactionId: expectString(obj.transactionId, "transactionId"),
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
async querySmartContract(input: {
|
|
322
|
+
contractIndex: bigint | number;
|
|
323
|
+
inputType: bigint | number;
|
|
324
|
+
input: Uint8Array | string;
|
|
325
|
+
}): Promise<{ responseBytes: Uint8Array; responseBase64: string }> {
|
|
326
|
+
const requestBytes =
|
|
327
|
+
typeof input.input === "string" ? decodeBase64(input.input) : input.input;
|
|
328
|
+
const requestData = encodeBase64(requestBytes);
|
|
329
|
+
const url = new URL("live/v1/querySmartContract", base);
|
|
330
|
+
const json = await requestJson("POST", url, {
|
|
331
|
+
contractIndex: toJsonInteger(input.contractIndex),
|
|
332
|
+
inputType: toJsonInteger(input.inputType),
|
|
333
|
+
inputSize: toJsonInteger(requestBytes.byteLength),
|
|
334
|
+
requestData,
|
|
335
|
+
});
|
|
336
|
+
const obj = expectObject(json);
|
|
337
|
+
const responseBase64 = expectString(obj.responseData, "responseData");
|
|
338
|
+
return { responseBytes: decodeBase64(responseBase64), responseBase64 };
|
|
339
|
+
},
|
|
340
|
+
} as const;
|
|
341
|
+
|
|
342
|
+
const query = {
|
|
343
|
+
async getLastProcessedTick(): Promise<LastProcessedTick> {
|
|
344
|
+
const url = new URL("query/v1/getLastProcessedTick", base);
|
|
345
|
+
const json = await requestJson("GET", url);
|
|
346
|
+
const obj = expectObject(json);
|
|
347
|
+
return {
|
|
348
|
+
tickNumber: parseJsonInteger(obj.tickNumber, "tickNumber"),
|
|
349
|
+
epoch: parseJsonInteger(obj.epoch, "epoch"),
|
|
350
|
+
intervalInitialTick: parseJsonInteger(obj.intervalInitialTick, "intervalInitialTick"),
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
async getTransactionByHash(hash: string): Promise<QueryTransaction> {
|
|
355
|
+
const url = new URL("query/v1/getTransactionByHash", base);
|
|
356
|
+
const json = await requestJson("POST", url, { hash });
|
|
357
|
+
return parseQueryTransaction(json, "transaction");
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async getTransactionsForIdentity(
|
|
361
|
+
input: TransactionsForIdentityRequest,
|
|
362
|
+
): Promise<TransactionsForIdentityResponse> {
|
|
363
|
+
const url = new URL("query/v1/getTransactionsForIdentity", base);
|
|
364
|
+
const json = await requestJson("POST", url, serializeIdentityTxQuery(input));
|
|
365
|
+
const obj = expectObject(json);
|
|
366
|
+
const hits = expectObject(obj.hits);
|
|
367
|
+
const txs = expectArray(obj.transactions, "transactions").map((t, i) =>
|
|
368
|
+
parseQueryTransaction(t, `transactions[${i}]`),
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
validForTick: parseJsonInteger(obj.validForTick, "validForTick"),
|
|
372
|
+
hits: {
|
|
373
|
+
total: parseJsonInteger(hits.total, "hits.total"),
|
|
374
|
+
from: parseJsonInteger(hits.from, "hits.from"),
|
|
375
|
+
size: parseJsonInteger(hits.size, "hits.size"),
|
|
376
|
+
},
|
|
377
|
+
transactions: txs,
|
|
378
|
+
};
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
async getTransactionsForTick(
|
|
382
|
+
tickNumber: bigint | number,
|
|
383
|
+
): Promise<readonly QueryTransaction[]> {
|
|
384
|
+
const url = new URL("query/v1/getTransactionsForTick", base);
|
|
385
|
+
const json = await requestJson("POST", url, { tickNumber: toJsonInteger(tickNumber) });
|
|
386
|
+
const obj = expectObject(json);
|
|
387
|
+
return expectArray(obj.transactions, "transactions").map((t, i) =>
|
|
388
|
+
parseQueryTransaction(t, `transactions[${i}]`),
|
|
389
|
+
);
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
async getTickData(tickNumber: bigint | number): Promise<TickData> {
|
|
393
|
+
const url = new URL("query/v1/getTickData", base);
|
|
394
|
+
const json = await requestJson("POST", url, { tickNumber: toJsonInteger(tickNumber) });
|
|
395
|
+
const obj = expectObject(json);
|
|
396
|
+
const tickData = expectObject(obj.tickData);
|
|
397
|
+
return {
|
|
398
|
+
tickNumber: parseJsonInteger(tickData.tickNumber, "tickData.tickNumber"),
|
|
399
|
+
epoch: parseJsonInteger(tickData.epoch, "tickData.epoch"),
|
|
400
|
+
computorIndex: parseJsonInteger(tickData.computorIndex, "tickData.computorIndex"),
|
|
401
|
+
timestamp: parseJsonBigintString(tickData.timestamp, "tickData.timestamp"),
|
|
402
|
+
varStruct: expectString(tickData.varStruct, "tickData.varStruct"),
|
|
403
|
+
timeLock: expectString(tickData.timeLock, "tickData.timeLock"),
|
|
404
|
+
transactionHashes: expectArray(
|
|
405
|
+
tickData.transactionHashes,
|
|
406
|
+
"tickData.transactionHashes",
|
|
407
|
+
).map((s, i) => expectString(s, `tickData.transactionHashes[${i}]`)),
|
|
408
|
+
contractFees: expectArray(tickData.contractFees, "tickData.contractFees").map((s, i) =>
|
|
409
|
+
parseJsonBigintString(s, `tickData.contractFees[${i}]`),
|
|
410
|
+
),
|
|
411
|
+
signature: expectString(tickData.signature, "tickData.signature"),
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async getProcessedTickIntervals(): Promise<readonly ProcessedTickInterval[]> {
|
|
416
|
+
const url = new URL("query/v1/getProcessedTickIntervals", base);
|
|
417
|
+
const json = await requestJson("GET", url);
|
|
418
|
+
const obj = expectObject(json);
|
|
419
|
+
const intervals = expectArray(obj.processedTickIntervals, "processedTickIntervals");
|
|
420
|
+
return intervals.map((i, idx) => {
|
|
421
|
+
const interval = expectObject(i, `processedTickIntervals[${idx}]`);
|
|
422
|
+
return {
|
|
423
|
+
epoch: parseJsonInteger(interval.epoch, `processedTickIntervals[${idx}].epoch`),
|
|
424
|
+
firstTick: parseJsonInteger(
|
|
425
|
+
interval.firstTick,
|
|
426
|
+
`processedTickIntervals[${idx}].firstTick`,
|
|
427
|
+
),
|
|
428
|
+
lastTick: parseJsonInteger(interval.lastTick, `processedTickIntervals[${idx}].lastTick`),
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
async getComputorListsForEpoch(epoch: bigint | number): Promise<readonly ComputorList[]> {
|
|
434
|
+
const url = new URL("query/v1/getComputorListsForEpoch", base);
|
|
435
|
+
const json = await requestJson("POST", url, { epoch: toJsonInteger(epoch) });
|
|
436
|
+
const obj = expectObject(json);
|
|
437
|
+
const lists = expectArray(obj.computorsLists, "computorsLists");
|
|
438
|
+
return lists.map((l, idx) => {
|
|
439
|
+
const list = expectObject(l, `computorsLists[${idx}]`);
|
|
440
|
+
return {
|
|
441
|
+
epoch: parseJsonInteger(list.epoch, `computorsLists[${idx}].epoch`),
|
|
442
|
+
tickNumber: parseJsonInteger(list.tickNumber, `computorsLists[${idx}].tickNumber`),
|
|
443
|
+
identities: expectArray(list.identities, `computorsLists[${idx}].identities`).map(
|
|
444
|
+
(s, j) => expectString(s, `computorsLists[${idx}].identities[${j}]`),
|
|
445
|
+
),
|
|
446
|
+
signature: expectString(list.signature, `computorsLists[${idx}].signature`),
|
|
447
|
+
};
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
async *getTransactionsForIdentityPages(
|
|
451
|
+
input: TransactionsForIdentityPagingInput,
|
|
452
|
+
): AsyncGenerator<TransactionsForIdentityResponse, void, void> {
|
|
453
|
+
let offset = toBigintPagination(input.offset ?? input.pagination?.offset ?? 0);
|
|
454
|
+
const pageSize = toBigintPagination(input.pageSize ?? input.pagination?.size ?? 100);
|
|
455
|
+
const limit = input.limit !== undefined ? toBigintPagination(input.limit) : undefined;
|
|
456
|
+
let consumed = 0n;
|
|
457
|
+
|
|
458
|
+
while (true) {
|
|
459
|
+
if (limit !== undefined && consumed >= limit) return;
|
|
460
|
+
const remaining = limit !== undefined ? limit - consumed : undefined;
|
|
461
|
+
const size = remaining !== undefined ? minBigint(pageSize, remaining) : pageSize;
|
|
462
|
+
const response = await query.getTransactionsForIdentity({
|
|
463
|
+
identity: input.identity,
|
|
464
|
+
filters: input.filters,
|
|
465
|
+
ranges: input.ranges,
|
|
466
|
+
pagination: { offset, size },
|
|
467
|
+
});
|
|
468
|
+
yield response;
|
|
469
|
+
const got = BigInt(response.transactions.length);
|
|
470
|
+
consumed += got;
|
|
471
|
+
if (got === 0n) return;
|
|
472
|
+
offset += got;
|
|
473
|
+
if (offset >= response.hits.total) return;
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
async getTransactionsForIdentityAll(
|
|
477
|
+
input: TransactionsForIdentityPagingInput,
|
|
478
|
+
): Promise<readonly QueryTransaction[]> {
|
|
479
|
+
const out: QueryTransaction[] = [];
|
|
480
|
+
for await (const page of query.getTransactionsForIdentityPages(input)) {
|
|
481
|
+
out.push(...page.transactions);
|
|
482
|
+
}
|
|
483
|
+
return out;
|
|
484
|
+
},
|
|
485
|
+
} as const;
|
|
486
|
+
|
|
487
|
+
return { live, query };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function shouldRetryRpc(error: unknown, config: ReturnType<typeof normalizeRetryConfig>): boolean {
|
|
491
|
+
if (!(error instanceof RpcError)) return false;
|
|
492
|
+
if (error.code === "rpc_fetch_error") return true;
|
|
493
|
+
const status = error.details.status;
|
|
494
|
+
return typeof status === "number" && config.retryOnStatuses.includes(status);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function toBigintPagination(value: bigint | number): bigint {
|
|
498
|
+
if (typeof value === "bigint") return value;
|
|
499
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
500
|
+
throw new TypeError("Pagination value must be an integer");
|
|
501
|
+
}
|
|
502
|
+
return BigInt(value);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function minBigint(a: bigint, b: bigint): bigint {
|
|
506
|
+
return a < b ? a : b;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function normalizeRpcBaseUrl(input: string): string {
|
|
510
|
+
const url = new URL(ensureTrailingSlash(input));
|
|
511
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
512
|
+
if (path.endsWith("/live/v1")) {
|
|
513
|
+
url.pathname = path.slice(0, -"/live/v1".length) || "/";
|
|
514
|
+
} else if (path.endsWith("/query/v1")) {
|
|
515
|
+
url.pathname = path.slice(0, -"/query/v1".length) || "/";
|
|
516
|
+
}
|
|
517
|
+
url.pathname = ensureTrailingSlash(url.pathname);
|
|
518
|
+
return url.toString();
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function ensureTrailingSlash(value: string): string {
|
|
522
|
+
return value.endsWith("/") ? value : `${value}/`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function encodeBase64(bytes: Uint8Array): string {
|
|
526
|
+
// Node/Bun
|
|
527
|
+
if (typeof Buffer !== "undefined") return Buffer.from(bytes).toString("base64");
|
|
528
|
+
|
|
529
|
+
let binary = "";
|
|
530
|
+
for (const b of bytes) binary += String.fromCharCode(b);
|
|
531
|
+
// Browser fallback
|
|
532
|
+
return btoa(binary);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function decodeBase64(base64: string): Uint8Array {
|
|
536
|
+
if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(base64, "base64"));
|
|
537
|
+
const binary = atob(base64);
|
|
538
|
+
const bytes = new Uint8Array(binary.length);
|
|
539
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
540
|
+
return bytes;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function expectObject(value: unknown, label = "value"): Record<string, unknown> {
|
|
544
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
545
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not an object`, {
|
|
546
|
+
url: "",
|
|
547
|
+
method: "",
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return value as Record<string, unknown>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function expectArray(value: unknown, label: string): unknown[] {
|
|
554
|
+
if (!Array.isArray(value)) {
|
|
555
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not an array`, {
|
|
556
|
+
url: "",
|
|
557
|
+
method: "",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return value;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function expectString(value: unknown, label: string): string {
|
|
564
|
+
if (typeof value !== "string") {
|
|
565
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not a string`, {
|
|
566
|
+
url: "",
|
|
567
|
+
method: "",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return value;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function expectInt32(value: unknown, label: string): number {
|
|
574
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
575
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not an int`, {
|
|
576
|
+
url: "",
|
|
577
|
+
method: "",
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (value < -2147483648 || value > 2147483647) {
|
|
581
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not int32`, {
|
|
582
|
+
url: "",
|
|
583
|
+
method: "",
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function parseJsonInteger(value: unknown, label: string): bigint {
|
|
590
|
+
if (typeof value === "number") {
|
|
591
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
592
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not an integer`, {
|
|
593
|
+
url: "",
|
|
594
|
+
method: "",
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return BigInt(value);
|
|
598
|
+
}
|
|
599
|
+
if (typeof value === "string") {
|
|
600
|
+
if (!/^-?\d+$/.test(value)) {
|
|
601
|
+
throw new RpcError(
|
|
602
|
+
"rpc_invalid_payload",
|
|
603
|
+
`Invalid RPC payload: ${label} is not an integer string`,
|
|
604
|
+
{
|
|
605
|
+
url: "",
|
|
606
|
+
method: "",
|
|
607
|
+
},
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
return BigInt(value);
|
|
611
|
+
}
|
|
612
|
+
throw new RpcError("rpc_invalid_payload", `Invalid RPC payload: ${label} is not an integer`, {
|
|
613
|
+
url: "",
|
|
614
|
+
method: "",
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function parseJsonBigintString(value: unknown, label: string): bigint {
|
|
619
|
+
const text = expectString(value, label);
|
|
620
|
+
if (!/^-?\d+$/.test(text)) {
|
|
621
|
+
throw new RpcError(
|
|
622
|
+
"rpc_invalid_payload",
|
|
623
|
+
`Invalid RPC payload: ${label} is not a decimal string`,
|
|
624
|
+
{
|
|
625
|
+
url: "",
|
|
626
|
+
method: "",
|
|
627
|
+
},
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return BigInt(text);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function toJsonInteger(value: bigint | number): number | string {
|
|
634
|
+
if (typeof value === "number") {
|
|
635
|
+
if (!Number.isFinite(value) || !Number.isInteger(value)) {
|
|
636
|
+
throw new TypeError("Expected integer number");
|
|
637
|
+
}
|
|
638
|
+
return value;
|
|
639
|
+
}
|
|
640
|
+
const asNumber = Number(value);
|
|
641
|
+
if (Number.isSafeInteger(asNumber)) return asNumber;
|
|
642
|
+
return value.toString(10);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function parseQueryTransaction(value: unknown, label: string): QueryTransaction {
|
|
646
|
+
const obj = expectObject(value, label);
|
|
647
|
+
const tx: QueryTransaction = {
|
|
648
|
+
hash: expectString(obj.hash, `${label}.hash`),
|
|
649
|
+
amount: parseJsonBigintString(obj.amount, `${label}.amount`),
|
|
650
|
+
source: expectString(obj.source, `${label}.source`),
|
|
651
|
+
destination: expectString(obj.destination, `${label}.destination`),
|
|
652
|
+
tickNumber: parseJsonInteger(obj.tickNumber, `${label}.tickNumber`),
|
|
653
|
+
timestamp: parseJsonBigintString(obj.timestamp, `${label}.timestamp`),
|
|
654
|
+
inputType: parseJsonInteger(obj.inputType, `${label}.inputType`),
|
|
655
|
+
inputSize: parseJsonInteger(obj.inputSize, `${label}.inputSize`),
|
|
656
|
+
inputData: expectString(obj.inputData, `${label}.inputData`),
|
|
657
|
+
signature: expectString(obj.signature, `${label}.signature`),
|
|
658
|
+
};
|
|
659
|
+
if (typeof obj.moneyFlew === "boolean") {
|
|
660
|
+
return { ...tx, moneyFlew: obj.moneyFlew };
|
|
661
|
+
}
|
|
662
|
+
if (obj.moneyFlew === undefined) return tx;
|
|
663
|
+
throw new RpcError(
|
|
664
|
+
"rpc_invalid_payload",
|
|
665
|
+
`Invalid RPC payload: ${label}.moneyFlew is not boolean`,
|
|
666
|
+
{
|
|
667
|
+
url: "",
|
|
668
|
+
method: "",
|
|
669
|
+
},
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function serializeIdentityTxQuery(input: TransactionsForIdentityRequest): Record<string, unknown> {
|
|
674
|
+
const body: Record<string, unknown> = { identity: input.identity };
|
|
675
|
+
if (input.filters) body.filters = input.filters;
|
|
676
|
+
if (input.ranges) body.ranges = input.ranges;
|
|
677
|
+
if (input.pagination) {
|
|
678
|
+
body.pagination = {
|
|
679
|
+
...(input.pagination.offset !== undefined
|
|
680
|
+
? { offset: toJsonInteger(input.pagination.offset) }
|
|
681
|
+
: {}),
|
|
682
|
+
...(input.pagination.size !== undefined
|
|
683
|
+
? { size: toJsonInteger(input.pagination.size) }
|
|
684
|
+
: {}),
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
return body;
|
|
688
|
+
}
|