@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.
package/dist/index.d.cts CHANGED
@@ -88,7 +88,7 @@ interface SendRequest {
88
88
  coinId: string;
89
89
  memo?: string;
90
90
  }
91
- interface SendCoinlessRequest {
91
+ interface SendWholeTokenRequest {
92
92
  recipient: string;
93
93
  tokenId: string;
94
94
  memo?: string;
@@ -186,7 +186,9 @@ interface PaymentsV2 {
186
186
  limit?: number;
187
187
  }): Promise<HistoryPage>;
188
188
  send(req: SendRequest): Promise<TransferResult>;
189
- sendCoinless(req: SendCoinlessRequest): Promise<TransferResult>;
189
+ sendWholeToken(req: SendWholeTokenRequest): Promise<TransferResult>;
190
+ /** NFT-scoped twin: refuses a valued source. Connect's `send_nft` routes here. */
191
+ sendCoinless(req: SendWholeTokenRequest): Promise<TransferResult>;
190
192
  mint(coinId: string, amount: bigint): Promise<MintResult>;
191
193
  receive(): Promise<{
192
194
  transfers: IncomingTransfer[];
package/dist/index.d.ts CHANGED
@@ -88,7 +88,7 @@ interface SendRequest {
88
88
  coinId: string;
89
89
  memo?: string;
90
90
  }
91
- interface SendCoinlessRequest {
91
+ interface SendWholeTokenRequest {
92
92
  recipient: string;
93
93
  tokenId: string;
94
94
  memo?: string;
@@ -186,7 +186,9 @@ interface PaymentsV2 {
186
186
  limit?: number;
187
187
  }): Promise<HistoryPage>;
188
188
  send(req: SendRequest): Promise<TransferResult>;
189
- sendCoinless(req: SendCoinlessRequest): Promise<TransferResult>;
189
+ sendWholeToken(req: SendWholeTokenRequest): Promise<TransferResult>;
190
+ /** NFT-scoped twin: refuses a valued source. Connect's `send_nft` routes here. */
191
+ sendCoinless(req: SendWholeTokenRequest): Promise<TransferResult>;
190
192
  mint(coinId: string, amount: bigint): Promise<MintResult>;
191
193
  receive(): Promise<{
192
194
  transfers: IncomingTransfer[];
package/dist/index.js CHANGED
@@ -11821,6 +11821,9 @@ function createMachineStores(kv) {
11821
11821
  }
11822
11822
 
11823
11823
  // modules/payments-v2/machine/payload-view.ts
11824
+ function isWholeIntent(p) {
11825
+ return p.kind === "whole" || p.kind === "coinless";
11826
+ }
11824
11827
  function splitOf(p) {
11825
11828
  return p.kind === "coin" ? p.split : void 0;
11826
11829
  }
@@ -12189,7 +12192,14 @@ var TransferMachine = class {
12189
12192
  try {
12190
12193
  const coinId = coinIdOf(payload);
12191
12194
  const committedAmount = coinId === void 0 ? "0" : await this.settledAmount(engine, coinId, committed);
12192
- await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
12195
+ const committedAssets = coinId === void 0 ? await this.settledAssets(engine, committed) : void 0;
12196
+ await this.deps.recordHistory?.({
12197
+ transferId,
12198
+ payload,
12199
+ phase,
12200
+ committedAmount,
12201
+ ...committedAssets !== void 0 ? { committedAssets } : {}
12202
+ });
12193
12203
  } catch {
12194
12204
  }
12195
12205
  try {
@@ -12203,6 +12213,18 @@ var TransferMachine = class {
12203
12213
  * a journal-rehydrated resume leg has no source token, but every committed
12204
12214
  * leg carries its recipient blob, so the outcome-derived rule is uniform).
12205
12215
  */
12216
+ /** Every asset across the certified recipient blobs — the whole-spend twin of settledAmount. */
12217
+ async settledAssets(engine, committed) {
12218
+ const total = /* @__PURE__ */ new Map();
12219
+ for (const outcome of committed) {
12220
+ if (outcome.recipientBlob === void 0) continue;
12221
+ const token = await engine.decodeToken({ tokenId: "", token: outcome.recipientBlob });
12222
+ for (const asset of engine.readValue(token)?.assets ?? []) {
12223
+ total.set(asset.coinId, (total.get(asset.coinId) ?? 0n) + asset.amount);
12224
+ }
12225
+ }
12226
+ return [...total].map(([coinId, amount]) => ({ coinId, amount: amount.toString() }));
12227
+ }
12206
12228
  async settledAmount(engine, coinId, committed) {
12207
12229
  let total = 0n;
12208
12230
  for (const outcome of committed) {
@@ -12342,7 +12364,7 @@ async function runOne(ctx, job, report) {
12342
12364
  report.failed.push(job.transferId);
12343
12365
  return;
12344
12366
  }
12345
- if (job.payload.kind === "coinless" && !isCoinlessEnvelope(token.valueEnvelope)) {
12367
+ if (isWholeIntent(job.payload) && token.valueEnvelope === "bare_collection") {
12346
12368
  logger.warn(
12347
12369
  "PaymentsV2",
12348
12370
  `resume: token intent ${job.transferId} names a source carrying coin value \u2014 refusing`
@@ -12390,7 +12412,7 @@ function validateCoinPayload(c) {
12390
12412
  }
12391
12413
  return { ...c, kind: "coin" };
12392
12414
  }
12393
- function validateCoinlessPayload(p) {
12415
+ function validateWholePayload(p) {
12394
12416
  if (p.direct?.length !== 1 || typeof p.direct[0] !== "string" || p.direct[0] === "") {
12395
12417
  throw new SphereError(
12396
12418
  "token intent payload must name exactly one source token",
@@ -12400,7 +12422,7 @@ function validateCoinlessPayload(p) {
12400
12422
  if (p.split !== void 0) {
12401
12423
  throw new SphereError("token intent payload cannot carry a split", "VALIDATION_ERROR");
12402
12424
  }
12403
- return { ...p, kind: "coinless", direct: [p.direct[0]] };
12425
+ return { ...p, kind: "whole", direct: [p.direct[0]] };
12404
12426
  }
12405
12427
  function validatePayload(raw) {
12406
12428
  const p = raw !== null && typeof raw === "object" ? raw : null;
@@ -12414,13 +12436,13 @@ function validatePayload(raw) {
12414
12436
  throw new SphereError("intent payload is missing recipient", "VALIDATION_ERROR");
12415
12437
  }
12416
12438
  const kind = p.kind ?? "coin";
12417
- if (kind !== "coin" && kind !== "coinless") {
12439
+ if (kind !== "coin" && kind !== "whole" && kind !== "coinless") {
12418
12440
  throw new SphereError(
12419
12441
  `unsupported intent kind '${String(kind)}' \u2014 not resumable by this client`,
12420
12442
  "VALIDATION_ERROR"
12421
12443
  );
12422
12444
  }
12423
- return kind === "coinless" ? validateCoinlessPayload(p) : validateCoinPayload(p);
12445
+ return kind === "coin" ? validateCoinPayload(p) : validateWholePayload(p);
12424
12446
  }
12425
12447
 
12426
12448
  // modules/payments-v2/convergence.ts
@@ -12633,8 +12655,9 @@ async function openRow(entry, journal, decryptPayload) {
12633
12655
  };
12634
12656
  }
12635
12657
  function subject(payload) {
12636
- if (payload?.kind === "coinless") {
12637
- const tokenId = payload.direct?.[0];
12658
+ const rawKind = payload?.kind;
12659
+ if (rawKind === "whole" || rawKind === "coinless") {
12660
+ const tokenId = payload?.direct?.[0];
12638
12661
  return { coinId: "", amount: "", ...typeof tokenId === "string" ? { tokenId } : {} };
12639
12662
  }
12640
12663
  const coin = payload;
@@ -12707,10 +12730,10 @@ function buildPayload(recipientPubkey, request, spend, spentStates) {
12707
12730
  function messageOf(err) {
12708
12731
  return err instanceof Error ? err.message : String(err);
12709
12732
  }
12710
- function buildCoinlessPayload(recipientPubkey, request, spentStates) {
12733
+ function buildWholePayload(recipientPubkey, request, spentStates) {
12711
12734
  return {
12712
12735
  v: 2,
12713
- kind: "coinless",
12736
+ kind: "whole",
12714
12737
  recipient: recipientPubkey,
12715
12738
  ...request.memo !== void 0 ? { memo: request.memo } : {},
12716
12739
  direct: [request.tokenId],
@@ -12779,8 +12802,102 @@ async function finalizeMint(deps, mintId, token, coinId, amount) {
12779
12802
  deps.refreshView();
12780
12803
  }
12781
12804
 
12782
- // modules/payments-v2/send-coinless.ts
12783
- async function materializeCoinlessSpend(deps, input) {
12805
+ // modules/payments-v2/inventory/presentation.ts
12806
+ function toToken(tokenId, snapshot, asset, registry, flags) {
12807
+ const iconUrl = registry.getIconUrl(asset.coinId);
12808
+ return {
12809
+ id: tokenId,
12810
+ coinId: asset.coinId,
12811
+ symbol: registry.getSymbol(asset.coinId),
12812
+ name: registry.getName(asset.coinId),
12813
+ decimals: registry.getDecimals(asset.coinId),
12814
+ ...iconUrl !== null ? { iconUrl } : {},
12815
+ amount: asset.amount,
12816
+ status: flags.transferring ? "transferring" : "confirmed",
12817
+ createdAt: snapshot.createdAt,
12818
+ updatedAt: snapshot.updatedAt,
12819
+ lazy: true,
12820
+ ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12821
+ };
12822
+ }
12823
+ function aggregateAssets(entries, isTransferring, registry) {
12824
+ const groups = /* @__PURE__ */ new Map();
12825
+ for (const [tokenId, entry] of entries) {
12826
+ const moving = isTransferring(tokenId);
12827
+ for (const asset of entry.assets) {
12828
+ let group = groups.get(asset.coinId);
12829
+ if (!group) {
12830
+ group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12831
+ groups.set(asset.coinId, group);
12832
+ }
12833
+ if (moving) {
12834
+ group.transferring += BigInt(asset.amount);
12835
+ group.transferringCount += 1;
12836
+ } else {
12837
+ group.total += BigInt(asset.amount);
12838
+ group.count += 1;
12839
+ }
12840
+ }
12841
+ }
12842
+ return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12843
+ }
12844
+ function toAsset(coinId, totals, registry) {
12845
+ const iconUrl = registry.getIconUrl(coinId);
12846
+ return {
12847
+ coinId,
12848
+ symbol: registry.getSymbol(coinId),
12849
+ name: registry.getName(coinId),
12850
+ decimals: registry.getDecimals(coinId),
12851
+ ...iconUrl !== null ? { iconUrl } : {},
12852
+ totalAmount: totals.total.toString(),
12853
+ tokenCount: totals.count,
12854
+ confirmedAmount: totals.total.toString(),
12855
+ unconfirmedAmount: "0",
12856
+ confirmedTokenCount: totals.count,
12857
+ unconfirmedTokenCount: 0,
12858
+ transferringTokenCount: totals.transferringCount,
12859
+ transferringAmount: totals.transferring.toString(),
12860
+ priceUsd: null,
12861
+ priceEur: null,
12862
+ change24h: null,
12863
+ fiatValueUsd: null,
12864
+ fiatValueEur: null
12865
+ };
12866
+ }
12867
+ function priceIdOf(registry, coinId) {
12868
+ return registry.getName(coinId).toLowerCase();
12869
+ }
12870
+ async function withPrices(raw, registry, price) {
12871
+ try {
12872
+ const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
12873
+ const quotes = await price.getPrices(ids);
12874
+ return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
12875
+ } catch {
12876
+ return raw;
12877
+ }
12878
+ }
12879
+ function priceOne(asset, quote) {
12880
+ if (!quote) return asset;
12881
+ const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
12882
+ return {
12883
+ ...asset,
12884
+ priceUsd: quote.priceUsd,
12885
+ priceEur: quote.priceEur ?? null,
12886
+ change24h: quote.change24h ?? null,
12887
+ fiatValueUsd: human * quote.priceUsd,
12888
+ fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
12889
+ };
12890
+ }
12891
+ function transferringToken(tokenId, coinId, amount, registry, now) {
12892
+ const snapshot = { assets: [], createdAt: now, updatedAt: now };
12893
+ return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
12894
+ transferring: true,
12895
+ suspectedSpent: false
12896
+ });
12897
+ }
12898
+
12899
+ // modules/payments-v2/send-whole.ts
12900
+ async function materializeWholeSpend(deps, input) {
12784
12901
  const { engine } = deps;
12785
12902
  const tokenId = input.request.tokenId;
12786
12903
  const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
@@ -12788,14 +12905,20 @@ async function materializeCoinlessSpend(deps, input) {
12788
12905
  throw new SphereError(`Selected source ${tokenId} has no blob in storage`, "STORAGE_ERROR");
12789
12906
  }
12790
12907
  const token = await engine.decodeToken({ tokenId, token: bytes });
12791
- if (!isCoinlessEnvelope(token.valueEnvelope)) {
12908
+ if (token.valueEnvelope === "bare_collection") {
12909
+ throw new SphereError(
12910
+ `Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be sent`,
12911
+ "VALIDATION_ERROR"
12912
+ );
12913
+ }
12914
+ if (input.requireCoinless && token.value !== null) {
12792
12915
  throw new SphereError(
12793
- `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use send()`,
12916
+ `Token ${tokenId} carries coin value and cannot be sent with sendCoinless \u2014 use sendWholeToken, which requires coin-transfer authority`,
12794
12917
  "VALIDATION_ERROR"
12795
12918
  );
12796
12919
  }
12797
12920
  const keys = await engine.deliveryKeys(bytes);
12798
- const payload = buildCoinlessPayload(input.recipientPubkey, input.request, {
12921
+ const payload = buildWholePayload(input.recipientPubkey, input.request, {
12799
12922
  [tokenId]: { local: keys.stateHash, protocol: keys.stateHash }
12800
12923
  });
12801
12924
  const plan = {
@@ -12806,7 +12929,10 @@ async function materializeCoinlessSpend(deps, input) {
12806
12929
  sources: /* @__PURE__ */ new Map([[tokenId, token]]),
12807
12930
  ...input.request.memo !== void 0 ? { memo: input.request.memo } : {}
12808
12931
  };
12809
- return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens: [] };
12932
+ const sourceTokens = (token.value?.assets ?? []).map(
12933
+ (asset) => transferringToken(tokenId, asset.coinId, asset.amount, deps.registry, deps.now)
12934
+ );
12935
+ return { transferId: input.transferId, coinId: "", plan, sourceIds: input.sourceIds, sourceTokens };
12810
12936
  }
12811
12937
 
12812
12938
  // modules/payments-v2/send-errors.ts
@@ -12953,100 +13079,6 @@ async function takeSourceBlobs(deps, sourceIds) {
12953
13079
  return blobs;
12954
13080
  }
12955
13081
 
12956
- // modules/payments-v2/inventory/presentation.ts
12957
- function toToken(tokenId, snapshot, asset, registry, flags) {
12958
- const iconUrl = registry.getIconUrl(asset.coinId);
12959
- return {
12960
- id: tokenId,
12961
- coinId: asset.coinId,
12962
- symbol: registry.getSymbol(asset.coinId),
12963
- name: registry.getName(asset.coinId),
12964
- decimals: registry.getDecimals(asset.coinId),
12965
- ...iconUrl !== null ? { iconUrl } : {},
12966
- amount: asset.amount,
12967
- status: flags.transferring ? "transferring" : "confirmed",
12968
- createdAt: snapshot.createdAt,
12969
- updatedAt: snapshot.updatedAt,
12970
- lazy: true,
12971
- ...flags.suspectedSpent ? { suspectedSpent: true } : {}
12972
- };
12973
- }
12974
- function aggregateAssets(entries, isTransferring, registry) {
12975
- const groups = /* @__PURE__ */ new Map();
12976
- for (const [tokenId, entry] of entries) {
12977
- const moving = isTransferring(tokenId);
12978
- for (const asset of entry.assets) {
12979
- let group = groups.get(asset.coinId);
12980
- if (!group) {
12981
- group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
12982
- groups.set(asset.coinId, group);
12983
- }
12984
- if (moving) {
12985
- group.transferring += BigInt(asset.amount);
12986
- group.transferringCount += 1;
12987
- } else {
12988
- group.total += BigInt(asset.amount);
12989
- group.count += 1;
12990
- }
12991
- }
12992
- }
12993
- return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
12994
- }
12995
- function toAsset(coinId, totals, registry) {
12996
- const iconUrl = registry.getIconUrl(coinId);
12997
- return {
12998
- coinId,
12999
- symbol: registry.getSymbol(coinId),
13000
- name: registry.getName(coinId),
13001
- decimals: registry.getDecimals(coinId),
13002
- ...iconUrl !== null ? { iconUrl } : {},
13003
- totalAmount: totals.total.toString(),
13004
- tokenCount: totals.count,
13005
- confirmedAmount: totals.total.toString(),
13006
- unconfirmedAmount: "0",
13007
- confirmedTokenCount: totals.count,
13008
- unconfirmedTokenCount: 0,
13009
- transferringTokenCount: totals.transferringCount,
13010
- transferringAmount: totals.transferring.toString(),
13011
- priceUsd: null,
13012
- priceEur: null,
13013
- change24h: null,
13014
- fiatValueUsd: null,
13015
- fiatValueEur: null
13016
- };
13017
- }
13018
- function priceIdOf(registry, coinId) {
13019
- return registry.getName(coinId).toLowerCase();
13020
- }
13021
- async function withPrices(raw, registry, price) {
13022
- try {
13023
- const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
13024
- const quotes = await price.getPrices(ids);
13025
- return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
13026
- } catch {
13027
- return raw;
13028
- }
13029
- }
13030
- function priceOne(asset, quote) {
13031
- if (!quote) return asset;
13032
- const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
13033
- return {
13034
- ...asset,
13035
- priceUsd: quote.priceUsd,
13036
- priceEur: quote.priceEur ?? null,
13037
- change24h: quote.change24h ?? null,
13038
- fiatValueUsd: human * quote.priceUsd,
13039
- fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
13040
- };
13041
- }
13042
- function transferringToken(tokenId, coinId, amount, registry, now) {
13043
- const snapshot = { assets: [], createdAt: now, updatedAt: now };
13044
- return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
13045
- transferring: true,
13046
- suspectedSpent: false
13047
- });
13048
- }
13049
-
13050
13082
  // modules/payments-v2/history/History.ts
13051
13083
  function wireAssets(assets) {
13052
13084
  return assets.map((a) => ({ coinId: a.coinId, amount: a.amount }));
@@ -13302,10 +13334,10 @@ var InventoryView = class {
13302
13334
  if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
13303
13335
  return !this.suspected.has(stateKey(tokenId, entry.stateHash));
13304
13336
  }
13305
- /** #777: is this NAMED token a spendable COINLESS holding? Never a coin source. */
13306
- spendableCoinless(tokenId) {
13337
+ /** Is this NAMED token spendable whole — coinless or valued (#777)? */
13338
+ spendableToken(tokenId) {
13307
13339
  const entry = this.mirror.get(tokenId);
13308
- return entry !== void 0 && entry.coinless && this.isSpendable(tokenId, entry);
13340
+ return entry !== void 0 && this.isSpendable(tokenId, entry);
13309
13341
  }
13310
13342
  /**
13311
13343
  * Current mirrored state of an ACTIVE token, or undefined when it is unknown
@@ -13678,7 +13710,7 @@ async function screen(deps, engine, entry) {
13678
13710
  tokenId: keys.tokenId,
13679
13711
  stateHash: keys.stateHash,
13680
13712
  assets: toAssetAmounts(token),
13681
- ...isCoinlessEnvelope2(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13713
+ ...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
13682
13714
  }
13683
13715
  };
13684
13716
  }
@@ -13815,9 +13847,6 @@ function claimAck(entry) {
13815
13847
  function rejectAck(entry, reason) {
13816
13848
  return { deliveryId: entry.deliveryId, disposition: "rejected", reason, cursor: entry.cursor };
13817
13849
  }
13818
- function isCoinlessEnvelope2(envelope) {
13819
- return envelope.startsWith("none_");
13820
- }
13821
13850
  function toAssetAmounts(token) {
13822
13851
  return (token.value?.assets ?? []).map((asset) => ({
13823
13852
  coinId: asset.coinId,
@@ -14392,7 +14421,7 @@ var SpendQueue = class {
14392
14421
  * can release, so queueing would block until a timeout on a spend that cannot
14393
14422
  * become possible.
14394
14423
  */
14395
- planCoinless(reservationId, tokenId) {
14424
+ planWhole(reservationId, tokenId) {
14396
14425
  if (this.destroyed) {
14397
14426
  throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
14398
14427
  }
@@ -14400,11 +14429,8 @@ var SpendQueue = class {
14400
14429
  if (unproven !== null) {
14401
14430
  throw new SphereError(`Cannot spend yet: ${unproven}`, "SEND_SYNC_PENDING");
14402
14431
  }
14403
- if (this.deps.spendableCoinless?.(tokenId) !== true) {
14404
- throw new SphereError(
14405
- `Token ${tokenId} is not a spendable coinless holding`,
14406
- "VALIDATION_ERROR"
14407
- );
14432
+ if (this.deps.spendableToken?.(tokenId) !== true) {
14433
+ throw new SphereError(`Token ${tokenId} is not a spendable holding`, "VALIDATION_ERROR");
14408
14434
  }
14409
14435
  const holder = this.deps.ledger.holderOf(tokenId);
14410
14436
  if (holder !== void 0) {
@@ -14605,7 +14631,7 @@ function composeFacadeParts(deps, hooks) {
14605
14631
  const queue = new SpendQueue({
14606
14632
  ledger,
14607
14633
  getPool: (coinId) => view.pool(coinId),
14608
- spendableCoinless: (tokenId) => view.spendableCoinless(tokenId),
14634
+ spendableToken: (tokenId) => view.spendableToken(tokenId),
14609
14635
  ...deps.workBudget !== void 0 ? { workBudget: deps.workBudget } : {}
14610
14636
  });
14611
14637
  const historyStore = new History({
@@ -14686,12 +14712,12 @@ function buildMachineDeps(deps, hooks, historyStore) {
14686
14712
  ...deps.ownNametag !== void 0 ? { ownNametag: deps.ownNametag } : {},
14687
14713
  emit: deps.emit,
14688
14714
  now: deps.now ?? Date.now,
14689
- recordHistory: async ({ transferId, payload, committedAmount }) => {
14715
+ recordHistory: async ({ transferId, payload, committedAmount, committedAssets }) => {
14690
14716
  await historyStore.recordSent({
14691
14717
  transferId,
14692
14718
  // §5.9: the SETTLED amount, never payload.amount — the plan. A token
14693
14719
  // spend moved no coin: `assets: []` + tokenId (wallet-api#151 / §10).
14694
- ...payload.kind === "coinless" ? { assets: [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14720
+ ...isWholeIntent(payload) ? { assets: committedAssets ?? [], tokenId: payload.direct[0] } : { assets: [{ coinId: payload.coinId, amount: committedAmount }] },
14695
14721
  recipientPubkey: payload.recipient,
14696
14722
  ...payload.memo !== void 0 ? { memo: payload.memo } : {}
14697
14723
  });
@@ -14906,10 +14932,14 @@ var PaymentsFacade = class {
14906
14932
  return { queue: this.queue, view: this.view, storagePort: this.deps.storagePort, cache: this.prewarmed };
14907
14933
  }
14908
14934
  send(request) {
14909
- return this.track(this.sendOutcome(request));
14935
+ return this.track(this.runJob({ kind: "coin", request }, request.amount));
14936
+ }
14937
+ sendWholeToken(request) {
14938
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: false }, "0"));
14910
14939
  }
14940
+ /** NFT-scoped twin: refuses a valued source. */
14911
14941
  sendCoinless(request) {
14912
- return this.track(this.sendCoinlessOutcome(request));
14942
+ return this.track(this.runJob({ kind: "whole", request, requireCoinless: true }, "0"));
14913
14943
  }
14914
14944
  async receive() {
14915
14945
  const transfers = await this.track(this.receiveLoop.drainOnce());
@@ -14957,13 +14987,6 @@ var PaymentsFacade = class {
14957
14987
  // ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
14958
14988
  /** The ONE place a send() outcome is shaped: success emits in finishSend, a
14959
14989
  * CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
14960
- sendOutcome(request) {
14961
- return this.runJob({ kind: "coin", request }, request.amount);
14962
- }
14963
- /** A token spend has no amount; '0' keeps the shortfall arithmetic total-free. */
14964
- sendCoinlessOutcome(request) {
14965
- return this.runJob({ kind: "coinless", request }, "0");
14966
- }
14967
14990
  async runJob(job, amount) {
14968
14991
  const run = {
14969
14992
  amount,
@@ -15008,7 +15031,7 @@ var PaymentsFacade = class {
15008
15031
  const disposition = await this.runAttempt(ctx);
15009
15032
  if (disposition.kind === "rethrow") throw partialize(disposition.error, run);
15010
15033
  if (disposition.kind === "retry-full") {
15011
- if (job.kind === "coinless" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
15034
+ if (job.kind === "whole" || attempt >= MAX_RESELECT) throw partialize(disposition.error, run);
15012
15035
  continue;
15013
15036
  }
15014
15037
  if (disposition.kind === "success") {
@@ -15112,13 +15135,19 @@ var PaymentsFacade = class {
15112
15135
  async planAndMaterialize(recipientPubkey, job, run) {
15113
15136
  const transferId = this.newId();
15114
15137
  run.lastTransferId = transferId;
15115
- if (job.kind === "coinless") {
15116
- const spend2 = this.queue.planCoinless(transferId, job.request.tokenId);
15138
+ if (job.kind === "whole") {
15139
+ const spend2 = this.queue.planWhole(transferId, job.request.tokenId);
15117
15140
  const sourceIds2 = this.markPlanned(transferId, "", spend2);
15118
15141
  try {
15119
- return await materializeCoinlessSpend(
15120
- { engine: this.engine(), storagePort: this.deps.storagePort },
15121
- { transferId, recipientPubkey, request: job.request, sourceIds: sourceIds2 }
15142
+ return await materializeWholeSpend(
15143
+ { engine: this.engine(), storagePort: this.deps.storagePort, registry: this.deps.registry, now: this.nowMs() },
15144
+ {
15145
+ transferId,
15146
+ recipientPubkey,
15147
+ request: job.request,
15148
+ sourceIds: sourceIds2,
15149
+ requireCoinless: job.requireCoinless
15150
+ }
15122
15151
  );
15123
15152
  } catch (err) {
15124
15153
  this.settleFailure(transferId, "", sourceIds2);