@unicitylabs/sphere-sdk 0.14.0-dev.6 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +1 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +416 -223
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +111 -108
- package/dist/core/index.d.ts +111 -108
- package/dist/core/index.js +416 -223
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +1 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +1 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/wallet-api-v2/index.cjs +38 -5
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +23 -2
- package/dist/impl/wallet-api-v2/index.d.ts +23 -2
- package/dist/impl/wallet-api-v2/index.js +38 -5
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +416 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -109
- package/dist/index.d.ts +112 -109
- package/dist/index.js +416 -223
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +340 -199
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +68 -19
- package/dist/modules/payments-v2/index.d.ts +68 -19
- package/dist/modules/payments-v2/index.js +339 -199
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.js
CHANGED
|
@@ -10584,14 +10584,14 @@ var IntentResume = class {
|
|
|
10584
10584
|
}
|
|
10585
10585
|
let changeOutput = null;
|
|
10586
10586
|
if (payload.split) {
|
|
10587
|
-
const
|
|
10587
|
+
const splitOpIndex2 = payload.direct.length;
|
|
10588
10588
|
let splitDelivered = false;
|
|
10589
10589
|
{
|
|
10590
10590
|
const source = await engine.decodeToken(await provider.getToken(payload.split.tokenId));
|
|
10591
10591
|
const outcome = await this.host.certifyOperation(
|
|
10592
10592
|
{
|
|
10593
10593
|
kind: "split",
|
|
10594
|
-
opIndex:
|
|
10594
|
+
opIndex: splitOpIndex2,
|
|
10595
10595
|
uiTokenId: `v2_${payload.split.tokenId}`,
|
|
10596
10596
|
genesisId: payload.split.tokenId,
|
|
10597
10597
|
sourceSdkData: "",
|
|
@@ -20699,6 +20699,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20699
20699
|
const unsubHistory = deps.on(
|
|
20700
20700
|
"history:updated",
|
|
20701
20701
|
(entry) => {
|
|
20702
|
+
if (!("dedupKey" in entry)) return;
|
|
20702
20703
|
this._handleHistoryUpdated(entry).catch((err) => {
|
|
20703
20704
|
logger.warn(LOG_TAG2, "Error handling history:updated event:", err);
|
|
20704
20705
|
});
|
|
@@ -25968,7 +25969,10 @@ var STORE_KEYS = {
|
|
|
25968
25969
|
shortfalls: "shortfalls",
|
|
25969
25970
|
settlingLinks: "settling",
|
|
25970
25971
|
streamCursor: (s) => `cursor:${s}`,
|
|
25971
|
-
epochLatch: "epoch-latch"
|
|
25972
|
+
epochLatch: "epoch-latch",
|
|
25973
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
25974
|
+
suspectedSpent: "suspected-spent",
|
|
25975
|
+
knownSpends: "known-spends"
|
|
25972
25976
|
};
|
|
25973
25977
|
|
|
25974
25978
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -26218,7 +26222,7 @@ var TransferMachine = class {
|
|
|
26218
26222
|
}
|
|
26219
26223
|
if (cls === null) {
|
|
26220
26224
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
26221
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
26225
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
26222
26226
|
return { committed, deliveryPending };
|
|
26223
26227
|
}
|
|
26224
26228
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -26244,10 +26248,10 @@ var TransferMachine = class {
|
|
|
26244
26248
|
}
|
|
26245
26249
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
26246
26250
|
if (conflicts.length > 0) {
|
|
26247
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
26251
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
26248
26252
|
}
|
|
26249
26253
|
await this.completeIntent(r.transferId, r.payload);
|
|
26250
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26254
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26251
26255
|
return { deliveryPending, partial: null };
|
|
26252
26256
|
}
|
|
26253
26257
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -26429,7 +26433,7 @@ var TransferMachine = class {
|
|
|
26429
26433
|
}
|
|
26430
26434
|
return added;
|
|
26431
26435
|
}
|
|
26432
|
-
async settlePartial(r, committed, conflicts) {
|
|
26436
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
26433
26437
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
26434
26438
|
const entry = {
|
|
26435
26439
|
transferId: r.transferId,
|
|
@@ -26441,26 +26445,27 @@ var TransferMachine = class {
|
|
|
26441
26445
|
};
|
|
26442
26446
|
await this.stores.shortfalls.upsert(entry);
|
|
26443
26447
|
await this.completeIntent(r.transferId, r.payload);
|
|
26444
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26448
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26445
26449
|
return entry;
|
|
26446
26450
|
}
|
|
26447
|
-
closeTail(transferId, payload) {
|
|
26451
|
+
closeTail(engine, transferId, payload, committed) {
|
|
26448
26452
|
void (async () => {
|
|
26449
26453
|
try {
|
|
26450
26454
|
await this.completeIntent(transferId, payload);
|
|
26451
26455
|
} catch {
|
|
26452
26456
|
return;
|
|
26453
26457
|
}
|
|
26454
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
26458
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
26455
26459
|
})();
|
|
26456
26460
|
}
|
|
26457
26461
|
async completeIntent(transferId, payload) {
|
|
26458
26462
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
26459
26463
|
await this.deps.intents.complete(transferId, signature);
|
|
26460
26464
|
}
|
|
26461
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
26465
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
26462
26466
|
try {
|
|
26463
|
-
await this.
|
|
26467
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
26468
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
26464
26469
|
} catch {
|
|
26465
26470
|
}
|
|
26466
26471
|
try {
|
|
@@ -26468,6 +26473,26 @@ var TransferMachine = class {
|
|
|
26468
26473
|
} catch {
|
|
26469
26474
|
}
|
|
26470
26475
|
}
|
|
26476
|
+
/**
|
|
26477
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
26478
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
26479
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
26480
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
26481
|
+
*/
|
|
26482
|
+
async settledAmount(engine, coinId, committed) {
|
|
26483
|
+
let total = 0n;
|
|
26484
|
+
for (const outcome of committed) {
|
|
26485
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
26486
|
+
const token = await engine.decodeToken({
|
|
26487
|
+
v: TOKEN_BLOB_VERSION,
|
|
26488
|
+
network: 0,
|
|
26489
|
+
tokenId: "",
|
|
26490
|
+
token: outcome.recipientBlob
|
|
26491
|
+
});
|
|
26492
|
+
total += engine.balanceOf(token, coinId);
|
|
26493
|
+
}
|
|
26494
|
+
return total.toString();
|
|
26495
|
+
}
|
|
26471
26496
|
async disposeOnFailure(transferId, committed, cls) {
|
|
26472
26497
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
26473
26498
|
try {
|
|
@@ -26492,7 +26517,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
26492
26517
|
}
|
|
26493
26518
|
|
|
26494
26519
|
// modules/payments-v2/machine/resume.ts
|
|
26495
|
-
async function resumeAll(deps) {
|
|
26520
|
+
async function resumeAll(deps, options = {}) {
|
|
26496
26521
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
26497
26522
|
const stores = createMachineStores(deps.kv);
|
|
26498
26523
|
const open = await deps.intents.listOpen();
|
|
@@ -26502,7 +26527,12 @@ async function resumeAll(deps) {
|
|
|
26502
26527
|
} catch {
|
|
26503
26528
|
return report;
|
|
26504
26529
|
}
|
|
26505
|
-
await
|
|
26530
|
+
await dropStaleBackstops(
|
|
26531
|
+
stores,
|
|
26532
|
+
locals,
|
|
26533
|
+
new Set(open.map((i) => i.transferId)),
|
|
26534
|
+
options.isLocallyActive ?? (() => false)
|
|
26535
|
+
);
|
|
26506
26536
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
26507
26537
|
const ctx = {
|
|
26508
26538
|
deps,
|
|
@@ -26515,11 +26545,16 @@ async function resumeAll(deps) {
|
|
|
26515
26545
|
}
|
|
26516
26546
|
return report;
|
|
26517
26547
|
}
|
|
26518
|
-
async function
|
|
26548
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
26519
26549
|
for (const entry of locals.values()) {
|
|
26520
|
-
if (
|
|
26550
|
+
if (openIds.has(entry.transferId)) continue;
|
|
26551
|
+
if (entry.disposition !== "open") {
|
|
26521
26552
|
await stores.backstop.removeByKey(entry.transferId);
|
|
26553
|
+
continue;
|
|
26522
26554
|
}
|
|
26555
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
26556
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
26557
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
26523
26558
|
}
|
|
26524
26559
|
}
|
|
26525
26560
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -26727,7 +26762,9 @@ var Converger = class {
|
|
|
26727
26762
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
26728
26763
|
}
|
|
26729
26764
|
async resumePass() {
|
|
26730
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
26765
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
26766
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
26767
|
+
});
|
|
26731
26768
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
26732
26769
|
await this.deps.reconcile({
|
|
26733
26770
|
resumed: report.resumed,
|
|
@@ -26843,6 +26880,136 @@ function distinctOpCount(entries) {
|
|
|
26843
26880
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
26844
26881
|
}
|
|
26845
26882
|
|
|
26883
|
+
// modules/payments-v2/restore.ts
|
|
26884
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
26885
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
26886
|
+
async function reseedAndReset(deps) {
|
|
26887
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
26888
|
+
if (entry.disposition !== "open") continue;
|
|
26889
|
+
try {
|
|
26890
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
26891
|
+
if (entry.requiresSeedClose) {
|
|
26892
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
26893
|
+
}
|
|
26894
|
+
} catch (err) {
|
|
26895
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
26896
|
+
}
|
|
26897
|
+
}
|
|
26898
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
26899
|
+
await deps.fullRePull();
|
|
26900
|
+
}
|
|
26901
|
+
async function splitOpIndex(deps, envelope) {
|
|
26902
|
+
const payload = await deps.decryptPayload(envelope);
|
|
26903
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
26904
|
+
}
|
|
26905
|
+
|
|
26906
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
26907
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26908
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26909
|
+
return {
|
|
26910
|
+
id: tokenId,
|
|
26911
|
+
coinId: asset.coinId,
|
|
26912
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
26913
|
+
name: registry.getName(asset.coinId),
|
|
26914
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
26915
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
26916
|
+
amount: asset.amount,
|
|
26917
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
26918
|
+
createdAt: snapshot.createdAt,
|
|
26919
|
+
updatedAt: snapshot.updatedAt,
|
|
26920
|
+
lazy: true,
|
|
26921
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
26922
|
+
};
|
|
26923
|
+
}
|
|
26924
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
26925
|
+
const groups = /* @__PURE__ */ new Map();
|
|
26926
|
+
for (const [tokenId, entry] of entries) {
|
|
26927
|
+
const moving = isTransferring(tokenId);
|
|
26928
|
+
for (const asset of entry.assets) {
|
|
26929
|
+
let group = groups.get(asset.coinId);
|
|
26930
|
+
if (!group) {
|
|
26931
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
26932
|
+
groups.set(asset.coinId, group);
|
|
26933
|
+
}
|
|
26934
|
+
if (moving) {
|
|
26935
|
+
group.transferring += BigInt(asset.amount);
|
|
26936
|
+
group.transferringCount += 1;
|
|
26937
|
+
} else {
|
|
26938
|
+
group.total += BigInt(asset.amount);
|
|
26939
|
+
group.count += 1;
|
|
26940
|
+
}
|
|
26941
|
+
}
|
|
26942
|
+
}
|
|
26943
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
26944
|
+
}
|
|
26945
|
+
function toAsset(coinId, totals, registry) {
|
|
26946
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
26947
|
+
return {
|
|
26948
|
+
coinId,
|
|
26949
|
+
symbol: registry.getSymbol(coinId),
|
|
26950
|
+
name: registry.getName(coinId),
|
|
26951
|
+
decimals: registry.getDecimals(coinId),
|
|
26952
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
26953
|
+
totalAmount: totals.total.toString(),
|
|
26954
|
+
tokenCount: totals.count,
|
|
26955
|
+
confirmedAmount: totals.total.toString(),
|
|
26956
|
+
unconfirmedAmount: "0",
|
|
26957
|
+
confirmedTokenCount: totals.count,
|
|
26958
|
+
unconfirmedTokenCount: 0,
|
|
26959
|
+
transferringTokenCount: totals.transferringCount,
|
|
26960
|
+
transferringAmount: totals.transferring.toString(),
|
|
26961
|
+
priceUsd: null,
|
|
26962
|
+
priceEur: null,
|
|
26963
|
+
change24h: null,
|
|
26964
|
+
fiatValueUsd: null,
|
|
26965
|
+
fiatValueEur: null
|
|
26966
|
+
};
|
|
26967
|
+
}
|
|
26968
|
+
async function withPrices(raw, registry, price) {
|
|
26969
|
+
try {
|
|
26970
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
26971
|
+
const quotes = await price.getPrices(names);
|
|
26972
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
26973
|
+
} catch {
|
|
26974
|
+
return raw;
|
|
26975
|
+
}
|
|
26976
|
+
}
|
|
26977
|
+
function priceOne(asset, quote) {
|
|
26978
|
+
if (!quote) return asset;
|
|
26979
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
26980
|
+
return {
|
|
26981
|
+
...asset,
|
|
26982
|
+
priceUsd: quote.priceUsd,
|
|
26983
|
+
priceEur: quote.priceEur ?? null,
|
|
26984
|
+
change24h: quote.change24h ?? null,
|
|
26985
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
26986
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
26987
|
+
};
|
|
26988
|
+
}
|
|
26989
|
+
|
|
26990
|
+
// modules/payments-v2/machine/payload.ts
|
|
26991
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
26992
|
+
return {
|
|
26993
|
+
v: 2,
|
|
26994
|
+
recipient: recipientPubkey,
|
|
26995
|
+
coinId: request.coinId,
|
|
26996
|
+
amount: request.amount,
|
|
26997
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
26998
|
+
direct: [...spend.plan.direct],
|
|
26999
|
+
...spend.plan.split !== void 0 ? {
|
|
27000
|
+
split: {
|
|
27001
|
+
tokenId: spend.plan.split.tokenId,
|
|
27002
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
27003
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
27004
|
+
}
|
|
27005
|
+
} : {},
|
|
27006
|
+
spentStates
|
|
27007
|
+
};
|
|
27008
|
+
}
|
|
27009
|
+
function messageOf(err) {
|
|
27010
|
+
return err instanceof Error ? err.message : String(err);
|
|
27011
|
+
}
|
|
27012
|
+
|
|
26846
27013
|
// modules/payments-v2/history/History.ts
|
|
26847
27014
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
26848
27015
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -26901,13 +27068,14 @@ var History = class {
|
|
|
26901
27068
|
}
|
|
26902
27069
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
26903
27070
|
async post(build) {
|
|
27071
|
+
const record = build();
|
|
26904
27072
|
try {
|
|
26905
|
-
await this.deps.client.postHistory([
|
|
27073
|
+
await this.deps.client.postHistory([record]);
|
|
26906
27074
|
} catch (err) {
|
|
26907
27075
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
26908
27076
|
return;
|
|
26909
27077
|
}
|
|
26910
|
-
this.deps.emit("history:updated");
|
|
27078
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
26911
27079
|
}
|
|
26912
27080
|
ts(timestamp) {
|
|
26913
27081
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -26964,93 +27132,9 @@ var History = class {
|
|
|
26964
27132
|
}
|
|
26965
27133
|
};
|
|
26966
27134
|
|
|
26967
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
26968
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26969
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26970
|
-
return {
|
|
26971
|
-
id: tokenId,
|
|
26972
|
-
coinId: asset.coinId,
|
|
26973
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
26974
|
-
name: registry.getName(asset.coinId),
|
|
26975
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
26976
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
26977
|
-
amount: asset.amount,
|
|
26978
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
26979
|
-
createdAt: snapshot.createdAt,
|
|
26980
|
-
updatedAt: snapshot.updatedAt,
|
|
26981
|
-
lazy: true,
|
|
26982
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
26983
|
-
};
|
|
26984
|
-
}
|
|
26985
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
26986
|
-
const groups = /* @__PURE__ */ new Map();
|
|
26987
|
-
for (const [tokenId, entry] of entries) {
|
|
26988
|
-
const moving = isTransferring(tokenId);
|
|
26989
|
-
for (const asset of entry.assets) {
|
|
26990
|
-
let group = groups.get(asset.coinId);
|
|
26991
|
-
if (!group) {
|
|
26992
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
26993
|
-
groups.set(asset.coinId, group);
|
|
26994
|
-
}
|
|
26995
|
-
if (moving) {
|
|
26996
|
-
group.transferring += BigInt(asset.amount);
|
|
26997
|
-
group.transferringCount += 1;
|
|
26998
|
-
} else {
|
|
26999
|
-
group.total += BigInt(asset.amount);
|
|
27000
|
-
group.count += 1;
|
|
27001
|
-
}
|
|
27002
|
-
}
|
|
27003
|
-
}
|
|
27004
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
27005
|
-
}
|
|
27006
|
-
function toAsset(coinId, totals, registry) {
|
|
27007
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
27008
|
-
return {
|
|
27009
|
-
coinId,
|
|
27010
|
-
symbol: registry.getSymbol(coinId),
|
|
27011
|
-
name: registry.getName(coinId),
|
|
27012
|
-
decimals: registry.getDecimals(coinId),
|
|
27013
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
27014
|
-
totalAmount: totals.total.toString(),
|
|
27015
|
-
tokenCount: totals.count,
|
|
27016
|
-
confirmedAmount: totals.total.toString(),
|
|
27017
|
-
unconfirmedAmount: "0",
|
|
27018
|
-
confirmedTokenCount: totals.count,
|
|
27019
|
-
unconfirmedTokenCount: 0,
|
|
27020
|
-
transferringTokenCount: totals.transferringCount,
|
|
27021
|
-
transferringAmount: totals.transferring.toString(),
|
|
27022
|
-
priceUsd: null,
|
|
27023
|
-
priceEur: null,
|
|
27024
|
-
change24h: null,
|
|
27025
|
-
fiatValueUsd: null,
|
|
27026
|
-
fiatValueEur: null
|
|
27027
|
-
};
|
|
27028
|
-
}
|
|
27029
|
-
async function withPrices(raw, registry, price) {
|
|
27030
|
-
try {
|
|
27031
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
27032
|
-
const quotes = await price.getPrices(names);
|
|
27033
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
27034
|
-
} catch {
|
|
27035
|
-
return raw;
|
|
27036
|
-
}
|
|
27037
|
-
}
|
|
27038
|
-
function priceOne(asset, quote) {
|
|
27039
|
-
if (!quote) return asset;
|
|
27040
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
27041
|
-
return {
|
|
27042
|
-
...asset,
|
|
27043
|
-
priceUsd: quote.priceUsd,
|
|
27044
|
-
priceEur: quote.priceEur ?? null,
|
|
27045
|
-
change24h: quote.change24h ?? null,
|
|
27046
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
27047
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
27048
|
-
};
|
|
27049
|
-
}
|
|
27050
|
-
|
|
27051
27135
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
27052
|
-
var SUSPECTED_KEY =
|
|
27053
|
-
var KNOWN_SPENDS_KEY =
|
|
27136
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
27137
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
27054
27138
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
27055
27139
|
function stateKey(tokenId, stateHash) {
|
|
27056
27140
|
return `${tokenId}:${stateHash}`;
|
|
@@ -27354,19 +27438,24 @@ var Receive = class {
|
|
|
27354
27438
|
const engine = deps.engine();
|
|
27355
27439
|
const stored = [];
|
|
27356
27440
|
const pending = [];
|
|
27357
|
-
|
|
27441
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
27358
27442
|
try {
|
|
27359
|
-
epoch = deps.syncEpoch();
|
|
27360
27443
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
27361
|
-
|
|
27362
|
-
for
|
|
27363
|
-
await
|
|
27364
|
-
|
|
27444
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
27445
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
27446
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
27447
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
27448
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
27449
|
+
}
|
|
27450
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
27451
|
+
const served = deps.delivery.incomingEpoch();
|
|
27452
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
27453
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
27454
|
+
basis = null;
|
|
27365
27455
|
}
|
|
27366
|
-
await flushAcks(deps, pending, epoch);
|
|
27367
27456
|
} catch (err) {
|
|
27368
27457
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
27369
|
-
await flushAcks(deps, pending,
|
|
27458
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
27370
27459
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
27371
27460
|
});
|
|
27372
27461
|
}
|
|
@@ -27439,7 +27528,7 @@ async function announce(deps, entry, record) {
|
|
|
27439
27528
|
receivedAt
|
|
27440
27529
|
};
|
|
27441
27530
|
}
|
|
27442
|
-
async function flushAcks(deps, pending,
|
|
27531
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
27443
27532
|
let lastAcked = null;
|
|
27444
27533
|
try {
|
|
27445
27534
|
while (pending.length > 0) {
|
|
@@ -27450,7 +27539,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
27450
27539
|
}
|
|
27451
27540
|
} finally {
|
|
27452
27541
|
if (lastAcked !== null) {
|
|
27453
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
27542
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
27454
27543
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
27455
27544
|
}
|
|
27456
27545
|
}
|
|
@@ -27551,9 +27640,19 @@ var Requests = class {
|
|
|
27551
27640
|
}
|
|
27552
27641
|
});
|
|
27553
27642
|
}
|
|
27643
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
27554
27644
|
writeLink(requestId, transferId, committed) {
|
|
27555
27645
|
return this.mutateJournal((m) => {
|
|
27556
|
-
|
|
27646
|
+
const prev = m.get(requestId);
|
|
27647
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
27648
|
+
const next = {
|
|
27649
|
+
requestId,
|
|
27650
|
+
transferId,
|
|
27651
|
+
committed: committed || same && prev.committed,
|
|
27652
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
27653
|
+
};
|
|
27654
|
+
if (same && prev.committed === next.committed) return false;
|
|
27655
|
+
m.set(requestId, next);
|
|
27557
27656
|
return true;
|
|
27558
27657
|
});
|
|
27559
27658
|
}
|
|
@@ -27706,23 +27805,46 @@ var Requests = class {
|
|
|
27706
27805
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
27707
27806
|
if (transferId !== void 0) {
|
|
27708
27807
|
const committed = error instanceof PartialSendConflictError;
|
|
27709
|
-
await this.
|
|
27710
|
-
this.setStatus(id, "settling");
|
|
27808
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
27711
27809
|
} else {
|
|
27712
27810
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
27713
27811
|
this.setStatus(id, "paid");
|
|
27714
27812
|
}
|
|
27715
27813
|
} else {
|
|
27716
|
-
await this.
|
|
27717
|
-
this.setStatus(id, "pending");
|
|
27814
|
+
await this.revertPayable(id);
|
|
27718
27815
|
}
|
|
27719
27816
|
throw error;
|
|
27720
27817
|
}
|
|
27721
|
-
this.
|
|
27722
|
-
await this.respondPaid(id, result.id);
|
|
27723
|
-
await this.clearLink(id);
|
|
27818
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
27724
27819
|
return result;
|
|
27725
27820
|
}
|
|
27821
|
+
/**
|
|
27822
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
27823
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
27824
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
27825
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
27826
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
27827
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
27828
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
27829
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
27830
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
27831
|
+
*/
|
|
27832
|
+
async settle(requestId, transferId, opts) {
|
|
27833
|
+
let journalDown = false;
|
|
27834
|
+
try {
|
|
27835
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
27836
|
+
} catch (err) {
|
|
27837
|
+
if (!opts.respond) throw err;
|
|
27838
|
+
journalDown = true;
|
|
27839
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
27840
|
+
}
|
|
27841
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
27842
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
27843
|
+
return;
|
|
27844
|
+
}
|
|
27845
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
27846
|
+
this.setStatus(requestId, "paid");
|
|
27847
|
+
}
|
|
27726
27848
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
27727
27849
|
async respondPaid(id, transferId) {
|
|
27728
27850
|
try {
|
|
@@ -27758,32 +27880,32 @@ var Requests = class {
|
|
|
27758
27880
|
let aborted = null;
|
|
27759
27881
|
for (const [requestId, link] of [...this.journal]) {
|
|
27760
27882
|
if (link.committed || resumed.has(link.transferId)) {
|
|
27761
|
-
await this.resolvePaid(requestId, link
|
|
27883
|
+
await this.resolvePaid(requestId, link);
|
|
27762
27884
|
} else if (conflicted.has(link.transferId)) {
|
|
27763
27885
|
await this.revertPayable(requestId);
|
|
27764
27886
|
} else if (open.has(link.transferId)) {
|
|
27765
27887
|
} else {
|
|
27766
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
27888
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
27767
27889
|
}
|
|
27768
27890
|
}
|
|
27769
27891
|
}
|
|
27770
27892
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
27771
27893
|
// a probe failure defers the link to the next session.
|
|
27772
|
-
async reconcileUnaccounted(requestId,
|
|
27894
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
27773
27895
|
try {
|
|
27774
27896
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
27775
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
27776
|
-
else await this.resolvePaid(requestId,
|
|
27897
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
27898
|
+
else await this.resolvePaid(requestId, link);
|
|
27777
27899
|
} catch (err) {
|
|
27778
27900
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
27779
27901
|
}
|
|
27780
27902
|
return aborted;
|
|
27781
27903
|
}
|
|
27782
|
-
|
|
27783
|
-
|
|
27784
|
-
|
|
27785
|
-
this.setStatus(requestId, "paid");
|
|
27904
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
27905
|
+
resolvePaid(requestId, link) {
|
|
27906
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
27786
27907
|
}
|
|
27908
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
27787
27909
|
async revertPayable(requestId) {
|
|
27788
27910
|
await this.clearLink(requestId);
|
|
27789
27911
|
this.setStatus(requestId, "pending");
|
|
@@ -28107,11 +28229,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
28107
28229
|
client: deps.client,
|
|
28108
28230
|
fieldKey: deps.fieldKey,
|
|
28109
28231
|
registry: deps.registry,
|
|
28110
|
-
emit: (event) => deps.emit(event,
|
|
28232
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
28111
28233
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
28112
28234
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
28113
28235
|
});
|
|
28114
28236
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
28237
|
+
const machineStores = createMachineStores(deps.kv);
|
|
28115
28238
|
const heldStates = /* @__PURE__ */ new Map();
|
|
28116
28239
|
return {
|
|
28117
28240
|
ownPubkeyBytes,
|
|
@@ -28119,12 +28242,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
28119
28242
|
ledger,
|
|
28120
28243
|
queue,
|
|
28121
28244
|
historyStore,
|
|
28122
|
-
machineStores
|
|
28245
|
+
machineStores,
|
|
28123
28246
|
machineDeps,
|
|
28124
28247
|
machine: new TransferMachine(machineDeps),
|
|
28125
28248
|
heldStates,
|
|
28126
28249
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
28127
|
-
requests: buildRequests(deps, hooks)
|
|
28250
|
+
requests: buildRequests(deps, hooks),
|
|
28251
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
28252
|
+
};
|
|
28253
|
+
}
|
|
28254
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
28255
|
+
return {
|
|
28256
|
+
stores: machineStores,
|
|
28257
|
+
kv: deps.kv,
|
|
28258
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
28259
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
28260
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
28261
|
+
fullRePull: () => view.onEpochReset(),
|
|
28262
|
+
attention: (transferId, code, detail) => {
|
|
28263
|
+
deps.emit(
|
|
28264
|
+
"transfer:attention",
|
|
28265
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28266
|
+
);
|
|
28267
|
+
}
|
|
28128
28268
|
};
|
|
28129
28269
|
}
|
|
28130
28270
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -28146,11 +28286,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
28146
28286
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
28147
28287
|
emit: deps.emit,
|
|
28148
28288
|
now: deps.now ?? Date.now,
|
|
28149
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
28289
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
28150
28290
|
await historyStore.recordSent({
|
|
28151
28291
|
transferId,
|
|
28152
28292
|
coinId: payload.coinId,
|
|
28153
|
-
|
|
28293
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
28294
|
+
// recipient blobs), never payload.amount — the plan.
|
|
28295
|
+
amount: committedAmount,
|
|
28154
28296
|
recipientPubkey: payload.recipient,
|
|
28155
28297
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
28156
28298
|
});
|
|
@@ -28177,7 +28319,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
28177
28319
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28178
28320
|
);
|
|
28179
28321
|
},
|
|
28180
|
-
syncEpoch: deps.syncEpoch
|
|
28322
|
+
syncEpoch: deps.syncEpoch,
|
|
28181
28323
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
28182
28324
|
});
|
|
28183
28325
|
}
|
|
@@ -28230,6 +28372,7 @@ var PaymentsFacade = class {
|
|
|
28230
28372
|
this.receiveLoop = parts.receiveLoop;
|
|
28231
28373
|
this.heldStates = parts.heldStates;
|
|
28232
28374
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
28375
|
+
this.restoreDeps = parts.restoreDeps;
|
|
28233
28376
|
this.requests = parts.requests;
|
|
28234
28377
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
28235
28378
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -28258,6 +28401,7 @@ var PaymentsFacade = class {
|
|
|
28258
28401
|
receiveLoop;
|
|
28259
28402
|
heldStates;
|
|
28260
28403
|
ownPubkeyBytes;
|
|
28404
|
+
restoreDeps;
|
|
28261
28405
|
requests;
|
|
28262
28406
|
currentEngine = null;
|
|
28263
28407
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -28268,6 +28412,8 @@ var PaymentsFacade = class {
|
|
|
28268
28412
|
converger;
|
|
28269
28413
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
28270
28414
|
resumeFlight = new SingleFlight();
|
|
28415
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
28416
|
+
passChain = new SerialChain();
|
|
28271
28417
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
28272
28418
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
28273
28419
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -28275,8 +28421,8 @@ var PaymentsFacade = class {
|
|
|
28275
28421
|
if (this.started) return;
|
|
28276
28422
|
this.started = true;
|
|
28277
28423
|
this.heartbeat.start();
|
|
28278
|
-
await this.deps.session.start();
|
|
28279
28424
|
this.unsubscribers.push(
|
|
28425
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
28280
28426
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
28281
28427
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
28282
28428
|
}),
|
|
@@ -28291,6 +28437,7 @@ var PaymentsFacade = class {
|
|
|
28291
28437
|
if (status === "connected") this.heartbeat.recovered();
|
|
28292
28438
|
});
|
|
28293
28439
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
28440
|
+
await this.deps.session.start();
|
|
28294
28441
|
this.trackTail(this.runConvergencePass());
|
|
28295
28442
|
this.trackTail(this.seedHeldStates());
|
|
28296
28443
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -28331,7 +28478,7 @@ var PaymentsFacade = class {
|
|
|
28331
28478
|
}
|
|
28332
28479
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
28333
28480
|
send(request) {
|
|
28334
|
-
return this.track(this.
|
|
28481
|
+
return this.track(this.sendOutcome(request));
|
|
28335
28482
|
}
|
|
28336
28483
|
async receive() {
|
|
28337
28484
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -28346,20 +28493,27 @@ var PaymentsFacade = class {
|
|
|
28346
28493
|
if (!this.started) return;
|
|
28347
28494
|
await this.track(this.runConvergencePass());
|
|
28348
28495
|
}
|
|
28496
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
28497
|
+
async handleEpochChange(_newEpoch) {
|
|
28498
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
28499
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
28500
|
+
}
|
|
28349
28501
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
28350
28502
|
runConvergencePass() {
|
|
28351
|
-
return this.resumeFlight.run(
|
|
28352
|
-
|
|
28353
|
-
|
|
28354
|
-
|
|
28355
|
-
|
|
28503
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
28504
|
+
}
|
|
28505
|
+
async convergeBody() {
|
|
28506
|
+
const outcome = await this.converger.convergeOnce();
|
|
28507
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
28508
|
+
this.heartbeat.settle(outcome, pending);
|
|
28356
28509
|
}
|
|
28357
28510
|
nowMs() {
|
|
28358
28511
|
return (this.deps.now ?? Date.now)();
|
|
28359
28512
|
}
|
|
28360
28513
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
28361
|
-
|
|
28362
|
-
|
|
28514
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
28515
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
28516
|
+
async sendOutcome(request) {
|
|
28363
28517
|
const run = {
|
|
28364
28518
|
amount: request.amount,
|
|
28365
28519
|
delivered: [],
|
|
@@ -28367,12 +28521,36 @@ var PaymentsFacade = class {
|
|
|
28367
28521
|
tokenTransfers: [],
|
|
28368
28522
|
settledShortfalls: [],
|
|
28369
28523
|
firstPartialId: void 0,
|
|
28370
|
-
pendingAny: false
|
|
28524
|
+
pendingAny: false,
|
|
28525
|
+
lastTransferId: ""
|
|
28526
|
+
};
|
|
28527
|
+
try {
|
|
28528
|
+
return await this.sendWithPolicy(request, run);
|
|
28529
|
+
} catch (err) {
|
|
28530
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
28531
|
+
throw err;
|
|
28532
|
+
}
|
|
28533
|
+
}
|
|
28534
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
28535
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
28536
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
28537
|
+
emitCleanFailure(err, transferId) {
|
|
28538
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
28539
|
+
const failed = {
|
|
28540
|
+
id: transferId,
|
|
28541
|
+
status: "failed",
|
|
28542
|
+
tokens: [],
|
|
28543
|
+
tokenTransfers: [],
|
|
28544
|
+
error: messageOf(err)
|
|
28371
28545
|
};
|
|
28546
|
+
this.deps.emit("transfer:updated", failed);
|
|
28547
|
+
}
|
|
28548
|
+
async sendWithPolicy(request, run) {
|
|
28549
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
28372
28550
|
for (let attempt = 0; ; attempt++) {
|
|
28373
28551
|
let ctx;
|
|
28374
28552
|
try {
|
|
28375
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
28553
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
28376
28554
|
} catch (err) {
|
|
28377
28555
|
throw this.partialize(err, run);
|
|
28378
28556
|
}
|
|
@@ -28417,11 +28595,8 @@ var PaymentsFacade = class {
|
|
|
28417
28595
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
28418
28596
|
}
|
|
28419
28597
|
}
|
|
28420
|
-
/**
|
|
28421
|
-
*
|
|
28422
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
28423
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
28424
|
-
*/
|
|
28598
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
28599
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
28425
28600
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
28426
28601
|
if (disposition.deliveryPending) {
|
|
28427
28602
|
run.pendingAny = true;
|
|
@@ -28447,11 +28622,8 @@ var PaymentsFacade = class {
|
|
|
28447
28622
|
run.amount = partial.remainingAmount;
|
|
28448
28623
|
return null;
|
|
28449
28624
|
}
|
|
28450
|
-
/**
|
|
28451
|
-
*
|
|
28452
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
28453
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
28454
|
-
*/
|
|
28625
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
28626
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
28455
28627
|
async disposeFailedAttempt(ctx, err) {
|
|
28456
28628
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
28457
28629
|
this.settleKeepOpen(ctx);
|
|
@@ -28500,8 +28672,9 @@ var PaymentsFacade = class {
|
|
|
28500
28672
|
}
|
|
28501
28673
|
return { kind: "rethrow", error: err };
|
|
28502
28674
|
}
|
|
28503
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
28675
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
28504
28676
|
const transferId = this.newId();
|
|
28677
|
+
run.lastTransferId = transferId;
|
|
28505
28678
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
28506
28679
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
28507
28680
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -28633,11 +28806,7 @@ var PaymentsFacade = class {
|
|
|
28633
28806
|
this.deps.emit("transfer:updated", result);
|
|
28634
28807
|
return result;
|
|
28635
28808
|
}
|
|
28636
|
-
/**
|
|
28637
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
28638
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
28639
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
28640
|
-
*/
|
|
28809
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
28641
28810
|
partialize(err, run) {
|
|
28642
28811
|
if (run.delivered.length === 0) return err;
|
|
28643
28812
|
return new PartialSendConflictError(
|
|
@@ -28789,22 +28958,14 @@ var PaymentsFacade = class {
|
|
|
28789
28958
|
}
|
|
28790
28959
|
}
|
|
28791
28960
|
toUiToken(tokenId, coinId, amount) {
|
|
28792
|
-
const registry = this.deps.registry;
|
|
28793
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
28794
28961
|
const now = this.nowMs();
|
|
28795
|
-
return
|
|
28796
|
-
|
|
28797
|
-
|
|
28798
|
-
|
|
28799
|
-
|
|
28800
|
-
|
|
28801
|
-
|
|
28802
|
-
amount: amount.toString(),
|
|
28803
|
-
status: "transferring",
|
|
28804
|
-
createdAt: now,
|
|
28805
|
-
updatedAt: now,
|
|
28806
|
-
lazy: true
|
|
28807
|
-
};
|
|
28962
|
+
return toToken(
|
|
28963
|
+
tokenId,
|
|
28964
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
28965
|
+
{ coinId, amount: amount.toString() },
|
|
28966
|
+
this.deps.registry,
|
|
28967
|
+
{ transferring: true, suspectedSpent: false }
|
|
28968
|
+
);
|
|
28808
28969
|
}
|
|
28809
28970
|
track(op) {
|
|
28810
28971
|
const settled = op.then(
|
|
@@ -28819,27 +28980,6 @@ var PaymentsFacade = class {
|
|
|
28819
28980
|
this.track(op).catch(() => void 0);
|
|
28820
28981
|
}
|
|
28821
28982
|
};
|
|
28822
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
28823
|
-
return {
|
|
28824
|
-
v: 2,
|
|
28825
|
-
recipient: recipientPubkey,
|
|
28826
|
-
coinId: request.coinId,
|
|
28827
|
-
amount: request.amount,
|
|
28828
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
28829
|
-
direct: [...spend.plan.direct],
|
|
28830
|
-
...spend.plan.split !== void 0 ? {
|
|
28831
|
-
split: {
|
|
28832
|
-
tokenId: spend.plan.split.tokenId,
|
|
28833
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
28834
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
28835
|
-
}
|
|
28836
|
-
} : {},
|
|
28837
|
-
spentStates
|
|
28838
|
-
};
|
|
28839
|
-
}
|
|
28840
|
-
function messageOf(err) {
|
|
28841
|
-
return err instanceof Error ? err.message : String(err);
|
|
28842
|
-
}
|
|
28843
28983
|
|
|
28844
28984
|
// impl/wallet-api-v2/http.ts
|
|
28845
28985
|
var WalletApiHttpError = class extends Error {
|
|
@@ -29185,6 +29325,7 @@ var WalletApiSession = class {
|
|
|
29185
29325
|
epochLatch = null;
|
|
29186
29326
|
reseedChain = Promise.resolve();
|
|
29187
29327
|
reseedsPending = 0;
|
|
29328
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
29188
29329
|
handlers = /* @__PURE__ */ new Map();
|
|
29189
29330
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
29190
29331
|
socket = null;
|
|
@@ -29287,6 +29428,18 @@ var WalletApiSession = class {
|
|
|
29287
29428
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
29288
29429
|
}
|
|
29289
29430
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
29431
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
29432
|
+
currentEpoch() {
|
|
29433
|
+
return this.epochLatch ?? "";
|
|
29434
|
+
}
|
|
29435
|
+
/**
|
|
29436
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
29437
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
29438
|
+
*/
|
|
29439
|
+
subscribeEpochChange(handler) {
|
|
29440
|
+
this.epochHandlers.add(handler);
|
|
29441
|
+
return () => this.epochHandlers.delete(handler);
|
|
29442
|
+
}
|
|
29290
29443
|
noteEpoch(epoch) {
|
|
29291
29444
|
if (this.epochLatch === epoch) return;
|
|
29292
29445
|
const previous = this.epochLatch;
|
|
@@ -29300,7 +29453,7 @@ var WalletApiSession = class {
|
|
|
29300
29453
|
}
|
|
29301
29454
|
async runReseed(epoch, previous) {
|
|
29302
29455
|
try {
|
|
29303
|
-
|
|
29456
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
29304
29457
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
29305
29458
|
} catch {
|
|
29306
29459
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -29562,6 +29715,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29562
29715
|
wake;
|
|
29563
29716
|
deriveFn;
|
|
29564
29717
|
seenChain = new SerialChain();
|
|
29718
|
+
lastIncomingEpoch = null;
|
|
29565
29719
|
constructor(config) {
|
|
29566
29720
|
this.client = config.client;
|
|
29567
29721
|
this.kv = config.kv;
|
|
@@ -29656,6 +29810,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29656
29810
|
const seen = await this.readSeen();
|
|
29657
29811
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
29658
29812
|
for (; ; ) {
|
|
29813
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
29659
29814
|
for (const entry of page.entries) {
|
|
29660
29815
|
if (entry.status !== "unclaimed") continue;
|
|
29661
29816
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -29665,6 +29820,10 @@ var WalletApiDeliveryPort = class {
|
|
|
29665
29820
|
page = await this.client.listMailbox(page.cursor);
|
|
29666
29821
|
}
|
|
29667
29822
|
}
|
|
29823
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
29824
|
+
incomingEpoch() {
|
|
29825
|
+
return this.lastIncomingEpoch;
|
|
29826
|
+
}
|
|
29668
29827
|
toIncoming(entry) {
|
|
29669
29828
|
const bundle = this.openEnvelope(entry);
|
|
29670
29829
|
return {
|
|
@@ -29742,7 +29901,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29742
29901
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
29743
29902
|
}
|
|
29744
29903
|
async put(transferId, opIndex, bytes) {
|
|
29745
|
-
const {
|
|
29904
|
+
const { kv, fieldKey } = this.config;
|
|
29746
29905
|
const aad = this.aad(transferId, opIndex);
|
|
29747
29906
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
29748
29907
|
let envelope = await kv.get(cacheKey);
|
|
@@ -29750,10 +29909,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29750
29909
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
29751
29910
|
await kv.set(cacheKey, envelope);
|
|
29752
29911
|
}
|
|
29753
|
-
const
|
|
29754
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
29912
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
29755
29913
|
return this.open(record.payload, aad);
|
|
29756
29914
|
}
|
|
29915
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
29916
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
29917
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
29918
|
+
if (envelope === null) return false;
|
|
29919
|
+
await this.post(transferId, opIndex, envelope);
|
|
29920
|
+
return true;
|
|
29921
|
+
}
|
|
29922
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
29923
|
+
async post(transferId, opIndex, envelope) {
|
|
29924
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
29925
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
29926
|
+
}
|
|
29757
29927
|
async get(transferId, opIndex) {
|
|
29758
29928
|
const records = await this.config.client.getProgress(transferId);
|
|
29759
29929
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -29776,7 +29946,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29776
29946
|
};
|
|
29777
29947
|
|
|
29778
29948
|
// core/payments-v2-wiring.ts
|
|
29779
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
29949
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
29780
29950
|
const source = walletApi;
|
|
29781
29951
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
29782
29952
|
if (!source || network === null) {
|
|
@@ -29785,6 +29955,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
29785
29955
|
"INVALID_CONFIG"
|
|
29786
29956
|
);
|
|
29787
29957
|
}
|
|
29958
|
+
if (!(network in NETWORKS)) {
|
|
29959
|
+
throw new SphereError(
|
|
29960
|
+
`paymentsV2: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
29961
|
+
"INVALID_CONFIG"
|
|
29962
|
+
);
|
|
29963
|
+
}
|
|
29964
|
+
if (network !== sphereNetwork) {
|
|
29965
|
+
throw new SphereError(
|
|
29966
|
+
`paymentsV2: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
29967
|
+
"INVALID_CONFIG"
|
|
29968
|
+
);
|
|
29969
|
+
}
|
|
29788
29970
|
if (typeof source.paymentsV2Transport === "function") {
|
|
29789
29971
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
29790
29972
|
}
|
|
@@ -29850,8 +30032,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
29850
30032
|
deviceId: `${config.deviceId}:pv2`,
|
|
29851
30033
|
kv: args.kv,
|
|
29852
30034
|
webSocketFactory,
|
|
29853
|
-
emitStatus: args.emitStatus
|
|
29854
|
-
onEpochChange: args.onEpochChange
|
|
30035
|
+
emitStatus: args.emitStatus
|
|
29855
30036
|
});
|
|
29856
30037
|
return { session, client: authedWireClient(session, client) };
|
|
29857
30038
|
};
|
|
@@ -29921,8 +30102,7 @@ function composePaymentsV2(spec) {
|
|
|
29921
30102
|
identity: identitySlice,
|
|
29922
30103
|
network,
|
|
29923
30104
|
kv,
|
|
29924
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29925
|
-
onEpochChange: async () => void 0
|
|
30105
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29926
30106
|
});
|
|
29927
30107
|
const deps = {
|
|
29928
30108
|
session: transport.session,
|
|
@@ -29952,7 +30132,10 @@ function composePaymentsV2(spec) {
|
|
|
29952
30132
|
network,
|
|
29953
30133
|
ownPubkey: identity.chainPubkey,
|
|
29954
30134
|
ownNametag: host.nametag,
|
|
29955
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
30135
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
30136
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
30137
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
30138
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
29956
30139
|
};
|
|
29957
30140
|
return new PaymentsFacade(deps);
|
|
29958
30141
|
}
|
|
@@ -30282,18 +30465,20 @@ var Sphere = class _Sphere {
|
|
|
30282
30465
|
}
|
|
30283
30466
|
/**
|
|
30284
30467
|
* P9: validate + resolve the paymentsV2 opt-in BEFORE any storage writes —
|
|
30285
|
-
* fail-closed at init, never a silently-degraded composition.
|
|
30468
|
+
* fail-closed at init, never a silently-degraded composition. Returns the
|
|
30469
|
+
* resolved composition (null = flag off); #728: resolving here also asserts
|
|
30470
|
+
* the single-network invariant (walletApi.network known + equal to the
|
|
30471
|
+
* Sphere network) before any session/KV/provider exists.
|
|
30286
30472
|
*/
|
|
30287
|
-
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi) {
|
|
30288
|
-
if (flag !== true) return
|
|
30473
|
+
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi, network) {
|
|
30474
|
+
if (flag !== true) return null;
|
|
30289
30475
|
if (accountingConfig || swapConfig) {
|
|
30290
30476
|
throw new SphereError(
|
|
30291
30477
|
"paymentsV2 is incompatible with the accounting/swap modules (both are built on the legacy PaymentsModule and are deleted with the P11 flip) \u2014 disable them or the flag.",
|
|
30292
30478
|
"INVALID_CONFIG"
|
|
30293
30479
|
);
|
|
30294
30480
|
}
|
|
30295
|
-
resolvePaymentsV2Composition(walletApi);
|
|
30296
|
-
return true;
|
|
30481
|
+
return resolvePaymentsV2Composition(walletApi, network);
|
|
30297
30482
|
}
|
|
30298
30483
|
/**
|
|
30299
30484
|
* Configure TokenRegistry in the main bundle context.
|
|
@@ -30348,12 +30533,14 @@ var Sphere = class _Sphere {
|
|
|
30348
30533
|
options.walletApi
|
|
30349
30534
|
);
|
|
30350
30535
|
sphere._verification = options.verification;
|
|
30351
|
-
sphere.
|
|
30536
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30352
30537
|
options.paymentsV2,
|
|
30353
30538
|
accountingConfig,
|
|
30354
30539
|
swapConfig,
|
|
30355
|
-
options.walletApi
|
|
30540
|
+
options.walletApi,
|
|
30541
|
+
options.network
|
|
30356
30542
|
);
|
|
30543
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30357
30544
|
sphere._password = options.password ?? null;
|
|
30358
30545
|
sphere._passwordProtected = sphere._password !== null;
|
|
30359
30546
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -30420,12 +30607,14 @@ var Sphere = class _Sphere {
|
|
|
30420
30607
|
options.walletApi
|
|
30421
30608
|
);
|
|
30422
30609
|
sphere._verification = options.verification;
|
|
30423
|
-
sphere.
|
|
30610
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30424
30611
|
options.paymentsV2,
|
|
30425
30612
|
accountingConfig,
|
|
30426
30613
|
swapConfig,
|
|
30427
|
-
options.walletApi
|
|
30614
|
+
options.walletApi,
|
|
30615
|
+
options.network
|
|
30428
30616
|
);
|
|
30617
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30429
30618
|
sphere._password = options.password ?? null;
|
|
30430
30619
|
sphere._passwordProtected = sphere._password !== null;
|
|
30431
30620
|
if (!options.storage.isConnected()) {
|
|
@@ -30499,12 +30688,14 @@ var Sphere = class _Sphere {
|
|
|
30499
30688
|
options.walletApi
|
|
30500
30689
|
);
|
|
30501
30690
|
sphere._verification = options.verification;
|
|
30502
|
-
sphere.
|
|
30691
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30503
30692
|
options.paymentsV2,
|
|
30504
30693
|
accountingConfig,
|
|
30505
30694
|
swapConfig,
|
|
30506
|
-
options.walletApi
|
|
30695
|
+
options.walletApi,
|
|
30696
|
+
options.network
|
|
30507
30697
|
);
|
|
30698
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30508
30699
|
sphere._password = options.password ?? null;
|
|
30509
30700
|
sphere._passwordProtected = sphere._password !== null;
|
|
30510
30701
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -33162,7 +33353,9 @@ var Sphere = class _Sphere {
|
|
|
33162
33353
|
* setOracleApiKey rebuild is what future operations snapshot.
|
|
33163
33354
|
*/
|
|
33164
33355
|
async startPaymentsV2Inner(index, identity) {
|
|
33165
|
-
this._paymentsV2Composition
|
|
33356
|
+
if (this._paymentsV2Composition === null) {
|
|
33357
|
+
throw new SphereError("paymentsV2: composition unresolved \u2014 init never ran the flag gate", "INVALID_CONFIG");
|
|
33358
|
+
}
|
|
33166
33359
|
if (!this._addressModules.get(index)?.tokenEngine) {
|
|
33167
33360
|
throw new SphereError(
|
|
33168
33361
|
"paymentsV2 requires the v2 token engine \u2014 the oracle must supply a trust base + gateway URL (and API key where required).",
|