@unicitylabs/sphere-sdk 0.14.1-dev.1 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/index.cjs
CHANGED
|
@@ -11228,7 +11228,10 @@ var STORE_KEYS = {
|
|
|
11228
11228
|
shortfalls: "shortfalls",
|
|
11229
11229
|
settlingLinks: "settling",
|
|
11230
11230
|
streamCursor: (s) => `cursor:${s}`,
|
|
11231
|
-
epochLatch: "epoch-latch"
|
|
11231
|
+
epochLatch: "epoch-latch",
|
|
11232
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
11233
|
+
suspectedSpent: "suspected-spent",
|
|
11234
|
+
knownSpends: "known-spends"
|
|
11232
11235
|
};
|
|
11233
11236
|
|
|
11234
11237
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -11478,7 +11481,7 @@ var TransferMachine = class {
|
|
|
11478
11481
|
}
|
|
11479
11482
|
if (cls === null) {
|
|
11480
11483
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
11481
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
11484
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
11482
11485
|
return { committed, deliveryPending };
|
|
11483
11486
|
}
|
|
11484
11487
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -11504,10 +11507,10 @@ var TransferMachine = class {
|
|
|
11504
11507
|
}
|
|
11505
11508
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
11506
11509
|
if (conflicts.length > 0) {
|
|
11507
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
11510
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
11508
11511
|
}
|
|
11509
11512
|
await this.completeIntent(r.transferId, r.payload);
|
|
11510
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11513
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11511
11514
|
return { deliveryPending, partial: null };
|
|
11512
11515
|
}
|
|
11513
11516
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -11689,7 +11692,7 @@ var TransferMachine = class {
|
|
|
11689
11692
|
}
|
|
11690
11693
|
return added;
|
|
11691
11694
|
}
|
|
11692
|
-
async settlePartial(r, committed, conflicts) {
|
|
11695
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
11693
11696
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
11694
11697
|
const entry = {
|
|
11695
11698
|
transferId: r.transferId,
|
|
@@ -11701,26 +11704,27 @@ var TransferMachine = class {
|
|
|
11701
11704
|
};
|
|
11702
11705
|
await this.stores.shortfalls.upsert(entry);
|
|
11703
11706
|
await this.completeIntent(r.transferId, r.payload);
|
|
11704
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11707
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11705
11708
|
return entry;
|
|
11706
11709
|
}
|
|
11707
|
-
closeTail(transferId, payload) {
|
|
11710
|
+
closeTail(engine, transferId, payload, committed) {
|
|
11708
11711
|
void (async () => {
|
|
11709
11712
|
try {
|
|
11710
11713
|
await this.completeIntent(transferId, payload);
|
|
11711
11714
|
} catch {
|
|
11712
11715
|
return;
|
|
11713
11716
|
}
|
|
11714
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
11717
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
11715
11718
|
})();
|
|
11716
11719
|
}
|
|
11717
11720
|
async completeIntent(transferId, payload) {
|
|
11718
11721
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
11719
11722
|
await this.deps.intents.complete(transferId, signature);
|
|
11720
11723
|
}
|
|
11721
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
11724
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
11722
11725
|
try {
|
|
11723
|
-
await this.
|
|
11726
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
11727
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
11724
11728
|
} catch {
|
|
11725
11729
|
}
|
|
11726
11730
|
try {
|
|
@@ -11728,6 +11732,26 @@ var TransferMachine = class {
|
|
|
11728
11732
|
} catch {
|
|
11729
11733
|
}
|
|
11730
11734
|
}
|
|
11735
|
+
/**
|
|
11736
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
11737
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
11738
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
11739
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
11740
|
+
*/
|
|
11741
|
+
async settledAmount(engine, coinId, committed) {
|
|
11742
|
+
let total = 0n;
|
|
11743
|
+
for (const outcome of committed) {
|
|
11744
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
11745
|
+
const token = await engine.decodeToken({
|
|
11746
|
+
v: TOKEN_BLOB_VERSION,
|
|
11747
|
+
network: 0,
|
|
11748
|
+
tokenId: "",
|
|
11749
|
+
token: outcome.recipientBlob
|
|
11750
|
+
});
|
|
11751
|
+
total += engine.balanceOf(token, coinId);
|
|
11752
|
+
}
|
|
11753
|
+
return total.toString();
|
|
11754
|
+
}
|
|
11731
11755
|
async disposeOnFailure(transferId, committed, cls) {
|
|
11732
11756
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
11733
11757
|
try {
|
|
@@ -11752,7 +11776,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
11752
11776
|
}
|
|
11753
11777
|
|
|
11754
11778
|
// modules/payments-v2/machine/resume.ts
|
|
11755
|
-
async function resumeAll(deps) {
|
|
11779
|
+
async function resumeAll(deps, options = {}) {
|
|
11756
11780
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
11757
11781
|
const stores = createMachineStores(deps.kv);
|
|
11758
11782
|
const open = await deps.intents.listOpen();
|
|
@@ -11762,7 +11786,12 @@ async function resumeAll(deps) {
|
|
|
11762
11786
|
} catch {
|
|
11763
11787
|
return report;
|
|
11764
11788
|
}
|
|
11765
|
-
await
|
|
11789
|
+
await dropStaleBackstops(
|
|
11790
|
+
stores,
|
|
11791
|
+
locals,
|
|
11792
|
+
new Set(open.map((i) => i.transferId)),
|
|
11793
|
+
options.isLocallyActive ?? (() => false)
|
|
11794
|
+
);
|
|
11766
11795
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
11767
11796
|
const ctx = {
|
|
11768
11797
|
deps,
|
|
@@ -11775,11 +11804,16 @@ async function resumeAll(deps) {
|
|
|
11775
11804
|
}
|
|
11776
11805
|
return report;
|
|
11777
11806
|
}
|
|
11778
|
-
async function
|
|
11807
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
11779
11808
|
for (const entry of locals.values()) {
|
|
11780
|
-
if (
|
|
11809
|
+
if (openIds.has(entry.transferId)) continue;
|
|
11810
|
+
if (entry.disposition !== "open") {
|
|
11781
11811
|
await stores.backstop.removeByKey(entry.transferId);
|
|
11812
|
+
continue;
|
|
11782
11813
|
}
|
|
11814
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
11815
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
11816
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
11783
11817
|
}
|
|
11784
11818
|
}
|
|
11785
11819
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -11987,7 +12021,9 @@ var Converger = class {
|
|
|
11987
12021
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
11988
12022
|
}
|
|
11989
12023
|
async resumePass() {
|
|
11990
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
12024
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
12025
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
12026
|
+
});
|
|
11991
12027
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
11992
12028
|
await this.deps.reconcile({
|
|
11993
12029
|
resumed: report.resumed,
|
|
@@ -12103,6 +12139,136 @@ function distinctOpCount(entries) {
|
|
|
12103
12139
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12104
12140
|
}
|
|
12105
12141
|
|
|
12142
|
+
// modules/payments-v2/restore.ts
|
|
12143
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
12144
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
12145
|
+
async function reseedAndReset(deps) {
|
|
12146
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
12147
|
+
if (entry.disposition !== "open") continue;
|
|
12148
|
+
try {
|
|
12149
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
12150
|
+
if (entry.requiresSeedClose) {
|
|
12151
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
12152
|
+
}
|
|
12153
|
+
} catch (err) {
|
|
12154
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
12155
|
+
}
|
|
12156
|
+
}
|
|
12157
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
12158
|
+
await deps.fullRePull();
|
|
12159
|
+
}
|
|
12160
|
+
async function splitOpIndex(deps, envelope) {
|
|
12161
|
+
const payload = await deps.decryptPayload(envelope);
|
|
12162
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12163
|
+
}
|
|
12164
|
+
|
|
12165
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12166
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12167
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12168
|
+
return {
|
|
12169
|
+
id: tokenId,
|
|
12170
|
+
coinId: asset.coinId,
|
|
12171
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12172
|
+
name: registry.getName(asset.coinId),
|
|
12173
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12174
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12175
|
+
amount: asset.amount,
|
|
12176
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12177
|
+
createdAt: snapshot.createdAt,
|
|
12178
|
+
updatedAt: snapshot.updatedAt,
|
|
12179
|
+
lazy: true,
|
|
12180
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12181
|
+
};
|
|
12182
|
+
}
|
|
12183
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12184
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12185
|
+
for (const [tokenId, entry] of entries) {
|
|
12186
|
+
const moving = isTransferring(tokenId);
|
|
12187
|
+
for (const asset of entry.assets) {
|
|
12188
|
+
let group = groups.get(asset.coinId);
|
|
12189
|
+
if (!group) {
|
|
12190
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12191
|
+
groups.set(asset.coinId, group);
|
|
12192
|
+
}
|
|
12193
|
+
if (moving) {
|
|
12194
|
+
group.transferring += BigInt(asset.amount);
|
|
12195
|
+
group.transferringCount += 1;
|
|
12196
|
+
} else {
|
|
12197
|
+
group.total += BigInt(asset.amount);
|
|
12198
|
+
group.count += 1;
|
|
12199
|
+
}
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12203
|
+
}
|
|
12204
|
+
function toAsset(coinId, totals, registry) {
|
|
12205
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12206
|
+
return {
|
|
12207
|
+
coinId,
|
|
12208
|
+
symbol: registry.getSymbol(coinId),
|
|
12209
|
+
name: registry.getName(coinId),
|
|
12210
|
+
decimals: registry.getDecimals(coinId),
|
|
12211
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12212
|
+
totalAmount: totals.total.toString(),
|
|
12213
|
+
tokenCount: totals.count,
|
|
12214
|
+
confirmedAmount: totals.total.toString(),
|
|
12215
|
+
unconfirmedAmount: "0",
|
|
12216
|
+
confirmedTokenCount: totals.count,
|
|
12217
|
+
unconfirmedTokenCount: 0,
|
|
12218
|
+
transferringTokenCount: totals.transferringCount,
|
|
12219
|
+
transferringAmount: totals.transferring.toString(),
|
|
12220
|
+
priceUsd: null,
|
|
12221
|
+
priceEur: null,
|
|
12222
|
+
change24h: null,
|
|
12223
|
+
fiatValueUsd: null,
|
|
12224
|
+
fiatValueEur: null
|
|
12225
|
+
};
|
|
12226
|
+
}
|
|
12227
|
+
async function withPrices(raw, registry, price) {
|
|
12228
|
+
try {
|
|
12229
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12230
|
+
const quotes = await price.getPrices(names);
|
|
12231
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12232
|
+
} catch {
|
|
12233
|
+
return raw;
|
|
12234
|
+
}
|
|
12235
|
+
}
|
|
12236
|
+
function priceOne(asset, quote) {
|
|
12237
|
+
if (!quote) return asset;
|
|
12238
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12239
|
+
return {
|
|
12240
|
+
...asset,
|
|
12241
|
+
priceUsd: quote.priceUsd,
|
|
12242
|
+
priceEur: quote.priceEur ?? null,
|
|
12243
|
+
change24h: quote.change24h ?? null,
|
|
12244
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12245
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12246
|
+
};
|
|
12247
|
+
}
|
|
12248
|
+
|
|
12249
|
+
// modules/payments-v2/machine/payload.ts
|
|
12250
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12251
|
+
return {
|
|
12252
|
+
v: 2,
|
|
12253
|
+
recipient: recipientPubkey,
|
|
12254
|
+
coinId: request.coinId,
|
|
12255
|
+
amount: request.amount,
|
|
12256
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12257
|
+
direct: [...spend.plan.direct],
|
|
12258
|
+
...spend.plan.split !== void 0 ? {
|
|
12259
|
+
split: {
|
|
12260
|
+
tokenId: spend.plan.split.tokenId,
|
|
12261
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12262
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12263
|
+
}
|
|
12264
|
+
} : {},
|
|
12265
|
+
spentStates
|
|
12266
|
+
};
|
|
12267
|
+
}
|
|
12268
|
+
function messageOf(err) {
|
|
12269
|
+
return err instanceof Error ? err.message : String(err);
|
|
12270
|
+
}
|
|
12271
|
+
|
|
12106
12272
|
// modules/payments-v2/history/History.ts
|
|
12107
12273
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12108
12274
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -12161,13 +12327,14 @@ var History = class {
|
|
|
12161
12327
|
}
|
|
12162
12328
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
12163
12329
|
async post(build) {
|
|
12330
|
+
const record = build();
|
|
12164
12331
|
try {
|
|
12165
|
-
await this.deps.client.postHistory([
|
|
12332
|
+
await this.deps.client.postHistory([record]);
|
|
12166
12333
|
} catch (err) {
|
|
12167
12334
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
12168
12335
|
return;
|
|
12169
12336
|
}
|
|
12170
|
-
this.deps.emit("history:updated");
|
|
12337
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
12171
12338
|
}
|
|
12172
12339
|
ts(timestamp) {
|
|
12173
12340
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -12224,93 +12391,9 @@ var History = class {
|
|
|
12224
12391
|
}
|
|
12225
12392
|
};
|
|
12226
12393
|
|
|
12227
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
12228
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12229
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12230
|
-
return {
|
|
12231
|
-
id: tokenId,
|
|
12232
|
-
coinId: asset.coinId,
|
|
12233
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12234
|
-
name: registry.getName(asset.coinId),
|
|
12235
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12236
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12237
|
-
amount: asset.amount,
|
|
12238
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12239
|
-
createdAt: snapshot.createdAt,
|
|
12240
|
-
updatedAt: snapshot.updatedAt,
|
|
12241
|
-
lazy: true,
|
|
12242
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12243
|
-
};
|
|
12244
|
-
}
|
|
12245
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12246
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12247
|
-
for (const [tokenId, entry] of entries) {
|
|
12248
|
-
const moving = isTransferring(tokenId);
|
|
12249
|
-
for (const asset of entry.assets) {
|
|
12250
|
-
let group = groups.get(asset.coinId);
|
|
12251
|
-
if (!group) {
|
|
12252
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12253
|
-
groups.set(asset.coinId, group);
|
|
12254
|
-
}
|
|
12255
|
-
if (moving) {
|
|
12256
|
-
group.transferring += BigInt(asset.amount);
|
|
12257
|
-
group.transferringCount += 1;
|
|
12258
|
-
} else {
|
|
12259
|
-
group.total += BigInt(asset.amount);
|
|
12260
|
-
group.count += 1;
|
|
12261
|
-
}
|
|
12262
|
-
}
|
|
12263
|
-
}
|
|
12264
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12265
|
-
}
|
|
12266
|
-
function toAsset(coinId, totals, registry) {
|
|
12267
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12268
|
-
return {
|
|
12269
|
-
coinId,
|
|
12270
|
-
symbol: registry.getSymbol(coinId),
|
|
12271
|
-
name: registry.getName(coinId),
|
|
12272
|
-
decimals: registry.getDecimals(coinId),
|
|
12273
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12274
|
-
totalAmount: totals.total.toString(),
|
|
12275
|
-
tokenCount: totals.count,
|
|
12276
|
-
confirmedAmount: totals.total.toString(),
|
|
12277
|
-
unconfirmedAmount: "0",
|
|
12278
|
-
confirmedTokenCount: totals.count,
|
|
12279
|
-
unconfirmedTokenCount: 0,
|
|
12280
|
-
transferringTokenCount: totals.transferringCount,
|
|
12281
|
-
transferringAmount: totals.transferring.toString(),
|
|
12282
|
-
priceUsd: null,
|
|
12283
|
-
priceEur: null,
|
|
12284
|
-
change24h: null,
|
|
12285
|
-
fiatValueUsd: null,
|
|
12286
|
-
fiatValueEur: null
|
|
12287
|
-
};
|
|
12288
|
-
}
|
|
12289
|
-
async function withPrices(raw, registry, price) {
|
|
12290
|
-
try {
|
|
12291
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12292
|
-
const quotes = await price.getPrices(names);
|
|
12293
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12294
|
-
} catch {
|
|
12295
|
-
return raw;
|
|
12296
|
-
}
|
|
12297
|
-
}
|
|
12298
|
-
function priceOne(asset, quote) {
|
|
12299
|
-
if (!quote) return asset;
|
|
12300
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12301
|
-
return {
|
|
12302
|
-
...asset,
|
|
12303
|
-
priceUsd: quote.priceUsd,
|
|
12304
|
-
priceEur: quote.priceEur ?? null,
|
|
12305
|
-
change24h: quote.change24h ?? null,
|
|
12306
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12307
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12308
|
-
};
|
|
12309
|
-
}
|
|
12310
|
-
|
|
12311
12394
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
12312
|
-
var SUSPECTED_KEY =
|
|
12313
|
-
var KNOWN_SPENDS_KEY =
|
|
12395
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
12396
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
12314
12397
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
12315
12398
|
function stateKey(tokenId, stateHash) {
|
|
12316
12399
|
return `${tokenId}:${stateHash}`;
|
|
@@ -12614,19 +12697,24 @@ var Receive = class {
|
|
|
12614
12697
|
const engine = deps.engine();
|
|
12615
12698
|
const stored = [];
|
|
12616
12699
|
const pending = [];
|
|
12617
|
-
|
|
12700
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
12618
12701
|
try {
|
|
12619
|
-
epoch = deps.syncEpoch();
|
|
12620
12702
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
12621
|
-
|
|
12622
|
-
for
|
|
12623
|
-
await
|
|
12624
|
-
|
|
12703
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
12704
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
12705
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
12706
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
12707
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
12708
|
+
}
|
|
12709
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
12710
|
+
const served = deps.delivery.incomingEpoch();
|
|
12711
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
12712
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
12713
|
+
basis = null;
|
|
12625
12714
|
}
|
|
12626
|
-
await flushAcks(deps, pending, epoch);
|
|
12627
12715
|
} catch (err) {
|
|
12628
12716
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
12629
|
-
await flushAcks(deps, pending,
|
|
12717
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
12630
12718
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
12631
12719
|
});
|
|
12632
12720
|
}
|
|
@@ -12699,7 +12787,7 @@ async function announce(deps, entry, record) {
|
|
|
12699
12787
|
receivedAt
|
|
12700
12788
|
};
|
|
12701
12789
|
}
|
|
12702
|
-
async function flushAcks(deps, pending,
|
|
12790
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
12703
12791
|
let lastAcked = null;
|
|
12704
12792
|
try {
|
|
12705
12793
|
while (pending.length > 0) {
|
|
@@ -12710,7 +12798,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
12710
12798
|
}
|
|
12711
12799
|
} finally {
|
|
12712
12800
|
if (lastAcked !== null) {
|
|
12713
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
12801
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
12714
12802
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
12715
12803
|
}
|
|
12716
12804
|
}
|
|
@@ -12811,9 +12899,19 @@ var Requests = class {
|
|
|
12811
12899
|
}
|
|
12812
12900
|
});
|
|
12813
12901
|
}
|
|
12902
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
12814
12903
|
writeLink(requestId, transferId, committed) {
|
|
12815
12904
|
return this.mutateJournal((m) => {
|
|
12816
|
-
|
|
12905
|
+
const prev = m.get(requestId);
|
|
12906
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
12907
|
+
const next = {
|
|
12908
|
+
requestId,
|
|
12909
|
+
transferId,
|
|
12910
|
+
committed: committed || same && prev.committed,
|
|
12911
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
12912
|
+
};
|
|
12913
|
+
if (same && prev.committed === next.committed) return false;
|
|
12914
|
+
m.set(requestId, next);
|
|
12817
12915
|
return true;
|
|
12818
12916
|
});
|
|
12819
12917
|
}
|
|
@@ -12966,23 +13064,46 @@ var Requests = class {
|
|
|
12966
13064
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
12967
13065
|
if (transferId !== void 0) {
|
|
12968
13066
|
const committed = error instanceof PartialSendConflictError;
|
|
12969
|
-
await this.
|
|
12970
|
-
this.setStatus(id, "settling");
|
|
13067
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
12971
13068
|
} else {
|
|
12972
13069
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
12973
13070
|
this.setStatus(id, "paid");
|
|
12974
13071
|
}
|
|
12975
13072
|
} else {
|
|
12976
|
-
await this.
|
|
12977
|
-
this.setStatus(id, "pending");
|
|
13073
|
+
await this.revertPayable(id);
|
|
12978
13074
|
}
|
|
12979
13075
|
throw error;
|
|
12980
13076
|
}
|
|
12981
|
-
this.
|
|
12982
|
-
await this.respondPaid(id, result.id);
|
|
12983
|
-
await this.clearLink(id);
|
|
13077
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
12984
13078
|
return result;
|
|
12985
13079
|
}
|
|
13080
|
+
/**
|
|
13081
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
13082
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
13083
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
13084
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
13085
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
13086
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
13087
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
13088
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
13089
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
13090
|
+
*/
|
|
13091
|
+
async settle(requestId, transferId, opts) {
|
|
13092
|
+
let journalDown = false;
|
|
13093
|
+
try {
|
|
13094
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
13095
|
+
} catch (err) {
|
|
13096
|
+
if (!opts.respond) throw err;
|
|
13097
|
+
journalDown = true;
|
|
13098
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
13099
|
+
}
|
|
13100
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
13101
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
13102
|
+
return;
|
|
13103
|
+
}
|
|
13104
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
13105
|
+
this.setStatus(requestId, "paid");
|
|
13106
|
+
}
|
|
12986
13107
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
12987
13108
|
async respondPaid(id, transferId) {
|
|
12988
13109
|
try {
|
|
@@ -13018,32 +13139,32 @@ var Requests = class {
|
|
|
13018
13139
|
let aborted = null;
|
|
13019
13140
|
for (const [requestId, link] of [...this.journal]) {
|
|
13020
13141
|
if (link.committed || resumed.has(link.transferId)) {
|
|
13021
|
-
await this.resolvePaid(requestId, link
|
|
13142
|
+
await this.resolvePaid(requestId, link);
|
|
13022
13143
|
} else if (conflicted.has(link.transferId)) {
|
|
13023
13144
|
await this.revertPayable(requestId);
|
|
13024
13145
|
} else if (open.has(link.transferId)) {
|
|
13025
13146
|
} else {
|
|
13026
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
13147
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
13027
13148
|
}
|
|
13028
13149
|
}
|
|
13029
13150
|
}
|
|
13030
13151
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
13031
13152
|
// a probe failure defers the link to the next session.
|
|
13032
|
-
async reconcileUnaccounted(requestId,
|
|
13153
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
13033
13154
|
try {
|
|
13034
13155
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
13035
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
13036
|
-
else await this.resolvePaid(requestId,
|
|
13156
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
13157
|
+
else await this.resolvePaid(requestId, link);
|
|
13037
13158
|
} catch (err) {
|
|
13038
13159
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
13039
13160
|
}
|
|
13040
13161
|
return aborted;
|
|
13041
13162
|
}
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
this.setStatus(requestId, "paid");
|
|
13163
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
13164
|
+
resolvePaid(requestId, link) {
|
|
13165
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
13046
13166
|
}
|
|
13167
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
13047
13168
|
async revertPayable(requestId) {
|
|
13048
13169
|
await this.clearLink(requestId);
|
|
13049
13170
|
this.setStatus(requestId, "pending");
|
|
@@ -13367,11 +13488,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13367
13488
|
client: deps.client,
|
|
13368
13489
|
fieldKey: deps.fieldKey,
|
|
13369
13490
|
registry: deps.registry,
|
|
13370
|
-
emit: (event) => deps.emit(event,
|
|
13491
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
13371
13492
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
13372
13493
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
13373
13494
|
});
|
|
13374
13495
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
13496
|
+
const machineStores = createMachineStores(deps.kv);
|
|
13375
13497
|
const heldStates = /* @__PURE__ */ new Map();
|
|
13376
13498
|
return {
|
|
13377
13499
|
ownPubkeyBytes,
|
|
@@ -13379,12 +13501,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13379
13501
|
ledger,
|
|
13380
13502
|
queue,
|
|
13381
13503
|
historyStore,
|
|
13382
|
-
machineStores
|
|
13504
|
+
machineStores,
|
|
13383
13505
|
machineDeps,
|
|
13384
13506
|
machine: new TransferMachine(machineDeps),
|
|
13385
13507
|
heldStates,
|
|
13386
13508
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
13387
|
-
requests: buildRequests(deps, hooks)
|
|
13509
|
+
requests: buildRequests(deps, hooks),
|
|
13510
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13511
|
+
};
|
|
13512
|
+
}
|
|
13513
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13514
|
+
return {
|
|
13515
|
+
stores: machineStores,
|
|
13516
|
+
kv: deps.kv,
|
|
13517
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
13518
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
13519
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
13520
|
+
fullRePull: () => view.onEpochReset(),
|
|
13521
|
+
attention: (transferId, code, detail) => {
|
|
13522
|
+
deps.emit(
|
|
13523
|
+
"transfer:attention",
|
|
13524
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13525
|
+
);
|
|
13526
|
+
}
|
|
13388
13527
|
};
|
|
13389
13528
|
}
|
|
13390
13529
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -13406,11 +13545,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
13406
13545
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
13407
13546
|
emit: deps.emit,
|
|
13408
13547
|
now: deps.now ?? Date.now,
|
|
13409
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
13548
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
13410
13549
|
await historyStore.recordSent({
|
|
13411
13550
|
transferId,
|
|
13412
13551
|
coinId: payload.coinId,
|
|
13413
|
-
|
|
13552
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
13553
|
+
// recipient blobs), never payload.amount — the plan.
|
|
13554
|
+
amount: committedAmount,
|
|
13414
13555
|
recipientPubkey: payload.recipient,
|
|
13415
13556
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
13416
13557
|
});
|
|
@@ -13437,7 +13578,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
13437
13578
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13438
13579
|
);
|
|
13439
13580
|
},
|
|
13440
|
-
syncEpoch: deps.syncEpoch
|
|
13581
|
+
syncEpoch: deps.syncEpoch,
|
|
13441
13582
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13442
13583
|
});
|
|
13443
13584
|
}
|
|
@@ -13490,6 +13631,7 @@ var PaymentsFacade = class {
|
|
|
13490
13631
|
this.receiveLoop = parts.receiveLoop;
|
|
13491
13632
|
this.heldStates = parts.heldStates;
|
|
13492
13633
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13634
|
+
this.restoreDeps = parts.restoreDeps;
|
|
13493
13635
|
this.requests = parts.requests;
|
|
13494
13636
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13495
13637
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -13518,6 +13660,7 @@ var PaymentsFacade = class {
|
|
|
13518
13660
|
receiveLoop;
|
|
13519
13661
|
heldStates;
|
|
13520
13662
|
ownPubkeyBytes;
|
|
13663
|
+
restoreDeps;
|
|
13521
13664
|
requests;
|
|
13522
13665
|
currentEngine = null;
|
|
13523
13666
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -13528,6 +13671,8 @@ var PaymentsFacade = class {
|
|
|
13528
13671
|
converger;
|
|
13529
13672
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
13530
13673
|
resumeFlight = new SingleFlight();
|
|
13674
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
13675
|
+
passChain = new SerialChain();
|
|
13531
13676
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13532
13677
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13533
13678
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -13535,8 +13680,8 @@ var PaymentsFacade = class {
|
|
|
13535
13680
|
if (this.started) return;
|
|
13536
13681
|
this.started = true;
|
|
13537
13682
|
this.heartbeat.start();
|
|
13538
|
-
await this.deps.session.start();
|
|
13539
13683
|
this.unsubscribers.push(
|
|
13684
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
13540
13685
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
13541
13686
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
13542
13687
|
}),
|
|
@@ -13551,6 +13696,7 @@ var PaymentsFacade = class {
|
|
|
13551
13696
|
if (status === "connected") this.heartbeat.recovered();
|
|
13552
13697
|
});
|
|
13553
13698
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13699
|
+
await this.deps.session.start();
|
|
13554
13700
|
this.trackTail(this.runConvergencePass());
|
|
13555
13701
|
this.trackTail(this.seedHeldStates());
|
|
13556
13702
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13591,7 +13737,7 @@ var PaymentsFacade = class {
|
|
|
13591
13737
|
}
|
|
13592
13738
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
13593
13739
|
send(request) {
|
|
13594
|
-
return this.track(this.
|
|
13740
|
+
return this.track(this.sendOutcome(request));
|
|
13595
13741
|
}
|
|
13596
13742
|
async receive() {
|
|
13597
13743
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -13606,20 +13752,27 @@ var PaymentsFacade = class {
|
|
|
13606
13752
|
if (!this.started) return;
|
|
13607
13753
|
await this.track(this.runConvergencePass());
|
|
13608
13754
|
}
|
|
13755
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
13756
|
+
async handleEpochChange(_newEpoch) {
|
|
13757
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
13758
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
13759
|
+
}
|
|
13609
13760
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
13610
13761
|
runConvergencePass() {
|
|
13611
|
-
return this.resumeFlight.run(
|
|
13612
|
-
|
|
13613
|
-
|
|
13614
|
-
|
|
13615
|
-
|
|
13762
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
13763
|
+
}
|
|
13764
|
+
async convergeBody() {
|
|
13765
|
+
const outcome = await this.converger.convergeOnce();
|
|
13766
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13767
|
+
this.heartbeat.settle(outcome, pending);
|
|
13616
13768
|
}
|
|
13617
13769
|
nowMs() {
|
|
13618
13770
|
return (this.deps.now ?? Date.now)();
|
|
13619
13771
|
}
|
|
13620
13772
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
13621
|
-
|
|
13622
|
-
|
|
13773
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
13774
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
13775
|
+
async sendOutcome(request) {
|
|
13623
13776
|
const run = {
|
|
13624
13777
|
amount: request.amount,
|
|
13625
13778
|
delivered: [],
|
|
@@ -13627,12 +13780,36 @@ var PaymentsFacade = class {
|
|
|
13627
13780
|
tokenTransfers: [],
|
|
13628
13781
|
settledShortfalls: [],
|
|
13629
13782
|
firstPartialId: void 0,
|
|
13630
|
-
pendingAny: false
|
|
13783
|
+
pendingAny: false,
|
|
13784
|
+
lastTransferId: ""
|
|
13631
13785
|
};
|
|
13786
|
+
try {
|
|
13787
|
+
return await this.sendWithPolicy(request, run);
|
|
13788
|
+
} catch (err) {
|
|
13789
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
13790
|
+
throw err;
|
|
13791
|
+
}
|
|
13792
|
+
}
|
|
13793
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
13794
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
13795
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
13796
|
+
emitCleanFailure(err, transferId) {
|
|
13797
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
13798
|
+
const failed = {
|
|
13799
|
+
id: transferId,
|
|
13800
|
+
status: "failed",
|
|
13801
|
+
tokens: [],
|
|
13802
|
+
tokenTransfers: [],
|
|
13803
|
+
error: messageOf(err)
|
|
13804
|
+
};
|
|
13805
|
+
this.deps.emit("transfer:updated", failed);
|
|
13806
|
+
}
|
|
13807
|
+
async sendWithPolicy(request, run) {
|
|
13808
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
13632
13809
|
for (let attempt = 0; ; attempt++) {
|
|
13633
13810
|
let ctx;
|
|
13634
13811
|
try {
|
|
13635
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
13812
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
13636
13813
|
} catch (err) {
|
|
13637
13814
|
throw this.partialize(err, run);
|
|
13638
13815
|
}
|
|
@@ -13677,11 +13854,8 @@ var PaymentsFacade = class {
|
|
|
13677
13854
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
13678
13855
|
}
|
|
13679
13856
|
}
|
|
13680
|
-
/**
|
|
13681
|
-
*
|
|
13682
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
13683
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
13684
|
-
*/
|
|
13857
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
13858
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
13685
13859
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
13686
13860
|
if (disposition.deliveryPending) {
|
|
13687
13861
|
run.pendingAny = true;
|
|
@@ -13707,11 +13881,8 @@ var PaymentsFacade = class {
|
|
|
13707
13881
|
run.amount = partial.remainingAmount;
|
|
13708
13882
|
return null;
|
|
13709
13883
|
}
|
|
13710
|
-
/**
|
|
13711
|
-
*
|
|
13712
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
13713
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
13714
|
-
*/
|
|
13884
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
13885
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
13715
13886
|
async disposeFailedAttempt(ctx, err) {
|
|
13716
13887
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
13717
13888
|
this.settleKeepOpen(ctx);
|
|
@@ -13760,8 +13931,9 @@ var PaymentsFacade = class {
|
|
|
13760
13931
|
}
|
|
13761
13932
|
return { kind: "rethrow", error: err };
|
|
13762
13933
|
}
|
|
13763
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
13934
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
13764
13935
|
const transferId = this.newId();
|
|
13936
|
+
run.lastTransferId = transferId;
|
|
13765
13937
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
13766
13938
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
13767
13939
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -13893,11 +14065,7 @@ var PaymentsFacade = class {
|
|
|
13893
14065
|
this.deps.emit("transfer:updated", result);
|
|
13894
14066
|
return result;
|
|
13895
14067
|
}
|
|
13896
|
-
/**
|
|
13897
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
13898
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
13899
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
13900
|
-
*/
|
|
14068
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
13901
14069
|
partialize(err, run) {
|
|
13902
14070
|
if (run.delivered.length === 0) return err;
|
|
13903
14071
|
return new PartialSendConflictError(
|
|
@@ -14049,22 +14217,14 @@ var PaymentsFacade = class {
|
|
|
14049
14217
|
}
|
|
14050
14218
|
}
|
|
14051
14219
|
toUiToken(tokenId, coinId, amount) {
|
|
14052
|
-
const registry = this.deps.registry;
|
|
14053
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
14054
14220
|
const now = this.nowMs();
|
|
14055
|
-
return
|
|
14056
|
-
|
|
14057
|
-
|
|
14058
|
-
|
|
14059
|
-
|
|
14060
|
-
|
|
14061
|
-
|
|
14062
|
-
amount: amount.toString(),
|
|
14063
|
-
status: "transferring",
|
|
14064
|
-
createdAt: now,
|
|
14065
|
-
updatedAt: now,
|
|
14066
|
-
lazy: true
|
|
14067
|
-
};
|
|
14221
|
+
return toToken(
|
|
14222
|
+
tokenId,
|
|
14223
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
14224
|
+
{ coinId, amount: amount.toString() },
|
|
14225
|
+
this.deps.registry,
|
|
14226
|
+
{ transferring: true, suspectedSpent: false }
|
|
14227
|
+
);
|
|
14068
14228
|
}
|
|
14069
14229
|
track(op) {
|
|
14070
14230
|
const settled = op.then(
|
|
@@ -14079,27 +14239,6 @@ var PaymentsFacade = class {
|
|
|
14079
14239
|
this.track(op).catch(() => void 0);
|
|
14080
14240
|
}
|
|
14081
14241
|
};
|
|
14082
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
14083
|
-
return {
|
|
14084
|
-
v: 2,
|
|
14085
|
-
recipient: recipientPubkey,
|
|
14086
|
-
coinId: request.coinId,
|
|
14087
|
-
amount: request.amount,
|
|
14088
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
14089
|
-
direct: [...spend.plan.direct],
|
|
14090
|
-
...spend.plan.split !== void 0 ? {
|
|
14091
|
-
split: {
|
|
14092
|
-
tokenId: spend.plan.split.tokenId,
|
|
14093
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
14094
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
14095
|
-
}
|
|
14096
|
-
} : {},
|
|
14097
|
-
spentStates
|
|
14098
|
-
};
|
|
14099
|
-
}
|
|
14100
|
-
function messageOf(err) {
|
|
14101
|
-
return err instanceof Error ? err.message : String(err);
|
|
14102
|
-
}
|
|
14103
14242
|
|
|
14104
14243
|
// impl/wallet-api-v2/http.ts
|
|
14105
14244
|
var WalletApiHttpError = class extends Error {
|
|
@@ -14445,6 +14584,7 @@ var WalletApiSession = class {
|
|
|
14445
14584
|
epochLatch = null;
|
|
14446
14585
|
reseedChain = Promise.resolve();
|
|
14447
14586
|
reseedsPending = 0;
|
|
14587
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
14448
14588
|
handlers = /* @__PURE__ */ new Map();
|
|
14449
14589
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
14450
14590
|
socket = null;
|
|
@@ -14547,6 +14687,18 @@ var WalletApiSession = class {
|
|
|
14547
14687
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
14548
14688
|
}
|
|
14549
14689
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
14690
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
14691
|
+
currentEpoch() {
|
|
14692
|
+
return this.epochLatch ?? "";
|
|
14693
|
+
}
|
|
14694
|
+
/**
|
|
14695
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
14696
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
14697
|
+
*/
|
|
14698
|
+
subscribeEpochChange(handler) {
|
|
14699
|
+
this.epochHandlers.add(handler);
|
|
14700
|
+
return () => this.epochHandlers.delete(handler);
|
|
14701
|
+
}
|
|
14550
14702
|
noteEpoch(epoch) {
|
|
14551
14703
|
if (this.epochLatch === epoch) return;
|
|
14552
14704
|
const previous = this.epochLatch;
|
|
@@ -14560,7 +14712,7 @@ var WalletApiSession = class {
|
|
|
14560
14712
|
}
|
|
14561
14713
|
async runReseed(epoch, previous) {
|
|
14562
14714
|
try {
|
|
14563
|
-
|
|
14715
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
14564
14716
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
14565
14717
|
} catch {
|
|
14566
14718
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -14822,6 +14974,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14822
14974
|
wake;
|
|
14823
14975
|
deriveFn;
|
|
14824
14976
|
seenChain = new SerialChain();
|
|
14977
|
+
lastIncomingEpoch = null;
|
|
14825
14978
|
constructor(config) {
|
|
14826
14979
|
this.client = config.client;
|
|
14827
14980
|
this.kv = config.kv;
|
|
@@ -14916,6 +15069,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14916
15069
|
const seen = await this.readSeen();
|
|
14917
15070
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
14918
15071
|
for (; ; ) {
|
|
15072
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
14919
15073
|
for (const entry of page.entries) {
|
|
14920
15074
|
if (entry.status !== "unclaimed") continue;
|
|
14921
15075
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -14925,6 +15079,10 @@ var WalletApiDeliveryPort = class {
|
|
|
14925
15079
|
page = await this.client.listMailbox(page.cursor);
|
|
14926
15080
|
}
|
|
14927
15081
|
}
|
|
15082
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
15083
|
+
incomingEpoch() {
|
|
15084
|
+
return this.lastIncomingEpoch;
|
|
15085
|
+
}
|
|
14928
15086
|
toIncoming(entry) {
|
|
14929
15087
|
const bundle = this.openEnvelope(entry);
|
|
14930
15088
|
return {
|
|
@@ -15002,7 +15160,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
15002
15160
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
15003
15161
|
}
|
|
15004
15162
|
async put(transferId, opIndex, bytes) {
|
|
15005
|
-
const {
|
|
15163
|
+
const { kv, fieldKey } = this.config;
|
|
15006
15164
|
const aad = this.aad(transferId, opIndex);
|
|
15007
15165
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
15008
15166
|
let envelope = await kv.get(cacheKey);
|
|
@@ -15010,10 +15168,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
15010
15168
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
15011
15169
|
await kv.set(cacheKey, envelope);
|
|
15012
15170
|
}
|
|
15013
|
-
const
|
|
15014
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
15171
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
15015
15172
|
return this.open(record.payload, aad);
|
|
15016
15173
|
}
|
|
15174
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
15175
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
15176
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
15177
|
+
if (envelope === null) return false;
|
|
15178
|
+
await this.post(transferId, opIndex, envelope);
|
|
15179
|
+
return true;
|
|
15180
|
+
}
|
|
15181
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
15182
|
+
async post(transferId, opIndex, envelope) {
|
|
15183
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
15184
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
15185
|
+
}
|
|
15017
15186
|
async get(transferId, opIndex) {
|
|
15018
15187
|
const records = await this.config.client.getProgress(transferId);
|
|
15019
15188
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -15036,7 +15205,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
15036
15205
|
};
|
|
15037
15206
|
|
|
15038
15207
|
// core/payments-v2-wiring.ts
|
|
15039
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
15208
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
15040
15209
|
const source = walletApi;
|
|
15041
15210
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
15042
15211
|
if (!source || network === null) {
|
|
@@ -15045,6 +15214,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
15045
15214
|
"INVALID_CONFIG"
|
|
15046
15215
|
);
|
|
15047
15216
|
}
|
|
15217
|
+
if (!(network in NETWORKS)) {
|
|
15218
|
+
throw new SphereError(
|
|
15219
|
+
`payments: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
15220
|
+
"INVALID_CONFIG"
|
|
15221
|
+
);
|
|
15222
|
+
}
|
|
15223
|
+
if (network !== sphereNetwork) {
|
|
15224
|
+
throw new SphereError(
|
|
15225
|
+
`payments: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
15226
|
+
"INVALID_CONFIG"
|
|
15227
|
+
);
|
|
15228
|
+
}
|
|
15048
15229
|
if (typeof source.paymentsV2Transport === "function") {
|
|
15049
15230
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
15050
15231
|
}
|
|
@@ -15109,8 +15290,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
15109
15290
|
deviceId: `${config.deviceId}:pv2`,
|
|
15110
15291
|
kv: args.kv,
|
|
15111
15292
|
webSocketFactory,
|
|
15112
|
-
emitStatus: args.emitStatus
|
|
15113
|
-
onEpochChange: args.onEpochChange
|
|
15293
|
+
emitStatus: args.emitStatus
|
|
15114
15294
|
});
|
|
15115
15295
|
return { session, client: authedWireClient(session, client) };
|
|
15116
15296
|
};
|
|
@@ -15180,8 +15360,7 @@ function composePaymentsV2(spec) {
|
|
|
15180
15360
|
identity: identitySlice,
|
|
15181
15361
|
network,
|
|
15182
15362
|
kv,
|
|
15183
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15184
|
-
onEpochChange: async () => void 0
|
|
15363
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15185
15364
|
});
|
|
15186
15365
|
const deps = {
|
|
15187
15366
|
session: transport.session,
|
|
@@ -15211,7 +15390,10 @@ function composePaymentsV2(spec) {
|
|
|
15211
15390
|
network,
|
|
15212
15391
|
ownPubkey: identity.chainPubkey,
|
|
15213
15392
|
ownNametag: host.nametag,
|
|
15214
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
15393
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
15394
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
15395
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
15396
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
15215
15397
|
};
|
|
15216
15398
|
return new PaymentsFacade(deps);
|
|
15217
15399
|
}
|
|
@@ -15366,7 +15548,7 @@ var Sphere = class _Sphere {
|
|
|
15366
15548
|
static async init(options) {
|
|
15367
15549
|
if (options.debug) logger.configure({ debug: true });
|
|
15368
15550
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15369
|
-
resolvePaymentsV2Composition(options.walletApi);
|
|
15551
|
+
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15370
15552
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
15371
15553
|
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
15372
15554
|
const market = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -15508,7 +15690,7 @@ var Sphere = class _Sphere {
|
|
|
15508
15690
|
static async create(options) {
|
|
15509
15691
|
if (options.debug) logger.configure({ debug: true });
|
|
15510
15692
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15511
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15693
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15512
15694
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
15513
15695
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
15514
15696
|
}
|
|
@@ -15576,7 +15758,7 @@ var Sphere = class _Sphere {
|
|
|
15576
15758
|
static async load(options) {
|
|
15577
15759
|
if (options.debug) logger.configure({ debug: true });
|
|
15578
15760
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15579
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15761
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15580
15762
|
if (!await _Sphere.exists(options.storage)) {
|
|
15581
15763
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
15582
15764
|
}
|
|
@@ -15630,7 +15812,7 @@ var Sphere = class _Sphere {
|
|
|
15630
15812
|
static async import(options) {
|
|
15631
15813
|
if (options.debug) logger.configure({ debug: true });
|
|
15632
15814
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15633
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15815
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15634
15816
|
if (!options.mnemonic && !options.masterKey) {
|
|
15635
15817
|
throw new SphereError("Either mnemonic or masterKey is required", "INVALID_CONFIG");
|
|
15636
15818
|
}
|