@unicitylabs/sphere-sdk 0.14.2 → 0.14.4
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 +251 -27
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +251 -27
- 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 +1 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.js +1 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +251 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +251 -27
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +150 -20
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +2 -0
- package/dist/modules/payments-v2/index.d.ts +2 -0
- package/dist/modules/payments-v2/index.js +150 -20
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +102 -7
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +5 -1
- package/dist/token-engine/index.d.ts +5 -1
- package/dist/token-engine/index.js +102 -7
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -503,6 +503,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
503
503
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
504
504
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
505
505
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
506
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
506
507
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
507
508
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
508
509
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -1462,6 +1463,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
1462
1463
|
}
|
|
1463
1464
|
return out;
|
|
1464
1465
|
}
|
|
1466
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
1467
|
+
const holds = /* @__PURE__ */ new Map();
|
|
1468
|
+
let complete = true;
|
|
1469
|
+
for (const entry of await stores.backstop.list()) {
|
|
1470
|
+
if (entry.disposition !== "open") continue;
|
|
1471
|
+
let payload = null;
|
|
1472
|
+
try {
|
|
1473
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1474
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1475
|
+
} catch {
|
|
1476
|
+
complete = false;
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (payload === null) {
|
|
1480
|
+
complete = false;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
1484
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
1485
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
1486
|
+
}
|
|
1487
|
+
return { open: holds, complete };
|
|
1488
|
+
}
|
|
1465
1489
|
function groupJournal(journal) {
|
|
1466
1490
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
1467
1491
|
for (const entry of journal) {
|
|
@@ -1631,11 +1655,14 @@ function toAsset(coinId, totals, registry) {
|
|
|
1631
1655
|
fiatValueEur: null
|
|
1632
1656
|
};
|
|
1633
1657
|
}
|
|
1658
|
+
function priceIdOf(registry, coinId) {
|
|
1659
|
+
return registry.getName(coinId).toLowerCase();
|
|
1660
|
+
}
|
|
1634
1661
|
async function withPrices(raw, registry, price) {
|
|
1635
1662
|
try {
|
|
1636
|
-
const
|
|
1637
|
-
const quotes = await price.getPrices(
|
|
1638
|
-
return raw.map((a) => priceOne(a, quotes.get(registry
|
|
1663
|
+
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
1664
|
+
const quotes = await price.getPrices(ids);
|
|
1665
|
+
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
1639
1666
|
} catch {
|
|
1640
1667
|
return raw;
|
|
1641
1668
|
}
|
|
@@ -2719,10 +2746,21 @@ var InventoryView = class {
|
|
|
2719
2746
|
release(tokenId) {
|
|
2720
2747
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
2721
2748
|
}
|
|
2749
|
+
pinned(tokenId) {
|
|
2750
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2751
|
+
}
|
|
2752
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
2753
|
+
held(tokenId) {
|
|
2754
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
2755
|
+
}
|
|
2756
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
2757
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
2758
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
2722
2759
|
pool(coinId) {
|
|
2723
2760
|
const out = [];
|
|
2724
2761
|
for (const [tokenId, entry] of this.mirror) {
|
|
2725
|
-
if (entry.status !== "active"
|
|
2762
|
+
if (entry.status !== "active") continue;
|
|
2763
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
2726
2764
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2727
2765
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
2728
2766
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -2738,7 +2776,7 @@ var InventoryView = class {
|
|
|
2738
2776
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
2739
2777
|
out.push(
|
|
2740
2778
|
toToken(tokenId, entry, asset, registry, {
|
|
2741
|
-
transferring: this.
|
|
2779
|
+
transferring: this.held(tokenId),
|
|
2742
2780
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2743
2781
|
})
|
|
2744
2782
|
);
|
|
@@ -2746,7 +2784,7 @@ var InventoryView = class {
|
|
|
2746
2784
|
return out;
|
|
2747
2785
|
}
|
|
2748
2786
|
async assets(registry, price) {
|
|
2749
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
2787
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
2750
2788
|
if (!price || raw.length === 0) return raw;
|
|
2751
2789
|
return withPrices(raw, registry, price);
|
|
2752
2790
|
}
|
|
@@ -3524,6 +3562,20 @@ var Requests = class {
|
|
|
3524
3562
|
var ReservationLedger = class {
|
|
3525
3563
|
reservations = /* @__PURE__ */ new Map();
|
|
3526
3564
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
3565
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
3566
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
3567
|
+
authoritative = false;
|
|
3568
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
3569
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
3570
|
+
setAuthoritative(value) {
|
|
3571
|
+
const moved = this.authoritative !== value;
|
|
3572
|
+
this.authoritative = value;
|
|
3573
|
+
return moved;
|
|
3574
|
+
}
|
|
3575
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
3576
|
+
unprovenReason() {
|
|
3577
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
3578
|
+
}
|
|
3527
3579
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
3528
3580
|
reserve(reservationId, entries) {
|
|
3529
3581
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -3550,6 +3602,13 @@ var ReservationLedger = class {
|
|
|
3550
3602
|
getFreeAmount(tokenId, tokenAmount) {
|
|
3551
3603
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
3552
3604
|
}
|
|
3605
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
3606
|
+
holderOf(tokenId) {
|
|
3607
|
+
return this.tokenHolder.get(tokenId);
|
|
3608
|
+
}
|
|
3609
|
+
tokensOf(reservationId) {
|
|
3610
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
3611
|
+
}
|
|
3553
3612
|
clear() {
|
|
3554
3613
|
this.reservations.clear();
|
|
3555
3614
|
this.tokenHolder.clear();
|
|
@@ -3562,6 +3621,44 @@ var ReservationLedger = class {
|
|
|
3562
3621
|
}
|
|
3563
3622
|
};
|
|
3564
3623
|
|
|
3624
|
+
// modules/payments-v2/select/pins.ts
|
|
3625
|
+
var IntentPins = class {
|
|
3626
|
+
constructor(deps) {
|
|
3627
|
+
this.deps = deps;
|
|
3628
|
+
}
|
|
3629
|
+
pinned = /* @__PURE__ */ new Set();
|
|
3630
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
3631
|
+
adopt(transferId) {
|
|
3632
|
+
this.pinned.add(transferId);
|
|
3633
|
+
}
|
|
3634
|
+
async sync() {
|
|
3635
|
+
const { open, complete } = await this.deps.openIntents();
|
|
3636
|
+
let changed = false;
|
|
3637
|
+
for (const transferId of [...this.pinned]) {
|
|
3638
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
3639
|
+
this.pinned.delete(transferId);
|
|
3640
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
3641
|
+
this.deps.ledger.cancel(transferId);
|
|
3642
|
+
changed = true;
|
|
3643
|
+
}
|
|
3644
|
+
for (const [transferId, tokenIds] of open) {
|
|
3645
|
+
if (this.deps.isActive(transferId)) continue;
|
|
3646
|
+
this.pinned.add(transferId);
|
|
3647
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
3648
|
+
}
|
|
3649
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
3650
|
+
if (changed || gateMoved) this.deps.changed();
|
|
3651
|
+
}
|
|
3652
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
3653
|
+
pin(transferId, tokenIds) {
|
|
3654
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
3655
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
3656
|
+
if (free.length === 0) return false;
|
|
3657
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
3658
|
+
return true;
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
|
|
3565
3662
|
// modules/payments-v2/select/selector.ts
|
|
3566
3663
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
3567
3664
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -3652,6 +3749,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
3652
3749
|
// modules/payments-v2/select/queue.ts
|
|
3653
3750
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
3654
3751
|
var QUEUE_MAX_SIZE = 100;
|
|
3752
|
+
function insufficientBalance(view, amount) {
|
|
3753
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
3754
|
+
const message = view.pinnedTotal > 0n ? `Insufficient spendable balance: need ${amount.toString()}, ${view.freeTotal.toString()} free, ${view.pinnedTotal.toString()} pinned by ${String(view.pinnedBy)} transfer(s) still converging \u2014 see payments.pendingTransfers(); do not re-send.` : "Insufficient balance for this transaction";
|
|
3755
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
3756
|
+
}
|
|
3655
3757
|
var SpendQueue = class {
|
|
3656
3758
|
constructor(deps) {
|
|
3657
3759
|
this.deps = deps;
|
|
@@ -3664,12 +3766,10 @@ var SpendQueue = class {
|
|
|
3664
3766
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
3665
3767
|
}
|
|
3666
3768
|
const amount = parseAmount(request.amount);
|
|
3667
|
-
const
|
|
3769
|
+
const view = this.freeView(request.coinId);
|
|
3770
|
+
const { freeView, freeTotal } = view;
|
|
3668
3771
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
3669
|
-
throw
|
|
3670
|
-
"Insufficient balance for this transaction",
|
|
3671
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
3672
|
-
);
|
|
3772
|
+
throw insufficientBalance(view, amount);
|
|
3673
3773
|
}
|
|
3674
3774
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
3675
3775
|
if (plan !== null) {
|
|
@@ -3719,11 +3819,10 @@ var SpendQueue = class {
|
|
|
3719
3819
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3720
3820
|
return "rejected";
|
|
3721
3821
|
}
|
|
3722
|
-
const
|
|
3822
|
+
const view = this.freeView(entry.coinId);
|
|
3823
|
+
const { freeView, freeTotal } = view;
|
|
3723
3824
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
3724
|
-
entry.reject(
|
|
3725
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
3726
|
-
);
|
|
3825
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
3727
3826
|
return "rejected";
|
|
3728
3827
|
}
|
|
3729
3828
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -3768,18 +3867,26 @@ var SpendQueue = class {
|
|
|
3768
3867
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3769
3868
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
3770
3869
|
}
|
|
3771
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
3870
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
3871
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
3772
3872
|
freeView(coinId) {
|
|
3773
3873
|
const freeView = [];
|
|
3774
3874
|
let freeTotal = 0n;
|
|
3875
|
+
let pinnedTotal = 0n;
|
|
3876
|
+
const holders = /* @__PURE__ */ new Set();
|
|
3877
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
3775
3878
|
for (const entry of this.deps.getPool(coinId)) {
|
|
3776
3879
|
if (entry.amount <= 0n) continue;
|
|
3777
|
-
|
|
3880
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
3881
|
+
if (holder === void 0) {
|
|
3778
3882
|
freeView.push(entry);
|
|
3779
3883
|
freeTotal += entry.amount;
|
|
3884
|
+
} else {
|
|
3885
|
+
pinnedTotal += entry.amount;
|
|
3886
|
+
holders.add(holder);
|
|
3780
3887
|
}
|
|
3781
3888
|
}
|
|
3782
|
-
return { freeView, freeTotal };
|
|
3889
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
3783
3890
|
}
|
|
3784
3891
|
expectedChangeTotal(coinId) {
|
|
3785
3892
|
let total = 0n;
|
|
@@ -3821,13 +3928,17 @@ function supportsDeterministicMint(engine) {
|
|
|
3821
3928
|
}
|
|
3822
3929
|
function composeFacadeParts(deps, hooks) {
|
|
3823
3930
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
3931
|
+
const ledger = new ReservationLedger();
|
|
3824
3932
|
const view = new InventoryView({
|
|
3825
3933
|
port: deps.storagePort,
|
|
3826
3934
|
kv: deps.kv,
|
|
3827
3935
|
emit: (event) => deps.emit(event, {}),
|
|
3936
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
3937
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
3938
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
3939
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
3828
3940
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3829
3941
|
});
|
|
3830
|
-
const ledger = new ReservationLedger();
|
|
3831
3942
|
const queue = new SpendQueue({
|
|
3832
3943
|
ledger,
|
|
3833
3944
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -3848,6 +3959,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3848
3959
|
ownPubkeyBytes,
|
|
3849
3960
|
view,
|
|
3850
3961
|
ledger,
|
|
3962
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
3851
3963
|
queue,
|
|
3852
3964
|
historyStore,
|
|
3853
3965
|
machineStores,
|
|
@@ -3859,6 +3971,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3859
3971
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3860
3972
|
};
|
|
3861
3973
|
}
|
|
3974
|
+
function buildPins(deps, hooks, parts) {
|
|
3975
|
+
return new IntentPins({
|
|
3976
|
+
ledger: parts.ledger,
|
|
3977
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
3978
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
3979
|
+
release: (tokenId) => {
|
|
3980
|
+
parts.view.release(tokenId);
|
|
3981
|
+
},
|
|
3982
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
3983
|
+
});
|
|
3984
|
+
}
|
|
3862
3985
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3863
3986
|
return {
|
|
3864
3987
|
stores: machineStores,
|
|
@@ -3968,7 +4091,8 @@ var PaymentsFacade = class {
|
|
|
3968
4091
|
this.deps = deps;
|
|
3969
4092
|
const parts = composeFacadeParts(deps, {
|
|
3970
4093
|
engine: () => this.engine(),
|
|
3971
|
-
send: (request) => this.send(request)
|
|
4094
|
+
send: (request) => this.send(request),
|
|
4095
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
3972
4096
|
});
|
|
3973
4097
|
this.view = parts.view;
|
|
3974
4098
|
this.ledger = parts.ledger;
|
|
@@ -3982,6 +4106,7 @@ var PaymentsFacade = class {
|
|
|
3982
4106
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3983
4107
|
this.restoreDeps = parts.restoreDeps;
|
|
3984
4108
|
this.requests = parts.requests;
|
|
4109
|
+
this.pins = parts.pins;
|
|
3985
4110
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3986
4111
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
3987
4112
|
now: () => this.nowMs(),
|
|
@@ -4024,6 +4149,8 @@ var PaymentsFacade = class {
|
|
|
4024
4149
|
passChain = new SerialChain();
|
|
4025
4150
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
4026
4151
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
4152
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
4153
|
+
pins;
|
|
4027
4154
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
4028
4155
|
async start() {
|
|
4029
4156
|
if (this.started) return;
|
|
@@ -4046,6 +4173,7 @@ var PaymentsFacade = class {
|
|
|
4046
4173
|
});
|
|
4047
4174
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4048
4175
|
await this.deps.session.start();
|
|
4176
|
+
await this.pins.sync().catch(() => void 0);
|
|
4049
4177
|
this.trackTail(this.runConvergencePass());
|
|
4050
4178
|
this.trackTail(this.seedHeldStates());
|
|
4051
4179
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -4116,6 +4244,7 @@ var PaymentsFacade = class {
|
|
|
4116
4244
|
}
|
|
4117
4245
|
async convergeBody() {
|
|
4118
4246
|
const outcome = await this.converger.convergeOnce();
|
|
4247
|
+
await this.pins.sync().catch(() => void 0);
|
|
4119
4248
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4120
4249
|
this.heartbeat.settle(outcome, pending);
|
|
4121
4250
|
}
|
|
@@ -4355,6 +4484,7 @@ var PaymentsFacade = class {
|
|
|
4355
4484
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
4356
4485
|
settleKeepOpen(ctx) {
|
|
4357
4486
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4487
|
+
this.pins.adopt(ctx.transferId);
|
|
4358
4488
|
this.heartbeat.arm();
|
|
4359
4489
|
}
|
|
4360
4490
|
settlePartial(ctx, partial) {
|