@unicitylabs/sphere-sdk 0.14.0-dev.5 → 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 +713 -252
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +111 -87
- package/dist/core/index.d.ts +111 -87
- package/dist/core/index.js +713 -252
- 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 +45 -5
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +26 -2
- package/dist/impl/wallet-api-v2/index.d.ts +26 -2
- package/dist/impl/wallet-api-v2/index.js +45 -5
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +713 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +112 -88
- package/dist/index.d.ts +112 -88
- package/dist/index.js +713 -252
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +634 -228
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +112 -20
- package/dist/modules/payments-v2/index.d.ts +112 -20
- package/dist/modules/payments-v2/index.js +631 -228
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/package.json +1 -1
package/dist/core/index.js
CHANGED
|
@@ -10584,14 +10584,14 @@ var IntentResume = class {
|
|
|
10584
10584
|
}
|
|
10585
10585
|
let changeOutput = null;
|
|
10586
10586
|
if (payload.split) {
|
|
10587
|
-
const
|
|
10587
|
+
const splitOpIndex2 = payload.direct.length;
|
|
10588
10588
|
let splitDelivered = false;
|
|
10589
10589
|
{
|
|
10590
10590
|
const source = await engine.decodeToken(await provider.getToken(payload.split.tokenId));
|
|
10591
10591
|
const outcome = await this.host.certifyOperation(
|
|
10592
10592
|
{
|
|
10593
10593
|
kind: "split",
|
|
10594
|
-
opIndex:
|
|
10594
|
+
opIndex: splitOpIndex2,
|
|
10595
10595
|
uiTokenId: `v2_${payload.split.tokenId}`,
|
|
10596
10596
|
genesisId: payload.split.tokenId,
|
|
10597
10597
|
sourceSdkData: "",
|
|
@@ -20699,6 +20699,7 @@ var AccountingModule = class _AccountingModule {
|
|
|
20699
20699
|
const unsubHistory = deps.on(
|
|
20700
20700
|
"history:updated",
|
|
20701
20701
|
(entry) => {
|
|
20702
|
+
if (!("dedupKey" in entry)) return;
|
|
20702
20703
|
this._handleHistoryUpdated(entry).catch((err) => {
|
|
20703
20704
|
logger.warn(LOG_TAG2, "Error handling history:updated event:", err);
|
|
20704
20705
|
});
|
|
@@ -25968,7 +25969,10 @@ var STORE_KEYS = {
|
|
|
25968
25969
|
shortfalls: "shortfalls",
|
|
25969
25970
|
settlingLinks: "settling",
|
|
25970
25971
|
streamCursor: (s) => `cursor:${s}`,
|
|
25971
|
-
epochLatch: "epoch-latch"
|
|
25972
|
+
epochLatch: "epoch-latch",
|
|
25973
|
+
// §5.2 InventoryView durable overlays (#625/#679).
|
|
25974
|
+
suspectedSpent: "suspected-spent",
|
|
25975
|
+
knownSpends: "known-spends"
|
|
25972
25976
|
};
|
|
25973
25977
|
|
|
25974
25978
|
// modules/payments-v2/machine/journal.ts
|
|
@@ -25991,6 +25995,10 @@ function isValidationReject(err) {
|
|
|
25991
25995
|
if (e === null) return false;
|
|
25992
25996
|
return e.status === 422 || e.statusCode === 422 || e.code === "VALIDATION" || e.code === "VALIDATION_FAILED";
|
|
25993
25997
|
}
|
|
25998
|
+
function retryAfterMsOf(err) {
|
|
25999
|
+
const retryAfter = err !== null && typeof err === "object" ? err.retryAfter : void 0;
|
|
26000
|
+
return typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter >= 0 ? retryAfter : null;
|
|
26001
|
+
}
|
|
25994
26002
|
var chainRegistry = /* @__PURE__ */ new WeakMap();
|
|
25995
26003
|
var replayRegistry = /* @__PURE__ */ new WeakMap();
|
|
25996
26004
|
function registryFor(registry, kv, storeKey, make) {
|
|
@@ -26214,7 +26222,7 @@ var TransferMachine = class {
|
|
|
26214
26222
|
}
|
|
26215
26223
|
if (cls === null) {
|
|
26216
26224
|
await this.applyCommitted(engine, plan.transferId, committed);
|
|
26217
|
-
this.closeTail(plan.transferId, plan.payload);
|
|
26225
|
+
this.closeTail(engine, plan.transferId, plan.payload, committed);
|
|
26218
26226
|
return { committed, deliveryPending };
|
|
26219
26227
|
}
|
|
26220
26228
|
if (cls === "conflict" && committed.length > 0) {
|
|
@@ -26240,10 +26248,10 @@ var TransferMachine = class {
|
|
|
26240
26248
|
}
|
|
26241
26249
|
await this.applyCommitted(engine, r.transferId, committed);
|
|
26242
26250
|
if (conflicts.length > 0) {
|
|
26243
|
-
return { deliveryPending, partial: await this.settlePartial(r, committed, conflicts) };
|
|
26251
|
+
return { deliveryPending, partial: await this.settlePartial(engine, r, committed, conflicts) };
|
|
26244
26252
|
}
|
|
26245
26253
|
await this.completeIntent(r.transferId, r.payload);
|
|
26246
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26254
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26247
26255
|
return { deliveryPending, partial: null };
|
|
26248
26256
|
}
|
|
26249
26257
|
// ── shared transitions ─────────────────────────────────────────────────────
|
|
@@ -26425,7 +26433,7 @@ var TransferMachine = class {
|
|
|
26425
26433
|
}
|
|
26426
26434
|
return added;
|
|
26427
26435
|
}
|
|
26428
|
-
async settlePartial(r, committed, conflicts) {
|
|
26436
|
+
async settlePartial(engine, r, committed, conflicts) {
|
|
26429
26437
|
const undelivered = conflicts.reduce((sum, c) => sum + c.amount, 0n);
|
|
26430
26438
|
const entry = {
|
|
26431
26439
|
transferId: r.transferId,
|
|
@@ -26437,26 +26445,27 @@ var TransferMachine = class {
|
|
|
26437
26445
|
};
|
|
26438
26446
|
await this.stores.shortfalls.upsert(entry);
|
|
26439
26447
|
await this.completeIntent(r.transferId, r.payload);
|
|
26440
|
-
await this.finishBookkeeping(r.transferId, r.payload, "resumed");
|
|
26448
|
+
await this.finishBookkeeping(engine, r.transferId, r.payload, "resumed", committed);
|
|
26441
26449
|
return entry;
|
|
26442
26450
|
}
|
|
26443
|
-
closeTail(transferId, payload) {
|
|
26451
|
+
closeTail(engine, transferId, payload, committed) {
|
|
26444
26452
|
void (async () => {
|
|
26445
26453
|
try {
|
|
26446
26454
|
await this.completeIntent(transferId, payload);
|
|
26447
26455
|
} catch {
|
|
26448
26456
|
return;
|
|
26449
26457
|
}
|
|
26450
|
-
await this.finishBookkeeping(transferId, payload, "sent");
|
|
26458
|
+
await this.finishBookkeeping(engine, transferId, payload, "sent", committed);
|
|
26451
26459
|
})();
|
|
26452
26460
|
}
|
|
26453
26461
|
async completeIntent(transferId, payload) {
|
|
26454
26462
|
const signature = payload.split !== void 0 ? await this.deps.signComplete(transferId) : void 0;
|
|
26455
26463
|
await this.deps.intents.complete(transferId, signature);
|
|
26456
26464
|
}
|
|
26457
|
-
async finishBookkeeping(transferId, payload, phase) {
|
|
26465
|
+
async finishBookkeeping(engine, transferId, payload, phase, committed) {
|
|
26458
26466
|
try {
|
|
26459
|
-
await this.
|
|
26467
|
+
const committedAmount = await this.settledAmount(engine, payload.coinId, committed);
|
|
26468
|
+
await this.deps.recordHistory?.({ transferId, payload, phase, committedAmount });
|
|
26460
26469
|
} catch {
|
|
26461
26470
|
}
|
|
26462
26471
|
try {
|
|
@@ -26464,6 +26473,26 @@ var TransferMachine = class {
|
|
|
26464
26473
|
} catch {
|
|
26465
26474
|
}
|
|
26466
26475
|
}
|
|
26476
|
+
/**
|
|
26477
|
+
* §5.9: SENT history records what SETTLED, never the plan — the value of the
|
|
26478
|
+
* certified recipient blobs (summarize()'s committed set is the only source;
|
|
26479
|
+
* a journal-rehydrated resume leg has no source token, but every committed
|
|
26480
|
+
* leg carries its recipient blob, so the outcome-derived rule is uniform).
|
|
26481
|
+
*/
|
|
26482
|
+
async settledAmount(engine, coinId, committed) {
|
|
26483
|
+
let total = 0n;
|
|
26484
|
+
for (const outcome of committed) {
|
|
26485
|
+
if (outcome.recipientBlob === void 0) continue;
|
|
26486
|
+
const token = await engine.decodeToken({
|
|
26487
|
+
v: TOKEN_BLOB_VERSION,
|
|
26488
|
+
network: 0,
|
|
26489
|
+
tokenId: "",
|
|
26490
|
+
token: outcome.recipientBlob
|
|
26491
|
+
});
|
|
26492
|
+
total += engine.balanceOf(token, coinId);
|
|
26493
|
+
}
|
|
26494
|
+
return total.toString();
|
|
26495
|
+
}
|
|
26467
26496
|
async disposeOnFailure(transferId, committed, cls) {
|
|
26468
26497
|
if (committed.length > 0 || cls === "keep-open") return;
|
|
26469
26498
|
try {
|
|
@@ -26488,7 +26517,7 @@ function conflictAmount(engine, payload, op, source) {
|
|
|
26488
26517
|
}
|
|
26489
26518
|
|
|
26490
26519
|
// modules/payments-v2/machine/resume.ts
|
|
26491
|
-
async function resumeAll(deps) {
|
|
26520
|
+
async function resumeAll(deps, options = {}) {
|
|
26492
26521
|
const report = { resumed: [], conflicted: [], failed: [] };
|
|
26493
26522
|
const stores = createMachineStores(deps.kv);
|
|
26494
26523
|
const open = await deps.intents.listOpen();
|
|
@@ -26498,7 +26527,12 @@ async function resumeAll(deps) {
|
|
|
26498
26527
|
} catch {
|
|
26499
26528
|
return report;
|
|
26500
26529
|
}
|
|
26501
|
-
await
|
|
26530
|
+
await dropStaleBackstops(
|
|
26531
|
+
stores,
|
|
26532
|
+
locals,
|
|
26533
|
+
new Set(open.map((i) => i.transferId)),
|
|
26534
|
+
options.isLocallyActive ?? (() => false)
|
|
26535
|
+
);
|
|
26502
26536
|
const jobs = await gateAndDecode(deps, stores, open, locals, report);
|
|
26503
26537
|
const ctx = {
|
|
26504
26538
|
deps,
|
|
@@ -26511,11 +26545,16 @@ async function resumeAll(deps) {
|
|
|
26511
26545
|
}
|
|
26512
26546
|
return report;
|
|
26513
26547
|
}
|
|
26514
|
-
async function
|
|
26548
|
+
async function dropStaleBackstops(stores, locals, openIds, isLocallyActive) {
|
|
26515
26549
|
for (const entry of locals.values()) {
|
|
26516
|
-
if (
|
|
26550
|
+
if (openIds.has(entry.transferId)) continue;
|
|
26551
|
+
if (entry.disposition !== "open") {
|
|
26517
26552
|
await stores.backstop.removeByKey(entry.transferId);
|
|
26553
|
+
continue;
|
|
26518
26554
|
}
|
|
26555
|
+
if (isLocallyActive(entry.transferId)) continue;
|
|
26556
|
+
if ((await stores.deliveryJournal.byTransfer(entry.transferId)).size > 0) continue;
|
|
26557
|
+
await stores.backstop.removeByKey(entry.transferId);
|
|
26519
26558
|
}
|
|
26520
26559
|
}
|
|
26521
26560
|
async function gateAndDecode(deps, stores, open, locals, report) {
|
|
@@ -26628,6 +26667,349 @@ function validatePayload(raw) {
|
|
|
26628
26667
|
return p;
|
|
26629
26668
|
}
|
|
26630
26669
|
|
|
26670
|
+
// modules/payments-v2/convergence.ts
|
|
26671
|
+
var HEARTBEAT_SEED_MS = 5e3;
|
|
26672
|
+
var HEARTBEAT_CAP_MS = 12e4;
|
|
26673
|
+
var ConvergenceHeartbeat = class {
|
|
26674
|
+
constructor(deps) {
|
|
26675
|
+
this.deps = deps;
|
|
26676
|
+
}
|
|
26677
|
+
timer = null;
|
|
26678
|
+
delayMs = HEARTBEAT_SEED_MS;
|
|
26679
|
+
notBefore = 0;
|
|
26680
|
+
running = false;
|
|
26681
|
+
start() {
|
|
26682
|
+
this.running = true;
|
|
26683
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26684
|
+
this.notBefore = 0;
|
|
26685
|
+
}
|
|
26686
|
+
/** §7 quiescence unchanged: a mid-backoff stop() cancels cleanly. */
|
|
26687
|
+
stop() {
|
|
26688
|
+
this.running = false;
|
|
26689
|
+
this.clearTimer();
|
|
26690
|
+
}
|
|
26691
|
+
/** Reset to seed on progress, ×2 to the cap otherwise; idle stops rescheduling. */
|
|
26692
|
+
settle(outcome, pending) {
|
|
26693
|
+
if (outcome.retryAfterMs !== null) {
|
|
26694
|
+
this.notBefore = Math.max(this.notBefore, this.deps.now() + outcome.retryAfterMs);
|
|
26695
|
+
}
|
|
26696
|
+
if (!pending) {
|
|
26697
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26698
|
+
return;
|
|
26699
|
+
}
|
|
26700
|
+
const delay = outcome.progress ? HEARTBEAT_SEED_MS : this.delayMs;
|
|
26701
|
+
this.delayMs = Math.min(delay * 2, HEARTBEAT_CAP_MS);
|
|
26702
|
+
this.schedule(delay);
|
|
26703
|
+
}
|
|
26704
|
+
/** New pending work (keep-open park / journaled delivery / mint hold): prompt retry. */
|
|
26705
|
+
arm() {
|
|
26706
|
+
if (!this.running) return;
|
|
26707
|
+
this.delayMs = Math.min(HEARTBEAT_SEED_MS * 2, HEARTBEAT_CAP_MS);
|
|
26708
|
+
this.schedule(HEARTBEAT_SEED_MS);
|
|
26709
|
+
}
|
|
26710
|
+
/** connection:status recovery: the backoff resets; a pending tick re-fires at the seed. */
|
|
26711
|
+
recovered() {
|
|
26712
|
+
this.delayMs = HEARTBEAT_SEED_MS;
|
|
26713
|
+
if (this.timer !== null) this.arm();
|
|
26714
|
+
}
|
|
26715
|
+
schedule(delayMs) {
|
|
26716
|
+
this.clearTimer();
|
|
26717
|
+
if (!this.running) return;
|
|
26718
|
+
const floored = Math.max(delayMs, this.notBefore - this.deps.now());
|
|
26719
|
+
this.timer = setTimeout(() => {
|
|
26720
|
+
this.timer = null;
|
|
26721
|
+
this.deps.onTick();
|
|
26722
|
+
}, floored);
|
|
26723
|
+
}
|
|
26724
|
+
clearTimer() {
|
|
26725
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
26726
|
+
this.timer = null;
|
|
26727
|
+
}
|
|
26728
|
+
};
|
|
26729
|
+
var Converger = class {
|
|
26730
|
+
constructor(deps) {
|
|
26731
|
+
this.deps = deps;
|
|
26732
|
+
}
|
|
26733
|
+
/** Resume open intents, then drain the delivery + mint journals (all idempotent). */
|
|
26734
|
+
async convergeOnce() {
|
|
26735
|
+
let progress = false;
|
|
26736
|
+
let retryAfterMs = null;
|
|
26737
|
+
try {
|
|
26738
|
+
progress = await this.resumePass();
|
|
26739
|
+
} catch (err) {
|
|
26740
|
+
retryAfterMs = retryAfterMsOf(err);
|
|
26741
|
+
}
|
|
26742
|
+
try {
|
|
26743
|
+
const stats = await this.deps.stores.deliveryJournal.replay(this.replayDeps());
|
|
26744
|
+
progress = progress || stats.delivered > 0;
|
|
26745
|
+
} catch (err) {
|
|
26746
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
26747
|
+
}
|
|
26748
|
+
try {
|
|
26749
|
+
progress = await this.deps.replayMints() > 0 || progress;
|
|
26750
|
+
} catch (err) {
|
|
26751
|
+
retryAfterMs ??= retryAfterMsOf(err);
|
|
26752
|
+
}
|
|
26753
|
+
return { progress, retryAfterMs };
|
|
26754
|
+
}
|
|
26755
|
+
/** §4 heartbeat predicate — read fresh from the §6 stores, never cached. */
|
|
26756
|
+
async pendingWork() {
|
|
26757
|
+
const [backstop, journal, mints] = await Promise.all([
|
|
26758
|
+
this.deps.stores.backstop.list(),
|
|
26759
|
+
this.deps.stores.deliveryJournal.list(),
|
|
26760
|
+
this.deps.stores.mintJournal.list()
|
|
26761
|
+
]);
|
|
26762
|
+
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mints.length > 0;
|
|
26763
|
+
}
|
|
26764
|
+
async resumePass() {
|
|
26765
|
+
const report = await resumeAll(this.filteredMachineDeps(), {
|
|
26766
|
+
isLocallyActive: (id) => this.deps.isActiveOp(id)
|
|
26767
|
+
});
|
|
26768
|
+
const progress = report.resumed.length > 0 || report.conflicted.length > 0;
|
|
26769
|
+
await this.deps.reconcile({
|
|
26770
|
+
resumed: report.resumed,
|
|
26771
|
+
conflicted: report.conflicted,
|
|
26772
|
+
open: report.failed
|
|
26773
|
+
});
|
|
26774
|
+
if (progress) await this.deps.refreshView().catch(() => void 0);
|
|
26775
|
+
return progress;
|
|
26776
|
+
}
|
|
26777
|
+
filteredMachineDeps() {
|
|
26778
|
+
const intents = this.deps.machineDeps.intents;
|
|
26779
|
+
return {
|
|
26780
|
+
...this.deps.machineDeps,
|
|
26781
|
+
intents: {
|
|
26782
|
+
...intents,
|
|
26783
|
+
listOpen: async () => (await intents.listOpen()).filter((intent) => !this.deps.isActiveOp(intent.transferId))
|
|
26784
|
+
}
|
|
26785
|
+
};
|
|
26786
|
+
}
|
|
26787
|
+
replayDeps() {
|
|
26788
|
+
return {
|
|
26789
|
+
delivery: this.deps.delivery,
|
|
26790
|
+
now: () => this.deps.now(),
|
|
26791
|
+
attention: (transferId, code, detail) => {
|
|
26792
|
+
this.deps.emit(
|
|
26793
|
+
"transfer:attention",
|
|
26794
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
26795
|
+
);
|
|
26796
|
+
}
|
|
26797
|
+
};
|
|
26798
|
+
}
|
|
26799
|
+
};
|
|
26800
|
+
async function derivePendingTransfers(stores, decryptPayload) {
|
|
26801
|
+
const [backstop, journal, shortfalls] = await Promise.all([
|
|
26802
|
+
stores.backstop.list(),
|
|
26803
|
+
stores.deliveryJournal.list(),
|
|
26804
|
+
stores.shortfalls.list()
|
|
26805
|
+
]);
|
|
26806
|
+
const byTransfer = groupJournal(journal);
|
|
26807
|
+
const out = [];
|
|
26808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
26809
|
+
for (const entry of backstop) {
|
|
26810
|
+
if (entry.disposition !== "open") continue;
|
|
26811
|
+
seen.add(entry.transferId);
|
|
26812
|
+
out.push(await openRow(entry, byTransfer.get(entry.transferId) ?? [], decryptPayload));
|
|
26813
|
+
}
|
|
26814
|
+
for (const [transferId, entries] of byTransfer) {
|
|
26815
|
+
if (seen.has(transferId)) continue;
|
|
26816
|
+
seen.add(transferId);
|
|
26817
|
+
out.push(journalOnlyRow(transferId, entries));
|
|
26818
|
+
}
|
|
26819
|
+
for (const entry of shortfalls) {
|
|
26820
|
+
if (!seen.has(entry.transferId)) out.push(shortfallRow(entry));
|
|
26821
|
+
}
|
|
26822
|
+
return out;
|
|
26823
|
+
}
|
|
26824
|
+
function groupJournal(journal) {
|
|
26825
|
+
const byTransfer = /* @__PURE__ */ new Map();
|
|
26826
|
+
for (const entry of journal) {
|
|
26827
|
+
const mine = byTransfer.get(entry.transferId);
|
|
26828
|
+
if (mine === void 0) byTransfer.set(entry.transferId, [entry]);
|
|
26829
|
+
else mine.push(entry);
|
|
26830
|
+
}
|
|
26831
|
+
return byTransfer;
|
|
26832
|
+
}
|
|
26833
|
+
async function openRow(entry, journal, decryptPayload) {
|
|
26834
|
+
let payload = null;
|
|
26835
|
+
try {
|
|
26836
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
26837
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
26838
|
+
} catch {
|
|
26839
|
+
payload = null;
|
|
26840
|
+
}
|
|
26841
|
+
const total = Array.isArray(payload?.direct) ? payload.direct.length + (payload.split !== void 0 ? 1 : 0) : 0;
|
|
26842
|
+
const certified = distinctOpCount(journal);
|
|
26843
|
+
return {
|
|
26844
|
+
transferId: entry.transferId,
|
|
26845
|
+
kind: "open",
|
|
26846
|
+
recipient: typeof payload?.recipient === "string" ? payload.recipient : "",
|
|
26847
|
+
coinId: typeof payload?.coinId === "string" ? payload.coinId : "",
|
|
26848
|
+
amount: typeof payload?.amount === "string" ? payload.amount : "",
|
|
26849
|
+
legs: { certified, total: Math.max(total, certified) },
|
|
26850
|
+
deliveryPending: journal.length > 0,
|
|
26851
|
+
createdAt: entry.createdAt
|
|
26852
|
+
};
|
|
26853
|
+
}
|
|
26854
|
+
function journalOnlyRow(transferId, entries) {
|
|
26855
|
+
const certified = distinctOpCount(entries);
|
|
26856
|
+
return {
|
|
26857
|
+
transferId,
|
|
26858
|
+
kind: "open",
|
|
26859
|
+
recipient: entries[0].recipientPubkey,
|
|
26860
|
+
coinId: "",
|
|
26861
|
+
amount: "",
|
|
26862
|
+
legs: { certified, total: certified },
|
|
26863
|
+
deliveryPending: true,
|
|
26864
|
+
createdAt: 0
|
|
26865
|
+
};
|
|
26866
|
+
}
|
|
26867
|
+
function shortfallRow(entry) {
|
|
26868
|
+
return {
|
|
26869
|
+
transferId: entry.transferId,
|
|
26870
|
+
kind: "shortfall",
|
|
26871
|
+
recipient: entry.recipient,
|
|
26872
|
+
coinId: entry.coinId,
|
|
26873
|
+
amount: entry.remainingAmount,
|
|
26874
|
+
legs: { certified: entry.committedTokenIds.length, total: entry.committedTokenIds.length },
|
|
26875
|
+
deliveryPending: false,
|
|
26876
|
+
createdAt: entry.createdAt
|
|
26877
|
+
};
|
|
26878
|
+
}
|
|
26879
|
+
function distinctOpCount(entries) {
|
|
26880
|
+
return new Set(entries.map((entry) => entry.opIndex)).size;
|
|
26881
|
+
}
|
|
26882
|
+
|
|
26883
|
+
// modules/payments-v2/restore.ts
|
|
26884
|
+
var ATTENTION_RESEED_REJECTED = "intent:reseed-rejected";
|
|
26885
|
+
var ALL_STREAMS = ["inventory", "mailbox", "payment_requests", "history"];
|
|
26886
|
+
async function reseedAndReset(deps) {
|
|
26887
|
+
for (const entry of await deps.stores.backstop.list()) {
|
|
26888
|
+
if (entry.disposition !== "open") continue;
|
|
26889
|
+
try {
|
|
26890
|
+
await deps.reput(entry.transferId, entry.payloadEnvelope, entry.requiresSeedClose);
|
|
26891
|
+
if (entry.requiresSeedClose) {
|
|
26892
|
+
await deps.reseedCheckpoint(entry.transferId, await splitOpIndex(deps, entry.payloadEnvelope));
|
|
26893
|
+
}
|
|
26894
|
+
} catch (err) {
|
|
26895
|
+
deps.attention(entry.transferId, ATTENTION_RESEED_REJECTED, err instanceof Error ? err.message : String(err));
|
|
26896
|
+
}
|
|
26897
|
+
}
|
|
26898
|
+
for (const stream of ALL_STREAMS) await deps.kv.remove(STORE_KEYS.streamCursor(stream));
|
|
26899
|
+
await deps.fullRePull();
|
|
26900
|
+
}
|
|
26901
|
+
async function splitOpIndex(deps, envelope) {
|
|
26902
|
+
const payload = await deps.decryptPayload(envelope);
|
|
26903
|
+
return Array.isArray(payload.direct) ? payload.direct.length : 0;
|
|
26904
|
+
}
|
|
26905
|
+
|
|
26906
|
+
// modules/payments-v2/inventory/presentation.ts
|
|
26907
|
+
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26908
|
+
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26909
|
+
return {
|
|
26910
|
+
id: tokenId,
|
|
26911
|
+
coinId: asset.coinId,
|
|
26912
|
+
symbol: registry.getSymbol(asset.coinId),
|
|
26913
|
+
name: registry.getName(asset.coinId),
|
|
26914
|
+
decimals: registry.getDecimals(asset.coinId),
|
|
26915
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
26916
|
+
amount: asset.amount,
|
|
26917
|
+
status: flags.transferring ? "transferring" : "confirmed",
|
|
26918
|
+
createdAt: snapshot.createdAt,
|
|
26919
|
+
updatedAt: snapshot.updatedAt,
|
|
26920
|
+
lazy: true,
|
|
26921
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
26922
|
+
};
|
|
26923
|
+
}
|
|
26924
|
+
function aggregateAssets(entries, isTransferring, registry) {
|
|
26925
|
+
const groups = /* @__PURE__ */ new Map();
|
|
26926
|
+
for (const [tokenId, entry] of entries) {
|
|
26927
|
+
const moving = isTransferring(tokenId);
|
|
26928
|
+
for (const asset of entry.assets) {
|
|
26929
|
+
let group = groups.get(asset.coinId);
|
|
26930
|
+
if (!group) {
|
|
26931
|
+
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
26932
|
+
groups.set(asset.coinId, group);
|
|
26933
|
+
}
|
|
26934
|
+
if (moving) {
|
|
26935
|
+
group.transferring += BigInt(asset.amount);
|
|
26936
|
+
group.transferringCount += 1;
|
|
26937
|
+
} else {
|
|
26938
|
+
group.total += BigInt(asset.amount);
|
|
26939
|
+
group.count += 1;
|
|
26940
|
+
}
|
|
26941
|
+
}
|
|
26942
|
+
}
|
|
26943
|
+
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
26944
|
+
}
|
|
26945
|
+
function toAsset(coinId, totals, registry) {
|
|
26946
|
+
const iconUrl = registry.getIconUrl(coinId);
|
|
26947
|
+
return {
|
|
26948
|
+
coinId,
|
|
26949
|
+
symbol: registry.getSymbol(coinId),
|
|
26950
|
+
name: registry.getName(coinId),
|
|
26951
|
+
decimals: registry.getDecimals(coinId),
|
|
26952
|
+
...iconUrl !== null ? { iconUrl } : {},
|
|
26953
|
+
totalAmount: totals.total.toString(),
|
|
26954
|
+
tokenCount: totals.count,
|
|
26955
|
+
confirmedAmount: totals.total.toString(),
|
|
26956
|
+
unconfirmedAmount: "0",
|
|
26957
|
+
confirmedTokenCount: totals.count,
|
|
26958
|
+
unconfirmedTokenCount: 0,
|
|
26959
|
+
transferringTokenCount: totals.transferringCount,
|
|
26960
|
+
transferringAmount: totals.transferring.toString(),
|
|
26961
|
+
priceUsd: null,
|
|
26962
|
+
priceEur: null,
|
|
26963
|
+
change24h: null,
|
|
26964
|
+
fiatValueUsd: null,
|
|
26965
|
+
fiatValueEur: null
|
|
26966
|
+
};
|
|
26967
|
+
}
|
|
26968
|
+
async function withPrices(raw, registry, price) {
|
|
26969
|
+
try {
|
|
26970
|
+
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
26971
|
+
const quotes = await price.getPrices(names);
|
|
26972
|
+
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
26973
|
+
} catch {
|
|
26974
|
+
return raw;
|
|
26975
|
+
}
|
|
26976
|
+
}
|
|
26977
|
+
function priceOne(asset, quote) {
|
|
26978
|
+
if (!quote) return asset;
|
|
26979
|
+
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
26980
|
+
return {
|
|
26981
|
+
...asset,
|
|
26982
|
+
priceUsd: quote.priceUsd,
|
|
26983
|
+
priceEur: quote.priceEur ?? null,
|
|
26984
|
+
change24h: quote.change24h ?? null,
|
|
26985
|
+
fiatValueUsd: human * quote.priceUsd,
|
|
26986
|
+
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
26987
|
+
};
|
|
26988
|
+
}
|
|
26989
|
+
|
|
26990
|
+
// modules/payments-v2/machine/payload.ts
|
|
26991
|
+
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
26992
|
+
return {
|
|
26993
|
+
v: 2,
|
|
26994
|
+
recipient: recipientPubkey,
|
|
26995
|
+
coinId: request.coinId,
|
|
26996
|
+
amount: request.amount,
|
|
26997
|
+
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
26998
|
+
direct: [...spend.plan.direct],
|
|
26999
|
+
...spend.plan.split !== void 0 ? {
|
|
27000
|
+
split: {
|
|
27001
|
+
tokenId: spend.plan.split.tokenId,
|
|
27002
|
+
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
27003
|
+
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
27004
|
+
}
|
|
27005
|
+
} : {},
|
|
27006
|
+
spentStates
|
|
27007
|
+
};
|
|
27008
|
+
}
|
|
27009
|
+
function messageOf(err) {
|
|
27010
|
+
return err instanceof Error ? err.message : String(err);
|
|
27011
|
+
}
|
|
27012
|
+
|
|
26631
27013
|
// modules/payments-v2/history/History.ts
|
|
26632
27014
|
var WIRE_TOKEN_ID = /^(?:[0-9a-f]{2}){1,64}$/;
|
|
26633
27015
|
var WIRE_PUBKEY = /^0[23][0-9a-f]{64}$/;
|
|
@@ -26686,13 +27068,14 @@ var History = class {
|
|
|
26686
27068
|
}
|
|
26687
27069
|
// ── internals ────────────────────────────────────────────────────────────────
|
|
26688
27070
|
async post(build) {
|
|
27071
|
+
const record = build();
|
|
26689
27072
|
try {
|
|
26690
|
-
await this.deps.client.postHistory([
|
|
27073
|
+
await this.deps.client.postHistory([record]);
|
|
26691
27074
|
} catch (err) {
|
|
26692
27075
|
this.deps.log?.("history POST failed (dedupKey makes retry safe)", err);
|
|
26693
27076
|
return;
|
|
26694
27077
|
}
|
|
26695
|
-
this.deps.emit("history:updated");
|
|
27078
|
+
this.deps.emit("history:updated", this.toEntry(record));
|
|
26696
27079
|
}
|
|
26697
27080
|
ts(timestamp) {
|
|
26698
27081
|
return new Date(timestamp ?? (this.deps.now ?? Date.now)()).toISOString();
|
|
@@ -26749,93 +27132,9 @@ var History = class {
|
|
|
26749
27132
|
}
|
|
26750
27133
|
};
|
|
26751
27134
|
|
|
26752
|
-
// modules/payments-v2/inventory/presentation.ts
|
|
26753
|
-
function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
26754
|
-
const iconUrl = registry.getIconUrl(asset.coinId);
|
|
26755
|
-
return {
|
|
26756
|
-
id: tokenId,
|
|
26757
|
-
coinId: asset.coinId,
|
|
26758
|
-
symbol: registry.getSymbol(asset.coinId),
|
|
26759
|
-
name: registry.getName(asset.coinId),
|
|
26760
|
-
decimals: registry.getDecimals(asset.coinId),
|
|
26761
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
26762
|
-
amount: asset.amount,
|
|
26763
|
-
status: flags.transferring ? "transferring" : "confirmed",
|
|
26764
|
-
createdAt: snapshot.createdAt,
|
|
26765
|
-
updatedAt: snapshot.updatedAt,
|
|
26766
|
-
lazy: true,
|
|
26767
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
26768
|
-
};
|
|
26769
|
-
}
|
|
26770
|
-
function aggregateAssets(entries, isTransferring, registry) {
|
|
26771
|
-
const groups = /* @__PURE__ */ new Map();
|
|
26772
|
-
for (const [tokenId, entry] of entries) {
|
|
26773
|
-
const moving = isTransferring(tokenId);
|
|
26774
|
-
for (const asset of entry.assets) {
|
|
26775
|
-
let group = groups.get(asset.coinId);
|
|
26776
|
-
if (!group) {
|
|
26777
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
26778
|
-
groups.set(asset.coinId, group);
|
|
26779
|
-
}
|
|
26780
|
-
if (moving) {
|
|
26781
|
-
group.transferring += BigInt(asset.amount);
|
|
26782
|
-
group.transferringCount += 1;
|
|
26783
|
-
} else {
|
|
26784
|
-
group.total += BigInt(asset.amount);
|
|
26785
|
-
group.count += 1;
|
|
26786
|
-
}
|
|
26787
|
-
}
|
|
26788
|
-
}
|
|
26789
|
-
return [...groups].map(([coinId, totals]) => toAsset(coinId, totals, registry));
|
|
26790
|
-
}
|
|
26791
|
-
function toAsset(coinId, totals, registry) {
|
|
26792
|
-
const iconUrl = registry.getIconUrl(coinId);
|
|
26793
|
-
return {
|
|
26794
|
-
coinId,
|
|
26795
|
-
symbol: registry.getSymbol(coinId),
|
|
26796
|
-
name: registry.getName(coinId),
|
|
26797
|
-
decimals: registry.getDecimals(coinId),
|
|
26798
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
26799
|
-
totalAmount: totals.total.toString(),
|
|
26800
|
-
tokenCount: totals.count,
|
|
26801
|
-
confirmedAmount: totals.total.toString(),
|
|
26802
|
-
unconfirmedAmount: "0",
|
|
26803
|
-
confirmedTokenCount: totals.count,
|
|
26804
|
-
unconfirmedTokenCount: 0,
|
|
26805
|
-
transferringTokenCount: totals.transferringCount,
|
|
26806
|
-
transferringAmount: totals.transferring.toString(),
|
|
26807
|
-
priceUsd: null,
|
|
26808
|
-
priceEur: null,
|
|
26809
|
-
change24h: null,
|
|
26810
|
-
fiatValueUsd: null,
|
|
26811
|
-
fiatValueEur: null
|
|
26812
|
-
};
|
|
26813
|
-
}
|
|
26814
|
-
async function withPrices(raw, registry, price) {
|
|
26815
|
-
try {
|
|
26816
|
-
const names = [...new Set(raw.map((a) => registry.getName(a.coinId)))];
|
|
26817
|
-
const quotes = await price.getPrices(names);
|
|
26818
|
-
return raw.map((a) => priceOne(a, quotes.get(registry.getName(a.coinId))));
|
|
26819
|
-
} catch {
|
|
26820
|
-
return raw;
|
|
26821
|
-
}
|
|
26822
|
-
}
|
|
26823
|
-
function priceOne(asset, quote) {
|
|
26824
|
-
if (!quote) return asset;
|
|
26825
|
-
const human = Number(asset.totalAmount) / Math.pow(10, asset.decimals);
|
|
26826
|
-
return {
|
|
26827
|
-
...asset,
|
|
26828
|
-
priceUsd: quote.priceUsd,
|
|
26829
|
-
priceEur: quote.priceEur ?? null,
|
|
26830
|
-
change24h: quote.change24h ?? null,
|
|
26831
|
-
fiatValueUsd: human * quote.priceUsd,
|
|
26832
|
-
fiatValueEur: quote.priceEur != null ? human * quote.priceEur : null
|
|
26833
|
-
};
|
|
26834
|
-
}
|
|
26835
|
-
|
|
26836
27135
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
26837
|
-
var SUSPECTED_KEY =
|
|
26838
|
-
var KNOWN_SPENDS_KEY =
|
|
27136
|
+
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
27137
|
+
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
26839
27138
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
26840
27139
|
function stateKey(tokenId, stateHash) {
|
|
26841
27140
|
return `${tokenId}:${stateHash}`;
|
|
@@ -27139,19 +27438,24 @@ var Receive = class {
|
|
|
27139
27438
|
const engine = deps.engine();
|
|
27140
27439
|
const stored = [];
|
|
27141
27440
|
const pending = [];
|
|
27142
|
-
|
|
27441
|
+
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
27143
27442
|
try {
|
|
27144
|
-
epoch = deps.syncEpoch();
|
|
27145
27443
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
27146
|
-
|
|
27147
|
-
for
|
|
27148
|
-
await
|
|
27149
|
-
|
|
27444
|
+
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
27445
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
27446
|
+
for await (const entry of deps.delivery.incoming(basis === null ? void 0 : String(basis.cursor))) {
|
|
27447
|
+
await this.processEntry(deps, engine, entry, pending, stored);
|
|
27448
|
+
if (pending.length >= ACK_BATCH_SIZE) await flushAcks(deps, pending, pageEpoch);
|
|
27449
|
+
}
|
|
27450
|
+
await flushAcks(deps, pending, pageEpoch);
|
|
27451
|
+
const served = deps.delivery.incomingEpoch();
|
|
27452
|
+
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
27453
|
+
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
27454
|
+
basis = null;
|
|
27150
27455
|
}
|
|
27151
|
-
await flushAcks(deps, pending, epoch);
|
|
27152
27456
|
} catch (err) {
|
|
27153
27457
|
logger.warn("PaymentsV2", "receive drain interrupted \u2014 unacked entries retry next drain:", err);
|
|
27154
|
-
await flushAcks(deps, pending,
|
|
27458
|
+
await flushAcks(deps, pending, pageEpoch).catch((flushErr) => {
|
|
27155
27459
|
logger.warn("PaymentsV2", "receive ack flush failed \u2014 cursor holds at the acked prefix:", flushErr);
|
|
27156
27460
|
});
|
|
27157
27461
|
}
|
|
@@ -27224,7 +27528,7 @@ async function announce(deps, entry, record) {
|
|
|
27224
27528
|
receivedAt
|
|
27225
27529
|
};
|
|
27226
27530
|
}
|
|
27227
|
-
async function flushAcks(deps, pending,
|
|
27531
|
+
async function flushAcks(deps, pending, epochOf) {
|
|
27228
27532
|
let lastAcked = null;
|
|
27229
27533
|
try {
|
|
27230
27534
|
while (pending.length > 0) {
|
|
@@ -27235,7 +27539,7 @@ async function flushAcks(deps, pending, epoch) {
|
|
|
27235
27539
|
}
|
|
27236
27540
|
} finally {
|
|
27237
27541
|
if (lastAcked !== null) {
|
|
27238
|
-
const record = { cursor: lastAcked, syncEpoch:
|
|
27542
|
+
const record = { cursor: lastAcked, syncEpoch: epochOf() };
|
|
27239
27543
|
await deps.kv.set(STORE_KEYS.streamCursor("mailbox"), record);
|
|
27240
27544
|
}
|
|
27241
27545
|
}
|
|
@@ -27336,9 +27640,19 @@ var Requests = class {
|
|
|
27336
27640
|
}
|
|
27337
27641
|
});
|
|
27338
27642
|
}
|
|
27643
|
+
/** Idempotent for a same-transferId re-write: committed only ratchets up, createdAt kept. */
|
|
27339
27644
|
writeLink(requestId, transferId, committed) {
|
|
27340
27645
|
return this.mutateJournal((m) => {
|
|
27341
|
-
|
|
27646
|
+
const prev = m.get(requestId);
|
|
27647
|
+
const same = prev !== void 0 && prev.transferId === transferId;
|
|
27648
|
+
const next = {
|
|
27649
|
+
requestId,
|
|
27650
|
+
transferId,
|
|
27651
|
+
committed: committed || same && prev.committed,
|
|
27652
|
+
createdAt: same ? prev.createdAt : this.now()
|
|
27653
|
+
};
|
|
27654
|
+
if (same && prev.committed === next.committed) return false;
|
|
27655
|
+
m.set(requestId, next);
|
|
27342
27656
|
return true;
|
|
27343
27657
|
});
|
|
27344
27658
|
}
|
|
@@ -27491,23 +27805,46 @@ var Requests = class {
|
|
|
27491
27805
|
const transferId = error instanceof SphereError && error.transferId !== void 0 ? error.transferId : void 0;
|
|
27492
27806
|
if (transferId !== void 0) {
|
|
27493
27807
|
const committed = error instanceof PartialSendConflictError;
|
|
27494
|
-
await this.
|
|
27495
|
-
this.setStatus(id, "settling");
|
|
27808
|
+
await this.settle(id, transferId, { committed, respond: false });
|
|
27496
27809
|
} else {
|
|
27497
27810
|
logger.error("PaymentsV2", `possibly-committed send for ${id} carried no transferId \u2014 paid (fail-closed)`);
|
|
27498
27811
|
this.setStatus(id, "paid");
|
|
27499
27812
|
}
|
|
27500
27813
|
} else {
|
|
27501
|
-
await this.
|
|
27502
|
-
this.setStatus(id, "pending");
|
|
27814
|
+
await this.revertPayable(id);
|
|
27503
27815
|
}
|
|
27504
27816
|
throw error;
|
|
27505
27817
|
}
|
|
27506
|
-
this.
|
|
27507
|
-
await this.respondPaid(id, result.id);
|
|
27508
|
-
await this.clearLink(id);
|
|
27818
|
+
await this.settle(id, result.id, { committed: true, respond: true });
|
|
27509
27819
|
return result;
|
|
27510
27820
|
}
|
|
27821
|
+
/**
|
|
27822
|
+
* THE settlement invariant (#441 + the failed-respond P1): a settling link is
|
|
27823
|
+
* removed ONLY by (a) a CONFIRMED paid respond — a 2xx, or the 409
|
|
27824
|
+
* already-resolved absorb — or (b) a proven clean pre-commit failure
|
|
27825
|
+
* (revertPayable). Nothing else removes one: not a network error, not a 5xx,
|
|
27826
|
+
* not a reload. Every path that binds a request to a transfer outcome funnels
|
|
27827
|
+
* through here — pay()'s clean success (respond now), pay()'s
|
|
27828
|
+
* possibly-committed throw (respond deferred), and reconcile's deferred arms
|
|
27829
|
+
* — so a failed respond always leaves the link + 'settling' and the next
|
|
27830
|
+
* reconcile pass (the committed-link override) retries the respond.
|
|
27831
|
+
*/
|
|
27832
|
+
async settle(requestId, transferId, opts) {
|
|
27833
|
+
let journalDown = false;
|
|
27834
|
+
try {
|
|
27835
|
+
await this.writeLink(requestId, transferId, opts.committed);
|
|
27836
|
+
} catch (err) {
|
|
27837
|
+
if (!opts.respond) throw err;
|
|
27838
|
+
journalDown = true;
|
|
27839
|
+
logger.error("PaymentsV2", `settling link write failed for ${requestId} \u2014 responding fail-closed:`, err);
|
|
27840
|
+
}
|
|
27841
|
+
if (!(opts.respond && await this.respondPaid(requestId, transferId))) {
|
|
27842
|
+
this.setStatus(requestId, journalDown ? "paid" : "settling");
|
|
27843
|
+
return;
|
|
27844
|
+
}
|
|
27845
|
+
if (!journalDown) await this.clearLink(requestId).catch(() => void 0);
|
|
27846
|
+
this.setStatus(requestId, "paid");
|
|
27847
|
+
}
|
|
27511
27848
|
/** 'paid' respond leg: 409 = already resolved = idempotent success; other errors defer. */
|
|
27512
27849
|
async respondPaid(id, transferId) {
|
|
27513
27850
|
try {
|
|
@@ -27543,32 +27880,32 @@ var Requests = class {
|
|
|
27543
27880
|
let aborted = null;
|
|
27544
27881
|
for (const [requestId, link] of [...this.journal]) {
|
|
27545
27882
|
if (link.committed || resumed.has(link.transferId)) {
|
|
27546
|
-
await this.resolvePaid(requestId, link
|
|
27883
|
+
await this.resolvePaid(requestId, link);
|
|
27547
27884
|
} else if (conflicted.has(link.transferId)) {
|
|
27548
27885
|
await this.revertPayable(requestId);
|
|
27549
27886
|
} else if (open.has(link.transferId)) {
|
|
27550
27887
|
} else {
|
|
27551
|
-
aborted = await this.reconcileUnaccounted(requestId, link
|
|
27888
|
+
aborted = await this.reconcileUnaccounted(requestId, link, aborted);
|
|
27552
27889
|
}
|
|
27553
27890
|
}
|
|
27554
27891
|
}
|
|
27555
27892
|
// Unaccounted link: consult the server aborted authority (lazy, memoized);
|
|
27556
27893
|
// a probe failure defers the link to the next session.
|
|
27557
|
-
async reconcileUnaccounted(requestId,
|
|
27894
|
+
async reconcileUnaccounted(requestId, link, aborted) {
|
|
27558
27895
|
try {
|
|
27559
27896
|
aborted ??= new Set(await this.deps.listAbortedTransferIds());
|
|
27560
|
-
if (aborted.has(transferId)) await this.revertPayable(requestId);
|
|
27561
|
-
else await this.resolvePaid(requestId,
|
|
27897
|
+
if (aborted.has(link.transferId)) await this.revertPayable(requestId);
|
|
27898
|
+
else await this.resolvePaid(requestId, link);
|
|
27562
27899
|
} catch (err) {
|
|
27563
27900
|
logger.warn("PaymentsV2", `aborted-intent probe failed \u2014 ${requestId} deferred:`, err);
|
|
27564
27901
|
}
|
|
27565
27902
|
return aborted;
|
|
27566
27903
|
}
|
|
27567
|
-
|
|
27568
|
-
|
|
27569
|
-
|
|
27570
|
-
this.setStatus(requestId, "paid");
|
|
27904
|
+
/** Deferred paid: the ONE settlement path again — a failed respond keeps the link. */
|
|
27905
|
+
resolvePaid(requestId, link) {
|
|
27906
|
+
return this.settle(requestId, link.transferId, { committed: link.committed, respond: true });
|
|
27571
27907
|
}
|
|
27908
|
+
/** Removal cause (b): a PROVEN clean outcome (pre-commit failure / server-aborted). */
|
|
27572
27909
|
async revertPayable(requestId) {
|
|
27573
27910
|
await this.clearLink(requestId);
|
|
27574
27911
|
this.setStatus(requestId, "pending");
|
|
@@ -27892,11 +28229,12 @@ function composeFacadeParts(deps, hooks) {
|
|
|
27892
28229
|
client: deps.client,
|
|
27893
28230
|
fieldKey: deps.fieldKey,
|
|
27894
28231
|
registry: deps.registry,
|
|
27895
|
-
emit: (event) => deps.emit(event,
|
|
28232
|
+
emit: (event, entry) => deps.emit(event, entry),
|
|
27896
28233
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
27897
28234
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
27898
28235
|
});
|
|
27899
28236
|
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
28237
|
+
const machineStores = createMachineStores(deps.kv);
|
|
27900
28238
|
const heldStates = /* @__PURE__ */ new Map();
|
|
27901
28239
|
return {
|
|
27902
28240
|
ownPubkeyBytes,
|
|
@@ -27904,12 +28242,29 @@ function composeFacadeParts(deps, hooks) {
|
|
|
27904
28242
|
ledger,
|
|
27905
28243
|
queue,
|
|
27906
28244
|
historyStore,
|
|
27907
|
-
machineStores
|
|
28245
|
+
machineStores,
|
|
27908
28246
|
machineDeps,
|
|
27909
28247
|
machine: new TransferMachine(machineDeps),
|
|
27910
28248
|
heldStates,
|
|
27911
28249
|
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates),
|
|
27912
|
-
requests: buildRequests(deps, hooks)
|
|
28250
|
+
requests: buildRequests(deps, hooks),
|
|
28251
|
+
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
28252
|
+
};
|
|
28253
|
+
}
|
|
28254
|
+
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
28255
|
+
return {
|
|
28256
|
+
stores: machineStores,
|
|
28257
|
+
kv: deps.kv,
|
|
28258
|
+
reput: (transferId, envelope, requiresSeedClose) => machineDeps.intents.put(transferId, envelope, requiresSeedClose),
|
|
28259
|
+
reseedCheckpoint: (transferId, opIndex) => deps.checkpointStore.reseedCheckpoint(transferId, opIndex),
|
|
28260
|
+
decryptPayload: machineDeps.decryptPayload,
|
|
28261
|
+
fullRePull: () => view.onEpochReset(),
|
|
28262
|
+
attention: (transferId, code, detail) => {
|
|
28263
|
+
deps.emit(
|
|
28264
|
+
"transfer:attention",
|
|
28265
|
+
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28266
|
+
);
|
|
28267
|
+
}
|
|
27913
28268
|
};
|
|
27914
28269
|
}
|
|
27915
28270
|
function buildMachineDeps(deps, hooks, historyStore) {
|
|
@@ -27931,11 +28286,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
27931
28286
|
ownPubkey: hexToBytes4(deps.ownPubkey),
|
|
27932
28287
|
emit: deps.emit,
|
|
27933
28288
|
now: deps.now ?? Date.now,
|
|
27934
|
-
recordHistory: async ({ transferId, payload }) => {
|
|
28289
|
+
recordHistory: async ({ transferId, payload, committedAmount }) => {
|
|
27935
28290
|
await historyStore.recordSent({
|
|
27936
28291
|
transferId,
|
|
27937
28292
|
coinId: payload.coinId,
|
|
27938
|
-
|
|
28293
|
+
// §5.9: the SETTLED amount (machine-computed from the certified
|
|
28294
|
+
// recipient blobs), never payload.amount — the plan.
|
|
28295
|
+
amount: committedAmount,
|
|
27939
28296
|
recipientPubkey: payload.recipient,
|
|
27940
28297
|
...payload.memo !== void 0 ? { memo: payload.memo } : {}
|
|
27941
28298
|
});
|
|
@@ -27962,7 +28319,7 @@ function buildReceive(deps, hooks, historyStore, heldStates) {
|
|
|
27962
28319
|
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
27963
28320
|
);
|
|
27964
28321
|
},
|
|
27965
|
-
syncEpoch: deps.syncEpoch
|
|
28322
|
+
syncEpoch: deps.syncEpoch,
|
|
27966
28323
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
27967
28324
|
});
|
|
27968
28325
|
}
|
|
@@ -28015,8 +28372,24 @@ var PaymentsFacade = class {
|
|
|
28015
28372
|
this.receiveLoop = parts.receiveLoop;
|
|
28016
28373
|
this.heldStates = parts.heldStates;
|
|
28017
28374
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
28375
|
+
this.restoreDeps = parts.restoreDeps;
|
|
28018
28376
|
this.requests = parts.requests;
|
|
28019
28377
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
28378
|
+
this.heartbeat = new ConvergenceHeartbeat({
|
|
28379
|
+
now: () => this.nowMs(),
|
|
28380
|
+
onTick: () => this.trackTail(this.runConvergencePass())
|
|
28381
|
+
});
|
|
28382
|
+
this.converger = new Converger({
|
|
28383
|
+
machineDeps: this.machineDeps,
|
|
28384
|
+
stores: this.machineStores,
|
|
28385
|
+
delivery: deps.deliveryPort,
|
|
28386
|
+
reconcile: (report) => this.requests.reconcile(report),
|
|
28387
|
+
refreshView: () => this.view.delta(),
|
|
28388
|
+
replayMints: () => this.replayMints(),
|
|
28389
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id),
|
|
28390
|
+
emit: deps.emit,
|
|
28391
|
+
now: () => this.nowMs()
|
|
28392
|
+
});
|
|
28020
28393
|
}
|
|
28021
28394
|
view;
|
|
28022
28395
|
ledger;
|
|
@@ -28028,17 +28401,28 @@ var PaymentsFacade = class {
|
|
|
28028
28401
|
receiveLoop;
|
|
28029
28402
|
heldStates;
|
|
28030
28403
|
ownPubkeyBytes;
|
|
28404
|
+
restoreDeps;
|
|
28031
28405
|
requests;
|
|
28032
28406
|
currentEngine = null;
|
|
28033
28407
|
pendingOps = /* @__PURE__ */ new Set();
|
|
28034
28408
|
unsubscribers = [];
|
|
28035
28409
|
started = false;
|
|
28410
|
+
// §7 heartbeat: in-memory timing cell + pass runner (see convergence.ts).
|
|
28411
|
+
heartbeat;
|
|
28412
|
+
converger;
|
|
28413
|
+
/** Resume single-flight (§7): ticks, start() and resumeNow() coalesce onto ONE pass. */
|
|
28414
|
+
resumeFlight = new SingleFlight();
|
|
28415
|
+
/** §5.1/§7: restore + convergence passes SERIALIZE here. */
|
|
28416
|
+
passChain = new SerialChain();
|
|
28417
|
+
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
28418
|
+
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
28036
28419
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
28037
28420
|
async start() {
|
|
28038
28421
|
if (this.started) return;
|
|
28039
28422
|
this.started = true;
|
|
28040
|
-
|
|
28423
|
+
this.heartbeat.start();
|
|
28041
28424
|
this.unsubscribers.push(
|
|
28425
|
+
this.deps.session.subscribeEpochChange((epoch) => this.handleEpochChange(epoch)),
|
|
28042
28426
|
this.deps.session.subscribeStream("mailbox", () => {
|
|
28043
28427
|
this.trackTail(this.receiveLoop.drainOnce());
|
|
28044
28428
|
}),
|
|
@@ -28049,20 +28433,12 @@ var PaymentsFacade = class {
|
|
|
28049
28433
|
this.trackTail(this.requests.drainIncoming());
|
|
28050
28434
|
})
|
|
28051
28435
|
);
|
|
28052
|
-
this.
|
|
28053
|
-
|
|
28054
|
-
|
|
28055
|
-
|
|
28056
|
-
|
|
28057
|
-
|
|
28058
|
-
this.deps.emit(
|
|
28059
|
-
"transfer:attention",
|
|
28060
|
-
detail === void 0 ? { transferId, code } : { transferId, code, detail }
|
|
28061
|
-
);
|
|
28062
|
-
}
|
|
28063
|
-
})
|
|
28064
|
-
);
|
|
28065
|
-
this.trackTail(this.replayMints());
|
|
28436
|
+
const statusUnsub = this.deps.session.subscribeStatus?.((status) => {
|
|
28437
|
+
if (status === "connected") this.heartbeat.recovered();
|
|
28438
|
+
});
|
|
28439
|
+
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
28440
|
+
await this.deps.session.start();
|
|
28441
|
+
this.trackTail(this.runConvergencePass());
|
|
28066
28442
|
this.trackTail(this.seedHeldStates());
|
|
28067
28443
|
await this.view.fullPull().catch(() => void 0);
|
|
28068
28444
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
@@ -28070,6 +28446,7 @@ var PaymentsFacade = class {
|
|
|
28070
28446
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
28071
28447
|
async stop() {
|
|
28072
28448
|
this.started = false;
|
|
28449
|
+
this.heartbeat.stop();
|
|
28073
28450
|
this.receiveLoop.stop();
|
|
28074
28451
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
28075
28452
|
this.unsubscribers = [];
|
|
@@ -28095,9 +28472,13 @@ var PaymentsFacade = class {
|
|
|
28095
28472
|
history(page) {
|
|
28096
28473
|
return this.historyStore.page(page ?? {});
|
|
28097
28474
|
}
|
|
28475
|
+
/** §4 pending-transfers UI surface — derived on read, never cached (convergence.ts). */
|
|
28476
|
+
pendingTransfers() {
|
|
28477
|
+
return derivePendingTransfers(this.machineStores, this.machineDeps.decryptPayload);
|
|
28478
|
+
}
|
|
28098
28479
|
// ── money movement ─────────────────────────────────────────────────────────
|
|
28099
28480
|
send(request) {
|
|
28100
|
-
return this.track(this.
|
|
28481
|
+
return this.track(this.sendOutcome(request));
|
|
28101
28482
|
}
|
|
28102
28483
|
async receive() {
|
|
28103
28484
|
const transfers = await this.track(this.receiveLoop.drainOnce());
|
|
@@ -28107,9 +28488,32 @@ var PaymentsFacade = class {
|
|
|
28107
28488
|
mint(coinId, amount) {
|
|
28108
28489
|
return this.track(this.mintInner(coinId, amount));
|
|
28109
28490
|
}
|
|
28491
|
+
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
28492
|
+
async resumeNow() {
|
|
28493
|
+
if (!this.started) return;
|
|
28494
|
+
await this.track(this.runConvergencePass());
|
|
28495
|
+
}
|
|
28496
|
+
/** §5.1 restore — awaited by the session latch BEFORE streams resume; never coalesced onto a pre-restore pass. */
|
|
28497
|
+
async handleEpochChange(_newEpoch) {
|
|
28498
|
+
await this.passChain.enqueue(() => reseedAndReset(this.restoreDeps));
|
|
28499
|
+
await this.passChain.enqueue(() => this.convergeBody());
|
|
28500
|
+
}
|
|
28501
|
+
/** One single-flighted pass + its reschedule: concurrent callers coalesce. */
|
|
28502
|
+
runConvergencePass() {
|
|
28503
|
+
return this.resumeFlight.run(() => this.passChain.enqueue(() => this.convergeBody()));
|
|
28504
|
+
}
|
|
28505
|
+
async convergeBody() {
|
|
28506
|
+
const outcome = await this.converger.convergeOnce();
|
|
28507
|
+
const pending = await this.converger.pendingWork().catch(() => true);
|
|
28508
|
+
this.heartbeat.settle(outcome, pending);
|
|
28509
|
+
}
|
|
28510
|
+
nowMs() {
|
|
28511
|
+
return (this.deps.now ?? Date.now)();
|
|
28512
|
+
}
|
|
28110
28513
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
28111
|
-
|
|
28112
|
-
|
|
28514
|
+
/** The ONE place a send() outcome is shaped: success emits in finishSend, a
|
|
28515
|
+
* CLEAN rejection emits `transfer:updated{status:'failed'}` here (§4). */
|
|
28516
|
+
async sendOutcome(request) {
|
|
28113
28517
|
const run = {
|
|
28114
28518
|
amount: request.amount,
|
|
28115
28519
|
delivered: [],
|
|
@@ -28117,12 +28521,36 @@ var PaymentsFacade = class {
|
|
|
28117
28521
|
tokenTransfers: [],
|
|
28118
28522
|
settledShortfalls: [],
|
|
28119
28523
|
firstPartialId: void 0,
|
|
28120
|
-
pendingAny: false
|
|
28524
|
+
pendingAny: false,
|
|
28525
|
+
lastTransferId: ""
|
|
28121
28526
|
};
|
|
28527
|
+
try {
|
|
28528
|
+
return await this.sendWithPolicy(request, run);
|
|
28529
|
+
} catch (err) {
|
|
28530
|
+
this.emitCleanFailure(err, run.lastTransferId);
|
|
28531
|
+
throw err;
|
|
28532
|
+
}
|
|
28533
|
+
}
|
|
28534
|
+
/** Only a CLEAN failure (nothing certified, classifyError 'other') is 'failed'.
|
|
28535
|
+
* Keep-open/partial/conflict outcomes are pending/converging — labelling them
|
|
28536
|
+
* 'failed' invites a dApp re-send, i.e. a double-pay (#631/#676). */
|
|
28537
|
+
emitCleanFailure(err, transferId) {
|
|
28538
|
+
if (isPossiblyCommittedSendOutcome(err) || classifyError(err) !== "other") return;
|
|
28539
|
+
const failed = {
|
|
28540
|
+
id: transferId,
|
|
28541
|
+
status: "failed",
|
|
28542
|
+
tokens: [],
|
|
28543
|
+
tokenTransfers: [],
|
|
28544
|
+
error: messageOf(err)
|
|
28545
|
+
};
|
|
28546
|
+
this.deps.emit("transfer:updated", failed);
|
|
28547
|
+
}
|
|
28548
|
+
async sendWithPolicy(request, run) {
|
|
28549
|
+
const recipient = await this.requireSameNetworkRecipient(request.recipient);
|
|
28122
28550
|
for (let attempt = 0; ; attempt++) {
|
|
28123
28551
|
let ctx;
|
|
28124
28552
|
try {
|
|
28125
|
-
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount });
|
|
28553
|
+
ctx = await this.planAndMaterialize(recipient.chainPubkey, { ...request, amount: run.amount }, run);
|
|
28126
28554
|
} catch (err) {
|
|
28127
28555
|
throw this.partialize(err, run);
|
|
28128
28556
|
}
|
|
@@ -28156,21 +28584,24 @@ var PaymentsFacade = class {
|
|
|
28156
28584
|
return recipient;
|
|
28157
28585
|
}
|
|
28158
28586
|
async runAttempt(ctx) {
|
|
28587
|
+
this.activeMoneyOps.add(ctx.transferId);
|
|
28159
28588
|
try {
|
|
28160
28589
|
const { deliveryPending } = await this.machine.run(ctx.plan);
|
|
28161
28590
|
this.settleSuccess(ctx, ctx.sourceIds);
|
|
28162
28591
|
return { kind: "success", deliveryPending };
|
|
28163
28592
|
} catch (err) {
|
|
28164
|
-
return this.disposeFailedAttempt(ctx, err);
|
|
28593
|
+
return await this.disposeFailedAttempt(ctx, err);
|
|
28594
|
+
} finally {
|
|
28595
|
+
this.activeMoneyOps.delete(ctx.transferId);
|
|
28165
28596
|
}
|
|
28166
28597
|
}
|
|
28167
|
-
/**
|
|
28168
|
-
*
|
|
28169
|
-
* the machine BEFORE complete). Accumulate the settled set, then re-plan ONLY
|
|
28170
|
-
* the remainder under a NEW transferId — never the full amount.
|
|
28171
|
-
*/
|
|
28598
|
+
/** Partial (#677/#690): shortfall already durable (machine wrote it BEFORE
|
|
28599
|
+
* complete); accumulate settled, re-plan ONLY the remainder, NEW transferId. */
|
|
28172
28600
|
async consumePartial(ctx, disposition, run, attempt) {
|
|
28173
|
-
|
|
28601
|
+
if (disposition.deliveryPending) {
|
|
28602
|
+
run.pendingAny = true;
|
|
28603
|
+
this.heartbeat.arm();
|
|
28604
|
+
}
|
|
28174
28605
|
const partial = disposition.entry;
|
|
28175
28606
|
run.delivered.push(...partial.committedTokenIds);
|
|
28176
28607
|
run.firstPartialId ??= partial.transferId;
|
|
@@ -28191,11 +28622,8 @@ var PaymentsFacade = class {
|
|
|
28191
28622
|
run.amount = partial.remainingAmount;
|
|
28192
28623
|
return null;
|
|
28193
28624
|
}
|
|
28194
|
-
/**
|
|
28195
|
-
*
|
|
28196
|
-
* backstop still 'open' (committed>0) → converge via the same machine's
|
|
28197
|
-
* resumeIntent; clean conflict with a demoted source → bounded full re-plan.
|
|
28198
|
-
*/
|
|
28625
|
+
/** Failure disposition: possibly-committed → rethrow UNWRAPPED; backstop
|
|
28626
|
+
* 'open' → converge via resumeIntent; clean demoted conflict → full re-plan. */
|
|
28199
28627
|
async disposeFailedAttempt(ctx, err) {
|
|
28200
28628
|
if (isPossiblyCommittedSendOutcome(err)) {
|
|
28201
28629
|
this.settleKeepOpen(ctx);
|
|
@@ -28244,8 +28672,9 @@ var PaymentsFacade = class {
|
|
|
28244
28672
|
}
|
|
28245
28673
|
return { kind: "rethrow", error: err };
|
|
28246
28674
|
}
|
|
28247
|
-
async planAndMaterialize(recipientPubkey, request) {
|
|
28675
|
+
async planAndMaterialize(recipientPubkey, request, run) {
|
|
28248
28676
|
const transferId = this.newId();
|
|
28677
|
+
run.lastTransferId = transferId;
|
|
28249
28678
|
const planned = this.queue.plan(transferId, { coinId: request.coinId, amount: request.amount });
|
|
28250
28679
|
const spend = planned.kind === "planned" ? planned.spend : await planned.settled;
|
|
28251
28680
|
const sourceIds = this.markPlanned(transferId, request.coinId, spend);
|
|
@@ -28328,6 +28757,7 @@ var PaymentsFacade = class {
|
|
|
28328
28757
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
28329
28758
|
settleKeepOpen(ctx) {
|
|
28330
28759
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
28760
|
+
this.heartbeat.arm();
|
|
28331
28761
|
}
|
|
28332
28762
|
settlePartial(ctx, partial) {
|
|
28333
28763
|
this.ledger.cancel(ctx.transferId);
|
|
@@ -28364,6 +28794,7 @@ var PaymentsFacade = class {
|
|
|
28364
28794
|
for (const id of run.settledShortfalls.splice(0)) {
|
|
28365
28795
|
await this.machineStores.shortfalls.removeByKey(id).catch(() => void 0);
|
|
28366
28796
|
}
|
|
28797
|
+
if (run.pendingAny) this.heartbeat.arm();
|
|
28367
28798
|
const result = {
|
|
28368
28799
|
id: transferId,
|
|
28369
28800
|
status: run.pendingAny ? "confirmed" : "delivered",
|
|
@@ -28375,11 +28806,7 @@ var PaymentsFacade = class {
|
|
|
28375
28806
|
this.deps.emit("transfer:updated", result);
|
|
28376
28807
|
return result;
|
|
28377
28808
|
}
|
|
28378
|
-
/**
|
|
28379
|
-
* With nothing delivered the error passes UNWRAPPED (identity + cause kept);
|
|
28380
|
-
* after ≥1 delivered leg EVERY failure surfaces as PartialSendConflictError
|
|
28381
|
-
* over the accumulated settled set — never bare, never a full-amount retry.
|
|
28382
|
-
*/
|
|
28809
|
+
/** Nothing delivered → UNWRAPPED; after ≥1 delivered leg every failure surfaces as PartialSendConflictError over the settled set. */
|
|
28383
28810
|
partialize(err, run) {
|
|
28384
28811
|
if (run.delivered.length === 0) return err;
|
|
28385
28812
|
return new PartialSendConflictError(
|
|
@@ -28408,21 +28835,29 @@ var PaymentsFacade = class {
|
|
|
28408
28835
|
if (!/^(?:[0-9a-f]{2})+$/.test(coinId) || amount <= 0n) {
|
|
28409
28836
|
return { success: false, error: "coinId must be even-length lowercase hex and amount positive" };
|
|
28410
28837
|
}
|
|
28411
|
-
const engine = this.engine();
|
|
28412
28838
|
const mintId = this.newId();
|
|
28413
|
-
|
|
28839
|
+
this.activeMoneyOps.add(mintId);
|
|
28840
|
+
try {
|
|
28841
|
+
return await this.mintUnderJournal(mintId, coinId, amount);
|
|
28842
|
+
} finally {
|
|
28843
|
+
this.activeMoneyOps.delete(mintId);
|
|
28844
|
+
}
|
|
28845
|
+
}
|
|
28846
|
+
async mintUnderJournal(mintId, coinId, amount) {
|
|
28847
|
+
const engine = this.engine();
|
|
28414
28848
|
const entry = {
|
|
28415
28849
|
mintId,
|
|
28416
28850
|
coinId,
|
|
28417
28851
|
amount: amount.toString(),
|
|
28418
28852
|
tokenId: "",
|
|
28419
|
-
createdAt:
|
|
28853
|
+
createdAt: this.nowMs()
|
|
28420
28854
|
};
|
|
28421
28855
|
await this.machineStores.mintJournal.upsert(entry);
|
|
28422
28856
|
let token;
|
|
28423
28857
|
try {
|
|
28424
|
-
token = await engine.mint(
|
|
28858
|
+
token = await engine.mint(this.mintParams(coinId, amount), { transferId: mintId });
|
|
28425
28859
|
} catch (err) {
|
|
28860
|
+
this.heartbeat.arm();
|
|
28426
28861
|
return { success: false, error: messageOf(err) };
|
|
28427
28862
|
}
|
|
28428
28863
|
if (token.blob.tokenId !== entry.tokenId) {
|
|
@@ -28431,6 +28866,7 @@ var PaymentsFacade = class {
|
|
|
28431
28866
|
try {
|
|
28432
28867
|
await this.finalizeMint(engine, mintId, token, coinId, amount.toString());
|
|
28433
28868
|
} catch (err) {
|
|
28869
|
+
this.heartbeat.arm();
|
|
28434
28870
|
return { success: false, tokenId: token.blob.tokenId, error: messageOf(err) };
|
|
28435
28871
|
}
|
|
28436
28872
|
return { success: true, tokenId: token.blob.tokenId };
|
|
@@ -28453,18 +28889,22 @@ var PaymentsFacade = class {
|
|
|
28453
28889
|
this.heldStates.set(token.blob.tokenId, (await engine.deliveryKeys(bytes)).stateHash);
|
|
28454
28890
|
this.trackTail(this.view.delta());
|
|
28455
28891
|
}
|
|
28892
|
+
/** @returns how many journal entries were RESOLVED (cleared) — heartbeat progress. */
|
|
28456
28893
|
async replayMints() {
|
|
28894
|
+
let resolved = 0;
|
|
28457
28895
|
for (const entry of await this.machineStores.mintJournal.list()) {
|
|
28896
|
+
if (this.activeMoneyOps.has(entry.mintId)) continue;
|
|
28458
28897
|
try {
|
|
28459
|
-
await this.replayMint(entry);
|
|
28898
|
+
if (await this.replayMint(entry)) resolved += 1;
|
|
28460
28899
|
} catch {
|
|
28461
28900
|
}
|
|
28462
28901
|
}
|
|
28902
|
+
return resolved;
|
|
28463
28903
|
}
|
|
28464
28904
|
async replayMint(entry) {
|
|
28465
28905
|
if (entry.tokenId !== "" && await this.tokenInServerInventory(entry.tokenId)) {
|
|
28466
28906
|
await this.machineStores.mintJournal.removeByKey(entry.mintId);
|
|
28467
|
-
return;
|
|
28907
|
+
return true;
|
|
28468
28908
|
}
|
|
28469
28909
|
const engine = this.engine();
|
|
28470
28910
|
if (!supportsDeterministicMint(engine)) {
|
|
@@ -28472,7 +28912,7 @@ var PaymentsFacade = class {
|
|
|
28472
28912
|
transferId: entry.mintId,
|
|
28473
28913
|
code: ATTENTION_MINT_UNRESOLVED
|
|
28474
28914
|
});
|
|
28475
|
-
return;
|
|
28915
|
+
return false;
|
|
28476
28916
|
}
|
|
28477
28917
|
const token = await engine.mint(this.mintParams(entry.coinId, BigInt(entry.amount)), {
|
|
28478
28918
|
transferId: entry.mintId
|
|
@@ -28481,6 +28921,7 @@ var PaymentsFacade = class {
|
|
|
28481
28921
|
await this.machineStores.mintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
28482
28922
|
}
|
|
28483
28923
|
await this.finalizeMint(engine, entry.mintId, token, entry.coinId, entry.amount);
|
|
28924
|
+
return true;
|
|
28484
28925
|
}
|
|
28485
28926
|
mintParams(coinId, amount) {
|
|
28486
28927
|
return {
|
|
@@ -28506,17 +28947,6 @@ var PaymentsFacade = class {
|
|
|
28506
28947
|
newId() {
|
|
28507
28948
|
return (this.deps.newId ?? randomUUID)();
|
|
28508
28949
|
}
|
|
28509
|
-
async runResume() {
|
|
28510
|
-
const report = await resumeAll(this.machineDeps);
|
|
28511
|
-
await this.requests.reconcile({
|
|
28512
|
-
resumed: report.resumed,
|
|
28513
|
-
conflicted: report.conflicted,
|
|
28514
|
-
open: report.failed
|
|
28515
|
-
});
|
|
28516
|
-
if (report.resumed.length > 0 || report.conflicted.length > 0) {
|
|
28517
|
-
await this.view.delta().catch(() => void 0);
|
|
28518
|
-
}
|
|
28519
|
-
}
|
|
28520
28950
|
async seedHeldStates() {
|
|
28521
28951
|
let page = await this.deps.storagePort.listInventory();
|
|
28522
28952
|
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
@@ -28528,22 +28958,14 @@ var PaymentsFacade = class {
|
|
|
28528
28958
|
}
|
|
28529
28959
|
}
|
|
28530
28960
|
toUiToken(tokenId, coinId, amount) {
|
|
28531
|
-
const
|
|
28532
|
-
|
|
28533
|
-
|
|
28534
|
-
|
|
28535
|
-
|
|
28536
|
-
|
|
28537
|
-
|
|
28538
|
-
|
|
28539
|
-
decimals: registry.getDecimals(coinId),
|
|
28540
|
-
...iconUrl !== null ? { iconUrl } : {},
|
|
28541
|
-
amount: amount.toString(),
|
|
28542
|
-
status: "transferring",
|
|
28543
|
-
createdAt: now,
|
|
28544
|
-
updatedAt: now,
|
|
28545
|
-
lazy: true
|
|
28546
|
-
};
|
|
28961
|
+
const now = this.nowMs();
|
|
28962
|
+
return toToken(
|
|
28963
|
+
tokenId,
|
|
28964
|
+
{ assets: [], createdAt: now, updatedAt: now },
|
|
28965
|
+
{ coinId, amount: amount.toString() },
|
|
28966
|
+
this.deps.registry,
|
|
28967
|
+
{ transferring: true, suspectedSpent: false }
|
|
28968
|
+
);
|
|
28547
28969
|
}
|
|
28548
28970
|
track(op) {
|
|
28549
28971
|
const settled = op.then(
|
|
@@ -28558,27 +28980,6 @@ var PaymentsFacade = class {
|
|
|
28558
28980
|
this.track(op).catch(() => void 0);
|
|
28559
28981
|
}
|
|
28560
28982
|
};
|
|
28561
|
-
function buildPayload(recipientPubkey, request, spend, spentStates) {
|
|
28562
|
-
return {
|
|
28563
|
-
v: 2,
|
|
28564
|
-
recipient: recipientPubkey,
|
|
28565
|
-
coinId: request.coinId,
|
|
28566
|
-
amount: request.amount,
|
|
28567
|
-
...request.memo !== void 0 ? { memo: request.memo } : {},
|
|
28568
|
-
direct: [...spend.plan.direct],
|
|
28569
|
-
...spend.plan.split !== void 0 ? {
|
|
28570
|
-
split: {
|
|
28571
|
-
tokenId: spend.plan.split.tokenId,
|
|
28572
|
-
splitAmount: spend.plan.split.splitAmount.toString(),
|
|
28573
|
-
remainderAmount: spend.plan.split.remainderAmount.toString()
|
|
28574
|
-
}
|
|
28575
|
-
} : {},
|
|
28576
|
-
spentStates
|
|
28577
|
-
};
|
|
28578
|
-
}
|
|
28579
|
-
function messageOf(err) {
|
|
28580
|
-
return err instanceof Error ? err.message : String(err);
|
|
28581
|
-
}
|
|
28582
28983
|
|
|
28583
28984
|
// impl/wallet-api-v2/http.ts
|
|
28584
28985
|
var WalletApiHttpError = class extends Error {
|
|
@@ -28924,7 +29325,9 @@ var WalletApiSession = class {
|
|
|
28924
29325
|
epochLatch = null;
|
|
28925
29326
|
reseedChain = Promise.resolve();
|
|
28926
29327
|
reseedsPending = 0;
|
|
29328
|
+
epochHandlers = /* @__PURE__ */ new Set();
|
|
28927
29329
|
handlers = /* @__PURE__ */ new Map();
|
|
29330
|
+
statusHandlers = /* @__PURE__ */ new Set();
|
|
28928
29331
|
socket = null;
|
|
28929
29332
|
started = false;
|
|
28930
29333
|
stopped = false;
|
|
@@ -29025,6 +29428,18 @@ var WalletApiSession = class {
|
|
|
29025
29428
|
await this.deps.kv.set(this.refreshKey, tokens.refreshToken);
|
|
29026
29429
|
}
|
|
29027
29430
|
// ── epoch latch (F12) ─────────────────────────────────────────────────────
|
|
29431
|
+
/** §5.1: the latched server syncEpoch ('' before first server contact). */
|
|
29432
|
+
currentEpoch() {
|
|
29433
|
+
return this.epochLatch ?? "";
|
|
29434
|
+
}
|
|
29435
|
+
/**
|
|
29436
|
+
* §5.1 restore hook (FacadeSession contract): handlers are AWAITED inside
|
|
29437
|
+
* the reseed chain, BEFORE any stream nudge resumes.
|
|
29438
|
+
*/
|
|
29439
|
+
subscribeEpochChange(handler) {
|
|
29440
|
+
this.epochHandlers.add(handler);
|
|
29441
|
+
return () => this.epochHandlers.delete(handler);
|
|
29442
|
+
}
|
|
29028
29443
|
noteEpoch(epoch) {
|
|
29029
29444
|
if (this.epochLatch === epoch) return;
|
|
29030
29445
|
const previous = this.epochLatch;
|
|
@@ -29038,7 +29453,7 @@ var WalletApiSession = class {
|
|
|
29038
29453
|
}
|
|
29039
29454
|
async runReseed(epoch, previous) {
|
|
29040
29455
|
try {
|
|
29041
|
-
|
|
29456
|
+
for (const handler of [...this.epochHandlers]) await handler(epoch);
|
|
29042
29457
|
await this.deps.kv.set(STORE_KEYS.epochLatch, epoch);
|
|
29043
29458
|
} catch {
|
|
29044
29459
|
if (this.epochLatch === epoch) this.epochLatch = previous;
|
|
@@ -29075,10 +29490,16 @@ var WalletApiSession = class {
|
|
|
29075
29490
|
status() {
|
|
29076
29491
|
return this.lastStatus ?? "offline";
|
|
29077
29492
|
}
|
|
29493
|
+
/** Transition feed off the existing emitStatus point (FacadeSession.subscribeStatus). */
|
|
29494
|
+
subscribeStatus(handler) {
|
|
29495
|
+
this.statusHandlers.add(handler);
|
|
29496
|
+
return () => this.statusHandlers.delete(handler);
|
|
29497
|
+
}
|
|
29078
29498
|
emitStatus(status) {
|
|
29079
29499
|
if (this.stopped || this.lastStatus === status) return;
|
|
29080
29500
|
this.lastStatus = status;
|
|
29081
29501
|
this.deps.emitStatus(status);
|
|
29502
|
+
for (const handler of this.statusHandlers) handler(status);
|
|
29082
29503
|
}
|
|
29083
29504
|
async connect() {
|
|
29084
29505
|
if (this.stopped) return;
|
|
@@ -29294,6 +29715,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29294
29715
|
wake;
|
|
29295
29716
|
deriveFn;
|
|
29296
29717
|
seenChain = new SerialChain();
|
|
29718
|
+
lastIncomingEpoch = null;
|
|
29297
29719
|
constructor(config) {
|
|
29298
29720
|
this.client = config.client;
|
|
29299
29721
|
this.kv = config.kv;
|
|
@@ -29388,6 +29810,7 @@ var WalletApiDeliveryPort = class {
|
|
|
29388
29810
|
const seen = await this.readSeen();
|
|
29389
29811
|
let page = await this.client.listMailbox(sinceCursor === void 0 ? 0 : Number(sinceCursor));
|
|
29390
29812
|
for (; ; ) {
|
|
29813
|
+
this.lastIncomingEpoch = String(page.syncEpoch);
|
|
29391
29814
|
for (const entry of page.entries) {
|
|
29392
29815
|
if (entry.status !== "unclaimed") continue;
|
|
29393
29816
|
if (seen.has(entry.entryId)) continue;
|
|
@@ -29397,6 +29820,10 @@ var WalletApiDeliveryPort = class {
|
|
|
29397
29820
|
page = await this.client.listMailbox(page.cursor);
|
|
29398
29821
|
}
|
|
29399
29822
|
}
|
|
29823
|
+
/** §5.7: the page-honest epoch — the restore-detection source Receive keys on. */
|
|
29824
|
+
incomingEpoch() {
|
|
29825
|
+
return this.lastIncomingEpoch;
|
|
29826
|
+
}
|
|
29400
29827
|
toIncoming(entry) {
|
|
29401
29828
|
const bundle = this.openEnvelope(entry);
|
|
29402
29829
|
return {
|
|
@@ -29474,7 +29901,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29474
29901
|
return utf8(`${transferId}:${String(opIndex)}`);
|
|
29475
29902
|
}
|
|
29476
29903
|
async put(transferId, opIndex, bytes) {
|
|
29477
|
-
const {
|
|
29904
|
+
const { kv, fieldKey } = this.config;
|
|
29478
29905
|
const aad = this.aad(transferId, opIndex);
|
|
29479
29906
|
const cacheKey = this.cacheKey(transferId, opIndex);
|
|
29480
29907
|
let envelope = await kv.get(cacheKey);
|
|
@@ -29482,10 +29909,21 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29482
29909
|
envelope = encryptFieldBytes(fieldKey, bytes, aad);
|
|
29483
29910
|
await kv.set(cacheKey, envelope);
|
|
29484
29911
|
}
|
|
29485
|
-
const
|
|
29486
|
-
const record = await client.postProgress(transferId, opIndex, envelope, signature);
|
|
29912
|
+
const record = await this.post(transferId, opIndex, envelope);
|
|
29487
29913
|
return this.open(record.payload, aad);
|
|
29488
29914
|
}
|
|
29915
|
+
/** §5.1 restore (CheckpointReseeder): re-POST the cached ciphertext byte-identical; false = none cached. */
|
|
29916
|
+
async reseedCheckpoint(transferId, opIndex) {
|
|
29917
|
+
const envelope = await this.config.kv.get(this.cacheKey(transferId, opIndex));
|
|
29918
|
+
if (envelope === null) return false;
|
|
29919
|
+
await this.post(transferId, opIndex, envelope);
|
|
29920
|
+
return true;
|
|
29921
|
+
}
|
|
29922
|
+
/** The ONE signed progress-append path put() and reseedCheckpoint() share. */
|
|
29923
|
+
async post(transferId, opIndex, envelope) {
|
|
29924
|
+
const signature = await this.config.signProgress(progressSignMessage(transferId, opIndex, envelope));
|
|
29925
|
+
return this.config.client.postProgress(transferId, opIndex, envelope, signature);
|
|
29926
|
+
}
|
|
29489
29927
|
async get(transferId, opIndex) {
|
|
29490
29928
|
const records = await this.config.client.getProgress(transferId);
|
|
29491
29929
|
const record = records.find((r) => r.opIndex === opIndex);
|
|
@@ -29508,7 +29946,7 @@ var WalletApiSplitCheckpointStore = class {
|
|
|
29508
29946
|
};
|
|
29509
29947
|
|
|
29510
29948
|
// core/payments-v2-wiring.ts
|
|
29511
|
-
function resolvePaymentsV2Composition(walletApi) {
|
|
29949
|
+
function resolvePaymentsV2Composition(walletApi, sphereNetwork) {
|
|
29512
29950
|
const source = walletApi;
|
|
29513
29951
|
const network = typeof source?.network === "string" && source.network !== "" ? source.network : null;
|
|
29514
29952
|
if (!source || network === null) {
|
|
@@ -29517,6 +29955,18 @@ function resolvePaymentsV2Composition(walletApi) {
|
|
|
29517
29955
|
"INVALID_CONFIG"
|
|
29518
29956
|
);
|
|
29519
29957
|
}
|
|
29958
|
+
if (!(network in NETWORKS)) {
|
|
29959
|
+
throw new SphereError(
|
|
29960
|
+
`paymentsV2: walletApi.network "${network}" is not a known network (${Object.keys(NETWORKS).join("/")}) \u2014 a payments composition is single-network (Sphere network "${String(sphereNetwork)}").`,
|
|
29961
|
+
"INVALID_CONFIG"
|
|
29962
|
+
);
|
|
29963
|
+
}
|
|
29964
|
+
if (network !== sphereNetwork) {
|
|
29965
|
+
throw new SphereError(
|
|
29966
|
+
`paymentsV2: walletApi.network "${network}" does not match the Sphere network "${String(sphereNetwork)}" the engine/registry run on \u2014 a payments composition is single-network.`,
|
|
29967
|
+
"INVALID_CONFIG"
|
|
29968
|
+
);
|
|
29969
|
+
}
|
|
29520
29970
|
if (typeof source.paymentsV2Transport === "function") {
|
|
29521
29971
|
return { network, factory: source.paymentsV2Transport.bind(source) };
|
|
29522
29972
|
}
|
|
@@ -29582,8 +30032,7 @@ function defaultPaymentsV2Transport(config) {
|
|
|
29582
30032
|
deviceId: `${config.deviceId}:pv2`,
|
|
29583
30033
|
kv: args.kv,
|
|
29584
30034
|
webSocketFactory,
|
|
29585
|
-
emitStatus: args.emitStatus
|
|
29586
|
-
onEpochChange: args.onEpochChange
|
|
30035
|
+
emitStatus: args.emitStatus
|
|
29587
30036
|
});
|
|
29588
30037
|
return { session, client: authedWireClient(session, client) };
|
|
29589
30038
|
};
|
|
@@ -29653,8 +30102,7 @@ function composePaymentsV2(spec) {
|
|
|
29653
30102
|
identity: identitySlice,
|
|
29654
30103
|
network,
|
|
29655
30104
|
kv,
|
|
29656
|
-
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29657
|
-
onEpochChange: async () => void 0
|
|
30105
|
+
emitStatus: (status) => host.emit("connection:status", { status })
|
|
29658
30106
|
});
|
|
29659
30107
|
const deps = {
|
|
29660
30108
|
session: transport.session,
|
|
@@ -29684,7 +30132,10 @@ function composePaymentsV2(spec) {
|
|
|
29684
30132
|
network,
|
|
29685
30133
|
ownPubkey: identity.chainPubkey,
|
|
29686
30134
|
ownNametag: host.nametag,
|
|
29687
|
-
requestMemo: requestMemoCodec(identity.privateKey)
|
|
30135
|
+
requestMemo: requestMemoCodec(identity.privateKey),
|
|
30136
|
+
// §5.1: the session is the epoch authority — the facade registers its
|
|
30137
|
+
// restore hook on the session itself (FacadeSession.subscribeEpochChange).
|
|
30138
|
+
syncEpoch: () => transport.session.currentEpoch()
|
|
29688
30139
|
};
|
|
29689
30140
|
return new PaymentsFacade(deps);
|
|
29690
30141
|
}
|
|
@@ -30014,18 +30465,20 @@ var Sphere = class _Sphere {
|
|
|
30014
30465
|
}
|
|
30015
30466
|
/**
|
|
30016
30467
|
* P9: validate + resolve the paymentsV2 opt-in BEFORE any storage writes —
|
|
30017
|
-
* fail-closed at init, never a silently-degraded composition.
|
|
30468
|
+
* fail-closed at init, never a silently-degraded composition. Returns the
|
|
30469
|
+
* resolved composition (null = flag off); #728: resolving here also asserts
|
|
30470
|
+
* the single-network invariant (walletApi.network known + equal to the
|
|
30471
|
+
* Sphere network) before any session/KV/provider exists.
|
|
30018
30472
|
*/
|
|
30019
|
-
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi) {
|
|
30020
|
-
if (flag !== true) return
|
|
30473
|
+
static resolvePaymentsV2Flag(flag, accountingConfig, swapConfig, walletApi, network) {
|
|
30474
|
+
if (flag !== true) return null;
|
|
30021
30475
|
if (accountingConfig || swapConfig) {
|
|
30022
30476
|
throw new SphereError(
|
|
30023
30477
|
"paymentsV2 is incompatible with the accounting/swap modules (both are built on the legacy PaymentsModule and are deleted with the P11 flip) \u2014 disable them or the flag.",
|
|
30024
30478
|
"INVALID_CONFIG"
|
|
30025
30479
|
);
|
|
30026
30480
|
}
|
|
30027
|
-
resolvePaymentsV2Composition(walletApi);
|
|
30028
|
-
return true;
|
|
30481
|
+
return resolvePaymentsV2Composition(walletApi, network);
|
|
30029
30482
|
}
|
|
30030
30483
|
/**
|
|
30031
30484
|
* Configure TokenRegistry in the main bundle context.
|
|
@@ -30080,12 +30533,14 @@ var Sphere = class _Sphere {
|
|
|
30080
30533
|
options.walletApi
|
|
30081
30534
|
);
|
|
30082
30535
|
sphere._verification = options.verification;
|
|
30083
|
-
sphere.
|
|
30536
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30084
30537
|
options.paymentsV2,
|
|
30085
30538
|
accountingConfig,
|
|
30086
30539
|
swapConfig,
|
|
30087
|
-
options.walletApi
|
|
30540
|
+
options.walletApi,
|
|
30541
|
+
options.network
|
|
30088
30542
|
);
|
|
30543
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30089
30544
|
sphere._password = options.password ?? null;
|
|
30090
30545
|
sphere._passwordProtected = sphere._password !== null;
|
|
30091
30546
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -30152,12 +30607,14 @@ var Sphere = class _Sphere {
|
|
|
30152
30607
|
options.walletApi
|
|
30153
30608
|
);
|
|
30154
30609
|
sphere._verification = options.verification;
|
|
30155
|
-
sphere.
|
|
30610
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30156
30611
|
options.paymentsV2,
|
|
30157
30612
|
accountingConfig,
|
|
30158
30613
|
swapConfig,
|
|
30159
|
-
options.walletApi
|
|
30614
|
+
options.walletApi,
|
|
30615
|
+
options.network
|
|
30160
30616
|
);
|
|
30617
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30161
30618
|
sphere._password = options.password ?? null;
|
|
30162
30619
|
sphere._passwordProtected = sphere._password !== null;
|
|
30163
30620
|
if (!options.storage.isConnected()) {
|
|
@@ -30231,12 +30688,14 @@ var Sphere = class _Sphere {
|
|
|
30231
30688
|
options.walletApi
|
|
30232
30689
|
);
|
|
30233
30690
|
sphere._verification = options.verification;
|
|
30234
|
-
sphere.
|
|
30691
|
+
sphere._paymentsV2Composition = _Sphere.resolvePaymentsV2Flag(
|
|
30235
30692
|
options.paymentsV2,
|
|
30236
30693
|
accountingConfig,
|
|
30237
30694
|
swapConfig,
|
|
30238
|
-
options.walletApi
|
|
30695
|
+
options.walletApi,
|
|
30696
|
+
options.network
|
|
30239
30697
|
);
|
|
30698
|
+
sphere._paymentsV2Enabled = sphere._paymentsV2Composition !== null;
|
|
30240
30699
|
sphere._password = options.password ?? null;
|
|
30241
30700
|
sphere._passwordProtected = sphere._password !== null;
|
|
30242
30701
|
progress?.({ step: "storing_keys", message: "Storing wallet keys..." });
|
|
@@ -32894,7 +33353,9 @@ var Sphere = class _Sphere {
|
|
|
32894
33353
|
* setOracleApiKey rebuild is what future operations snapshot.
|
|
32895
33354
|
*/
|
|
32896
33355
|
async startPaymentsV2Inner(index, identity) {
|
|
32897
|
-
this._paymentsV2Composition
|
|
33356
|
+
if (this._paymentsV2Composition === null) {
|
|
33357
|
+
throw new SphereError("paymentsV2: composition unresolved \u2014 init never ran the flag gate", "INVALID_CONFIG");
|
|
33358
|
+
}
|
|
32898
33359
|
if (!this._addressModules.get(index)?.tokenEngine) {
|
|
32899
33360
|
throw new SphereError(
|
|
32900
33361
|
"paymentsV2 requires the v2 token engine \u2014 the oracle must supply a trust base + gateway URL (and API key where required).",
|