koilib 5.5.2 → 5.5.4

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.
@@ -0,0 +1,512 @@
1
+ import fetch from "cross-fetch";
2
+ import {
3
+ BlockJson,
4
+ TransactionJson,
5
+ CallContractOperationJson,
6
+ TransactionReceipt,
7
+ TransactionJsonWait,
8
+ } from "./interface";
9
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
10
+ // @ts-ignore
11
+ import { koinos } from "./protoModules/protocol-proto.js";
12
+ import { decodeBase64url, encodeBase64url } from "./utils";
13
+
14
+ /* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access */
15
+
16
+ async function sleep(ms: number): Promise<void> {
17
+ return new Promise((r) => setTimeout(r, ms));
18
+ }
19
+
20
+ /**
21
+ * Class to connect with the RPC node
22
+ */
23
+ export class Provider {
24
+ /**
25
+ * Array of URLs of RPC nodes
26
+ */
27
+ public rpcNodes: string[];
28
+
29
+ /**
30
+ * Function triggered when a node is down. Returns a
31
+ * boolean determining if the call should be aborted.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const provider = new Provider([
36
+ * "http://45.56.104.152:8080",
37
+ * "http://159.203.119.0:8080"
38
+ * ]);
39
+ *
40
+ * provider.onError = (error, node, newNode) => {
41
+ * console.log(`Error from node ${node}: ${error.message}`);
42
+ * console.log(`changing node to ${newNode}`);
43
+ * const abort = false;
44
+ * return abort;
45
+ * }
46
+ * ```
47
+ */
48
+ public onError: (
49
+ error: Error,
50
+
51
+ /** node that threw the error */
52
+ currentNode: string,
53
+
54
+ /** node used for the next iteration */
55
+ newNode: string
56
+ ) => boolean;
57
+
58
+ /**
59
+ * Index of current node in rpcNodes
60
+ */
61
+ public currentNodeId: number;
62
+
63
+ /**
64
+ *
65
+ * @param rpcNodes - URL of the rpc node, or array of urls
66
+ * to switch between them when someone is down
67
+ * @example
68
+ * ```ts
69
+ * const provider = new Provider([
70
+ * "http://45.56.104.152:8080",
71
+ * "http://159.203.119.0:8080"
72
+ * ]);
73
+ * ```
74
+ */
75
+ constructor(rpcNodes: string | string[]) {
76
+ if (Array.isArray(rpcNodes)) this.rpcNodes = rpcNodes;
77
+ else this.rpcNodes = [rpcNodes];
78
+ this.currentNodeId = 0;
79
+ this.onError = () => true;
80
+ }
81
+
82
+ /**
83
+ * Function to make jsonrpc requests to the RPC node
84
+ * @param method - jsonrpc method
85
+ * @param params - jsonrpc params
86
+ * @returns Result of jsonrpc response
87
+ */
88
+ async call<T = unknown>(method: string, params: unknown): Promise<T> {
89
+ /* eslint-disable no-await-in-loop */
90
+ // eslint-disable-next-line no-constant-condition
91
+ while (true) {
92
+ try {
93
+ const body = {
94
+ id: Math.round(Math.random() * 1000),
95
+ jsonrpc: "2.0",
96
+ method,
97
+ params,
98
+ };
99
+
100
+ const url = this.rpcNodes[this.currentNodeId];
101
+
102
+ const response = await fetch(url, {
103
+ method: "POST",
104
+ body: JSON.stringify(body),
105
+ });
106
+ const json = (await response.json()) as {
107
+ result: T;
108
+ error?: {
109
+ message?: string;
110
+ data?: string;
111
+ };
112
+ };
113
+
114
+ if (json.result !== undefined) return json.result;
115
+
116
+ if (!json.error) throw new Error("undefined error");
117
+ const { message, data } = json.error;
118
+ if (!data) throw new Error(message);
119
+ let dataJson: Record<string, unknown>;
120
+ try {
121
+ dataJson = JSON.parse(data);
122
+ } catch (e) {
123
+ dataJson = { data };
124
+ }
125
+ throw new Error(
126
+ JSON.stringify({
127
+ ...(message && { error: message }),
128
+ ...dataJson,
129
+ })
130
+ );
131
+ } catch (e) {
132
+ const currentNode = this.rpcNodes[this.currentNodeId];
133
+ this.currentNodeId = (this.currentNodeId + 1) % this.rpcNodes.length;
134
+ const newNode = this.rpcNodes[this.currentNodeId];
135
+ const abort = this.onError(e as Error, currentNode, newNode);
136
+ if (abort) throw e;
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Function to call "chain.get_account_nonce" to return the number of
143
+ * transactions for a particular account. If you are creating a new
144
+ * transaction consider using [[Provider.getNextNonce]].
145
+ * @param account - account address
146
+ * @param deserialize - If set true it will deserialize the nonce
147
+ * and return it as number (default). If set false it will return
148
+ * the nonce encoded as received from the RPC.
149
+ * @returns Nonce
150
+ */
151
+ async getNonce(
152
+ account: string,
153
+ deserialize = true
154
+ ): Promise<number | string> {
155
+ const { nonce: nonceBase64url } = await this.call<{ nonce: string }>(
156
+ "chain.get_account_nonce",
157
+ { account }
158
+ );
159
+
160
+ if (!deserialize) {
161
+ return nonceBase64url;
162
+ }
163
+
164
+ const valueBuffer = decodeBase64url(nonceBase64url);
165
+ const message = koinos.chain.value_type.decode(valueBuffer);
166
+ const object = koinos.chain.value_type.toObject(message, {
167
+ longs: String,
168
+ defaults: true,
169
+ }) as { uint64_value: string };
170
+ // todo: consider the case where nonce is greater than max safe integer
171
+ return Number(object.uint64_value);
172
+ }
173
+
174
+ /**
175
+ * Function to call "chain.get_account_nonce" (number of
176
+ * transactions for a particular account) and return the next nonce.
177
+ * This call is used when creating new transactions. The result is
178
+ * encoded in base64url
179
+ * @param account - account address
180
+ * @returns Nonce
181
+ */
182
+ async getNextNonce(account: string): Promise<string> {
183
+ const oldNonce = (await this.getNonce(account)) as number;
184
+ const message = koinos.chain.value_type.create({
185
+ // todo: consider using bigint for big nonces
186
+ uint64_value: String(oldNonce + 1),
187
+ });
188
+ const nonceEncoded = koinos.chain.value_type
189
+ .encode(message)
190
+ .finish() as Uint8Array;
191
+
192
+ return encodeBase64url(nonceEncoded);
193
+ }
194
+
195
+ async getAccountRc(account: string): Promise<string> {
196
+ const { rc } = await this.call<{ rc: string }>("chain.get_account_rc", {
197
+ account,
198
+ });
199
+ if (!rc) return "0";
200
+ return rc;
201
+ }
202
+
203
+ /**
204
+ * Get transactions by id and their corresponding block ids
205
+ */
206
+ async getTransactionsById(transactionIds: string[]): Promise<{
207
+ transactions: {
208
+ transaction: TransactionJson;
209
+ containing_blocks: string[];
210
+ }[];
211
+ }> {
212
+ return this.call<{
213
+ transactions: {
214
+ transaction: TransactionJson;
215
+ containing_blocks: string[];
216
+ }[];
217
+ }>("transaction_store.get_transactions_by_id", {
218
+ transaction_ids: transactionIds,
219
+ });
220
+ }
221
+
222
+ async getBlocksById(blockIds: string[]): Promise<{
223
+ block_items: {
224
+ block_id: string;
225
+ block_height: string;
226
+ block: BlockJson;
227
+ }[];
228
+ }> {
229
+ return this.call("block_store.get_blocks_by_id", {
230
+ block_ids: blockIds,
231
+ return_block: true,
232
+ return_receipt: false,
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Function to get info from the head block in the blockchain
238
+ */
239
+ async getHeadInfo(): Promise<{
240
+ head_block_time: string;
241
+ head_topology: {
242
+ id: string;
243
+ height: string;
244
+ previous: string;
245
+ };
246
+ head_state_merkle_root: string;
247
+ last_irreversible_block: string;
248
+ }> {
249
+ return this.call<{
250
+ head_block_time: string;
251
+ head_topology: {
252
+ id: string;
253
+ height: string;
254
+ previous: string;
255
+ };
256
+ head_state_merkle_root: string;
257
+ last_irreversible_block: string;
258
+ }>("chain.get_head_info", {});
259
+ }
260
+
261
+ /**
262
+ * Function to get the chain
263
+ */
264
+ async getChainId(): Promise<string> {
265
+ const { chain_id: chainId } = await this.call<{ chain_id: string }>(
266
+ "chain.get_chain_id",
267
+ {}
268
+ );
269
+ return chainId;
270
+ }
271
+
272
+ /**
273
+ * Function to get consecutive blocks in descending order
274
+ * @param height - Starting block height
275
+ * @param numBlocks - Number of blocks to fetch
276
+ * @param idRef - Block ID reference to speed up searching blocks.
277
+ * This ID must be from a greater block height. By default it
278
+ * gets the ID from the block head.
279
+ */
280
+ async getBlocks(
281
+ height: number,
282
+ numBlocks = 1,
283
+ idRef?: string
284
+ ): Promise<
285
+ {
286
+ block_id: string;
287
+ block_height: string;
288
+ block: BlockJson;
289
+ block_receipt: {
290
+ [x: string]: unknown;
291
+ };
292
+ }[]
293
+ > {
294
+ let blockIdRef = idRef;
295
+ if (!blockIdRef) {
296
+ const head = await this.getHeadInfo();
297
+ blockIdRef = head.head_topology.id;
298
+ }
299
+ return (
300
+ await this.call<{
301
+ block_items: {
302
+ block_id: string;
303
+ block_height: string;
304
+ block: BlockJson;
305
+ block_receipt: {
306
+ [x: string]: unknown;
307
+ };
308
+ }[];
309
+ }>("block_store.get_blocks_by_height", {
310
+ head_block_id: blockIdRef,
311
+ ancestor_start_height: height,
312
+ num_blocks: numBlocks,
313
+ return_block: true,
314
+ return_receipt: false,
315
+ })
316
+ ).block_items;
317
+ }
318
+
319
+ /**
320
+ * Function to get a block by its height
321
+ */
322
+ async getBlock(height: number): Promise<{
323
+ block_id: string;
324
+ block_height: string;
325
+ block: BlockJson;
326
+ block_receipt: {
327
+ [x: string]: unknown;
328
+ };
329
+ }> {
330
+ return (await this.getBlocks(height, 1))[0];
331
+ }
332
+
333
+ /**
334
+ * Function to wait for a transaction to be mined.
335
+ * @param txId - transaction id
336
+ * @param type - Type must be "byBlock" (default) or "byTransactionId".
337
+ * _byBlock_ will query the blockchain to get blocks and search for the
338
+ * transaction there. _byTransactionId_ will query the "transaction store"
339
+ * microservice to search the transaction by its id. If non of them is
340
+ * specified the function will use "byBlock" (as "byTransactionId"
341
+ * requires the transaction store, which is an optional microservice).
342
+ *
343
+ * When _byBlock_ is used it returns the block number.
344
+ *
345
+ * When _byTransactionId_ is used it returns the block id.
346
+ *
347
+ * @param timeout - Timeout in milliseconds. By default it is 15000
348
+ * @example
349
+ * ```ts
350
+ * const blockNumber = await provider.wait(txId);
351
+ * // const blockNumber = await provider.wait(txId, "byBlock", 15000);
352
+ * // const blockId = await provider.wait(txId, "byTransactionId", 15000);
353
+ * console.log("Transaction mined")
354
+ * ```
355
+ */
356
+ async wait(
357
+ txId: string,
358
+ type: "byTransactionId" | "byBlock" = "byBlock",
359
+ timeout = 15000
360
+ ): Promise<{
361
+ blockId: string;
362
+ blockNumber?: number;
363
+ }> {
364
+ const iniTime = Date.now();
365
+ if (type === "byTransactionId") {
366
+ while (Date.now() < iniTime + timeout) {
367
+ await sleep(1000);
368
+ const { transactions } = await this.getTransactionsById([txId]);
369
+ if (
370
+ transactions &&
371
+ transactions[0] &&
372
+ transactions[0].containing_blocks
373
+ )
374
+ return {
375
+ blockId: transactions[0].containing_blocks[0],
376
+ };
377
+ }
378
+ throw new Error(`Transaction not mined after ${timeout} ms`);
379
+ }
380
+
381
+ // byBlock
382
+ const findTxInBlocks = async (
383
+ ini: number,
384
+ numBlocks: number,
385
+ idRef: string
386
+ ): Promise<[number, string, string]> => {
387
+ const blocks = await this.getBlocks(ini, numBlocks, idRef);
388
+ let bNum = 0;
389
+ let bId = "";
390
+ blocks.forEach((block) => {
391
+ if (
392
+ !block ||
393
+ !block.block ||
394
+ !block.block_id ||
395
+ !block.block.transactions
396
+ )
397
+ return;
398
+ const tx = block.block.transactions.find((t) => t.id === txId);
399
+ if (tx) {
400
+ bNum = Number(block.block_height);
401
+ bId = block.block_id;
402
+ }
403
+ });
404
+ const lastId = blocks[blocks.length - 1].block_id;
405
+ return [bNum, bId, lastId];
406
+ };
407
+
408
+ let blockNumber = 0;
409
+ let iniBlock = 0;
410
+ let previousId = "";
411
+
412
+ while (Date.now() < iniTime + timeout) {
413
+ await sleep(1000);
414
+ const { head_topology: headTopology } = await this.getHeadInfo();
415
+ if (blockNumber === 0) {
416
+ blockNumber = Number(headTopology.height);
417
+ iniBlock = blockNumber;
418
+ }
419
+ if (
420
+ Number(headTopology.height) === blockNumber - 1 &&
421
+ previousId &&
422
+ previousId !== headTopology.id
423
+ ) {
424
+ const [bNum, bId, lastId] = await findTxInBlocks(
425
+ iniBlock,
426
+ Number(headTopology.height) - iniBlock + 1,
427
+ headTopology.id
428
+ );
429
+ if (bNum)
430
+ return {
431
+ blockId: bId,
432
+ blockNumber: bNum,
433
+ };
434
+ previousId = lastId;
435
+ blockNumber = Number(headTopology.height) + 1;
436
+ }
437
+ // eslint-disable-next-line no-continue
438
+ if (blockNumber > Number(headTopology.height)) continue;
439
+ const [bNum, bId, lastId] = await findTxInBlocks(
440
+ blockNumber,
441
+ 1,
442
+ headTopology.id
443
+ );
444
+ if (bNum)
445
+ return {
446
+ blockId: bId,
447
+ blockNumber: bNum,
448
+ };
449
+ if (!previousId) previousId = lastId;
450
+ blockNumber += 1;
451
+ }
452
+ throw new Error(
453
+ `Transaction not mined after ${timeout} ms. Blocks checked from ${iniBlock} to ${blockNumber}`
454
+ );
455
+ }
456
+
457
+ /**
458
+ * Function to call "chain.submit_transaction" to send a signed
459
+ * transaction to the blockchain.
460
+ *
461
+ * It also has the option to not broadcast the transaction (to not
462
+ * include the transaction the mempool), which is useful if you
463
+ * want to test the interaction with a contract and check the
464
+ * possible events triggered.
465
+ * @param transaction - Transaction
466
+ * @param broadcast - Option to broadcast the transaction to the
467
+ * whole network. By default it is true.
468
+ * @returns It returns the receipt received from the RPC node
469
+ * and the transaction with the arrow function "wait" (see [[wait]])
470
+ */
471
+ async sendTransaction(
472
+ transaction: TransactionJson | TransactionJsonWait,
473
+ broadcast = true
474
+ ): Promise<{
475
+ receipt: TransactionReceipt;
476
+ transaction: TransactionJsonWait;
477
+ }> {
478
+ const response = await this.call<{ receipt: TransactionReceipt }>(
479
+ "chain.submit_transaction",
480
+ { transaction, broadcast }
481
+ );
482
+ (transaction as TransactionJsonWait).wait = async (
483
+ type: "byTransactionId" | "byBlock" = "byBlock",
484
+ timeout = 15000
485
+ ) => {
486
+ return this.wait(transaction.id as string, type, timeout);
487
+ };
488
+ return { ...response, transaction: transaction as TransactionJsonWait };
489
+ }
490
+
491
+ /**
492
+ * Function to call "chain.submit_block" to send a signed
493
+ * block to the blockchain.
494
+ */
495
+ async submitBlock(block: BlockJson): Promise<Record<string, never>> {
496
+ return this.call("chain.submit_block", { block });
497
+ }
498
+
499
+ /**
500
+ * Function to call "chain.read_contract" to read a contract.
501
+ * This function is used by [[Contract]] class when read methods
502
+ * are invoked.
503
+ */
504
+ async readContract(operation: CallContractOperationJson): Promise<{
505
+ result: string;
506
+ logs: string;
507
+ }> {
508
+ return this.call("chain.read_contract", operation);
509
+ }
510
+ }
511
+
512
+ export default Provider;