@unicitylabs/sphere-sdk 0.14.0-dev.6 → 0.14.0-dev.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +1 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +416 -223
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +111 -108
- package/dist/core/index.d.ts +111 -108
- package/dist/core/index.js +416 -223
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +1 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +1 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/wallet-api-v2/index.cjs +38 -5
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +23 -2
- package/dist/impl/wallet-api-v2/index.d.ts +23 -2
- package/dist/impl/wallet-api-v2/index.js +38 -5
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +416 -223
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -109
- package/dist/index.d.ts +112 -109
- package/dist/index.js +416 -223
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +340 -199
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +68 -19
- package/dist/modules/payments-v2/index.d.ts +68 -19
- package/dist/modules/payments-v2/index.js +339 -199
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var payments_v2_exports = {};
|
|
32
32
|
__export(payments_v2_exports, {
|
|
33
33
|
ATTENTION_MINT_UNRESOLVED: () => ATTENTION_MINT_UNRESOLVED,
|
|
34
|
+
ATTENTION_RESEED_REJECTED: () => ATTENTION_RESEED_REJECTED,
|
|
34
35
|
HEARTBEAT_CAP_MS: () => HEARTBEAT_CAP_MS,
|
|
35
36
|
HEARTBEAT_SEED_MS: () => HEARTBEAT_SEED_MS,
|
|
36
37
|
MAX_RESELECT: () => MAX_RESELECT,
|
|
@@ -66,7 +67,10 @@ var STORE_KEYS = {
|
|
|
66
67
|
shortfalls: "shortfalls",
|
|
67
68
|
settlingLinks: "settling",
|
|
68
69
|
streamCursor: (s) => `cursor:${s}`,
|
|
69
|
-
epochLatch: "epoch-latch"
|
|
70
|
+
epochLatch: "epoch-latch",
|
|
71
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
72
|
+
suspectedSpent: "suspected-spent",
|
|
73
|
+
knownSpends: "known-spends"
|
|
70
74
|
};
|
|
71
75
|
|
|
72
76
|
// node_modules/@noble/hashes/utils.js
|
|
@@ -858,7 +862,7 @@ var TransferMachine = class {
|
|
|
858
862
|
}
|
|
859
863
|
if (cls === null) {
|
|
860
864
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
861
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
865
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
862
866
|
return { committed, deliveryPending };
|
|
863
867
|
}
|
|
864
868
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -884,10 +888,10 @@ var TransferMachine = class {
|
|
|
884
888
|
}
|
|
885
889
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
886
890
|
if (conflicts.length > 0) {
|
|
887
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
891
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
888
892
|
}
|
|
889
893
|
await this.completeIntent(r.transferId, r.payload);
|
|
890
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
894
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
891
895
|
return { deliveryPending, partial: null };
|
|
892
896
|
}
|
|
893
897
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -1069,7 +1073,7 @@ var TransferMachine = class {
|
|
|
1069
1073
|
}
|
|
1070
1074
|
return added;
|
|
1071
1075
|
}
|
|
1072
|
-
async settlePartial(r, committed, conflicts) {
|
|
1076
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
1073
1077
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
1074
1078
|
const entry = {
|
|
1075
1079
|
transferId: r.transferId,
|
|
@@ -1081,26 +1085,27 @@ var TransferMachine = class {
|
|
|
1081
1085
|
};
|
|
1082
1086
|
await this.stores.shortfalls.upsert(entry);
|
|
1083
1087
|
await this.completeIntent(r.transferId, r.payload);
|
|
1084
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
1088
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
1085
1089
|
return entry;
|
|
1086
1090
|
}
|
|
1087
|
-
closeTail(transferId, payload) {
|
|
1091
|
+
closeTail(engine, transferId, payload, committed) {
|
|
1088
1092
|
void (async () => {
|
|
1089
1093
|
try {
|
|
1090
1094
|
await this.completeIntent(transferId, payload);
|
|
1091
1095
|
} catch {
|
|
1092
1096
|
return;
|
|
1093
1097
|
}
|
|
1094
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
1098
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
1095
1099
|
})();
|
|
1096
1100
|
}
|
|
1097
1101
|
async completeIntent(transferId, payload) {
|
|
1098
1102
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
1099
1103
|
await this.deps.intents.complete(transferId, signature);
|
|
1100
1104
|
}
|
|
1101
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
1105
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
1102
1106
|
try {
|
|
1103
|
-
await this.
|
|
1107
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
1108
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
1104
1109
|
} catch {
|
|
1105
1110
|
}
|
|
1106
1111
|
try {
|
|
@@ -1108,6 +1113,26 @@ var TransferMachine = class {
|
|
|
1108
1113
|
} catch {
|
|
1109
1114
|
}
|
|
1110
1115
|
}
|
|
1116
|
+
/**
|
|
1117
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
1118
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
1119
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
1120
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
1121
|
+
*/
|
|
1122
|
+
async settledAmount(engine, coinId, committed) {
|
|
1123
|
+
let total = 0n;
|
|
1124
|
+
for (const outcome of committed) {
|
|
1125
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
1126
|
+
const token = await engine.decodeToken({
|
|
1127
|
+
v: TOKEN_BLOB_VERSION,
|
|
1128
|
+
network: 0,
|
|
1129
|
+
tokenId: "",
|
|
1130
|
+
token: outcome.recipientBlob
|
|
1131
|
+
});
|
|
1132
|
+
total += engine.balanceOf(token, coinId);
|
|
1133
|
+
}
|
|
1134
|
+
return total.toString();
|
|
1135
|
+
}
|
|
1111
1136
|
async disposeOnFailure(transferId, committed, cls) {
|
|
1112
1137
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
1113
1138
|
try {
|
|
@@ -1132,7 +1157,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
1132
1157
|
}
|
|
1133
1158
|
|
|
1134
1159
|
// modules/payments-v2/machine/resume.ts
|
|
1135
|
-
async function resumeAll(deps) {
|
|
1160
|
+
async function resumeAll(deps, options = {}) {
|
|
1136
1161
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
1137
1162
|
const stores = createMachineStores(deps.kv);
|
|
1138
1163
|
const open = await deps.intents.listOpen();
|
|
@@ -1142,7 +1167,12 @@ async function resumeAll(deps) {
|
|
|
1142
1167
|
} catch {
|
|
1143
1168
|
return report;
|
|
1144
1169
|
}
|
|
1145
|
-
await
|
|
1170
|
+
await dropStaleBackstops(
|
|
1171
|
+
stores,
|
|
1172
|
+
locals,
|
|
1173
|
+
new Set(open.map((i) => i.transferId)),
|
|
1174
|
+
options.isLocallyActive ?? (() => false)
|
|
1175
|
+
);
|
|
1146
1176
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
1147
1177
|
const ctx = {
|
|
1148
1178
|
deps,
|
|
@@ -1155,11 +1185,16 @@ async function resumeAll(deps) {
|
|
|
1155
1185
|
}
|
|
1156
1186
|
return report;
|
|
1157
1187
|
}
|
|
1158
|
-
async function
|
|
1188
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
1159
1189
|
for (const entry of locals.values()) {
|
|
1160
|
-
if (
|
|
1190
|
+
if (openIds.has(entry.transferId)) continue;
|
|
1191
|
+
if (entry.disposition !== "open") {
|
|
1161
1192
|
await stores.backstop.removeByKey(entry.transferId);
|
|
1193
|
+
continue;
|
|
1162
1194
|
}
|
|
1195
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
1196
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
1197
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
1163
1198
|
}
|
|
1164
1199
|
}
|
|
1165
1200
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -1367,7 +1402,9 @@ var Converger = class {
|
|
|
1367
1402
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
1368
1403
|
}
|
|
1369
1404
|
async resumePass() {
|
|
1370
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
1405
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
1406
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
1407
|
+
});
|
|
1371
1408
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
1372
1409
|
await this.deps.reconcile({
|
|
1373
1410
|
resumed: report.resumed,
|
|
@@ -1483,6 +1520,136 @@ function distinctOpCount(entries) {
|
|
|
1483
1520
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
1484
1521
|
}
|
|
1485
1522
|
|
|
1523
|
+
// modules/payments-v2/restore.ts
|
|
1524
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
1525
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
1526
|
+
async function reseedAndReset(deps) {
|
|
1527
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
1528
|
+
if (entry.disposition !== "open") continue;
|
|
1529
|
+
try {
|
|
1530
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
1531
|
+
if (entry.requiresSeedClose) {
|
|
1532
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
1533
|
+
}
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
1539
|
+
await deps.fullRePull();
|
|
1540
|
+
}
|
|
1541
|
+
async function splitOpIndex(deps, envelope) {
|
|
1542
|
+
const payload = await deps.decryptPayload(envelope);
|
|
1543
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
1547
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
1548
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
1549
|
+
return {
|
|
1550
|
+
id: tokenId,
|
|
1551
|
+
coinId: asset.coinId,
|
|
1552
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
1553
|
+
name: registry.getName(asset.coinId),
|
|
1554
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
1555
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1556
|
+
amount: asset.amount,
|
|
1557
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
1558
|
+
createdAt: snapshot.createdAt,
|
|
1559
|
+
updatedAt: snapshot.updatedAt,
|
|
1560
|
+
lazy: true,
|
|
1561
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
1565
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1566
|
+
for (const [tokenId, entry] of entries) {
|
|
1567
|
+
const moving = isTransferring(tokenId);
|
|
1568
|
+
for (const asset of entry.assets) {
|
|
1569
|
+
let group = groups.get(asset.coinId);
|
|
1570
|
+
if (!group) {
|
|
1571
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
1572
|
+
groups.set(asset.coinId, group);
|
|
1573
|
+
}
|
|
1574
|
+
if (moving) {
|
|
1575
|
+
group.transferring += BigInt(asset.amount);
|
|
1576
|
+
group.transferringCount += 1;
|
|
1577
|
+
} else {
|
|
1578
|
+
group.total += BigInt(asset.amount);
|
|
1579
|
+
group.count += 1;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
1584
|
+
}
|
|
1585
|
+
function toAsset(coinId, totals, registry) {
|
|
1586
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
1587
|
+
return {
|
|
1588
|
+
coinId,
|
|
1589
|
+
symbol: registry.getSymbol(coinId),
|
|
1590
|
+
name: registry.getName(coinId),
|
|
1591
|
+
decimals: registry.getDecimals(coinId),
|
|
1592
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1593
|
+
totalAmount: totals.total.toString(),
|
|
1594
|
+
tokenCount: totals.count,
|
|
1595
|
+
confirmedAmount: totals.total.toString(),
|
|
1596
|
+
unconfirmedAmount: "0",
|
|
1597
|
+
confirmedTokenCount: totals.count,
|
|
1598
|
+
unconfirmedTokenCount: 0,
|
|
1599
|
+
transferringTokenCount: totals.transferringCount,
|
|
1600
|
+
transferringAmount: totals.transferring.toString(),
|
|
1601
|
+
priceUsd: null,
|
|
1602
|
+
priceEur: null,
|
|
1603
|
+
change24h: null,
|
|
1604
|
+
fiatValueUsd: null,
|
|
1605
|
+
fiatValueEur: null
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
async function withPrices(raw, registry, price) {
|
|
1609
|
+
try {
|
|
1610
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
1611
|
+
const quotes = await price.getPrices(names);
|
|
1612
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
1613
|
+
} catch {
|
|
1614
|
+
return raw;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
function priceOne(asset, quote) {
|
|
1618
|
+
if (!quote) return asset;
|
|
1619
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
1620
|
+
return {
|
|
1621
|
+
...asset,
|
|
1622
|
+
priceUsd: quote.priceUsd,
|
|
1623
|
+
priceEur: quote.priceEur ?? null,
|
|
1624
|
+
change24h: quote.change24h ?? null,
|
|
1625
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
1626
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// modules/payments-v2/machine/payload.ts
|
|
1631
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
1632
|
+
return {
|
|
1633
|
+
v: 2,
|
|
1634
|
+
recipient: recipientPubkey,
|
|
1635
|
+
coinId: request.coinId,
|
|
1636
|
+
amount: request.amount,
|
|
1637
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
1638
|
+
direct: [...spend.plan.direct],
|
|
1639
|
+
...spend.plan.split !== void 0 ? {
|
|
1640
|
+
split: {
|
|
1641
|
+
tokenId: spend.plan.split.tokenId,
|
|
1642
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
1643
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
1644
|
+
}
|
|
1645
|
+
} : {},
|
|
1646
|
+
spentStates
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
function messageOf(err) {
|
|
1650
|
+
return err instanceof Error ? err.message : String(err);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1486
1653
|
// node_modules/@noble/ciphers/utils.js
|
|
1487
1654
|
function isBytes2(a) {
|
|
1488
1655
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -2391,13 +2558,14 @@ var History = class {
|
|
|
2391
2558
|
}
|
|
2392
2559
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
2393
2560
|
async post(build) {
|
|
2561
|
+
const record = build();
|
|
2394
2562
|
try {
|
|
2395
|
-
await this.deps.client.postHistory([
|
|
2563
|
+
await this.deps.client.postHistory([record]);
|
|
2396
2564
|
} catch (err) {
|
|
2397
2565
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
2398
2566
|
return;
|
|
2399
2567
|
}
|
|
2400
|
-
this.deps.emit("history:updated");
|
|
2568
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
2401
2569
|
}
|
|
2402
2570
|
ts(timestamp) {
|
|
2403
2571
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -2454,93 +2622,9 @@ var History = class {
|
|
|
2454
2622
|
}
|
|
2455
2623
|
};
|
|
2456
2624
|
|
|
2457
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
2458
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
2459
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2460
|
-
return {
|
|
2461
|
-
id: tokenId,
|
|
2462
|
-
coinId: asset.coinId,
|
|
2463
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
2464
|
-
name: registry.getName(asset.coinId),
|
|
2465
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
2466
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2467
|
-
amount: asset.amount,
|
|
2468
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
2469
|
-
createdAt: snapshot.createdAt,
|
|
2470
|
-
updatedAt: snapshot.updatedAt,
|
|
2471
|
-
lazy: true,
|
|
2472
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2473
|
-
};
|
|
2474
|
-
}
|
|
2475
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
2476
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2477
|
-
for (const [tokenId, entry] of entries) {
|
|
2478
|
-
const moving = isTransferring(tokenId);
|
|
2479
|
-
for (const asset of entry.assets) {
|
|
2480
|
-
let group = groups.get(asset.coinId);
|
|
2481
|
-
if (!group) {
|
|
2482
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2483
|
-
groups.set(asset.coinId, group);
|
|
2484
|
-
}
|
|
2485
|
-
if (moving) {
|
|
2486
|
-
group.transferring += BigInt(asset.amount);
|
|
2487
|
-
group.transferringCount += 1;
|
|
2488
|
-
} else {
|
|
2489
|
-
group.total += BigInt(asset.amount);
|
|
2490
|
-
group.count += 1;
|
|
2491
|
-
}
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
2495
|
-
}
|
|
2496
|
-
function toAsset(coinId, totals, registry) {
|
|
2497
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
2498
|
-
return {
|
|
2499
|
-
coinId,
|
|
2500
|
-
symbol: registry.getSymbol(coinId),
|
|
2501
|
-
name: registry.getName(coinId),
|
|
2502
|
-
decimals: registry.getDecimals(coinId),
|
|
2503
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2504
|
-
totalAmount: totals.total.toString(),
|
|
2505
|
-
tokenCount: totals.count,
|
|
2506
|
-
confirmedAmount: totals.total.toString(),
|
|
2507
|
-
unconfirmedAmount: "0",
|
|
2508
|
-
confirmedTokenCount: totals.count,
|
|
2509
|
-
unconfirmedTokenCount: 0,
|
|
2510
|
-
transferringTokenCount: totals.transferringCount,
|
|
2511
|
-
transferringAmount: totals.transferring.toString(),
|
|
2512
|
-
priceUsd: null,
|
|
2513
|
-
priceEur: null,
|
|
2514
|
-
change24h: null,
|
|
2515
|
-
fiatValueUsd: null,
|
|
2516
|
-
fiatValueEur: null
|
|
2517
|
-
};
|
|
2518
|
-
}
|
|
2519
|
-
async function withPrices(raw, registry, price) {
|
|
2520
|
-
try {
|
|
2521
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
2522
|
-
const quotes = await price.getPrices(names);
|
|
2523
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
2524
|
-
} catch {
|
|
2525
|
-
return raw;
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
function priceOne(asset, quote) {
|
|
2529
|
-
if (!quote) return asset;
|
|
2530
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
2531
|
-
return {
|
|
2532
|
-
...asset,
|
|
2533
|
-
priceUsd: quote.priceUsd,
|
|
2534
|
-
priceEur: quote.priceEur ?? null,
|
|
2535
|
-
change24h: quote.change24h ?? null,
|
|
2536
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
2537
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
2538
|
-
};
|
|
2539
|
-
}
|
|
2540
|
-
|
|
2541
2625
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2542
|
-
var SUSPECTED_KEY =
|
|
2543
|
-
var KNOWN_SPENDS_KEY =
|
|
2626
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2627
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2544
2628
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
2545
2629
|
function stateKey(tokenId, stateHash) {
|
|
2546
2630
|
return `${tokenId}:${stateHash}`;
|
|
@@ -2936,19 +3020,24 @@ var Receive = class {
|
|
|
2936
3020
|
const engine = deps.engine();
|
|
2937
3021
|
const stored = [];
|
|
2938
3022
|
const pending = [];
|
|
2939
|
-
|
|
3023
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2940
3024
|
try {
|
|
2941
|
-
epoch = deps.syncEpoch();
|
|
2942
3025
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2943
|
-
|
|
2944
|
-
for
|
|
2945
|
-
await
|
|
2946
|
-
|
|
3026
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
3027
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
3028
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
3029
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
3030
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
3031
|
+
}
|
|
3032
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
3033
|
+
const served = deps.delivery.incomingEpoch();
|
|
3034
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
3035
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
3036
|
+
basis = null;
|
|
2947
3037
|
}
|
|
2948
|
-
await flushAcks(deps, pending, epoch);
|
|
2949
3038
|
} catch (err) {
|
|
2950
3039
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
2951
|
-
await flushAcks(deps, pending,
|
|
3040
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
2952
3041
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
2953
3042
|
});
|
|
2954
3043
|
}
|
|
@@ -3021,7 +3110,7 @@ async function announce(deps, entry, record) {
|
|
|
3021
3110
|
receivedAt
|
|
3022
3111
|
};
|
|
3023
3112
|
}
|
|
3024
|
-
async function flushAcks(deps, pending,
|
|
3113
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
3025
3114
|
let lastAcked = null;
|
|
3026
3115
|
try {
|
|
3027
3116
|
while (pending.length > 0) {
|
|
@@ -3032,7 +3121,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
3032
3121
|
}
|
|
3033
3122
|
} finally {
|
|
3034
3123
|
if (lastAcked !== null) {
|
|
3035
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
3124
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
3036
3125
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
3037
3126
|
}
|
|
3038
3127
|
}
|
|
@@ -3133,9 +3222,19 @@ var Requests = class {
|
|
|
3133
3222
|
}
|
|
3134
3223
|
});
|
|
3135
3224
|
}
|
|
3225
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
3136
3226
|
writeLink(requestId, transferId, committed) {
|
|
3137
3227
|
return this.mutateJournal((m) => {
|
|
3138
|
-
|
|
3228
|
+
const prev = m.get(requestId);
|
|
3229
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
3230
|
+
const next = {
|
|
3231
|
+
requestId,
|
|
3232
|
+
transferId,
|
|
3233
|
+
committed: committed || same && prev.committed,
|
|
3234
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
3235
|
+
};
|
|
3236
|
+
if (same && prev.committed === next.committed) return false;
|
|
3237
|
+
m.set(requestId, next);
|
|
3139
3238
|
return true;
|
|
3140
3239
|
});
|
|
3141
3240
|
}
|
|
@@ -3288,23 +3387,46 @@ var Requests = class {
|
|
|
3288
3387
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
3289
3388
|
if (transferId !== void 0) {
|
|
3290
3389
|
const committed = error instanceof PartialSendConflictError;
|
|
3291
|
-
await this.
|
|
3292
|
-
this.setStatus(id, "settling");
|
|
3390
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
3293
3391
|
} else {
|
|
3294
3392
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
3295
3393
|
this.setStatus(id, "paid");
|
|
3296
3394
|
}
|
|
3297
3395
|
} else {
|
|
3298
|
-
await this.
|
|
3299
|
-
this.setStatus(id, "pending");
|
|
3396
|
+
await this.revertPayable(id);
|
|
3300
3397
|
}
|
|
3301
3398
|
throw error;
|
|
3302
3399
|
}
|
|
3303
|
-
this.
|
|
3304
|
-
await this.respondPaid(id, result.id);
|
|
3305
|
-
await this.clearLink(id);
|
|
3400
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
3306
3401
|
return result;
|
|
3307
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
3405
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
3406
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
3407
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
3408
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
3409
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
3410
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
3411
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
3412
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
3413
|
+
*/
|
|
3414
|
+
async settle(requestId, transferId, opts) {
|
|
3415
|
+
let journalDown = false;
|
|
3416
|
+
try {
|
|
3417
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
3418
|
+
} catch (err) {
|
|
3419
|
+
if (!opts.respond) throw err;
|
|
3420
|
+
journalDown = true;
|
|
3421
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
3422
|
+
}
|
|
3423
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
3424
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
3425
|
+
return;
|
|
3426
|
+
}
|
|
3427
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
3428
|
+
this.setStatus(requestId, "paid");
|
|
3429
|
+
}
|
|
3308
3430
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
3309
3431
|
async respondPaid(id, transferId) {
|
|
3310
3432
|
try {
|
|
@@ -3340,32 +3462,32 @@ var Requests = class {
|
|
|
3340
3462
|
let aborted = null;
|
|
3341
3463
|
for (const [requestId, link] of [...this.journal]) {
|
|
3342
3464
|
if (link.committed || resumed.has(link.transferId)) {
|
|
3343
|
-
await this.resolvePaid(requestId, link
|
|
3465
|
+
await this.resolvePaid(requestId, link);
|
|
3344
3466
|
} else if (conflicted.has(link.transferId)) {
|
|
3345
3467
|
await this.revertPayable(requestId);
|
|
3346
3468
|
} else if (open.has(link.transferId)) {
|
|
3347
3469
|
} else {
|
|
3348
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
3470
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
3349
3471
|
}
|
|
3350
3472
|
}
|
|
3351
3473
|
}
|
|
3352
3474
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
3353
3475
|
// a probe failure defers the link to the next session.
|
|
3354
|
-
async reconcileUnaccounted(requestId,
|
|
3476
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
3355
3477
|
try {
|
|
3356
3478
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
3357
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
3358
|
-
else await this.resolvePaid(requestId,
|
|
3479
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
3480
|
+
else await this.resolvePaid(requestId, link);
|
|
3359
3481
|
} catch (err) {
|
|
3360
3482
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
3361
3483
|
}
|
|
3362
3484
|
return aborted;
|
|
3363
3485
|
}
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
this.setStatus(requestId, "paid");
|
|
3486
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
3487
|
+
resolvePaid(requestId, link) {
|
|
3488
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
3368
3489
|
}
|
|
3490
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
3369
3491
|
async revertPayable(requestId) {
|
|
3370
3492
|
await this.clearLink(requestId);
|
|
3371
3493
|
this.setStatus(requestId, "pending");
|
|
@@ -3689,11 +3811,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3689
3811
|
client: deps.client,
|
|
3690
3812
|
fieldKey: deps.fieldKey,
|
|
3691
3813
|
registry: deps.registry,
|
|
3692
|
-
emit: (event) => deps.emit(event,
|
|
3814
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
3693
3815
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
3694
3816
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
3695
3817
|
});
|
|
3696
3818
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
3819
|
+
const machineStores = createMachineStores(deps.kv);
|
|
3697
3820
|
const heldStates = /* @__PURE__ */ new Map();
|
|
3698
3821
|
return {
|
|
3699
3822
|
ownPubkeyBytes,
|
|
@@ -3701,12 +3824,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3701
3824
|
ledger,
|
|
3702
3825
|
queue,
|
|
3703
3826
|
historyStore,
|
|
3704
|
-
machineStores
|
|
3827
|
+
machineStores,
|
|
3705
3828
|
machineDeps,
|
|
3706
3829
|
machine: new TransferMachine(machineDeps),
|
|
3707
3830
|
heldStates,
|
|
3708
3831
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
3709
|
-
requests: buildRequests(deps, hooks)
|
|
3832
|
+
requests: buildRequests(deps, hooks),
|
|
3833
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3837
|
+
return {
|
|
3838
|
+
stores: machineStores,
|
|
3839
|
+
kv: deps.kv,
|
|
3840
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
3841
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
3842
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
3843
|
+
fullRePull: () => view.onEpochReset(),
|
|
3844
|
+
attention: (transferId, code, detail) => {
|
|
3845
|
+
deps.emit(
|
|
3846
|
+
"transfer:attention",
|
|
3847
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3848
|
+
);
|
|
3849
|
+
}
|
|
3710
3850
|
};
|
|
3711
3851
|
}
|
|
3712
3852
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -3728,11 +3868,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
3728
3868
|
ownPubkey: hexToBytes(deps.ownPubkey),
|
|
3729
3869
|
emit: deps.emit,
|
|
3730
3870
|
now: deps.now ?? Date.now,
|
|
3731
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
3871
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
3732
3872
|
await historyStore.recordSent({
|
|
3733
3873
|
transferId,
|
|
3734
3874
|
coinId: payload.coinId,
|
|
3735
|
-
|
|
3875
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
3876
|
+
// recipient blobs), never payload.amount — the plan.
|
|
3877
|
+
amount: committedAmount,
|
|
3736
3878
|
recipientPubkey: payload.recipient,
|
|
3737
3879
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
3738
3880
|
});
|
|
@@ -3759,7 +3901,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
3759
3901
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3760
3902
|
);
|
|
3761
3903
|
},
|
|
3762
|
-
syncEpoch: deps.syncEpoch
|
|
3904
|
+
syncEpoch: deps.syncEpoch,
|
|
3763
3905
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3764
3906
|
});
|
|
3765
3907
|
}
|
|
@@ -3812,6 +3954,7 @@ var PaymentsFacade = class {
|
|
|
3812
3954
|
this.receiveLoop = parts.receiveLoop;
|
|
3813
3955
|
this.heldStates = parts.heldStates;
|
|
3814
3956
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3957
|
+
this.restoreDeps = parts.restoreDeps;
|
|
3815
3958
|
this.requests = parts.requests;
|
|
3816
3959
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3817
3960
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -3840,6 +3983,7 @@ var PaymentsFacade = class {
|
|
|
3840
3983
|
receiveLoop;
|
|
3841
3984
|
heldStates;
|
|
3842
3985
|
ownPubkeyBytes;
|
|
3986
|
+
restoreDeps;
|
|
3843
3987
|
requests;
|
|
3844
3988
|
currentEngine = null;
|
|
3845
3989
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -3850,6 +3994,8 @@ var PaymentsFacade = class {
|
|
|
3850
3994
|
converger;
|
|
3851
3995
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
3852
3996
|
resumeFlight = new SingleFlight();
|
|
3997
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
3998
|
+
passChain = new SerialChain();
|
|
3853
3999
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3854
4000
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
3855
4001
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -3857,8 +4003,8 @@ var PaymentsFacade = class {
|
|
|
3857
4003
|
if (this.started) return;
|
|
3858
4004
|
this.started = true;
|
|
3859
4005
|
this.heartbeat.start();
|
|
3860
|
-
await this.deps.session.start();
|
|
3861
4006
|
this.unsubscribers.push(
|
|
4007
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
3862
4008
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
3863
4009
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
3864
4010
|
}),
|
|
@@ -3873,6 +4019,7 @@ var PaymentsFacade = class {
|
|
|
3873
4019
|
if (status === "connected") this.heartbeat.recovered();
|
|
3874
4020
|
});
|
|
3875
4021
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4022
|
+
await this.deps.session.start();
|
|
3876
4023
|
this.trackTail(this.runConvergencePass());
|
|
3877
4024
|
this.trackTail(this.seedHeldStates());
|
|
3878
4025
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -3913,7 +4060,7 @@ var PaymentsFacade = class {
|
|
|
3913
4060
|
}
|
|
3914
4061
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
3915
4062
|
send(request) {
|
|
3916
|
-
return this.track(this.
|
|
4063
|
+
return this.track(this.sendOutcome(request));
|
|
3917
4064
|
}
|
|
3918
4065
|
async receive() {
|
|
3919
4066
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -3928,20 +4075,27 @@ var PaymentsFacade = class {
|
|
|
3928
4075
|
if (!this.started) return;
|
|
3929
4076
|
await this.track(this.runConvergencePass());
|
|
3930
4077
|
}
|
|
4078
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
4079
|
+
async handleEpochChange(_newEpoch) {
|
|
4080
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
4081
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
4082
|
+
}
|
|
3931
4083
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
3932
4084
|
runConvergencePass() {
|
|
3933
|
-
return this.resumeFlight.run(
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
4085
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
4086
|
+
}
|
|
4087
|
+
async convergeBody() {
|
|
4088
|
+
const outcome = await this.converger.convergeOnce();
|
|
4089
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4090
|
+
this.heartbeat.settle(outcome, pending);
|
|
3938
4091
|
}
|
|
3939
4092
|
nowMs() {
|
|
3940
4093
|
return (this.deps.now ?? Date.now)();
|
|
3941
4094
|
}
|
|
3942
4095
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3943
|
-
|
|
3944
|
-
|
|
4096
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
4097
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
4098
|
+
async sendOutcome(request) {
|
|
3945
4099
|
const run = {
|
|
3946
4100
|
amount: request.amount,
|
|
3947
4101
|
delivered: [],
|
|
@@ -3949,12 +4103,36 @@ var PaymentsFacade = class {
|
|
|
3949
4103
|
tokenTransfers: [],
|
|
3950
4104
|
settledShortfalls: [],
|
|
3951
4105
|
firstPartialId: void 0,
|
|
3952
|
-
pendingAny: false
|
|
4106
|
+
pendingAny: false,
|
|
4107
|
+
lastTransferId: ""
|
|
4108
|
+
};
|
|
4109
|
+
try {
|
|
4110
|
+
return await this.sendWithPolicy(request, run);
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
4113
|
+
throw err;
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
4117
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
4118
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
4119
|
+
emitCleanFailure(err, transferId) {
|
|
4120
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
4121
|
+
const failed = {
|
|
4122
|
+
id: transferId,
|
|
4123
|
+
status: "failed",
|
|
4124
|
+
tokens: [],
|
|
4125
|
+
tokenTransfers: [],
|
|
4126
|
+
error: messageOf(err)
|
|
3953
4127
|
};
|
|
4128
|
+
this.deps.emit("transfer:updated", failed);
|
|
4129
|
+
}
|
|
4130
|
+
async sendWithPolicy(request, run) {
|
|
4131
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
3954
4132
|
for (let attempt = 0; ; attempt++) {
|
|
3955
4133
|
let ctx;
|
|
3956
4134
|
try {
|
|
3957
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
4135
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
3958
4136
|
} catch (err) {
|
|
3959
4137
|
throw this.partialize(err, run);
|
|
3960
4138
|
}
|
|
@@ -3999,11 +4177,8 @@ var PaymentsFacade = class {
|
|
|
3999
4177
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
4000
4178
|
}
|
|
4001
4179
|
}
|
|
4002
|
-
/**
|
|
4003
|
-
*
|
|
4004
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
4005
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
4006
|
-
*/
|
|
4180
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
4181
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
4007
4182
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
4008
4183
|
if (disposition.deliveryPending) {
|
|
4009
4184
|
run.pendingAny = true;
|
|
@@ -4029,11 +4204,8 @@ var PaymentsFacade = class {
|
|
|
4029
4204
|
run.amount = partial.remainingAmount;
|
|
4030
4205
|
return null;
|
|
4031
4206
|
}
|
|
4032
|
-
/**
|
|
4033
|
-
*
|
|
4034
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
4035
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
4036
|
-
*/
|
|
4207
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
4208
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
4037
4209
|
async disposeFailedAttempt(ctx, err) {
|
|
4038
4210
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
4039
4211
|
this.settleKeepOpen(ctx);
|
|
@@ -4082,8 +4254,9 @@ var PaymentsFacade = class {
|
|
|
4082
4254
|
}
|
|
4083
4255
|
return { kind: "rethrow", error: err };
|
|
4084
4256
|
}
|
|
4085
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
4257
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
4086
4258
|
const transferId = this.newId();
|
|
4259
|
+
run.lastTransferId = transferId;
|
|
4087
4260
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
4088
4261
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
4089
4262
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -4215,11 +4388,7 @@ var PaymentsFacade = class {
|
|
|
4215
4388
|
this.deps.emit("transfer:updated", result);
|
|
4216
4389
|
return result;
|
|
4217
4390
|
}
|
|
4218
|
-
/**
|
|
4219
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
4220
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
4221
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
4222
|
-
*/
|
|
4391
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
4223
4392
|
partialize(err, run) {
|
|
4224
4393
|
if (run.delivered.length === 0) return err;
|
|
4225
4394
|
return new PartialSendConflictError(
|
|
@@ -4371,22 +4540,14 @@ var PaymentsFacade = class {
|
|
|
4371
4540
|
}
|
|
4372
4541
|
}
|
|
4373
4542
|
toUiToken(tokenId, coinId, amount) {
|
|
4374
|
-
const registry = this.deps.registry;
|
|
4375
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
4376
4543
|
const now = this.nowMs();
|
|
4377
|
-
return
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
amount: amount.toString(),
|
|
4385
|
-
status: "transferring",
|
|
4386
|
-
createdAt: now,
|
|
4387
|
-
updatedAt: now,
|
|
4388
|
-
lazy: true
|
|
4389
|
-
};
|
|
4544
|
+
return toToken(
|
|
4545
|
+
tokenId,
|
|
4546
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
4547
|
+
{ coinId, amount: amount.toString() },
|
|
4548
|
+
this.deps.registry,
|
|
4549
|
+
{ transferring: true, suspectedSpent: false }
|
|
4550
|
+
);
|
|
4390
4551
|
}
|
|
4391
4552
|
track(op) {
|
|
4392
4553
|
const settled = op.then(
|
|
@@ -4401,30 +4562,10 @@ var PaymentsFacade = class {
|
|
|
4401
4562
|
this.track(op).catch(() => void 0);
|
|
4402
4563
|
}
|
|
4403
4564
|
};
|
|
4404
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
4405
|
-
return {
|
|
4406
|
-
v: 2,
|
|
4407
|
-
recipient: recipientPubkey,
|
|
4408
|
-
coinId: request.coinId,
|
|
4409
|
-
amount: request.amount,
|
|
4410
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
4411
|
-
direct: [...spend.plan.direct],
|
|
4412
|
-
...spend.plan.split !== void 0 ? {
|
|
4413
|
-
split: {
|
|
4414
|
-
tokenId: spend.plan.split.tokenId,
|
|
4415
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
4416
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
4417
|
-
}
|
|
4418
|
-
} : {},
|
|
4419
|
-
spentStates
|
|
4420
|
-
};
|
|
4421
|
-
}
|
|
4422
|
-
function messageOf(err) {
|
|
4423
|
-
return err instanceof Error ? err.message : String(err);
|
|
4424
|
-
}
|
|
4425
4565
|
// Annotate the CommonJS export names for ESM import in node:
|
|
4426
4566
|
0 && (module.exports = {
|
|
4427
4567
|
ATTENTION_MINT_UNRESOLVED,
|
|
4568
|
+
ATTENTION_RESEED_REJECTED,
|
|
4428
4569
|
HEARTBEAT_CAP_MS,
|
|
4429
4570
|
HEARTBEAT_SEED_MS,
|
|
4430
4571
|
MAX_RESELECT,
|