@unicitylabs/sphere-sdk 0.14.3 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +1 -1
- package/dist/connect/index.js.map +1 -1
- package/dist/core/index.cjs +245 -24
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.js +245 -24
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/connect/index.cjs +1 -1
- package/dist/impl/browser/connect/index.cjs.map +1 -1
- package/dist/impl/browser/connect/index.js +1 -1
- package/dist/impl/browser/connect/index.js.map +1 -1
- package/dist/impl/wallet-api-v2/index.cjs +1 -0
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.js +1 -0
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +245 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +245 -24
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +144 -17
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +2 -0
- package/dist/modules/payments-v2/index.d.ts +2 -0
- package/dist/modules/payments-v2/index.js +144 -17
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +102 -7
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +5 -1
- package/dist/token-engine/index.d.ts +5 -1
- package/dist/token-engine/index.js +102 -7
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
package/dist/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,86 @@ 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
|
+
function lateDeliveringSignal(signal) {
|
|
9384
|
+
return new Proxy(signal, {
|
|
9385
|
+
get(target, prop) {
|
|
9386
|
+
if (prop === "addEventListener") {
|
|
9387
|
+
return (type, listener, options) => {
|
|
9388
|
+
if (type === "abort" && target.aborted) {
|
|
9389
|
+
queueMicrotask(() => {
|
|
9390
|
+
listener.call(target, new Event("abort"));
|
|
9391
|
+
});
|
|
9392
|
+
return;
|
|
9393
|
+
}
|
|
9394
|
+
target.addEventListener(type, listener, options);
|
|
9395
|
+
};
|
|
9396
|
+
}
|
|
9397
|
+
const value = Reflect.get(target, prop, target);
|
|
9398
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
9399
|
+
}
|
|
9400
|
+
});
|
|
9401
|
+
}
|
|
9402
|
+
function rejectOnAbort(signal) {
|
|
9403
|
+
return new Promise((_resolve, reject) => {
|
|
9404
|
+
if (signal.aborted) {
|
|
9405
|
+
reject(signal.reason);
|
|
9406
|
+
return;
|
|
9407
|
+
}
|
|
9408
|
+
signal.addEventListener(
|
|
9409
|
+
"abort",
|
|
9410
|
+
() => {
|
|
9411
|
+
reject(signal.reason);
|
|
9412
|
+
},
|
|
9413
|
+
{ once: true }
|
|
9414
|
+
);
|
|
9415
|
+
});
|
|
9416
|
+
}
|
|
9417
|
+
async function awaitProofBounded(deps, transaction, external) {
|
|
9418
|
+
const controller = new AbortController();
|
|
9419
|
+
const relayAbort = () => {
|
|
9420
|
+
controller.abort(external?.reason);
|
|
9421
|
+
};
|
|
9422
|
+
if (external !== void 0) {
|
|
9423
|
+
if (external.aborted) relayAbort();
|
|
9424
|
+
else external.addEventListener("abort", relayAbort, { once: true });
|
|
9425
|
+
}
|
|
9426
|
+
const timer = setTimeout(() => {
|
|
9427
|
+
controller.abort(
|
|
9428
|
+
new SphereError(
|
|
9429
|
+
`inclusion proof not confirmed within ${String(deps.timeoutMs)} ms`,
|
|
9430
|
+
"AGGREGATOR_ERROR"
|
|
9431
|
+
)
|
|
9432
|
+
);
|
|
9433
|
+
}, deps.timeoutMs);
|
|
9434
|
+
const wait = waitInclusionProof(
|
|
9435
|
+
deps.client,
|
|
9436
|
+
deps.trustBase,
|
|
9437
|
+
deps.predicateVerifier,
|
|
9438
|
+
transaction,
|
|
9439
|
+
lateDeliveringSignal(controller.signal),
|
|
9440
|
+
deps.intervalMs
|
|
9441
|
+
);
|
|
9442
|
+
wait.catch(() => void 0);
|
|
9443
|
+
try {
|
|
9444
|
+
return await Promise.race([wait, rejectOnAbort(controller.signal)]);
|
|
9445
|
+
} finally {
|
|
9446
|
+
clearTimeout(timer);
|
|
9447
|
+
external?.removeEventListener("abort", relayAbort);
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
|
|
9370
9451
|
// token-engine/errors.ts
|
|
9371
9452
|
var TransferConflictError = class extends SphereError {
|
|
9372
9453
|
/**
|
|
@@ -9591,11 +9672,13 @@ var SphereTokenEngine = class {
|
|
|
9591
9672
|
constructor(deps) {
|
|
9592
9673
|
this.deps = deps;
|
|
9593
9674
|
this.privateKeyHex = HexConverter.encode(deps.privateKey);
|
|
9675
|
+
this.proofTimeoutMs = resolveProofTimeoutMs(deps.proofTimeoutMs);
|
|
9594
9676
|
}
|
|
9595
9677
|
/** F13 advertisement: mint under a (transferId, opIndex) seed is idempotent-recoverable — a same-seed re-call converges, never double-mints. */
|
|
9596
9678
|
deterministicMint = true;
|
|
9597
9679
|
/** Hex form of the wallet key — deriveRealization's ikm input (Part E.1). */
|
|
9598
9680
|
privateKeyHex;
|
|
9681
|
+
proofTimeoutMs;
|
|
9599
9682
|
// ── identity ────────────────────────────────────────────────────────────────
|
|
9600
9683
|
getIdentity() {
|
|
9601
9684
|
return { chainPubkey: new Uint8Array(this.deps.signingService.publicKey) };
|
|
@@ -9938,14 +10021,17 @@ var SphereTokenEngine = class {
|
|
|
9938
10021
|
submitError = err ?? new SphereError(`${failLabel}: submit failed`, failCode);
|
|
9939
10022
|
}
|
|
9940
10023
|
try {
|
|
9941
|
-
return await
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
10024
|
+
return await awaitProofBounded(
|
|
10025
|
+
{
|
|
10026
|
+
client: this.deps.client,
|
|
10027
|
+
trustBase: this.deps.trustBase,
|
|
10028
|
+
predicateVerifier: this.deps.predicateVerifier,
|
|
10029
|
+
timeoutMs: this.proofTimeoutMs,
|
|
10030
|
+
intervalMs: this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
10031
|
+
// #683
|
|
10032
|
+
},
|
|
9945
10033
|
transaction,
|
|
9946
|
-
options?.signal
|
|
9947
|
-
this.deps.proofPollIntervalMs ?? DEFAULT_PROOF_POLL_INTERVAL_MS
|
|
9948
|
-
// #683 finer poll cadence
|
|
10034
|
+
options?.signal
|
|
9949
10035
|
);
|
|
9950
10036
|
} catch (err) {
|
|
9951
10037
|
if (err instanceof Error && err.message === `Invalid inclusion proof status: ${InclusionProofVerificationStatus.TRANSACTION_HASH_MISMATCH}`) {
|
|
@@ -9961,6 +10047,13 @@ var SphereTokenEngine = class {
|
|
|
9961
10047
|
);
|
|
9962
10048
|
}
|
|
9963
10049
|
}
|
|
10050
|
+
/**
|
|
10051
|
+
* One inclusion-proof wait, with the deadline OWNED here (#739). The SDK's own
|
|
10052
|
+
* default deadline is unreliable — see lateDeliveringSignal — and a lost one
|
|
10053
|
+
* means an op that never settles, which also wedges PaymentsFacade.stop().
|
|
10054
|
+
* A deadline hit throws, and the caller maps that to ProofUnconfirmedError
|
|
10055
|
+
* (keep-open), which is the correct posture: the spend may be on-chain.
|
|
10056
|
+
*/
|
|
9964
10057
|
/** Fail fast if this engine's key does not own the token's current state. */
|
|
9965
10058
|
assertOwned(token) {
|
|
9966
10059
|
if (!this.isOwnedBy(token, this.deps.signingService.publicKey)) {
|
|
@@ -10060,6 +10153,8 @@ async function createSphereTokenEngine(config) {
|
|
|
10060
10153
|
networkId: trustBase.networkId,
|
|
10061
10154
|
// #683: forward the (optional) proof-poll cadence; undefined → the engine default.
|
|
10062
10155
|
proofPollIntervalMs: config.proofPollIntervalMs,
|
|
10156
|
+
proofTimeoutMs: config.proofTimeoutMs,
|
|
10157
|
+
// #739: was declared but never wired
|
|
10063
10158
|
// Opt-in parallel verification (2.0.2). Absent → the sequential verifier.
|
|
10064
10159
|
...config.verification ? { tokenVerifier: createWorkerTokenVerifier(config.verification) } : {}
|
|
10065
10160
|
};
|
|
@@ -11969,6 +12064,29 @@ async function derivePendingTransfers(stores, decryptPayload) {
|
|
|
11969
12064
|
}
|
|
11970
12065
|
return out;
|
|
11971
12066
|
}
|
|
12067
|
+
async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
12068
|
+
const holds = /* @__PURE__ */ new Map();
|
|
12069
|
+
let complete = true;
|
|
12070
|
+
for (const entry of await stores.backstop.list()) {
|
|
12071
|
+
if (entry.disposition !== "open") continue;
|
|
12072
|
+
let payload = null;
|
|
12073
|
+
try {
|
|
12074
|
+
const raw = await decryptPayload(entry.payloadEnvelope);
|
|
12075
|
+
if (raw !== null && typeof raw === "object") payload = raw;
|
|
12076
|
+
} catch {
|
|
12077
|
+
complete = false;
|
|
12078
|
+
continue;
|
|
12079
|
+
}
|
|
12080
|
+
if (payload === null) {
|
|
12081
|
+
complete = false;
|
|
12082
|
+
continue;
|
|
12083
|
+
}
|
|
12084
|
+
const direct = Array.isArray(payload?.direct) ? payload.direct.filter((id) => typeof id === "string") : [];
|
|
12085
|
+
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
12086
|
+
holds.set(entry.transferId, [...direct, ...split]);
|
|
12087
|
+
}
|
|
12088
|
+
return { open: holds, complete };
|
|
12089
|
+
}
|
|
11972
12090
|
function groupJournal(journal) {
|
|
11973
12091
|
const byTransfer = /* @__PURE__ */ new Map();
|
|
11974
12092
|
for (const entry of journal) {
|
|
@@ -12379,10 +12497,21 @@ var InventoryView = class {
|
|
|
12379
12497
|
release(tokenId) {
|
|
12380
12498
|
if (this.inFlight.delete(tokenId)) this.deps.emit("inventory:updated");
|
|
12381
12499
|
}
|
|
12500
|
+
pinned(tokenId) {
|
|
12501
|
+
return this.deps.isPinned?.(tokenId) ?? false;
|
|
12502
|
+
}
|
|
12503
|
+
/** In flight here OR pinned by a reservation: either way not spendable (#737). */
|
|
12504
|
+
held(tokenId) {
|
|
12505
|
+
return this.inFlight.has(tokenId) || this.pinned(tokenId);
|
|
12506
|
+
}
|
|
12507
|
+
// A PINNED token stays in the pool: the queue applies the ledger filter itself
|
|
12508
|
+
// and needs the entry to say how much is pinned when it refuses (#737). In
|
|
12509
|
+
// flight without a pin (spent, awaiting the mirror refresh) stays out.
|
|
12382
12510
|
pool(coinId) {
|
|
12383
12511
|
const out = [];
|
|
12384
12512
|
for (const [tokenId, entry] of this.mirror) {
|
|
12385
|
-
if (entry.status !== "active"
|
|
12513
|
+
if (entry.status !== "active") continue;
|
|
12514
|
+
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) continue;
|
|
12386
12515
|
if (this.suspected.has(stateKey(tokenId, entry.stateHash))) continue;
|
|
12387
12516
|
const asset = entry.assets.find((a) => a.coinId === coinId);
|
|
12388
12517
|
if (asset) out.push({ tokenId, amount: BigInt(asset.amount) });
|
|
@@ -12398,7 +12527,7 @@ var InventoryView = class {
|
|
|
12398
12527
|
if (filter?.coinId !== void 0 && asset.coinId !== filter.coinId) continue;
|
|
12399
12528
|
out.push(
|
|
12400
12529
|
toToken(tokenId, entry, asset, registry, {
|
|
12401
|
-
transferring: this.
|
|
12530
|
+
transferring: this.held(tokenId),
|
|
12402
12531
|
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
12403
12532
|
})
|
|
12404
12533
|
);
|
|
@@ -12406,7 +12535,7 @@ var InventoryView = class {
|
|
|
12406
12535
|
return out;
|
|
12407
12536
|
}
|
|
12408
12537
|
async assets(registry, price) {
|
|
12409
|
-
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.
|
|
12538
|
+
const raw = aggregateAssets(this.activeEntries(), (tokenId) => this.held(tokenId), registry);
|
|
12410
12539
|
if (!price || raw.length === 0) return raw;
|
|
12411
12540
|
return withPrices(raw, registry, price);
|
|
12412
12541
|
}
|
|
@@ -13092,6 +13221,20 @@ var Requests = class {
|
|
|
13092
13221
|
var ReservationLedger = class {
|
|
13093
13222
|
reservations = /* @__PURE__ */ new Map();
|
|
13094
13223
|
tokenHolder = /* @__PURE__ */ new Map();
|
|
13224
|
+
// #738: the held-set is RECONSTRUCTED from the open intents, so until
|
|
13225
|
+
// IntentPins proves every one of them "no holder" means unknown, not free.
|
|
13226
|
+
authoritative = false;
|
|
13227
|
+
/** Sole writer: IntentPins.sync(), true only if it reconstructed every open intent.
|
|
13228
|
+
* Returns whether the gate actually moved, so callers signal transitions, not passes. */
|
|
13229
|
+
setAuthoritative(value) {
|
|
13230
|
+
const moved = this.authoritative !== value;
|
|
13231
|
+
this.authoritative = value;
|
|
13232
|
+
return moved;
|
|
13233
|
+
}
|
|
13234
|
+
/** Non-null while the held-set is unproven: nothing plans, nothing reads free. */
|
|
13235
|
+
unprovenReason() {
|
|
13236
|
+
return this.authoritative ? null : "Cannot yet verify which tokens are held by transfers still converging \u2014 spending is paused until that check succeeds";
|
|
13237
|
+
}
|
|
13095
13238
|
// All-or-nothing: every entry is validated before any state mutates.
|
|
13096
13239
|
reserve(reservationId, entries) {
|
|
13097
13240
|
if (entries.length === 0) throw new Error("EMPTY_RESERVATION");
|
|
@@ -13118,6 +13261,13 @@ var ReservationLedger = class {
|
|
|
13118
13261
|
getFreeAmount(tokenId, tokenAmount) {
|
|
13119
13262
|
return this.tokenHolder.has(tokenId) ? 0n : tokenAmount;
|
|
13120
13263
|
}
|
|
13264
|
+
/** #737: who pins this token — the reporting side reads it so a held token is never called spendable. */
|
|
13265
|
+
holderOf(tokenId) {
|
|
13266
|
+
return this.tokenHolder.get(tokenId);
|
|
13267
|
+
}
|
|
13268
|
+
tokensOf(reservationId) {
|
|
13269
|
+
return [...this.reservations.get(reservationId) ?? []];
|
|
13270
|
+
}
|
|
13121
13271
|
clear() {
|
|
13122
13272
|
this.reservations.clear();
|
|
13123
13273
|
this.tokenHolder.clear();
|
|
@@ -13130,6 +13280,44 @@ var ReservationLedger = class {
|
|
|
13130
13280
|
}
|
|
13131
13281
|
};
|
|
13132
13282
|
|
|
13283
|
+
// modules/payments-v2/select/pins.ts
|
|
13284
|
+
var IntentPins = class {
|
|
13285
|
+
constructor(deps) {
|
|
13286
|
+
this.deps = deps;
|
|
13287
|
+
}
|
|
13288
|
+
pinned = /* @__PURE__ */ new Set();
|
|
13289
|
+
/** The keep-open reservation IS this intent's pin from now on. */
|
|
13290
|
+
adopt(transferId) {
|
|
13291
|
+
this.pinned.add(transferId);
|
|
13292
|
+
}
|
|
13293
|
+
async sync() {
|
|
13294
|
+
const { open, complete } = await this.deps.openIntents();
|
|
13295
|
+
let changed = false;
|
|
13296
|
+
for (const transferId of [...this.pinned]) {
|
|
13297
|
+
if (open.has(transferId) || this.deps.isActive(transferId)) continue;
|
|
13298
|
+
this.pinned.delete(transferId);
|
|
13299
|
+
for (const tokenId of this.deps.ledger.tokensOf(transferId)) this.deps.release(tokenId);
|
|
13300
|
+
this.deps.ledger.cancel(transferId);
|
|
13301
|
+
changed = true;
|
|
13302
|
+
}
|
|
13303
|
+
for (const [transferId, tokenIds] of open) {
|
|
13304
|
+
if (this.deps.isActive(transferId)) continue;
|
|
13305
|
+
this.pinned.add(transferId);
|
|
13306
|
+
changed = this.pin(transferId, tokenIds) || changed;
|
|
13307
|
+
}
|
|
13308
|
+
const gateMoved = this.deps.ledger.setAuthoritative(complete);
|
|
13309
|
+
if (changed || gateMoved) this.deps.changed();
|
|
13310
|
+
}
|
|
13311
|
+
/** Every pass, not just on adoption; the free subset, since a partial pin beats none. */
|
|
13312
|
+
pin(transferId, tokenIds) {
|
|
13313
|
+
if (this.deps.ledger.tokensOf(transferId).length > 0) return false;
|
|
13314
|
+
const free = tokenIds.filter((tokenId) => this.deps.ledger.holderOf(tokenId) === void 0);
|
|
13315
|
+
if (free.length === 0) return false;
|
|
13316
|
+
this.deps.ledger.reserve(transferId, free.map((tokenId) => ({ tokenId })));
|
|
13317
|
+
return true;
|
|
13318
|
+
}
|
|
13319
|
+
};
|
|
13320
|
+
|
|
13133
13321
|
// modules/payments-v2/select/selector.ts
|
|
13134
13322
|
var DEFAULT_WORK_BUDGET = 2e4;
|
|
13135
13323
|
var MAX_COMBINATION_SIZE = 5;
|
|
@@ -13220,6 +13408,11 @@ function searchCombinationsOfSize(window, size, target, search) {
|
|
|
13220
13408
|
// modules/payments-v2/select/queue.ts
|
|
13221
13409
|
var QUEUE_TIMEOUT_MS = 3e4;
|
|
13222
13410
|
var QUEUE_MAX_SIZE = 100;
|
|
13411
|
+
function insufficientBalance(view, amount) {
|
|
13412
|
+
if (view.unproven !== null) return new SphereError(view.unproven, "SEND_INSUFFICIENT_BALANCE");
|
|
13413
|
+
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";
|
|
13414
|
+
return new SphereError(message, "SEND_INSUFFICIENT_BALANCE");
|
|
13415
|
+
}
|
|
13223
13416
|
var SpendQueue = class {
|
|
13224
13417
|
constructor(deps) {
|
|
13225
13418
|
this.deps = deps;
|
|
@@ -13232,12 +13425,10 @@ var SpendQueue = class {
|
|
|
13232
13425
|
throw new SphereError("Module has been destroyed", "MODULE_DESTROYED");
|
|
13233
13426
|
}
|
|
13234
13427
|
const amount = parseAmount(request.amount);
|
|
13235
|
-
const
|
|
13428
|
+
const view = this.freeView(request.coinId);
|
|
13429
|
+
const { freeView, freeTotal } = view;
|
|
13236
13430
|
if (freeTotal + this.expectedChangeTotal(request.coinId) < amount) {
|
|
13237
|
-
throw
|
|
13238
|
-
"Insufficient balance for this transaction",
|
|
13239
|
-
"SEND_INSUFFICIENT_BALANCE"
|
|
13240
|
-
);
|
|
13431
|
+
throw insufficientBalance(view, amount);
|
|
13241
13432
|
}
|
|
13242
13433
|
const plan = selectCoins(freeView, amount, { workBudget: this.deps.workBudget });
|
|
13243
13434
|
if (plan !== null) {
|
|
@@ -13287,11 +13478,10 @@ var SpendQueue = class {
|
|
|
13287
13478
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13288
13479
|
return "rejected";
|
|
13289
13480
|
}
|
|
13290
|
-
const
|
|
13481
|
+
const view = this.freeView(entry.coinId);
|
|
13482
|
+
const { freeView, freeTotal } = view;
|
|
13291
13483
|
if (freeTotal + this.expectedChangeTotal(entry.coinId) < entry.amount) {
|
|
13292
|
-
entry.reject(
|
|
13293
|
-
new SphereError("Insufficient balance for this transaction", "SEND_INSUFFICIENT_BALANCE")
|
|
13294
|
-
);
|
|
13484
|
+
entry.reject(insufficientBalance(view, entry.amount));
|
|
13295
13485
|
return "rejected";
|
|
13296
13486
|
}
|
|
13297
13487
|
const plan = selectCoins(freeView, entry.amount, { workBudget: this.deps.workBudget });
|
|
@@ -13336,18 +13526,26 @@ var SpendQueue = class {
|
|
|
13336
13526
|
entry.reject(new SphereError("Send queue timeout", "SEND_QUEUE_TIMEOUT"));
|
|
13337
13527
|
if (entries.length === 0) this.queues.delete(coinId);
|
|
13338
13528
|
}
|
|
13339
|
-
// Whole-token semantics: only fully-free tokens enter the selector pool.
|
|
13529
|
+
// Whole-token semantics: only fully-free tokens enter the selector pool. What
|
|
13530
|
+
// the reservations hold back is counted too — the refusal has to name it (#737).
|
|
13340
13531
|
freeView(coinId) {
|
|
13341
13532
|
const freeView = [];
|
|
13342
13533
|
let freeTotal = 0n;
|
|
13534
|
+
let pinnedTotal = 0n;
|
|
13535
|
+
const holders = /* @__PURE__ */ new Set();
|
|
13536
|
+
const unproven = this.deps.ledger.unprovenReason();
|
|
13343
13537
|
for (const entry of this.deps.getPool(coinId)) {
|
|
13344
13538
|
if (entry.amount <= 0n) continue;
|
|
13345
|
-
|
|
13539
|
+
const holder = unproven === null ? this.deps.ledger.holderOf(entry.tokenId) : entry.tokenId;
|
|
13540
|
+
if (holder === void 0) {
|
|
13346
13541
|
freeView.push(entry);
|
|
13347
13542
|
freeTotal += entry.amount;
|
|
13543
|
+
} else {
|
|
13544
|
+
pinnedTotal += entry.amount;
|
|
13545
|
+
holders.add(holder);
|
|
13348
13546
|
}
|
|
13349
13547
|
}
|
|
13350
|
-
return { freeView, freeTotal };
|
|
13548
|
+
return { freeView, freeTotal, pinnedTotal, pinnedBy: holders.size, unproven };
|
|
13351
13549
|
}
|
|
13352
13550
|
expectedChangeTotal(coinId) {
|
|
13353
13551
|
let total = 0n;
|
|
@@ -13389,13 +13587,17 @@ function supportsDeterministicMint(engine) {
|
|
|
13389
13587
|
}
|
|
13390
13588
|
function composeFacadeParts(deps, hooks) {
|
|
13391
13589
|
const ownPubkeyBytes = hexToBytes4(deps.ownPubkey);
|
|
13590
|
+
const ledger = new ReservationLedger();
|
|
13392
13591
|
const view = new InventoryView({
|
|
13393
13592
|
port: deps.storagePort,
|
|
13394
13593
|
kv: deps.kv,
|
|
13395
13594
|
emit: (event) => deps.emit(event, {}),
|
|
13595
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
13596
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
13597
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
13598
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
13396
13599
|
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
13397
13600
|
});
|
|
13398
|
-
const ledger = new ReservationLedger();
|
|
13399
13601
|
const queue = new SpendQueue({
|
|
13400
13602
|
ledger,
|
|
13401
13603
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -13416,6 +13618,7 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13416
13618
|
ownPubkeyBytes,
|
|
13417
13619
|
view,
|
|
13418
13620
|
ledger,
|
|
13621
|
+
pins: buildPins(deps, hooks, { ledger, view, machineStores, machineDeps }),
|
|
13419
13622
|
queue,
|
|
13420
13623
|
historyStore,
|
|
13421
13624
|
machineStores,
|
|
@@ -13427,6 +13630,17 @@ function composeFacadeParts(deps, hooks) {
|
|
|
13427
13630
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
13428
13631
|
};
|
|
13429
13632
|
}
|
|
13633
|
+
function buildPins(deps, hooks, parts) {
|
|
13634
|
+
return new IntentPins({
|
|
13635
|
+
ledger: parts.ledger,
|
|
13636
|
+
openIntents: () => deriveOpenIntentHolds(parts.machineStores, parts.machineDeps.decryptPayload),
|
|
13637
|
+
isActive: (transferId) => hooks.isActiveOp(transferId),
|
|
13638
|
+
release: (tokenId) => {
|
|
13639
|
+
parts.view.release(tokenId);
|
|
13640
|
+
},
|
|
13641
|
+
changed: () => deps.emit("inventory:updated", {})
|
|
13642
|
+
});
|
|
13643
|
+
}
|
|
13430
13644
|
function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
13431
13645
|
return {
|
|
13432
13646
|
stores: machineStores,
|
|
@@ -13536,7 +13750,8 @@ var PaymentsFacade = class {
|
|
|
13536
13750
|
this.deps = deps;
|
|
13537
13751
|
const parts = composeFacadeParts(deps, {
|
|
13538
13752
|
engine: () => this.engine(),
|
|
13539
|
-
send: (request) => this.send(request)
|
|
13753
|
+
send: (request) => this.send(request),
|
|
13754
|
+
isActiveOp: (id) => this.activeMoneyOps.has(id)
|
|
13540
13755
|
});
|
|
13541
13756
|
this.view = parts.view;
|
|
13542
13757
|
this.ledger = parts.ledger;
|
|
@@ -13550,6 +13765,7 @@ var PaymentsFacade = class {
|
|
|
13550
13765
|
this.ownPubkeyBytes = parts.ownPubkeyBytes;
|
|
13551
13766
|
this.restoreDeps = parts.restoreDeps;
|
|
13552
13767
|
this.requests = parts.requests;
|
|
13768
|
+
this.pins = parts.pins;
|
|
13553
13769
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
13554
13770
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
13555
13771
|
now: () => this.nowMs(),
|
|
@@ -13592,6 +13808,8 @@ var PaymentsFacade = class {
|
|
|
13592
13808
|
passChain = new SerialChain();
|
|
13593
13809
|
/** §7 ownership: ids with an in-process machine attempt — the pass never adopts one. */
|
|
13594
13810
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
13811
|
+
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
13812
|
+
pins;
|
|
13595
13813
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
13596
13814
|
async start() {
|
|
13597
13815
|
if (this.started) return;
|
|
@@ -13614,6 +13832,7 @@ var PaymentsFacade = class {
|
|
|
13614
13832
|
});
|
|
13615
13833
|
if (statusUnsub !== void 0) this.unsubscribers.push(statusUnsub);
|
|
13616
13834
|
await this.deps.session.start();
|
|
13835
|
+
await this.pins.sync().catch(() => void 0);
|
|
13617
13836
|
this.trackTail(this.runConvergencePass());
|
|
13618
13837
|
this.trackTail(this.seedHeldStates());
|
|
13619
13838
|
await this.view.fullPull().catch(() => void 0);
|
|
@@ -13684,6 +13903,7 @@ var PaymentsFacade = class {
|
|
|
13684
13903
|
}
|
|
13685
13904
|
async convergeBody() {
|
|
13686
13905
|
const outcome = await this.converger.convergeOnce();
|
|
13906
|
+
await this.pins.sync().catch(() => void 0);
|
|
13687
13907
|
const pending = await this.converger.pendingWork().catch(() => true);
|
|
13688
13908
|
this.heartbeat.settle(outcome, pending);
|
|
13689
13909
|
}
|
|
@@ -13923,6 +14143,7 @@ var PaymentsFacade = class {
|
|
|
13923
14143
|
/** Open intent: sources stay reserved + in-flight (§5.2) until resume adopts them. */
|
|
13924
14144
|
settleKeepOpen(ctx) {
|
|
13925
14145
|
this.queue.clearExpectedChange(ctx.transferId);
|
|
14146
|
+
this.pins.adopt(ctx.transferId);
|
|
13926
14147
|
this.heartbeat.arm();
|
|
13927
14148
|
}
|
|
13928
14149
|
settlePartial(ctx, partial) {
|