@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.cjs
CHANGED
|
@@ -9288,6 +9288,7 @@ var import_CertificationResponse = require("@unicitylabs/state-transition-sdk/li
|
|
|
9288
9288
|
var import_StateId = require("@unicitylabs/state-transition-sdk/lib/api/StateId.js");
|
|
9289
9289
|
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProof.js");
|
|
9290
9290
|
var import_InclusionProofResponse = require("@unicitylabs/state-transition-sdk/lib/api/InclusionProofResponse.js");
|
|
9291
|
+
var import_JsonRpcNetworkError = require("@unicitylabs/state-transition-sdk/lib/api/json-rpc/JsonRpcNetworkError.js");
|
|
9291
9292
|
var import_RootTrustBase = require("@unicitylabs/state-transition-sdk/lib/api/bft/RootTrustBase.js");
|
|
9292
9293
|
var import_InclusionProofUtils = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils.js");
|
|
9293
9294
|
var import_InclusionProofVerificationRule = require("@unicitylabs/state-transition-sdk/lib/transaction/verification/rule/InclusionProofVerificationRule.js");
|
|
@@ -9453,6 +9454,75 @@ function decodeSpherePaymentData(bytes) {
|
|
|
9453
9454
|
return Promise.resolve(SpherePaymentData.fromCBOR(bytes));
|
|
9454
9455
|
}
|
|
9455
9456
|
|
|
9457
|
+
// token-engine/proof-wait.ts
|
|
9458
|
+
var DEFAULT_PROOF_TIMEOUT_MS = 1e4;
|
|
9459
|
+
function resolveProofTimeoutMs(configured) {
|
|
9460
|
+
if (configured === void 0) return DEFAULT_PROOF_TIMEOUT_MS;
|
|
9461
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
9462
|
+
throw new SphereError(
|
|
9463
|
+
`proofTimeoutMs must be a positive number of milliseconds (got ${String(configured)}); an unbounded inclusion-proof wait is not supported`,
|
|
9464
|
+
"INVALID_CONFIG"
|
|
9465
|
+
);
|
|
9466
|
+
}
|
|
9467
|
+
return configured;
|
|
9468
|
+
}
|
|
9469
|
+
var TRANSIENT_HTTP_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
9470
|
+
function isTransientGatewayError(err) {
|
|
9471
|
+
return err instanceof import_JsonRpcNetworkError.JsonRpcNetworkError && TRANSIENT_HTTP_STATUSES.has(err.status);
|
|
9472
|
+
}
|
|
9473
|
+
function pause(ms, signal) {
|
|
9474
|
+
return new Promise((resolve) => {
|
|
9475
|
+
const timer = setTimeout(resolve, ms);
|
|
9476
|
+
signal.addEventListener(
|
|
9477
|
+
"abort",
|
|
9478
|
+
() => {
|
|
9479
|
+
clearTimeout(timer);
|
|
9480
|
+
resolve();
|
|
9481
|
+
},
|
|
9482
|
+
{ once: true }
|
|
9483
|
+
);
|
|
9484
|
+
});
|
|
9485
|
+
}
|
|
9486
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9487
|
+
const controller = new AbortController();
|
|
9488
|
+
const relayAbort = () => {
|
|
9489
|
+
controller.abort(external?.reason);
|
|
9490
|
+
};
|
|
9491
|
+
if (external !== void 0) {
|
|
9492
|
+
if (external.aborted) relayAbort();
|
|
9493
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9494
|
+
}
|
|
9495
|
+
const timer = setTimeout(() => {
|
|
9496
|
+
controller.abort(
|
|
9497
|
+
new SphereError(
|
|
9498
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9499
|
+
"AGGREGATOR_ERROR"
|
|
9500
|
+
)
|
|
9501
|
+
);
|
|
9502
|
+
}, deps.timeoutMs);
|
|
9503
|
+
try {
|
|
9504
|
+
for (; ; ) {
|
|
9505
|
+
try {
|
|
9506
|
+
return await (0, import_InclusionProofUtils.waitInclusionProof)(
|
|
9507
|
+
deps.client,
|
|
9508
|
+
deps.trustBase,
|
|
9509
|
+
deps.predicateVerifier,
|
|
9510
|
+
transaction,
|
|
9511
|
+
controller.signal,
|
|
9512
|
+
deps.intervalMs
|
|
9513
|
+
);
|
|
9514
|
+
} catch (err) {
|
|
9515
|
+
if (controller.signal.aborted) throw err;
|
|
9516
|
+
if (!isTransientGatewayError(err)) throw err;
|
|
9517
|
+
await pause(deps.intervalMs, controller.signal);
|
|
9518
|
+
}
|
|
9519
|
+
}
|
|
9520
|
+
} finally {
|
|
9521
|
+
clearTimeout(timer);
|
|
9522
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9523
|
+
}
|
|
9524
|
+
}
|
|
9525
|
+
|
|
9456
9526
|
// token-engine/errors.ts
|
|
9457
9527
|
var TransferConflictError = class extends SphereError {
|
|
9458
9528
|
/**
|
|
@@ -9677,11 +9747,13 @@ var SphereTokenEngine = class {
|
|
|
9677
9747
|
constructor(deps) {
|
|
9678
9748
|
this.deps = deps;
|
|
9679
9749
|
this.privateKeyHex = import_HexConverter.HexConverter.encode(deps.privateKey);
|
|
9750
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9680
9751
|
}
|
|
9681
9752
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9682
9753
|
deterministicMint = true;
|
|
9683
9754
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9684
9755
|
privateKeyHex;
|
|
9756
|
+
proofTimeoutMs;
|
|
9685
9757
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9686
9758
|
getIdentity() {
|
|
9687
9759
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -10024,14 +10096,17 @@ var SphereTokenEngine = class {
|
|
|
10024
10096
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
10025
10097
|
}
|
|
10026
10098
|
try {
|
|
10027
|
-
return await (
|
|
10028
|
-
|
|
10029
|
-
|
|
10030
|
-
|
|
10099
|
+
return await awaitProofBounded(
|
|
10100
|
+
{
|
|
10101
|
+
client: this.deps.client,
|
|
10102
|
+
trustBase: this.deps.trustBase,
|
|
10103
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10104
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10105
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10106
|
+
// #683
|
|
10107
|
+
},
|
|
10031
10108
|
transaction,
|
|
10032
|
-
options?.signal
|
|
10033
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10034
|
-
// #683 finer poll cadence
|
|
10109
|
+
options?.signal
|
|
10035
10110
|
);
|
|
10036
10111
|
} catch (err) {
|
|
10037
10112
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${import_InclusionProofVerificationRule.InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -10047,6 +10122,13 @@ var SphereTokenEngine = class {
|
|
|
10047
10122
|
);
|
|
10048
10123
|
}
|
|
10049
10124
|
}
|
|
10125
|
+
/**
|
|
10126
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10127
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10128
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10129
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10130
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10131
|
+
*/
|
|
10050
10132
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
10051
10133
|
assertOwned(token) {
|
|
10052
10134
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10146,6 +10228,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10146
10228
|
networkId: trustBase.networkId,
|
|
10147
10229
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10148
10230
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10231
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10232
|
+
// #739: was declared but never wired
|
|
10149
10233
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10150
10234
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10151
10235
|
};
|
|
@@ -12055,6 +12139,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
12055
12139
|
}
|
|
12056
12140
|
return out;
|
|
12057
12141
|
}
|
|
12142
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12143
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12144
|
+
let complete = true;
|
|
12145
|
+
for (const entry of await stores.backstop.list()) {
|
|
12146
|
+
if (entry.disposition !== "open") continue;
|
|
12147
|
+
let payload = null;
|
|
12148
|
+
try {
|
|
12149
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12150
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12151
|
+
} catch {
|
|
12152
|
+
complete = false;
|
|
12153
|
+
continue;
|
|
12154
|
+
}
|
|
12155
|
+
if (payload === null) {
|
|
12156
|
+
complete = false;
|
|
12157
|
+
continue;
|
|
12158
|
+
}
|
|
12159
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12160
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12161
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12162
|
+
}
|
|
12163
|
+
return { open: holds, complete };
|
|
12164
|
+
}
|
|
12058
12165
|
function groupJournal(journal) {
|
|
12059
12166
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
12060
12167
|
for (const entry of journal) {
|
|
@@ -12465,10 +12572,21 @@ var InventoryView = class {
|
|
|
12465
12572
|
release(tokenId) {
|
|
12466
12573
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12467
12574
|
}
|
|
12575
|
+
pinned(tokenId) {
|
|
12576
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12577
|
+
}
|
|
12578
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12579
|
+
held(tokenId) {
|
|
12580
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12581
|
+
}
|
|
12582
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12583
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12584
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12468
12585
|
pool(coinId) {
|
|
12469
12586
|
const out = [];
|
|
12470
12587
|
for (const [tokenId, entry] of this.mirror) {
|
|
12471
|
-
if (entry.status !== "active"
|
|
12588
|
+
if (entry.status !== "active") continue;
|
|
12589
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12472
12590
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12473
12591
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12474
12592
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12484,7 +12602,7 @@ var InventoryView = class {
|
|
|
12484
12602
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12485
12603
|
out.push(
|
|
12486
12604
|
toToken(tokenId, entry, asset, registry, {
|
|
12487
|
-
transferring: this.
|
|
12605
|
+
transferring: this.held(tokenId),
|
|
12488
12606
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12489
12607
|
})
|
|
12490
12608
|
);
|
|
@@ -12492,7 +12610,7 @@ var InventoryView = class {
|
|
|
12492
12610
|
return out;
|
|
12493
12611
|
}
|
|
12494
12612
|
async assets(registry, price) {
|
|
12495
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12613
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12496
12614
|
if (!price || raw.length === 0) return raw;
|
|
12497
12615
|
return withPrices(raw, registry, price);
|
|
12498
12616
|
}
|
|
@@ -13178,6 +13296,20 @@ var Requests = class {
|
|
|
13178
13296
|
var ReservationLedger = class {
|
|
13179
13297
|
reservations = /* @__PURE__ */ new Map();
|
|
13180
13298
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13299
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13300
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13301
|
+
authoritative = false;
|
|
13302
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13303
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13304
|
+
setAuthoritative(value) {
|
|
13305
|
+
const moved = this.authoritative !== value;
|
|
13306
|
+
this.authoritative = value;
|
|
13307
|
+
return moved;
|
|
13308
|
+
}
|
|
13309
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13310
|
+
unprovenReason() {
|
|
13311
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13312
|
+
}
|
|
13181
13313
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13182
13314
|
reserve(reservationId, entries) {
|
|
13183
13315
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13204,6 +13336,13 @@ var ReservationLedger = class {
|
|
|
13204
13336
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13205
13337
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13206
13338
|
}
|
|
13339
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13340
|
+
holderOf(tokenId) {
|
|
13341
|
+
return this.tokenHolder.get(tokenId);
|
|
13342
|
+
}
|
|
13343
|
+
tokensOf(reservationId) {
|
|
13344
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13345
|
+
}
|
|
13207
13346
|
clear() {
|
|
13208
13347
|
this.reservations.clear();
|
|
13209
13348
|
this.tokenHolder.clear();
|
|
@@ -13216,6 +13355,44 @@ var ReservationLedger = class {
|
|
|
13216
13355
|
}
|
|
13217
13356
|
};
|
|
13218
13357
|
|
|
13358
|
+
// modules/payments-v2/select/pins.ts
|
|
13359
|
+
var IntentPins = class {
|
|
13360
|
+
constructor(deps) {
|
|
13361
|
+
this.deps = deps;
|
|
13362
|
+
}
|
|
13363
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13364
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13365
|
+
adopt(transferId) {
|
|
13366
|
+
this.pinned.add(transferId);
|
|
13367
|
+
}
|
|
13368
|
+
async sync() {
|
|
13369
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13370
|
+
let changed = false;
|
|
13371
|
+
for (const transferId of [...this.pinned]) {
|
|
13372
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13373
|
+
this.pinned.delete(transferId);
|
|
13374
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13375
|
+
this.deps.ledger.cancel(transferId);
|
|
13376
|
+
changed = true;
|
|
13377
|
+
}
|
|
13378
|
+
for (const [transferId, tokenIds] of open) {
|
|
13379
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13380
|
+
this.pinned.add(transferId);
|
|
13381
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13382
|
+
}
|
|
13383
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13384
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13385
|
+
}
|
|
13386
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13387
|
+
pin(transferId, tokenIds) {
|
|
13388
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13389
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13390
|
+
if (free.length === 0) return false;
|
|
13391
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13392
|
+
return true;
|
|
13393
|
+
}
|
|
13394
|
+
};
|
|
13395
|
+
|
|
13219
13396
|
// modules/payments-v2/select/selector.ts
|
|
13220
13397
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13221
13398
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13306,6 +13483,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13306
13483
|
// modules/payments-v2/select/queue.ts
|
|
13307
13484
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13308
13485
|
var QUEUE_MAX_SIZE = 100;
|
|
13486
|
+
function insufficientBalance(view, amount) {
|
|
13487
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13488
|
+
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";
|
|
13489
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13490
|
+
}
|
|
13309
13491
|
var SpendQueue = class {
|
|
13310
13492
|
constructor(deps) {
|
|
13311
13493
|
this.deps = deps;
|
|
@@ -13318,12 +13500,10 @@ var SpendQueue = class {
|
|
|
13318
13500
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13319
13501
|
}
|
|
13320
13502
|
const amount = parseAmount(request.amount);
|
|
13321
|
-
const
|
|
13503
|
+
const view = this.freeView(request.coinId);
|
|
13504
|
+
const { freeView, freeTotal } = view;
|
|
13322
13505
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13323
|
-
throw
|
|
13324
|
-
"Insufficient balance for this transaction",
|
|
13325
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13326
|
-
);
|
|
13506
|
+
throw insufficientBalance(view, amount);
|
|
13327
13507
|
}
|
|
13328
13508
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13329
13509
|
if (plan !== null) {
|
|
@@ -13373,11 +13553,10 @@ var SpendQueue = class {
|
|
|
13373
13553
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13374
13554
|
return "rejected";
|
|
13375
13555
|
}
|
|
13376
|
-
const
|
|
13556
|
+
const view = this.freeView(entry.coinId);
|
|
13557
|
+
const { freeView, freeTotal } = view;
|
|
13377
13558
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13378
|
-
entry.reject(
|
|
13379
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13380
|
-
);
|
|
13559
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13381
13560
|
return "rejected";
|
|
13382
13561
|
}
|
|
13383
13562
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13422,18 +13601,26 @@ var SpendQueue = class {
|
|
|
13422
13601
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13423
13602
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13424
13603
|
}
|
|
13425
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13604
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13605
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13426
13606
|
freeView(coinId) {
|
|
13427
13607
|
const freeView = [];
|
|
13428
13608
|
let freeTotal = 0n;
|
|
13609
|
+
let pinnedTotal = 0n;
|
|
13610
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13611
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13429
13612
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13430
13613
|
if (entry.amount <= 0n) continue;
|
|
13431
|
-
|
|
13614
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13615
|
+
if (holder === void 0) {
|
|
13432
13616
|
freeView.push(entry);
|
|
13433
13617
|
freeTotal += entry.amount;
|
|
13618
|
+
} else {
|
|
13619
|
+
pinnedTotal += entry.amount;
|
|
13620
|
+
holders.add(holder);
|
|
13434
13621
|
}
|
|
13435
13622
|
}
|
|
13436
|
-
return { freeView, freeTotal };
|
|
13623
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13437
13624
|
}
|
|
13438
13625
|
expectedChangeTotal(coinId) {
|
|
13439
13626
|
let total = 0n;
|
|
@@ -13475,13 +13662,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13475
13662
|
}
|
|
13476
13663
|
function composeFacadeParts(deps, hooks) {
|
|
13477
13664
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13665
|
+
const ledger = new ReservationLedger();
|
|
13478
13666
|
const view = new InventoryView({
|
|
13479
13667
|
port: deps.storagePort,
|
|
13480
13668
|
kv: deps.kv,
|
|
13481
13669
|
emit: (event) => deps.emit(event, {}),
|
|
13670
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13671
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13672
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13673
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13482
13674
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13483
13675
|
});
|
|
13484
|
-
const ledger = new ReservationLedger();
|
|
13485
13676
|
const queue = new SpendQueue({
|
|
13486
13677
|
ledger,
|
|
13487
13678
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13502,6 +13693,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13502
13693
|
ownPubkeyBytes,
|
|
13503
13694
|
view,
|
|
13504
13695
|
ledger,
|
|
13696
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13505
13697
|
queue,
|
|
13506
13698
|
historyStore,
|
|
13507
13699
|
machineStores,
|
|
@@ -13513,6 +13705,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13513
13705
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13514
13706
|
};
|
|
13515
13707
|
}
|
|
13708
|
+
function buildPins(deps, hooks, parts) {
|
|
13709
|
+
return new IntentPins({
|
|
13710
|
+
ledger: parts.ledger,
|
|
13711
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13712
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13713
|
+
release: (tokenId) => {
|
|
13714
|
+
parts.view.release(tokenId);
|
|
13715
|
+
},
|
|
13716
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13717
|
+
});
|
|
13718
|
+
}
|
|
13516
13719
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13517
13720
|
return {
|
|
13518
13721
|
stores: machineStores,
|
|
@@ -13622,7 +13825,8 @@ var PaymentsFacade = class {
|
|
|
13622
13825
|
this.deps = deps;
|
|
13623
13826
|
const parts = composeFacadeParts(deps, {
|
|
13624
13827
|
engine: () => this.engine(),
|
|
13625
|
-
send: (request) => this.send(request)
|
|
13828
|
+
send: (request) => this.send(request),
|
|
13829
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13626
13830
|
});
|
|
13627
13831
|
this.view = parts.view;
|
|
13628
13832
|
this.ledger = parts.ledger;
|
|
@@ -13636,6 +13840,7 @@ var PaymentsFacade = class {
|
|
|
13636
13840
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13637
13841
|
this.restoreDeps = parts.restoreDeps;
|
|
13638
13842
|
this.requests = parts.requests;
|
|
13843
|
+
this.pins = parts.pins;
|
|
13639
13844
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13640
13845
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13641
13846
|
now: () => this.nowMs(),
|
|
@@ -13678,6 +13883,8 @@ var PaymentsFacade = class {
|
|
|
13678
13883
|
passChain = new SerialChain();
|
|
13679
13884
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13680
13885
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13886
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13887
|
+
pins;
|
|
13681
13888
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13682
13889
|
async start() {
|
|
13683
13890
|
if (this.started) return;
|
|
@@ -13700,6 +13907,7 @@ var PaymentsFacade = class {
|
|
|
13700
13907
|
});
|
|
13701
13908
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13702
13909
|
await this.deps.session.start();
|
|
13910
|
+
await this.pins.sync().catch(() => void 0);
|
|
13703
13911
|
this.trackTail(this.runConvergencePass());
|
|
13704
13912
|
this.trackTail(this.seedHeldStates());
|
|
13705
13913
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13770,6 +13978,7 @@ var PaymentsFacade = class {
|
|
|
13770
13978
|
}
|
|
13771
13979
|
async convergeBody() {
|
|
13772
13980
|
const outcome = await this.converger.convergeOnce();
|
|
13981
|
+
await this.pins.sync().catch(() => void 0);
|
|
13773
13982
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13774
13983
|
this.heartbeat.settle(outcome, pending);
|
|
13775
13984
|
}
|
|
@@ -14009,6 +14218,7 @@ var PaymentsFacade = class {
|
|
|
14009
14218
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
14010
14219
|
settleKeepOpen(ctx) {
|
|
14011
14220
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14221
|
+
this.pins.adopt(ctx.transferId);
|
|
14012
14222
|
this.heartbeat.arm();
|
|
14013
14223
|
}
|
|
14014
14224
|
settlePartial(ctx, partial) {
|