@unicitylabs/sphere-sdk 0.14.3 → 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 +245 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +245 -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 +245 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +245 -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 +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
package/dist/index.cjs
CHANGED
|
@@ -9346,6 +9346,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
9346
9346
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
9347
9347
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
9348
9348
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
9349
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
9349
9350
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
9350
9351
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
9351
9352
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -9511,6 +9512,86 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9511
9512
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9512
9513
|
}
|
|
9513
9514
|
|
|
9515
|
+
// token-engine/proof-wait.ts
|
|
9516
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9517
|
+
function resolveProofTimeoutMs(configured) {
|
|
9518
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9519
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9520
|
+
throw new SphereError(
|
|
9521
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9522
|
+
"INVALID_CONFIG"
|
|
9523
|
+
);
|
|
9524
|
+
}
|
|
9525
|
+
return configured;
|
|
9526
|
+
}
|
|
9527
|
+
function lateDeliveringSignal(signal) {
|
|
9528
|
+
return new Proxy(signal, {
|
|
9529
|
+
get(target, prop) {
|
|
9530
|
+
if (prop === "addEventListener") {
|
|
9531
|
+
return (type, listener, options) => {
|
|
9532
|
+
if (type === "abort" && target.aborted) {
|
|
9533
|
+
queueMicrotask(() => {
|
|
9534
|
+
listener.call(target, new Event("abort"));
|
|
9535
|
+
});
|
|
9536
|
+
return;
|
|
9537
|
+
}
|
|
9538
|
+
target.addEventListener(type, listener, options);
|
|
9539
|
+
};
|
|
9540
|
+
}
|
|
9541
|
+
const value = Reflect.get(target, prop, target);
|
|
9542
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9543
|
+
}
|
|
9544
|
+
});
|
|
9545
|
+
}
|
|
9546
|
+
function rejectOnAbort(signal) {
|
|
9547
|
+
return new Promise((_resolve, reject) => {
|
|
9548
|
+
if (signal.aborted) {
|
|
9549
|
+
reject(signal.reason);
|
|
9550
|
+
return;
|
|
9551
|
+
}
|
|
9552
|
+
signal.addEventListener(
|
|
9553
|
+
"abort",
|
|
9554
|
+
() => {
|
|
9555
|
+
reject(signal.reason);
|
|
9556
|
+
},
|
|
9557
|
+
{ once: true }
|
|
9558
|
+
);
|
|
9559
|
+
});
|
|
9560
|
+
}
|
|
9561
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9562
|
+
const controller = new AbortController();
|
|
9563
|
+
const relayAbort = () => {
|
|
9564
|
+
controller.abort(external?.reason);
|
|
9565
|
+
};
|
|
9566
|
+
if (external !== void 0) {
|
|
9567
|
+
if (external.aborted) relayAbort();
|
|
9568
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9569
|
+
}
|
|
9570
|
+
const timer = setTimeout(() => {
|
|
9571
|
+
controller.abort(
|
|
9572
|
+
new SphereError(
|
|
9573
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9574
|
+
"AGGREGATOR_ERROR"
|
|
9575
|
+
)
|
|
9576
|
+
);
|
|
9577
|
+
}, deps.timeoutMs);
|
|
9578
|
+
const wait = (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
9579
|
+
deps.client,
|
|
9580
|
+
deps.trustBase,
|
|
9581
|
+
deps.predicateVerifier,
|
|
9582
|
+
transaction,
|
|
9583
|
+
lateDeliveringSignal(controller.signal),
|
|
9584
|
+
deps.intervalMs
|
|
9585
|
+
);
|
|
9586
|
+
wait.catch(() => void 0);
|
|
9587
|
+
try {
|
|
9588
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9589
|
+
} finally {
|
|
9590
|
+
clearTimeout(timer);
|
|
9591
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9592
|
+
}
|
|
9593
|
+
}
|
|
9594
|
+
|
|
9514
9595
|
// token-engine/errors.ts
|
|
9515
9596
|
var TransferConflictError = class extends SphereError {
|
|
9516
9597
|
/**
|
|
@@ -9735,11 +9816,13 @@ var SphereTokenEngine = class {
|
|
|
9735
9816
|
constructor(deps) {
|
|
9736
9817
|
this.deps = deps;
|
|
9737
9818
|
this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
|
|
9819
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9738
9820
|
}
|
|
9739
9821
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9740
9822
|
deterministicMint = true;
|
|
9741
9823
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9742
9824
|
privateKeyHex;
|
|
9825
|
+
proofTimeoutMs;
|
|
9743
9826
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9744
9827
|
getIdentity() {
|
|
9745
9828
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -10082,14 +10165,17 @@ var SphereTokenEngine = class {
|
|
|
10082
10165
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
10083
10166
|
}
|
|
10084
10167
|
try {
|
|
10085
|
-
return await (
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10168
|
+
return await awaitProofBounded(
|
|
10169
|
+
{
|
|
10170
|
+
client: this.deps.client,
|
|
10171
|
+
trustBase: this.deps.trustBase,
|
|
10172
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10173
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10174
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10175
|
+
// #683
|
|
10176
|
+
},
|
|
10089
10177
|
transaction,
|
|
10090
|
-
options?.signal
|
|
10091
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10092
|
-
// #683 finer poll cadence
|
|
10178
|
+
options?.signal
|
|
10093
10179
|
);
|
|
10094
10180
|
} catch (err) {
|
|
10095
10181
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10105,6 +10191,13 @@ var SphereTokenEngine = class {
|
|
|
10105
10191
|
);
|
|
10106
10192
|
}
|
|
10107
10193
|
}
|
|
10194
|
+
/**
|
|
10195
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10196
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10197
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10198
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10199
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10200
|
+
*/
|
|
10108
10201
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
10109
10202
|
assertOwned(token) {
|
|
10110
10203
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10204,6 +10297,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10204
10297
|
networkId: trustBase.networkId,
|
|
10205
10298
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10206
10299
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10300
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10301
|
+
// #739: was declared but never wired
|
|
10207
10302
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10208
10303
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10209
10304
|
};
|
|
@@ -12113,6 +12208,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
12113
12208
|
}
|
|
12114
12209
|
return out;
|
|
12115
12210
|
}
|
|
12211
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12212
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12213
|
+
let complete = true;
|
|
12214
|
+
for (const entry of await stores.backstop.list()) {
|
|
12215
|
+
if (entry.disposition !== "open") continue;
|
|
12216
|
+
let payload = null;
|
|
12217
|
+
try {
|
|
12218
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12219
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12220
|
+
} catch {
|
|
12221
|
+
complete = false;
|
|
12222
|
+
continue;
|
|
12223
|
+
}
|
|
12224
|
+
if (payload === null) {
|
|
12225
|
+
complete = false;
|
|
12226
|
+
continue;
|
|
12227
|
+
}
|
|
12228
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12229
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12230
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12231
|
+
}
|
|
12232
|
+
return { open: holds, complete };
|
|
12233
|
+
}
|
|
12116
12234
|
function groupJournal(journal) {
|
|
12117
12235
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
12118
12236
|
for (const entry of journal) {
|
|
@@ -12523,10 +12641,21 @@ var InventoryView = class {
|
|
|
12523
12641
|
release(tokenId) {
|
|
12524
12642
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12525
12643
|
}
|
|
12644
|
+
pinned(tokenId) {
|
|
12645
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12646
|
+
}
|
|
12647
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12648
|
+
held(tokenId) {
|
|
12649
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12650
|
+
}
|
|
12651
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12652
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12653
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12526
12654
|
pool(coinId) {
|
|
12527
12655
|
const out = [];
|
|
12528
12656
|
for (const [tokenId, entry] of this.mirror) {
|
|
12529
|
-
if (entry.status !== "active"
|
|
12657
|
+
if (entry.status !== "active") continue;
|
|
12658
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12530
12659
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12531
12660
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12532
12661
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12542,7 +12671,7 @@ var InventoryView = class {
|
|
|
12542
12671
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12543
12672
|
out.push(
|
|
12544
12673
|
toToken(tokenId, entry, asset, registry, {
|
|
12545
|
-
transferring: this.
|
|
12674
|
+
transferring: this.held(tokenId),
|
|
12546
12675
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12547
12676
|
})
|
|
12548
12677
|
);
|
|
@@ -12550,7 +12679,7 @@ var InventoryView = class {
|
|
|
12550
12679
|
return out;
|
|
12551
12680
|
}
|
|
12552
12681
|
async assets(registry, price) {
|
|
12553
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12682
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12554
12683
|
if (!price || raw.length === 0) return raw;
|
|
12555
12684
|
return withPrices(raw, registry, price);
|
|
12556
12685
|
}
|
|
@@ -13236,6 +13365,20 @@ var Requests = class {
|
|
|
13236
13365
|
var ReservationLedger = class {
|
|
13237
13366
|
reservations = /* @__PURE__ */ new Map();
|
|
13238
13367
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13368
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13369
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13370
|
+
authoritative = false;
|
|
13371
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13372
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13373
|
+
setAuthoritative(value) {
|
|
13374
|
+
const moved = this.authoritative !== value;
|
|
13375
|
+
this.authoritative = value;
|
|
13376
|
+
return moved;
|
|
13377
|
+
}
|
|
13378
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13379
|
+
unprovenReason() {
|
|
13380
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13381
|
+
}
|
|
13239
13382
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13240
13383
|
reserve(reservationId, entries) {
|
|
13241
13384
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13262,6 +13405,13 @@ var ReservationLedger = class {
|
|
|
13262
13405
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13263
13406
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13264
13407
|
}
|
|
13408
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13409
|
+
holderOf(tokenId) {
|
|
13410
|
+
return this.tokenHolder.get(tokenId);
|
|
13411
|
+
}
|
|
13412
|
+
tokensOf(reservationId) {
|
|
13413
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13414
|
+
}
|
|
13265
13415
|
clear() {
|
|
13266
13416
|
this.reservations.clear();
|
|
13267
13417
|
this.tokenHolder.clear();
|
|
@@ -13274,6 +13424,44 @@ var ReservationLedger = class {
|
|
|
13274
13424
|
}
|
|
13275
13425
|
};
|
|
13276
13426
|
|
|
13427
|
+
// modules/payments-v2/select/pins.ts
|
|
13428
|
+
var IntentPins = class {
|
|
13429
|
+
constructor(deps) {
|
|
13430
|
+
this.deps = deps;
|
|
13431
|
+
}
|
|
13432
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13433
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13434
|
+
adopt(transferId) {
|
|
13435
|
+
this.pinned.add(transferId);
|
|
13436
|
+
}
|
|
13437
|
+
async sync() {
|
|
13438
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13439
|
+
let changed = false;
|
|
13440
|
+
for (const transferId of [...this.pinned]) {
|
|
13441
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13442
|
+
this.pinned.delete(transferId);
|
|
13443
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13444
|
+
this.deps.ledger.cancel(transferId);
|
|
13445
|
+
changed = true;
|
|
13446
|
+
}
|
|
13447
|
+
for (const [transferId, tokenIds] of open) {
|
|
13448
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13449
|
+
this.pinned.add(transferId);
|
|
13450
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13451
|
+
}
|
|
13452
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13453
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13454
|
+
}
|
|
13455
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13456
|
+
pin(transferId, tokenIds) {
|
|
13457
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13458
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13459
|
+
if (free.length === 0) return false;
|
|
13460
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13461
|
+
return true;
|
|
13462
|
+
}
|
|
13463
|
+
};
|
|
13464
|
+
|
|
13277
13465
|
// modules/payments-v2/select/selector.ts
|
|
13278
13466
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13279
13467
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13364,6 +13552,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13364
13552
|
// modules/payments-v2/select/queue.ts
|
|
13365
13553
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13366
13554
|
var QUEUE_MAX_SIZE = 100;
|
|
13555
|
+
function insufficientBalance(view, amount) {
|
|
13556
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13557
|
+
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";
|
|
13558
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13559
|
+
}
|
|
13367
13560
|
var SpendQueue = class {
|
|
13368
13561
|
constructor(deps) {
|
|
13369
13562
|
this.deps = deps;
|
|
@@ -13376,12 +13569,10 @@ var SpendQueue = class {
|
|
|
13376
13569
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13377
13570
|
}
|
|
13378
13571
|
const amount = parseAmount(request.amount);
|
|
13379
|
-
const
|
|
13572
|
+
const view = this.freeView(request.coinId);
|
|
13573
|
+
const { freeView, freeTotal } = view;
|
|
13380
13574
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13381
|
-
throw
|
|
13382
|
-
"Insufficient balance for this transaction",
|
|
13383
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13384
|
-
);
|
|
13575
|
+
throw insufficientBalance(view, amount);
|
|
13385
13576
|
}
|
|
13386
13577
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13387
13578
|
if (plan !== null) {
|
|
@@ -13431,11 +13622,10 @@ var SpendQueue = class {
|
|
|
13431
13622
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13432
13623
|
return "rejected";
|
|
13433
13624
|
}
|
|
13434
|
-
const
|
|
13625
|
+
const view = this.freeView(entry.coinId);
|
|
13626
|
+
const { freeView, freeTotal } = view;
|
|
13435
13627
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13436
|
-
entry.reject(
|
|
13437
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13438
|
-
);
|
|
13628
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13439
13629
|
return "rejected";
|
|
13440
13630
|
}
|
|
13441
13631
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13480,18 +13670,26 @@ var SpendQueue = class {
|
|
|
13480
13670
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13481
13671
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13482
13672
|
}
|
|
13483
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13673
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13674
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13484
13675
|
freeView(coinId) {
|
|
13485
13676
|
const freeView = [];
|
|
13486
13677
|
let freeTotal = 0n;
|
|
13678
|
+
let pinnedTotal = 0n;
|
|
13679
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13680
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13487
13681
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13488
13682
|
if (entry.amount <= 0n) continue;
|
|
13489
|
-
|
|
13683
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13684
|
+
if (holder === void 0) {
|
|
13490
13685
|
freeView.push(entry);
|
|
13491
13686
|
freeTotal += entry.amount;
|
|
13687
|
+
} else {
|
|
13688
|
+
pinnedTotal += entry.amount;
|
|
13689
|
+
holders.add(holder);
|
|
13492
13690
|
}
|
|
13493
13691
|
}
|
|
13494
|
-
return { freeView, freeTotal };
|
|
13692
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13495
13693
|
}
|
|
13496
13694
|
expectedChangeTotal(coinId) {
|
|
13497
13695
|
let total = 0n;
|
|
@@ -13533,13 +13731,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13533
13731
|
}
|
|
13534
13732
|
function composeFacadeParts(deps, hooks) {
|
|
13535
13733
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13734
|
+
const ledger = new ReservationLedger();
|
|
13536
13735
|
const view = new InventoryView({
|
|
13537
13736
|
port: deps.storagePort,
|
|
13538
13737
|
kv: deps.kv,
|
|
13539
13738
|
emit: (event) => deps.emit(event, {}),
|
|
13739
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13740
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13741
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13742
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13540
13743
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13541
13744
|
});
|
|
13542
|
-
const ledger = new ReservationLedger();
|
|
13543
13745
|
const queue = new SpendQueue({
|
|
13544
13746
|
ledger,
|
|
13545
13747
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13560,6 +13762,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13560
13762
|
ownPubkeyBytes,
|
|
13561
13763
|
view,
|
|
13562
13764
|
ledger,
|
|
13765
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13563
13766
|
queue,
|
|
13564
13767
|
historyStore,
|
|
13565
13768
|
machineStores,
|
|
@@ -13571,6 +13774,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13571
13774
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13572
13775
|
};
|
|
13573
13776
|
}
|
|
13777
|
+
function buildPins(deps, hooks, parts) {
|
|
13778
|
+
return new IntentPins({
|
|
13779
|
+
ledger: parts.ledger,
|
|
13780
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13781
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13782
|
+
release: (tokenId) => {
|
|
13783
|
+
parts.view.release(tokenId);
|
|
13784
|
+
},
|
|
13785
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13786
|
+
});
|
|
13787
|
+
}
|
|
13574
13788
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13575
13789
|
return {
|
|
13576
13790
|
stores: machineStores,
|
|
@@ -13680,7 +13894,8 @@ var PaymentsFacade = class {
|
|
|
13680
13894
|
this.deps = deps;
|
|
13681
13895
|
const parts = composeFacadeParts(deps, {
|
|
13682
13896
|
engine: () => this.engine(),
|
|
13683
|
-
send: (request) => this.send(request)
|
|
13897
|
+
send: (request) => this.send(request),
|
|
13898
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13684
13899
|
});
|
|
13685
13900
|
this.view = parts.view;
|
|
13686
13901
|
this.ledger = parts.ledger;
|
|
@@ -13694,6 +13909,7 @@ var PaymentsFacade = class {
|
|
|
13694
13909
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13695
13910
|
this.restoreDeps = parts.restoreDeps;
|
|
13696
13911
|
this.requests = parts.requests;
|
|
13912
|
+
this.pins = parts.pins;
|
|
13697
13913
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13698
13914
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13699
13915
|
now: () => this.nowMs(),
|
|
@@ -13736,6 +13952,8 @@ var PaymentsFacade = class {
|
|
|
13736
13952
|
passChain = new SerialChain();
|
|
13737
13953
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13738
13954
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13955
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13956
|
+
pins;
|
|
13739
13957
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13740
13958
|
async start() {
|
|
13741
13959
|
if (this.started) return;
|
|
@@ -13758,6 +13976,7 @@ var PaymentsFacade = class {
|
|
|
13758
13976
|
});
|
|
13759
13977
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13760
13978
|
await this.deps.session.start();
|
|
13979
|
+
await this.pins.sync().catch(() => void 0);
|
|
13761
13980
|
this.trackTail(this.runConvergencePass());
|
|
13762
13981
|
this.trackTail(this.seedHeldStates());
|
|
13763
13982
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13828,6 +14047,7 @@ var PaymentsFacade = class {
|
|
|
13828
14047
|
}
|
|
13829
14048
|
async convergeBody() {
|
|
13830
14049
|
const outcome = await this.converger.convergeOnce();
|
|
14050
|
+
await this.pins.sync().catch(() => void 0);
|
|
13831
14051
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13832
14052
|
this.heartbeat.settle(outcome, pending);
|
|
13833
14053
|
}
|
|
@@ -14067,6 +14287,7 @@ var PaymentsFacade = class {
|
|
|
14067
14287
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
14068
14288
|
settleKeepOpen(ctx) {
|
|
14069
14289
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14290
|
+
this.pins.adopt(ctx.transferId);
|
|
14070
14291
|
this.heartbeat.arm();
|
|
14071
14292
|
}
|
|
14072
14293
|
settlePartial(ctx, partial) {
|