@unicitylabs/sphere-sdk 0.14.0-dev.6 → 0.14.0
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
|
@@ -23,7 +23,10 @@ var STORE_KEYS = {
|
|
|
23
23
|
shortfalls: "shortfalls",
|
|
24
24
|
settlingLinks: "settling",
|
|
25
25
|
streamCursor: (s) => `cursor:${s}`,
|
|
26
|
-
epochLatch: "epoch-latch"
|
|
26
|
+
epochLatch: "epoch-latch",
|
|
27
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
28
|
+
suspectedSpent: "suspected-spent",
|
|
29
|
+
knownSpends: "known-spends"
|
|
27
30
|
};
|
|
28
31
|
|
|
29
32
|
// node_modules/@noble/hashes/utils.js
|
|
@@ -815,7 +818,7 @@ var TransferMachine = class {
|
|
|
815
818
|
}
|
|
816
819
|
if (cls === null) {
|
|
817
820
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
818
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
821
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
819
822
|
return { committed, deliveryPending };
|
|
820
823
|
}
|
|
821
824
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -841,10 +844,10 @@ var TransferMachine = class {
|
|
|
841
844
|
}
|
|
842
845
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
843
846
|
if (conflicts.length > 0) {
|
|
844
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
847
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
845
848
|
}
|
|
846
849
|
await this.completeIntent(r.transferId, r.payload);
|
|
847
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
850
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
848
851
|
return { deliveryPending, partial: null };
|
|
849
852
|
}
|
|
850
853
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -1026,7 +1029,7 @@ var TransferMachine = class {
|
|
|
1026
1029
|
}
|
|
1027
1030
|
return added;
|
|
1028
1031
|
}
|
|
1029
|
-
async settlePartial(r, committed, conflicts) {
|
|
1032
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
1030
1033
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
1031
1034
|
const entry = {
|
|
1032
1035
|
transferId: r.transferId,
|
|
@@ -1038,26 +1041,27 @@ var TransferMachine = class {
|
|
|
1038
1041
|
};
|
|
1039
1042
|
await this.stores.shortfalls.upsert(entry);
|
|
1040
1043
|
await this.completeIntent(r.transferId, r.payload);
|
|
1041
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
1044
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
1042
1045
|
return entry;
|
|
1043
1046
|
}
|
|
1044
|
-
closeTail(transferId, payload) {
|
|
1047
|
+
closeTail(engine, transferId, payload, committed) {
|
|
1045
1048
|
void (async () => {
|
|
1046
1049
|
try {
|
|
1047
1050
|
await this.completeIntent(transferId, payload);
|
|
1048
1051
|
} catch {
|
|
1049
1052
|
return;
|
|
1050
1053
|
}
|
|
1051
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
1054
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
1052
1055
|
})();
|
|
1053
1056
|
}
|
|
1054
1057
|
async completeIntent(transferId, payload) {
|
|
1055
1058
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
1056
1059
|
await this.deps.intents.complete(transferId, signature);
|
|
1057
1060
|
}
|
|
1058
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
1061
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
1059
1062
|
try {
|
|
1060
|
-
await this.
|
|
1063
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
1064
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
1061
1065
|
} catch {
|
|
1062
1066
|
}
|
|
1063
1067
|
try {
|
|
@@ -1065,6 +1069,26 @@ var TransferMachine = class {
|
|
|
1065
1069
|
} catch {
|
|
1066
1070
|
}
|
|
1067
1071
|
}
|
|
1072
|
+
/**
|
|
1073
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
1074
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
1075
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
1076
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
1077
|
+
*/
|
|
1078
|
+
async settledAmount(engine, coinId, committed) {
|
|
1079
|
+
let total = 0n;
|
|
1080
|
+
for (const outcome of committed) {
|
|
1081
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
1082
|
+
const token = await engine.decodeToken({
|
|
1083
|
+
v: TOKEN_BLOB_VERSION,
|
|
1084
|
+
network: 0,
|
|
1085
|
+
tokenId: "",
|
|
1086
|
+
token: outcome.recipientBlob
|
|
1087
|
+
});
|
|
1088
|
+
total += engine.balanceOf(token, coinId);
|
|
1089
|
+
}
|
|
1090
|
+
return total.toString();
|
|
1091
|
+
}
|
|
1068
1092
|
async disposeOnFailure(transferId, committed, cls) {
|
|
1069
1093
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
1070
1094
|
try {
|
|
@@ -1089,7 +1113,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
1089
1113
|
}
|
|
1090
1114
|
|
|
1091
1115
|
// modules/payments-v2/machine/resume.ts
|
|
1092
|
-
async function resumeAll(deps) {
|
|
1116
|
+
async function resumeAll(deps, options = {}) {
|
|
1093
1117
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
1094
1118
|
const stores = createMachineStores(deps.kv);
|
|
1095
1119
|
const open = await deps.intents.listOpen();
|
|
@@ -1099,7 +1123,12 @@ async function resumeAll(deps) {
|
|
|
1099
1123
|
} catch {
|
|
1100
1124
|
return report;
|
|
1101
1125
|
}
|
|
1102
|
-
await
|
|
1126
|
+
await dropStaleBackstops(
|
|
1127
|
+
stores,
|
|
1128
|
+
locals,
|
|
1129
|
+
new Set(open.map((i) => i.transferId)),
|
|
1130
|
+
options.isLocallyActive ?? (() => false)
|
|
1131
|
+
);
|
|
1103
1132
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
1104
1133
|
const ctx = {
|
|
1105
1134
|
deps,
|
|
@@ -1112,11 +1141,16 @@ async function resumeAll(deps) {
|
|
|
1112
1141
|
}
|
|
1113
1142
|
return report;
|
|
1114
1143
|
}
|
|
1115
|
-
async function
|
|
1144
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
1116
1145
|
for (const entry of locals.values()) {
|
|
1117
|
-
if (
|
|
1146
|
+
if (openIds.has(entry.transferId)) continue;
|
|
1147
|
+
if (entry.disposition !== "open") {
|
|
1118
1148
|
await stores.backstop.removeByKey(entry.transferId);
|
|
1149
|
+
continue;
|
|
1119
1150
|
}
|
|
1151
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
1152
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
1153
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
1120
1154
|
}
|
|
1121
1155
|
}
|
|
1122
1156
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -1324,7 +1358,9 @@ var Converger = class {
|
|
|
1324
1358
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
1325
1359
|
}
|
|
1326
1360
|
async resumePass() {
|
|
1327
|
-
const report = await resumeAll(this.filteredMachineDeps()
|
|
1361
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
1362
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
1363
|
+
});
|
|
1328
1364
|
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
1329
1365
|
await this.deps.reconcile({
|
|
1330
1366
|
resumed: report.resumed,
|
|
@@ -1440,6 +1476,136 @@ function distinctOpCount(entries) {
|
|
|
1440
1476
|
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
1441
1477
|
}
|
|
1442
1478
|
|
|
1479
|
+
// modules/payments-v2/restore.ts
|
|
1480
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
1481
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
1482
|
+
async function reseedAndReset(deps) {
|
|
1483
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
1484
|
+
if (entry.disposition !== "open") continue;
|
|
1485
|
+
try {
|
|
1486
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
1487
|
+
if (entry.requiresSeedClose) {
|
|
1488
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
1489
|
+
}
|
|
1490
|
+
} catch (err) {
|
|
1491
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
1495
|
+
await deps.fullRePull();
|
|
1496
|
+
}
|
|
1497
|
+
async function splitOpIndex(deps, envelope) {
|
|
1498
|
+
const payload = await deps.decryptPayload(envelope);
|
|
1499
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
1503
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
1504
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
1505
|
+
return {
|
|
1506
|
+
id: tokenId,
|
|
1507
|
+
coinId: asset.coinId,
|
|
1508
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
1509
|
+
name: registry.getName(asset.coinId),
|
|
1510
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
1511
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1512
|
+
amount: asset.amount,
|
|
1513
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
1514
|
+
createdAt: snapshot.createdAt,
|
|
1515
|
+
updatedAt: snapshot.updatedAt,
|
|
1516
|
+
lazy: true,
|
|
1517
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
1521
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1522
|
+
for (const [tokenId, entry] of entries) {
|
|
1523
|
+
const moving = isTransferring(tokenId);
|
|
1524
|
+
for (const asset of entry.assets) {
|
|
1525
|
+
let group = groups.get(asset.coinId);
|
|
1526
|
+
if (!group) {
|
|
1527
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
1528
|
+
groups.set(asset.coinId, group);
|
|
1529
|
+
}
|
|
1530
|
+
if (moving) {
|
|
1531
|
+
group.transferring += BigInt(asset.amount);
|
|
1532
|
+
group.transferringCount += 1;
|
|
1533
|
+
} else {
|
|
1534
|
+
group.total += BigInt(asset.amount);
|
|
1535
|
+
group.count += 1;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
1540
|
+
}
|
|
1541
|
+
function toAsset(coinId, totals, registry) {
|
|
1542
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
1543
|
+
return {
|
|
1544
|
+
coinId,
|
|
1545
|
+
symbol: registry.getSymbol(coinId),
|
|
1546
|
+
name: registry.getName(coinId),
|
|
1547
|
+
decimals: registry.getDecimals(coinId),
|
|
1548
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
1549
|
+
totalAmount: totals.total.toString(),
|
|
1550
|
+
tokenCount: totals.count,
|
|
1551
|
+
confirmedAmount: totals.total.toString(),
|
|
1552
|
+
unconfirmedAmount: "0",
|
|
1553
|
+
confirmedTokenCount: totals.count,
|
|
1554
|
+
unconfirmedTokenCount: 0,
|
|
1555
|
+
transferringTokenCount: totals.transferringCount,
|
|
1556
|
+
transferringAmount: totals.transferring.toString(),
|
|
1557
|
+
priceUsd: null,
|
|
1558
|
+
priceEur: null,
|
|
1559
|
+
change24h: null,
|
|
1560
|
+
fiatValueUsd: null,
|
|
1561
|
+
fiatValueEur: null
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
async function withPrices(raw, registry, price) {
|
|
1565
|
+
try {
|
|
1566
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
1567
|
+
const quotes = await price.getPrices(names);
|
|
1568
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
1569
|
+
} catch {
|
|
1570
|
+
return raw;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
function priceOne(asset, quote) {
|
|
1574
|
+
if (!quote) return asset;
|
|
1575
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
1576
|
+
return {
|
|
1577
|
+
...asset,
|
|
1578
|
+
priceUsd: quote.priceUsd,
|
|
1579
|
+
priceEur: quote.priceEur ?? null,
|
|
1580
|
+
change24h: quote.change24h ?? null,
|
|
1581
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
1582
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
// modules/payments-v2/machine/payload.ts
|
|
1587
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
1588
|
+
return {
|
|
1589
|
+
v: 2,
|
|
1590
|
+
recipient: recipientPubkey,
|
|
1591
|
+
coinId: request.coinId,
|
|
1592
|
+
amount: request.amount,
|
|
1593
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
1594
|
+
direct: [...spend.plan.direct],
|
|
1595
|
+
...spend.plan.split !== void 0 ? {
|
|
1596
|
+
split: {
|
|
1597
|
+
tokenId: spend.plan.split.tokenId,
|
|
1598
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
1599
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
1600
|
+
}
|
|
1601
|
+
} : {},
|
|
1602
|
+
spentStates
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
function messageOf(err) {
|
|
1606
|
+
return err instanceof Error ? err.message : String(err);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1443
1609
|
// node_modules/@noble/ciphers/utils.js
|
|
1444
1610
|
function isBytes2(a) {
|
|
1445
1611
|
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
@@ -2348,13 +2514,14 @@ var History = class {
|
|
|
2348
2514
|
}
|
|
2349
2515
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
2350
2516
|
async post(build) {
|
|
2517
|
+
const record = build();
|
|
2351
2518
|
try {
|
|
2352
|
-
await this.deps.client.postHistory([
|
|
2519
|
+
await this.deps.client.postHistory([record]);
|
|
2353
2520
|
} catch (err) {
|
|
2354
2521
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
2355
2522
|
return;
|
|
2356
2523
|
}
|
|
2357
|
-
this.deps.emit("history:updated");
|
|
2524
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
2358
2525
|
}
|
|
2359
2526
|
ts(timestamp) {
|
|
2360
2527
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -2411,93 +2578,9 @@ var History = class {
|
|
|
2411
2578
|
}
|
|
2412
2579
|
};
|
|
2413
2580
|
|
|
2414
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
2415
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
2416
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
2417
|
-
return {
|
|
2418
|
-
id: tokenId,
|
|
2419
|
-
coinId: asset.coinId,
|
|
2420
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
2421
|
-
name: registry.getName(asset.coinId),
|
|
2422
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
2423
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2424
|
-
amount: asset.amount,
|
|
2425
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
2426
|
-
createdAt: snapshot.createdAt,
|
|
2427
|
-
updatedAt: snapshot.updatedAt,
|
|
2428
|
-
lazy: true,
|
|
2429
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
2433
|
-
const groups = /* @__PURE__ */ new Map();
|
|
2434
|
-
for (const [tokenId, entry] of entries) {
|
|
2435
|
-
const moving = isTransferring(tokenId);
|
|
2436
|
-
for (const asset of entry.assets) {
|
|
2437
|
-
let group = groups.get(asset.coinId);
|
|
2438
|
-
if (!group) {
|
|
2439
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2440
|
-
groups.set(asset.coinId, group);
|
|
2441
|
-
}
|
|
2442
|
-
if (moving) {
|
|
2443
|
-
group.transferring += BigInt(asset.amount);
|
|
2444
|
-
group.transferringCount += 1;
|
|
2445
|
-
} else {
|
|
2446
|
-
group.total += BigInt(asset.amount);
|
|
2447
|
-
group.count += 1;
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
}
|
|
2451
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
2452
|
-
}
|
|
2453
|
-
function toAsset(coinId, totals, registry) {
|
|
2454
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
2455
|
-
return {
|
|
2456
|
-
coinId,
|
|
2457
|
-
symbol: registry.getSymbol(coinId),
|
|
2458
|
-
name: registry.getName(coinId),
|
|
2459
|
-
decimals: registry.getDecimals(coinId),
|
|
2460
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
2461
|
-
totalAmount: totals.total.toString(),
|
|
2462
|
-
tokenCount: totals.count,
|
|
2463
|
-
confirmedAmount: totals.total.toString(),
|
|
2464
|
-
unconfirmedAmount: "0",
|
|
2465
|
-
confirmedTokenCount: totals.count,
|
|
2466
|
-
unconfirmedTokenCount: 0,
|
|
2467
|
-
transferringTokenCount: totals.transferringCount,
|
|
2468
|
-
transferringAmount: totals.transferring.toString(),
|
|
2469
|
-
priceUsd: null,
|
|
2470
|
-
priceEur: null,
|
|
2471
|
-
change24h: null,
|
|
2472
|
-
fiatValueUsd: null,
|
|
2473
|
-
fiatValueEur: null
|
|
2474
|
-
};
|
|
2475
|
-
}
|
|
2476
|
-
async function withPrices(raw, registry, price) {
|
|
2477
|
-
try {
|
|
2478
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
2479
|
-
const quotes = await price.getPrices(names);
|
|
2480
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
2481
|
-
} catch {
|
|
2482
|
-
return raw;
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
function priceOne(asset, quote) {
|
|
2486
|
-
if (!quote) return asset;
|
|
2487
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
2488
|
-
return {
|
|
2489
|
-
...asset,
|
|
2490
|
-
priceUsd: quote.priceUsd,
|
|
2491
|
-
priceEur: quote.priceEur ?? null,
|
|
2492
|
-
change24h: quote.change24h ?? null,
|
|
2493
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
2494
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
2495
|
-
};
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
2581
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2499
|
-
var SUSPECTED_KEY =
|
|
2500
|
-
var KNOWN_SPENDS_KEY =
|
|
2582
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2583
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2501
2584
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
2502
2585
|
function stateKey(tokenId, stateHash) {
|
|
2503
2586
|
return `${tokenId}:${stateHash}`;
|
|
@@ -2893,19 +2976,24 @@ var Receive = class {
|
|
|
2893
2976
|
const engine = deps.engine();
|
|
2894
2977
|
const stored = [];
|
|
2895
2978
|
const pending = [];
|
|
2896
|
-
|
|
2979
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2897
2980
|
try {
|
|
2898
|
-
epoch = deps.syncEpoch();
|
|
2899
2981
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2900
|
-
|
|
2901
|
-
for
|
|
2902
|
-
await
|
|
2903
|
-
|
|
2982
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
2983
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
2984
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
2985
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
2986
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
2987
|
+
}
|
|
2988
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
2989
|
+
const served = deps.delivery.incomingEpoch();
|
|
2990
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
2991
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
2992
|
+
basis = null;
|
|
2904
2993
|
}
|
|
2905
|
-
await flushAcks(deps, pending, epoch);
|
|
2906
2994
|
} catch (err) {
|
|
2907
2995
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
2908
|
-
await flushAcks(deps, pending,
|
|
2996
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
2909
2997
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
2910
2998
|
});
|
|
2911
2999
|
}
|
|
@@ -2978,7 +3066,7 @@ async function announce(deps, entry, record) {
|
|
|
2978
3066
|
receivedAt
|
|
2979
3067
|
};
|
|
2980
3068
|
}
|
|
2981
|
-
async function flushAcks(deps, pending,
|
|
3069
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
2982
3070
|
let lastAcked = null;
|
|
2983
3071
|
try {
|
|
2984
3072
|
while (pending.length > 0) {
|
|
@@ -2989,7 +3077,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
2989
3077
|
}
|
|
2990
3078
|
} finally {
|
|
2991
3079
|
if (lastAcked !== null) {
|
|
2992
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
3080
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
2993
3081
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
2994
3082
|
}
|
|
2995
3083
|
}
|
|
@@ -3090,9 +3178,19 @@ var Requests = class {
|
|
|
3090
3178
|
}
|
|
3091
3179
|
});
|
|
3092
3180
|
}
|
|
3181
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
3093
3182
|
writeLink(requestId, transferId, committed) {
|
|
3094
3183
|
return this.mutateJournal((m) => {
|
|
3095
|
-
|
|
3184
|
+
const prev = m.get(requestId);
|
|
3185
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
3186
|
+
const next = {
|
|
3187
|
+
requestId,
|
|
3188
|
+
transferId,
|
|
3189
|
+
committed: committed || same && prev.committed,
|
|
3190
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
3191
|
+
};
|
|
3192
|
+
if (same && prev.committed === next.committed) return false;
|
|
3193
|
+
m.set(requestId, next);
|
|
3096
3194
|
return true;
|
|
3097
3195
|
});
|
|
3098
3196
|
}
|
|
@@ -3245,23 +3343,46 @@ var Requests = class {
|
|
|
3245
3343
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
3246
3344
|
if (transferId !== void 0) {
|
|
3247
3345
|
const committed = error instanceof PartialSendConflictError;
|
|
3248
|
-
await this.
|
|
3249
|
-
this.setStatus(id, "settling");
|
|
3346
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
3250
3347
|
} else {
|
|
3251
3348
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
3252
3349
|
this.setStatus(id, "paid");
|
|
3253
3350
|
}
|
|
3254
3351
|
} else {
|
|
3255
|
-
await this.
|
|
3256
|
-
this.setStatus(id, "pending");
|
|
3352
|
+
await this.revertPayable(id);
|
|
3257
3353
|
}
|
|
3258
3354
|
throw error;
|
|
3259
3355
|
}
|
|
3260
|
-
this.
|
|
3261
|
-
await this.respondPaid(id, result.id);
|
|
3262
|
-
await this.clearLink(id);
|
|
3356
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
3263
3357
|
return result;
|
|
3264
3358
|
}
|
|
3359
|
+
/**
|
|
3360
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
3361
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
3362
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
3363
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
3364
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
3365
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
3366
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
3367
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
3368
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
3369
|
+
*/
|
|
3370
|
+
async settle(requestId, transferId, opts) {
|
|
3371
|
+
let journalDown = false;
|
|
3372
|
+
try {
|
|
3373
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
3374
|
+
} catch (err) {
|
|
3375
|
+
if (!opts.respond) throw err;
|
|
3376
|
+
journalDown = true;
|
|
3377
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
3378
|
+
}
|
|
3379
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
3380
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
3384
|
+
this.setStatus(requestId, "paid");
|
|
3385
|
+
}
|
|
3265
3386
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
3266
3387
|
async respondPaid(id, transferId) {
|
|
3267
3388
|
try {
|
|
@@ -3297,32 +3418,32 @@ var Requests = class {
|
|
|
3297
3418
|
let aborted = null;
|
|
3298
3419
|
for (const [requestId, link] of [...this.journal]) {
|
|
3299
3420
|
if (link.committed || resumed.has(link.transferId)) {
|
|
3300
|
-
await this.resolvePaid(requestId, link
|
|
3421
|
+
await this.resolvePaid(requestId, link);
|
|
3301
3422
|
} else if (conflicted.has(link.transferId)) {
|
|
3302
3423
|
await this.revertPayable(requestId);
|
|
3303
3424
|
} else if (open.has(link.transferId)) {
|
|
3304
3425
|
} else {
|
|
3305
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
3426
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
3306
3427
|
}
|
|
3307
3428
|
}
|
|
3308
3429
|
}
|
|
3309
3430
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
3310
3431
|
// a probe failure defers the link to the next session.
|
|
3311
|
-
async reconcileUnaccounted(requestId,
|
|
3432
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
3312
3433
|
try {
|
|
3313
3434
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
3314
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
3315
|
-
else await this.resolvePaid(requestId,
|
|
3435
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
3436
|
+
else await this.resolvePaid(requestId, link);
|
|
3316
3437
|
} catch (err) {
|
|
3317
3438
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
3318
3439
|
}
|
|
3319
3440
|
return aborted;
|
|
3320
3441
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
this.setStatus(requestId, "paid");
|
|
3442
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
3443
|
+
resolvePaid(requestId, link) {
|
|
3444
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
3325
3445
|
}
|
|
3446
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
3326
3447
|
async revertPayable(requestId) {
|
|
3327
3448
|
await this.clearLink(requestId);
|
|
3328
3449
|
this.setStatus(requestId, "pending");
|
|
@@ -3646,11 +3767,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3646
3767
|
client: deps.client,
|
|
3647
3768
|
fieldKey: deps.fieldKey,
|
|
3648
3769
|
registry: deps.registry,
|
|
3649
|
-
emit: (event) => deps.emit(event,
|
|
3770
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
3650
3771
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
3651
3772
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
3652
3773
|
});
|
|
3653
3774
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
3775
|
+
const machineStores = createMachineStores(deps.kv);
|
|
3654
3776
|
const heldStates = /* @__PURE__ */ new Map();
|
|
3655
3777
|
return {
|
|
3656
3778
|
ownPubkeyBytes,
|
|
@@ -3658,12 +3780,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3658
3780
|
ledger,
|
|
3659
3781
|
queue,
|
|
3660
3782
|
historyStore,
|
|
3661
|
-
machineStores
|
|
3783
|
+
machineStores,
|
|
3662
3784
|
machineDeps,
|
|
3663
3785
|
machine: new TransferMachine(machineDeps),
|
|
3664
3786
|
heldStates,
|
|
3665
3787
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
3666
|
-
requests: buildRequests(deps, hooks)
|
|
3788
|
+
requests: buildRequests(deps, hooks),
|
|
3789
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3790
|
+
};
|
|
3791
|
+
}
|
|
3792
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3793
|
+
return {
|
|
3794
|
+
stores: machineStores,
|
|
3795
|
+
kv: deps.kv,
|
|
3796
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
3797
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
3798
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
3799
|
+
fullRePull: () => view.onEpochReset(),
|
|
3800
|
+
attention: (transferId, code, detail) => {
|
|
3801
|
+
deps.emit(
|
|
3802
|
+
"transfer:attention",
|
|
3803
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3667
3806
|
};
|
|
3668
3807
|
}
|
|
3669
3808
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -3685,11 +3824,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
3685
3824
|
ownPubkey: hexToBytes(deps.ownPubkey),
|
|
3686
3825
|
emit: deps.emit,
|
|
3687
3826
|
now: deps.now ?? Date.now,
|
|
3688
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
3827
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
3689
3828
|
await historyStore.recordSent({
|
|
3690
3829
|
transferId,
|
|
3691
3830
|
coinId: payload.coinId,
|
|
3692
|
-
|
|
3831
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
3832
|
+
// recipient blobs), never payload.amount — the plan.
|
|
3833
|
+
amount: committedAmount,
|
|
3693
3834
|
recipientPubkey: payload.recipient,
|
|
3694
3835
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
3695
3836
|
});
|
|
@@ -3716,7 +3857,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
3716
3857
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
3717
3858
|
);
|
|
3718
3859
|
},
|
|
3719
|
-
syncEpoch: deps.syncEpoch
|
|
3860
|
+
syncEpoch: deps.syncEpoch,
|
|
3720
3861
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3721
3862
|
});
|
|
3722
3863
|
}
|
|
@@ -3769,6 +3910,7 @@ var PaymentsFacade = class {
|
|
|
3769
3910
|
this.receiveLoop = parts.receiveLoop;
|
|
3770
3911
|
this.heldStates = parts.heldStates;
|
|
3771
3912
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3913
|
+
this.restoreDeps = parts.restoreDeps;
|
|
3772
3914
|
this.requests = parts.requests;
|
|
3773
3915
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3774
3916
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
@@ -3797,6 +3939,7 @@ var PaymentsFacade = class {
|
|
|
3797
3939
|
receiveLoop;
|
|
3798
3940
|
heldStates;
|
|
3799
3941
|
ownPubkeyBytes;
|
|
3942
|
+
restoreDeps;
|
|
3800
3943
|
requests;
|
|
3801
3944
|
currentEngine = null;
|
|
3802
3945
|
pendingOps = /* @__PURE__ */ new Set();
|
|
@@ -3807,6 +3950,8 @@ var PaymentsFacade = class {
|
|
|
3807
3950
|
converger;
|
|
3808
3951
|
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
3809
3952
|
resumeFlight = new SingleFlight();
|
|
3953
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
3954
|
+
passChain = new SerialChain();
|
|
3810
3955
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3811
3956
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
3812
3957
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -3814,8 +3959,8 @@ var PaymentsFacade = class {
|
|
|
3814
3959
|
if (this.started) return;
|
|
3815
3960
|
this.started = true;
|
|
3816
3961
|
this.heartbeat.start();
|
|
3817
|
-
await this.deps.session.start();
|
|
3818
3962
|
this.unsubscribers.push(
|
|
3963
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
3819
3964
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
3820
3965
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
3821
3966
|
}),
|
|
@@ -3830,6 +3975,7 @@ var PaymentsFacade = class {
|
|
|
3830
3975
|
if (status === "connected") this.heartbeat.recovered();
|
|
3831
3976
|
});
|
|
3832
3977
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
3978
|
+
await this.deps.session.start();
|
|
3833
3979
|
this.trackTail(this.runConvergencePass());
|
|
3834
3980
|
this.trackTail(this.seedHeldStates());
|
|
3835
3981
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -3870,7 +4016,7 @@ var PaymentsFacade = class {
|
|
|
3870
4016
|
}
|
|
3871
4017
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
3872
4018
|
send(request) {
|
|
3873
|
-
return this.track(this.
|
|
4019
|
+
return this.track(this.sendOutcome(request));
|
|
3874
4020
|
}
|
|
3875
4021
|
async receive() {
|
|
3876
4022
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -3885,20 +4031,27 @@ var PaymentsFacade = class {
|
|
|
3885
4031
|
if (!this.started) return;
|
|
3886
4032
|
await this.track(this.runConvergencePass());
|
|
3887
4033
|
}
|
|
4034
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
4035
|
+
async handleEpochChange(_newEpoch) {
|
|
4036
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
4037
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
4038
|
+
}
|
|
3888
4039
|
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
3889
4040
|
runConvergencePass() {
|
|
3890
|
-
return this.resumeFlight.run(
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4041
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
4042
|
+
}
|
|
4043
|
+
async convergeBody() {
|
|
4044
|
+
const outcome = await this.converger.convergeOnce();
|
|
4045
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4046
|
+
this.heartbeat.settle(outcome, pending);
|
|
3895
4047
|
}
|
|
3896
4048
|
nowMs() {
|
|
3897
4049
|
return (this.deps.now ?? Date.now)();
|
|
3898
4050
|
}
|
|
3899
4051
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
3900
|
-
|
|
3901
|
-
|
|
4052
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
4053
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
4054
|
+
async sendOutcome(request) {
|
|
3902
4055
|
const run = {
|
|
3903
4056
|
amount: request.amount,
|
|
3904
4057
|
delivered: [],
|
|
@@ -3906,12 +4059,36 @@ var PaymentsFacade = class {
|
|
|
3906
4059
|
tokenTransfers: [],
|
|
3907
4060
|
settledShortfalls: [],
|
|
3908
4061
|
firstPartialId: void 0,
|
|
3909
|
-
pendingAny: false
|
|
4062
|
+
pendingAny: false,
|
|
4063
|
+
lastTransferId: ""
|
|
4064
|
+
};
|
|
4065
|
+
try {
|
|
4066
|
+
return await this.sendWithPolicy(request, run);
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
4069
|
+
throw err;
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
4073
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
4074
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
4075
|
+
emitCleanFailure(err, transferId) {
|
|
4076
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
4077
|
+
const failed = {
|
|
4078
|
+
id: transferId,
|
|
4079
|
+
status: "failed",
|
|
4080
|
+
tokens: [],
|
|
4081
|
+
tokenTransfers: [],
|
|
4082
|
+
error: messageOf(err)
|
|
3910
4083
|
};
|
|
4084
|
+
this.deps.emit("transfer:updated", failed);
|
|
4085
|
+
}
|
|
4086
|
+
async sendWithPolicy(request, run) {
|
|
4087
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
3911
4088
|
for (let attempt = 0; ; attempt++) {
|
|
3912
4089
|
let ctx;
|
|
3913
4090
|
try {
|
|
3914
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
4091
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
3915
4092
|
} catch (err) {
|
|
3916
4093
|
throw this.partialize(err, run);
|
|
3917
4094
|
}
|
|
@@ -3956,11 +4133,8 @@ var PaymentsFacade = class {
|
|
|
3956
4133
|
this.activeMoneyOps.delete(ctx.transferId);
|
|
3957
4134
|
}
|
|
3958
4135
|
}
|
|
3959
|
-
/**
|
|
3960
|
-
*
|
|
3961
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
3962
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
3963
|
-
*/
|
|
4136
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
4137
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
3964
4138
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
3965
4139
|
if (disposition.deliveryPending) {
|
|
3966
4140
|
run.pendingAny = true;
|
|
@@ -3986,11 +4160,8 @@ var PaymentsFacade = class {
|
|
|
3986
4160
|
run.amount = partial.remainingAmount;
|
|
3987
4161
|
return null;
|
|
3988
4162
|
}
|
|
3989
|
-
/**
|
|
3990
|
-
*
|
|
3991
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
3992
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
3993
|
-
*/
|
|
4163
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
4164
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
3994
4165
|
async disposeFailedAttempt(ctx, err) {
|
|
3995
4166
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
3996
4167
|
this.settleKeepOpen(ctx);
|
|
@@ -4039,8 +4210,9 @@ var PaymentsFacade = class {
|
|
|
4039
4210
|
}
|
|
4040
4211
|
return { kind: "rethrow", error: err };
|
|
4041
4212
|
}
|
|
4042
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
4213
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
4043
4214
|
const transferId = this.newId();
|
|
4215
|
+
run.lastTransferId = transferId;
|
|
4044
4216
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
4045
4217
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
4046
4218
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -4172,11 +4344,7 @@ var PaymentsFacade = class {
|
|
|
4172
4344
|
this.deps.emit("transfer:updated", result);
|
|
4173
4345
|
return result;
|
|
4174
4346
|
}
|
|
4175
|
-
/**
|
|
4176
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
4177
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
4178
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
4179
|
-
*/
|
|
4347
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
4180
4348
|
partialize(err, run) {
|
|
4181
4349
|
if (run.delivered.length === 0) return err;
|
|
4182
4350
|
return new PartialSendConflictError(
|
|
@@ -4328,22 +4496,14 @@ var PaymentsFacade = class {
|
|
|
4328
4496
|
}
|
|
4329
4497
|
}
|
|
4330
4498
|
toUiToken(tokenId, coinId, amount) {
|
|
4331
|
-
const registry = this.deps.registry;
|
|
4332
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
4333
4499
|
const now = this.nowMs();
|
|
4334
|
-
return
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
amount: amount.toString(),
|
|
4342
|
-
status: "transferring",
|
|
4343
|
-
createdAt: now,
|
|
4344
|
-
updatedAt: now,
|
|
4345
|
-
lazy: true
|
|
4346
|
-
};
|
|
4500
|
+
return toToken(
|
|
4501
|
+
tokenId,
|
|
4502
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
4503
|
+
{ coinId, amount: amount.toString() },
|
|
4504
|
+
this.deps.registry,
|
|
4505
|
+
{ transferring: true, suspectedSpent: false }
|
|
4506
|
+
);
|
|
4347
4507
|
}
|
|
4348
4508
|
track(op) {
|
|
4349
4509
|
const settled = op.then(
|
|
@@ -4358,29 +4518,9 @@ var PaymentsFacade = class {
|
|
|
4358
4518
|
this.track(op).catch(() => void 0);
|
|
4359
4519
|
}
|
|
4360
4520
|
};
|
|
4361
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
4362
|
-
return {
|
|
4363
|
-
v: 2,
|
|
4364
|
-
recipient: recipientPubkey,
|
|
4365
|
-
coinId: request.coinId,
|
|
4366
|
-
amount: request.amount,
|
|
4367
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
4368
|
-
direct: [...spend.plan.direct],
|
|
4369
|
-
...spend.plan.split !== void 0 ? {
|
|
4370
|
-
split: {
|
|
4371
|
-
tokenId: spend.plan.split.tokenId,
|
|
4372
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
4373
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
4374
|
-
}
|
|
4375
|
-
} : {},
|
|
4376
|
-
spentStates
|
|
4377
|
-
};
|
|
4378
|
-
}
|
|
4379
|
-
function messageOf(err) {
|
|
4380
|
-
return err instanceof Error ? err.message : String(err);
|
|
4381
|
-
}
|
|
4382
4521
|
export {
|
|
4383
4522
|
ATTENTION_MINT_UNRESOLVED,
|
|
4523
|
+
ATTENTION_RESEED_REJECTED,
|
|
4384
4524
|
HEARTBEAT_CAP_MS,
|
|
4385
4525
|
HEARTBEAT_SEED_MS,
|
|
4386
4526
|
MAX_RESELECT,
|