@unicitylabs/sphere-sdk 0.17.6 → 0.18.0-dev.2

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 (44) hide show
  1. package/dist/connect/chunks/{chunk-T4DZCXWS.js → chunk-FGQ6CKKK.js} +2 -2
  2. package/dist/connect/chunks/{chunk-T4DZCXWS.js.map → chunk-FGQ6CKKK.js.map} +1 -1
  3. package/dist/connect/chunks/{chunk-BRYRG2TL.js → chunk-JYBI36GK.js} +2 -2
  4. package/dist/connect/chunks/{chunk-MA32MTCD.js → chunk-OAAY2JEA.js} +2 -2
  5. package/dist/connect/index.cjs +1 -1
  6. package/dist/connect/index.cjs.map +1 -1
  7. package/dist/connect/index.js +3 -3
  8. package/dist/connect/internal/host.js +2 -2
  9. package/dist/core/index.cjs +304 -47
  10. package/dist/core/index.cjs.map +1 -1
  11. package/dist/core/index.d.cts +392 -320
  12. package/dist/core/index.d.ts +392 -320
  13. package/dist/core/index.js +304 -47
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/impl/browser/connect/index.cjs +1 -1
  16. package/dist/impl/browser/connect/index.cjs.map +1 -1
  17. package/dist/impl/browser/connect/index.js +2 -2
  18. package/dist/impl/wallet-api-v2/index.cjs +2 -0
  19. package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
  20. package/dist/impl/wallet-api-v2/index.d.cts +30 -30
  21. package/dist/impl/wallet-api-v2/index.d.ts +30 -30
  22. package/dist/impl/wallet-api-v2/index.js +2 -0
  23. package/dist/impl/wallet-api-v2/index.js.map +1 -1
  24. package/dist/index.cjs +304 -47
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +279 -154
  27. package/dist/index.d.ts +279 -154
  28. package/dist/index.js +304 -47
  29. package/dist/index.js.map +1 -1
  30. package/dist/modules/payments-v2/index.cjs +249 -28
  31. package/dist/modules/payments-v2/index.cjs.map +1 -1
  32. package/dist/modules/payments-v2/index.d.cts +252 -166
  33. package/dist/modules/payments-v2/index.d.ts +252 -166
  34. package/dist/modules/payments-v2/index.js +249 -28
  35. package/dist/modules/payments-v2/index.js.map +1 -1
  36. package/dist/token-engine/index.cjs +455 -427
  37. package/dist/token-engine/index.cjs.map +1 -1
  38. package/dist/token-engine/index.d.cts +26 -1
  39. package/dist/token-engine/index.d.ts +26 -1
  40. package/dist/token-engine/index.js +455 -427
  41. package/dist/token-engine/index.js.map +1 -1
  42. package/package.json +1 -1
  43. /package/dist/connect/chunks/{chunk-BRYRG2TL.js.map → chunk-JYBI36GK.js.map} +0 -0
  44. /package/dist/connect/chunks/{chunk-MA32MTCD.js.map → chunk-OAAY2JEA.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -10460,6 +10460,19 @@ async function readTokenNft(token) {
10460
10460
  }
10461
10461
  }
10462
10462
 
10463
+ // token-engine/plugin-ops.ts
10464
+ function contextWithMintVerifiers(base, verifiers) {
10465
+ const registry = new import_MintJustificationVerifierService.MintJustificationVerifierService();
10466
+ for (const verifier of verifiers) registry.register(verifier);
10467
+ return new import_VerificationContext.VerificationContext(
10468
+ base.trustBase,
10469
+ base.predicateVerifier,
10470
+ base.unicityCertificateVerifier,
10471
+ registry,
10472
+ new import_TokenIssuanceVerifierService.TokenIssuanceVerifierService(false)
10473
+ );
10474
+ }
10475
+
10463
10476
  // token-engine/split-checkpoint.ts
10464
10477
  var CHECKPOINT_VERSION = 2;
10465
10478
  var CHECKPOINT_SDK_VERSION = "@unicitylabs/state-transition-sdk@3.0.1";
@@ -10606,6 +10619,10 @@ var SphereTokenEngine = class {
10606
10619
  const data = token.sdkToken.genesis.data;
10607
10620
  return data ? new Uint8Array(data) : null;
10608
10621
  }
10622
+ readTokenJustification(token) {
10623
+ const justification = token.sdkToken.genesis.justification;
10624
+ return justification ? new Uint8Array(justification) : null;
10625
+ }
10609
10626
  readNft(token) {
10610
10627
  return readTokenNft(token);
10611
10628
  }
@@ -10637,7 +10654,7 @@ var SphereTokenEngine = class {
10637
10654
  const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10638
10655
  const tokenType = params.tokenType ? new import_TokenType.TokenType(params.tokenType) : import_TokenType.TokenType.generate();
10639
10656
  const salt = params.salt ? import_TokenSalt.TokenSalt.fromBytes(params.salt) : import_TokenSalt.TokenSalt.generate();
10640
- const mintTx = await import_MintTransaction.MintTransaction.create(this.deps.networkId, recipient, { data: params.data, tokenType, salt });
10657
+ const mintTx = await import_MintTransaction.MintTransaction.create(this.deps.networkId, recipient, { data: params.data, tokenType, salt, justification: params.justification ?? null });
10641
10658
  const certificationData = await import_CertificationData.CertificationData.fromMintTransaction(mintTx);
10642
10659
  const proof = await this.submitAndAwaitProof(
10643
10660
  certificationData,
@@ -10647,34 +10664,30 @@ var SphereTokenEngine = class {
10647
10664
  options
10648
10665
  );
10649
10666
  const certified = await mintTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10650
- const token = await import_Token.Token.mint(certified, this.deps.verificationContext);
10651
- return wrapToken(token);
10667
+ const verifiers = params.mintJustificationVerifiers;
10668
+ return wrapToken(await import_Token.Token.mint(certified, verifiers ? contextWithMintVerifiers(this.deps, verifiers) : this.deps.verificationContext));
10652
10669
  }
10653
10670
  buildNftMint(params) {
10654
10671
  return planNftMint(this.deps, params);
10655
10672
  }
10656
10673
  async transfer(params, options) {
10657
10674
  this.assertOwned(params.token);
10658
- const transferId = this.resolveTransferId(options);
10659
- const recipient = import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey);
10675
+ return this.spend(params.token, import_SignaturePredicate.SignaturePredicate.create(params.recipientPubkey), params.data ?? null, options, "Transfer certification failed");
10676
+ }
10677
+ async burn(params, options) {
10678
+ this.assertOwned(params.token);
10679
+ return this.spend(params.token, import_BurnPredicate.BurnPredicate.create(sha256(params.reasonBytes)), params.reasonBytes, options, "Burn certification failed");
10680
+ }
10681
+ async spend(token, recipient, data, options, failLabel) {
10660
10682
  const stateMask = import_StateMask.StateMask.fromBytes(
10661
- deriveRealization(this.privateKeyHex, transferId, this.resolveOpIndex(options), "stateMask")
10683
+ deriveRealization(this.privateKeyHex, this.resolveTransferId(options), this.resolveOpIndex(options), "stateMask")
10662
10684
  );
10663
- const transferTx = await import_TransferTransaction.TransferTransaction.create(params.token.sdkToken, recipient, stateMask, {
10664
- data: params.data ?? null
10665
- });
10685
+ const transferTx = await import_TransferTransaction.TransferTransaction.create(token.sdkToken, recipient, stateMask, { data });
10666
10686
  const unlockScript = await import_SignaturePredicateUnlockScript.SignaturePredicateUnlockScript.create(transferTx, this.deps.signingService);
10667
10687
  const certificationData = await import_CertificationData.CertificationData.fromTransaction(transferTx, unlockScript);
10668
- const proof = await this.submitAndAwaitProof(
10669
- certificationData,
10670
- transferTx,
10671
- "Transfer certification failed",
10672
- "TRANSFER_FAILED",
10673
- options
10674
- );
10688
+ const proof = await this.submitAndAwaitProof(certificationData, transferTx, failLabel, "TRANSFER_FAILED", options);
10675
10689
  const certified = await transferTx.toCertifiedTransaction(this.deps.trustBase, this.deps.predicateVerifier, this.deps.unicityCertificateVerifier, proof);
10676
- const transferred = await params.token.sdkToken.transfer(certified, this.deps.verificationContext);
10677
- return wrapToken(transferred);
10690
+ return wrapToken(await token.sdkToken.transfer(certified, this.deps.verificationContext));
10678
10691
  }
10679
10692
  async split(params, options) {
10680
10693
  this.assertOwned(params.token);
@@ -10989,6 +11002,20 @@ var SphereTokenEngine = class {
10989
11002
 
10990
11003
  // token-engine/factory.ts
10991
11004
  var DEFAULT_VERIFICATION_POOL_SIZE = 4;
11005
+ function registerPluginVerifiers(registry, plugins) {
11006
+ for (const plugin of plugins ?? []) {
11007
+ for (const verifier of plugin.mintJustificationVerifiers ?? []) {
11008
+ try {
11009
+ registry.register(verifier);
11010
+ } catch (err) {
11011
+ throw new SphereError(
11012
+ `Token plugin '${plugin.id}' registers mint-reason tag ${verifier.tag} twice or over another plugin's: ` + (err instanceof Error ? err.message : String(err)),
11013
+ "INVALID_CONFIG"
11014
+ );
11015
+ }
11016
+ }
11017
+ }
11018
+ }
10992
11019
  function disposedError() {
10993
11020
  return new SphereError(
10994
11021
  "Verification worker pool disposed \u2014 the in-flight verification was cancelled",
@@ -11070,6 +11097,7 @@ async function createSphereTokenEngine(config) {
11070
11097
  mintJustificationVerifier.register(
11071
11098
  new import_SplitMintJustificationVerifier.SplitMintJustificationVerifier(decodeSpherePaymentData)
11072
11099
  );
11100
+ registerPluginVerifiers(mintJustificationVerifier, config.plugins);
11073
11101
  const deps = {
11074
11102
  client: new import_StateTransitionClient.StateTransitionClient(new import_AggregatorClient.AggregatorClient(config.aggregatorUrl, config.apiKey ?? null)),
11075
11103
  trustBase,
@@ -12187,6 +12215,8 @@ var STORE_KEYS = {
12187
12215
  deliveryJournal: "delivery-journal",
12188
12216
  mintJournal: "mint-journal",
12189
12217
  nftMintJournal: "nft-mint-journal",
12218
+ customMintJournal: "custom-mint-journal",
12219
+ burnJournal: "burn-journal",
12190
12220
  shortfalls: "shortfalls",
12191
12221
  settlingLinks: "settling",
12192
12222
  streamCursor: (s) => `cursor:${s}`,
@@ -12371,6 +12401,8 @@ function createMachineStores(kv) {
12371
12401
  deliveryJournal: new DeliveryJournal(kv),
12372
12402
  mintJournal: new ListStore(kv, STORE_KEYS.mintJournal, (e) => e.mintId),
12373
12403
  nftMintJournal: new ListStore(kv, STORE_KEYS.nftMintJournal, (e) => e.mintId),
12404
+ customMintJournal: new ListStore(kv, STORE_KEYS.customMintJournal, (e) => e.mintId),
12405
+ burnJournal: new ListStore(kv, STORE_KEYS.burnJournal, (e) => e.burnId),
12374
12406
  shortfalls: new ListStore(kv, STORE_KEYS.shortfalls, (e) => e.transferId)
12375
12407
  };
12376
12408
  }
@@ -13078,7 +13110,12 @@ var Converger = class {
13078
13110
  } catch (err) {
13079
13111
  retryAfterMs ??= retryAfterMsOf(err);
13080
13112
  }
13081
- for (const replay of [() => this.deps.replayMints(), () => this.deps.replayNftMints()]) {
13113
+ for (const replay of [
13114
+ () => this.deps.replayMints(),
13115
+ () => this.deps.replayNftMints(),
13116
+ () => this.deps.replayCustomMints(),
13117
+ () => this.deps.replayBurns()
13118
+ ]) {
13082
13119
  try {
13083
13120
  progress = await replay() > 0 || progress;
13084
13121
  } catch (err) {
@@ -13089,14 +13126,17 @@ var Converger = class {
13089
13126
  }
13090
13127
  /** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
13091
13128
  async pendingWork() {
13092
- const [backstop, journal, mints, nftMints] = await Promise.all([
13129
+ const [backstop, journal, mints, nftMints, customMints, burns] = await Promise.all([
13093
13130
  this.deps.stores.backstop.list(),
13094
13131
  this.deps.stores.deliveryJournal.list(),
13095
13132
  this.deps.stores.mintJournal.list(),
13096
- this.deps.stores.nftMintJournal.list()
13133
+ this.deps.stores.nftMintJournal.list(),
13134
+ this.deps.stores.customMintJournal.list(),
13135
+ this.deps.stores.burnJournal.list()
13097
13136
  ]);
13098
- const mintsOpen = mints.length > 0 || nftMints.length > 0;
13099
- return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen;
13137
+ const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
13138
+ const burnsOpen = burns.some((e) => !e.settled);
13139
+ return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
13100
13140
  }
13101
13141
  async resumePass() {
13102
13142
  const report = await resumeAll(this.filteredMachineDeps(), {
@@ -13328,6 +13368,12 @@ async function decodeView(deps, tokenId, bytes) {
13328
13368
 
13329
13369
  // modules/payments-v2/inventory/token-data.ts
13330
13370
  async function readTokenData(deps, tokenId) {
13371
+ return deps.engine.readTokenData(await heldToken(deps, tokenId));
13372
+ }
13373
+ async function readTokenJustification(deps, tokenId) {
13374
+ return deps.engine.readTokenJustification(await heldToken(deps, tokenId));
13375
+ }
13376
+ async function heldToken(deps, tokenId) {
13331
13377
  if (deps.view.stateHashOf(tokenId) === void 0) {
13332
13378
  throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
13333
13379
  }
@@ -13335,7 +13381,7 @@ async function readTokenData(deps, tokenId) {
13335
13381
  if (bytes === void 0) {
13336
13382
  throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
13337
13383
  }
13338
- return deps.engine.readTokenData(await deps.engine.decodeToken({ tokenId, token: bytes }));
13384
+ return deps.engine.decodeToken({ tokenId, token: bytes });
13339
13385
  }
13340
13386
 
13341
13387
  // modules/payments-v2/history/History.ts
@@ -15171,6 +15217,7 @@ function mintParams(recipientPubkey, coinId, amount) {
15171
15217
 
15172
15218
  // modules/payments-v2/mint.ts
15173
15219
  var ATTENTION_MINT_UNRESOLVED = "mint:unresolved";
15220
+ var INVENTORY_SCAN_PAGE_LIMIT = 50;
15174
15221
  function runMintUnderJournal(deps, input) {
15175
15222
  return mintUnderJournal(deps, input);
15176
15223
  }
@@ -15223,6 +15270,15 @@ async function finalizeMint(deps, input) {
15223
15270
  deps.noteHeldState(token.blob.tokenId, (await deps.engine.deliveryKeys(bytes)).stateHash);
15224
15271
  deps.refreshView();
15225
15272
  }
15273
+ async function tokenInServerInventory(storagePort, tokenId) {
15274
+ let page = await storagePort.listInventory();
15275
+ for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
15276
+ if (page.items.some((item) => item.tokenId === tokenId && item.status === "active")) return true;
15277
+ if (!page.more) return false;
15278
+ page = await storagePort.listInventory(page.cursor);
15279
+ }
15280
+ return false;
15281
+ }
15226
15282
  async function replayCoinMints(deps) {
15227
15283
  let resolved = 0;
15228
15284
  for (const entry of await deps.mintJournal.list()) {
@@ -15326,6 +15382,199 @@ async function mintJournaled(deps, entry) {
15326
15382
  await finalizeMint(deps, { mintId: entry.mintId, token, assets: [], journal: deps.nftMintJournal });
15327
15383
  }
15328
15384
 
15385
+ // modules/payments-v2/mint-custom.ts
15386
+ var HEX32 = /^[0-9a-f]{64}$/;
15387
+ function assertRequest(request) {
15388
+ if (request.tokenType.length !== 32) {
15389
+ throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
15390
+ }
15391
+ if (request.salt.length < 16 || request.salt.length > 64) {
15392
+ throw new SphereError("mintCustom: salt must be 16 to 64 bytes", "VALIDATION_ERROR");
15393
+ }
15394
+ if (request.data.length === 0) {
15395
+ throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
15396
+ }
15397
+ for (const asset of request.assets) {
15398
+ if (!HEX32.test(asset.coinId) || asset.amount <= 0n) {
15399
+ throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
15400
+ }
15401
+ }
15402
+ }
15403
+ async function runCustomMintUnderJournal(deps, input) {
15404
+ try {
15405
+ assertRequest(input.request);
15406
+ } catch (err) {
15407
+ return { success: false, error: messageOf(err) };
15408
+ }
15409
+ const entry = journalEntryOf2(input.mintId, input.request, deps.now());
15410
+ await deps.customMintJournal.upsert(entry);
15411
+ let tokenId;
15412
+ try {
15413
+ tokenId = await mintJournaled2(deps, entry, input.request.mintJustificationVerifiers);
15414
+ } catch (err) {
15415
+ deps.armHeartbeat();
15416
+ return { success: false, error: messageOf(err) };
15417
+ }
15418
+ return { success: true, tokenId };
15419
+ }
15420
+ async function replayCustomMints(deps) {
15421
+ let resolved = 0;
15422
+ for (const entry of await deps.customMintJournal.list()) {
15423
+ if (deps.isActiveOp(entry.mintId)) continue;
15424
+ try {
15425
+ if (entry.tokenId !== "" && await deps.tokenInServerInventory(entry.tokenId)) {
15426
+ await deps.customMintJournal.removeByKey(entry.mintId);
15427
+ } else {
15428
+ await mintJournaled2(deps, entry, void 0);
15429
+ }
15430
+ resolved += 1;
15431
+ } catch {
15432
+ continue;
15433
+ }
15434
+ }
15435
+ return resolved;
15436
+ }
15437
+ function journalEntryOf2(mintId, request, createdAt) {
15438
+ return {
15439
+ mintId,
15440
+ tokenId: "",
15441
+ dataHex: bytesToHex3(request.data),
15442
+ saltHex: bytesToHex3(request.salt),
15443
+ tokenTypeHex: bytesToHex3(request.tokenType),
15444
+ justificationHex: request.justification ? bytesToHex3(request.justification) : null,
15445
+ assets: request.assets.map((a) => ({ coinId: a.coinId, amount: a.amount.toString() })),
15446
+ createdAt
15447
+ };
15448
+ }
15449
+ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
15450
+ const token = await deps.engine.mintDataToken(
15451
+ {
15452
+ recipientPubkey: deps.ownPubkeyBytes,
15453
+ data: hexToBytes4(entry.dataHex),
15454
+ tokenType: hexToBytes4(entry.tokenTypeHex),
15455
+ salt: hexToBytes4(entry.saltHex),
15456
+ ...entry.justificationHex !== null ? { justification: hexToBytes4(entry.justificationHex) } : {},
15457
+ ...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
15458
+ },
15459
+ { transferId: entry.mintId }
15460
+ );
15461
+ if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
15462
+ throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
15463
+ }
15464
+ if (entry.tokenId === "") {
15465
+ await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
15466
+ }
15467
+ await finalizeMint(deps, {
15468
+ mintId: entry.mintId,
15469
+ token,
15470
+ assets: entry.assets,
15471
+ journal: deps.customMintJournal
15472
+ });
15473
+ return token.blob.tokenId;
15474
+ }
15475
+
15476
+ // modules/payments-v2/burn.ts
15477
+ var HEX322 = /^[0-9a-f]{64}$/;
15478
+ async function heldToken2(deps, tokenId) {
15479
+ const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
15480
+ if (bytes === void 0) {
15481
+ throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
15482
+ }
15483
+ return deps.engine.decodeToken({ tokenId, token: bytes });
15484
+ }
15485
+ function assetsOf(token) {
15486
+ return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
15487
+ }
15488
+ async function runBurnUnderJournal(deps, input) {
15489
+ const { burnId } = input;
15490
+ const { tokenId, reasonBytes } = input.request;
15491
+ if (!HEX322.test(tokenId) || reasonBytes.length === 0) {
15492
+ return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty" };
15493
+ }
15494
+ let token;
15495
+ try {
15496
+ token = await heldToken2(deps, tokenId);
15497
+ if (!deps.engine.isOwnedBy(token, deps.engine.getIdentity().chainPubkey)) {
15498
+ throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
15499
+ }
15500
+ } catch (err) {
15501
+ return { success: false, burnId, tokenId, error: messageOf(err) };
15502
+ }
15503
+ const entry = {
15504
+ burnId,
15505
+ tokenId,
15506
+ reasonHex: bytesToHex3(reasonBytes),
15507
+ burnedTokenHex: null,
15508
+ settled: false,
15509
+ assets: assetsOf(token),
15510
+ createdAt: deps.now()
15511
+ };
15512
+ await deps.burnJournal.upsert(entry);
15513
+ try {
15514
+ const settled = await burnJournaled(deps, entry, token, { applied: false });
15515
+ return { success: true, burnId, tokenId, burnedToken: hexToBytes4(settled.burnedTokenHex ?? "") };
15516
+ } catch (err) {
15517
+ deps.armHeartbeat();
15518
+ return { success: false, burnId, tokenId, error: messageOf(err) };
15519
+ }
15520
+ }
15521
+ async function replayBurns(deps) {
15522
+ let progressed = 0;
15523
+ for (const entry of await deps.burnJournal.list()) {
15524
+ if (entry.settled || deps.isActiveOp(entry.burnId)) continue;
15525
+ try {
15526
+ const token = entry.burnedTokenHex === null ? await heldToken2(deps, entry.tokenId) : null;
15527
+ const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
15528
+ await burnJournaled(deps, entry, token, { applied });
15529
+ progressed += 1;
15530
+ } catch {
15531
+ continue;
15532
+ }
15533
+ }
15534
+ return progressed;
15535
+ }
15536
+ async function pendingBurns(deps) {
15537
+ return (await deps.burnJournal.list()).map((entry) => ({
15538
+ burnId: entry.burnId,
15539
+ tokenId: entry.tokenId,
15540
+ reasonBytes: hexToBytes4(entry.reasonHex),
15541
+ burnedToken: entry.burnedTokenHex === null ? null : hexToBytes4(entry.burnedTokenHex),
15542
+ settled: entry.settled,
15543
+ createdAt: entry.createdAt
15544
+ }));
15545
+ }
15546
+ async function acknowledgeBurn(deps, burnId) {
15547
+ const entry = await deps.burnJournal.getByKey(burnId);
15548
+ if (entry === void 0) return;
15549
+ if (!entry.settled) {
15550
+ throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
15551
+ }
15552
+ await deps.burnJournal.removeByKey(burnId);
15553
+ }
15554
+ async function burnJournaled(deps, entry, token, state) {
15555
+ let current = entry;
15556
+ if (current.burnedTokenHex === null) {
15557
+ if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
15558
+ const burned = await deps.engine.burn(
15559
+ { token, reasonBytes: hexToBytes4(current.reasonHex) },
15560
+ { transferId: current.burnId }
15561
+ );
15562
+ if (burned.blob.tokenId !== current.tokenId) {
15563
+ throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
15564
+ }
15565
+ current = { ...current, burnedTokenHex: bytesToHex3(burned.blob.token) };
15566
+ await deps.burnJournal.upsert(current);
15567
+ }
15568
+ if (!state.applied) {
15569
+ await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [] });
15570
+ }
15571
+ await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
15572
+ current = { ...current, settled: true };
15573
+ await deps.burnJournal.upsert(current);
15574
+ deps.refreshView();
15575
+ return current;
15576
+ }
15577
+
15329
15578
  // modules/payments-v2/send-whole.ts
15330
15579
  async function materializeWholeSpend(deps, input) {
15331
15580
  const { engine } = deps;
@@ -15511,7 +15760,6 @@ async function takeSourceBlobs(deps, sourceIds) {
15511
15760
 
15512
15761
  // modules/payments-v2/PaymentsFacade.ts
15513
15762
  var MAX_RESELECT = 8;
15514
- var INVENTORY_SCAN_PAGE_LIMIT = 50;
15515
15763
  var PaymentsFacade = class {
15516
15764
  constructor(deps) {
15517
15765
  this.deps = deps;
@@ -15548,6 +15796,8 @@ var PaymentsFacade = class {
15548
15796
  refreshView: () => this.view.delta(),
15549
15797
  replayMints: () => replayCoinMints(this.mintDeps()),
15550
15798
  replayNftMints: () => replayNftMints(this.mintDeps()),
15799
+ replayCustomMints: () => replayCustomMints(this.mintDeps()),
15800
+ replayBurns: () => replayBurns(this.mintDeps()),
15551
15801
  isActiveOp: (id) => this.activeMoneyOps.has(id),
15552
15802
  emit: deps.emit,
15553
15803
  now: () => this.nowMs()
@@ -15643,6 +15893,9 @@ var PaymentsFacade = class {
15643
15893
  tokenData(tokenId) {
15644
15894
  return readTokenData(this.readDeps(), tokenId);
15645
15895
  }
15896
+ tokenJustification(tokenId) {
15897
+ return readTokenJustification(this.readDeps(), tokenId);
15898
+ }
15646
15899
  nft(tokenId) {
15647
15900
  return readNft(this.readDeps(), tokenId);
15648
15901
  }
@@ -15691,8 +15944,12 @@ var PaymentsFacade = class {
15691
15944
  return this.track(this.mintInner(coinId, amount));
15692
15945
  }
15693
15946
  mintNft(request) {
15694
- return this.track(this.ownedMint((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15947
+ return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
15695
15948
  }
15949
+ mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
15950
+ burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
15951
+ pendingBurns = () => this.track(pendingBurns(this.mintDeps()));
15952
+ acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.mintDeps(), burnId));
15696
15953
  // §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
15697
15954
  async resumeNow() {
15698
15955
  if (!this.started) return;
@@ -15716,12 +15973,15 @@ var PaymentsFacade = class {
15716
15973
  nowMs() {
15717
15974
  return (this.deps.now ?? Date.now)();
15718
15975
  }
15719
- /** One snapshot serves the coin and the NFT mint, live and replayed. */
15976
+ /** One snapshot serves the coin, NFT and custom mints and the burn, live and replayed. */
15720
15977
  mintDeps() {
15721
15978
  return {
15722
15979
  engine: this.engine(),
15723
15980
  mintJournal: this.machineStores.mintJournal,
15724
15981
  nftMintJournal: this.machineStores.nftMintJournal,
15982
+ customMintJournal: this.machineStores.customMintJournal,
15983
+ burnJournal: this.machineStores.burnJournal,
15984
+ recordSent: (input) => this.historyStore.recordSent(input),
15725
15985
  storagePort: this.deps.storagePort,
15726
15986
  recordMint: (input) => this.historyStore.recordMint(input),
15727
15987
  armHeartbeat: () => this.heartbeat.arm(),
@@ -15730,7 +15990,7 @@ var PaymentsFacade = class {
15730
15990
  ownPubkeyBytes: this.ownPubkeyBytes,
15731
15991
  nftTokenType: this.deps.nftTokenType,
15732
15992
  isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
15733
- tokenInServerInventory: (tokenId) => this.tokenInServerInventory(tokenId),
15993
+ tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
15734
15994
  emit: this.deps.emit,
15735
15995
  now: () => this.nowMs()
15736
15996
  };
@@ -16051,28 +16311,17 @@ var PaymentsFacade = class {
16051
16311
  if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
16052
16312
  return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
16053
16313
  }
16054
- return this.ownedMint((mintId) => runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount }));
16314
+ return this.ownedOp((mintId) => runMintUnderJournal(this.mintDeps(), { mintId, coinId, amount }));
16055
16315
  }
16056
- /** A live mint under a fresh id — its replay stays hands-off while this attempt runs. */
16057
- async ownedMint(run) {
16058
- const mintId = this.newId();
16059
- this.activeMoneyOps.add(mintId);
16316
+ /** A live money op under a fresh id — its replay stays hands-off while this attempt runs (§7). */
16317
+ async ownedOp(run) {
16318
+ const id = this.newId();
16319
+ this.activeMoneyOps.add(id);
16060
16320
  try {
16061
- return await run(mintId);
16321
+ return await run(id);
16062
16322
  } finally {
16063
- this.activeMoneyOps.delete(mintId);
16064
- }
16065
- }
16066
- async tokenInServerInventory(tokenId) {
16067
- let page = await this.deps.storagePort.listInventory();
16068
- for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
16069
- if (page.items.some((item) => item.tokenId === tokenId && item.status === "active")) {
16070
- return true;
16071
- }
16072
- if (!page.more) return false;
16073
- page = await this.deps.storagePort.listInventory(page.cursor);
16323
+ this.activeMoneyOps.delete(id);
16074
16324
  }
16075
- return false;
16076
16325
  }
16077
16326
  // ── wiring internals ───────────────────────────────────────────────────────
16078
16327
  engine() {
@@ -17464,6 +17713,8 @@ var Sphere = class _Sphere {
17464
17713
  * the rebuild after an api-key change share one pool configuration.
17465
17714
  */
17466
17715
  _verification;
17716
+ /** Token plugins (SphereInitOptions.plugins), applied to every engine like _verification. */
17717
+ _plugins;
17467
17718
  /** This Sphere's OWN token registry. Disposed by destroy(); never the process global. */
17468
17719
  _registry = null;
17469
17720
  // Events
@@ -17577,6 +17828,7 @@ var Sphere = class _Sphere {
17577
17828
  communications: options.communications,
17578
17829
  password: options.password,
17579
17830
  verification: options.verification,
17831
+ plugins: options.plugins,
17580
17832
  discoverAddresses: options.discoverAddresses,
17581
17833
  onProgress: options.onProgress
17582
17834
  });
@@ -17614,6 +17866,7 @@ var Sphere = class _Sphere {
17614
17866
  communications: options.communications,
17615
17867
  password: options.password,
17616
17868
  verification: options.verification,
17869
+ plugins: options.plugins,
17617
17870
  discoverAddresses: options.discoverAddresses,
17618
17871
  onProgress: options.onProgress
17619
17872
  });
@@ -17792,6 +18045,7 @@ var Sphere = class _Sphere {
17792
18045
  options.communications
17793
18046
  );
17794
18047
  sphere._verification = options.verification;
18048
+ sphere._plugins = options.plugins;
17795
18049
  sphere._paymentsV2Composition = composition;
17796
18050
  sphere._password = options.password ?? null;
17797
18051
  sphere._passwordProtected = sphere._password !== null;
@@ -17856,6 +18110,7 @@ var Sphere = class _Sphere {
17856
18110
  options.communications
17857
18111
  );
17858
18112
  sphere._verification = options.verification;
18113
+ sphere._plugins = options.plugins;
17859
18114
  sphere._paymentsV2Composition = composition;
17860
18115
  sphere._password = options.password ?? null;
17861
18116
  sphere._passwordProtected = sphere._password !== null;
@@ -17942,6 +18197,7 @@ var Sphere = class _Sphere {
17942
18197
  options.communications
17943
18198
  );
17944
18199
  sphere._verification = options.verification;
18200
+ sphere._plugins = options.plugins;
17945
18201
  sphere._paymentsV2Composition = composition;
17946
18202
  sphere._password = options.password ?? null;
17947
18203
  sphere._passwordProtected = sphere._password !== null;
@@ -20252,7 +20508,8 @@ var Sphere = class _Sphere {
20252
20508
  apiKey: oracle.getApiKey?.(),
20253
20509
  privateKey: hexToBytes4(privateKey),
20254
20510
  trustBaseJson,
20255
- ...this._verification ? { verification: this._verification } : {}
20511
+ ...this._verification ? { verification: this._verification } : {},
20512
+ ...this._plugins ? { plugins: this._plugins } : {}
20256
20513
  });
20257
20514
  } catch (err) {
20258
20515
  logger.warn(