@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
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,75 @@ 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
|
+
var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
9528
|
+
function isTransientGatewayError(err) {
|
|
9529
|
+
return err instanceof import_JsonRpcNetworkError.JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
|
|
9530
|
+
}
|
|
9531
|
+
function pause(ms, signal) {
|
|
9532
|
+
return new Promise((resolve) => {
|
|
9533
|
+
const timer = setTimeout(resolve, ms);
|
|
9534
|
+
signal.addEventListener(
|
|
9535
|
+
"abort",
|
|
9536
|
+
() => {
|
|
9537
|
+
clearTimeout(timer);
|
|
9538
|
+
resolve();
|
|
9539
|
+
},
|
|
9540
|
+
{ once: true }
|
|
9541
|
+
);
|
|
9542
|
+
});
|
|
9543
|
+
}
|
|
9544
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9545
|
+
const controller = new AbortController();
|
|
9546
|
+
const relayAbort = () => {
|
|
9547
|
+
controller.abort(external?.reason);
|
|
9548
|
+
};
|
|
9549
|
+
if (external !== void 0) {
|
|
9550
|
+
if (external.aborted) relayAbort();
|
|
9551
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9552
|
+
}
|
|
9553
|
+
const timer = setTimeout(() => {
|
|
9554
|
+
controller.abort(
|
|
9555
|
+
new SphereError(
|
|
9556
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9557
|
+
"AGGREGATOR_ERROR"
|
|
9558
|
+
)
|
|
9559
|
+
);
|
|
9560
|
+
}, deps.timeoutMs);
|
|
9561
|
+
try {
|
|
9562
|
+
for (; ; ) {
|
|
9563
|
+
try {
|
|
9564
|
+
return await (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
9565
|
+
deps.client,
|
|
9566
|
+
deps.trustBase,
|
|
9567
|
+
deps.predicateVerifier,
|
|
9568
|
+
transaction,
|
|
9569
|
+
controller.signal,
|
|
9570
|
+
deps.intervalMs
|
|
9571
|
+
);
|
|
9572
|
+
} catch (err) {
|
|
9573
|
+
if (controller.signal.aborted) throw err;
|
|
9574
|
+
if (!isTransientGatewayError(err)) throw err;
|
|
9575
|
+
await pause(deps.intervalMs, controller.signal);
|
|
9576
|
+
}
|
|
9577
|
+
}
|
|
9578
|
+
} finally {
|
|
9579
|
+
clearTimeout(timer);
|
|
9580
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9581
|
+
}
|
|
9582
|
+
}
|
|
9583
|
+
|
|
9514
9584
|
// token-engine/errors.ts
|
|
9515
9585
|
var TransferConflictError = class extends SphereError {
|
|
9516
9586
|
/**
|
|
@@ -9735,11 +9805,13 @@ var SphereTokenEngine = class {
|
|
|
9735
9805
|
constructor(deps) {
|
|
9736
9806
|
this.deps = deps;
|
|
9737
9807
|
this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
|
|
9808
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9738
9809
|
}
|
|
9739
9810
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9740
9811
|
deterministicMint = true;
|
|
9741
9812
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9742
9813
|
privateKeyHex;
|
|
9814
|
+
proofTimeoutMs;
|
|
9743
9815
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9744
9816
|
getIdentity() {
|
|
9745
9817
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -10082,14 +10154,17 @@ var SphereTokenEngine = class {
|
|
|
10082
10154
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
10083
10155
|
}
|
|
10084
10156
|
try {
|
|
10085
|
-
return await (
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10157
|
+
return await awaitProofBounded(
|
|
10158
|
+
{
|
|
10159
|
+
client: this.deps.client,
|
|
10160
|
+
trustBase: this.deps.trustBase,
|
|
10161
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10162
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10163
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10164
|
+
// #683
|
|
10165
|
+
},
|
|
10089
10166
|
transaction,
|
|
10090
|
-
options?.signal
|
|
10091
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10092
|
-
// #683 finer poll cadence
|
|
10167
|
+
options?.signal
|
|
10093
10168
|
);
|
|
10094
10169
|
} catch (err) {
|
|
10095
10170
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10105,6 +10180,13 @@ var SphereTokenEngine = class {
|
|
|
10105
10180
|
);
|
|
10106
10181
|
}
|
|
10107
10182
|
}
|
|
10183
|
+
/**
|
|
10184
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10185
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10186
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10187
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10188
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10189
|
+
*/
|
|
10108
10190
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
10109
10191
|
assertOwned(token) {
|
|
10110
10192
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10204,6 +10286,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10204
10286
|
networkId: trustBase.networkId,
|
|
10205
10287
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10206
10288
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10289
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10290
|
+
// #739: was declared but never wired
|
|
10207
10291
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10208
10292
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10209
10293
|
};
|
|
@@ -12113,6 +12197,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
12113
12197
|
}
|
|
12114
12198
|
return out;
|
|
12115
12199
|
}
|
|
12200
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12201
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12202
|
+
let complete = true;
|
|
12203
|
+
for (const entry of await stores.backstop.list()) {
|
|
12204
|
+
if (entry.disposition !== "open") continue;
|
|
12205
|
+
let payload = null;
|
|
12206
|
+
try {
|
|
12207
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12208
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12209
|
+
} catch {
|
|
12210
|
+
complete = false;
|
|
12211
|
+
continue;
|
|
12212
|
+
}
|
|
12213
|
+
if (payload === null) {
|
|
12214
|
+
complete = false;
|
|
12215
|
+
continue;
|
|
12216
|
+
}
|
|
12217
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12218
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12219
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12220
|
+
}
|
|
12221
|
+
return { open: holds, complete };
|
|
12222
|
+
}
|
|
12116
12223
|
function groupJournal(journal) {
|
|
12117
12224
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
12118
12225
|
for (const entry of journal) {
|
|
@@ -12523,10 +12630,21 @@ var InventoryView = class {
|
|
|
12523
12630
|
release(tokenId) {
|
|
12524
12631
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12525
12632
|
}
|
|
12633
|
+
pinned(tokenId) {
|
|
12634
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12635
|
+
}
|
|
12636
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12637
|
+
held(tokenId) {
|
|
12638
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12639
|
+
}
|
|
12640
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12641
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12642
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12526
12643
|
pool(coinId) {
|
|
12527
12644
|
const out = [];
|
|
12528
12645
|
for (const [tokenId, entry] of this.mirror) {
|
|
12529
|
-
if (entry.status !== "active"
|
|
12646
|
+
if (entry.status !== "active") continue;
|
|
12647
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12530
12648
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12531
12649
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12532
12650
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12542,7 +12660,7 @@ var InventoryView = class {
|
|
|
12542
12660
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12543
12661
|
out.push(
|
|
12544
12662
|
toToken(tokenId, entry, asset, registry, {
|
|
12545
|
-
transferring: this.
|
|
12663
|
+
transferring: this.held(tokenId),
|
|
12546
12664
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12547
12665
|
})
|
|
12548
12666
|
);
|
|
@@ -12550,7 +12668,7 @@ var InventoryView = class {
|
|
|
12550
12668
|
return out;
|
|
12551
12669
|
}
|
|
12552
12670
|
async assets(registry, price) {
|
|
12553
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12671
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12554
12672
|
if (!price || raw.length === 0) return raw;
|
|
12555
12673
|
return withPrices(raw, registry, price);
|
|
12556
12674
|
}
|
|
@@ -13236,6 +13354,20 @@ var Requests = class {
|
|
|
13236
13354
|
var ReservationLedger = class {
|
|
13237
13355
|
reservations = /* @__PURE__ */ new Map();
|
|
13238
13356
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13357
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13358
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13359
|
+
authoritative = false;
|
|
13360
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13361
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13362
|
+
setAuthoritative(value) {
|
|
13363
|
+
const moved = this.authoritative !== value;
|
|
13364
|
+
this.authoritative = value;
|
|
13365
|
+
return moved;
|
|
13366
|
+
}
|
|
13367
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13368
|
+
unprovenReason() {
|
|
13369
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13370
|
+
}
|
|
13239
13371
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13240
13372
|
reserve(reservationId, entries) {
|
|
13241
13373
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13262,6 +13394,13 @@ var ReservationLedger = class {
|
|
|
13262
13394
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13263
13395
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13264
13396
|
}
|
|
13397
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13398
|
+
holderOf(tokenId) {
|
|
13399
|
+
return this.tokenHolder.get(tokenId);
|
|
13400
|
+
}
|
|
13401
|
+
tokensOf(reservationId) {
|
|
13402
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13403
|
+
}
|
|
13265
13404
|
clear() {
|
|
13266
13405
|
this.reservations.clear();
|
|
13267
13406
|
this.tokenHolder.clear();
|
|
@@ -13274,6 +13413,44 @@ var ReservationLedger = class {
|
|
|
13274
13413
|
}
|
|
13275
13414
|
};
|
|
13276
13415
|
|
|
13416
|
+
// modules/payments-v2/select/pins.ts
|
|
13417
|
+
var IntentPins = class {
|
|
13418
|
+
constructor(deps) {
|
|
13419
|
+
this.deps = deps;
|
|
13420
|
+
}
|
|
13421
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13422
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13423
|
+
adopt(transferId) {
|
|
13424
|
+
this.pinned.add(transferId);
|
|
13425
|
+
}
|
|
13426
|
+
async sync() {
|
|
13427
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13428
|
+
let changed = false;
|
|
13429
|
+
for (const transferId of [...this.pinned]) {
|
|
13430
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13431
|
+
this.pinned.delete(transferId);
|
|
13432
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13433
|
+
this.deps.ledger.cancel(transferId);
|
|
13434
|
+
changed = true;
|
|
13435
|
+
}
|
|
13436
|
+
for (const [transferId, tokenIds] of open) {
|
|
13437
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13438
|
+
this.pinned.add(transferId);
|
|
13439
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13440
|
+
}
|
|
13441
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13442
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13443
|
+
}
|
|
13444
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13445
|
+
pin(transferId, tokenIds) {
|
|
13446
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13447
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13448
|
+
if (free.length === 0) return false;
|
|
13449
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13450
|
+
return true;
|
|
13451
|
+
}
|
|
13452
|
+
};
|
|
13453
|
+
|
|
13277
13454
|
// modules/payments-v2/select/selector.ts
|
|
13278
13455
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13279
13456
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13364,6 +13541,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13364
13541
|
// modules/payments-v2/select/queue.ts
|
|
13365
13542
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13366
13543
|
var QUEUE_MAX_SIZE = 100;
|
|
13544
|
+
function insufficientBalance(view, amount) {
|
|
13545
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13546
|
+
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";
|
|
13547
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13548
|
+
}
|
|
13367
13549
|
var SpendQueue = class {
|
|
13368
13550
|
constructor(deps) {
|
|
13369
13551
|
this.deps = deps;
|
|
@@ -13376,12 +13558,10 @@ var SpendQueue = class {
|
|
|
13376
13558
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13377
13559
|
}
|
|
13378
13560
|
const amount = parseAmount(request.amount);
|
|
13379
|
-
const
|
|
13561
|
+
const view = this.freeView(request.coinId);
|
|
13562
|
+
const { freeView, freeTotal } = view;
|
|
13380
13563
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13381
|
-
throw
|
|
13382
|
-
"Insufficient balance for this transaction",
|
|
13383
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13384
|
-
);
|
|
13564
|
+
throw insufficientBalance(view, amount);
|
|
13385
13565
|
}
|
|
13386
13566
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13387
13567
|
if (plan !== null) {
|
|
@@ -13431,11 +13611,10 @@ var SpendQueue = class {
|
|
|
13431
13611
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13432
13612
|
return "rejected";
|
|
13433
13613
|
}
|
|
13434
|
-
const
|
|
13614
|
+
const view = this.freeView(entry.coinId);
|
|
13615
|
+
const { freeView, freeTotal } = view;
|
|
13435
13616
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13436
|
-
entry.reject(
|
|
13437
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13438
|
-
);
|
|
13617
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13439
13618
|
return "rejected";
|
|
13440
13619
|
}
|
|
13441
13620
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13480,18 +13659,26 @@ var SpendQueue = class {
|
|
|
13480
13659
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13481
13660
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13482
13661
|
}
|
|
13483
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13662
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13663
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13484
13664
|
freeView(coinId) {
|
|
13485
13665
|
const freeView = [];
|
|
13486
13666
|
let freeTotal = 0n;
|
|
13667
|
+
let pinnedTotal = 0n;
|
|
13668
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13669
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13487
13670
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13488
13671
|
if (entry.amount <= 0n) continue;
|
|
13489
|
-
|
|
13672
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13673
|
+
if (holder === void 0) {
|
|
13490
13674
|
freeView.push(entry);
|
|
13491
13675
|
freeTotal += entry.amount;
|
|
13676
|
+
} else {
|
|
13677
|
+
pinnedTotal += entry.amount;
|
|
13678
|
+
holders.add(holder);
|
|
13492
13679
|
}
|
|
13493
13680
|
}
|
|
13494
|
-
return { freeView, freeTotal };
|
|
13681
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13495
13682
|
}
|
|
13496
13683
|
expectedChangeTotal(coinId) {
|
|
13497
13684
|
let total = 0n;
|
|
@@ -13533,13 +13720,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13533
13720
|
}
|
|
13534
13721
|
function composeFacadeParts(deps, hooks) {
|
|
13535
13722
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13723
|
+
const ledger = new ReservationLedger();
|
|
13536
13724
|
const view = new InventoryView({
|
|
13537
13725
|
port: deps.storagePort,
|
|
13538
13726
|
kv: deps.kv,
|
|
13539
13727
|
emit: (event) => deps.emit(event, {}),
|
|
13728
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13729
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13730
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13731
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13540
13732
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13541
13733
|
});
|
|
13542
|
-
const ledger = new ReservationLedger();
|
|
13543
13734
|
const queue = new SpendQueue({
|
|
13544
13735
|
ledger,
|
|
13545
13736
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13560,6 +13751,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13560
13751
|
ownPubkeyBytes,
|
|
13561
13752
|
view,
|
|
13562
13753
|
ledger,
|
|
13754
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13563
13755
|
queue,
|
|
13564
13756
|
historyStore,
|
|
13565
13757
|
machineStores,
|
|
@@ -13571,6 +13763,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13571
13763
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13572
13764
|
};
|
|
13573
13765
|
}
|
|
13766
|
+
function buildPins(deps, hooks, parts) {
|
|
13767
|
+
return new IntentPins({
|
|
13768
|
+
ledger: parts.ledger,
|
|
13769
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13770
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13771
|
+
release: (tokenId) => {
|
|
13772
|
+
parts.view.release(tokenId);
|
|
13773
|
+
},
|
|
13774
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13775
|
+
});
|
|
13776
|
+
}
|
|
13574
13777
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13575
13778
|
return {
|
|
13576
13779
|
stores: machineStores,
|
|
@@ -13680,7 +13883,8 @@ var PaymentsFacade = class {
|
|
|
13680
13883
|
this.deps = deps;
|
|
13681
13884
|
const parts = composeFacadeParts(deps, {
|
|
13682
13885
|
engine: () => this.engine(),
|
|
13683
|
-
send: (request) => this.send(request)
|
|
13886
|
+
send: (request) => this.send(request),
|
|
13887
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13684
13888
|
});
|
|
13685
13889
|
this.view = parts.view;
|
|
13686
13890
|
this.ledger = parts.ledger;
|
|
@@ -13694,6 +13898,7 @@ var PaymentsFacade = class {
|
|
|
13694
13898
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13695
13899
|
this.restoreDeps = parts.restoreDeps;
|
|
13696
13900
|
this.requests = parts.requests;
|
|
13901
|
+
this.pins = parts.pins;
|
|
13697
13902
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13698
13903
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13699
13904
|
now: () => this.nowMs(),
|
|
@@ -13736,6 +13941,8 @@ var PaymentsFacade = class {
|
|
|
13736
13941
|
passChain = new SerialChain();
|
|
13737
13942
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13738
13943
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13944
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13945
|
+
pins;
|
|
13739
13946
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13740
13947
|
async start() {
|
|
13741
13948
|
if (this.started) return;
|
|
@@ -13758,6 +13965,7 @@ var PaymentsFacade = class {
|
|
|
13758
13965
|
});
|
|
13759
13966
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13760
13967
|
await this.deps.session.start();
|
|
13968
|
+
await this.pins.sync().catch(() => void 0);
|
|
13761
13969
|
this.trackTail(this.runConvergencePass());
|
|
13762
13970
|
this.trackTail(this.seedHeldStates());
|
|
13763
13971
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13828,6 +14036,7 @@ var PaymentsFacade = class {
|
|
|
13828
14036
|
}
|
|
13829
14037
|
async convergeBody() {
|
|
13830
14038
|
const outcome = await this.converger.convergeOnce();
|
|
14039
|
+
await this.pins.sync().catch(() => void 0);
|
|
13831
14040
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13832
14041
|
this.heartbeat.settle(outcome, pending);
|
|
13833
14042
|
}
|
|
@@ -14067,6 +14276,7 @@ var PaymentsFacade = class {
|
|
|
14067
14276
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
14068
14277
|
settleKeepOpen(ctx) {
|
|
14069
14278
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14279
|
+
this.pins.adopt(ctx.transferId);
|
|
14070
14280
|
this.heartbeat.arm();
|
|
14071
14281
|
}
|
|
14072
14282
|
settlePartial(ctx, partial) {
|