@unicitylabs/sphere-sdk 0.17.0 → 0.17.1

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.
@@ -89,7 +89,7 @@ interface SendRequest {
89
89
  coinId: string;
90
90
  memo?: string;
91
91
  }
92
- interface SendCoinlessRequest {
92
+ interface SendWholeTokenRequest {
93
93
  recipient: string;
94
94
  tokenId: string;
95
95
  memo?: string;
@@ -187,7 +187,9 @@ interface PaymentsV2 {
187
187
  limit?: number;
188
188
  }): Promise<HistoryPage>;
189
189
  send(req: SendRequest): Promise<TransferResult>;
190
- sendCoinless(req: SendCoinlessRequest): Promise<TransferResult>;
190
+ sendWholeToken(req: SendWholeTokenRequest): Promise<TransferResult>;
191
+ /** NFT-scoped twin: refuses a valued source. Connect's `send_nft` routes here. */
192
+ sendCoinless(req: SendWholeTokenRequest): Promise<TransferResult>;
191
193
  mint(coinId: string, amount: bigint): Promise<MintResult>;
192
194
  receive(): Promise<{
193
195
  transfers: IncomingTransfer[];
@@ -89,7 +89,7 @@ interface SendRequest {
89
89
  coinId: string;
90
90
  memo?: string;
91
91
  }
92
- interface SendCoinlessRequest {
92
+ interface SendWholeTokenRequest {
93
93
  recipient: string;
94
94
  tokenId: string;
95
95
  memo?: string;
@@ -187,7 +187,9 @@ interface PaymentsV2 {
187
187
  limit?: number;
188
188
  }): Promise<HistoryPage>;
189
189
  send(req: SendRequest): Promise<TransferResult>;
190
- sendCoinless(req: SendCoinlessRequest): Promise<TransferResult>;
190
+ sendWholeToken(req: SendWholeTokenRequest): Promise<TransferResult>;
191
+ /** NFT-scoped twin: refuses a valued source. Connect's `send_nft` routes here. */
192
+ sendCoinless(req: SendWholeTokenRequest): Promise<TransferResult>;
191
193
  mint(coinId: string, amount: bigint): Promise<MintResult>;
192
194
  receive(): Promise<{
193
195
  transfers: IncomingTransfer[];
@@ -11803,6 +11803,9 @@ function createMachineStores(kv) {
11803
11803
  }
11804
11804
 
11805
11805
  // modules/payments-v2/machine/payload-view.ts
11806
+ function isWholeIntent(p) {
11807
+ return p.kind === "whole" || p.kind === "coinless";
11808
+ }
11806
11809
  function splitOf(p) {
11807
11810
  return p.kind === "coin" ? p.split : void 0;
11808
11811
  }
@@ -12171,7 +12174,14 @@ var TransferMachine = class {
12171
12174
  try {
12172
12175
  const coinId = coinIdOf(payload);
12173
12176
  const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12174
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12177
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
12178
+ await this.deps.recordHistory?.({
12179
+ transferId,
12180
+ payload,
12181
+ phase,
12182
+ committedAmount,
12183
+ ...committedAssets !== void 0 ? { committedAssets } : {}
12184
+ });
12175
12185
  } catch {
12176
12186
  }
12177
12187
  try {
@@ -12185,6 +12195,18 @@ var TransferMachine = class {
12185
12195
  * a journal-rehydrated resume leg has no source token, but every committed
12186
12196
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
12187
12197
  */
12198
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
12199
+ async settledAssets(engine, committed) {
12200
+ const total = /* @__PURE__ */ new Map();
12201
+ for (const outcome of committed) {
12202
+ if (outcome.recipientBlob === void 0) continue;
12203
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
12204
+ for (const asset of engine.readValue(token)?.assets ?? []) {
12205
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
12206
+ }
12207
+ }
12208
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
12209
+ }
12188
12210
  async settledAmount(engine, coinId, committed) {
12189
12211
  let total = 0n;
12190
12212
  for (const outcome of committed) {
@@ -12324,7 +12346,7 @@ async function runOne(ctx, job, report) {
12324
12346
  report.failed.push(job.transferId);
12325
12347
  return;
12326
12348
  }
12327
- if (job.payload.kind === "coinless" && !isCoinlessEnvelope(token.valueEnvelope)) {
12349
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
12328
12350
  logger.warn(
12329
12351
  "PaymentsV2",
12330
12352
  `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
@@ -12372,7 +12394,7 @@ function validateCoinPayload(c) {
12372
12394
  }
12373
12395
  return { ...c, kind: "coin" };
12374
12396
  }
12375
- function validateCoinlessPayload(p) {
12397
+ function validateWholePayload(p) {
12376
12398
  if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12377
12399
  throw new SphereError(
12378
12400
  "token intent payload must name exactly one source token",
@@ -12382,7 +12404,7 @@ function validateCoinlessPayload(p) {
12382
12404
  if (p.split !== void 0) {
12383
12405
  throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12384
12406
  }
12385
- return { ...p, kind: "coinless", direct: [p.direct[0]] };
12407
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
12386
12408
  }
12387
12409
  function validatePayload(raw) {
12388
12410
  const p = raw !== null && typeof raw === "object" ? raw : null;
@@ -12396,13 +12418,13 @@ function validatePayload(raw) {
12396
12418
  throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12397
12419
  }
12398
12420
  const kind = p.kind ?? "coin";
12399
- if (kind !== "coin" && kind !== "coinless") {
12421
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
12400
12422
  throw new SphereError(
12401
12423
  `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12402
12424
  "VALIDATION_ERROR"
12403
12425
  );
12404
12426
  }
12405
- return kind === "coinless" ? validateCoinlessPayload(p) : validateCoinPayload(p);
12427
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
12406
12428
  }
12407
12429
 
12408
12430
  // modules/payments-v2/convergence.ts
@@ -12615,8 +12637,9 @@ async function openRow(entry, journal, decryptPayload) {
12615
12637
  };
12616
12638
  }
12617
12639
  function subject(payload) {
12618
- if (payload?.kind === "coinless") {
12619
- const tokenId = payload.direct?.[0];
12640
+ const rawKind = payload?.kind;
12641
+ if (rawKind === "whole" || rawKind === "coinless") {
12642
+ const tokenId = payload?.direct?.[0];
12620
12643
  return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12621
12644
  }
12622
12645
  const coin = payload;
@@ -12689,10 +12712,10 @@ function buildPayload(recipientPubkey, request, spend, spentStates) {
12689
12712
  function messageOf(err) {
12690
12713
  return err instanceof Error ? err.message : String(err);
12691
12714
  }
12692
- function buildCoinlessPayload(recipientPubkey, request, spentStates) {
12715
+ function buildWholePayload(recipientPubkey, request, spentStates) {
12693
12716
  return {
12694
12717
  v: 2,
12695
- kind: "coinless",
12718
+ kind: "whole",
12696
12719
  recipient: recipientPubkey,
12697
12720
  ...request.memo !== void 0 ? { memo: request.memo } : {},
12698
12721
  direct: [request.tokenId],
@@ -12761,8 +12784,102 @@ async function finalizeMint(deps, mintId, token, coinId, amount) {
12761
12784
  deps.refreshView();
12762
12785
  }
12763
12786
 
12764
- // modules/payments-v2/send-coinless.ts
12765
- async function materializeCoinlessSpend(deps, input) {
12787
+ // modules/payments-v2/inventory/presentation.ts
12788
+ function toToken(tokenId, snapshot, asset, registry, flags) {
12789
+ const iconUrl = registry.getIconUrl(asset.coinId);
12790
+ return {
12791
+ id: tokenId,
12792
+ coinId: asset.coinId,
12793
+ symbol: registry.getSymbol(asset.coinId),
12794
+ name: registry.getName(asset.coinId),
12795
+ decimals: registry.getDecimals(asset.coinId),
12796
+ ...iconUrl !== null ? { iconUrl } : {},
12797
+ amount: asset.amount,
12798
+ status: flags.transferring ? "transferring" : "confirmed",
12799
+ createdAt: snapshot.createdAt,
12800
+ updatedAt: snapshot.updatedAt,
12801
+ lazy: true,
12802
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12803
+ };
12804
+ }
12805
+ function aggregateAssets(entries, isTransferring, registry) {
12806
+ const groups = /* @__PURE__ */ new Map();
12807
+ for (const [tokenId, entry] of entries) {
12808
+ const moving = isTransferring(tokenId);
12809
+ for (const asset of entry.assets) {
12810
+ let group = groups.get(asset.coinId);
12811
+ if (!group) {
12812
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12813
+ groups.set(asset.coinId, group);
12814
+ }
12815
+ if (moving) {
12816
+ group.transferring += BigInt(asset.amount);
12817
+ group.transferringCount += 1;
12818
+ } else {
12819
+ group.total += BigInt(asset.amount);
12820
+ group.count += 1;
12821
+ }
12822
+ }
12823
+ }
12824
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12825
+ }
12826
+ function toAsset(coinId, totals, registry) {
12827
+ const iconUrl = registry.getIconUrl(coinId);
12828
+ return {
12829
+ coinId,
12830
+ symbol: registry.getSymbol(coinId),
12831
+ name: registry.getName(coinId),
12832
+ decimals: registry.getDecimals(coinId),
12833
+ ...iconUrl !== null ? { iconUrl } : {},
12834
+ totalAmount: totals.total.toString(),
12835
+ tokenCount: totals.count,
12836
+ confirmedAmount: totals.total.toString(),
12837
+ unconfirmedAmount: "0",
12838
+ confirmedTokenCount: totals.count,
12839
+ unconfirmedTokenCount: 0,
12840
+ transferringTokenCount: totals.transferringCount,
12841
+ transferringAmount: totals.transferring.toString(),
12842
+ priceUsd: null,
12843
+ priceEur: null,
12844
+ change24h: null,
12845
+ fiatValueUsd: null,
12846
+ fiatValueEur: null
12847
+ };
12848
+ }
12849
+ function priceIdOf(registry, coinId) {
12850
+ return registry.getName(coinId).toLowerCase();
12851
+ }
12852
+ async function withPrices(raw, registry, price) {
12853
+ try {
12854
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12855
+ const quotes = await price.getPrices(ids);
12856
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12857
+ } catch {
12858
+ return raw;
12859
+ }
12860
+ }
12861
+ function priceOne(asset, quote) {
12862
+ if (!quote) return asset;
12863
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12864
+ return {
12865
+ ...asset,
12866
+ priceUsd: quote.priceUsd,
12867
+ priceEur: quote.priceEur ?? null,
12868
+ change24h: quote.change24h ?? null,
12869
+ fiatValueUsd: human * quote.priceUsd,
12870
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12871
+ };
12872
+ }
12873
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12874
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12875
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12876
+ transferring: true,
12877
+ suspectedSpent: false
12878
+ });
12879
+ }
12880
+
12881
+ // modules/payments-v2/send-whole.ts
12882
+ async function materializeWholeSpend(deps, input) {
12766
12883
  const { engine } = deps;
12767
12884
  const tokenId = input.request.tokenId;
12768
12885
  const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
@@ -12770,14 +12887,20 @@ async function materializeCoinlessSpend(deps, input) {
12770
12887
  throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12771
12888
  }
12772
12889
  const token = await engine.decodeToken({ tokenId, token: bytes });
12773
- if (!isCoinlessEnvelope(token.valueEnvelope)) {
12890
+ if (token.valueEnvelope === "bare_collection") {
12891
+ throw new SphereError(
12892
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
12893
+ "VALIDATION_ERROR"
12894
+ );
12895
+ }
12896
+ if (input.requireCoinless && token.value !== null) {
12774
12897
  throw new SphereError(
12775
- `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use send()`,
12898
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
12776
12899
  "VALIDATION_ERROR"
12777
12900
  );
12778
12901
  }
12779
12902
  const keys = await engine.deliveryKeys(bytes);
12780
- const payload = buildCoinlessPayload(input.recipientPubkey, input.request, {
12903
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
12781
12904
  [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
12782
12905
  });
12783
12906
  const plan = {
@@ -12788,7 +12911,10 @@ async function materializeCoinlessSpend(deps, input) {
12788
12911
  sources: /* @__PURE__ */ new Map([[tokenId, token]]),
12789
12912
  ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
12790
12913
  };
12791
- return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens: [] };
12914
+ const sourceTokens = (token.value?.assets ?? []).map(
12915
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
12916
+ );
12917
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
12792
12918
  }
12793
12919
 
12794
12920
  // modules/payments-v2/send-errors.ts
@@ -12935,100 +13061,6 @@ async function takeSourceBlobs(deps, sourceIds) {
12935
13061
  return blobs;
12936
13062
  }
12937
13063
 
12938
- // modules/payments-v2/inventory/presentation.ts
12939
- function toToken(tokenId, snapshot, asset, registry, flags) {
12940
- const iconUrl = registry.getIconUrl(asset.coinId);
12941
- return {
12942
- id: tokenId,
12943
- coinId: asset.coinId,
12944
- symbol: registry.getSymbol(asset.coinId),
12945
- name: registry.getName(asset.coinId),
12946
- decimals: registry.getDecimals(asset.coinId),
12947
- ...iconUrl !== null ? { iconUrl } : {},
12948
- amount: asset.amount,
12949
- status: flags.transferring ? "transferring" : "confirmed",
12950
- createdAt: snapshot.createdAt,
12951
- updatedAt: snapshot.updatedAt,
12952
- lazy: true,
12953
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12954
- };
12955
- }
12956
- function aggregateAssets(entries, isTransferring, registry) {
12957
- const groups = /* @__PURE__ */ new Map();
12958
- for (const [tokenId, entry] of entries) {
12959
- const moving = isTransferring(tokenId);
12960
- for (const asset of entry.assets) {
12961
- let group = groups.get(asset.coinId);
12962
- if (!group) {
12963
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12964
- groups.set(asset.coinId, group);
12965
- }
12966
- if (moving) {
12967
- group.transferring += BigInt(asset.amount);
12968
- group.transferringCount += 1;
12969
- } else {
12970
- group.total += BigInt(asset.amount);
12971
- group.count += 1;
12972
- }
12973
- }
12974
- }
12975
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12976
- }
12977
- function toAsset(coinId, totals, registry) {
12978
- const iconUrl = registry.getIconUrl(coinId);
12979
- return {
12980
- coinId,
12981
- symbol: registry.getSymbol(coinId),
12982
- name: registry.getName(coinId),
12983
- decimals: registry.getDecimals(coinId),
12984
- ...iconUrl !== null ? { iconUrl } : {},
12985
- totalAmount: totals.total.toString(),
12986
- tokenCount: totals.count,
12987
- confirmedAmount: totals.total.toString(),
12988
- unconfirmedAmount: "0",
12989
- confirmedTokenCount: totals.count,
12990
- unconfirmedTokenCount: 0,
12991
- transferringTokenCount: totals.transferringCount,
12992
- transferringAmount: totals.transferring.toString(),
12993
- priceUsd: null,
12994
- priceEur: null,
12995
- change24h: null,
12996
- fiatValueUsd: null,
12997
- fiatValueEur: null
12998
- };
12999
- }
13000
- function priceIdOf(registry, coinId) {
13001
- return registry.getName(coinId).toLowerCase();
13002
- }
13003
- async function withPrices(raw, registry, price) {
13004
- try {
13005
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13006
- const quotes = await price.getPrices(ids);
13007
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13008
- } catch {
13009
- return raw;
13010
- }
13011
- }
13012
- function priceOne(asset, quote) {
13013
- if (!quote) return asset;
13014
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
13015
- return {
13016
- ...asset,
13017
- priceUsd: quote.priceUsd,
13018
- priceEur: quote.priceEur ?? null,
13019
- change24h: quote.change24h ?? null,
13020
- fiatValueUsd: human * quote.priceUsd,
13021
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
13022
- };
13023
- }
13024
- function transferringToken(tokenId, coinId, amount, registry, now) {
13025
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
13026
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13027
- transferring: true,
13028
- suspectedSpent: false
13029
- });
13030
- }
13031
-
13032
13064
  // modules/payments-v2/history/History.ts
13033
13065
  function wireAssets(assets) {
13034
13066
  return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
@@ -13284,10 +13316,10 @@ var InventoryView = class {
13284
13316
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13285
13317
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13286
13318
  }
13287
- /** #777: is this NAMED token a spendable COINLESS holding? Never a coin source. */
13288
- spendableCoinless(tokenId) {
13319
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
13320
+ spendableToken(tokenId) {
13289
13321
  const entry = this.mirror.get(tokenId);
13290
- return entry !== void 0 && entry.coinless && this.isSpendable(tokenId, entry);
13322
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
13291
13323
  }
13292
13324
  /**
13293
13325
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
@@ -13660,7 +13692,7 @@ async function screen(deps, engine, entry) {
13660
13692
  tokenId: keys.tokenId,
13661
13693
  stateHash: keys.stateHash,
13662
13694
  assets: toAssetAmounts(token),
13663
- ...isCoinlessEnvelope2(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13695
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13664
13696
  }
13665
13697
  };
13666
13698
  }
@@ -13797,9 +13829,6 @@ function claimAck(entry) {
13797
13829
  function rejectAck(entry, reason) {
13798
13830
  return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
13799
13831
  }
13800
- function isCoinlessEnvelope2(envelope) {
13801
- return envelope.startsWith("none_");
13802
- }
13803
13832
  function toAssetAmounts(token) {
13804
13833
  return (token.value?.assets ?? []).map((asset) => ({
13805
13834
  coinId: asset.coinId,
@@ -14374,7 +14403,7 @@ var SpendQueue = class {
14374
14403
  * can release, so queueing would block until a timeout on a spend that cannot
14375
14404
  * become possible.
14376
14405
  */
14377
- planCoinless(reservationId, tokenId) {
14406
+ planWhole(reservationId, tokenId) {
14378
14407
  if (this.destroyed) {
14379
14408
  throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14380
14409
  }
@@ -14382,11 +14411,8 @@ var SpendQueue = class {
14382
14411
  if (unproven !== null) {
14383
14412
  throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14384
14413
  }
14385
- if (this.deps.spendableCoinless?.(tokenId) !== true) {
14386
- throw new SphereError(
14387
- `Token ${tokenId} is not a spendable coinless holding`,
14388
- "VALIDATION_ERROR"
14389
- );
14414
+ if (this.deps.spendableToken?.(tokenId) !== true) {
14415
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
14390
14416
  }
14391
14417
  const holder = this.deps.ledger.holderOf(tokenId);
14392
14418
  if (holder !== void 0) {
@@ -14587,7 +14613,7 @@ function composeFacadeParts(deps, hooks) {
14587
14613
  const queue = new SpendQueue({
14588
14614
  ledger,
14589
14615
  getPool: (coinId) => view.pool(coinId),
14590
- spendableCoinless: (tokenId) => view.spendableCoinless(tokenId),
14616
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
14591
14617
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14592
14618
  });
14593
14619
  const historyStore = new History({
@@ -14668,12 +14694,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
14668
14694
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
14669
14695
  emit: deps.emit,
14670
14696
  now: deps.now ?? Date.now,
14671
- recordHistory: async ({ transferId, payload, committedAmount }) => {
14697
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
14672
14698
  await historyStore.recordSent({
14673
14699
  transferId,
14674
14700
  // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14675
14701
  // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14676
- ...payload.kind === "coinless" ? { assets: [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14702
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14677
14703
  recipientPubkey: payload.recipient,
14678
14704
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14679
14705
  });
@@ -14888,10 +14914,14 @@ var PaymentsFacade = class {
14888
14914
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14889
14915
  }
14890
14916
  send(request) {
14891
- return this.track(this.sendOutcome(request));
14917
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
14918
+ }
14919
+ sendWholeToken(request) {
14920
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
14892
14921
  }
14922
+ /** NFT-scoped twin: refuses a valued source. */
14893
14923
  sendCoinless(request) {
14894
- return this.track(this.sendCoinlessOutcome(request));
14924
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
14895
14925
  }
14896
14926
  async receive() {
14897
14927
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -14939,13 +14969,6 @@ var PaymentsFacade = class {
14939
14969
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14940
14970
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14941
14971
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14942
- sendOutcome(request) {
14943
- return this.runJob({ kind: "coin", request }, request.amount);
14944
- }
14945
- /** A token spend has no amount; '0' keeps the shortfall arithmetic total-free. */
14946
- sendCoinlessOutcome(request) {
14947
- return this.runJob({ kind: "coinless", request }, "0");
14948
- }
14949
14972
  async runJob(job, amount) {
14950
14973
  const run = {
14951
14974
  amount,
@@ -14990,7 +15013,7 @@ var PaymentsFacade = class {
14990
15013
  const disposition = await this.runAttempt(ctx);
14991
15014
  if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
14992
15015
  if (disposition.kind === "retry-full") {
14993
- if (job.kind === "coinless" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
15016
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
14994
15017
  continue;
14995
15018
  }
14996
15019
  if (disposition.kind === "success") {
@@ -15094,13 +15117,19 @@ var PaymentsFacade = class {
15094
15117
  async planAndMaterialize(recipientPubkey, job, run) {
15095
15118
  const transferId = this.newId();
15096
15119
  run.lastTransferId = transferId;
15097
- if (job.kind === "coinless") {
15098
- const spend2 = this.queue.planCoinless(transferId, job.request.tokenId);
15120
+ if (job.kind === "whole") {
15121
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
15099
15122
  const sourceIds2 = this.markPlanned(transferId, "", spend2);
15100
15123
  try {
15101
- return await materializeCoinlessSpend(
15102
- { engine: this.engine(), storagePort: this.deps.storagePort },
15103
- { transferId, recipientPubkey, request: job.request, sourceIds: sourceIds2 }
15124
+ return await materializeWholeSpend(
15125
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
15126
+ {
15127
+ transferId,
15128
+ recipientPubkey,
15129
+ request: job.request,
15130
+ sourceIds: sourceIds2,
15131
+ requireCoinless: job.requireCoinless
15132
+ }
15104
15133
  );
15105
15134
  } catch (err) {
15106
15135
  this.settleFailure(transferId, "", sourceIds2);