@wishknish/knishio-client-ts 0.8.2 → 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.
Files changed (38) hide show
  1. package/dist/index.cjs +425 -263
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +44 -17
  4. package/dist/index.d.ts +44 -17
  5. package/dist/index.iife.js +425 -265
  6. package/dist/index.iife.js.map +1 -1
  7. package/dist/index.js +447 -285
  8. package/dist/index.js.map +1 -1
  9. package/package.json +6 -9
  10. package/src/KnishIOClient.ts +180 -57
  11. package/src/constants.ts +1 -2
  12. package/src/core/Molecule.ts +53 -0
  13. package/src/core/Wallet.ts +84 -3
  14. package/src/libraries/Decimal.ts +1 -1
  15. package/src/libraries/GraphQLClient.ts +83 -0
  16. package/src/libraries/Hex.ts +4 -3
  17. package/src/libraries/UrqlClientWrapper.ts +13 -13
  18. package/src/libraries/crypto.ts +4 -4
  19. package/src/mutation/MutationClaimShadowWallet.ts +3 -1
  20. package/src/mutation/MutationTransferTokens.ts +18 -0
  21. package/src/query/QueryActiveSession.ts +0 -1
  22. package/src/query/QueryPolicy.ts +0 -1
  23. package/src/response/ResponseAuthorizationGuest.ts +2 -2
  24. package/src/response/ResponseBalance.ts +9 -6
  25. package/src/response/ResponseLinkIdentifier.ts +1 -1
  26. package/src/response/ResponseRequestAuthorizationGuest.ts +2 -2
  27. package/src/schemas/index.ts +2 -1
  28. package/src/subscribe/Subscribe.ts +3 -2
  29. package/src/types/assertions.ts +1 -6
  30. package/src/types/client.ts +12 -19
  31. package/src/types/guards.ts +2 -3
  32. package/src/types/index.ts +6 -0
  33. package/src/validation/UNIVERSAL_CONFIGURATION_INTERFACES.ts +3 -5
  34. package/src/validation/ValidationService.ts +6 -15
  35. package/src/validation/schemas.ts +5 -1
  36. package/src/versions/HashAtom.ts +11 -9
  37. package/src/query/QueryUserActivity.ts +0 -153
  38. 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.8.2",
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 --ext .ts,.tsx --fix",
98
+ "lint": "eslint src",
99
+ "lint:fix": "eslint src --fix",
104
100
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
105
- "test": "vitest",
106
- "test:coverage": "vitest --coverage",
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",
@@ -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,
@@ -136,7 +137,9 @@ import type {
136
137
  MetaType,
137
138
  MetaId,
138
139
  BatchId,
139
- MetaFilter
140
+ MetaFilter,
141
+ RequestPolicy,
142
+ TransferRecipientInput
140
143
  } from '@/types'
141
144
 
142
145
  /**
@@ -152,6 +155,10 @@ export default class KnishIOClient {
152
155
  private $__client!: GraphQLClient
153
156
  private $__serverSdkVersion: number = 3
154
157
  private $__logging: boolean = false
158
+ // Default urql request policy applied to reads when the caller doesn't pass
159
+ // one. null = urql's default (cache-first). Server/sync clients set this to
160
+ // 'network-only' so a long-lived client never serves a stale cached read.
161
+ private $__defaultRequestPolicy: RequestPolicy | null = null
155
162
  private $__authTokenObjects: Record<string, AuthToken | null> = {}
156
163
  private $__authToken: AuthToken | null = null
157
164
  private $__authInProcess: boolean = false
@@ -176,6 +183,7 @@ export default class KnishIOClient {
176
183
  socket?: { socketUri: string | null; appKey?: string } | null
177
184
  serverSdkVersion?: number
178
185
  logging?: boolean
186
+ defaultRequestPolicy?: RequestPolicy | null
179
187
  }) {
180
188
  // Phase 2 Enhancement: Use standardized configuration validation
181
189
  const standardValidationResult = ConfigValidator.validateClientConfig(config)
@@ -213,14 +221,19 @@ export default class KnishIOClient {
213
221
  serverSdkVersion = 3,
214
222
  logging = false
215
223
  } = validatedConfig
216
-
224
+ // Read from the RAW config: defaultRequestPolicy is an optional add-on the
225
+ // strict schema now permits, but the validated/inferred shape may not carry
226
+ // it depending on the parser, so take it straight from the caller's config.
227
+ const defaultRequestPolicy = (config.defaultRequestPolicy ?? null) as RequestPolicy | null
228
+
217
229
  this.initialize({
218
230
  uri: uri as string | string[],
219
231
  cellSlug: cellSlug as string | null,
220
232
  socket: socket as { socketUri: string | null; appKey?: string } | null,
221
233
  client: client as GraphQLClient | null,
222
234
  serverSdkVersion,
223
- logging
235
+ logging,
236
+ defaultRequestPolicy
224
237
  })
225
238
  }
226
239
 
@@ -233,7 +246,8 @@ export default class KnishIOClient {
233
246
  socket = null,
234
247
  client = null,
235
248
  serverSdkVersion = 3,
236
- logging = false
249
+ logging = false,
250
+ defaultRequestPolicy = null
237
251
  }: {
238
252
  uri: string | string[]
239
253
  cellSlug?: string | null
@@ -241,6 +255,7 @@ export default class KnishIOClient {
241
255
  client?: GraphQLClient | null
242
256
  serverSdkVersion?: number
243
257
  logging?: boolean
258
+ defaultRequestPolicy?: RequestPolicy | null
244
259
  }): void {
245
260
  this.reset()
246
261
 
@@ -269,6 +284,9 @@ export default class KnishIOClient {
269
284
  })
270
285
 
271
286
  this.$__serverSdkVersion = serverSdkVersion
287
+ // Config (not session state) — set here, NOT cleared in reset(), mirroring
288
+ // serverSdkVersion. A re-initialize updates it.
289
+ this.$__defaultRequestPolicy = defaultRequestPolicy
272
290
  }
273
291
 
274
292
  /**
@@ -324,6 +342,22 @@ export default class KnishIOClient {
324
342
  return this.$__serverSdkVersion
325
343
  }
326
344
 
345
+ /**
346
+ * Gets the client's default urql request policy (null = urql default).
347
+ */
348
+ getDefaultRequestPolicy(): RequestPolicy | null {
349
+ return this.$__defaultRequestPolicy
350
+ }
351
+
352
+ /**
353
+ * Sets the default urql request policy applied to reads that don't specify
354
+ * one. Pass 'network-only' on a long-lived server/sync client so it never
355
+ * serves a stale cached read; null restores urql's default (cache-first).
356
+ */
357
+ setDefaultRequestPolicy(policy: RequestPolicy | null): void {
358
+ this.$__defaultRequestPolicy = policy
359
+ }
360
+
327
361
  /**
328
362
  * Reset common properties
329
363
  */
@@ -596,7 +630,16 @@ export default class KnishIOClient {
596
630
 
597
631
  // Execute the query/mutation, forwarding any urql context (e.g.
598
632
  // requestPolicy) assembled by the caller (queryMeta) / createQueryContext().
599
- return await query.execute({ variables: variables || {}, context })
633
+ // When the caller did NOT specify a request policy, fall back to this
634
+ // client's defaultRequestPolicy (e.g. a long-lived server/sync client set
635
+ // to 'network-only' so it never serves a stale read from the urql cache —
636
+ // removing the footgun of having to pass network-only on every call).
637
+ // Precedence: per-call context.requestPolicy > client default > a query's
638
+ // own createQueryContext() (e.g. ContinuId) > urql default.
639
+ const effectiveContext = (this.$__defaultRequestPolicy && !(context as Record<string, any>).requestPolicy)
640
+ ? { requestPolicy: this.$__defaultRequestPolicy, ...context }
641
+ : context
642
+ return await query.execute({ variables: variables || {}, context: effectiveContext })
600
643
  }
601
644
 
602
645
  /**
@@ -760,6 +803,96 @@ export default class KnishIOClient {
760
803
  return await this.executeQuery(query)
761
804
  }
762
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
+
763
896
  /**
764
897
  * Request authorization token (guest or profile)
765
898
  *
@@ -1395,51 +1528,6 @@ export default class KnishIOClient {
1395
1528
  }) as Promise<Response>
1396
1529
  }
1397
1530
 
1398
- /**
1399
- * Query user activity
1400
- */
1401
- async queryUserActivity({
1402
- bundleHash,
1403
- metaType,
1404
- metaId,
1405
- ipAddress = null,
1406
- browser = null,
1407
- osCpu = null,
1408
- resolution = null,
1409
- timeZone = null,
1410
- countBy = null,
1411
- interval = null
1412
- }: {
1413
- bundleHash: BundleHash | string
1414
- metaType: MetaType | string
1415
- metaId: MetaId | string
1416
- ipAddress?: string | null
1417
- browser?: string | null
1418
- osCpu?: string | null
1419
- resolution?: string | null
1420
- timeZone?: string | null
1421
- countBy?: string | null
1422
- interval?: string | null
1423
- }): Promise<Response> {
1424
- const query = this.createQuery(QueryUserActivity)
1425
-
1426
- const variables: Record<string, any> = {
1427
- bundleHash,
1428
- metaType,
1429
- metaId
1430
- }
1431
-
1432
- if (ipAddress !== null) variables.ipAddress = ipAddress
1433
- if (browser !== null) variables.browser = browser
1434
- if (osCpu !== null) variables.osCpu = osCpu
1435
- if (resolution !== null) variables.resolution = resolution
1436
- if (timeZone !== null) variables.timeZone = timeZone
1437
- if (countBy !== null) variables.countBy = countBy
1438
- if (interval !== null) variables.interval = interval
1439
-
1440
- return this.executeQuery(query, variables) as Promise<Response>
1441
- }
1442
-
1443
1531
  /**
1444
1532
  * Query token information
1445
1533
  */
@@ -1579,7 +1667,7 @@ export default class KnishIOClient {
1579
1667
  amount = null,
1580
1668
  meta = null,
1581
1669
  batchId = null,
1582
- units: _units = null
1670
+ units = null
1583
1671
  }: {
1584
1672
  token: TokenSlug | string
1585
1673
  amount?: number | string | null
@@ -1589,6 +1677,33 @@ export default class KnishIOClient {
1589
1677
  }): Promise<Response> {
1590
1678
  this.log('info', `KnishIOClient::createToken() - Creating token ${token}...`)
1591
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
+
1592
1707
  // Create the molecule mutation
1593
1708
  const mutation = await this.createMoleculeMutation({ mutationClass: MutationCreateToken })
1594
1709
 
@@ -1603,8 +1718,8 @@ export default class KnishIOClient {
1603
1718
  // Initialize the create token mutation
1604
1719
  await mutation.fillMolecule({
1605
1720
  recipientWallet,
1606
- amount: amount ?? 0,
1607
- meta
1721
+ amount: resolvedAmount,
1722
+ meta: tokenMeta
1608
1723
  })
1609
1724
 
1610
1725
  // Execute the mutation
@@ -2292,9 +2407,17 @@ export default class KnishIOClient {
2292
2407
  molecule
2293
2408
  })
2294
2409
 
2295
- // 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
+ }
2296
2419
  mutation.fillMolecule({
2297
- meta: { encrypt: encrypt ? 'true' : 'false' }
2420
+ meta: authMeta
2298
2421
  })
2299
2422
 
2300
2423
  // Execute the mutation
package/src/constants.ts CHANGED
@@ -231,8 +231,7 @@ export const QUERY_TYPES = [
231
231
  'Batch',
232
232
  'ActiveSession',
233
233
  'Policy',
234
- 'Token',
235
- 'UserActivity'
234
+ 'Token'
236
235
  ] as const
237
236
 
238
237
  export type QueryType = typeof QUERY_TYPES[number]
@@ -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
@@ -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
- decryptedString = new TextDecoder().decode(decryptedUint8)
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
- return JSON.parse(decryptedString)
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
  // =============================================================================
@@ -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, debug: boolean = false): 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