@wishknish/knishio-client-ts 0.8.3 → 0.9.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/dist/index.cjs +392 -258
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -16
- package/dist/index.d.ts +37 -16
- package/dist/index.iife.js +392 -260
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +414 -280
- package/dist/index.js.map +1 -1
- package/package.json +6 -9
- package/src/KnishIOClient.ts +135 -53
- package/src/constants.ts +1 -2
- package/src/core/Molecule.ts +53 -0
- package/src/core/Wallet.ts +84 -3
- package/src/libraries/Decimal.ts +1 -1
- package/src/libraries/GraphQLClient.ts +83 -0
- package/src/libraries/Hex.ts +4 -3
- package/src/libraries/UrqlClientWrapper.ts +13 -13
- package/src/libraries/crypto.ts +4 -4
- package/src/mutation/MutationClaimShadowWallet.ts +3 -1
- package/src/mutation/MutationTransferTokens.ts +18 -0
- package/src/query/QueryActiveSession.ts +0 -1
- package/src/query/QueryPolicy.ts +0 -1
- package/src/response/ResponseAuthorizationGuest.ts +2 -2
- package/src/response/ResponseBalance.ts +9 -6
- package/src/response/ResponseLinkIdentifier.ts +1 -1
- package/src/response/ResponseRequestAuthorizationGuest.ts +2 -2
- package/src/subscribe/Subscribe.ts +3 -2
- package/src/types/assertions.ts +1 -6
- package/src/types/client.ts +12 -19
- package/src/types/guards.ts +2 -3
- package/src/validation/UNIVERSAL_CONFIGURATION_INTERFACES.ts +3 -5
- package/src/validation/ValidationService.ts +6 -15
- package/src/versions/HashAtom.ts +11 -9
- package/src/query/QueryUserActivity.ts +0 -153
- package/src/response/ResponseQueryUserActivity.ts +0 -89
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wishknish/knishio-client-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
@@ -71,17 +71,12 @@
|
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@types/isomorphic-fetch": "^0.0.39",
|
|
74
|
-
"@types/jest": "^30.0.0",
|
|
75
74
|
"@types/jssha": "^3.0.0",
|
|
76
75
|
"@types/node": "^22.8.1",
|
|
77
|
-
"@types/pg": "^8.10.0",
|
|
78
76
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
|
79
77
|
"@typescript-eslint/parser": "^8.15.0",
|
|
80
78
|
"eslint": "^9.16.0",
|
|
81
|
-
"jest": "^30.2.0",
|
|
82
|
-
"pg": "^8.11.0",
|
|
83
79
|
"prettier": "^3.4.2",
|
|
84
|
-
"ts-jest": "^29.4.6",
|
|
85
80
|
"tsup": "^8.3.5",
|
|
86
81
|
"tsx": "^4.20.5",
|
|
87
82
|
"typescript": "^5.6.3",
|
|
@@ -100,10 +95,12 @@
|
|
|
100
95
|
"build:test": "tsup src/index.ts --format esm,cjs,iife --tsconfig tsconfig.build.json --clean",
|
|
101
96
|
"build:dev": "tsup src/index.ts --format esm,cjs --dts --sourcemap --watch",
|
|
102
97
|
"typecheck": "tsc --noEmit",
|
|
103
|
-
"lint": "eslint src
|
|
98
|
+
"lint": "eslint src",
|
|
99
|
+
"lint:fix": "eslint src --fix",
|
|
104
100
|
"format": "prettier --write \"src/**/*.{ts,tsx}\"",
|
|
105
|
-
"test": "vitest",
|
|
106
|
-
"test:
|
|
101
|
+
"test": "vitest run",
|
|
102
|
+
"test:watch": "vitest",
|
|
103
|
+
"test:coverage": "vitest run --coverage",
|
|
107
104
|
"test:ui": "vitest --ui",
|
|
108
105
|
"dev": "vite",
|
|
109
106
|
"prepublishOnly": "npm run build",
|
package/src/KnishIOClient.ts
CHANGED
|
@@ -50,7 +50,8 @@ import Decimal from '@/libraries/Decimal'
|
|
|
50
50
|
import GraphQLClient from '@/libraries/GraphQLClient'
|
|
51
51
|
import {
|
|
52
52
|
generateBundleHash,
|
|
53
|
-
generateSecret
|
|
53
|
+
generateSecret,
|
|
54
|
+
generateBatchId
|
|
54
55
|
} from '@/libraries/crypto'
|
|
55
56
|
import Molecule from '@/core/Molecule'
|
|
56
57
|
import Wallet from '@/core/Wallet'
|
|
@@ -80,7 +81,6 @@ import QueryBatchHistory from '@/query/QueryBatchHistory'
|
|
|
80
81
|
import QueryAtom from '@/query/QueryAtom'
|
|
81
82
|
import QueryPolicy from '@/query/QueryPolicy'
|
|
82
83
|
import QueryActiveSession from '@/query/QueryActiveSession'
|
|
83
|
-
import QueryUserActivity from '@/query/QueryUserActivity'
|
|
84
84
|
import QueryToken from '@/query/QueryToken'
|
|
85
85
|
import QueryMetaTypeViaAtom from '@/query/QueryMetaTypeViaAtom'
|
|
86
86
|
import QueryMetaTypeViaMolecule from '@/query/QueryMetaTypeViaMolecule'
|
|
@@ -121,6 +121,7 @@ import {
|
|
|
121
121
|
TransferBalanceException,
|
|
122
122
|
WalletShadowException,
|
|
123
123
|
StackableUnitAmountException,
|
|
124
|
+
StackableUnitDecimalsException,
|
|
124
125
|
AuthorizationRejectedException,
|
|
125
126
|
MolecularHashMismatchException,
|
|
126
127
|
SignatureMismatchException,
|
|
@@ -137,7 +138,8 @@ import type {
|
|
|
137
138
|
MetaId,
|
|
138
139
|
BatchId,
|
|
139
140
|
MetaFilter,
|
|
140
|
-
RequestPolicy
|
|
141
|
+
RequestPolicy,
|
|
142
|
+
TransferRecipientInput
|
|
141
143
|
} from '@/types'
|
|
142
144
|
|
|
143
145
|
/**
|
|
@@ -801,6 +803,96 @@ export default class KnishIOClient {
|
|
|
801
803
|
return await this.executeQuery(query)
|
|
802
804
|
}
|
|
803
805
|
|
|
806
|
+
/**
|
|
807
|
+
* Fund N recipients from a single source in ONE molecule (multi-recipient sibling of
|
|
808
|
+
* transferToken). Each recipient gets its own subset of stackable units (or a fungible
|
|
809
|
+
* amount); a remainder returns the rest to the sender. Conserves:
|
|
810
|
+
* -balance + Σamounts + (balance - Σ) === 0.
|
|
811
|
+
*
|
|
812
|
+
* @param token - token slug
|
|
813
|
+
* @param recipients - destinations (bundleHash + units OR amount + optional batchId)
|
|
814
|
+
* @param sourceWallet - optional pre-resolved source (defaults to the client's own wallet)
|
|
815
|
+
*/
|
|
816
|
+
async transferTokens({
|
|
817
|
+
token,
|
|
818
|
+
recipients,
|
|
819
|
+
sourceWallet = null
|
|
820
|
+
}: {
|
|
821
|
+
token: string
|
|
822
|
+
recipients: TransferRecipientInput[]
|
|
823
|
+
sourceWallet?: Wallet | null
|
|
824
|
+
}): Promise<Response | null> {
|
|
825
|
+
// Per-recipient amount: stackable -> unit count; fungible -> explicit amount (never both)
|
|
826
|
+
const amounts: number[] = recipients.map(recipient => {
|
|
827
|
+
const unitCount = recipient.units ? recipient.units.length : 0
|
|
828
|
+
if (unitCount > 0) {
|
|
829
|
+
if (recipient.amount !== null && recipient.amount !== undefined && recipient.amount > 0) {
|
|
830
|
+
throw new StackableUnitAmountException()
|
|
831
|
+
}
|
|
832
|
+
return unitCount
|
|
833
|
+
}
|
|
834
|
+
return recipient.amount ?? 0
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
const total = amounts.reduce((sum, amount) => sum + amount, 0)
|
|
838
|
+
|
|
839
|
+
// Resolve the source (queryBalance now carries tokenUnits, so the split below sees real units)
|
|
840
|
+
if (sourceWallet === null) {
|
|
841
|
+
sourceWallet = await this.querySourceWallet({
|
|
842
|
+
token,
|
|
843
|
+
amount: total
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (sourceWallet === null || Decimal.cmp(Number(sourceWallet.balance), total) < 0) {
|
|
848
|
+
throw new TransferBalanceException()
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// Build a shadow recipient wallet per destination + assign a distinct batch id
|
|
852
|
+
const recipientWallets: Wallet[] = recipients.map(recipient => {
|
|
853
|
+
const recipientWallet = Wallet.create({
|
|
854
|
+
bundle: recipient.bundleHash,
|
|
855
|
+
token
|
|
856
|
+
})
|
|
857
|
+
if (recipient.batchId !== null && recipient.batchId !== undefined) {
|
|
858
|
+
recipientWallet.batchId = recipient.batchId
|
|
859
|
+
} else {
|
|
860
|
+
recipientWallet.initBatchId({ sourceWallet: sourceWallet as Wallet })
|
|
861
|
+
}
|
|
862
|
+
return recipientWallet
|
|
863
|
+
})
|
|
864
|
+
|
|
865
|
+
// Canonical remainder (carries the source's token/characters; reuses the source batch id)
|
|
866
|
+
const remainderWallet = sourceWallet.createRemainder(this.getSecret())
|
|
867
|
+
|
|
868
|
+
// Stackable (NFT): partition the source's tokenUnits across source (SENT union), each
|
|
869
|
+
// recipient (its subset), and remainder (KEPT) BEFORE the molecule is built. No-op for fungible.
|
|
870
|
+
if (recipients.some(recipient => recipient.units && recipient.units.length > 0)) {
|
|
871
|
+
sourceWallet.splitUnitsMulti(
|
|
872
|
+
recipients.map(recipient => recipient.units ?? []),
|
|
873
|
+
recipientWallets,
|
|
874
|
+
remainderWallet
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const molecule = await this.createMolecule({
|
|
879
|
+
sourceWallet,
|
|
880
|
+
remainderWallet
|
|
881
|
+
})
|
|
882
|
+
|
|
883
|
+
const query = await this.createMoleculeMutation({
|
|
884
|
+
mutationClass: MutationTransferTokens,
|
|
885
|
+
molecule
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
query.fillMoleculeMulti({
|
|
889
|
+
recipientWallets,
|
|
890
|
+
amounts
|
|
891
|
+
})
|
|
892
|
+
|
|
893
|
+
return await this.executeQuery(query)
|
|
894
|
+
}
|
|
895
|
+
|
|
804
896
|
/**
|
|
805
897
|
* Request authorization token (guest or profile)
|
|
806
898
|
*
|
|
@@ -1436,51 +1528,6 @@ export default class KnishIOClient {
|
|
|
1436
1528
|
}) as Promise<Response>
|
|
1437
1529
|
}
|
|
1438
1530
|
|
|
1439
|
-
/**
|
|
1440
|
-
* Query user activity
|
|
1441
|
-
*/
|
|
1442
|
-
async queryUserActivity({
|
|
1443
|
-
bundleHash,
|
|
1444
|
-
metaType,
|
|
1445
|
-
metaId,
|
|
1446
|
-
ipAddress = null,
|
|
1447
|
-
browser = null,
|
|
1448
|
-
osCpu = null,
|
|
1449
|
-
resolution = null,
|
|
1450
|
-
timeZone = null,
|
|
1451
|
-
countBy = null,
|
|
1452
|
-
interval = null
|
|
1453
|
-
}: {
|
|
1454
|
-
bundleHash: BundleHash | string
|
|
1455
|
-
metaType: MetaType | string
|
|
1456
|
-
metaId: MetaId | string
|
|
1457
|
-
ipAddress?: string | null
|
|
1458
|
-
browser?: string | null
|
|
1459
|
-
osCpu?: string | null
|
|
1460
|
-
resolution?: string | null
|
|
1461
|
-
timeZone?: string | null
|
|
1462
|
-
countBy?: string | null
|
|
1463
|
-
interval?: string | null
|
|
1464
|
-
}): Promise<Response> {
|
|
1465
|
-
const query = this.createQuery(QueryUserActivity)
|
|
1466
|
-
|
|
1467
|
-
const variables: Record<string, any> = {
|
|
1468
|
-
bundleHash,
|
|
1469
|
-
metaType,
|
|
1470
|
-
metaId
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
if (ipAddress !== null) variables.ipAddress = ipAddress
|
|
1474
|
-
if (browser !== null) variables.browser = browser
|
|
1475
|
-
if (osCpu !== null) variables.osCpu = osCpu
|
|
1476
|
-
if (resolution !== null) variables.resolution = resolution
|
|
1477
|
-
if (timeZone !== null) variables.timeZone = timeZone
|
|
1478
|
-
if (countBy !== null) variables.countBy = countBy
|
|
1479
|
-
if (interval !== null) variables.interval = interval
|
|
1480
|
-
|
|
1481
|
-
return this.executeQuery(query, variables) as Promise<Response>
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
1531
|
/**
|
|
1485
1532
|
* Query token information
|
|
1486
1533
|
*/
|
|
@@ -1620,7 +1667,7 @@ export default class KnishIOClient {
|
|
|
1620
1667
|
amount = null,
|
|
1621
1668
|
meta = null,
|
|
1622
1669
|
batchId = null,
|
|
1623
|
-
units
|
|
1670
|
+
units = null
|
|
1624
1671
|
}: {
|
|
1625
1672
|
token: TokenSlug | string
|
|
1626
1673
|
amount?: number | string | null
|
|
@@ -1630,6 +1677,33 @@ export default class KnishIOClient {
|
|
|
1630
1677
|
}): Promise<Response> {
|
|
1631
1678
|
this.log('info', `KnishIOClient::createToken() - Creating token ${token}...`)
|
|
1632
1679
|
|
|
1680
|
+
const tokenMeta: Record<string, any> = meta || {}
|
|
1681
|
+
const fungibility = tokenMeta.fungibility
|
|
1682
|
+
let resolvedAmount: number | string = amount ?? 0
|
|
1683
|
+
|
|
1684
|
+
// For a stackable token, ensure a batch ID is present (mirror JS createToken)
|
|
1685
|
+
if (fungibility === 'stackable') {
|
|
1686
|
+
tokenMeta.batchId = batchId || generateBatchId({})
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Token-unit initialization for stackable / nonfungible tokens: the unit IDs ARE the supply.
|
|
1690
|
+
// Meta values are sent as strings (the validator's MetaItemInput.value is a GraphQL String).
|
|
1691
|
+
if (
|
|
1692
|
+
(fungibility === 'stackable' || fungibility === 'nonfungible' || fungibility === 'non-fungible') &&
|
|
1693
|
+
units && units.length > 0
|
|
1694
|
+
) {
|
|
1695
|
+
if (Number(tokenMeta.decimals ?? 0) > 0) {
|
|
1696
|
+
throw new StackableUnitDecimalsException()
|
|
1697
|
+
}
|
|
1698
|
+
if (Number(amount ?? 0) > 0) {
|
|
1699
|
+
throw new StackableUnitAmountException()
|
|
1700
|
+
}
|
|
1701
|
+
resolvedAmount = units.length
|
|
1702
|
+
tokenMeta.splittable = '1'
|
|
1703
|
+
tokenMeta.decimals = '0'
|
|
1704
|
+
tokenMeta.tokenUnits = JSON.stringify(units)
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1633
1707
|
// Create the molecule mutation
|
|
1634
1708
|
const mutation = await this.createMoleculeMutation({ mutationClass: MutationCreateToken })
|
|
1635
1709
|
|
|
@@ -1644,8 +1718,8 @@ export default class KnishIOClient {
|
|
|
1644
1718
|
// Initialize the create token mutation
|
|
1645
1719
|
await mutation.fillMolecule({
|
|
1646
1720
|
recipientWallet,
|
|
1647
|
-
amount:
|
|
1648
|
-
meta
|
|
1721
|
+
amount: resolvedAmount,
|
|
1722
|
+
meta: tokenMeta
|
|
1649
1723
|
})
|
|
1650
1724
|
|
|
1651
1725
|
// Execute the mutation
|
|
@@ -2333,9 +2407,17 @@ export default class KnishIOClient {
|
|
|
2333
2407
|
molecule
|
|
2334
2408
|
})
|
|
2335
2409
|
|
|
2336
|
-
// Initialize the profile auth mutation
|
|
2410
|
+
// Initialize the profile auth mutation.
|
|
2411
|
+
// PQ-transport Phase E: convey the AUTH source wallet's ML-KEM768 public key as a SIGNED
|
|
2412
|
+
// walletPubkey U-atom meta, so the validator can encrypt CipherHash responses back to THIS
|
|
2413
|
+
// wallet (the one that decrypts them). Signed → MITM can't swap the target. Conveyed
|
|
2414
|
+
// unconditionally (parity with JS/Kotlin/PHP), so even a plaintext auth primes enc_pubkey.
|
|
2415
|
+
const authMeta: Record<string, string> = { encrypt: encrypt ? 'true' : 'false' }
|
|
2416
|
+
if (wallet.pubkey) {
|
|
2417
|
+
authMeta.walletPubkey = wallet.pubkey
|
|
2418
|
+
}
|
|
2337
2419
|
mutation.fillMolecule({
|
|
2338
|
-
meta:
|
|
2420
|
+
meta: authMeta
|
|
2339
2421
|
})
|
|
2340
2422
|
|
|
2341
2423
|
// Execute the mutation
|
package/src/constants.ts
CHANGED
package/src/core/Molecule.ts
CHANGED
|
@@ -319,6 +319,59 @@ export default class Molecule {
|
|
|
319
319
|
return this
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Initialize a MULTI-recipient V-type molecule: one source debits its FULL balance to fund
|
|
324
|
+
* N recipients (each its own amount + stackable units) plus a remainder back to the sender.
|
|
325
|
+
* Multi-recipient sibling of initValue (WP line 544: fund multiple recipients with one
|
|
326
|
+
* transaction). recipientWallets is parallel to amounts.
|
|
327
|
+
*/
|
|
328
|
+
initValues({
|
|
329
|
+
recipientWallets,
|
|
330
|
+
amounts
|
|
331
|
+
}: {
|
|
332
|
+
recipientWallets: Wallet[]
|
|
333
|
+
amounts: number[]
|
|
334
|
+
}): Molecule {
|
|
335
|
+
if (!this.sourceWallet) {
|
|
336
|
+
throw new Error('Source wallet required for value transfer')
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const total = amounts.reduce((sum, amount) => sum + amount, 0)
|
|
340
|
+
|
|
341
|
+
if (Number(this.sourceWallet.balance) - total < 0) {
|
|
342
|
+
throw new BalanceInsufficientException()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Source atom: debit the ENTIRE balance (UTXO drain); carries the SENT union of token units
|
|
346
|
+
this.addAtom(Atom.create({
|
|
347
|
+
isotope: 'V',
|
|
348
|
+
wallet: this.sourceWallet,
|
|
349
|
+
value: -Number(this.sourceWallet.balance)
|
|
350
|
+
}))
|
|
351
|
+
|
|
352
|
+
// One atom per recipient: +amount_i, walletBundle -> recipient bundle, its own SENT units
|
|
353
|
+
recipientWallets.forEach((recipientWallet, i) => {
|
|
354
|
+
this.addAtom(Atom.create({
|
|
355
|
+
isotope: 'V',
|
|
356
|
+
wallet: recipientWallet,
|
|
357
|
+
value: amounts[i]!,
|
|
358
|
+
metaType: 'walletBundle',
|
|
359
|
+
metaId: recipientWallet.bundle!
|
|
360
|
+
}))
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
// Remainder atom: +(balance - total), walletBundle -> sender bundle, KEPT units
|
|
364
|
+
this.addAtom(Atom.create({
|
|
365
|
+
isotope: 'V',
|
|
366
|
+
wallet: this.remainderWallet!,
|
|
367
|
+
value: Number(this.sourceWallet.balance) - total,
|
|
368
|
+
metaType: 'walletBundle',
|
|
369
|
+
metaId: this.remainderWallet!.bundle!
|
|
370
|
+
}))
|
|
371
|
+
|
|
372
|
+
return this
|
|
373
|
+
}
|
|
374
|
+
|
|
322
375
|
/**
|
|
323
376
|
* Sign the molecule with one-time signature
|
|
324
377
|
* Matches JavaScript SDK sign method
|
package/src/core/Wallet.ts
CHANGED
|
@@ -349,6 +349,42 @@ export default class Wallet {
|
|
|
349
349
|
remainderWallet.tokenUnits = remainderTokenUnits
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Split token units across MULTIPLE recipients (N-way sibling of splitUnits).
|
|
354
|
+
*
|
|
355
|
+
* The source retains the SENT union (all units leaving), each recipient gets its own
|
|
356
|
+
* subset, and the remainder keeps the KEPT units (those not assigned to any recipient).
|
|
357
|
+
* recipientUnitLists is parallel to recipientWallets. No-op when no units are sent.
|
|
358
|
+
*
|
|
359
|
+
* @param recipientUnitLists - per-recipient arrays of token unit IDs (parallel to recipientWallets)
|
|
360
|
+
* @param recipientWallets - destination wallets
|
|
361
|
+
* @param remainderWallet - wallet to receive the KEPT units
|
|
362
|
+
*/
|
|
363
|
+
splitUnitsMulti(
|
|
364
|
+
recipientUnitLists: string[][],
|
|
365
|
+
recipientWallets: Wallet[],
|
|
366
|
+
remainderWallet: Wallet
|
|
367
|
+
): void {
|
|
368
|
+
const sentIds = new Set<string>(recipientUnitLists.flat())
|
|
369
|
+
|
|
370
|
+
// Nothing to split (fungible transfer) — leave token units untouched
|
|
371
|
+
if (sentIds.size === 0) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Each recipient gets its own subset of the source's token units
|
|
376
|
+
recipientWallets.forEach((recipientWallet, i) => {
|
|
377
|
+
const ids = recipientUnitLists[i] ?? []
|
|
378
|
+
recipientWallet.tokenUnits = this.tokenUnits.filter(tokenUnit => ids.includes(tokenUnit.id))
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// The remainder keeps everything not sent to any recipient (KEPT)
|
|
382
|
+
remainderWallet.tokenUnits = this.tokenUnits.filter(tokenUnit => !sentIds.has(tokenUnit.id))
|
|
383
|
+
|
|
384
|
+
// The source carries the SENT union (the ownership authority the validator reads)
|
|
385
|
+
this.tokenUnits = this.tokenUnits.filter(tokenUnit => sentIds.has(tokenUnit.id))
|
|
386
|
+
}
|
|
387
|
+
|
|
352
388
|
/**
|
|
353
389
|
* Get token units data
|
|
354
390
|
* Stub for compatibility
|
|
@@ -410,6 +446,16 @@ export default class Wallet {
|
|
|
410
446
|
}
|
|
411
447
|
|
|
412
448
|
async decryptMessage(encryptedData: { cipherText: string; encryptedMessage: string }): Promise<any> {
|
|
449
|
+
const decryptedString = await this._mlkemDecryptToString(encryptedData)
|
|
450
|
+
return decryptedString === null ? null : JSON.parse(decryptedString)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* ML-KEM768 decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string (no JSON.parse).
|
|
455
|
+
* Shared by {@link decryptMessage} (which JSON.parses the result) and the PQ CipherHash transport
|
|
456
|
+
* ({@link decryptMyMessageML768}, which needs the raw response JSON text). PQ-transport Phase E.
|
|
457
|
+
*/
|
|
458
|
+
async _mlkemDecryptToString(encryptedData: { cipherText: string; encryptedMessage: string }): Promise<string | null> {
|
|
413
459
|
const { cipherText, encryptedMessage } = encryptedData
|
|
414
460
|
let sharedSecret
|
|
415
461
|
try {
|
|
@@ -438,9 +484,8 @@ export default class Wallet {
|
|
|
438
484
|
console.info('Wallet::decryptMessage() - deserialized encrypted message', deserializedEncryptedMessage)
|
|
439
485
|
return null
|
|
440
486
|
}
|
|
441
|
-
let decryptedString
|
|
442
487
|
try {
|
|
443
|
-
|
|
488
|
+
return new TextDecoder().decode(decryptedUint8)
|
|
444
489
|
} catch (e) {
|
|
445
490
|
console.warn('Wallet::decryptMessage() - Decoding failed', e)
|
|
446
491
|
console.info('Wallet::decryptMessage() - my public key', this.pubkey)
|
|
@@ -449,7 +494,43 @@ export default class Wallet {
|
|
|
449
494
|
console.info('Wallet::decryptMessage() - decrypted Uint8Array', decryptedUint8)
|
|
450
495
|
return null
|
|
451
496
|
}
|
|
452
|
-
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Canonical cross-SDK hashShare for a public key: standard base64 of SHAKE256(pubkey_utf8, 8 bytes)
|
|
501
|
+
* — byte-matches the validator's hash_share and the JS/Kotlin/PHP hashShare. `shake256(pubkey, 64)`
|
|
502
|
+
* = 64 bits = 8 bytes (hex) → hex-decode → standard base64 via serializeKey. PQ-transport Phase E.
|
|
503
|
+
*/
|
|
504
|
+
hashShare(pubkey: string): string {
|
|
505
|
+
const hex = shake256(pubkey, 64)
|
|
506
|
+
const bytes = new Uint8Array(hex.length / 2)
|
|
507
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
508
|
+
bytes[i] = parseInt(hex.substr(i * 2, 2), 16)
|
|
509
|
+
}
|
|
510
|
+
return this.serializeKey(bytes)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Post-quantum (ML-KEM768) CipherHash request envelope: a stringified single-recipient map
|
|
515
|
+
* `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued, via
|
|
516
|
+
* {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ-transport Phase E.
|
|
517
|
+
*/
|
|
518
|
+
async encryptStringML768(message: any, recipientPubkey: string): Promise<string> {
|
|
519
|
+
const envelope = await this.encryptMessage(message, recipientPubkey)
|
|
520
|
+
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope })
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Decrypt a CipherHash response map addressed to THIS wallet's ML-KEM pubkey
|
|
525
|
+
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
526
|
+
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
527
|
+
*/
|
|
528
|
+
async decryptMyMessageML768(map: Record<string, { cipherText: string; encryptedMessage: string }>): Promise<string | null> {
|
|
529
|
+
const envelope = map[this.hashShare(this.pubkey)]
|
|
530
|
+
if (!envelope) {
|
|
531
|
+
return null
|
|
532
|
+
}
|
|
533
|
+
return this._mlkemDecryptToString(envelope)
|
|
453
534
|
}
|
|
454
535
|
|
|
455
536
|
// =============================================================================
|
package/src/libraries/Decimal.ts
CHANGED
|
@@ -72,7 +72,7 @@ export default class Decimal {
|
|
|
72
72
|
* @param debug Enable debug logging
|
|
73
73
|
* @returns 0 if equal, 1 if value1 > value2, -1 if value1 < value2
|
|
74
74
|
*/
|
|
75
|
-
static cmp(value1: number, value2: number,
|
|
75
|
+
static cmp(value1: number, value2: number, _debug: boolean = false): number {
|
|
76
76
|
const val1 = Decimal.val(value1) * MULTIPLIER
|
|
77
77
|
const val2 = Decimal.val(value2) * MULTIPLIER
|
|
78
78
|
|
|
@@ -62,6 +62,22 @@ import type {
|
|
|
62
62
|
} from '@/types'
|
|
63
63
|
import type Wallet from '@/core/Wallet'
|
|
64
64
|
|
|
65
|
+
const CIPHER_HASH_QUERY = 'query ( $Hash: String! ) { CipherHash ( Hash: $Hash ) { hash } }'
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Light parse of a GraphQL request body's operation type + root field name, for the CipherHash
|
|
69
|
+
* bypass decision (no full GraphQL parse needed). Operation type = the first query/mutation/
|
|
70
|
+
* subscription keyword (default 'query' for an anonymous `{ ... }`); root field = the first
|
|
71
|
+
* identifier inside the top-level selection set.
|
|
72
|
+
*/
|
|
73
|
+
function parseOperation(query: string): { type: string; name: string } {
|
|
74
|
+
const typeMatch = (query || '').match(/\b(query|mutation|subscription)\b/i)
|
|
75
|
+
const type = typeMatch?.[1]?.toLowerCase() ?? 'query'
|
|
76
|
+
const braceIdx = (query || '').indexOf('{')
|
|
77
|
+
const nameMatch = braceIdx >= 0 ? query.slice(braceIdx + 1).match(/[A-Za-z_][A-Za-z0-9_]*/) : null
|
|
78
|
+
return { type, name: nameMatch?.[0] ?? '' }
|
|
79
|
+
}
|
|
80
|
+
|
|
65
81
|
/**
|
|
66
82
|
* GraphQL client wrapper for KnishIO SDK
|
|
67
83
|
* Provides a unified interface for GraphQL operations
|
|
@@ -125,6 +141,10 @@ export default class GraphQLClient implements IGraphQLClient {
|
|
|
125
141
|
return createClient({
|
|
126
142
|
url: serverUri,
|
|
127
143
|
exchanges,
|
|
144
|
+
// PQ-transport Phase E: when encryption is on, route fetch through the CipherHash wrapper
|
|
145
|
+
// (encrypt the request body to the validator's ML-KEM pubkey, decrypt the response).
|
|
146
|
+
// Omitted → urql uses the global fetch (plaintext).
|
|
147
|
+
...(this.cipherLink ? { fetch: ((input: any, init: any) => this.cipherFetch(input, init)) as typeof fetch } : {}),
|
|
128
148
|
fetchOptions: () => ({
|
|
129
149
|
headers: {
|
|
130
150
|
'X-Auth-Token': this.$__authToken
|
|
@@ -135,6 +155,69 @@ export default class GraphQLClient implements IGraphQLClient {
|
|
|
135
155
|
})
|
|
136
156
|
}
|
|
137
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Whether an outgoing GraphQL request body should be wrapped in CipherHash. Bypass (plaintext):
|
|
160
|
+
* introspection `__schema`, `ContinuId`, the `AccessToken` mutation, and the U-isotope
|
|
161
|
+
* `ProposeMolecule` (auth bootstrap — the key exchange itself can't be encrypted). Mirrors the
|
|
162
|
+
* Kotlin/PHP/validator bypass set.
|
|
163
|
+
*/
|
|
164
|
+
private shouldEncrypt(body: string): boolean {
|
|
165
|
+
let parsed: any
|
|
166
|
+
try {
|
|
167
|
+
parsed = JSON.parse(body)
|
|
168
|
+
} catch (e) {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
const { type, name } = parseOperation(parsed.query)
|
|
172
|
+
if (type === 'query' && (name === '__schema' || name === 'ContinuId')) return false
|
|
173
|
+
if (type === 'mutation' && name === 'AccessToken') return false
|
|
174
|
+
if (type === 'mutation' && name === 'ProposeMolecule') {
|
|
175
|
+
const isotope = parsed.variables?.molecule?.atoms?.[0]?.isotope
|
|
176
|
+
if (isotope === 'U') return false
|
|
177
|
+
}
|
|
178
|
+
return true
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Custom `fetch` that wraps a GraphQL request in the ML-KEM CipherHash envelope and decrypts the
|
|
183
|
+
* response (PQ-transport Phase E). Operates on the raw POST body (mirrors the JS/Kotlin transform).
|
|
184
|
+
* Reads the CURRENT client wallet + validator pubkey from auth.
|
|
185
|
+
*/
|
|
186
|
+
private async cipherFetch(input: any, init: any): Promise<Response> {
|
|
187
|
+
const wallet = this.$__wallet
|
|
188
|
+
const serverPubkey = this.$__pubkey
|
|
189
|
+
let encryptedRequest = false
|
|
190
|
+
let requestInit = init
|
|
191
|
+
|
|
192
|
+
if (wallet && serverPubkey && init && typeof init.body === 'string' && this.shouldEncrypt(init.body)) {
|
|
193
|
+
const hashVar = await wallet.encryptStringML768(init.body, serverPubkey)
|
|
194
|
+
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) }
|
|
195
|
+
encryptedRequest = true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const response = await fetch(input, requestInit)
|
|
199
|
+
if (!encryptedRequest) {
|
|
200
|
+
return response
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Decrypt the CipherHash response back to the inner GraphQL response JSON.
|
|
204
|
+
const text = await response.text()
|
|
205
|
+
const init2 = { status: response.status, statusText: response.statusText, headers: response.headers }
|
|
206
|
+
let parsed: any
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(text)
|
|
209
|
+
} catch (e) {
|
|
210
|
+
return new Response(text, init2)
|
|
211
|
+
}
|
|
212
|
+
const hash = parsed?.data?.CipherHash?.hash
|
|
213
|
+
if (typeof hash !== 'string') {
|
|
214
|
+
// Plaintext (e.g. a validator-side error response) — pass through unchanged.
|
|
215
|
+
return new Response(text, init2)
|
|
216
|
+
}
|
|
217
|
+
const decrypted = await wallet!.decryptMyMessageML768(JSON.parse(hash))
|
|
218
|
+
return new Response(decrypted != null ? decrypted : text, init2)
|
|
219
|
+
}
|
|
220
|
+
|
|
138
221
|
setAuthData({
|
|
139
222
|
token,
|
|
140
223
|
pubkey,
|
package/src/libraries/Hex.ts
CHANGED
|
@@ -72,7 +72,8 @@ export default class Hex {
|
|
|
72
72
|
? ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F']
|
|
73
73
|
: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
// indices are provably in-range (val is a byte; /16 and %16 are 0..15 for a 16-elem set)
|
|
76
|
+
return set[Math.floor(val / 16)]! + set[val % 16]!
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
const opts: Required<HexOptions> = Object.assign(
|
|
@@ -97,7 +98,7 @@ export default class Hex {
|
|
|
97
98
|
let column = 0
|
|
98
99
|
|
|
99
100
|
for (let i = 0; i < arrayLike.length; ++i) {
|
|
100
|
-
str += numberToHex(arrayLike[i]
|
|
101
|
+
str += numberToHex(arrayLike[i]!, opts.uppercase)
|
|
101
102
|
|
|
102
103
|
if (i === arrayLike.length - 1) {
|
|
103
104
|
break
|
|
@@ -135,7 +136,7 @@ export default class Hex {
|
|
|
135
136
|
let curr = -1
|
|
136
137
|
|
|
137
138
|
for (let i = 0; i < target.length; ++i) {
|
|
138
|
-
const c = target[i]
|
|
139
|
+
const c = target[i]!
|
|
139
140
|
const val = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'].indexOf(c)
|
|
140
141
|
|
|
141
142
|
if (val === -1) {
|