@unicitylabs/sphere-sdk 0.14.3 → 0.14.5
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 +234 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +234 -24
- 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 +234 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +234 -24
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +144 -17
- 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 +144 -17
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +91 -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 +91 -7
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1066,6 +1066,8 @@ declare class PaymentsFacade implements PaymentsV2 {
|
|
|
1066
1066
|
private readonly passChain;
|
|
1067
1067
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
1068
1068
|
private readonly activeMoneyOps;
|
|
1069
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
1070
|
+
private readonly pins;
|
|
1069
1071
|
constructor(deps: PaymentsFacadeDeps);
|
|
1070
1072
|
start(): Promise<void>;
|
|
1071
1073
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
@@ -1066,6 +1066,8 @@ declare class PaymentsFacade implements PaymentsV2 {
|
|
|
1066
1066
|
private readonly passChain;
|
|
1067
1067
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
1068
1068
|
private readonly activeMoneyOps;
|
|
1069
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
1070
|
+
private readonly pins;
|
|
1069
1071
|
constructor(deps: PaymentsFacadeDeps);
|
|
1070
1072
|
start(): Promise<void>;
|
|
1071
1073
|
/** §7 same-address restart gate: resolves only after in-flight ops settle. */
|
|
@@ -458,6 +458,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
458
458
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
459
459
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
460
460
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
461
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
461
462
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
462
463
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
463
464
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -1417,6 +1418,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
1417
1418
|
}
|
|
1418
1419
|
return out;
|
|
1419
1420
|
}
|
|
1421
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
1422
|
+
const holds = /* @__PURE__ */ new Map();
|
|
1423
|
+
let complete = true;
|
|
1424
|
+
for (const entry of await stores.backstop.list()) {
|
|
1425
|
+
if (entry.disposition !== "open") continue;
|
|
1426
|
+
let payload = null;
|
|
1427
|
+
try {
|
|
1428
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
1429
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
1430
|
+
} catch {
|
|
1431
|
+
complete = false;
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
if (payload === null) {
|
|
1435
|
+
complete = false;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
1439
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
1440
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
1441
|
+
}
|
|
1442
|
+
return { open: holds, complete };
|
|
1443
|
+
}
|
|
1420
1444
|
function groupJournal(journal) {
|
|
1421
1445
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
1422
1446
|
for (const entry of journal) {
|
|
@@ -2677,10 +2701,21 @@ var InventoryView = class {
|
|
|
2677
2701
|
release(tokenId) {
|
|
2678
2702
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
2679
2703
|
}
|
|
2704
|
+
pinned(tokenId) {
|
|
2705
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2706
|
+
}
|
|
2707
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
2708
|
+
held(tokenId) {
|
|
2709
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
2710
|
+
}
|
|
2711
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
2712
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
2713
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
2680
2714
|
pool(coinId) {
|
|
2681
2715
|
const out = [];
|
|
2682
2716
|
for (const [tokenId, entry] of this.mirror) {
|
|
2683
|
-
if (entry.status !== "active"
|
|
2717
|
+
if (entry.status !== "active") continue;
|
|
2718
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
2684
2719
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
2685
2720
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
2686
2721
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -2696,7 +2731,7 @@ var InventoryView = class {
|
|
|
2696
2731
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
2697
2732
|
out.push(
|
|
2698
2733
|
toToken(tokenId, entry, asset, registry, {
|
|
2699
|
-
transferring: this.
|
|
2734
|
+
transferring: this.held(tokenId),
|
|
2700
2735
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2701
2736
|
})
|
|
2702
2737
|
);
|
|
@@ -2704,7 +2739,7 @@ var InventoryView = class {
|
|
|
2704
2739
|
return out;
|
|
2705
2740
|
}
|
|
2706
2741
|
async assets(registry, price) {
|
|
2707
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
2742
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
2708
2743
|
if (!price || raw.length === 0) return raw;
|
|
2709
2744
|
return withPrices(raw, registry, price);
|
|
2710
2745
|
}
|
|
@@ -3482,6 +3517,20 @@ var Requests = class {
|
|
|
3482
3517
|
var ReservationLedger = class {
|
|
3483
3518
|
reservations = /* @__PURE__ */ new Map();
|
|
3484
3519
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
3520
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
3521
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
3522
|
+
authoritative = false;
|
|
3523
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
3524
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
3525
|
+
setAuthoritative(value) {
|
|
3526
|
+
const moved = this.authoritative !== value;
|
|
3527
|
+
this.authoritative = value;
|
|
3528
|
+
return moved;
|
|
3529
|
+
}
|
|
3530
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
3531
|
+
unprovenReason() {
|
|
3532
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
3533
|
+
}
|
|
3485
3534
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
3486
3535
|
reserve(reservationId, entries) {
|
|
3487
3536
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -3508,6 +3557,13 @@ var ReservationLedger = class {
|
|
|
3508
3557
|
getFreeAmount(tokenId, tokenAmount) {
|
|
3509
3558
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
3510
3559
|
}
|
|
3560
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
3561
|
+
holderOf(tokenId) {
|
|
3562
|
+
return this.tokenHolder.get(tokenId);
|
|
3563
|
+
}
|
|
3564
|
+
tokensOf(reservationId) {
|
|
3565
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
3566
|
+
}
|
|
3511
3567
|
clear() {
|
|
3512
3568
|
this.reservations.clear();
|
|
3513
3569
|
this.tokenHolder.clear();
|
|
@@ -3520,6 +3576,44 @@ var ReservationLedger = class {
|
|
|
3520
3576
|
}
|
|
3521
3577
|
};
|
|
3522
3578
|
|
|
3579
|
+
// modules/payments-v2/select/pins.ts
|
|
3580
|
+
var IntentPins = class {
|
|
3581
|
+
constructor(deps) {
|
|
3582
|
+
this.deps = deps;
|
|
3583
|
+
}
|
|
3584
|
+
pinned = /* @__PURE__ */ new Set();
|
|
3585
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
3586
|
+
adopt(transferId) {
|
|
3587
|
+
this.pinned.add(transferId);
|
|
3588
|
+
}
|
|
3589
|
+
async sync() {
|
|
3590
|
+
const { open, complete } = await this.deps.openIntents();
|
|
3591
|
+
let changed = false;
|
|
3592
|
+
for (const transferId of [...this.pinned]) {
|
|
3593
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
3594
|
+
this.pinned.delete(transferId);
|
|
3595
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
3596
|
+
this.deps.ledger.cancel(transferId);
|
|
3597
|
+
changed = true;
|
|
3598
|
+
}
|
|
3599
|
+
for (const [transferId, tokenIds] of open) {
|
|
3600
|
+
if (this.deps.isActive(transferId)) continue;
|
|
3601
|
+
this.pinned.add(transferId);
|
|
3602
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
3603
|
+
}
|
|
3604
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
3605
|
+
if (changed || gateMoved) this.deps.changed();
|
|
3606
|
+
}
|
|
3607
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
3608
|
+
pin(transferId, tokenIds) {
|
|
3609
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
3610
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
3611
|
+
if (free.length === 0) return false;
|
|
3612
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
3613
|
+
return true;
|
|
3614
|
+
}
|
|
3615
|
+
};
|
|
3616
|
+
|
|
3523
3617
|
// modules/payments-v2/select/selector.ts
|
|
3524
3618
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
3525
3619
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -3610,6 +3704,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
3610
3704
|
// modules/payments-v2/select/queue.ts
|
|
3611
3705
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
3612
3706
|
var QUEUE_MAX_SIZE = 100;
|
|
3707
|
+
function insufficientBalance(view, amount) {
|
|
3708
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
3709
|
+
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";
|
|
3710
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
3711
|
+
}
|
|
3613
3712
|
var SpendQueue = class {
|
|
3614
3713
|
constructor(deps) {
|
|
3615
3714
|
this.deps = deps;
|
|
@@ -3622,12 +3721,10 @@ var SpendQueue = class {
|
|
|
3622
3721
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
3623
3722
|
}
|
|
3624
3723
|
const amount = parseAmount(request.amount);
|
|
3625
|
-
const
|
|
3724
|
+
const view = this.freeView(request.coinId);
|
|
3725
|
+
const { freeView, freeTotal } = view;
|
|
3626
3726
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
3627
|
-
throw
|
|
3628
|
-
"Insufficient balance for this transaction",
|
|
3629
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
3630
|
-
);
|
|
3727
|
+
throw insufficientBalance(view, amount);
|
|
3631
3728
|
}
|
|
3632
3729
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
3633
3730
|
if (plan !== null) {
|
|
@@ -3677,11 +3774,10 @@ var SpendQueue = class {
|
|
|
3677
3774
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3678
3775
|
return "rejected";
|
|
3679
3776
|
}
|
|
3680
|
-
const
|
|
3777
|
+
const view = this.freeView(entry.coinId);
|
|
3778
|
+
const { freeView, freeTotal } = view;
|
|
3681
3779
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
3682
|
-
entry.reject(
|
|
3683
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
3684
|
-
);
|
|
3780
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
3685
3781
|
return "rejected";
|
|
3686
3782
|
}
|
|
3687
3783
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -3726,18 +3822,26 @@ var SpendQueue = class {
|
|
|
3726
3822
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
3727
3823
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
3728
3824
|
}
|
|
3729
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
3825
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
3826
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
3730
3827
|
freeView(coinId) {
|
|
3731
3828
|
const freeView = [];
|
|
3732
3829
|
let freeTotal = 0n;
|
|
3830
|
+
let pinnedTotal = 0n;
|
|
3831
|
+
const holders = /* @__PURE__ */ new Set();
|
|
3832
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
3733
3833
|
for (const entry of this.deps.getPool(coinId)) {
|
|
3734
3834
|
if (entry.amount <= 0n) continue;
|
|
3735
|
-
|
|
3835
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
3836
|
+
if (holder === void 0) {
|
|
3736
3837
|
freeView.push(entry);
|
|
3737
3838
|
freeTotal += entry.amount;
|
|
3839
|
+
} else {
|
|
3840
|
+
pinnedTotal += entry.amount;
|
|
3841
|
+
holders.add(holder);
|
|
3738
3842
|
}
|
|
3739
3843
|
}
|
|
3740
|
-
return { freeView, freeTotal };
|
|
3844
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
3741
3845
|
}
|
|
3742
3846
|
expectedChangeTotal(coinId) {
|
|
3743
3847
|
let total = 0n;
|
|
@@ -3779,13 +3883,17 @@ function supportsDeterministicMint(engine) {
|
|
|
3779
3883
|
}
|
|
3780
3884
|
function composeFacadeParts(deps, hooks) {
|
|
3781
3885
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
3886
|
+
const ledger = new ReservationLedger();
|
|
3782
3887
|
const view = new InventoryView({
|
|
3783
3888
|
port: deps.storagePort,
|
|
3784
3889
|
kv: deps.kv,
|
|
3785
3890
|
emit: (event) => deps.emit(event, {}),
|
|
3891
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
3892
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
3893
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
3894
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
3786
3895
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
3787
3896
|
});
|
|
3788
|
-
const ledger = new ReservationLedger();
|
|
3789
3897
|
const queue = new SpendQueue({
|
|
3790
3898
|
ledger,
|
|
3791
3899
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -3806,6 +3914,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3806
3914
|
ownPubkeyBytes,
|
|
3807
3915
|
view,
|
|
3808
3916
|
ledger,
|
|
3917
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
3809
3918
|
queue,
|
|
3810
3919
|
historyStore,
|
|
3811
3920
|
machineStores,
|
|
@@ -3817,6 +3926,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
3817
3926
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
3818
3927
|
};
|
|
3819
3928
|
}
|
|
3929
|
+
function buildPins(deps, hooks, parts) {
|
|
3930
|
+
return new IntentPins({
|
|
3931
|
+
ledger: parts.ledger,
|
|
3932
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
3933
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
3934
|
+
release: (tokenId) => {
|
|
3935
|
+
parts.view.release(tokenId);
|
|
3936
|
+
},
|
|
3937
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
3938
|
+
});
|
|
3939
|
+
}
|
|
3820
3940
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
3821
3941
|
return {
|
|
3822
3942
|
stores: machineStores,
|
|
@@ -3926,7 +4046,8 @@ var PaymentsFacade = class {
|
|
|
3926
4046
|
this.deps = deps;
|
|
3927
4047
|
const parts = composeFacadeParts(deps, {
|
|
3928
4048
|
engine: () => this.engine(),
|
|
3929
|
-
send: (request) => this.send(request)
|
|
4049
|
+
send: (request) => this.send(request),
|
|
4050
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
3930
4051
|
});
|
|
3931
4052
|
this.view = parts.view;
|
|
3932
4053
|
this.ledger = parts.ledger;
|
|
@@ -3940,6 +4061,7 @@ var PaymentsFacade = class {
|
|
|
3940
4061
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
3941
4062
|
this.restoreDeps = parts.restoreDeps;
|
|
3942
4063
|
this.requests = parts.requests;
|
|
4064
|
+
this.pins = parts.pins;
|
|
3943
4065
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
3944
4066
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
3945
4067
|
now: () => this.nowMs(),
|
|
@@ -3982,6 +4104,8 @@ var PaymentsFacade = class {
|
|
|
3982
4104
|
passChain = new SerialChain();
|
|
3983
4105
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
3984
4106
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
4107
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
4108
|
+
pins;
|
|
3985
4109
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
3986
4110
|
async start() {
|
|
3987
4111
|
if (this.started) return;
|
|
@@ -4004,6 +4128,7 @@ var PaymentsFacade = class {
|
|
|
4004
4128
|
});
|
|
4005
4129
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
4006
4130
|
await this.deps.session.start();
|
|
4131
|
+
await this.pins.sync().catch(() => void 0);
|
|
4007
4132
|
this.trackTail(this.runConvergencePass());
|
|
4008
4133
|
this.trackTail(this.seedHeldStates());
|
|
4009
4134
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -4074,6 +4199,7 @@ var PaymentsFacade = class {
|
|
|
4074
4199
|
}
|
|
4075
4200
|
async convergeBody() {
|
|
4076
4201
|
const outcome = await this.converger.convergeOnce();
|
|
4202
|
+
await this.pins.sync().catch(() => void 0);
|
|
4077
4203
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
4078
4204
|
this.heartbeat.settle(outcome, pending);
|
|
4079
4205
|
}
|
|
@@ -4313,6 +4439,7 @@ var PaymentsFacade = class {
|
|
|
4313
4439
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
4314
4440
|
settleKeepOpen(ctx) {
|
|
4315
4441
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
4442
|
+
this.pins.adopt(ctx.transferId);
|
|
4316
4443
|
this.heartbeat.arm();
|
|
4317
4444
|
}
|
|
4318
4445
|
settlePartial(ctx, partial) {
|