@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.cjs
CHANGED
|
@@ -11170,7 +11170,10 @@ var STORE_KEYS = {
|
|
|
11170
11170
|
shortfalls: "shortfalls",
|
|
11171
11171
|
settlingLinks: "settling",
|
|
11172
11172
|
streamCursor: (s) => `cursor:${s}`,
|
|
11173
|
-
epochLatch: "epoch-latch"
|
|
11173
|
+
epochLatch: "epoch-latch",
|
|
11174
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
11175
|
+
suspectedSpent: "suspected-spent",
|
|
11176
|
+
knownSpends: "known-spends"
|
|
11174
11177
|
};
|
|
11175
11178
|
|
|
11176
11179
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -11420,7 +11423,7 @@ var TransferMachine = class {
|
|
|
11420
11423
|
}
|
|
11421
11424
|
if (cls === null) {
|
|
11422
11425
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
11423
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
11426
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
11424
11427
|
return { committed, deliveryPending };
|
|
11425
11428
|
}
|
|
11426
11429
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -11446,10 +11449,10 @@ var TransferMachine = class {
|
|
|
11446
11449
|
}
|
|
11447
11450
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
11448
11451
|
if (conflicts.length > 0) {
|
|
11449
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
11452
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
11450
11453
|
}
|
|
11451
11454
|
await this.completeIntent(r.transferId, r.payload);
|
|
11452
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11455
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11453
11456
|
return { deliveryPending, partial: null };
|
|
11454
11457
|
}
|
|
11455
11458
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -11631,7 +11634,7 @@ var TransferMachine = class {
|
|
|
11631
11634
|
}
|
|
11632
11635
|
return added;
|
|
11633
11636
|
}
|
|
11634
|
-
async settlePartial(r, committed, conflicts) {
|
|
11637
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
11635
11638
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
11636
11639
|
const entry = {
|
|
11637
11640
|
transferId: r.transferId,
|
|
@@ -11643,26 +11646,27 @@ var TransferMachine = class {
|
|
|
11643
11646
|
};
|
|
11644
11647
|
await this.stores.shortfalls.upsert(entry);
|
|
11645
11648
|
await this.completeIntent(r.transferId, r.payload);
|
|
11646
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11649
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11647
11650
|
return entry;
|
|
11648
11651
|
}
|
|
11649
|
-
closeTail(transferId, payload) {
|
|
11652
|
+
closeTail(engine, transferId, payload, committed) {
|
|
11650
11653
|
void (async () => {
|
|
11651
11654
|
try {
|
|
11652
11655
|
await this.completeIntent(transferId, payload);
|
|
11653
11656
|
} catch {
|
|
11654
11657
|
return;
|
|
11655
11658
|
}
|
|
11656
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
11659
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
11657
11660
|
})();
|
|
11658
11661
|
}
|
|
11659
11662
|
async completeIntent(transferId, payload) {
|
|
11660
11663
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
11661
11664
|
await this.deps.intents.complete(transferId, signature);
|
|
11662
11665
|
}
|
|
11663
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
11666
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
11664
11667
|
try {
|
|
11665
|
-
await this.
|
|
11668
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
11669
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
11666
11670
|
} catch {
|
|
11667
11671
|
}
|
|
11668
11672
|
try {
|
|
@@ -11670,6 +11674,26 @@ var TransferMachine = class {
|
|
|
11670
11674
|
} catch {
|
|
11671
11675
|
}
|
|
11672
11676
|
}
|
|
11677
|
+
/**
|
|
11678
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
11679
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
11680
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
11681
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
11682
|
+
*/
|
|
11683
|
+
async settledAmount(engine, coinId, committed) {
|
|
11684
|
+
let total = 0n;
|
|
11685
|
+
for (const outcome of committed) {
|
|
11686
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
11687
|
+
const token = await engine.decodeToken({
|
|
11688
|
+
v: TOKEN_BLOB_VERSION,
|
|
11689
|
+
network: 0,
|
|
11690
|
+
tokenId: "",
|
|
11691
|
+
token: outcome.recipientBlob
|
|
11692
|
+
});
|
|
11693
|
+
total += engine.balanceOf(token, coinId);
|
|
11694
|
+
}
|
|
11695
|
+
return total.toString();
|
|
11696
|
+
}
|
|
11673
11697
|
async disposeOnFailure(transferId, committed, cls) {
|
|
11674
11698
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
11675
11699
|
try {
|
|
@@ -11694,7 +11718,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
11694
11718
|
}
|
|
11695
11719
|
|
|
11696
11720
|
// modules/payments-v2/machine/resume.ts
|
|
11697
|
-
async function resumeAll(deps) {
|
|
11721
|
+
async function resumeAll(deps, options = {}) {
|
|
11698
11722
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
11699
11723
|
const stores = createMachineStores(deps.kv);
|
|
11700
11724
|
const open = await deps.intents.listOpen();
|
|
@@ -11704,7 +11728,12 @@ async function resumeAll(deps) {
|
|
|
11704
11728
|
} catch {
|
|
11705
11729
|
return report;
|
|
11706
11730
|
}
|
|
11707
|
-
await
|
|
11731
|
+
await dropStaleBackstops(
|
|
11732
|
+
stores,
|
|
11733
|
+
locals,
|
|
11734
|
+
new Set(open.map((i) => i.transferId)),
|
|
11735
|
+
options.isLocallyActive ?? (() => false)
|
|
11736
|
+
);
|
|
11708
11737
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
11709
11738
|
const ctx = {
|
|
11710
11739
|
deps,
|
|
@@ -11717,11 +11746,16 @@ async function resumeAll(deps) {
|
|
|
11717
11746
|
}
|
|
11718
11747
|
return report;
|
|
11719
11748
|
}
|
|
11720
|
-
async function
|
|
11749
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
11721
11750
|
for (const entry of locals.values()) {
|
|
11722
|
-
if (
|
|
11751
|
+
if (openIds.has(entry.transferId)) continue;
|
|
11752
|
+
if (entry.disposition !== "open") {
|
|
11723
11753
|
await stores.backstop.removeByKey(entry.transferId);
|
|
11754
|
+
continue;
|
|
11724
11755
|
}
|
|
11756
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
11757
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
11758
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
11725
11759
|
}
|
|
11726
11760
|
}
|
|
11727
11761
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -11929,7 +11963,9 @@ var Converger = class {
|
|
|
11929
11963
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
11930
11964
|
}
|
|
11931
11965
|
async resumePass() {
|
|
11932
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
11966
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
11967
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
11968
|
+
});
|
|
11933
11969
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
11934
11970
|
await this.deps.reconcile({
|
|
11935
11971
|
resumed: report.resumed,
|
|
@@ -12045,6 +12081,136 @@ function distinctOpCount(entries) {
|
|
|
12045
12081
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
12046
12082
|
}
|
|
12047
12083
|
|
|
12084
|
+
// modules/payments-v2/restore.ts
|
|
12085
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
12086
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
12087
|
+
async function reseedAndReset(deps) {
|
|
12088
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
12089
|
+
if (entry.disposition !== "open") continue;
|
|
12090
|
+
try {
|
|
12091
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
12092
|
+
if (entry.requiresSeedClose) {
|
|
12093
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
12094
|
+
}
|
|
12095
|
+
} catch (err) {
|
|
12096
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
12097
|
+
}
|
|
12098
|
+
}
|
|
12099
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
12100
|
+
await deps.fullRePull();
|
|
12101
|
+
}
|
|
12102
|
+
async function splitOpIndex(deps, envelope) {
|
|
12103
|
+
const payload = await deps.decryptPayload(envelope);
|
|
12104
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12105
|
+
}
|
|
12106
|
+
|
|
12107
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12108
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12109
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12110
|
+
return {
|
|
12111
|
+
id: tokenId,
|
|
12112
|
+
coinId: asset.coinId,
|
|
12113
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12114
|
+
name: registry.getName(asset.coinId),
|
|
12115
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12116
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12117
|
+
amount: asset.amount,
|
|
12118
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12119
|
+
createdAt: snapshot.createdAt,
|
|
12120
|
+
updatedAt: snapshot.updatedAt,
|
|
12121
|
+
lazy: true,
|
|
12122
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12123
|
+
};
|
|
12124
|
+
}
|
|
12125
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12126
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12127
|
+
for (const [tokenId, entry] of entries) {
|
|
12128
|
+
const moving = isTransferring(tokenId);
|
|
12129
|
+
for (const asset of entry.assets) {
|
|
12130
|
+
let group = groups.get(asset.coinId);
|
|
12131
|
+
if (!group) {
|
|
12132
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12133
|
+
groups.set(asset.coinId, group);
|
|
12134
|
+
}
|
|
12135
|
+
if (moving) {
|
|
12136
|
+
group.transferring += BigInt(asset.amount);
|
|
12137
|
+
group.transferringCount += 1;
|
|
12138
|
+
} else {
|
|
12139
|
+
group.total += BigInt(asset.amount);
|
|
12140
|
+
group.count += 1;
|
|
12141
|
+
}
|
|
12142
|
+
}
|
|
12143
|
+
}
|
|
12144
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12145
|
+
}
|
|
12146
|
+
function toAsset(coinId, totals, registry) {
|
|
12147
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12148
|
+
return {
|
|
12149
|
+
coinId,
|
|
12150
|
+
symbol: registry.getSymbol(coinId),
|
|
12151
|
+
name: registry.getName(coinId),
|
|
12152
|
+
decimals: registry.getDecimals(coinId),
|
|
12153
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12154
|
+
totalAmount: totals.total.toString(),
|
|
12155
|
+
tokenCount: totals.count,
|
|
12156
|
+
confirmedAmount: totals.total.toString(),
|
|
12157
|
+
unconfirmedAmount: "0",
|
|
12158
|
+
confirmedTokenCount: totals.count,
|
|
12159
|
+
unconfirmedTokenCount: 0,
|
|
12160
|
+
transferringTokenCount: totals.transferringCount,
|
|
12161
|
+
transferringAmount: totals.transferring.toString(),
|
|
12162
|
+
priceUsd: null,
|
|
12163
|
+
priceEur: null,
|
|
12164
|
+
change24h: null,
|
|
12165
|
+
fiatValueUsd: null,
|
|
12166
|
+
fiatValueEur: null
|
|
12167
|
+
};
|
|
12168
|
+
}
|
|
12169
|
+
async function withPrices(raw, registry, price) {
|
|
12170
|
+
try {
|
|
12171
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12172
|
+
const quotes = await price.getPrices(names);
|
|
12173
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12174
|
+
} catch {
|
|
12175
|
+
return raw;
|
|
12176
|
+
}
|
|
12177
|
+
}
|
|
12178
|
+
function priceOne(asset, quote) {
|
|
12179
|
+
if (!quote) return asset;
|
|
12180
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12181
|
+
return {
|
|
12182
|
+
...asset,
|
|
12183
|
+
priceUsd: quote.priceUsd,
|
|
12184
|
+
priceEur: quote.priceEur ?? null,
|
|
12185
|
+
change24h: quote.change24h ?? null,
|
|
12186
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12187
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12188
|
+
};
|
|
12189
|
+
}
|
|
12190
|
+
|
|
12191
|
+
// modules/payments-v2/machine/payload.ts
|
|
12192
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12193
|
+
return {
|
|
12194
|
+
v: 2,
|
|
12195
|
+
recipient: recipientPubkey,
|
|
12196
|
+
coinId: request.coinId,
|
|
12197
|
+
amount: request.amount,
|
|
12198
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12199
|
+
direct: [...spend.plan.direct],
|
|
12200
|
+
...spend.plan.split !== void 0 ? {
|
|
12201
|
+
split: {
|
|
12202
|
+
tokenId: spend.plan.split.tokenId,
|
|
12203
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12204
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12205
|
+
}
|
|
12206
|
+
} : {},
|
|
12207
|
+
spentStates
|
|
12208
|
+
};
|
|
12209
|
+
}
|
|
12210
|
+
function messageOf(err) {
|
|
12211
|
+
return err instanceof Error ? err.message : String(err);
|
|
12212
|
+
}
|
|
12213
|
+
|
|
12048
12214
|
// modules/payments-v2/history/History.ts
|
|
12049
12215
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
12050
12216
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -12103,13 +12269,14 @@ var History = class {
|
|
|
12103
12269
|
}
|
|
12104
12270
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
12105
12271
|
async post(build) {
|
|
12272
|
+
const record = build();
|
|
12106
12273
|
try {
|
|
12107
|
-
await this.deps.client.postHistory([
|
|
12274
|
+
await this.deps.client.postHistory([record]);
|
|
12108
12275
|
} catch (err) {
|
|
12109
12276
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
12110
12277
|
return;
|
|
12111
12278
|
}
|
|
12112
|
-
this.deps.emit("history:updated");
|
|
12279
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
12113
12280
|
}
|
|
12114
12281
|
ts(timestamp) {
|
|
12115
12282
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -12166,93 +12333,9 @@ var History = class {
|
|
|
12166
12333
|
}
|
|
12167
12334
|
};
|
|
12168
12335
|
|
|
12169
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
12170
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12171
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12172
|
-
return {
|
|
12173
|
-
id: tokenId,
|
|
12174
|
-
coinId: asset.coinId,
|
|
12175
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12176
|
-
name: registry.getName(asset.coinId),
|
|
12177
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12178
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12179
|
-
amount: asset.amount,
|
|
12180
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12181
|
-
createdAt: snapshot.createdAt,
|
|
12182
|
-
updatedAt: snapshot.updatedAt,
|
|
12183
|
-
lazy: true,
|
|
12184
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12185
|
-
};
|
|
12186
|
-
}
|
|
12187
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12188
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12189
|
-
for (const [tokenId, entry] of entries) {
|
|
12190
|
-
const moving = isTransferring(tokenId);
|
|
12191
|
-
for (const asset of entry.assets) {
|
|
12192
|
-
let group = groups.get(asset.coinId);
|
|
12193
|
-
if (!group) {
|
|
12194
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12195
|
-
groups.set(asset.coinId, group);
|
|
12196
|
-
}
|
|
12197
|
-
if (moving) {
|
|
12198
|
-
group.transferring += BigInt(asset.amount);
|
|
12199
|
-
group.transferringCount += 1;
|
|
12200
|
-
} else {
|
|
12201
|
-
group.total += BigInt(asset.amount);
|
|
12202
|
-
group.count += 1;
|
|
12203
|
-
}
|
|
12204
|
-
}
|
|
12205
|
-
}
|
|
12206
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12207
|
-
}
|
|
12208
|
-
function toAsset(coinId, totals, registry) {
|
|
12209
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12210
|
-
return {
|
|
12211
|
-
coinId,
|
|
12212
|
-
symbol: registry.getSymbol(coinId),
|
|
12213
|
-
name: registry.getName(coinId),
|
|
12214
|
-
decimals: registry.getDecimals(coinId),
|
|
12215
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12216
|
-
totalAmount: totals.total.toString(),
|
|
12217
|
-
tokenCount: totals.count,
|
|
12218
|
-
confirmedAmount: totals.total.toString(),
|
|
12219
|
-
unconfirmedAmount: "0",
|
|
12220
|
-
confirmedTokenCount: totals.count,
|
|
12221
|
-
unconfirmedTokenCount: 0,
|
|
12222
|
-
transferringTokenCount: totals.transferringCount,
|
|
12223
|
-
transferringAmount: totals.transferring.toString(),
|
|
12224
|
-
priceUsd: null,
|
|
12225
|
-
priceEur: null,
|
|
12226
|
-
change24h: null,
|
|
12227
|
-
fiatValueUsd: null,
|
|
12228
|
-
fiatValueEur: null
|
|
12229
|
-
};
|
|
12230
|
-
}
|
|
12231
|
-
async function withPrices(raw, registry, price) {
|
|
12232
|
-
try {
|
|
12233
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12234
|
-
const quotes = await price.getPrices(names);
|
|
12235
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12236
|
-
} catch {
|
|
12237
|
-
return raw;
|
|
12238
|
-
}
|
|
12239
|
-
}
|
|
12240
|
-
function priceOne(asset, quote) {
|
|
12241
|
-
if (!quote) return asset;
|
|
12242
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12243
|
-
return {
|
|
12244
|
-
...asset,
|
|
12245
|
-
priceUsd: quote.priceUsd,
|
|
12246
|
-
priceEur: quote.priceEur ?? null,
|
|
12247
|
-
change24h: quote.change24h ?? null,
|
|
12248
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12249
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12250
|
-
};
|
|
12251
|
-
}
|
|
12252
|
-
|
|
12253
12336
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
12254
|
-
var SUSPECTED_KEY =
|
|
12255
|
-
var KNOWN_SPENDS_KEY =
|
|
12337
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
12338
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
12256
12339
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
12257
12340
|
function stateKey(tokenId, stateHash) {
|
|
12258
12341
|
return `${tokenId}:${stateHash}`;
|
|
@@ -12556,19 +12639,24 @@ var Receive = class {
|
|
|
12556
12639
|
const engine = deps.engine();
|
|
12557
12640
|
const stored = [];
|
|
12558
12641
|
const pending = [];
|
|
12559
|
-
|
|
12642
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
12560
12643
|
try {
|
|
12561
|
-
epoch = deps.syncEpoch();
|
|
12562
12644
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
12563
|
-
|
|
12564
|
-
for
|
|
12565
|
-
await
|
|
12566
|
-
|
|
12645
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
12646
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
12647
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
12648
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
12649
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
12650
|
+
}
|
|
12651
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
12652
|
+
const served = deps.delivery.incomingEpoch();
|
|
12653
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
12654
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
12655
|
+
basis = null;
|
|
12567
12656
|
}
|
|
12568
|
-
await flushAcks(deps, pending, epoch);
|
|
12569
12657
|
} catch (err) {
|
|
12570
12658
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
12571
|
-
await flushAcks(deps, pending,
|
|
12659
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
12572
12660
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
12573
12661
|
});
|
|
12574
12662
|
}
|
|
@@ -12641,7 +12729,7 @@ async function announce(deps, entry, record) {
|
|
|
12641
12729
|
receivedAt
|
|
12642
12730
|
};
|
|
12643
12731
|
}
|
|
12644
|
-
async function flushAcks(deps, pending,
|
|
12732
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
12645
12733
|
let lastAcked = null;
|
|
12646
12734
|
try {
|
|
12647
12735
|
while (pending.length > 0) {
|
|
@@ -12652,7 +12740,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
12652
12740
|
}
|
|
12653
12741
|
} finally {
|
|
12654
12742
|
if (lastAcked !== null) {
|
|
12655
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
12743
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
12656
12744
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
12657
12745
|
}
|
|
12658
12746
|
}
|
|
@@ -12753,9 +12841,19 @@ var Requests = class {
|
|
|
12753
12841
|
}
|
|
12754
12842
|
});
|
|
12755
12843
|
}
|
|
12844
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
12756
12845
|
writeLink(requestId, transferId, committed) {
|
|
12757
12846
|
return this.mutateJournal((m) => {
|
|
12758
|
-
|
|
12847
|
+
const prev = m.get(requestId);
|
|
12848
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
12849
|
+
const next = {
|
|
12850
|
+
requestId,
|
|
12851
|
+
transferId,
|
|
12852
|
+
committed: committed || same && prev.committed,
|
|
12853
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
12854
|
+
};
|
|
12855
|
+
if (same && prev.committed === next.committed) return false;
|
|
12856
|
+
m.set(requestId, next);
|
|
12759
12857
|
return true;
|
|
12760
12858
|
});
|
|
12761
12859
|
}
|
|
@@ -12908,23 +13006,46 @@ var Requests = class {
|
|
|
12908
13006
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
12909
13007
|
if (transferId !== void 0) {
|
|
12910
13008
|
const committed = error instanceof PartialSendConflictError;
|
|
12911
|
-
await this.
|
|
12912
|
-
this.setStatus(id, "settling");
|
|
13009
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
12913
13010
|
} else {
|
|
12914
13011
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
12915
13012
|
this.setStatus(id, "paid");
|
|
12916
13013
|
}
|
|
12917
13014
|
} else {
|
|
12918
|
-
await this.
|
|
12919
|
-
this.setStatus(id, "pending");
|
|
13015
|
+
await this.revertPayable(id);
|
|
12920
13016
|
}
|
|
12921
13017
|
throw error;
|
|
12922
13018
|
}
|
|
12923
|
-
this.
|
|
12924
|
-
await this.respondPaid(id, result.id);
|
|
12925
|
-
await this.clearLink(id);
|
|
13019
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
12926
13020
|
return result;
|
|
12927
13021
|
}
|
|
13022
|
+
/**
|
|
13023
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
13024
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
13025
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
13026
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
13027
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
13028
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
13029
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
13030
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
13031
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
13032
|
+
*/
|
|
13033
|
+
async settle(requestId, transferId, opts) {
|
|
13034
|
+
let journalDown = false;
|
|
13035
|
+
try {
|
|
13036
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
13037
|
+
} catch (err) {
|
|
13038
|
+
if (!opts.respond) throw err;
|
|
13039
|
+
journalDown = true;
|
|
13040
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
13041
|
+
}
|
|
13042
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
13043
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
13044
|
+
return;
|
|
13045
|
+
}
|
|
13046
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
13047
|
+
this.setStatus(requestId, "paid");
|
|
13048
|
+
}
|
|
12928
13049
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
12929
13050
|
async respondPaid(id, transferId) {
|
|
12930
13051
|
try {
|
|
@@ -12960,32 +13081,32 @@ var Requests = class {
|
|
|
12960
13081
|
let aborted = null;
|
|
12961
13082
|
for (const [requestId, link] of [...this.journal]) {
|
|
12962
13083
|
if (link.committed || resumed.has(link.transferId)) {
|
|
12963
|
-
await this.resolvePaid(requestId, link
|
|
13084
|
+
await this.resolvePaid(requestId, link);
|
|
12964
13085
|
} else if (conflicted.has(link.transferId)) {
|
|
12965
13086
|
await this.revertPayable(requestId);
|
|
12966
13087
|
} else if (open.has(link.transferId)) {
|
|
12967
13088
|
} else {
|
|
12968
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
13089
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
12969
13090
|
}
|
|
12970
13091
|
}
|
|
12971
13092
|
}
|
|
12972
13093
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
12973
13094
|
// a probe failure defers the link to the next session.
|
|
12974
|
-
async reconcileUnaccounted(requestId,
|
|
13095
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
12975
13096
|
try {
|
|
12976
13097
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
12977
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
12978
|
-
else await this.resolvePaid(requestId,
|
|
13098
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
13099
|
+
else await this.resolvePaid(requestId, link);
|
|
12979
13100
|
} catch (err) {
|
|
12980
13101
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
12981
13102
|
}
|
|
12982
13103
|
return aborted;
|
|
12983
13104
|
}
|
|
12984
|
-
|
|
12985
|
-
|
|
12986
|
-
|
|
12987
|
-
this.setStatus(requestId, "paid");
|
|
13105
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
13106
|
+
resolvePaid(requestId, link) {
|
|
13107
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
12988
13108
|
}
|
|
13109
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
12989
13110
|
async revertPayable(requestId) {
|
|
12990
13111
|
await this.clearLink(requestId);
|
|
12991
13112
|
this.setStatus(requestId, "pending");
|
|
@@ -13309,11 +13430,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13309
13430
|
client: deps.client,
|
|
13310
13431
|
fieldKey: deps.fieldKey,
|
|
13311
13432
|
registry: deps.registry,
|
|
13312
|
-
emit: (event) => deps.emit(event,
|
|
13433
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
13313
13434
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
13314
13435
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
13315
13436
|
});
|
|
13316
13437
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
13438
|
+
const machineStores = createMachineStores(deps.kv);
|
|
13317
13439
|
const heldStates = /* @__PURE__ */ new Map();
|
|
13318
13440
|
return {
|
|
13319
13441
|
ownPubkeyBytes,
|
|
@@ -13321,12 +13443,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13321
13443
|
ledger,
|
|
13322
13444
|
queue,
|
|
13323
13445
|
historyStore,
|
|
13324
|
-
machineStores
|
|
13446
|
+
machineStores,
|
|
13325
13447
|
machineDeps,
|
|
13326
13448
|
machine: new TransferMachine(machineDeps),
|
|
13327
13449
|
heldStates,
|
|
13328
13450
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
13329
|
-
requests: buildRequests(deps, hooks)
|
|
13451
|
+
requests: buildRequests(deps, hooks),
|
|
13452
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13453
|
+
};
|
|
13454
|
+
}
|
|
13455
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13456
|
+
return {
|
|
13457
|
+
stores: machineStores,
|
|
13458
|
+
kv: deps.kv,
|
|
13459
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
13460
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
13461
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
13462
|
+
fullRePull: () => view.onEpochReset(),
|
|
13463
|
+
attention: (transferId, code, detail) => {
|
|
13464
|
+
deps.emit(
|
|
13465
|
+
"transfer:attention",
|
|
13466
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13467
|
+
);
|
|
13468
|
+
}
|
|
13330
13469
|
};
|
|
13331
13470
|
}
|
|
13332
13471
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -13348,11 +13487,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
13348
13487
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
13349
13488
|
emit: deps.emit,
|
|
13350
13489
|
now: deps.now ?? Date.now,
|
|
13351
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
13490
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
13352
13491
|
await historyStore.recordSent({
|
|
13353
13492
|
transferId,
|
|
13354
13493
|
coinId: payload.coinId,
|
|
13355
|
-
|
|
13494
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
13495
|
+
// recipient blobs), never payload.amount — the plan.
|
|
13496
|
+
amount: committedAmount,
|
|
13356
13497
|
recipientPubkey: payload.recipient,
|
|
13357
13498
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
13358
13499
|
});
|
|
@@ -13379,7 +13520,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
13379
13520
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13380
13521
|
);
|
|
13381
13522
|
},
|
|
13382
|
-
syncEpoch: deps.syncEpoch
|
|
13523
|
+
syncEpoch: deps.syncEpoch,
|
|
13383
13524
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13384
13525
|
});
|
|
13385
13526
|
}
|
|
@@ -13432,6 +13573,7 @@ var PaymentsFacade = class {
|
|
|
13432
13573
|
this.receiveLoop = parts.receiveLoop;
|
|
13433
13574
|
this.heldStates = parts.heldStates;
|
|
13434
13575
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13576
|
+
this.restoreDeps = parts.restoreDeps;
|
|
13435
13577
|
this.requests = parts.requests;
|
|
13436
13578
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13437
13579
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -13460,6 +13602,7 @@ var PaymentsFacade = class {
|
|
|
13460
13602
|
receiveLoop;
|
|
13461
13603
|
heldStates;
|
|
13462
13604
|
ownPubkeyBytes;
|
|
13605
|
+
restoreDeps;
|
|
13463
13606
|
requests;
|
|
13464
13607
|
currentEngine = null;
|
|
13465
13608
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -13470,6 +13613,8 @@ var PaymentsFacade = class {
|
|
|
13470
13613
|
converger;
|
|
13471
13614
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
13472
13615
|
resumeFlight = new SingleFlight();
|
|
13616
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
13617
|
+
passChain = new SerialChain();
|
|
13473
13618
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13474
13619
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13475
13620
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -13477,8 +13622,8 @@ var PaymentsFacade = class {
|
|
|
13477
13622
|
if (this.started) return;
|
|
13478
13623
|
this.started = true;
|
|
13479
13624
|
this.heartbeat.start();
|
|
13480
|
-
await this.deps.session.start();
|
|
13481
13625
|
this.unsubscribers.push(
|
|
13626
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
13482
13627
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
13483
13628
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
13484
13629
|
}),
|
|
@@ -13493,6 +13638,7 @@ var PaymentsFacade = class {
|
|
|
13493
13638
|
if (status === "connected") this.heartbeat.recovered();
|
|
13494
13639
|
});
|
|
13495
13640
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13641
|
+
await this.deps.session.start();
|
|
13496
13642
|
this.trackTail(this.runConvergencePass());
|
|
13497
13643
|
this.trackTail(this.seedHeldStates());
|
|
13498
13644
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13533,7 +13679,7 @@ var PaymentsFacade = class {
|
|
|
13533
13679
|
}
|
|
13534
13680
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
13535
13681
|
send(request) {
|
|
13536
|
-
return this.track(this.
|
|
13682
|
+
return this.track(this.sendOutcome(request));
|
|
13537
13683
|
}
|
|
13538
13684
|
async receive() {
|
|
13539
13685
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -13548,20 +13694,27 @@ var PaymentsFacade = class {
|
|
|
13548
13694
|
if (!this.started) return;
|
|
13549
13695
|
await this.track(this.runConvergencePass());
|
|
13550
13696
|
}
|
|
13697
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
13698
|
+
async handleEpochChange(_newEpoch) {
|
|
13699
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
13700
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
13701
|
+
}
|
|
13551
13702
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
13552
13703
|
runConvergencePass() {
|
|
13553
|
-
return this.resumeFlight.run(
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13704
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
13705
|
+
}
|
|
13706
|
+
async convergeBody() {
|
|
13707
|
+
const outcome = await this.converger.convergeOnce();
|
|
13708
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13709
|
+
this.heartbeat.settle(outcome, pending);
|
|
13558
13710
|
}
|
|
13559
13711
|
nowMs() {
|
|
13560
13712
|
return (this.deps.now ?? Date.now)();
|
|
13561
13713
|
}
|
|
13562
13714
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
13563
|
-
|
|
13564
|
-
|
|
13715
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
13716
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
13717
|
+
async sendOutcome(request) {
|
|
13565
13718
|
const run = {
|
|
13566
13719
|
amount: request.amount,
|
|
13567
13720
|
delivered: [],
|
|
@@ -13569,12 +13722,36 @@ var PaymentsFacade = class {
|
|
|
13569
13722
|
tokenTransfers: [],
|
|
13570
13723
|
settledShortfalls: [],
|
|
13571
13724
|
firstPartialId: void 0,
|
|
13572
|
-
pendingAny: false
|
|
13725
|
+
pendingAny: false,
|
|
13726
|
+
lastTransferId: ""
|
|
13573
13727
|
};
|
|
13728
|
+
try {
|
|
13729
|
+
return await this.sendWithPolicy(request, run);
|
|
13730
|
+
} catch (err) {
|
|
13731
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
13732
|
+
throw err;
|
|
13733
|
+
}
|
|
13734
|
+
}
|
|
13735
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
13736
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
13737
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
13738
|
+
emitCleanFailure(err, transferId) {
|
|
13739
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
13740
|
+
const failed = {
|
|
13741
|
+
id: transferId,
|
|
13742
|
+
status: "failed",
|
|
13743
|
+
tokens: [],
|
|
13744
|
+
tokenTransfers: [],
|
|
13745
|
+
error: messageOf(err)
|
|
13746
|
+
};
|
|
13747
|
+
this.deps.emit("transfer:updated", failed);
|
|
13748
|
+
}
|
|
13749
|
+
async sendWithPolicy(request, run) {
|
|
13750
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
13574
13751
|
for (let attempt = 0; ; attempt++) {
|
|
13575
13752
|
let ctx;
|
|
13576
13753
|
try {
|
|
13577
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
13754
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
13578
13755
|
} catch (err) {
|
|
13579
13756
|
throw this.partialize(err, run);
|
|
13580
13757
|
}
|
|
@@ -13619,11 +13796,8 @@ var PaymentsFacade = class {
|
|
|
13619
13796
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
13620
13797
|
}
|
|
13621
13798
|
}
|
|
13622
|
-
/**
|
|
13623
|
-
*
|
|
13624
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
13625
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
13626
|
-
*/
|
|
13799
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
13800
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
13627
13801
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
13628
13802
|
if (disposition.deliveryPending) {
|
|
13629
13803
|
run.pendingAny = true;
|
|
@@ -13649,11 +13823,8 @@ var PaymentsFacade = class {
|
|
|
13649
13823
|
run.amount = partial.remainingAmount;
|
|
13650
13824
|
return null;
|
|
13651
13825
|
}
|
|
13652
|
-
/**
|
|
13653
|
-
*
|
|
13654
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
13655
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
13656
|
-
*/
|
|
13826
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
13827
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
13657
13828
|
async disposeFailedAttempt(ctx, err) {
|
|
13658
13829
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
13659
13830
|
this.settleKeepOpen(ctx);
|
|
@@ -13702,8 +13873,9 @@ var PaymentsFacade = class {
|
|
|
13702
13873
|
}
|
|
13703
13874
|
return { kind: "rethrow", error: err };
|
|
13704
13875
|
}
|
|
13705
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
13876
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
13706
13877
|
const transferId = this.newId();
|
|
13878
|
+
run.lastTransferId = transferId;
|
|
13707
13879
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
13708
13880
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
13709
13881
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -13835,11 +14007,7 @@ var PaymentsFacade = class {
|
|
|
13835
14007
|
this.deps.emit("transfer:updated", result);
|
|
13836
14008
|
return result;
|
|
13837
14009
|
}
|
|
13838
|
-
/**
|
|
13839
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
13840
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
13841
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
13842
|
-
*/
|
|
14010
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
13843
14011
|
partialize(err, run) {
|
|
13844
14012
|
if (run.delivered.length === 0) return err;
|
|
13845
14013
|
return new PartialSendConflictError(
|
|
@@ -13991,22 +14159,14 @@ var PaymentsFacade = class {
|
|
|
13991
14159
|
}
|
|
13992
14160
|
}
|
|
13993
14161
|
toUiToken(tokenId, coinId, amount) {
|
|
13994
|
-
const registry = this.deps.registry;
|
|
13995
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
13996
14162
|
const now = this.nowMs();
|
|
13997
|
-
return
|
|
13998
|
-
|
|
13999
|
-
|
|
14000
|
-
|
|
14001
|
-
|
|
14002
|
-
|
|
14003
|
-
|
|
14004
|
-
amount: amount.toString(),
|
|
14005
|
-
status: "transferring",
|
|
14006
|
-
createdAt: now,
|
|
14007
|
-
updatedAt: now,
|
|
14008
|
-
lazy: true
|
|
14009
|
-
};
|
|
14163
|
+
return toToken(
|
|
14164
|
+
tokenId,
|
|
14165
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
14166
|
+
{ coinId, amount: amount.toString() },
|
|
14167
|
+
this.deps.registry,
|
|
14168
|
+
{ transferring: true, suspectedSpent: false }
|
|
14169
|
+
);
|
|
14010
14170
|
}
|
|
14011
14171
|
track(op) {
|
|
14012
14172
|
const settled = op.then(
|
|
@@ -14021,27 +14181,6 @@ var PaymentsFacade = class {
|
|
|
14021
14181
|
this.track(op).catch(() => void 0);
|
|
14022
14182
|
}
|
|
14023
14183
|
};
|
|
14024
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
14025
|
-
return {
|
|
14026
|
-
v: 2,
|
|
14027
|
-
recipient: recipientPubkey,
|
|
14028
|
-
coinId: request.coinId,
|
|
14029
|
-
amount: request.amount,
|
|
14030
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
14031
|
-
direct: [...spend.plan.direct],
|
|
14032
|
-
...spend.plan.split !== void 0 ? {
|
|
14033
|
-
split: {
|
|
14034
|
-
tokenId: spend.plan.split.tokenId,
|
|
14035
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
14036
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
14037
|
-
}
|
|
14038
|
-
} : {},
|
|
14039
|
-
spentStates
|
|
14040
|
-
};
|
|
14041
|
-
}
|
|
14042
|
-
function messageOf(err) {
|
|
14043
|
-
return err instanceof Error ? err.message : String(err);
|
|
14044
|
-
}
|
|
14045
14184
|
|
|
14046
14185
|
// impl/wallet-api-v2/http.ts
|
|
14047
14186
|
var WalletApiHttpError = class extends Error {
|
|
@@ -14387,6 +14526,7 @@ var WalletApiSession = class {
|
|
|
14387
14526
|
epochLatch = null;
|
|
14388
14527
|
reseedChain = Promise.resolve();
|
|
14389
14528
|
reseedsPending = 0;
|
|
14529
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
14390
14530
|
handlers = /* @__PURE__ */ new Map();
|
|
14391
14531
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
14392
14532
|
socket = null;
|
|
@@ -14489,6 +14629,18 @@ var WalletApiSession = class {
|
|
|
14489
14629
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
14490
14630
|
}
|
|
14491
14631
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
14632
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
14633
|
+
currentEpoch() {
|
|
14634
|
+
return this.epochLatch ?? "";
|
|
14635
|
+
}
|
|
14636
|
+
/**
|
|
14637
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
14638
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
14639
|
+
*/
|
|
14640
|
+
subscribeEpochChange(handler) {
|
|
14641
|
+
this.epochHandlers.add(handler);
|
|
14642
|
+
return () => this.epochHandlers.delete(handler);
|
|
14643
|
+
}
|
|
14492
14644
|
noteEpoch(epoch) {
|
|
14493
14645
|
if (this.epochLatch === epoch) return;
|
|
14494
14646
|
const previous = this.epochLatch;
|
|
@@ -14502,7 +14654,7 @@ var WalletApiSession = class {
|
|
|
14502
14654
|
}
|
|
14503
14655
|
async runReseed(epoch, previous) {
|
|
14504
14656
|
try {
|
|
14505
|
-
|
|
14657
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
14506
14658
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
14507
14659
|
} catch {
|
|
14508
14660
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -14764,6 +14916,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14764
14916
|
wake;
|
|
14765
14917
|
deriveFn;
|
|
14766
14918
|
seenChain = new SerialChain();
|
|
14919
|
+
lastIncomingEpoch = null;
|
|
14767
14920
|
constructor(config) {
|
|
14768
14921
|
this.client = config.client;
|
|
14769
14922
|
this.kv = config.kv;
|
|
@@ -14858,6 +15011,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14858
15011
|
const seen = await this.readSeen();
|
|
14859
15012
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
14860
15013
|
for (; ; ) {
|
|
15014
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
14861
15015
|
for (const entry of page.entries) {
|
|
14862
15016
|
if (entry.status !== "unclaimed") continue;
|
|
14863
15017
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -14867,6 +15021,10 @@ var WalletApiDeliveryPort = class {
|
|
|
14867
15021
|
page = await this.client.listMailbox(page.cursor);
|
|
14868
15022
|
}
|
|
14869
15023
|
}
|
|
15024
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
15025
|
+
incomingEpoch() {
|
|
15026
|
+
return this.lastIncomingEpoch;
|
|
15027
|
+
}
|
|
14870
15028
|
toIncoming(entry) {
|
|
14871
15029
|
const bundle = this.openEnvelope(entry);
|
|
14872
15030
|
return {
|
|
@@ -14944,7 +15102,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14944
15102
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
14945
15103
|
}
|
|
14946
15104
|
async put(transferId, opIndex, bytes) {
|
|
14947
|
-
const {
|
|
15105
|
+
const { kv, fieldKey } = this.config;
|
|
14948
15106
|
const aad = this.aad(transferId, opIndex);
|
|
14949
15107
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
14950
15108
|
let envelope = await kv.get(cacheKey);
|
|
@@ -14952,10 +15110,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14952
15110
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
14953
15111
|
await kv.set(cacheKey, envelope);
|
|
14954
15112
|
}
|
|
14955
|
-
const
|
|
14956
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
15113
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
14957
15114
|
return this.open(record.payload, aad);
|
|
14958
15115
|
}
|
|
15116
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
15117
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
15118
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
15119
|
+
if (envelope === null) return false;
|
|
15120
|
+
await this.post(transferId, opIndex, envelope);
|
|
15121
|
+
return true;
|
|
15122
|
+
}
|
|
15123
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
15124
|
+
async post(transferId, opIndex, envelope) {
|
|
15125
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
15126
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
15127
|
+
}
|
|
14959
15128
|
async get(transferId, opIndex) {
|
|
14960
15129
|
const records = await this.config.client.getProgress(transferId);
|
|
14961
15130
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -14978,7 +15147,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14978
15147
|
};
|
|
14979
15148
|
|
|
14980
15149
|
// core/payments-v2-wiring.ts
|
|
14981
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
15150
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
14982
15151
|
const source = walletApi;
|
|
14983
15152
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
14984
15153
|
if (!source || network === null) {
|
|
@@ -14987,6 +15156,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
14987
15156
|
"INVALID_CONFIG"
|
|
14988
15157
|
);
|
|
14989
15158
|
}
|
|
15159
|
+
if (!(network in NETWORKS)) {
|
|
15160
|
+
throw new SphereError(
|
|
15161
|
+
`payments: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
15162
|
+
"INVALID_CONFIG"
|
|
15163
|
+
);
|
|
15164
|
+
}
|
|
15165
|
+
if (network !== sphereNetwork) {
|
|
15166
|
+
throw new SphereError(
|
|
15167
|
+
`payments: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
15168
|
+
"INVALID_CONFIG"
|
|
15169
|
+
);
|
|
15170
|
+
}
|
|
14990
15171
|
if (typeof source.paymentsV2Transport === "function") {
|
|
14991
15172
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
14992
15173
|
}
|
|
@@ -15051,8 +15232,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
15051
15232
|
deviceId: `${config.deviceId}:pv2`,
|
|
15052
15233
|
kv: args.kv,
|
|
15053
15234
|
webSocketFactory,
|
|
15054
|
-
emitStatus: args.emitStatus
|
|
15055
|
-
onEpochChange: args.onEpochChange
|
|
15235
|
+
emitStatus: args.emitStatus
|
|
15056
15236
|
});
|
|
15057
15237
|
return { session, client: authedWireClient(session, client) };
|
|
15058
15238
|
};
|
|
@@ -15122,8 +15302,7 @@ function composePaymentsV2(spec) {
|
|
|
15122
15302
|
identity: identitySlice,
|
|
15123
15303
|
network,
|
|
15124
15304
|
kv,
|
|
15125
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15126
|
-
onEpochChange: async () => void 0
|
|
15305
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15127
15306
|
});
|
|
15128
15307
|
const deps = {
|
|
15129
15308
|
session: transport.session,
|
|
@@ -15153,7 +15332,10 @@ function composePaymentsV2(spec) {
|
|
|
15153
15332
|
network,
|
|
15154
15333
|
ownPubkey: identity.chainPubkey,
|
|
15155
15334
|
ownNametag: host.nametag,
|
|
15156
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
15335
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
15336
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
15337
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
15338
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
15157
15339
|
};
|
|
15158
15340
|
return new PaymentsFacade(deps);
|
|
15159
15341
|
}
|
|
@@ -15308,7 +15490,7 @@ var Sphere = class _Sphere {
|
|
|
15308
15490
|
static async init(options) {
|
|
15309
15491
|
if (options.debug) logger.configure({ debug: true });
|
|
15310
15492
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15311
|
-
resolvePaymentsV2Composition(options.walletApi);
|
|
15493
|
+
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15312
15494
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
15313
15495
|
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
15314
15496
|
const market = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -15450,7 +15632,7 @@ var Sphere = class _Sphere {
|
|
|
15450
15632
|
static async create(options) {
|
|
15451
15633
|
if (options.debug) logger.configure({ debug: true });
|
|
15452
15634
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15453
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15635
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15454
15636
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
15455
15637
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
15456
15638
|
}
|
|
@@ -15518,7 +15700,7 @@ var Sphere = class _Sphere {
|
|
|
15518
15700
|
static async load(options) {
|
|
15519
15701
|
if (options.debug) logger.configure({ debug: true });
|
|
15520
15702
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15521
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15703
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15522
15704
|
if (!await _Sphere.exists(options.storage)) {
|
|
15523
15705
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
15524
15706
|
}
|
|
@@ -15572,7 +15754,7 @@ var Sphere = class _Sphere {
|
|
|
15572
15754
|
static async import(options) {
|
|
15573
15755
|
if (options.debug) logger.configure({ debug: true });
|
|
15574
15756
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15575
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15757
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15576
15758
|
if (!options.mnemonic && !options.masterKey) {
|
|
15577
15759
|
throw new SphereError("Either mnemonic or masterKey is required", "INVALID_CONFIG");
|
|
15578
15760
|
}
|