@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/index.js
CHANGED
|
@@ -11100,7 +11100,10 @@ var STORE_KEYS = {
|
|
|
11100
11100
|
shortfalls: "shortfalls",
|
|
11101
11101
|
settlingLinks: "settling",
|
|
11102
11102
|
streamCursor: (s) => `cursor:${s}`,
|
|
11103
|
-
epochLatch: "epoch-latch"
|
|
11103
|
+
epochLatch: "epoch-latch",
|
|
11104
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
11105
|
+
suspectedSpent: "suspected-spent",
|
|
11106
|
+
knownSpends: "known-spends"
|
|
11104
11107
|
};
|
|
11105
11108
|
|
|
11106
11109
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -11350,7 +11353,7 @@ var TransferMachine = class {
|
|
|
11350
11353
|
}
|
|
11351
11354
|
if (cls === null) {
|
|
11352
11355
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
11353
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
11356
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
11354
11357
|
return { committed, deliveryPending };
|
|
11355
11358
|
}
|
|
11356
11359
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -11376,10 +11379,10 @@ var TransferMachine = class {
|
|
|
11376
11379
|
}
|
|
11377
11380
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
11378
11381
|
if (conflicts.length > 0) {
|
|
11379
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
11382
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
11380
11383
|
}
|
|
11381
11384
|
await this.completeIntent(r.transferId, r.payload);
|
|
11382
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11385
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11383
11386
|
return { deliveryPending, partial: null };
|
|
11384
11387
|
}
|
|
11385
11388
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -11561,7 +11564,7 @@ var TransferMachine = class {
|
|
|
11561
11564
|
}
|
|
11562
11565
|
return added;
|
|
11563
11566
|
}
|
|
11564
|
-
async settlePartial(r, committed, conflicts) {
|
|
11567
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
11565
11568
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
11566
11569
|
const entry = {
|
|
11567
11570
|
transferId: r.transferId,
|
|
@@ -11573,26 +11576,27 @@ var TransferMachine = class {
|
|
|
11573
11576
|
};
|
|
11574
11577
|
await this.stores.shortfalls.upsert(entry);
|
|
11575
11578
|
await this.completeIntent(r.transferId, r.payload);
|
|
11576
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
11579
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
11577
11580
|
return entry;
|
|
11578
11581
|
}
|
|
11579
|
-
closeTail(transferId, payload) {
|
|
11582
|
+
closeTail(engine, transferId, payload, committed) {
|
|
11580
11583
|
void (async () => {
|
|
11581
11584
|
try {
|
|
11582
11585
|
await this.completeIntent(transferId, payload);
|
|
11583
11586
|
} catch {
|
|
11584
11587
|
return;
|
|
11585
11588
|
}
|
|
11586
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
11589
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
11587
11590
|
})();
|
|
11588
11591
|
}
|
|
11589
11592
|
async completeIntent(transferId, payload) {
|
|
11590
11593
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
11591
11594
|
await this.deps.intents.complete(transferId, signature);
|
|
11592
11595
|
}
|
|
11593
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
11596
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
11594
11597
|
try {
|
|
11595
|
-
await this.
|
|
11598
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
11599
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
11596
11600
|
} catch {
|
|
11597
11601
|
}
|
|
11598
11602
|
try {
|
|
@@ -11600,6 +11604,26 @@ var TransferMachine = class {
|
|
|
11600
11604
|
} catch {
|
|
11601
11605
|
}
|
|
11602
11606
|
}
|
|
11607
|
+
/**
|
|
11608
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
11609
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
11610
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
11611
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
11612
|
+
*/
|
|
11613
|
+
async settledAmount(engine, coinId, committed) {
|
|
11614
|
+
let total = 0n;
|
|
11615
|
+
for (const outcome of committed) {
|
|
11616
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
11617
|
+
const token = await engine.decodeToken({
|
|
11618
|
+
v: TOKEN_BLOB_VERSION,
|
|
11619
|
+
network: 0,
|
|
11620
|
+
tokenId: "",
|
|
11621
|
+
token: outcome.recipientBlob
|
|
11622
|
+
});
|
|
11623
|
+
total += engine.balanceOf(token, coinId);
|
|
11624
|
+
}
|
|
11625
|
+
return total.toString();
|
|
11626
|
+
}
|
|
11603
11627
|
async disposeOnFailure(transferId, committed, cls) {
|
|
11604
11628
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
11605
11629
|
try {
|
|
@@ -11624,7 +11648,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
11624
11648
|
}
|
|
11625
11649
|
|
|
11626
11650
|
// modules/payments-v2/machine/resume.ts
|
|
11627
|
-
async function resumeAll(deps) {
|
|
11651
|
+
async function resumeAll(deps, options = {}) {
|
|
11628
11652
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
11629
11653
|
const stores = createMachineStores(deps.kv);
|
|
11630
11654
|
const open = await deps.intents.listOpen();
|
|
@@ -11634,7 +11658,12 @@ async function resumeAll(deps) {
|
|
|
11634
11658
|
} catch {
|
|
11635
11659
|
return report;
|
|
11636
11660
|
}
|
|
11637
|
-
await
|
|
11661
|
+
await dropStaleBackstops(
|
|
11662
|
+
stores,
|
|
11663
|
+
locals,
|
|
11664
|
+
new Set(open.map((i) => i.transferId)),
|
|
11665
|
+
options.isLocallyActive ?? (() => false)
|
|
11666
|
+
);
|
|
11638
11667
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
11639
11668
|
const ctx = {
|
|
11640
11669
|
deps,
|
|
@@ -11647,11 +11676,16 @@ async function resumeAll(deps) {
|
|
|
11647
11676
|
}
|
|
11648
11677
|
return report;
|
|
11649
11678
|
}
|
|
11650
|
-
async function
|
|
11679
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
11651
11680
|
for (const entry of locals.values()) {
|
|
11652
|
-
if (
|
|
11681
|
+
if (openIds.has(entry.transferId)) continue;
|
|
11682
|
+
if (entry.disposition !== "open") {
|
|
11653
11683
|
await stores.backstop.removeByKey(entry.transferId);
|
|
11684
|
+
continue;
|
|
11654
11685
|
}
|
|
11686
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
11687
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
11688
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
11655
11689
|
}
|
|
11656
11690
|
}
|
|
11657
11691
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -11859,7 +11893,9 @@ var Converger = class {
|
|
|
11859
11893
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
11860
11894
|
}
|
|
11861
11895
|
async resumePass() {
|
|
11862
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
11896
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
11897
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
11898
|
+
});
|
|
11863
11899
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
11864
11900
|
await this.deps.reconcile({
|
|
11865
11901
|
resumed: report.resumed,
|
|
@@ -11975,6 +12011,136 @@ function distinctOpCount(entries) {
|
|
|
11975
12011
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
11976
12012
|
}
|
|
11977
12013
|
|
|
12014
|
+
// modules/payments-v2/restore.ts
|
|
12015
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
12016
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
12017
|
+
async function reseedAndReset(deps) {
|
|
12018
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
12019
|
+
if (entry.disposition !== "open") continue;
|
|
12020
|
+
try {
|
|
12021
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
12022
|
+
if (entry.requiresSeedClose) {
|
|
12023
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
12024
|
+
}
|
|
12025
|
+
} catch (err) {
|
|
12026
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
12027
|
+
}
|
|
12028
|
+
}
|
|
12029
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
12030
|
+
await deps.fullRePull();
|
|
12031
|
+
}
|
|
12032
|
+
async function splitOpIndex(deps, envelope) {
|
|
12033
|
+
const payload = await deps.decryptPayload(envelope);
|
|
12034
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
12035
|
+
}
|
|
12036
|
+
|
|
12037
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
12038
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12039
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12040
|
+
return {
|
|
12041
|
+
id: tokenId,
|
|
12042
|
+
coinId: asset.coinId,
|
|
12043
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
12044
|
+
name: registry.getName(asset.coinId),
|
|
12045
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
12046
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12047
|
+
amount: asset.amount,
|
|
12048
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
12049
|
+
createdAt: snapshot.createdAt,
|
|
12050
|
+
updatedAt: snapshot.updatedAt,
|
|
12051
|
+
lazy: true,
|
|
12052
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12053
|
+
};
|
|
12054
|
+
}
|
|
12055
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
12056
|
+
const groups = /* @__PURE__ */ new Map();
|
|
12057
|
+
for (const [tokenId, entry] of entries) {
|
|
12058
|
+
const moving = isTransferring(tokenId);
|
|
12059
|
+
for (const asset of entry.assets) {
|
|
12060
|
+
let group = groups.get(asset.coinId);
|
|
12061
|
+
if (!group) {
|
|
12062
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12063
|
+
groups.set(asset.coinId, group);
|
|
12064
|
+
}
|
|
12065
|
+
if (moving) {
|
|
12066
|
+
group.transferring += BigInt(asset.amount);
|
|
12067
|
+
group.transferringCount += 1;
|
|
12068
|
+
} else {
|
|
12069
|
+
group.total += BigInt(asset.amount);
|
|
12070
|
+
group.count += 1;
|
|
12071
|
+
}
|
|
12072
|
+
}
|
|
12073
|
+
}
|
|
12074
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12075
|
+
}
|
|
12076
|
+
function toAsset(coinId, totals, registry) {
|
|
12077
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
12078
|
+
return {
|
|
12079
|
+
coinId,
|
|
12080
|
+
symbol: registry.getSymbol(coinId),
|
|
12081
|
+
name: registry.getName(coinId),
|
|
12082
|
+
decimals: registry.getDecimals(coinId),
|
|
12083
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
12084
|
+
totalAmount: totals.total.toString(),
|
|
12085
|
+
tokenCount: totals.count,
|
|
12086
|
+
confirmedAmount: totals.total.toString(),
|
|
12087
|
+
unconfirmedAmount: "0",
|
|
12088
|
+
confirmedTokenCount: totals.count,
|
|
12089
|
+
unconfirmedTokenCount: 0,
|
|
12090
|
+
transferringTokenCount: totals.transferringCount,
|
|
12091
|
+
transferringAmount: totals.transferring.toString(),
|
|
12092
|
+
priceUsd: null,
|
|
12093
|
+
priceEur: null,
|
|
12094
|
+
change24h: null,
|
|
12095
|
+
fiatValueUsd: null,
|
|
12096
|
+
fiatValueEur: null
|
|
12097
|
+
};
|
|
12098
|
+
}
|
|
12099
|
+
async function withPrices(raw, registry, price) {
|
|
12100
|
+
try {
|
|
12101
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12102
|
+
const quotes = await price.getPrices(names);
|
|
12103
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12104
|
+
} catch {
|
|
12105
|
+
return raw;
|
|
12106
|
+
}
|
|
12107
|
+
}
|
|
12108
|
+
function priceOne(asset, quote) {
|
|
12109
|
+
if (!quote) return asset;
|
|
12110
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12111
|
+
return {
|
|
12112
|
+
...asset,
|
|
12113
|
+
priceUsd: quote.priceUsd,
|
|
12114
|
+
priceEur: quote.priceEur ?? null,
|
|
12115
|
+
change24h: quote.change24h ?? null,
|
|
12116
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
12117
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12118
|
+
};
|
|
12119
|
+
}
|
|
12120
|
+
|
|
12121
|
+
// modules/payments-v2/machine/payload.ts
|
|
12122
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
12123
|
+
return {
|
|
12124
|
+
v: 2,
|
|
12125
|
+
recipient: recipientPubkey,
|
|
12126
|
+
coinId: request.coinId,
|
|
12127
|
+
amount: request.amount,
|
|
12128
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
12129
|
+
direct: [...spend.plan.direct],
|
|
12130
|
+
...spend.plan.split !== void 0 ? {
|
|
12131
|
+
split: {
|
|
12132
|
+
tokenId: spend.plan.split.tokenId,
|
|
12133
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
12134
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
12135
|
+
}
|
|
12136
|
+
} : {},
|
|
12137
|
+
spentStates
|
|
12138
|
+
};
|
|
12139
|
+
}
|
|
12140
|
+
function messageOf(err) {
|
|
12141
|
+
return err instanceof Error ? err.message : String(err);
|
|
12142
|
+
}
|
|
12143
|
+
|
|
11978
12144
|
// modules/payments-v2/history/History.ts
|
|
11979
12145
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
11980
12146
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -12033,13 +12199,14 @@ var History = class {
|
|
|
12033
12199
|
}
|
|
12034
12200
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
12035
12201
|
async post(build) {
|
|
12202
|
+
const record = build();
|
|
12036
12203
|
try {
|
|
12037
|
-
await this.deps.client.postHistory([
|
|
12204
|
+
await this.deps.client.postHistory([record]);
|
|
12038
12205
|
} catch (err) {
|
|
12039
12206
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
12040
12207
|
return;
|
|
12041
12208
|
}
|
|
12042
|
-
this.deps.emit("history:updated");
|
|
12209
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
12043
12210
|
}
|
|
12044
12211
|
ts(timestamp) {
|
|
12045
12212
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -12096,93 +12263,9 @@ var History = class {
|
|
|
12096
12263
|
}
|
|
12097
12264
|
};
|
|
12098
12265
|
|
|
12099
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
12100
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
12101
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
12102
|
-
return {
|
|
12103
|
-
id: tokenId,
|
|
12104
|
-
coinId: asset.coinId,
|
|
12105
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
12106
|
-
name: registry.getName(asset.coinId),
|
|
12107
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
12108
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12109
|
-
amount: asset.amount,
|
|
12110
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
12111
|
-
createdAt: snapshot.createdAt,
|
|
12112
|
-
updatedAt: snapshot.updatedAt,
|
|
12113
|
-
lazy: true,
|
|
12114
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
12115
|
-
};
|
|
12116
|
-
}
|
|
12117
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
12118
|
-
const groups = /* @__PURE__ */ new Map();
|
|
12119
|
-
for (const [tokenId, entry] of entries) {
|
|
12120
|
-
const moving = isTransferring(tokenId);
|
|
12121
|
-
for (const asset of entry.assets) {
|
|
12122
|
-
let group = groups.get(asset.coinId);
|
|
12123
|
-
if (!group) {
|
|
12124
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
12125
|
-
groups.set(asset.coinId, group);
|
|
12126
|
-
}
|
|
12127
|
-
if (moving) {
|
|
12128
|
-
group.transferring += BigInt(asset.amount);
|
|
12129
|
-
group.transferringCount += 1;
|
|
12130
|
-
} else {
|
|
12131
|
-
group.total += BigInt(asset.amount);
|
|
12132
|
-
group.count += 1;
|
|
12133
|
-
}
|
|
12134
|
-
}
|
|
12135
|
-
}
|
|
12136
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
12137
|
-
}
|
|
12138
|
-
function toAsset(coinId, totals, registry) {
|
|
12139
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
12140
|
-
return {
|
|
12141
|
-
coinId,
|
|
12142
|
-
symbol: registry.getSymbol(coinId),
|
|
12143
|
-
name: registry.getName(coinId),
|
|
12144
|
-
decimals: registry.getDecimals(coinId),
|
|
12145
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
12146
|
-
totalAmount: totals.total.toString(),
|
|
12147
|
-
tokenCount: totals.count,
|
|
12148
|
-
confirmedAmount: totals.total.toString(),
|
|
12149
|
-
unconfirmedAmount: "0",
|
|
12150
|
-
confirmedTokenCount: totals.count,
|
|
12151
|
-
unconfirmedTokenCount: 0,
|
|
12152
|
-
transferringTokenCount: totals.transferringCount,
|
|
12153
|
-
transferringAmount: totals.transferring.toString(),
|
|
12154
|
-
priceUsd: null,
|
|
12155
|
-
priceEur: null,
|
|
12156
|
-
change24h: null,
|
|
12157
|
-
fiatValueUsd: null,
|
|
12158
|
-
fiatValueEur: null
|
|
12159
|
-
};
|
|
12160
|
-
}
|
|
12161
|
-
async function withPrices(raw, registry, price) {
|
|
12162
|
-
try {
|
|
12163
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
12164
|
-
const quotes = await price.getPrices(names);
|
|
12165
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
12166
|
-
} catch {
|
|
12167
|
-
return raw;
|
|
12168
|
-
}
|
|
12169
|
-
}
|
|
12170
|
-
function priceOne(asset, quote) {
|
|
12171
|
-
if (!quote) return asset;
|
|
12172
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
12173
|
-
return {
|
|
12174
|
-
...asset,
|
|
12175
|
-
priceUsd: quote.priceUsd,
|
|
12176
|
-
priceEur: quote.priceEur ?? null,
|
|
12177
|
-
change24h: quote.change24h ?? null,
|
|
12178
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
12179
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
12180
|
-
};
|
|
12181
|
-
}
|
|
12182
|
-
|
|
12183
12266
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
12184
|
-
var SUSPECTED_KEY =
|
|
12185
|
-
var KNOWN_SPENDS_KEY =
|
|
12267
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
12268
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
12186
12269
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
12187
12270
|
function stateKey(tokenId, stateHash) {
|
|
12188
12271
|
return `${tokenId}:${stateHash}`;
|
|
@@ -12486,19 +12569,24 @@ var Receive = class {
|
|
|
12486
12569
|
const engine = deps.engine();
|
|
12487
12570
|
const stored = [];
|
|
12488
12571
|
const pending = [];
|
|
12489
|
-
|
|
12572
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
12490
12573
|
try {
|
|
12491
|
-
epoch = deps.syncEpoch();
|
|
12492
12574
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
12493
|
-
|
|
12494
|
-
for
|
|
12495
|
-
await
|
|
12496
|
-
|
|
12575
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
12576
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
12577
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
12578
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
12579
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
12580
|
+
}
|
|
12581
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
12582
|
+
const served = deps.delivery.incomingEpoch();
|
|
12583
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
12584
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
12585
|
+
basis = null;
|
|
12497
12586
|
}
|
|
12498
|
-
await flushAcks(deps, pending, epoch);
|
|
12499
12587
|
} catch (err) {
|
|
12500
12588
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
12501
|
-
await flushAcks(deps, pending,
|
|
12589
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
12502
12590
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
12503
12591
|
});
|
|
12504
12592
|
}
|
|
@@ -12571,7 +12659,7 @@ async function announce(deps, entry, record) {
|
|
|
12571
12659
|
receivedAt
|
|
12572
12660
|
};
|
|
12573
12661
|
}
|
|
12574
|
-
async function flushAcks(deps, pending,
|
|
12662
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
12575
12663
|
let lastAcked = null;
|
|
12576
12664
|
try {
|
|
12577
12665
|
while (pending.length > 0) {
|
|
@@ -12582,7 +12670,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
12582
12670
|
}
|
|
12583
12671
|
} finally {
|
|
12584
12672
|
if (lastAcked !== null) {
|
|
12585
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
12673
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
12586
12674
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
12587
12675
|
}
|
|
12588
12676
|
}
|
|
@@ -12683,9 +12771,19 @@ var Requests = class {
|
|
|
12683
12771
|
}
|
|
12684
12772
|
});
|
|
12685
12773
|
}
|
|
12774
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
12686
12775
|
writeLink(requestId, transferId, committed) {
|
|
12687
12776
|
return this.mutateJournal((m) => {
|
|
12688
|
-
|
|
12777
|
+
const prev = m.get(requestId);
|
|
12778
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
12779
|
+
const next = {
|
|
12780
|
+
requestId,
|
|
12781
|
+
transferId,
|
|
12782
|
+
committed: committed || same && prev.committed,
|
|
12783
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
12784
|
+
};
|
|
12785
|
+
if (same && prev.committed === next.committed) return false;
|
|
12786
|
+
m.set(requestId, next);
|
|
12689
12787
|
return true;
|
|
12690
12788
|
});
|
|
12691
12789
|
}
|
|
@@ -12838,23 +12936,46 @@ var Requests = class {
|
|
|
12838
12936
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
12839
12937
|
if (transferId !== void 0) {
|
|
12840
12938
|
const committed = error instanceof PartialSendConflictError;
|
|
12841
|
-
await this.
|
|
12842
|
-
this.setStatus(id, "settling");
|
|
12939
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
12843
12940
|
} else {
|
|
12844
12941
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
12845
12942
|
this.setStatus(id, "paid");
|
|
12846
12943
|
}
|
|
12847
12944
|
} else {
|
|
12848
|
-
await this.
|
|
12849
|
-
this.setStatus(id, "pending");
|
|
12945
|
+
await this.revertPayable(id);
|
|
12850
12946
|
}
|
|
12851
12947
|
throw error;
|
|
12852
12948
|
}
|
|
12853
|
-
this.
|
|
12854
|
-
await this.respondPaid(id, result.id);
|
|
12855
|
-
await this.clearLink(id);
|
|
12949
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
12856
12950
|
return result;
|
|
12857
12951
|
}
|
|
12952
|
+
/**
|
|
12953
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
12954
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
12955
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
12956
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
12957
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
12958
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
12959
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
12960
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
12961
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
12962
|
+
*/
|
|
12963
|
+
async settle(requestId, transferId, opts) {
|
|
12964
|
+
let journalDown = false;
|
|
12965
|
+
try {
|
|
12966
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
12967
|
+
} catch (err) {
|
|
12968
|
+
if (!opts.respond) throw err;
|
|
12969
|
+
journalDown = true;
|
|
12970
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
12971
|
+
}
|
|
12972
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
12973
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
12974
|
+
return;
|
|
12975
|
+
}
|
|
12976
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
12977
|
+
this.setStatus(requestId, "paid");
|
|
12978
|
+
}
|
|
12858
12979
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
12859
12980
|
async respondPaid(id, transferId) {
|
|
12860
12981
|
try {
|
|
@@ -12890,32 +13011,32 @@ var Requests = class {
|
|
|
12890
13011
|
let aborted = null;
|
|
12891
13012
|
for (const [requestId, link] of [...this.journal]) {
|
|
12892
13013
|
if (link.committed || resumed.has(link.transferId)) {
|
|
12893
|
-
await this.resolvePaid(requestId, link
|
|
13014
|
+
await this.resolvePaid(requestId, link);
|
|
12894
13015
|
} else if (conflicted.has(link.transferId)) {
|
|
12895
13016
|
await this.revertPayable(requestId);
|
|
12896
13017
|
} else if (open.has(link.transferId)) {
|
|
12897
13018
|
} else {
|
|
12898
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
13019
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
12899
13020
|
}
|
|
12900
13021
|
}
|
|
12901
13022
|
}
|
|
12902
13023
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
12903
13024
|
// a probe failure defers the link to the next session.
|
|
12904
|
-
async reconcileUnaccounted(requestId,
|
|
13025
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
12905
13026
|
try {
|
|
12906
13027
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
12907
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
12908
|
-
else await this.resolvePaid(requestId,
|
|
13028
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
13029
|
+
else await this.resolvePaid(requestId, link);
|
|
12909
13030
|
} catch (err) {
|
|
12910
13031
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
12911
13032
|
}
|
|
12912
13033
|
return aborted;
|
|
12913
13034
|
}
|
|
12914
|
-
|
|
12915
|
-
|
|
12916
|
-
|
|
12917
|
-
this.setStatus(requestId, "paid");
|
|
13035
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
13036
|
+
resolvePaid(requestId, link) {
|
|
13037
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
12918
13038
|
}
|
|
13039
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
12919
13040
|
async revertPayable(requestId) {
|
|
12920
13041
|
await this.clearLink(requestId);
|
|
12921
13042
|
this.setStatus(requestId, "pending");
|
|
@@ -13239,11 +13360,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13239
13360
|
client: deps.client,
|
|
13240
13361
|
fieldKey: deps.fieldKey,
|
|
13241
13362
|
registry: deps.registry,
|
|
13242
|
-
emit: (event) => deps.emit(event,
|
|
13363
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
13243
13364
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
13244
13365
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
13245
13366
|
});
|
|
13246
13367
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
13368
|
+
const machineStores = createMachineStores(deps.kv);
|
|
13247
13369
|
const heldStates = /* @__PURE__ */ new Map();
|
|
13248
13370
|
return {
|
|
13249
13371
|
ownPubkeyBytes,
|
|
@@ -13251,12 +13373,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13251
13373
|
ledger,
|
|
13252
13374
|
queue,
|
|
13253
13375
|
historyStore,
|
|
13254
|
-
machineStores
|
|
13376
|
+
machineStores,
|
|
13255
13377
|
machineDeps,
|
|
13256
13378
|
machine: new TransferMachine(machineDeps),
|
|
13257
13379
|
heldStates,
|
|
13258
13380
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
13259
|
-
requests: buildRequests(deps, hooks)
|
|
13381
|
+
requests: buildRequests(deps, hooks),
|
|
13382
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13383
|
+
};
|
|
13384
|
+
}
|
|
13385
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13386
|
+
return {
|
|
13387
|
+
stores: machineStores,
|
|
13388
|
+
kv: deps.kv,
|
|
13389
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
13390
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
13391
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
13392
|
+
fullRePull: () => view.onEpochReset(),
|
|
13393
|
+
attention: (transferId, code, detail) => {
|
|
13394
|
+
deps.emit(
|
|
13395
|
+
"transfer:attention",
|
|
13396
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13397
|
+
);
|
|
13398
|
+
}
|
|
13260
13399
|
};
|
|
13261
13400
|
}
|
|
13262
13401
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -13278,11 +13417,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
13278
13417
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
13279
13418
|
emit: deps.emit,
|
|
13280
13419
|
now: deps.now ?? Date.now,
|
|
13281
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
13420
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
13282
13421
|
await historyStore.recordSent({
|
|
13283
13422
|
transferId,
|
|
13284
13423
|
coinId: payload.coinId,
|
|
13285
|
-
|
|
13424
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
13425
|
+
// recipient blobs), never payload.amount — the plan.
|
|
13426
|
+
amount: committedAmount,
|
|
13286
13427
|
recipientPubkey: payload.recipient,
|
|
13287
13428
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
13288
13429
|
});
|
|
@@ -13309,7 +13450,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
13309
13450
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
13310
13451
|
);
|
|
13311
13452
|
},
|
|
13312
|
-
syncEpoch: deps.syncEpoch
|
|
13453
|
+
syncEpoch: deps.syncEpoch,
|
|
13313
13454
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13314
13455
|
});
|
|
13315
13456
|
}
|
|
@@ -13362,6 +13503,7 @@ var PaymentsFacade = class {
|
|
|
13362
13503
|
this.receiveLoop = parts.receiveLoop;
|
|
13363
13504
|
this.heldStates = parts.heldStates;
|
|
13364
13505
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13506
|
+
this.restoreDeps = parts.restoreDeps;
|
|
13365
13507
|
this.requests = parts.requests;
|
|
13366
13508
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13367
13509
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -13390,6 +13532,7 @@ var PaymentsFacade = class {
|
|
|
13390
13532
|
receiveLoop;
|
|
13391
13533
|
heldStates;
|
|
13392
13534
|
ownPubkeyBytes;
|
|
13535
|
+
restoreDeps;
|
|
13393
13536
|
requests;
|
|
13394
13537
|
currentEngine = null;
|
|
13395
13538
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -13400,6 +13543,8 @@ var PaymentsFacade = class {
|
|
|
13400
13543
|
converger;
|
|
13401
13544
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
13402
13545
|
resumeFlight = new SingleFlight();
|
|
13546
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
13547
|
+
passChain = new SerialChain();
|
|
13403
13548
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13404
13549
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13405
13550
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -13407,8 +13552,8 @@ var PaymentsFacade = class {
|
|
|
13407
13552
|
if (this.started) return;
|
|
13408
13553
|
this.started = true;
|
|
13409
13554
|
this.heartbeat.start();
|
|
13410
|
-
await this.deps.session.start();
|
|
13411
13555
|
this.unsubscribers.push(
|
|
13556
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
13412
13557
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
13413
13558
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
13414
13559
|
}),
|
|
@@ -13423,6 +13568,7 @@ var PaymentsFacade = class {
|
|
|
13423
13568
|
if (status === "connected") this.heartbeat.recovered();
|
|
13424
13569
|
});
|
|
13425
13570
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13571
|
+
await this.deps.session.start();
|
|
13426
13572
|
this.trackTail(this.runConvergencePass());
|
|
13427
13573
|
this.trackTail(this.seedHeldStates());
|
|
13428
13574
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13463,7 +13609,7 @@ var PaymentsFacade = class {
|
|
|
13463
13609
|
}
|
|
13464
13610
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
13465
13611
|
send(request) {
|
|
13466
|
-
return this.track(this.
|
|
13612
|
+
return this.track(this.sendOutcome(request));
|
|
13467
13613
|
}
|
|
13468
13614
|
async receive() {
|
|
13469
13615
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -13478,20 +13624,27 @@ var PaymentsFacade = class {
|
|
|
13478
13624
|
if (!this.started) return;
|
|
13479
13625
|
await this.track(this.runConvergencePass());
|
|
13480
13626
|
}
|
|
13627
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
13628
|
+
async handleEpochChange(_newEpoch) {
|
|
13629
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
13630
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
13631
|
+
}
|
|
13481
13632
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
13482
13633
|
runConvergencePass() {
|
|
13483
|
-
return this.resumeFlight.run(
|
|
13484
|
-
|
|
13485
|
-
|
|
13486
|
-
|
|
13487
|
-
|
|
13634
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
13635
|
+
}
|
|
13636
|
+
async convergeBody() {
|
|
13637
|
+
const outcome = await this.converger.convergeOnce();
|
|
13638
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13639
|
+
this.heartbeat.settle(outcome, pending);
|
|
13488
13640
|
}
|
|
13489
13641
|
nowMs() {
|
|
13490
13642
|
return (this.deps.now ?? Date.now)();
|
|
13491
13643
|
}
|
|
13492
13644
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
13493
|
-
|
|
13494
|
-
|
|
13645
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
13646
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
13647
|
+
async sendOutcome(request) {
|
|
13495
13648
|
const run = {
|
|
13496
13649
|
amount: request.amount,
|
|
13497
13650
|
delivered: [],
|
|
@@ -13499,12 +13652,36 @@ var PaymentsFacade = class {
|
|
|
13499
13652
|
tokenTransfers: [],
|
|
13500
13653
|
settledShortfalls: [],
|
|
13501
13654
|
firstPartialId: void 0,
|
|
13502
|
-
pendingAny: false
|
|
13655
|
+
pendingAny: false,
|
|
13656
|
+
lastTransferId: ""
|
|
13503
13657
|
};
|
|
13658
|
+
try {
|
|
13659
|
+
return await this.sendWithPolicy(request, run);
|
|
13660
|
+
} catch (err) {
|
|
13661
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
13662
|
+
throw err;
|
|
13663
|
+
}
|
|
13664
|
+
}
|
|
13665
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
13666
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
13667
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
13668
|
+
emitCleanFailure(err, transferId) {
|
|
13669
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
13670
|
+
const failed = {
|
|
13671
|
+
id: transferId,
|
|
13672
|
+
status: "failed",
|
|
13673
|
+
tokens: [],
|
|
13674
|
+
tokenTransfers: [],
|
|
13675
|
+
error: messageOf(err)
|
|
13676
|
+
};
|
|
13677
|
+
this.deps.emit("transfer:updated", failed);
|
|
13678
|
+
}
|
|
13679
|
+
async sendWithPolicy(request, run) {
|
|
13680
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
13504
13681
|
for (let attempt = 0; ; attempt++) {
|
|
13505
13682
|
let ctx;
|
|
13506
13683
|
try {
|
|
13507
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
13684
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
13508
13685
|
} catch (err) {
|
|
13509
13686
|
throw this.partialize(err, run);
|
|
13510
13687
|
}
|
|
@@ -13549,11 +13726,8 @@ var PaymentsFacade = class {
|
|
|
13549
13726
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
13550
13727
|
}
|
|
13551
13728
|
}
|
|
13552
|
-
/**
|
|
13553
|
-
*
|
|
13554
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
13555
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
13556
|
-
*/
|
|
13729
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
13730
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
13557
13731
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
13558
13732
|
if (disposition.deliveryPending) {
|
|
13559
13733
|
run.pendingAny = true;
|
|
@@ -13579,11 +13753,8 @@ var PaymentsFacade = class {
|
|
|
13579
13753
|
run.amount = partial.remainingAmount;
|
|
13580
13754
|
return null;
|
|
13581
13755
|
}
|
|
13582
|
-
/**
|
|
13583
|
-
*
|
|
13584
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
13585
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
13586
|
-
*/
|
|
13756
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
13757
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
13587
13758
|
async disposeFailedAttempt(ctx, err) {
|
|
13588
13759
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
13589
13760
|
this.settleKeepOpen(ctx);
|
|
@@ -13632,8 +13803,9 @@ var PaymentsFacade = class {
|
|
|
13632
13803
|
}
|
|
13633
13804
|
return { kind: "rethrow", error: err };
|
|
13634
13805
|
}
|
|
13635
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
13806
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
13636
13807
|
const transferId = this.newId();
|
|
13808
|
+
run.lastTransferId = transferId;
|
|
13637
13809
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
13638
13810
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
13639
13811
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -13765,11 +13937,7 @@ var PaymentsFacade = class {
|
|
|
13765
13937
|
this.deps.emit("transfer:updated", result);
|
|
13766
13938
|
return result;
|
|
13767
13939
|
}
|
|
13768
|
-
/**
|
|
13769
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
13770
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
13771
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
13772
|
-
*/
|
|
13940
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
13773
13941
|
partialize(err, run) {
|
|
13774
13942
|
if (run.delivered.length === 0) return err;
|
|
13775
13943
|
return new PartialSendConflictError(
|
|
@@ -13921,22 +14089,14 @@ var PaymentsFacade = class {
|
|
|
13921
14089
|
}
|
|
13922
14090
|
}
|
|
13923
14091
|
toUiToken(tokenId, coinId, amount) {
|
|
13924
|
-
const registry = this.deps.registry;
|
|
13925
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
13926
14092
|
const now = this.nowMs();
|
|
13927
|
-
return
|
|
13928
|
-
|
|
13929
|
-
|
|
13930
|
-
|
|
13931
|
-
|
|
13932
|
-
|
|
13933
|
-
|
|
13934
|
-
amount: amount.toString(),
|
|
13935
|
-
status: "transferring",
|
|
13936
|
-
createdAt: now,
|
|
13937
|
-
updatedAt: now,
|
|
13938
|
-
lazy: true
|
|
13939
|
-
};
|
|
14093
|
+
return toToken(
|
|
14094
|
+
tokenId,
|
|
14095
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
14096
|
+
{ coinId, amount: amount.toString() },
|
|
14097
|
+
this.deps.registry,
|
|
14098
|
+
{ transferring: true, suspectedSpent: false }
|
|
14099
|
+
);
|
|
13940
14100
|
}
|
|
13941
14101
|
track(op) {
|
|
13942
14102
|
const settled = op.then(
|
|
@@ -13951,27 +14111,6 @@ var PaymentsFacade = class {
|
|
|
13951
14111
|
this.track(op).catch(() => void 0);
|
|
13952
14112
|
}
|
|
13953
14113
|
};
|
|
13954
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
13955
|
-
return {
|
|
13956
|
-
v: 2,
|
|
13957
|
-
recipient: recipientPubkey,
|
|
13958
|
-
coinId: request.coinId,
|
|
13959
|
-
amount: request.amount,
|
|
13960
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
13961
|
-
direct: [...spend.plan.direct],
|
|
13962
|
-
...spend.plan.split !== void 0 ? {
|
|
13963
|
-
split: {
|
|
13964
|
-
tokenId: spend.plan.split.tokenId,
|
|
13965
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
13966
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
13967
|
-
}
|
|
13968
|
-
} : {},
|
|
13969
|
-
spentStates
|
|
13970
|
-
};
|
|
13971
|
-
}
|
|
13972
|
-
function messageOf(err) {
|
|
13973
|
-
return err instanceof Error ? err.message : String(err);
|
|
13974
|
-
}
|
|
13975
14114
|
|
|
13976
14115
|
// impl/wallet-api-v2/http.ts
|
|
13977
14116
|
var WalletApiHttpError = class extends Error {
|
|
@@ -14317,6 +14456,7 @@ var WalletApiSession = class {
|
|
|
14317
14456
|
epochLatch = null;
|
|
14318
14457
|
reseedChain = Promise.resolve();
|
|
14319
14458
|
reseedsPending = 0;
|
|
14459
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
14320
14460
|
handlers = /* @__PURE__ */ new Map();
|
|
14321
14461
|
statusHandlers = /* @__PURE__ */ new Set();
|
|
14322
14462
|
socket = null;
|
|
@@ -14419,6 +14559,18 @@ var WalletApiSession = class {
|
|
|
14419
14559
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
14420
14560
|
}
|
|
14421
14561
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
14562
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
14563
|
+
currentEpoch() {
|
|
14564
|
+
return this.epochLatch ?? "";
|
|
14565
|
+
}
|
|
14566
|
+
/**
|
|
14567
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
14568
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
14569
|
+
*/
|
|
14570
|
+
subscribeEpochChange(handler) {
|
|
14571
|
+
this.epochHandlers.add(handler);
|
|
14572
|
+
return () => this.epochHandlers.delete(handler);
|
|
14573
|
+
}
|
|
14422
14574
|
noteEpoch(epoch) {
|
|
14423
14575
|
if (this.epochLatch === epoch) return;
|
|
14424
14576
|
const previous = this.epochLatch;
|
|
@@ -14432,7 +14584,7 @@ var WalletApiSession = class {
|
|
|
14432
14584
|
}
|
|
14433
14585
|
async runReseed(epoch, previous) {
|
|
14434
14586
|
try {
|
|
14435
|
-
|
|
14587
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
14436
14588
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
14437
14589
|
} catch {
|
|
14438
14590
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -14694,6 +14846,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14694
14846
|
wake;
|
|
14695
14847
|
deriveFn;
|
|
14696
14848
|
seenChain = new SerialChain();
|
|
14849
|
+
lastIncomingEpoch = null;
|
|
14697
14850
|
constructor(config) {
|
|
14698
14851
|
this.client = config.client;
|
|
14699
14852
|
this.kv = config.kv;
|
|
@@ -14788,6 +14941,7 @@ var WalletApiDeliveryPort = class {
|
|
|
14788
14941
|
const seen = await this.readSeen();
|
|
14789
14942
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
14790
14943
|
for (; ; ) {
|
|
14944
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
14791
14945
|
for (const entry of page.entries) {
|
|
14792
14946
|
if (entry.status !== "unclaimed") continue;
|
|
14793
14947
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -14797,6 +14951,10 @@ var WalletApiDeliveryPort = class {
|
|
|
14797
14951
|
page = await this.client.listMailbox(page.cursor);
|
|
14798
14952
|
}
|
|
14799
14953
|
}
|
|
14954
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
14955
|
+
incomingEpoch() {
|
|
14956
|
+
return this.lastIncomingEpoch;
|
|
14957
|
+
}
|
|
14800
14958
|
toIncoming(entry) {
|
|
14801
14959
|
const bundle = this.openEnvelope(entry);
|
|
14802
14960
|
return {
|
|
@@ -14874,7 +15032,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14874
15032
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
14875
15033
|
}
|
|
14876
15034
|
async put(transferId, opIndex, bytes) {
|
|
14877
|
-
const {
|
|
15035
|
+
const { kv, fieldKey } = this.config;
|
|
14878
15036
|
const aad = this.aad(transferId, opIndex);
|
|
14879
15037
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
14880
15038
|
let envelope = await kv.get(cacheKey);
|
|
@@ -14882,10 +15040,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14882
15040
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
14883
15041
|
await kv.set(cacheKey, envelope);
|
|
14884
15042
|
}
|
|
14885
|
-
const
|
|
14886
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
15043
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
14887
15044
|
return this.open(record.payload, aad);
|
|
14888
15045
|
}
|
|
15046
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
15047
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
15048
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
15049
|
+
if (envelope === null) return false;
|
|
15050
|
+
await this.post(transferId, opIndex, envelope);
|
|
15051
|
+
return true;
|
|
15052
|
+
}
|
|
15053
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
15054
|
+
async post(transferId, opIndex, envelope) {
|
|
15055
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
15056
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
15057
|
+
}
|
|
14889
15058
|
async get(transferId, opIndex) {
|
|
14890
15059
|
const records = await this.config.client.getProgress(transferId);
|
|
14891
15060
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -14908,7 +15077,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
14908
15077
|
};
|
|
14909
15078
|
|
|
14910
15079
|
// core/payments-v2-wiring.ts
|
|
14911
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
15080
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
14912
15081
|
const source = walletApi;
|
|
14913
15082
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
14914
15083
|
if (!source || network === null) {
|
|
@@ -14917,6 +15086,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
14917
15086
|
"INVALID_CONFIG"
|
|
14918
15087
|
);
|
|
14919
15088
|
}
|
|
15089
|
+
if (!(network in NETWORKS)) {
|
|
15090
|
+
throw new SphereError(
|
|
15091
|
+
`payments: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
15092
|
+
"INVALID_CONFIG"
|
|
15093
|
+
);
|
|
15094
|
+
}
|
|
15095
|
+
if (network !== sphereNetwork) {
|
|
15096
|
+
throw new SphereError(
|
|
15097
|
+
`payments: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
15098
|
+
"INVALID_CONFIG"
|
|
15099
|
+
);
|
|
15100
|
+
}
|
|
14920
15101
|
if (typeof source.paymentsV2Transport === "function") {
|
|
14921
15102
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
14922
15103
|
}
|
|
@@ -14981,8 +15162,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
14981
15162
|
deviceId: `${config.deviceId}:pv2`,
|
|
14982
15163
|
kv: args.kv,
|
|
14983
15164
|
webSocketFactory,
|
|
14984
|
-
emitStatus: args.emitStatus
|
|
14985
|
-
onEpochChange: args.onEpochChange
|
|
15165
|
+
emitStatus: args.emitStatus
|
|
14986
15166
|
});
|
|
14987
15167
|
return { session, client: authedWireClient(session, client) };
|
|
14988
15168
|
};
|
|
@@ -15052,8 +15232,7 @@ function composePaymentsV2(spec) {
|
|
|
15052
15232
|
identity: identitySlice,
|
|
15053
15233
|
network,
|
|
15054
15234
|
kv,
|
|
15055
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15056
|
-
onEpochChange: async () => void 0
|
|
15235
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
15057
15236
|
});
|
|
15058
15237
|
const deps = {
|
|
15059
15238
|
session: transport.session,
|
|
@@ -15083,7 +15262,10 @@ function composePaymentsV2(spec) {
|
|
|
15083
15262
|
network,
|
|
15084
15263
|
ownPubkey: identity.chainPubkey,
|
|
15085
15264
|
ownNametag: host.nametag,
|
|
15086
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
15265
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
15266
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
15267
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
15268
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
15087
15269
|
};
|
|
15088
15270
|
return new PaymentsFacade(deps);
|
|
15089
15271
|
}
|
|
@@ -15238,7 +15420,7 @@ var Sphere = class _Sphere {
|
|
|
15238
15420
|
static async init(options) {
|
|
15239
15421
|
if (options.debug) logger.configure({ debug: true });
|
|
15240
15422
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15241
|
-
resolvePaymentsV2Composition(options.walletApi);
|
|
15423
|
+
resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15242
15424
|
_Sphere.configureTokenRegistry(options.storage, options.network);
|
|
15243
15425
|
const groupChat = _Sphere.resolveGroupChatConfig(options.groupChat, options.network);
|
|
15244
15426
|
const market = _Sphere.resolveMarketConfig(options.market);
|
|
@@ -15380,7 +15562,7 @@ var Sphere = class _Sphere {
|
|
|
15380
15562
|
static async create(options) {
|
|
15381
15563
|
if (options.debug) logger.configure({ debug: true });
|
|
15382
15564
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15383
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15565
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15384
15566
|
if (!options.mnemonic || !_Sphere.validateMnemonic(options.mnemonic)) {
|
|
15385
15567
|
throw new SphereError("Invalid mnemonic", "INVALID_IDENTITY");
|
|
15386
15568
|
}
|
|
@@ -15448,7 +15630,7 @@ var Sphere = class _Sphere {
|
|
|
15448
15630
|
static async load(options) {
|
|
15449
15631
|
if (options.debug) logger.configure({ debug: true });
|
|
15450
15632
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15451
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15633
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15452
15634
|
if (!await _Sphere.exists(options.storage)) {
|
|
15453
15635
|
throw new SphereError("No wallet found. Use Sphere.create() to create a new wallet.", "NOT_INITIALIZED");
|
|
15454
15636
|
}
|
|
@@ -15502,7 +15684,7 @@ var Sphere = class _Sphere {
|
|
|
15502
15684
|
static async import(options) {
|
|
15503
15685
|
if (options.debug) logger.configure({ debug: true });
|
|
15504
15686
|
_Sphere.refuseRetiredModuleOptions(options);
|
|
15505
|
-
const composition = resolvePaymentsV2Composition(options.walletApi);
|
|
15687
|
+
const composition = resolvePaymentsV2Composition(options.walletApi, options.network);
|
|
15506
15688
|
if (!options.mnemonic && !options.masterKey) {
|
|
15507
15689
|
throw new SphereError("Either mnemonic or masterKey is required", "INVALID_CONFIG");
|
|
15508
15690
|
}
|