@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/core/index.js
CHANGED
|
@@ -9202,6 +9202,7 @@ import { CertificationResponse, CertificationStatus } from "@unicitylabs/state-t
|
|
|
9202
9202
|
import { StateId } from "@unicitylabs/state-transition-sdk/lib/api/StateId.js";
|
|
9203
9203
|
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js";
|
|
9204
9204
|
import { InclusionProofResponse } from "@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js";
|
|
9205
|
+
import { JsonRpcNetworkError } from "@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js";
|
|
9205
9206
|
import { RootTrustBase } from "@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js";
|
|
9206
9207
|
import { waitInclusionProof } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js";
|
|
9207
9208
|
import { InclusionProofVerificationStatus } from "@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js";
|
|
@@ -9367,6 +9368,75 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9367
9368
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9368
9369
|
}
|
|
9369
9370
|
|
|
9371
|
+
// token-engine/proof-wait.ts
|
|
9372
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9373
|
+
function resolveProofTimeoutMs(configured) {
|
|
9374
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9375
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9376
|
+
throw new SphereError(
|
|
9377
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9378
|
+
"INVALID_CONFIG"
|
|
9379
|
+
);
|
|
9380
|
+
}
|
|
9381
|
+
return configured;
|
|
9382
|
+
}
|
|
9383
|
+
var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
9384
|
+
function isTransientGatewayError(err) {
|
|
9385
|
+
return err instanceof JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
|
|
9386
|
+
}
|
|
9387
|
+
function pause(ms, signal) {
|
|
9388
|
+
return new Promise((resolve) => {
|
|
9389
|
+
const timer = setTimeout(resolve, ms);
|
|
9390
|
+
signal.addEventListener(
|
|
9391
|
+
"abort",
|
|
9392
|
+
() => {
|
|
9393
|
+
clearTimeout(timer);
|
|
9394
|
+
resolve();
|
|
9395
|
+
},
|
|
9396
|
+
{ once: true }
|
|
9397
|
+
);
|
|
9398
|
+
});
|
|
9399
|
+
}
|
|
9400
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9401
|
+
const controller = new AbortController();
|
|
9402
|
+
const relayAbort = () => {
|
|
9403
|
+
controller.abort(external?.reason);
|
|
9404
|
+
};
|
|
9405
|
+
if (external !== void 0) {
|
|
9406
|
+
if (external.aborted) relayAbort();
|
|
9407
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9408
|
+
}
|
|
9409
|
+
const timer = setTimeout(() => {
|
|
9410
|
+
controller.abort(
|
|
9411
|
+
new SphereError(
|
|
9412
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9413
|
+
"AGGREGATOR_ERROR"
|
|
9414
|
+
)
|
|
9415
|
+
);
|
|
9416
|
+
}, deps.timeoutMs);
|
|
9417
|
+
try {
|
|
9418
|
+
for (; ; ) {
|
|
9419
|
+
try {
|
|
9420
|
+
return await waitInclusionProof(
|
|
9421
|
+
deps.client,
|
|
9422
|
+
deps.trustBase,
|
|
9423
|
+
deps.predicateVerifier,
|
|
9424
|
+
transaction,
|
|
9425
|
+
controller.signal,
|
|
9426
|
+
deps.intervalMs
|
|
9427
|
+
);
|
|
9428
|
+
} catch (err) {
|
|
9429
|
+
if (controller.signal.aborted) throw err;
|
|
9430
|
+
if (!isTransientGatewayError(err)) throw err;
|
|
9431
|
+
await pause(deps.intervalMs, controller.signal);
|
|
9432
|
+
}
|
|
9433
|
+
}
|
|
9434
|
+
} finally {
|
|
9435
|
+
clearTimeout(timer);
|
|
9436
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9437
|
+
}
|
|
9438
|
+
}
|
|
9439
|
+
|
|
9370
9440
|
// token-engine/errors.ts
|
|
9371
9441
|
var TransferConflictError = class extends SphereError {
|
|
9372
9442
|
/**
|
|
@@ -9591,11 +9661,13 @@ var SphereTokenEngine = class {
|
|
|
9591
9661
|
constructor(deps) {
|
|
9592
9662
|
this.deps = deps;
|
|
9593
9663
|
this.privateKeyHex = HexConverter.encode(deps.privateKey);
|
|
9664
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9594
9665
|
}
|
|
9595
9666
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9596
9667
|
deterministicMint = true;
|
|
9597
9668
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9598
9669
|
privateKeyHex;
|
|
9670
|
+
proofTimeoutMs;
|
|
9599
9671
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9600
9672
|
getIdentity() {
|
|
9601
9673
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -9938,14 +10010,17 @@ var SphereTokenEngine = class {
|
|
|
9938
10010
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
9939
10011
|
}
|
|
9940
10012
|
try {
|
|
9941
|
-
return await
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
10013
|
+
return await awaitProofBounded(
|
|
10014
|
+
{
|
|
10015
|
+
client: this.deps.client,
|
|
10016
|
+
trustBase: this.deps.trustBase,
|
|
10017
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10018
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10019
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10020
|
+
// #683
|
|
10021
|
+
},
|
|
9945
10022
|
transaction,
|
|
9946
|
-
options?.signal
|
|
9947
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
9948
|
-
// #683 finer poll cadence
|
|
10023
|
+
options?.signal
|
|
9949
10024
|
);
|
|
9950
10025
|
} catch (err) {
|
|
9951
10026
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -9961,6 +10036,13 @@ var SphereTokenEngine = class {
|
|
|
9961
10036
|
);
|
|
9962
10037
|
}
|
|
9963
10038
|
}
|
|
10039
|
+
/**
|
|
10040
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10041
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10042
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10043
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10044
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10045
|
+
*/
|
|
9964
10046
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
9965
10047
|
assertOwned(token) {
|
|
9966
10048
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10060,6 +10142,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10060
10142
|
networkId: trustBase.networkId,
|
|
10061
10143
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10062
10144
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10145
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10146
|
+
// #739: was declared but never wired
|
|
10063
10147
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10064
10148
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10065
10149
|
};
|
|
@@ -11969,6 +12053,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
11969
12053
|
}
|
|
11970
12054
|
return out;
|
|
11971
12055
|
}
|
|
12056
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12057
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12058
|
+
let complete = true;
|
|
12059
|
+
for (const entry of await stores.backstop.list()) {
|
|
12060
|
+
if (entry.disposition !== "open") continue;
|
|
12061
|
+
let payload = null;
|
|
12062
|
+
try {
|
|
12063
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12064
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12065
|
+
} catch {
|
|
12066
|
+
complete = false;
|
|
12067
|
+
continue;
|
|
12068
|
+
}
|
|
12069
|
+
if (payload === null) {
|
|
12070
|
+
complete = false;
|
|
12071
|
+
continue;
|
|
12072
|
+
}
|
|
12073
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12074
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12075
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12076
|
+
}
|
|
12077
|
+
return { open: holds, complete };
|
|
12078
|
+
}
|
|
11972
12079
|
function groupJournal(journal) {
|
|
11973
12080
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
11974
12081
|
for (const entry of journal) {
|
|
@@ -12379,10 +12486,21 @@ var InventoryView = class {
|
|
|
12379
12486
|
release(tokenId) {
|
|
12380
12487
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12381
12488
|
}
|
|
12489
|
+
pinned(tokenId) {
|
|
12490
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12491
|
+
}
|
|
12492
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12493
|
+
held(tokenId) {
|
|
12494
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12495
|
+
}
|
|
12496
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12497
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12498
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12382
12499
|
pool(coinId) {
|
|
12383
12500
|
const out = [];
|
|
12384
12501
|
for (const [tokenId, entry] of this.mirror) {
|
|
12385
|
-
if (entry.status !== "active"
|
|
12502
|
+
if (entry.status !== "active") continue;
|
|
12503
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12386
12504
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12387
12505
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12388
12506
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12398,7 +12516,7 @@ var InventoryView = class {
|
|
|
12398
12516
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12399
12517
|
out.push(
|
|
12400
12518
|
toToken(tokenId, entry, asset, registry, {
|
|
12401
|
-
transferring: this.
|
|
12519
|
+
transferring: this.held(tokenId),
|
|
12402
12520
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12403
12521
|
})
|
|
12404
12522
|
);
|
|
@@ -12406,7 +12524,7 @@ var InventoryView = class {
|
|
|
12406
12524
|
return out;
|
|
12407
12525
|
}
|
|
12408
12526
|
async assets(registry, price) {
|
|
12409
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12527
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12410
12528
|
if (!price || raw.length === 0) return raw;
|
|
12411
12529
|
return withPrices(raw, registry, price);
|
|
12412
12530
|
}
|
|
@@ -13092,6 +13210,20 @@ var Requests = class {
|
|
|
13092
13210
|
var ReservationLedger = class {
|
|
13093
13211
|
reservations = /* @__PURE__ */ new Map();
|
|
13094
13212
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13213
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13214
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13215
|
+
authoritative = false;
|
|
13216
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13217
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13218
|
+
setAuthoritative(value) {
|
|
13219
|
+
const moved = this.authoritative !== value;
|
|
13220
|
+
this.authoritative = value;
|
|
13221
|
+
return moved;
|
|
13222
|
+
}
|
|
13223
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13224
|
+
unprovenReason() {
|
|
13225
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13226
|
+
}
|
|
13095
13227
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13096
13228
|
reserve(reservationId, entries) {
|
|
13097
13229
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13118,6 +13250,13 @@ var ReservationLedger = class {
|
|
|
13118
13250
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13119
13251
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13120
13252
|
}
|
|
13253
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13254
|
+
holderOf(tokenId) {
|
|
13255
|
+
return this.tokenHolder.get(tokenId);
|
|
13256
|
+
}
|
|
13257
|
+
tokensOf(reservationId) {
|
|
13258
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13259
|
+
}
|
|
13121
13260
|
clear() {
|
|
13122
13261
|
this.reservations.clear();
|
|
13123
13262
|
this.tokenHolder.clear();
|
|
@@ -13130,6 +13269,44 @@ var ReservationLedger = class {
|
|
|
13130
13269
|
}
|
|
13131
13270
|
};
|
|
13132
13271
|
|
|
13272
|
+
// modules/payments-v2/select/pins.ts
|
|
13273
|
+
var IntentPins = class {
|
|
13274
|
+
constructor(deps) {
|
|
13275
|
+
this.deps = deps;
|
|
13276
|
+
}
|
|
13277
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13278
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13279
|
+
adopt(transferId) {
|
|
13280
|
+
this.pinned.add(transferId);
|
|
13281
|
+
}
|
|
13282
|
+
async sync() {
|
|
13283
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13284
|
+
let changed = false;
|
|
13285
|
+
for (const transferId of [...this.pinned]) {
|
|
13286
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13287
|
+
this.pinned.delete(transferId);
|
|
13288
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13289
|
+
this.deps.ledger.cancel(transferId);
|
|
13290
|
+
changed = true;
|
|
13291
|
+
}
|
|
13292
|
+
for (const [transferId, tokenIds] of open) {
|
|
13293
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13294
|
+
this.pinned.add(transferId);
|
|
13295
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13296
|
+
}
|
|
13297
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13298
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13299
|
+
}
|
|
13300
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13301
|
+
pin(transferId, tokenIds) {
|
|
13302
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13303
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13304
|
+
if (free.length === 0) return false;
|
|
13305
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13306
|
+
return true;
|
|
13307
|
+
}
|
|
13308
|
+
};
|
|
13309
|
+
|
|
13133
13310
|
// modules/payments-v2/select/selector.ts
|
|
13134
13311
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13135
13312
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13220,6 +13397,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13220
13397
|
// modules/payments-v2/select/queue.ts
|
|
13221
13398
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13222
13399
|
var QUEUE_MAX_SIZE = 100;
|
|
13400
|
+
function insufficientBalance(view, amount) {
|
|
13401
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13402
|
+
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";
|
|
13403
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13404
|
+
}
|
|
13223
13405
|
var SpendQueue = class {
|
|
13224
13406
|
constructor(deps) {
|
|
13225
13407
|
this.deps = deps;
|
|
@@ -13232,12 +13414,10 @@ var SpendQueue = class {
|
|
|
13232
13414
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13233
13415
|
}
|
|
13234
13416
|
const amount = parseAmount(request.amount);
|
|
13235
|
-
const
|
|
13417
|
+
const view = this.freeView(request.coinId);
|
|
13418
|
+
const { freeView, freeTotal } = view;
|
|
13236
13419
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13237
|
-
throw
|
|
13238
|
-
"Insufficient balance for this transaction",
|
|
13239
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13240
|
-
);
|
|
13420
|
+
throw insufficientBalance(view, amount);
|
|
13241
13421
|
}
|
|
13242
13422
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13243
13423
|
if (plan !== null) {
|
|
@@ -13287,11 +13467,10 @@ var SpendQueue = class {
|
|
|
13287
13467
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13288
13468
|
return "rejected";
|
|
13289
13469
|
}
|
|
13290
|
-
const
|
|
13470
|
+
const view = this.freeView(entry.coinId);
|
|
13471
|
+
const { freeView, freeTotal } = view;
|
|
13291
13472
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13292
|
-
entry.reject(
|
|
13293
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13294
|
-
);
|
|
13473
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13295
13474
|
return "rejected";
|
|
13296
13475
|
}
|
|
13297
13476
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13336,18 +13515,26 @@ var SpendQueue = class {
|
|
|
13336
13515
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13337
13516
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13338
13517
|
}
|
|
13339
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13518
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13519
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13340
13520
|
freeView(coinId) {
|
|
13341
13521
|
const freeView = [];
|
|
13342
13522
|
let freeTotal = 0n;
|
|
13523
|
+
let pinnedTotal = 0n;
|
|
13524
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13525
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13343
13526
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13344
13527
|
if (entry.amount <= 0n) continue;
|
|
13345
|
-
|
|
13528
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13529
|
+
if (holder === void 0) {
|
|
13346
13530
|
freeView.push(entry);
|
|
13347
13531
|
freeTotal += entry.amount;
|
|
13532
|
+
} else {
|
|
13533
|
+
pinnedTotal += entry.amount;
|
|
13534
|
+
holders.add(holder);
|
|
13348
13535
|
}
|
|
13349
13536
|
}
|
|
13350
|
-
return { freeView, freeTotal };
|
|
13537
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13351
13538
|
}
|
|
13352
13539
|
expectedChangeTotal(coinId) {
|
|
13353
13540
|
let total = 0n;
|
|
@@ -13389,13 +13576,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13389
13576
|
}
|
|
13390
13577
|
function composeFacadeParts(deps, hooks) {
|
|
13391
13578
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13579
|
+
const ledger = new ReservationLedger();
|
|
13392
13580
|
const view = new InventoryView({
|
|
13393
13581
|
port: deps.storagePort,
|
|
13394
13582
|
kv: deps.kv,
|
|
13395
13583
|
emit: (event) => deps.emit(event, {}),
|
|
13584
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13585
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13586
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13587
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13396
13588
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13397
13589
|
});
|
|
13398
|
-
const ledger = new ReservationLedger();
|
|
13399
13590
|
const queue = new SpendQueue({
|
|
13400
13591
|
ledger,
|
|
13401
13592
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13416,6 +13607,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13416
13607
|
ownPubkeyBytes,
|
|
13417
13608
|
view,
|
|
13418
13609
|
ledger,
|
|
13610
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13419
13611
|
queue,
|
|
13420
13612
|
historyStore,
|
|
13421
13613
|
machineStores,
|
|
@@ -13427,6 +13619,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13427
13619
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13428
13620
|
};
|
|
13429
13621
|
}
|
|
13622
|
+
function buildPins(deps, hooks, parts) {
|
|
13623
|
+
return new IntentPins({
|
|
13624
|
+
ledger: parts.ledger,
|
|
13625
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13626
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13627
|
+
release: (tokenId) => {
|
|
13628
|
+
parts.view.release(tokenId);
|
|
13629
|
+
},
|
|
13630
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13631
|
+
});
|
|
13632
|
+
}
|
|
13430
13633
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13431
13634
|
return {
|
|
13432
13635
|
stores: machineStores,
|
|
@@ -13536,7 +13739,8 @@ var PaymentsFacade = class {
|
|
|
13536
13739
|
this.deps = deps;
|
|
13537
13740
|
const parts = composeFacadeParts(deps, {
|
|
13538
13741
|
engine: () => this.engine(),
|
|
13539
|
-
send: (request) => this.send(request)
|
|
13742
|
+
send: (request) => this.send(request),
|
|
13743
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13540
13744
|
});
|
|
13541
13745
|
this.view = parts.view;
|
|
13542
13746
|
this.ledger = parts.ledger;
|
|
@@ -13550,6 +13754,7 @@ var PaymentsFacade = class {
|
|
|
13550
13754
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13551
13755
|
this.restoreDeps = parts.restoreDeps;
|
|
13552
13756
|
this.requests = parts.requests;
|
|
13757
|
+
this.pins = parts.pins;
|
|
13553
13758
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13554
13759
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13555
13760
|
now: () => this.nowMs(),
|
|
@@ -13592,6 +13797,8 @@ var PaymentsFacade = class {
|
|
|
13592
13797
|
passChain = new SerialChain();
|
|
13593
13798
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13594
13799
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13800
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13801
|
+
pins;
|
|
13595
13802
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13596
13803
|
async start() {
|
|
13597
13804
|
if (this.started) return;
|
|
@@ -13614,6 +13821,7 @@ var PaymentsFacade = class {
|
|
|
13614
13821
|
});
|
|
13615
13822
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13616
13823
|
await this.deps.session.start();
|
|
13824
|
+
await this.pins.sync().catch(() => void 0);
|
|
13617
13825
|
this.trackTail(this.runConvergencePass());
|
|
13618
13826
|
this.trackTail(this.seedHeldStates());
|
|
13619
13827
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13684,6 +13892,7 @@ var PaymentsFacade = class {
|
|
|
13684
13892
|
}
|
|
13685
13893
|
async convergeBody() {
|
|
13686
13894
|
const outcome = await this.converger.convergeOnce();
|
|
13895
|
+
await this.pins.sync().catch(() => void 0);
|
|
13687
13896
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13688
13897
|
this.heartbeat.settle(outcome, pending);
|
|
13689
13898
|
}
|
|
@@ -13923,6 +14132,7 @@ var PaymentsFacade = class {
|
|
|
13923
14132
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13924
14133
|
settleKeepOpen(ctx) {
|
|
13925
14134
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14135
|
+
this.pins.adopt(ctx.transferId);
|
|
13926
14136
|
this.heartbeat.arm();
|
|
13927
14137
|
}
|
|
13928
14138
|
settlePartial(ctx, partial) {
|