@wishknish/knishio-client-ts 0.9.2 → 0.9.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/dist/index.cjs +173 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.iife.js +3463 -339
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +173 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/Wallet.ts +21 -2
- package/src/libraries/CheckMolecule.ts +194 -4
- package/src/libraries/array.ts +6 -4
- package/src/libraries/strings.ts +12 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishknish/knishio-client-ts",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"productName": "Knish.IO TypeScript SDK Client",
|
|
5
5
|
"description": "TypeScript implementation of the Knish.IO SDK to consume Knish.IO GraphQL APIs with enhanced type safety.",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
package/src/core/Wallet.ts
CHANGED
|
@@ -608,11 +608,30 @@ export default class Wallet {
|
|
|
608
608
|
}
|
|
609
609
|
|
|
610
610
|
serializeKey(key: Uint8Array): string {
|
|
611
|
-
|
|
611
|
+
if (typeof Buffer !== 'undefined') {
|
|
612
|
+
return Buffer.from(key).toString('base64')
|
|
613
|
+
}
|
|
614
|
+
// A single String.fromCharCode(...key) spread overflows the call stack once the
|
|
615
|
+
// payload exceeds the engine's argument limit (~64K), so convert in chunks.
|
|
616
|
+
let binary = ''
|
|
617
|
+
const CHUNK_SIZE = 0x8000
|
|
618
|
+
for (let i = 0; i < key.length; i += CHUNK_SIZE) {
|
|
619
|
+
binary += String.fromCharCode(...key.subarray(i, i + CHUNK_SIZE))
|
|
620
|
+
}
|
|
621
|
+
return btoa(binary)
|
|
612
622
|
}
|
|
613
623
|
|
|
614
624
|
deserializeKey(serializedKey: string): Uint8Array {
|
|
625
|
+
if (typeof Buffer !== 'undefined') {
|
|
626
|
+
return new Uint8Array(Buffer.from(serializedKey, 'base64'))
|
|
627
|
+
}
|
|
628
|
+
// Spreading the binary string ([...str]) allocates one single-char string per
|
|
629
|
+
// byte — prohibitive for multi-MB payloads. Index directly instead.
|
|
615
630
|
const binaryString = atob(serializedKey)
|
|
616
|
-
|
|
631
|
+
const bytes = new Uint8Array(binaryString.length)
|
|
632
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
633
|
+
bytes[i] = binaryString.charCodeAt(i)
|
|
634
|
+
}
|
|
635
|
+
return bytes
|
|
617
636
|
}
|
|
618
637
|
}
|
|
@@ -139,6 +139,10 @@ export default class CheckMolecule {
|
|
|
139
139
|
this.isotopeU() &&
|
|
140
140
|
this.isotopeI() &&
|
|
141
141
|
this.isotopeR() &&
|
|
142
|
+
this.isotopeP() &&
|
|
143
|
+
this.isotopeA() &&
|
|
144
|
+
this.isotopeB() &&
|
|
145
|
+
this.isotopeF() &&
|
|
142
146
|
this.isotopeV(senderWallet || null)
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -369,6 +373,172 @@ export default class CheckMolecule {
|
|
|
369
373
|
return true
|
|
370
374
|
}
|
|
371
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Validate isotope P atoms (peering)
|
|
378
|
+
* @return True if valid
|
|
379
|
+
* @throws WrongTokenTypeException if the token slug is not USER
|
|
380
|
+
* @throws MetaMissingException if the required peerHost meta field is absent
|
|
381
|
+
*/
|
|
382
|
+
isotopeP(): boolean {
|
|
383
|
+
for (const atom of this.molecule.getIsotopes('P')) {
|
|
384
|
+
if (atom.token !== 'USER') {
|
|
385
|
+
throw new WrongTokenTypeException(`Check::isotopeP() - "${atom.token}" is not a valid Token slug for "${atom.isotope}" isotope Atoms!`)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const metas = atom.aggregatedMeta()
|
|
389
|
+
|
|
390
|
+
if (!Object.prototype.hasOwnProperty.call(metas, 'peerHost') || !metas.peerHost) {
|
|
391
|
+
throw new MetaMissingException('Check::isotopeP() - Required meta field "peerHost" is missing!')
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Validate isotope A atoms (append request)
|
|
400
|
+
* @return True if valid
|
|
401
|
+
* @throws WrongTokenTypeException if the token slug is not USER
|
|
402
|
+
* @throws MetaMissingException if metaType, metaId or the action meta field is absent
|
|
403
|
+
*/
|
|
404
|
+
isotopeA(): boolean {
|
|
405
|
+
for (const atom of this.molecule.getIsotopes('A')) {
|
|
406
|
+
if (atom.token !== 'USER') {
|
|
407
|
+
throw new WrongTokenTypeException(`Check::isotopeA() - "${atom.token}" is not a valid Token slug for "${atom.isotope}" isotope Atoms!`)
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (!atom.metaType) {
|
|
411
|
+
throw new MetaMissingException('Check::isotopeA() - Required field "metaType" is missing!')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (!atom.metaId) {
|
|
415
|
+
throw new MetaMissingException('Check::isotopeA() - Required field "metaId" is missing!')
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const metas = atom.aggregatedMeta()
|
|
419
|
+
|
|
420
|
+
if (!Object.prototype.hasOwnProperty.call(metas, 'action') || !metas.action) {
|
|
421
|
+
throw new MetaMissingException('Check::isotopeA() - Required meta field "action" is missing!')
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return true
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Validate isotope B atoms (buffer/exchange)
|
|
430
|
+
*
|
|
431
|
+
* Buffer molecules are cross-isotope: their V atoms do not balance on their own
|
|
432
|
+
* because a B atom absorbs the difference. isotopeV() therefore skips its V-only
|
|
433
|
+
* conservation checks whenever B (or F) atoms are present, and conservation is
|
|
434
|
+
* enforced here instead over the combined V+B set.
|
|
435
|
+
*
|
|
436
|
+
* @return True if valid
|
|
437
|
+
* @throws MetaMissingException if metaType is not walletBundle, or metaId is absent
|
|
438
|
+
* @throws TransferMalformedException if a B atom value is not a number
|
|
439
|
+
* @throws TransferUnbalancedException if V+B atom values do not sum to zero
|
|
440
|
+
*/
|
|
441
|
+
isotopeB(): boolean {
|
|
442
|
+
const isotopeB = this.molecule.getIsotopes('B')
|
|
443
|
+
|
|
444
|
+
if (isotopeB.length === 0) {
|
|
445
|
+
return true
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
for (const atom of isotopeB) {
|
|
449
|
+
// B atoms must reference a wallet bundle
|
|
450
|
+
if (!atom.metaType || atom.metaType !== 'walletBundle') {
|
|
451
|
+
throw new MetaMissingException('Check::isotopeB() - B-isotope atoms must have metaType "walletBundle"!')
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!atom.metaId) {
|
|
455
|
+
throw new MetaMissingException('Check::isotopeB() - B-isotope atoms must have a metaId!')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Value must be parseable as a number
|
|
459
|
+
const value = atom.value !== null ? Number(atom.value) : Number.NaN
|
|
460
|
+
if (Number.isNaN(value)) {
|
|
461
|
+
throw new TransferMalformedException('Check::isotopeB() - B-isotope atom value is not a valid number!')
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// V+B balance conservation: sum of all V and B atom values must equal zero
|
|
466
|
+
const vAtoms = this.molecule.getIsotopes('V')
|
|
467
|
+
if (vAtoms.length > 0) {
|
|
468
|
+
let sum = 0
|
|
469
|
+
for (const atom of [...vAtoms, ...isotopeB]) {
|
|
470
|
+
const value = atom.value !== null ? Number(atom.value) : Number.NaN
|
|
471
|
+
if (!Number.isNaN(value)) {
|
|
472
|
+
sum += value
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (sum !== 0) {
|
|
476
|
+
throw new TransferUnbalancedException('Check::isotopeB() - V+B atom values do not balance to zero!')
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return true
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Validate isotope F atoms (fusion/NFT)
|
|
485
|
+
*
|
|
486
|
+
* Mirrors isotopeB() and additionally forbids negative values. Must stay paired
|
|
487
|
+
* with the hasCrossIsotope gate in isotopeV(), which is keyed on B *or* F: without
|
|
488
|
+
* this check an F-isotope molecule would skip V-only conservation with nothing
|
|
489
|
+
* validating V+F conservation in its place.
|
|
490
|
+
*
|
|
491
|
+
* @return True if valid
|
|
492
|
+
* @throws MetaMissingException if metaType is not walletBundle, or metaId is absent
|
|
493
|
+
* @throws TransferMalformedException if an F atom value is not a number, or is negative
|
|
494
|
+
* @throws TransferUnbalancedException if V+F atom values do not sum to zero
|
|
495
|
+
*/
|
|
496
|
+
isotopeF(): boolean {
|
|
497
|
+
const isotopeF = this.molecule.getIsotopes('F')
|
|
498
|
+
|
|
499
|
+
if (isotopeF.length === 0) {
|
|
500
|
+
return true
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const atom of isotopeF) {
|
|
504
|
+
// F atoms must reference a wallet bundle
|
|
505
|
+
if (!atom.metaType || atom.metaType !== 'walletBundle') {
|
|
506
|
+
throw new MetaMissingException('Check::isotopeF() - F-isotope atoms must have metaType "walletBundle"!')
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (!atom.metaId) {
|
|
510
|
+
throw new MetaMissingException('Check::isotopeF() - F-isotope atoms must have a metaId!')
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Value must be parseable
|
|
514
|
+
const value = atom.value !== null ? Number(atom.value) : Number.NaN
|
|
515
|
+
if (Number.isNaN(value)) {
|
|
516
|
+
throw new TransferMalformedException('Check::isotopeF() - F-isotope atom value is not a valid number!')
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
if (value < 0) {
|
|
520
|
+
throw new TransferMalformedException('Check::isotopeF() - F-isotope atom value must not be negative!')
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// V+F balance conservation: sum of all V and F atom values must equal zero
|
|
525
|
+
const vAtoms = this.molecule.getIsotopes('V')
|
|
526
|
+
if (vAtoms.length > 0) {
|
|
527
|
+
let sum = 0
|
|
528
|
+
for (const atom of [...vAtoms, ...isotopeF]) {
|
|
529
|
+
const value = atom.value !== null ? Number(atom.value) : Number.NaN
|
|
530
|
+
if (!Number.isNaN(value)) {
|
|
531
|
+
sum += value
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (sum !== 0) {
|
|
535
|
+
throw new TransferUnbalancedException('Check::isotopeF() - V+F atom values do not balance to zero!')
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return true
|
|
540
|
+
}
|
|
541
|
+
|
|
372
542
|
/**
|
|
373
543
|
* Validate isotope V atoms (value transfers)
|
|
374
544
|
* @param senderWallet - Optional wallet for balance validation
|
|
@@ -382,13 +552,22 @@ export default class CheckMolecule {
|
|
|
382
552
|
return true
|
|
383
553
|
}
|
|
384
554
|
|
|
555
|
+
// When B or F atoms are present, cross-isotope conservation is validated
|
|
556
|
+
// by isotopeB()/isotopeF() — skip V-only conservation check.
|
|
557
|
+
//
|
|
558
|
+
// This is deliberately keyed on isotope PRESENCE, not on atom shape. A buffer
|
|
559
|
+
// deposit is [V,B,V] and a withdraw is [B,V,B]; a shape-based test accepts the
|
|
560
|
+
// first and rejects the second, which is exactly the defect this mirrors away.
|
|
561
|
+
const hasCrossIsotope = this.molecule.getIsotopes('B').length > 0 ||
|
|
562
|
+
this.molecule.getIsotopes('F').length > 0
|
|
563
|
+
|
|
385
564
|
const firstAtom = this.molecule.atoms[0]
|
|
386
565
|
|
|
387
566
|
if (!firstAtom) {
|
|
388
567
|
throw new AtomsMissingException('Check::isotopeV() - Missing first atom')
|
|
389
568
|
}
|
|
390
569
|
|
|
391
|
-
if (firstAtom.isotope === 'V' && isotopeV.length === 2) {
|
|
570
|
+
if (!hasCrossIsotope && firstAtom.isotope === 'V' && isotopeV.length === 2) {
|
|
392
571
|
const endAtom = isotopeV[isotopeV.length - 1]
|
|
393
572
|
|
|
394
573
|
if (!endAtom) {
|
|
@@ -404,6 +583,14 @@ export default class CheckMolecule {
|
|
|
404
583
|
throw new TransferMalformedException()
|
|
405
584
|
}
|
|
406
585
|
|
|
586
|
+
// Conservation check for 2-atom transfers. This branch returns early, so
|
|
587
|
+
// without this the general sum check below is never reached and a two-atom
|
|
588
|
+
// V transfer that creates or destroys value is accepted.
|
|
589
|
+
const firstValueForPair = firstAtom.value !== null ? Number(firstAtom.value) : 0
|
|
590
|
+
if ((firstValueForPair + endValue) !== 0) {
|
|
591
|
+
throw new TransferUnbalancedException()
|
|
592
|
+
}
|
|
593
|
+
|
|
407
594
|
return true
|
|
408
595
|
}
|
|
409
596
|
|
|
@@ -449,8 +636,10 @@ export default class CheckMolecule {
|
|
|
449
636
|
}
|
|
450
637
|
}
|
|
451
638
|
|
|
452
|
-
//
|
|
453
|
-
|
|
639
|
+
// V-only conservation: all V atoms must sum to zero (skip for B/F cross-isotope,
|
|
640
|
+
// where the balancing atom is a different isotope and conservation is enforced
|
|
641
|
+
// by isotopeB()/isotopeF() instead)
|
|
642
|
+
if (!hasCrossIsotope && sum !== 0) {
|
|
454
643
|
throw new TransferUnbalancedException()
|
|
455
644
|
}
|
|
456
645
|
|
|
@@ -470,7 +659,8 @@ export default class CheckMolecule {
|
|
|
470
659
|
}
|
|
471
660
|
|
|
472
661
|
// Does the remainder match what should be there in the source wallet, if provided?
|
|
473
|
-
|
|
662
|
+
// Skip for cross-isotope (B/F) — conservation is validated by isotopeB()/isotopeF()
|
|
663
|
+
if (!hasCrossIsotope && remainder !== sum) {
|
|
474
664
|
throw new TransferRemainderException()
|
|
475
665
|
}
|
|
476
666
|
} else if (sum !== 0) {
|
package/src/libraries/array.ts
CHANGED
|
@@ -57,11 +57,13 @@ export function deepCloning<T>(
|
|
|
57
57
|
* Used for batch processing operations
|
|
58
58
|
*/
|
|
59
59
|
export function chunkArray<T>(arr: T[], size: number): T[][] {
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
// Iterative: the previous recursive form (`concat` + `slice(size)` per level) blew
|
|
61
|
+
// the call stack and allocated O(n^2) once arr grew past a few thousand elements.
|
|
62
|
+
const chunks: T[][] = []
|
|
63
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
64
|
+
chunks.push(arr.slice(i, i + size))
|
|
62
65
|
}
|
|
63
|
-
|
|
64
|
-
return [arr.slice(0, size)].concat(chunkArray(arr.slice(size), size))
|
|
66
|
+
return chunks
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
package/src/libraries/strings.ts
CHANGED
|
@@ -220,15 +220,20 @@ export function hexToBase64(hexString: string): string {
|
|
|
220
220
|
|
|
221
221
|
// Convert hex to bytes
|
|
222
222
|
const bytes = hexStringToBuffer(cleanHex)
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
if (typeof
|
|
226
|
-
// Browser environment
|
|
227
|
-
return btoa(String.fromCharCode(...bytes))
|
|
228
|
-
} else {
|
|
229
|
-
// Node.js environment or manual conversion
|
|
223
|
+
|
|
224
|
+
// Buffer fast path (Node). Byte-identical to the btoa encoding below.
|
|
225
|
+
if (typeof Buffer !== 'undefined') {
|
|
230
226
|
return Buffer.from(bytes).toString('base64')
|
|
231
227
|
}
|
|
228
|
+
// Browser fallback: a single String.fromCharCode(...bytes) spread overflows the
|
|
229
|
+
// call stack once the payload exceeds the engine's argument limit (~64K), so
|
|
230
|
+
// convert in chunks.
|
|
231
|
+
let binary = ''
|
|
232
|
+
const CHUNK_SIZE = 0x8000
|
|
233
|
+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
|
234
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + CHUNK_SIZE))
|
|
235
|
+
}
|
|
236
|
+
return btoa(binary)
|
|
232
237
|
} catch (error) {
|
|
233
238
|
throw new Error(`Failed to convert hex to base64: ${error instanceof Error ? error.message : String(error)}`)
|
|
234
239
|
}
|