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.
package/src/utils.ts ADDED
@@ -0,0 +1,533 @@
1
+ import * as multibase from "multibase";
2
+ import { sha256 } from "@noble/hashes/sha256";
3
+ import { ripemd160 } from "@noble/hashes/ripemd160";
4
+ import { Abi, TypeField } from "./interface";
5
+ import tokenProtoJson from "./jsonDescriptors/token-proto.json";
6
+
7
+ /**
8
+ * Converts an hex string to Uint8Array
9
+ */
10
+ export function toUint8Array(hex: string): Uint8Array {
11
+ const pairs = hex.match(/[\dA-F]{2}/gi);
12
+ if (!pairs) throw new Error("Invalid hex");
13
+ return new Uint8Array(
14
+ pairs.map((s) => parseInt(s, 16)) // convert to integers
15
+ );
16
+ }
17
+
18
+ /**
19
+ * Converts Uint8Array to hex string
20
+ */
21
+ export function toHexString(buffer: Uint8Array): string {
22
+ return Array.from(buffer)
23
+ .map((n) => `0${Number(n).toString(16)}`.slice(-2))
24
+ .join("");
25
+ }
26
+
27
+ /**
28
+ * Encodes an Uint8Array in base58
29
+ */
30
+ export function encodeBase58(buffer: Uint8Array): string {
31
+ return new TextDecoder().decode(multibase.encode("z", buffer)).slice(1);
32
+ }
33
+
34
+ /**
35
+ * Decodes a buffer formatted in base58
36
+ */
37
+ export function decodeBase58(bs58: string): Uint8Array {
38
+ return multibase.decode(`z${bs58}`);
39
+ }
40
+
41
+ /**
42
+ * Encodes an Uint8Array in base64url
43
+ */
44
+ export function encodeBase64url(buffer: Uint8Array): string {
45
+ return new TextDecoder().decode(multibase.encode("U", buffer)).slice(1);
46
+ }
47
+
48
+ /**
49
+ * Decodes a buffer formatted in base64url
50
+ */
51
+ export function decodeBase64url(bs64url: string): Uint8Array {
52
+ return multibase.decode(`U${bs64url}`);
53
+ }
54
+
55
+ /**
56
+ * Encodes an Uint8Array in base64
57
+ */
58
+ export function encodeBase64(buffer: Uint8Array): string {
59
+ return new TextDecoder().decode(multibase.encode("M", buffer)).slice(1);
60
+ }
61
+
62
+ export function multihash(buffer: Uint8Array, code = "sha2-256"): Uint8Array {
63
+ switch (code) {
64
+ case "sha2-256": {
65
+ return new Uint8Array([18, buffer.length, ...buffer]);
66
+ }
67
+ default:
68
+ throw new Error(`multihash code ${code} not supported`);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Decodes a buffer formatted in base64
74
+ */
75
+ export function decodeBase64(bs64: string): Uint8Array {
76
+ return multibase.decode(`M${bs64}`);
77
+ }
78
+
79
+ /**
80
+ * Calculates the merkle root of sha256 hashes
81
+ */
82
+ export function calculateMerkleRoot(hashes: Uint8Array[]): Uint8Array {
83
+ if (!hashes.length) return sha256(new Uint8Array());
84
+
85
+ while (hashes.length > 1) {
86
+ for (let i = 0; i < hashes.length; i += 2) {
87
+ if (i + 1 < hashes.length) {
88
+ const leftHash = hashes[i];
89
+ const rightHash = hashes[i + 1];
90
+
91
+ const sumHash = sha256(new Uint8Array([...leftHash, ...rightHash]));
92
+
93
+ hashes[i / 2] = new Uint8Array(sumHash);
94
+ } else {
95
+ hashes[i / 2] = hashes[i];
96
+ }
97
+ }
98
+
99
+ hashes = hashes.slice(0, Math.ceil(hashes.length / 2));
100
+ }
101
+
102
+ return hashes[0];
103
+ }
104
+
105
+ /**
106
+ * Encodes a public or private key in base58 using
107
+ * the bitcoin format (see [Bitcoin Base58Check encoding](https://en.bitcoin.it/wiki/Base58Check_encoding)
108
+ * and [Bitcoin WIF](https://en.bitcoin.it/wiki/Wallet_import_format)).
109
+ *
110
+ * For private keys this encode is also known as
111
+ * wallet import format (WIF).
112
+ */
113
+ export function bitcoinEncode(
114
+ buffer: Uint8Array,
115
+ type: "public" | "private",
116
+ compressed = false
117
+ ): string {
118
+ let bufferCheck: Uint8Array;
119
+ let prefixBuffer: Uint8Array;
120
+ let offsetChecksum: number;
121
+ if (type === "public") {
122
+ bufferCheck = new Uint8Array(25);
123
+ prefixBuffer = new Uint8Array(21);
124
+ bufferCheck[0] = 0;
125
+ prefixBuffer[0] = 0;
126
+ offsetChecksum = 21;
127
+ } else {
128
+ if (compressed) {
129
+ bufferCheck = new Uint8Array(38);
130
+ prefixBuffer = new Uint8Array(34);
131
+ offsetChecksum = 34;
132
+ bufferCheck[33] = 1;
133
+ prefixBuffer[33] = 1;
134
+ } else {
135
+ bufferCheck = new Uint8Array(37);
136
+ prefixBuffer = new Uint8Array(33);
137
+ offsetChecksum = 33;
138
+ }
139
+ bufferCheck[0] = 128;
140
+ prefixBuffer[0] = 128;
141
+ }
142
+ prefixBuffer.set(buffer, 1);
143
+ const firstHash = sha256(prefixBuffer);
144
+ const doubleHash = sha256(firstHash);
145
+ const checksum = new Uint8Array(4);
146
+ checksum.set(doubleHash.slice(0, 4));
147
+ bufferCheck.set(buffer, 1);
148
+ bufferCheck.set(checksum, offsetChecksum);
149
+ return encodeBase58(bufferCheck);
150
+ }
151
+
152
+ /**
153
+ * Decodes a public or private key formatted in base58 using
154
+ * the bitcoin format (see [Bitcoin Base58Check encoding](https://en.bitcoin.it/wiki/Base58Check_encoding)
155
+ * and [Bitcoin WIF](https://en.bitcoin.it/wiki/Wallet_import_format)).
156
+ *
157
+ * For private keys this encode is also known as
158
+ * wallet import format (WIF).
159
+ */
160
+ export function bitcoinDecode(value: string): Uint8Array {
161
+ const buffer = decodeBase58(value);
162
+ const privateKey = new Uint8Array(32);
163
+ const checksum = new Uint8Array(4);
164
+ // const prefix = buffer[0];
165
+ privateKey.set(buffer.slice(1, 33));
166
+ if (value[0] !== "5") {
167
+ // compressed
168
+ checksum.set(buffer.slice(34, 38));
169
+ } else {
170
+ checksum.set(buffer.slice(33, 37));
171
+ }
172
+ // TODO: verify prefix and checksum
173
+ return privateKey;
174
+ }
175
+
176
+ /**
177
+ * Computes a bitcoin address, which is the format used in Koinos
178
+ *
179
+ * address = bitcoinEncode( ripemd160 ( sha256 ( publicKey ) ) )
180
+ */
181
+ export function bitcoinAddress(publicKey: Uint8Array): string {
182
+ const hash = sha256(publicKey);
183
+ const hash160 = ripemd160(hash);
184
+ return bitcoinEncode(hash160, "public");
185
+ }
186
+
187
+ /**
188
+ * Checks if the last 4 bytes matches with the double sha256
189
+ * of the first part
190
+ */
191
+ export function isChecksum(buffer: Uint8Array): boolean {
192
+ const dataLength = buffer.length - 4;
193
+ const data = new Uint8Array(dataLength);
194
+ data.set(buffer.slice(0, dataLength));
195
+
196
+ const checksum = new Uint8Array(4);
197
+ checksum.set(buffer.slice(dataLength));
198
+
199
+ const doubleHash = sha256(sha256(data));
200
+
201
+ // checksum must be the first 4 bytes of the double hash
202
+ for (let i = 0; i < 4; i += 1) {
203
+ if (checksum[i] !== doubleHash[i]) return false;
204
+ }
205
+
206
+ return true;
207
+ }
208
+
209
+ /**
210
+ * Checks if the checksum of an address is correct.
211
+ *
212
+ * The address has 3 parts in this order:
213
+ * - prefix: 1 byte
214
+ * - data: 20 bytes
215
+ * - checksum: 4 bytes
216
+ *
217
+ * checks:
218
+ * - It must be "pay to public key hash" (P2PKH). That is prefix 0
219
+ * - checksum = first 4 bytes of sha256(sha256(prefix + data))
220
+ *
221
+ * See [How to generate a bitcoin address step by step](https://medium.com/coinmonks/how-to-generate-a-bitcoin-address-step-by-step-9d7fcbf1ad0b).
222
+ */
223
+ export function isChecksumAddress(address: string | Uint8Array): boolean {
224
+ const bufferAddress =
225
+ typeof address === "string" ? decodeBase58(address) : address;
226
+
227
+ // it must have 25 bytes
228
+ if (bufferAddress.length !== 25) return false;
229
+
230
+ // it must have prefix 0 (P2PKH address)
231
+ if (bufferAddress[0] !== 0) return false;
232
+
233
+ return isChecksum(bufferAddress);
234
+ }
235
+
236
+ /**
237
+ * Checks if the checksum of an private key WIF is correct.
238
+ *
239
+ * The private key WIF has 3 parts in this order:
240
+ * - prefix: 1 byte
241
+ * - private key: 32 bytes
242
+ * - compressed: 1 byte for compressed public key (no byte for uncompressed)
243
+ * - checksum: 4 bytes
244
+ *
245
+ * checks:
246
+ * - It must use version 0x80 in the prefix
247
+ * - If the corresponding public key is compressed the byte 33 must be 0x01
248
+ * - checksum = first 4 bytes of
249
+ * sha256(sha256(prefix + private key + compressed))
250
+ *
251
+ * See [Bitcoin WIF](https://en.bitcoin.it/wiki/Wallet_import_format).
252
+ */
253
+ export function isChecksumWif(wif: string | Uint8Array): boolean {
254
+ const bufferWif = typeof wif === "string" ? decodeBase58(wif) : wif;
255
+
256
+ // it must have 37 or 38 bytes
257
+ if (bufferWif.length !== 37 && bufferWif.length !== 38) return false;
258
+
259
+ const compressed = bufferWif.length === 38;
260
+
261
+ // if compressed then the last byte must be 0x01
262
+ if (compressed && bufferWif[33] !== 1) return false;
263
+
264
+ // it must have prefix version for private keys (0x80)
265
+ if (bufferWif[0] !== 128) return false;
266
+
267
+ return isChecksum(bufferWif);
268
+ }
269
+
270
+ /**
271
+ * Function to format a number in a decimal point number
272
+ * @example
273
+ * ```js
274
+ * const amount = formatUnits("123456", 8);
275
+ * console.log(amount);
276
+ * // '0.00123456'
277
+ * ```
278
+ */
279
+ export function formatUnits(
280
+ value: string | number | bigint,
281
+ decimals: number
282
+ ): string {
283
+ let v = typeof value === "string" ? value : BigInt(value).toString();
284
+ const sign = v[0] === "-" ? "-" : "";
285
+ v = v.replace("-", "").padStart(decimals + 1, "0");
286
+ const integerPart = v
287
+ .substring(0, v.length - decimals)
288
+ .replace(/^0+(?=\d)/, "");
289
+ const decimalPart = v.substring(v.length - decimals);
290
+ return `${sign}${integerPart}.${decimalPart}`.replace(/(\.0+)?(0+)$/, "");
291
+ }
292
+
293
+ /**
294
+ * Function to format a decimal point number in an integer
295
+ * @example
296
+ * ```js
297
+ * const amount = parseUnits("0.00123456", 8);
298
+ * console.log(amount);
299
+ * // '123456'
300
+ * ```
301
+ */
302
+ export function parseUnits(value: string, decimals: number): string {
303
+ const sign = value[0] === "-" ? "-" : "";
304
+ // eslint-disable-next-line prefer-const
305
+ let [integerPart, decimalPart] = value
306
+ .replace("-", "")
307
+ .replace(",", ".")
308
+ .split(".");
309
+ if (!decimalPart) decimalPart = "";
310
+ decimalPart = decimalPart.padEnd(decimals, "0");
311
+ return `${sign}${`${integerPart}${decimalPart}`.replace(/^0+(?=\d)/, "")}`;
312
+ }
313
+
314
+ /**
315
+ * Makes a copy of a value. The returned value can be modified
316
+ * without altering the original one. Although this is not needed
317
+ * for strings or numbers and only needed for objects and arrays,
318
+ * all these options are covered in a single function
319
+ *
320
+ * It is assumed that the argument is number, string, or contructions
321
+ * of these types inside objects or arrays.
322
+ */
323
+ function copyValue(value: unknown): unknown {
324
+ if (typeof value === "string" || typeof value === "number") {
325
+ return value;
326
+ }
327
+ return JSON.parse(JSON.stringify(value)) as unknown;
328
+ }
329
+
330
+ export function btypeDecodeValue(
331
+ valueEncoded: unknown,
332
+ typeField: TypeField,
333
+ verifyChecksum: boolean
334
+ ): unknown {
335
+ // No byte conversion
336
+ if (typeField.type !== "bytes") return copyValue(valueEncoded);
337
+
338
+ const value = valueEncoded as string;
339
+
340
+ // Default byte conversion
341
+ if (!typeField.btype) {
342
+ return decodeBase64url(value);
343
+ }
344
+
345
+ // Specific byte conversion
346
+ switch (typeField.btype) {
347
+ case "BASE58":
348
+ return decodeBase58(value);
349
+ case "CONTRACT_ID":
350
+ case "ADDRESS":
351
+ const valueDecoded = decodeBase58(value);
352
+ if (verifyChecksum && !isChecksumAddress(valueDecoded)) {
353
+ throw new Error(`${value} is an invalid address`);
354
+ }
355
+ return valueDecoded;
356
+ case "BASE64":
357
+ return decodeBase64url(value);
358
+ case "HEX":
359
+ case "BLOCK_ID":
360
+ case "TRANSACTION_ID":
361
+ return toUint8Array(value.slice(2));
362
+ default:
363
+ throw new Error(`unknown btype ${typeField.btype}`);
364
+ }
365
+ }
366
+
367
+ export function btypeEncodeValue(
368
+ valueDecoded: unknown,
369
+ typeField: TypeField,
370
+ verifyChecksum: boolean
371
+ ): unknown {
372
+ // No byte conversion
373
+ if (typeField.type !== "bytes") return copyValue(valueDecoded);
374
+
375
+ const value = valueDecoded as Uint8Array;
376
+
377
+ // Default byte conversion
378
+ if (!typeField.btype) {
379
+ return encodeBase64url(value);
380
+ }
381
+
382
+ // Specific byte conversion
383
+ switch (typeField.btype) {
384
+ case "BASE58":
385
+ return encodeBase58(value);
386
+ case "CONTRACT_ID":
387
+ case "ADDRESS":
388
+ const valueEncoded = encodeBase58(value);
389
+ if (verifyChecksum && !isChecksumAddress(value)) {
390
+ throw new Error(`${valueEncoded} is an invalid address`);
391
+ }
392
+ return valueEncoded;
393
+ case "BASE64":
394
+ return encodeBase64url(value);
395
+ case "HEX":
396
+ case "BLOCK_ID":
397
+ case "TRANSACTION_ID":
398
+ return `0x${toHexString(value)}`;
399
+ default:
400
+ throw new Error(`unknown btype ${typeField.btype}`);
401
+ }
402
+ }
403
+
404
+ export function btypeDecode(
405
+ valueEncoded: Record<string, unknown> | unknown[],
406
+ fields: Record<string, TypeField>,
407
+ verifyChecksum: boolean
408
+ ) {
409
+ if (typeof valueEncoded !== "object") return valueEncoded;
410
+ const valueDecoded = {} as Record<string, unknown>;
411
+ Object.keys(fields).forEach((name) => {
412
+ if (!valueEncoded[name]) return;
413
+ if (fields[name].rule === "repeated")
414
+ valueDecoded[name] = (valueEncoded[name] as unknown[]).map(
415
+ (itemEncoded) => {
416
+ if (fields[name].subtypes)
417
+ return btypeDecode(
418
+ itemEncoded as Record<string, unknown>,
419
+ fields[name].subtypes!,
420
+ verifyChecksum
421
+ );
422
+ return btypeDecodeValue(itemEncoded, fields[name], verifyChecksum);
423
+ }
424
+ );
425
+ else if (fields[name].subtypes)
426
+ valueDecoded[name] = btypeDecode(
427
+ valueEncoded[name] as Record<string, unknown>,
428
+ fields[name].subtypes!,
429
+ verifyChecksum
430
+ );
431
+ else
432
+ valueDecoded[name] = btypeDecodeValue(
433
+ valueEncoded[name],
434
+ fields[name],
435
+ verifyChecksum
436
+ );
437
+ });
438
+ return valueDecoded;
439
+ }
440
+
441
+ export function btypeEncode(
442
+ valueDecoded: Record<string, unknown> | unknown[],
443
+ fields: Record<string, TypeField>,
444
+ verifyChecksum: boolean
445
+ ) {
446
+ if (typeof valueDecoded !== "object") return valueDecoded;
447
+ const valueEncoded = {} as Record<string, unknown>;
448
+ Object.keys(fields).forEach((name) => {
449
+ if (!valueDecoded[name]) return;
450
+ if (fields[name].rule === "repeated")
451
+ valueEncoded[name] = (valueDecoded[name] as unknown[]).map(
452
+ (itemDecoded) => {
453
+ if (fields[name].subtypes)
454
+ return btypeEncode(
455
+ itemDecoded as Record<string, unknown>,
456
+ fields[name].subtypes!,
457
+ verifyChecksum
458
+ );
459
+ return btypeEncodeValue(itemDecoded, fields[name], verifyChecksum);
460
+ }
461
+ );
462
+ else if (fields[name].subtypes)
463
+ valueEncoded[name] = btypeEncode(
464
+ valueDecoded[name] as Record<string, unknown>,
465
+ fields[name].subtypes!,
466
+ verifyChecksum
467
+ );
468
+ else
469
+ valueEncoded[name] = btypeEncodeValue(
470
+ valueDecoded[name],
471
+ fields[name],
472
+ verifyChecksum
473
+ );
474
+ });
475
+ return valueEncoded;
476
+ }
477
+
478
+ /**
479
+ * ABI for tokens
480
+ */
481
+ export const tokenAbi: Abi = {
482
+ methods: {
483
+ name: {
484
+ entry_point: 0x82a3537f,
485
+ argument: "name_arguments",
486
+ return: "name_result",
487
+ read_only: true,
488
+ },
489
+ symbol: {
490
+ entry_point: 0xb76a7ca1,
491
+ argument: "symbol_arguments",
492
+ return: "symbol_result",
493
+ read_only: true,
494
+ },
495
+ decimals: {
496
+ entry_point: 0xee80fd2f,
497
+ argument: "decimals_arguments",
498
+ return: "decimals_result",
499
+ read_only: true,
500
+ },
501
+ totalSupply: {
502
+ entry_point: 0xb0da3934,
503
+ argument: "total_supply_arguments",
504
+ return: "total_supply_result",
505
+ read_only: true,
506
+ },
507
+ balanceOf: {
508
+ entry_point: 0x5c721497,
509
+ argument: "balance_of_arguments",
510
+ return: "balance_of_result",
511
+ read_only: true,
512
+ default_output: { value: "0" },
513
+ },
514
+ transfer: {
515
+ entry_point: 0x27f576ca,
516
+ argument: "transfer_arguments",
517
+ return: "transfer_result",
518
+ },
519
+ mint: {
520
+ entry_point: 0xdc6f17bb,
521
+ argument: "mint_arguments",
522
+ return: "mint_result",
523
+ },
524
+ burn: {
525
+ entry_point: 0x859facc5,
526
+ argument: "burn_arguments",
527
+ return: "burn_result",
528
+ },
529
+ },
530
+ koilib_types: tokenProtoJson,
531
+ };
532
+
533
+ //export const ProtocolTypes = protocolJson;