@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.js
CHANGED
|
@@ -9218,6 +9218,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
9218
9218
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
9219
9219
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
9220
9220
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
9221
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
9221
9222
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
9222
9223
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
9223
9224
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -9383,6 +9384,75 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9383
9384
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9384
9385
|
}
|
|
9385
9386
|
|
|
9387
|
+
// token-engine/proof-wait.ts
|
|
9388
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9389
|
+
function resolveProofTimeoutMs(configured) {
|
|
9390
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9391
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9392
|
+
throw new SphereError(
|
|
9393
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9394
|
+
"INVALID_CONFIG"
|
|
9395
|
+
);
|
|
9396
|
+
}
|
|
9397
|
+
return configured;
|
|
9398
|
+
}
|
|
9399
|
+
var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
9400
|
+
function isTransientGatewayError(err) {
|
|
9401
|
+
return err instanceof JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
|
|
9402
|
+
}
|
|
9403
|
+
function pause(ms, signal) {
|
|
9404
|
+
return new Promise((resolve) => {
|
|
9405
|
+
const timer = setTimeout(resolve, ms);
|
|
9406
|
+
signal.addEventListener(
|
|
9407
|
+
"abort",
|
|
9408
|
+
() => {
|
|
9409
|
+
clearTimeout(timer);
|
|
9410
|
+
resolve();
|
|
9411
|
+
},
|
|
9412
|
+
{ once: true }
|
|
9413
|
+
);
|
|
9414
|
+
});
|
|
9415
|
+
}
|
|
9416
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9417
|
+
const controller = new AbortController();
|
|
9418
|
+
const relayAbort = () => {
|
|
9419
|
+
controller.abort(external?.reason);
|
|
9420
|
+
};
|
|
9421
|
+
if (external !== void 0) {
|
|
9422
|
+
if (external.aborted) relayAbort();
|
|
9423
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9424
|
+
}
|
|
9425
|
+
const timer = setTimeout(() => {
|
|
9426
|
+
controller.abort(
|
|
9427
|
+
new SphereError(
|
|
9428
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9429
|
+
"AGGREGATOR_ERROR"
|
|
9430
|
+
)
|
|
9431
|
+
);
|
|
9432
|
+
}, deps.timeoutMs);
|
|
9433
|
+
try {
|
|
9434
|
+
for (; ; ) {
|
|
9435
|
+
try {
|
|
9436
|
+
return await waitInclusionProof(
|
|
9437
|
+
deps.client,
|
|
9438
|
+
deps.trustBase,
|
|
9439
|
+
deps.predicateVerifier,
|
|
9440
|
+
transaction,
|
|
9441
|
+
controller.signal,
|
|
9442
|
+
deps.intervalMs
|
|
9443
|
+
);
|
|
9444
|
+
} catch (err) {
|
|
9445
|
+
if (controller.signal.aborted) throw err;
|
|
9446
|
+
if (!isTransientGatewayError(err)) throw err;
|
|
9447
|
+
await pause(deps.intervalMs, controller.signal);
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
} finally {
|
|
9451
|
+
clearTimeout(timer);
|
|
9452
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9453
|
+
}
|
|
9454
|
+
}
|
|
9455
|
+
|
|
9386
9456
|
// token-engine/errors.ts
|
|
9387
9457
|
var TransferConflictError = class extends SphereError {
|
|
9388
9458
|
/**
|
|
@@ -9607,11 +9677,13 @@ var SphereTokenEngine = class {
|
|
|
9607
9677
|
constructor(deps) {
|
|
9608
9678
|
this.deps = deps;
|
|
9609
9679
|
this.privateKeyHex = HexConverter.encode(deps.privateKey);
|
|
9680
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9610
9681
|
}
|
|
9611
9682
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9612
9683
|
deterministicMint = true;
|
|
9613
9684
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9614
9685
|
privateKeyHex;
|
|
9686
|
+
proofTimeoutMs;
|
|
9615
9687
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9616
9688
|
getIdentity() {
|
|
9617
9689
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -9954,14 +10026,17 @@ var SphereTokenEngine = class {
|
|
|
9954
10026
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
9955
10027
|
}
|
|
9956
10028
|
try {
|
|
9957
|
-
return await
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
10029
|
+
return await awaitProofBounded(
|
|
10030
|
+
{
|
|
10031
|
+
client: this.deps.client,
|
|
10032
|
+
trustBase: this.deps.trustBase,
|
|
10033
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10034
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10035
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10036
|
+
// #683
|
|
10037
|
+
},
|
|
9961
10038
|
transaction,
|
|
9962
|
-
options?.signal
|
|
9963
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
9964
|
-
// #683 finer poll cadence
|
|
10039
|
+
options?.signal
|
|
9965
10040
|
);
|
|
9966
10041
|
} catch (err) {
|
|
9967
10042
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -9977,6 +10052,13 @@ var SphereTokenEngine = class {
|
|
|
9977
10052
|
);
|
|
9978
10053
|
}
|
|
9979
10054
|
}
|
|
10055
|
+
/**
|
|
10056
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10057
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10058
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10059
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10060
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10061
|
+
*/
|
|
9980
10062
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
9981
10063
|
assertOwned(token) {
|
|
9982
10064
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10076,6 +10158,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10076
10158
|
networkId: trustBase.networkId,
|
|
10077
10159
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10078
10160
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10161
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10162
|
+
// #739: was declared but never wired
|
|
10079
10163
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10080
10164
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10081
10165
|
};
|
|
@@ -11985,6 +12069,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
11985
12069
|
}
|
|
11986
12070
|
return out;
|
|
11987
12071
|
}
|
|
12072
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12073
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12074
|
+
let complete = true;
|
|
12075
|
+
for (const entry of await stores.backstop.list()) {
|
|
12076
|
+
if (entry.disposition !== "open") continue;
|
|
12077
|
+
let payload = null;
|
|
12078
|
+
try {
|
|
12079
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12080
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12081
|
+
} catch {
|
|
12082
|
+
complete = false;
|
|
12083
|
+
continue;
|
|
12084
|
+
}
|
|
12085
|
+
if (payload === null) {
|
|
12086
|
+
complete = false;
|
|
12087
|
+
continue;
|
|
12088
|
+
}
|
|
12089
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12090
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12091
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12092
|
+
}
|
|
12093
|
+
return { open: holds, complete };
|
|
12094
|
+
}
|
|
11988
12095
|
function groupJournal(journal) {
|
|
11989
12096
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
11990
12097
|
for (const entry of journal) {
|
|
@@ -12395,10 +12502,21 @@ var InventoryView = class {
|
|
|
12395
12502
|
release(tokenId) {
|
|
12396
12503
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12397
12504
|
}
|
|
12505
|
+
pinned(tokenId) {
|
|
12506
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12507
|
+
}
|
|
12508
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12509
|
+
held(tokenId) {
|
|
12510
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12511
|
+
}
|
|
12512
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12513
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12514
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12398
12515
|
pool(coinId) {
|
|
12399
12516
|
const out = [];
|
|
12400
12517
|
for (const [tokenId, entry] of this.mirror) {
|
|
12401
|
-
if (entry.status !== "active"
|
|
12518
|
+
if (entry.status !== "active") continue;
|
|
12519
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12402
12520
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12403
12521
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12404
12522
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12414,7 +12532,7 @@ var InventoryView = class {
|
|
|
12414
12532
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12415
12533
|
out.push(
|
|
12416
12534
|
toToken(tokenId, entry, asset, registry, {
|
|
12417
|
-
transferring: this.
|
|
12535
|
+
transferring: this.held(tokenId),
|
|
12418
12536
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12419
12537
|
})
|
|
12420
12538
|
);
|
|
@@ -12422,7 +12540,7 @@ var InventoryView = class {
|
|
|
12422
12540
|
return out;
|
|
12423
12541
|
}
|
|
12424
12542
|
async assets(registry, price) {
|
|
12425
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12543
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12426
12544
|
if (!price || raw.length === 0) return raw;
|
|
12427
12545
|
return withPrices(raw, registry, price);
|
|
12428
12546
|
}
|
|
@@ -13108,6 +13226,20 @@ var Requests = class {
|
|
|
13108
13226
|
var ReservationLedger = class {
|
|
13109
13227
|
reservations = /* @__PURE__ */ new Map();
|
|
13110
13228
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13229
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13230
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13231
|
+
authoritative = false;
|
|
13232
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13233
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13234
|
+
setAuthoritative(value) {
|
|
13235
|
+
const moved = this.authoritative !== value;
|
|
13236
|
+
this.authoritative = value;
|
|
13237
|
+
return moved;
|
|
13238
|
+
}
|
|
13239
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13240
|
+
unprovenReason() {
|
|
13241
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13242
|
+
}
|
|
13111
13243
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13112
13244
|
reserve(reservationId, entries) {
|
|
13113
13245
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13134,6 +13266,13 @@ var ReservationLedger = class {
|
|
|
13134
13266
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13135
13267
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13136
13268
|
}
|
|
13269
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13270
|
+
holderOf(tokenId) {
|
|
13271
|
+
return this.tokenHolder.get(tokenId);
|
|
13272
|
+
}
|
|
13273
|
+
tokensOf(reservationId) {
|
|
13274
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13275
|
+
}
|
|
13137
13276
|
clear() {
|
|
13138
13277
|
this.reservations.clear();
|
|
13139
13278
|
this.tokenHolder.clear();
|
|
@@ -13146,6 +13285,44 @@ var ReservationLedger = class {
|
|
|
13146
13285
|
}
|
|
13147
13286
|
};
|
|
13148
13287
|
|
|
13288
|
+
// modules/payments-v2/select/pins.ts
|
|
13289
|
+
var IntentPins = class {
|
|
13290
|
+
constructor(deps) {
|
|
13291
|
+
this.deps = deps;
|
|
13292
|
+
}
|
|
13293
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13294
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13295
|
+
adopt(transferId) {
|
|
13296
|
+
this.pinned.add(transferId);
|
|
13297
|
+
}
|
|
13298
|
+
async sync() {
|
|
13299
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13300
|
+
let changed = false;
|
|
13301
|
+
for (const transferId of [...this.pinned]) {
|
|
13302
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13303
|
+
this.pinned.delete(transferId);
|
|
13304
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13305
|
+
this.deps.ledger.cancel(transferId);
|
|
13306
|
+
changed = true;
|
|
13307
|
+
}
|
|
13308
|
+
for (const [transferId, tokenIds] of open) {
|
|
13309
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13310
|
+
this.pinned.add(transferId);
|
|
13311
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13312
|
+
}
|
|
13313
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13314
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13315
|
+
}
|
|
13316
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13317
|
+
pin(transferId, tokenIds) {
|
|
13318
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13319
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13320
|
+
if (free.length === 0) return false;
|
|
13321
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13322
|
+
return true;
|
|
13323
|
+
}
|
|
13324
|
+
};
|
|
13325
|
+
|
|
13149
13326
|
// modules/payments-v2/select/selector.ts
|
|
13150
13327
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13151
13328
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13236,6 +13413,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13236
13413
|
// modules/payments-v2/select/queue.ts
|
|
13237
13414
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13238
13415
|
var QUEUE_MAX_SIZE = 100;
|
|
13416
|
+
function insufficientBalance(view, amount) {
|
|
13417
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13418
|
+
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";
|
|
13419
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13420
|
+
}
|
|
13239
13421
|
var SpendQueue = class {
|
|
13240
13422
|
constructor(deps) {
|
|
13241
13423
|
this.deps = deps;
|
|
@@ -13248,12 +13430,10 @@ var SpendQueue = class {
|
|
|
13248
13430
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13249
13431
|
}
|
|
13250
13432
|
const amount = parseAmount(request.amount);
|
|
13251
|
-
const
|
|
13433
|
+
const view = this.freeView(request.coinId);
|
|
13434
|
+
const { freeView, freeTotal } = view;
|
|
13252
13435
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13253
|
-
throw
|
|
13254
|
-
"Insufficient balance for this transaction",
|
|
13255
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13256
|
-
);
|
|
13436
|
+
throw insufficientBalance(view, amount);
|
|
13257
13437
|
}
|
|
13258
13438
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13259
13439
|
if (plan !== null) {
|
|
@@ -13303,11 +13483,10 @@ var SpendQueue = class {
|
|
|
13303
13483
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13304
13484
|
return "rejected";
|
|
13305
13485
|
}
|
|
13306
|
-
const
|
|
13486
|
+
const view = this.freeView(entry.coinId);
|
|
13487
|
+
const { freeView, freeTotal } = view;
|
|
13307
13488
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13308
|
-
entry.reject(
|
|
13309
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13310
|
-
);
|
|
13489
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13311
13490
|
return "rejected";
|
|
13312
13491
|
}
|
|
13313
13492
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13352,18 +13531,26 @@ var SpendQueue = class {
|
|
|
13352
13531
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13353
13532
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13354
13533
|
}
|
|
13355
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13534
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13535
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13356
13536
|
freeView(coinId) {
|
|
13357
13537
|
const freeView = [];
|
|
13358
13538
|
let freeTotal = 0n;
|
|
13539
|
+
let pinnedTotal = 0n;
|
|
13540
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13541
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13359
13542
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13360
13543
|
if (entry.amount <= 0n) continue;
|
|
13361
|
-
|
|
13544
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13545
|
+
if (holder === void 0) {
|
|
13362
13546
|
freeView.push(entry);
|
|
13363
13547
|
freeTotal += entry.amount;
|
|
13548
|
+
} else {
|
|
13549
|
+
pinnedTotal += entry.amount;
|
|
13550
|
+
holders.add(holder);
|
|
13364
13551
|
}
|
|
13365
13552
|
}
|
|
13366
|
-
return { freeView, freeTotal };
|
|
13553
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13367
13554
|
}
|
|
13368
13555
|
expectedChangeTotal(coinId) {
|
|
13369
13556
|
let total = 0n;
|
|
@@ -13405,13 +13592,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13405
13592
|
}
|
|
13406
13593
|
function composeFacadeParts(deps, hooks) {
|
|
13407
13594
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13595
|
+
const ledger = new ReservationLedger();
|
|
13408
13596
|
const view = new InventoryView({
|
|
13409
13597
|
port: deps.storagePort,
|
|
13410
13598
|
kv: deps.kv,
|
|
13411
13599
|
emit: (event) => deps.emit(event, {}),
|
|
13600
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13601
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13602
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13603
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13412
13604
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13413
13605
|
});
|
|
13414
|
-
const ledger = new ReservationLedger();
|
|
13415
13606
|
const queue = new SpendQueue({
|
|
13416
13607
|
ledger,
|
|
13417
13608
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13432,6 +13623,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13432
13623
|
ownPubkeyBytes,
|
|
13433
13624
|
view,
|
|
13434
13625
|
ledger,
|
|
13626
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13435
13627
|
queue,
|
|
13436
13628
|
historyStore,
|
|
13437
13629
|
machineStores,
|
|
@@ -13443,6 +13635,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13443
13635
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13444
13636
|
};
|
|
13445
13637
|
}
|
|
13638
|
+
function buildPins(deps, hooks, parts) {
|
|
13639
|
+
return new IntentPins({
|
|
13640
|
+
ledger: parts.ledger,
|
|
13641
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13642
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13643
|
+
release: (tokenId) => {
|
|
13644
|
+
parts.view.release(tokenId);
|
|
13645
|
+
},
|
|
13646
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13647
|
+
});
|
|
13648
|
+
}
|
|
13446
13649
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13447
13650
|
return {
|
|
13448
13651
|
stores: machineStores,
|
|
@@ -13552,7 +13755,8 @@ var PaymentsFacade = class {
|
|
|
13552
13755
|
this.deps = deps;
|
|
13553
13756
|
const parts = composeFacadeParts(deps, {
|
|
13554
13757
|
engine: () => this.engine(),
|
|
13555
|
-
send: (request) => this.send(request)
|
|
13758
|
+
send: (request) => this.send(request),
|
|
13759
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13556
13760
|
});
|
|
13557
13761
|
this.view = parts.view;
|
|
13558
13762
|
this.ledger = parts.ledger;
|
|
@@ -13566,6 +13770,7 @@ var PaymentsFacade = class {
|
|
|
13566
13770
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13567
13771
|
this.restoreDeps = parts.restoreDeps;
|
|
13568
13772
|
this.requests = parts.requests;
|
|
13773
|
+
this.pins = parts.pins;
|
|
13569
13774
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13570
13775
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13571
13776
|
now: () => this.nowMs(),
|
|
@@ -13608,6 +13813,8 @@ var PaymentsFacade = class {
|
|
|
13608
13813
|
passChain = new SerialChain();
|
|
13609
13814
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13610
13815
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13816
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13817
|
+
pins;
|
|
13611
13818
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13612
13819
|
async start() {
|
|
13613
13820
|
if (this.started) return;
|
|
@@ -13630,6 +13837,7 @@ var PaymentsFacade = class {
|
|
|
13630
13837
|
});
|
|
13631
13838
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13632
13839
|
await this.deps.session.start();
|
|
13840
|
+
await this.pins.sync().catch(() => void 0);
|
|
13633
13841
|
this.trackTail(this.runConvergencePass());
|
|
13634
13842
|
this.trackTail(this.seedHeldStates());
|
|
13635
13843
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13700,6 +13908,7 @@ var PaymentsFacade = class {
|
|
|
13700
13908
|
}
|
|
13701
13909
|
async convergeBody() {
|
|
13702
13910
|
const outcome = await this.converger.convergeOnce();
|
|
13911
|
+
await this.pins.sync().catch(() => void 0);
|
|
13703
13912
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13704
13913
|
this.heartbeat.settle(outcome, pending);
|
|
13705
13914
|
}
|
|
@@ -13939,6 +14148,7 @@ var PaymentsFacade = class {
|
|
|
13939
14148
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13940
14149
|
settleKeepOpen(ctx) {
|
|
13941
14150
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14151
|
+
this.pins.adopt(ctx.transferId);
|
|
13942
14152
|
this.heartbeat.arm();
|
|
13943
14153
|
}
|
|
13944
14154
|
settlePartial(ctx, partial) {
|