@unicitylabs/sphere-sdk 0.14.1-dev.1 → 0.14.1-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +20 -5
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.d.cts +0 -1
- package/dist/connect/index.d.ts +0 -1
- package/dist/connect/index.js +20 -5
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +395 -213
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +10 -3
- package/dist/core/index.d.ts +10 -3
- package/dist/core/index.js +395 -213
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +10 -2
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.d.cts +0 -1
- package/dist/impl/browser/connect/index.d.ts +0 -1
- package/dist/impl/browser/connect/index.js +10 -2
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/nodejs/connect/index.cjs +9 -1
- package/dist/impl/nodejs/connect/index.cjs.map +1 -1
- package/dist/impl/nodejs/connect/index.d.cts +0 -1
- package/dist/impl/nodejs/connect/index.d.ts +0 -1
- package/dist/impl/nodejs/connect/index.js +9 -1
- package/dist/impl/nodejs/connect/index.js.map +1 -1
- package/dist/impl/nodejs/index.d.cts +8 -1
- package/dist/impl/nodejs/index.d.ts +8 -1
- package/dist/impl/shared/wallet-api/index.d.cts +8 -1
- package/dist/impl/shared/wallet-api/index.d.ts +8 -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 +395 -213
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -3
- package/dist/index.d.ts +10 -3
- package/dist/index.js +395 -213
- 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
|
@@ -11084,7 +11084,10 @@ var STORE_KEYS = {
|
|
|
11084
11084
|
shortfalls: "shortfalls",
|
|
11085
11085
|
settlingLinks: "settling",
|
|
11086
11086
|
streamCursor: (s) => `cursor:${s}`,
|
|
11087
|
-
epochLatch: "epoch-latch"
|
|
11087
|
+
epochLatch: "epoch-latch",
|
|
11088
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
11089
|
+
suspectedSpent: "suspected-spent",
|
|
11090
|
+
knownSpends: "known-spends"
|
|
11088
11091
|
};
|
|
11089
11092
|
|
|
11090
11093
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -11334,7 +11337,7 @@ var TransferMachine = class {
|
|
|
11334
11337
|
}
|
|
11335
11338
|
if (cls === null) {
|
|
11336
11339
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
11337
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
11340
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
11338
11341
|
return { committed, deliveryPending };
|
|
11339
11342
|
}
|
|
11340
11343
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -11360,10 +11363,10 @@ var TransferMachine = class {
|
|
|
11360
11363
|
}
|
|
11361
11364
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
11362
11365
|
if (conflicts.length > 0) {
|
|
11363
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
11366
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
11364
11367
|
}
|
|
11365
11368
|
await this.completeIntent(r.transferId, r.payload);
|
|
11366
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11369
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11367
11370
|
return { deliveryPending, partial: null };
|
|
11368
11371
|
}
|
|
11369
11372
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -11545,7 +11548,7 @@ var TransferMachine = class {
|
|
|
11545
11548
|
}
|
|
11546
11549
|
return added;
|
|
11547
11550
|
}
|
|
11548
|
-
async settlePartial(r, committed, conflicts) {
|
|
11551
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
11549
11552
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
11550
11553
|
const entry = {
|
|
11551
11554
|
transferId: r.transferId,
|
|
@@ -11557,26 +11560,27 @@ var TransferMachine = class {
|
|
|
11557
11560
|
};
|
|
11558
11561
|
await this.stores.shortfalls.upsert(entry);
|
|
11559
11562
|
await this.completeIntent(r.transferId, r.payload);
|
|
11560
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11563
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11561
11564
|
return entry;
|
|
11562
11565
|
}
|
|
11563
|
-
closeTail(transferId, payload) {
|
|
11566
|
+
closeTail(engine, transferId, payload, committed) {
|
|
11564
11567
|
void (async () => {
|
|
11565
11568
|
try {
|
|
11566
11569
|
await this.completeIntent(transferId, payload);
|
|
11567
11570
|
} catch {
|
|
11568
11571
|
return;
|
|
11569
11572
|
}
|
|
11570
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
11573
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
11571
11574
|
})();
|
|
11572
11575
|
}
|
|
11573
11576
|
async completeIntent(transferId, payload) {
|
|
11574
11577
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
11575
11578
|
await this.deps.intents.complete(transferId, signature);
|
|
11576
11579
|
}
|
|
11577
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
11580
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
11578
11581
|
try {
|
|
11579
|
-
await this.
|
|
11582
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
11583
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
11580
11584
|
} catch {
|
|
11581
11585
|
}
|
|
11582
11586
|
try {
|
|
@@ -11584,6 +11588,26 @@ var TransferMachine = class {
|
|
|
11584
11588
|
} catch {
|
|
11585
11589
|
}
|
|
11586
11590
|
}
|
|
11591
|
+
/**
|
|
11592
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
11593
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
11594
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
11595
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
11596
|
+
*/
|
|
11597
|
+
async settledAmount(engine, coinId, committed) {
|
|
11598
|
+
let total = 0n;
|
|
11599
|
+
for (const outcome of committed) {
|
|
11600
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
11601
|
+
const token = await engine.decodeToken({
|
|
11602
|
+
v: TOKEN_BLOB_VERSION,
|
|
11603
|
+
network: 0,
|
|
11604
|
+
tokenId: "",
|
|
11605
|
+
token: outcome.recipientBlob
|
|
11606
|
+
});
|
|
11607
|
+
total += engine.balanceOf(token, coinId);
|
|
11608
|
+
}
|
|
11609
|
+
return total.toString();
|
|
11610
|
+
}
|
|
11587
11611
|
async disposeOnFailure(transferId, committed, cls) {
|
|
11588
11612
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
11589
11613
|
try {
|
|
@@ -11608,7 +11632,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
11608
11632
|
}
|
|
11609
11633
|
|
|
11610
11634
|
// modules/payments-v2/machine/resume.ts
|
|
11611
|
-
async function resumeAll(deps) {
|
|
11635
|
+
async function resumeAll(deps, options = {}) {
|
|
11612
11636
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
11613
11637
|
const stores = createMachineStores(deps.kv);
|
|
11614
11638
|
const open = await deps.intents.listOpen();
|
|
@@ -11618,7 +11642,12 @@ async function resumeAll(deps) {
|
|
|
11618
11642
|
} catch {
|
|
11619
11643
|
return report;
|
|
11620
11644
|
}
|
|
11621
|
-
await
|
|
11645
|
+
await dropStaleBackstops(
|
|
11646
|
+
stores,
|
|
11647
|
+
locals,
|
|
11648
|
+
new Set(open.map((i) => i.transferId)),
|
|
11649
|
+
options.isLocallyActive ?? (() => false)
|
|
11650
|
+
);
|
|
11622
11651
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
11623
11652
|
const ctx = {
|
|
11624
11653
|
deps,
|
|
@@ -11631,11 +11660,16 @@ async function resumeAll(deps) {
|
|
|
11631
11660
|
}
|
|
11632
11661
|
return report;
|
|
11633
11662
|
}
|
|
11634
|
-
async function
|
|
11663
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
11635
11664
|
for (const entry of locals.values()) {
|
|
11636
|
-
if (
|
|
11665
|
+
if (openIds.has(entry.transferId)) continue;
|
|
11666
|
+
if (entry.disposition !== "open") {
|
|
11637
11667
|
await stores.backstop.removeByKey(entry.transferId);
|
|
11668
|
+
continue;
|
|
11638
11669
|
}
|
|
11670
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
11671
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
11672
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
11639
11673
|
}
|
|
11640
11674
|
}
|
|
11641
11675
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -11843,7 +11877,9 @@ var Converger = class {
|
|
|
11843
11877
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
11844
11878
|
}
|
|
11845
11879
|
async resumePass() {
|
|
11846
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
11880
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
11881
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
11882
|
+
});
|
|
11847
11883
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
11848
11884
|
await this.deps.reconcile({
|
|
11849
11885
|
resumed: report.resumed,
|
|
@@ -11959,6 +11995,136 @@ function distinctOpCount(entries) {
|
|
|
11959
11995
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
11960
11996
|
}
|
|
11961
11997
|
|
|
11998
|
+
// modules/payments-v2/restore.ts
|
|
11999
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
12000
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
12001
|
+
async function reseedAndReset(deps) {
|
|
12002
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
12003
|
+
if (entry.disposition !== "open") continue;
|
|
12004
|
+
try {
|
|
12005
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
12006
|
+
if (entry.requiresSeedClose) {
|
|
12007
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
12008
|
+
}
|
|
12009
|
+
} catch (err) {
|
|
12010
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
12011
|
+
}
|
|
12012
|
+
}
|
|
12013
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
12014
|
+
await deps.fullRePull();
|
|
12015
|
+
}
|
|
12016
|
+
async function splitOpIndex(deps, envelope) {
|
|
12017
|
+
const payload = await deps.decryptPayload(envelope);
|
|
12018
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12019
|
+
}
|
|
12020
|
+
|
|
12021
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12022
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12023
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12024
|
+
return {
|
|
12025
|
+
id: tokenId,
|
|
12026
|
+
coinId: asset.coinId,
|
|
12027
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12028
|
+
name: registry.getName(asset.coinId),
|
|
12029
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12030
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12031
|
+
amount: asset.amount,
|
|
12032
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12033
|
+
createdAt: snapshot.createdAt,
|
|
12034
|
+
updatedAt: snapshot.updatedAt,
|
|
12035
|
+
lazy: true,
|
|
12036
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12037
|
+
};
|
|
12038
|
+
}
|
|
12039
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12040
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12041
|
+
for (const [tokenId, entry] of entries) {
|
|
12042
|
+
const moving = isTransferring(tokenId);
|
|
12043
|
+
for (const asset of entry.assets) {
|
|
12044
|
+
let group = groups.get(asset.coinId);
|
|
12045
|
+
if (!group) {
|
|
12046
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12047
|
+
groups.set(asset.coinId, group);
|
|
12048
|
+
}
|
|
12049
|
+
if (moving) {
|
|
12050
|
+
group.transferring += BigInt(asset.amount);
|
|
12051
|
+
group.transferringCount += 1;
|
|
12052
|
+
} else {
|
|
12053
|
+
group.total += BigInt(asset.amount);
|
|
12054
|
+
group.count += 1;
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
12057
|
+
}
|
|
12058
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12059
|
+
}
|
|
12060
|
+
function toAsset(coinId, totals, registry) {
|
|
12061
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12062
|
+
return {
|
|
12063
|
+
coinId,
|
|
12064
|
+
symbol: registry.getSymbol(coinId),
|
|
12065
|
+
name: registry.getName(coinId),
|
|
12066
|
+
decimals: registry.getDecimals(coinId),
|
|
12067
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12068
|
+
totalAmount: totals.total.toString(),
|
|
12069
|
+
tokenCount: totals.count,
|
|
12070
|
+
confirmedAmount: totals.total.toString(),
|
|
12071
|
+
unconfirmedAmount: "0",
|
|
12072
|
+
confirmedTokenCount: totals.count,
|
|
12073
|
+
unconfirmedTokenCount: 0,
|
|
12074
|
+
transferringTokenCount: totals.transferringCount,
|
|
12075
|
+
transferringAmount: totals.transferring.toString(),
|
|
12076
|
+
priceUsd: null,
|
|
12077
|
+
priceEur: null,
|
|
12078
|
+
change24h: null,
|
|
12079
|
+
fiatValueUsd: null,
|
|
12080
|
+
fiatValueEur: null
|
|
12081
|
+
};
|
|
12082
|
+
}
|
|
12083
|
+
async function withPrices(raw, registry, price) {
|
|
12084
|
+
try {
|
|
12085
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12086
|
+
const quotes = await price.getPrices(names);
|
|
12087
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12088
|
+
} catch {
|
|
12089
|
+
return raw;
|
|
12090
|
+
}
|
|
12091
|
+
}
|
|
12092
|
+
function priceOne(asset, quote) {
|
|
12093
|
+
if (!quote) return asset;
|
|
12094
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12095
|
+
return {
|
|
12096
|
+
...asset,
|
|
12097
|
+
priceUsd: quote.priceUsd,
|
|
12098
|
+
priceEur: quote.priceEur ?? null,
|
|
12099
|
+
change24h: quote.change24h ?? null,
|
|
12100
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12101
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12102
|
+
};
|
|
12103
|
+
}
|
|
12104
|
+
|
|
12105
|
+
// modules/payments-v2/machine/payload.ts
|
|
12106
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12107
|
+
return {
|
|
12108
|
+
v: 2,
|
|
12109
|
+
recipient: recipientPubkey,
|
|
12110
|
+
coinId: request.coinId,
|
|
12111
|
+
amount: request.amount,
|
|
12112
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12113
|
+
direct: [...spend.plan.direct],
|
|
12114
|
+
...spend.plan.split !== void 0 ? {
|
|
12115
|
+
split: {
|
|
12116
|
+
tokenId: spend.plan.split.tokenId,
|
|
12117
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12118
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12119
|
+
}
|
|
12120
|
+
} : {},
|
|
12121
|
+
spentStates
|
|
12122
|
+
};
|
|
12123
|
+
}
|
|
12124
|
+
function messageOf(err) {
|
|
12125
|
+
return err instanceof Error ? err.message : String(err);
|
|
12126
|
+
}
|
|
12127
|
+
|
|
11962
12128
|
// modules/payments-v2/history/History.ts
|
|
11963
12129
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
11964
12130
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -12017,13 +12183,14 @@ var History = class {
|
|
|
12017
12183
|
}
|
|
12018
12184
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
12019
12185
|
async post(build) {
|
|
12186
|
+
const record = build();
|
|
12020
12187
|
try {
|
|
12021
|
-
await this.deps.client.postHistory([
|
|
12188
|
+
await this.deps.client.postHistory([record]);
|
|
12022
12189
|
} catch (err) {
|
|
12023
12190
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
12024
12191
|
return;
|
|
12025
12192
|
}
|
|
12026
|
-
this.deps.emit("history:updated");
|
|
12193
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
12027
12194
|
}
|
|
12028
12195
|
ts(timestamp) {
|
|
12029
12196
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -12080,93 +12247,9 @@ var History = class {
|
|
|
12080
12247
|
}
|
|
12081
12248
|
};
|
|
12082
12249
|
|
|
12083
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
12084
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12085
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12086
|
-
return {
|
|
12087
|
-
id: tokenId,
|
|
12088
|
-
coinId: asset.coinId,
|
|
12089
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12090
|
-
name: registry.getName(asset.coinId),
|
|
12091
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12092
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12093
|
-
amount: asset.amount,
|
|
12094
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12095
|
-
createdAt: snapshot.createdAt,
|
|
12096
|
-
updatedAt: snapshot.updatedAt,
|
|
12097
|
-
lazy: true,
|
|
12098
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12099
|
-
};
|
|
12100
|
-
}
|
|
12101
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12102
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12103
|
-
for (const [tokenId, entry] of entries) {
|
|
12104
|
-
const moving = isTransferring(tokenId);
|
|
12105
|
-
for (const asset of entry.assets) {
|
|
12106
|
-
let group = groups.get(asset.coinId);
|
|
12107
|
-
if (!group) {
|
|
12108
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12109
|
-
groups.set(asset.coinId, group);
|
|
12110
|
-
}
|
|
12111
|
-
if (moving) {
|
|
12112
|
-
group.transferring += BigInt(asset.amount);
|
|
12113
|
-
group.transferringCount += 1;
|
|
12114
|
-
} else {
|
|
12115
|
-
group.total += BigInt(asset.amount);
|
|
12116
|
-
group.count += 1;
|
|
12117
|
-
}
|
|
12118
|
-
}
|
|
12119
|
-
}
|
|
12120
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12121
|
-
}
|
|
12122
|
-
function toAsset(coinId, totals, registry) {
|
|
12123
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12124
|
-
return {
|
|
12125
|
-
coinId,
|
|
12126
|
-
symbol: registry.getSymbol(coinId),
|
|
12127
|
-
name: registry.getName(coinId),
|
|
12128
|
-
decimals: registry.getDecimals(coinId),
|
|
12129
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12130
|
-
totalAmount: totals.total.toString(),
|
|
12131
|
-
tokenCount: totals.count,
|
|
12132
|
-
confirmedAmount: totals.total.toString(),
|
|
12133
|
-
unconfirmedAmount: "0",
|
|
12134
|
-
confirmedTokenCount: totals.count,
|
|
12135
|
-
unconfirmedTokenCount: 0,
|
|
12136
|
-
transferringTokenCount: totals.transferringCount,
|
|
12137
|
-
transferringAmount: totals.transferring.toString(),
|
|
12138
|
-
priceUsd: null,
|
|
12139
|
-
priceEur: null,
|
|
12140
|
-
change24h: null,
|
|
12141
|
-
fiatValueUsd: null,
|
|
12142
|
-
fiatValueEur: null
|
|
12143
|
-
};
|
|
12144
|
-
}
|
|
12145
|
-
async function withPrices(raw, registry, price) {
|
|
12146
|
-
try {
|
|
12147
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12148
|
-
const quotes = await price.getPrices(names);
|
|
12149
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12150
|
-
} catch {
|
|
12151
|
-
return raw;
|
|
12152
|
-
}
|
|
12153
|
-
}
|
|
12154
|
-
function priceOne(asset, quote) {
|
|
12155
|
-
if (!quote) return asset;
|
|
12156
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12157
|
-
return {
|
|
12158
|
-
...asset,
|
|
12159
|
-
priceUsd: quote.priceUsd,
|
|
12160
|
-
priceEur: quote.priceEur ?? null,
|
|
12161
|
-
change24h: quote.change24h ?? null,
|
|
12162
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12163
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12164
|
-
};
|
|
12165
|
-
}
|
|
12166
|
-
|
|
12167
12250
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
12168
|
-
var SUSPECTED_KEY =
|
|
12169
|
-
var KNOWN_SPENDS_KEY =
|
|
12251
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
12252
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
12170
12253
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
12171
12254
|
function stateKey(tokenId, stateHash) {
|
|
12172
12255
|
return `${tokenId}:${stateHash}`;
|
|
@@ -12470,19 +12553,24 @@ var Receive = class {
|
|
|
12470
12553
|
const engine = deps.engine();
|
|
12471
12554
|
const stored = [];
|
|
12472
12555
|
const pending = [];
|
|
12473
|
-
|
|
12556
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
12474
12557
|
try {
|
|
12475
|
-
epoch = deps.syncEpoch();
|
|
12476
12558
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
12477
|
-
|
|
12478
|
-
for
|
|
12479
|
-
await
|
|
12480
|
-
|
|
12559
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
12560
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
12561
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
12562
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
12563
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
12564
|
+
}
|
|
12565
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
12566
|
+
const served = deps.delivery.incomingEpoch();
|
|
12567
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
12568
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
12569
|
+
basis = null;
|
|
12481
12570
|
}
|
|
12482
|
-
await flushAcks(deps, pending, epoch);
|
|
12483
12571
|
} catch (err) {
|
|
12484
12572
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
12485
|
-
await flushAcks(deps, pending,
|
|
12573
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
12486
12574
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
12487
12575
|
});
|
|
12488
12576
|
}
|
|
@@ -12555,7 +12643,7 @@ async function announce(deps, entry, record) {
|
|
|
12555
12643
|
receivedAt
|
|
12556
12644
|
};
|
|
12557
12645
|
}
|
|
12558
|
-
async function flushAcks(deps, pending,
|
|
12646
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
12559
12647
|
let lastAcked = null;
|
|
12560
12648
|
try {
|
|
12561
12649
|
while (pending.length > 0) {
|
|
@@ -12566,7 +12654,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
12566
12654
|
}
|
|
12567
12655
|
} finally {
|
|
12568
12656
|
if (lastAcked !== null) {
|
|
12569
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
12657
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
12570
12658
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
12571
12659
|
}
|
|
12572
12660
|
}
|
|
@@ -12667,9 +12755,19 @@ var Requests = class {
|
|
|
12667
12755
|
}
|
|
12668
12756
|
});
|
|
12669
12757
|
}
|
|
12758
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
12670
12759
|
writeLink(requestId, transferId, committed) {
|
|
12671
12760
|
return this.mutateJournal((m) => {
|
|
12672
|
-
|
|
12761
|
+
const prev = m.get(requestId);
|
|
12762
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
12763
|
+
const next = {
|
|
12764
|
+
requestId,
|
|
12765
|
+
transferId,
|
|
12766
|
+
committed: committed || same && prev.committed,
|
|
12767
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
12768
|
+
};
|
|
12769
|
+
if (same && prev.committed === next.committed) return false;
|
|
12770
|
+
m.set(requestId, next);
|
|
12673
12771
|
return true;
|
|
12674
12772
|
});
|
|
12675
12773
|
}
|
|
@@ -12822,23 +12920,46 @@ var Requests = class {
|
|
|
12822
12920
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
12823
12921
|
if (transferId !== void 0) {
|
|
12824
12922
|
const committed = error instanceof PartialSendConflictError;
|
|
12825
|
-
await this.
|
|
12826
|
-
this.setStatus(id, "settling");
|
|
12923
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
12827
12924
|
} else {
|
|
12828
12925
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
12829
12926
|
this.setStatus(id, "paid");
|
|
12830
12927
|
}
|
|
12831
12928
|
} else {
|
|
12832
|
-
await this.
|
|
12833
|
-
this.setStatus(id, "pending");
|
|
12929
|
+
await this.revertPayable(id);
|
|
12834
12930
|
}
|
|
12835
12931
|
throw error;
|
|
12836
12932
|
}
|
|
12837
|
-
this.
|
|
12838
|
-
await this.respondPaid(id, result.id);
|
|
12839
|
-
await this.clearLink(id);
|
|
12933
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
12840
12934
|
return result;
|
|
12841
12935
|
}
|
|
12936
|
+
/**
|
|
12937
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
12938
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
12939
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
12940
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
12941
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
12942
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
12943
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
12944
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
12945
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
12946
|
+
*/
|
|
12947
|
+
async settle(requestId, transferId, opts) {
|
|
12948
|
+
let journalDown = false;
|
|
12949
|
+
try {
|
|
12950
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
12951
|
+
} catch (err) {
|
|
12952
|
+
if (!opts.respond) throw err;
|
|
12953
|
+
journalDown = true;
|
|
12954
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
12955
|
+
}
|
|
12956
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
12957
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
12958
|
+
return;
|
|
12959
|
+
}
|
|
12960
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
12961
|
+
this.setStatus(requestId, "paid");
|
|
12962
|
+
}
|
|
12842
12963
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
12843
12964
|
async respondPaid(id, transferId) {
|
|
12844
12965
|
try {
|
|
@@ -12874,32 +12995,32 @@ var Requests = class {
|
|
|
12874
12995
|
let aborted = null;
|
|
12875
12996
|
for (const [requestId, link] of [...this.journal]) {
|
|
12876
12997
|
if (link.committed || resumed.has(link.transferId)) {
|
|
12877
|
-
await this.resolvePaid(requestId, link
|
|
12998
|
+
await this.resolvePaid(requestId, link);
|
|
12878
12999
|
} else if (conflicted.has(link.transferId)) {
|
|
12879
13000
|
await this.revertPayable(requestId);
|
|
12880
13001
|
} else if (open.has(link.transferId)) {
|
|
12881
13002
|
} else {
|
|
12882
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
13003
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
12883
13004
|
}
|
|
12884
13005
|
}
|
|
12885
13006
|
}
|
|
12886
13007
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
12887
13008
|
// a probe failure defers the link to the next session.
|
|
12888
|
-
async reconcileUnaccounted(requestId,
|
|
13009
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
12889
13010
|
try {
|
|
12890
13011
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
12891
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
12892
|
-
else await this.resolvePaid(requestId,
|
|
13012
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
13013
|
+
else await this.resolvePaid(requestId, link);
|
|
12893
13014
|
} catch (err) {
|
|
12894
13015
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
12895
13016
|
}
|
|
12896
13017
|
return aborted;
|
|
12897
13018
|
}
|
|
12898
|
-
|
|
12899
|
-
|
|
12900
|
-
|
|
12901
|
-
this.setStatus(requestId, "paid");
|
|
13019
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
13020
|
+
resolvePaid(requestId, link) {
|
|
13021
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
12902
13022
|
}
|
|
13023
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
12903
13024
|
async revertPayable(requestId) {
|
|
12904
13025
|
await this.clearLink(requestId);
|
|
12905
13026
|
this.setStatus(requestId, "pending");
|
|
@@ -13223,11 +13344,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13223
13344
|
client: deps.client,
|
|
13224
13345
|
fieldKey: deps.fieldKey,
|
|
13225
13346
|
registry: deps.registry,
|
|
13226
|
-
emit: (event) => deps.emit(event,
|
|
13347
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
13227
13348
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
13228
13349
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
13229
13350
|
});
|
|
13230
13351
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
13352
|
+
const machineStores = createMachineStores(deps.kv);
|
|
13231
13353
|
const heldStates = /* @__PURE__ */ new Map();
|
|
13232
13354
|
return {
|
|
13233
13355
|
ownPubkeyBytes,
|
|
@@ -13235,12 +13357,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13235
13357
|
ledger,
|
|
13236
13358
|
queue,
|
|
13237
13359
|
historyStore,
|
|
13238
|
-
machineStores
|
|
13360
|
+
machineStores,
|
|
13239
13361
|
machineDeps,
|
|
13240
13362
|
machine: new TransferMachine(machineDeps),
|
|
13241
13363
|
heldStates,
|
|
13242
13364
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
13243
|
-
requests: buildRequests(deps, hooks)
|
|
13365
|
+
requests: buildRequests(deps, hooks),
|
|
13366
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13367
|
+
};
|
|
13368
|
+
}
|
|
13369
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13370
|
+
return {
|
|
13371
|
+
stores: machineStores,
|
|
13372
|
+
kv: deps.kv,
|
|
13373
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
13374
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
13375
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
13376
|
+
fullRePull: () => view.onEpochReset(),
|
|
13377
|
+
attention: (transferId, code, detail) => {
|
|
13378
|
+
deps.emit(
|
|
13379
|
+
"transfer:attention",
|
|
13380
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13381
|
+
);
|
|
13382
|
+
}
|
|
13244
13383
|
};
|
|
13245
13384
|
}
|
|
13246
13385
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -13262,11 +13401,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
13262
13401
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
13263
13402
|
emit: deps.emit,
|
|
13264
13403
|
now: deps.now ?? Date.now,
|
|
13265
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
13404
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
13266
13405
|
await historyStore.recordSent({
|
|
13267
13406
|
transferId,
|
|
13268
13407
|
coinId: payload.coinId,
|
|
13269
|
-
|
|
13408
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
13409
|
+
// recipient blobs), never payload.amount — the plan.
|
|
13410
|
+
amount: committedAmount,
|
|
13270
13411
|
recipientPubkey: payload.recipient,
|
|
13271
13412
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
13272
13413
|
});
|
|
@@ -13293,7 +13434,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
13293
13434
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13294
13435
|
);
|
|
13295
13436
|
},
|
|
13296
|
-
syncEpoch: deps.syncEpoch
|
|
13437
|
+
syncEpoch: deps.syncEpoch,
|
|
13297
13438
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13298
13439
|
});
|
|
13299
13440
|
}
|
|
@@ -13346,6 +13487,7 @@ var PaymentsFacade = class {
|
|
|
13346
13487
|
this.receiveLoop = parts.receiveLoop;
|
|
13347
13488
|
this.heldStates = parts.heldStates;
|
|
13348
13489
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13490
|
+
this.restoreDeps = parts.restoreDeps;
|
|
13349
13491
|
this.requests = parts.requests;
|
|
13350
13492
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13351
13493
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -13374,6 +13516,7 @@ var PaymentsFacade = class {
|
|
|
13374
13516
|
receiveLoop;
|
|
13375
13517
|
heldStates;
|
|
13376
13518
|
ownPubkeyBytes;
|
|
13519
|
+
restoreDeps;
|
|
13377
13520
|
requests;
|
|
13378
13521
|
currentEngine = null;
|
|
13379
13522
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -13384,6 +13527,8 @@ var PaymentsFacade = class {
|
|
|
13384
13527
|
converger;
|
|
13385
13528
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
13386
13529
|
resumeFlight = new SingleFlight();
|
|
13530
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
13531
|
+
passChain = new SerialChain();
|
|
13387
13532
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13388
13533
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13389
13534
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -13391,8 +13536,8 @@ var PaymentsFacade = class {
|
|
|
13391
13536
|
if (this.started) return;
|
|
13392
13537
|
this.started = true;
|
|
13393
13538
|
this.heartbeat.start();
|
|
13394
|
-
await this.deps.session.start();
|
|
13395
13539
|
this.unsubscribers.push(
|
|
13540
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
13396
13541
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
13397
13542
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
13398
13543
|
}),
|
|
@@ -13407,6 +13552,7 @@ var PaymentsFacade = class {
|
|
|
13407
13552
|
if (status === "connected") this.heartbeat.recovered();
|
|
13408
13553
|
});
|
|
13409
13554
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13555
|
+
await this.deps.session.start();
|
|
13410
13556
|
this.trackTail(this.runConvergencePass());
|
|
13411
13557
|
this.trackTail(this.seedHeldStates());
|
|
13412
13558
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13447,7 +13593,7 @@ var PaymentsFacade = class {
|
|
|
13447
13593
|
}
|
|
13448
13594
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
13449
13595
|
send(request) {
|
|
13450
|
-
return this.track(this.
|
|
13596
|
+
return this.track(this.sendOutcome(request));
|
|
13451
13597
|
}
|
|
13452
13598
|
async receive() {
|
|
13453
13599
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -13462,20 +13608,27 @@ var PaymentsFacade = class {
|
|
|
13462
13608
|
if (!this.started) return;
|
|
13463
13609
|
await this.track(this.runConvergencePass());
|
|
13464
13610
|
}
|
|
13611
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
13612
|
+
async handleEpochChange(_newEpoch) {
|
|
13613
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
13614
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
13615
|
+
}
|
|
13465
13616
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
13466
13617
|
runConvergencePass() {
|
|
13467
|
-
return this.resumeFlight.run(
|
|
13468
|
-
|
|
13469
|
-
|
|
13470
|
-
|
|
13471
|
-
|
|
13618
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
13619
|
+
}
|
|
13620
|
+
async convergeBody() {
|
|
13621
|
+
const outcome = await this.converger.convergeOnce();
|
|
13622
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13623
|
+
this.heartbeat.settle(outcome, pending);
|
|
13472
13624
|
}
|
|
13473
13625
|
nowMs() {
|
|
13474
13626
|
return (this.deps.now ?? Date.now)();
|
|
13475
13627
|
}
|
|
13476
13628
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
13477
|
-
|
|
13478
|
-
|
|
13629
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
13630
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
13631
|
+
async sendOutcome(request) {
|
|
13479
13632
|
const run = {
|
|
13480
13633
|
amount: request.amount,
|
|
13481
13634
|
delivered: [],
|
|
@@ -13483,12 +13636,36 @@ var PaymentsFacade = class {
|
|
|
13483
13636
|
tokenTransfers: [],
|
|
13484
13637
|
settledShortfalls: [],
|
|
13485
13638
|
firstPartialId: void 0,
|
|
13486
|
-
pendingAny: false
|
|
13639
|
+
pendingAny: false,
|
|
13640
|
+
lastTransferId: ""
|
|
13487
13641
|
};
|
|
13642
|
+
try {
|
|
13643
|
+
return await this.sendWithPolicy(request, run);
|
|
13644
|
+
} catch (err) {
|
|
13645
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
13646
|
+
throw err;
|
|
13647
|
+
}
|
|
13648
|
+
}
|
|
13649
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
13650
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
13651
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
13652
|
+
emitCleanFailure(err, transferId) {
|
|
13653
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
13654
|
+
const failed = {
|
|
13655
|
+
id: transferId,
|
|
13656
|
+
status: "failed",
|
|
13657
|
+
tokens: [],
|
|
13658
|
+
tokenTransfers: [],
|
|
13659
|
+
error: messageOf(err)
|
|
13660
|
+
};
|
|
13661
|
+
this.deps.emit("transfer:updated", failed);
|
|
13662
|
+
}
|
|
13663
|
+
async sendWithPolicy(request, run) {
|
|
13664
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
13488
13665
|
for (let attempt = 0; ; attempt++) {
|
|
13489
13666
|
let ctx;
|
|
13490
13667
|
try {
|
|
13491
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
13668
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
13492
13669
|
} catch (err) {
|
|
13493
13670
|
throw this.partialize(err, run);
|
|
13494
13671
|
}
|
|
@@ -13533,11 +13710,8 @@ var PaymentsFacade = class {
|
|
|
13533
13710
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
13534
13711
|
}
|
|
13535
13712
|
}
|
|
13536
|
-
/**
|
|
13537
|
-
*
|
|
13538
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
13539
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
13540
|
-
*/
|
|
13713
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
13714
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
13541
13715
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
13542
13716
|
if (disposition.deliveryPending) {
|
|
13543
13717
|
run.pendingAny = true;
|
|
@@ -13563,11 +13737,8 @@ var PaymentsFacade = class {
|
|
|
13563
13737
|
run.amount = partial.remainingAmount;
|
|
13564
13738
|
return null;
|
|
13565
13739
|
}
|
|
13566
|
-
/**
|
|
13567
|
-
*
|
|
13568
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
13569
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
13570
|
-
*/
|
|
13740
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
13741
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
13571
13742
|
async disposeFailedAttempt(ctx, err) {
|
|
13572
13743
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
13573
13744
|
this.settleKeepOpen(ctx);
|
|
@@ -13616,8 +13787,9 @@ var PaymentsFacade = class {
|
|
|
13616
13787
|
}
|
|
13617
13788
|
return { kind: "rethrow", error: err };
|
|
13618
13789
|
}
|
|
13619
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
13790
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
13620
13791
|
const transferId = this.newId();
|
|
13792
|
+
run.lastTransferId = transferId;
|
|
13621
13793
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
13622
13794
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
13623
13795
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -13749,11 +13921,7 @@ var PaymentsFacade = class {
|
|
|
13749
13921
|
this.deps.emit("transfer:updated", result);
|
|
13750
13922
|
return result;
|
|
13751
13923
|
}
|
|
13752
|
-
/**
|
|
13753
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
13754
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
13755
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
13756
|
-
*/
|
|
13924
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
13757
13925
|
partialize(err, run) {
|
|
13758
13926
|
if (run.delivered.length === 0) return err;
|
|
13759
13927
|
return new PartialSendConflictError(
|
|
@@ -13905,22 +14073,14 @@ var PaymentsFacade = class {
|
|
|
13905
14073
|
}
|
|
13906
14074
|
}
|
|
13907
14075
|
toUiToken(tokenId, coinId, amount) {
|
|
13908
|
-
const registry = this.deps.registry;
|
|
13909
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
13910
14076
|
const now = this.nowMs();
|
|
13911
|
-
return
|
|
13912
|
-
|
|
13913
|
-
|
|
13914
|
-
|
|
13915
|
-
|
|
13916
|
-
|
|
13917
|
-
|
|
13918
|
-
amount: amount.toString(),
|
|
13919
|
-
status: "transferring",
|
|
13920
|
-
createdAt: now,
|
|
13921
|
-
updatedAt: now,
|
|
13922
|
-
lazy: true
|
|
13923
|
-
};
|
|
14077
|
+
return toToken(
|
|
14078
|
+
tokenId,
|
|
14079
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
14080
|
+
{ coinId, amount: amount.toString() },
|
|
14081
|
+
this.deps.registry,
|
|
14082
|
+
{ transferring: true, suspectedSpent: false }
|
|
14083
|
+
);
|
|
13924
14084
|
}
|
|
13925
14085
|
track(op) {
|
|
13926
14086
|
const settled = op.then(
|
|
@@ -13935,27 +14095,6 @@ var PaymentsFacade = class {
|
|
|
13935
14095
|
this.track(op).catch(() => void 0);
|
|
13936
14096
|
}
|
|
13937
14097
|
};
|
|
13938
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
13939
|
-
return {
|
|
13940
|
-
v: 2,
|
|
13941
|
-
recipient: recipientPubkey,
|
|
13942
|
-
coinId: request.coinId,
|
|
13943
|
-
amount: request.amount,
|
|
13944
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
13945
|
-
direct: [...spend.plan.direct],
|
|
13946
|
-
...spend.plan.split !== void 0 ? {
|
|
13947
|
-
split: {
|
|
13948
|
-
tokenId: spend.plan.split.tokenId,
|
|
13949
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
13950
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
13951
|
-
}
|
|
13952
|
-
} : {},
|
|
13953
|
-
spentStates
|
|
13954
|
-
};
|
|
13955
|
-
}
|
|
13956
|
-
function messageOf(err) {
|
|
13957
|
-
return err instanceof Error ? err.message : String(err);
|
|
13958
|
-
}
|
|
13959
14098
|
|
|
13960
14099
|
// impl/wallet-api-v2/http.ts
|
|
13961
14100
|
var WalletApiHttpError = class extends Error {
|
|
@@ -14301,6 +14440,7 @@ var WalletApiSession = class {
|
|
|
14301
14440
|
epochLatch = null;
|
|
14302
14441
|
reseedChain = Promise.resolve();
|
|
14303
14442
|
reseedsPending = 0;
|
|
14443
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
14304
14444
|
handlers = /* @__PURE__ */ new Map();
|
|
14305
14445
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
14306
14446
|
socket = null;
|
|
@@ -14403,6 +14543,18 @@ var WalletApiSession = class {
|
|
|
14403
14543
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
14404
14544
|
}
|
|
14405
14545
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
14546
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
14547
|
+
currentEpoch() {
|
|
14548
|
+
return this.epochLatch ?? "";
|
|
14549
|
+
}
|
|
14550
|
+
/**
|
|
14551
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
14552
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
14553
|
+
*/
|
|
14554
|
+
subscribeEpochChange(handler) {
|
|
14555
|
+
this.epochHandlers.add(handler);
|
|
14556
|
+
return () => this.epochHandlers.delete(handler);
|
|
14557
|
+
}
|
|
14406
14558
|
noteEpoch(epoch) {
|
|
14407
14559
|
if (this.epochLatch === epoch) return;
|
|
14408
14560
|
const previous = this.epochLatch;
|
|
@@ -14416,7 +14568,7 @@ var WalletApiSession = class {
|
|
|
14416
14568
|
}
|
|
14417
14569
|
async runReseed(epoch, previous) {
|
|
14418
14570
|
try {
|
|
14419
|
-
|
|
14571
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
14420
14572
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
14421
14573
|
} catch {
|
|
14422
14574
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -14678,6 +14830,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14678
14830
|
wake;
|
|
14679
14831
|
deriveFn;
|
|
14680
14832
|
seenChain = new SerialChain();
|
|
14833
|
+
lastIncomingEpoch = null;
|
|
14681
14834
|
constructor(config) {
|
|
14682
14835
|
this.client = config.client;
|
|
14683
14836
|
this.kv = config.kv;
|
|
@@ -14772,6 +14925,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14772
14925
|
const seen = await this.readSeen();
|
|
14773
14926
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
14774
14927
|
for (; ; ) {
|
|
14928
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
14775
14929
|
for (const entry of page.entries) {
|
|
14776
14930
|
if (entry.status !== "unclaimed") continue;
|
|
14777
14931
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -14781,6 +14935,10 @@ var WalletApiDeliveryPort = class {
|
|
|
14781
14935
|
page = await this.client.listMailbox(page.cursor);
|
|
14782
14936
|
}
|
|
14783
14937
|
}
|
|
14938
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
14939
|
+
incomingEpoch() {
|
|
14940
|
+
return this.lastIncomingEpoch;
|
|
14941
|
+
}
|
|
14784
14942
|
toIncoming(entry) {
|
|
14785
14943
|
const bundle = this.openEnvelope(entry);
|
|
14786
14944
|
return {
|
|
@@ -14858,7 +15016,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14858
15016
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
14859
15017
|
}
|
|
14860
15018
|
async put(transferId, opIndex, bytes) {
|
|
14861
|
-
const {
|
|
15019
|
+
const { kv, fieldKey } = this.config;
|
|
14862
15020
|
const aad = this.aad(transferId, opIndex);
|
|
14863
15021
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
14864
15022
|
let envelope = await kv.get(cacheKey);
|
|
@@ -14866,10 +15024,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14866
15024
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
14867
15025
|
await kv.set(cacheKey, envelope);
|
|
14868
15026
|
}
|
|
14869
|
-
const
|
|
14870
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
15027
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
14871
15028
|
return this.open(record.payload, aad);
|
|
14872
15029
|
}
|
|
15030
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
15031
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
15032
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
15033
|
+
if (envelope === null) return false;
|
|
15034
|
+
await this.post(transferId, opIndex, envelope);
|
|
15035
|
+
return true;
|
|
15036
|
+
}
|
|
15037
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
15038
|
+
async post(transferId, opIndex, envelope) {
|
|
15039
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
15040
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
15041
|
+
}
|
|
14873
15042
|
async get(transferId, opIndex) {
|
|
14874
15043
|
const records = await this.config.client.getProgress(transferId);
|
|
14875
15044
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -14892,7 +15061,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14892
15061
|
};
|
|
14893
15062
|
|
|
14894
15063
|
// core/payments-v2-wiring.ts
|
|
14895
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
15064
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
14896
15065
|
const source = walletApi;
|
|
14897
15066
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
14898
15067
|
if (!source || network === null) {
|
|
@@ -14901,6 +15070,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
14901
15070
|
"INVALID_CONFIG"
|
|
14902
15071
|
);
|
|
14903
15072
|
}
|
|
15073
|
+
if (!(network in NETWORKS)) {
|
|
15074
|
+
throw new SphereError(
|
|
15075
|
+
`payments: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
15076
|
+
"INVALID_CONFIG"
|
|
15077
|
+
);
|
|
15078
|
+
}
|
|
15079
|
+
if (network !== sphereNetwork) {
|
|
15080
|
+
throw new SphereError(
|
|
15081
|
+
`payments: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
15082
|
+
"INVALID_CONFIG"
|
|
15083
|
+
);
|
|
15084
|
+
}
|
|
14904
15085
|
if (typeof source.paymentsV2Transport === "function") {
|
|
14905
15086
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
14906
15087
|
}
|
|
@@ -14965,8 +15146,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
14965
15146
|
deviceId: `${config.deviceId}:pv2`,
|
|
14966
15147
|
kv: args.kv,
|
|
14967
15148
|
webSocketFactory,
|
|
14968
|
-
emitStatus: args.emitStatus
|
|
14969
|
-
onEpochChange: args.onEpochChange
|
|
15149
|
+
emitStatus: args.emitStatus
|
|
14970
15150
|
});
|
|
14971
15151
|
return { session, client: authedWireClient(session, client) };
|
|
14972
15152
|
};
|
|
@@ -15036,8 +15216,7 @@ function composePaymentsV2(spec) {
|
|
|
15036
15216
|
identity: identitySlice,
|
|
15037
15217
|
network,
|
|
15038
15218
|
kv,
|
|
15039
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15040
|
-
onEpochChange: async () => void 0
|
|
15219
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15041
15220
|
});
|
|
15042
15221
|
const deps = {
|
|
15043
15222
|
session: transport.session,
|
|
@@ -15067,7 +15246,10 @@ function composePaymentsV2(spec) {
|
|
|
15067
15246
|
network,
|
|
15068
15247
|
ownPubkey: identity.chainPubkey,
|
|
15069
15248
|
ownNametag: host.nametag,
|
|
15070
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
15249
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
15250
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
15251
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
15252
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
15071
15253
|
};
|
|
15072
15254
|
return new PaymentsFacade(deps);
|
|
15073
15255
|
}
|
|
@@ -15222,7 +15404,7 @@ var Sphere = class _Sphere {
|
|
|
15222
15404
|
static async init(options) {
|
|
15223
15405
|
if (options.debug) logger.configure({ debug: true });
|
|
15224
15406
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15225
|
-
resolvePaymentsV2Composition(options.walletApi);
|
|
15407
|
+
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15226
15408
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
15227
15409
|
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
15228
15410
|
const market = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -15364,7 +15546,7 @@ var Sphere = class _Sphere {
|
|
|
15364
15546
|
static async create(options) {
|
|
15365
15547
|
if (options.debug) logger.configure({ debug: true });
|
|
15366
15548
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15367
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15549
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15368
15550
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
15369
15551
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
15370
15552
|
}
|
|
@@ -15432,7 +15614,7 @@ var Sphere = class _Sphere {
|
|
|
15432
15614
|
static async load(options) {
|
|
15433
15615
|
if (options.debug) logger.configure({ debug: true });
|
|
15434
15616
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15435
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15617
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15436
15618
|
if (!await _Sphere.exists(options.storage)) {
|
|
15437
15619
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
15438
15620
|
}
|
|
@@ -15486,7 +15668,7 @@ var Sphere = class _Sphere {
|
|
|
15486
15668
|
static async import(options) {
|
|
15487
15669
|
if (options.debug) logger.configure({ debug: true });
|
|
15488
15670
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15489
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15671
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15490
15672
|
if (!options.mnemonic && !options.masterKey) {
|
|
15491
15673
|
throw new SphereError("Either mnemonic or masterKey is required", "INVALID_CONFIG");
|
|
15492
15674
|
}
|