@unicitylabs/sphere-sdk 0.18.0-dev.2 → 0.18.0-dev.3
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/chunks/{chunk-OAAY2JEA.js → chunk-7BZL7HE2.js} +3 -3
- package/dist/connect/chunks/{chunk-MQQ3TMHM.js → chunk-CJ4D6M6W.js} +2 -2
- package/dist/connect/chunks/{chunk-FGQ6CKKK.js → chunk-LDEOQNG2.js} +2 -2
- package/dist/connect/chunks/{chunk-FGQ6CKKK.js.map → chunk-LDEOQNG2.js.map} +1 -1
- package/dist/connect/chunks/{chunk-3CYK5SIU.js → chunk-WCKTUB4V.js} +1 -1
- package/dist/connect/chunks/chunk-WCKTUB4V.js.map +1 -0
- package/dist/connect/chunks/{chunk-JYBI36GK.js → chunk-WL6VYP7G.js} +2 -2
- package/dist/connect/index.cjs +1 -1
- package/dist/connect/index.cjs.map +1 -1
- package/dist/connect/index.js +5 -5
- package/dist/connect/internal/host.js +3 -3
- package/dist/connect/internal/nft-wire.js +2 -2
- package/dist/core/index.cjs +831 -343
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +30 -1
- package/dist/core/index.d.ts +30 -1
- package/dist/core/index.js +831 -343
- 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 +2 -2
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +1 -0
- package/dist/impl/nodejs/index.d.ts +1 -0
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/impl/shared/wallet-api/index.d.cts +1 -0
- package/dist/impl/shared/wallet-api/index.d.ts +1 -0
- package/dist/impl/wallet-api-v2/index.cjs +2 -1
- package/dist/impl/wallet-api-v2/index.cjs.map +1 -1
- package/dist/impl/wallet-api-v2/index.d.cts +3 -0
- package/dist/impl/wallet-api-v2/index.d.ts +3 -0
- package/dist/impl/wallet-api-v2/index.js +2 -1
- package/dist/impl/wallet-api-v2/index.js.map +1 -1
- package/dist/index.cjs +833 -343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +832 -343
- package/dist/index.js.map +1 -1
- package/dist/modules/payments-v2/index.cjs +658 -259
- package/dist/modules/payments-v2/index.cjs.map +1 -1
- package/dist/modules/payments-v2/index.d.cts +65 -2
- package/dist/modules/payments-v2/index.d.ts +65 -2
- package/dist/modules/payments-v2/index.js +656 -259
- package/dist/modules/payments-v2/index.js.map +1 -1
- package/dist/token-engine/index.cjs +185 -65
- package/dist/token-engine/index.cjs.map +1 -1
- package/dist/token-engine/index.d.cts +20 -3
- package/dist/token-engine/index.d.ts +20 -3
- package/dist/token-engine/index.js +184 -65
- package/dist/token-engine/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/connect/chunks/chunk-3CYK5SIU.js.map +0 -1
- /package/dist/connect/chunks/{chunk-OAAY2JEA.js.map → chunk-7BZL7HE2.js.map} +0 -0
- /package/dist/connect/chunks/{chunk-MQQ3TMHM.js.map → chunk-CJ4D6M6W.js.map} +0 -0
- /package/dist/connect/chunks/{chunk-JYBI36GK.js.map → chunk-WL6VYP7G.js.map} +0 -0
|
@@ -33,6 +33,7 @@ __export(payments_v2_exports, {
|
|
|
33
33
|
ATTENTION_MINT_UNRESOLVED: () => ATTENTION_MINT_UNRESOLVED,
|
|
34
34
|
ATTENTION_RECIPIENT_NETWORK_UNVERIFIED: () => ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
|
|
35
35
|
ATTENTION_RESEED_REJECTED: () => ATTENTION_RESEED_REJECTED,
|
|
36
|
+
CUSTOM_MINT_MAX_PAYLOAD_BYTES: () => CUSTOM_MINT_MAX_PAYLOAD_BYTES,
|
|
36
37
|
HEARTBEAT_CAP_MS: () => HEARTBEAT_CAP_MS,
|
|
37
38
|
HEARTBEAT_SEED_MS: () => HEARTBEAT_SEED_MS,
|
|
38
39
|
MAX_RESELECT: () => MAX_RESELECT,
|
|
@@ -46,6 +47,7 @@ __export(payments_v2_exports, {
|
|
|
46
47
|
STORE_KEYS: () => STORE_KEYS,
|
|
47
48
|
createScopedKV: () => createScopedKV,
|
|
48
49
|
encodeNftContent: () => encodeNftContent,
|
|
50
|
+
isLiveBurn: () => isLiveBurn,
|
|
49
51
|
isNftDocumentLink: () => isNftDocumentLink,
|
|
50
52
|
isRetryableAckError: () => isRetryableAckError,
|
|
51
53
|
parseNftDocument: () => parseNftDocument,
|
|
@@ -1753,6 +1755,33 @@ function decryptField(key, envelope) {
|
|
|
1753
1755
|
return new TextDecoder().decode(decryptFieldBytes(key, envelope));
|
|
1754
1756
|
}
|
|
1755
1757
|
|
|
1758
|
+
// token-engine/claims.ts
|
|
1759
|
+
var CoinClaims = class _CoinClaims {
|
|
1760
|
+
issuers = /* @__PURE__ */ new Map();
|
|
1761
|
+
policies = [];
|
|
1762
|
+
static fromPlugins(plugins) {
|
|
1763
|
+
const claims = new _CoinClaims();
|
|
1764
|
+
for (const plugin of plugins ?? []) {
|
|
1765
|
+
for (const policy of plugin.tokenIssuancePolicies ?? []) claims.add(policy);
|
|
1766
|
+
}
|
|
1767
|
+
return claims;
|
|
1768
|
+
}
|
|
1769
|
+
add(policy) {
|
|
1770
|
+
const tokenType = import_HexConverter.HexConverter.encode(policy.tokenType.bytes);
|
|
1771
|
+
const coinIds = policy.coinIds.map((coinId) => coinId.toLowerCase());
|
|
1772
|
+
const taken = coinIds.find((coinId) => this.issuers.has(coinId));
|
|
1773
|
+
if (taken !== void 0) throw new Error(`Coin ${taken} is already issued by token type ${this.issuers.get(taken)}.`);
|
|
1774
|
+
for (const coinId of coinIds) this.issuers.set(coinId, tokenType);
|
|
1775
|
+
this.policies.push(JSON.stringify([tokenType, [...coinIds].sort(), policy.revision ?? ""]));
|
|
1776
|
+
}
|
|
1777
|
+
fingerprint() {
|
|
1778
|
+
return [...this.policies].sort().join("\n");
|
|
1779
|
+
}
|
|
1780
|
+
issuerOf(coinId) {
|
|
1781
|
+
return this.issuers.get(coinId.toLowerCase()) ?? null;
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
|
|
1756
1785
|
// modules/payments-v2/machine/payload-view.ts
|
|
1757
1786
|
function isWholeIntent(p) {
|
|
1758
1787
|
return p.kind === "whole" || p.kind === "coinless";
|
|
@@ -2030,6 +2059,9 @@ function createScopedKV(storage, network, chainPubkey) {
|
|
|
2030
2059
|
}
|
|
2031
2060
|
};
|
|
2032
2061
|
}
|
|
2062
|
+
function isLiveBurn(entry) {
|
|
2063
|
+
return !entry.settled && entry.failure === void 0;
|
|
2064
|
+
}
|
|
2033
2065
|
var STORE_KEYS = {
|
|
2034
2066
|
intentBackstop: "intents",
|
|
2035
2067
|
checkpointCache: "checkpoints",
|
|
@@ -2044,7 +2076,8 @@ var STORE_KEYS = {
|
|
|
2044
2076
|
epochLatch: "epoch-latch",
|
|
2045
2077
|
// §5.2 InventoryView durable overlays (#625/#679).
|
|
2046
2078
|
suspectedSpent: "suspected-spent",
|
|
2047
|
-
knownSpends: "known-spends"
|
|
2079
|
+
knownSpends: "known-spends",
|
|
2080
|
+
verifiedTokens: "verified-tokens"
|
|
2048
2081
|
};
|
|
2049
2082
|
|
|
2050
2083
|
// modules/payments-v2/inventory/presentation.ts
|
|
@@ -2062,18 +2095,21 @@ function toToken(tokenId, snapshot, asset, registry, flags) {
|
|
|
2062
2095
|
createdAt: snapshot.createdAt,
|
|
2063
2096
|
updatedAt: snapshot.updatedAt,
|
|
2064
2097
|
lazy: true,
|
|
2065
|
-
...flags.suspectedSpent ? { suspectedSpent: true } : {}
|
|
2098
|
+
...flags.suspectedSpent ? { suspectedSpent: true } : {},
|
|
2099
|
+
...flags.standing !== "trusted" ? { unverified: flags.standing } : {}
|
|
2066
2100
|
};
|
|
2067
2101
|
}
|
|
2068
|
-
function aggregateAssets(entries,
|
|
2102
|
+
function aggregateAssets(entries, reading, registry) {
|
|
2069
2103
|
const groups = /* @__PURE__ */ new Map();
|
|
2070
2104
|
for (const [tokenId, entry] of entries) {
|
|
2071
|
-
const moving = isTransferring(tokenId);
|
|
2105
|
+
const moving = reading.isTransferring(tokenId);
|
|
2072
2106
|
for (const asset of entry.assets) {
|
|
2073
|
-
|
|
2107
|
+
const standing = reading.standing(tokenId, asset.coinId);
|
|
2108
|
+
const key = standing === "trusted" ? asset.coinId : `${asset.coinId}:${standing}`;
|
|
2109
|
+
let group = groups.get(key);
|
|
2074
2110
|
if (!group) {
|
|
2075
|
-
group = { total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2076
|
-
groups.set(
|
|
2111
|
+
group = { coinId: asset.coinId, standing, total: 0n, count: 0, transferring: 0n, transferringCount: 0 };
|
|
2112
|
+
groups.set(key, group);
|
|
2077
2113
|
}
|
|
2078
2114
|
if (moving) {
|
|
2079
2115
|
group.transferring += BigInt(asset.amount);
|
|
@@ -2084,9 +2120,10 @@ function aggregateAssets(entries, isTransferring, registry) {
|
|
|
2084
2120
|
}
|
|
2085
2121
|
}
|
|
2086
2122
|
}
|
|
2087
|
-
return [...groups].map((
|
|
2123
|
+
return [...groups.values()].map((totals) => toAsset(totals, registry));
|
|
2088
2124
|
}
|
|
2089
|
-
function toAsset(
|
|
2125
|
+
function toAsset(totals, registry) {
|
|
2126
|
+
const coinId = totals.coinId;
|
|
2090
2127
|
const iconUrl = registry.getIconUrl(coinId);
|
|
2091
2128
|
return {
|
|
2092
2129
|
coinId,
|
|
@@ -2106,7 +2143,8 @@ function toAsset(coinId, totals, registry) {
|
|
|
2106
2143
|
priceEur: null,
|
|
2107
2144
|
change24h: null,
|
|
2108
2145
|
fiatValueUsd: null,
|
|
2109
|
-
fiatValueEur: null
|
|
2146
|
+
fiatValueEur: null,
|
|
2147
|
+
...totals.standing !== "trusted" ? { unverified: totals.standing } : {}
|
|
2110
2148
|
};
|
|
2111
2149
|
}
|
|
2112
2150
|
function priceIdOf(registry, coinId) {
|
|
@@ -2114,9 +2152,9 @@ function priceIdOf(registry, coinId) {
|
|
|
2114
2152
|
}
|
|
2115
2153
|
async function withPrices(raw, registry, price) {
|
|
2116
2154
|
try {
|
|
2117
|
-
const ids = [...new Set(raw.map((a) => priceIdOf(registry, a.coinId)))];
|
|
2155
|
+
const ids = [...new Set(raw.filter((a) => !a.unverified).map((a) => priceIdOf(registry, a.coinId)))];
|
|
2118
2156
|
const quotes = await price.getPrices(ids);
|
|
2119
|
-
return raw.map((a) => priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
2157
|
+
return raw.map((a) => a.unverified ? a : priceOne(a, quotes.get(priceIdOf(registry, a.coinId))));
|
|
2120
2158
|
} catch {
|
|
2121
2159
|
return raw;
|
|
2122
2160
|
}
|
|
@@ -2137,11 +2175,13 @@ function transferringToken(tokenId, coinId, amount, registry, now) {
|
|
|
2137
2175
|
const snapshot = { assets: [], createdAt: now, updatedAt: now };
|
|
2138
2176
|
return toToken(tokenId, snapshot, { coinId, amount: amount.toString() }, registry, {
|
|
2139
2177
|
transferring: true,
|
|
2140
|
-
suspectedSpent: false
|
|
2178
|
+
suspectedSpent: false,
|
|
2179
|
+
standing: "trusted"
|
|
2141
2180
|
});
|
|
2142
2181
|
}
|
|
2143
2182
|
|
|
2144
2183
|
// modules/payments-v2/inventory/InventoryView.ts
|
|
2184
|
+
var TRUST_ALL = { isClaimed: () => false, standing: () => "trusted" };
|
|
2145
2185
|
var SUSPECTED_KEY = STORE_KEYS.suspectedSpent;
|
|
2146
2186
|
var KNOWN_SPENDS_KEY = STORE_KEYS.knownSpends;
|
|
2147
2187
|
var CURSOR_KEY = STORE_KEYS.streamCursor("inventory");
|
|
@@ -2235,6 +2275,12 @@ var InventoryView = class {
|
|
|
2235
2275
|
for (const tokenId of tokenIds) if (this.inFlight.delete(tokenId)) changed = true;
|
|
2236
2276
|
if (changed) this.deps.emit("inventory:updated");
|
|
2237
2277
|
}
|
|
2278
|
+
get trust() {
|
|
2279
|
+
return this.deps.trust ?? TRUST_ALL;
|
|
2280
|
+
}
|
|
2281
|
+
trusts(tokenId, coinId) {
|
|
2282
|
+
return this.trust.standing(tokenId, coinId) === "trusted";
|
|
2283
|
+
}
|
|
2238
2284
|
pinned(tokenId) {
|
|
2239
2285
|
return this.deps.isPinned?.(tokenId) ?? false;
|
|
2240
2286
|
}
|
|
@@ -2261,6 +2307,7 @@ var InventoryView = class {
|
|
|
2261
2307
|
isSpendable(tokenId, entry) {
|
|
2262
2308
|
if (entry.status !== "active") return false;
|
|
2263
2309
|
if (this.inFlight.has(tokenId) && !this.pinned(tokenId)) return false;
|
|
2310
|
+
if (!entry.assets.every((asset) => this.trusts(tokenId, asset.coinId))) return false;
|
|
2264
2311
|
return !this.suspected.has(stateKey(tokenId, entry.stateHash));
|
|
2265
2312
|
}
|
|
2266
2313
|
/** Is this NAMED token spendable whole — coinless or valued (#777)? */
|
|
@@ -2287,7 +2334,8 @@ var InventoryView = class {
|
|
|
2287
2334
|
out.push(
|
|
2288
2335
|
toToken(tokenId, entry, asset, registry, {
|
|
2289
2336
|
transferring: this.held(tokenId),
|
|
2290
|
-
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash))
|
|
2337
|
+
suspectedSpent: this.suspected.has(stateKey(tokenId, entry.stateHash)),
|
|
2338
|
+
standing: this.trust.standing(tokenId, asset.coinId)
|
|
2291
2339
|
})
|
|
2292
2340
|
);
|
|
2293
2341
|
}
|
|
@@ -2317,10 +2365,21 @@ var InventoryView = class {
|
|
|
2317
2365
|
return out;
|
|
2318
2366
|
}
|
|
2319
2367
|
async assets(registry, price) {
|
|
2320
|
-
const
|
|
2368
|
+
const reading = {
|
|
2369
|
+
isTransferring: (tokenId) => this.held(tokenId),
|
|
2370
|
+
standing: (tokenId, coinId) => this.trust.standing(tokenId, coinId)
|
|
2371
|
+
};
|
|
2372
|
+
const raw = aggregateAssets(this.activeEntries(), reading, registry);
|
|
2321
2373
|
if (!price || raw.length === 0) return raw;
|
|
2322
2374
|
return withPrices(raw, registry, price);
|
|
2323
2375
|
}
|
|
2376
|
+
claimHolders() {
|
|
2377
|
+
const out = [];
|
|
2378
|
+
for (const [tokenId, entry] of this.activeEntries()) {
|
|
2379
|
+
if (entry.assets.some((asset) => this.trust.isClaimed(asset.coinId))) out.push(tokenId);
|
|
2380
|
+
}
|
|
2381
|
+
return out;
|
|
2382
|
+
}
|
|
2324
2383
|
*activeEntries() {
|
|
2325
2384
|
for (const [tokenId, entry] of this.mirror) {
|
|
2326
2385
|
if (entry.status === "active") yield [tokenId, entry];
|
|
@@ -2490,6 +2549,171 @@ var InventoryView = class {
|
|
|
2490
2549
|
}
|
|
2491
2550
|
};
|
|
2492
2551
|
|
|
2552
|
+
// modules/payments-v2/inventory/verdicts.ts
|
|
2553
|
+
var VERDICT_RETRY_MS = 3e4;
|
|
2554
|
+
var VERDICT_RETRY_MAX_MS = 60 * 60 * 1e3;
|
|
2555
|
+
function honoursClaims(claims, token) {
|
|
2556
|
+
const issuers = (token.value?.assets ?? []).map((asset) => claims.issuerOf(asset.coinId));
|
|
2557
|
+
return issuers.some((issuer) => issuer !== null) && issuers.every((issuer) => issuer === null || issuer === token.tokenType);
|
|
2558
|
+
}
|
|
2559
|
+
var TokenVerdicts = class {
|
|
2560
|
+
constructor(deps) {
|
|
2561
|
+
this.deps = deps;
|
|
2562
|
+
}
|
|
2563
|
+
verified = /* @__PURE__ */ new Set();
|
|
2564
|
+
refused = /* @__PURE__ */ new Set();
|
|
2565
|
+
retries = /* @__PURE__ */ new Map();
|
|
2566
|
+
writes = new SerialChain();
|
|
2567
|
+
hydration = null;
|
|
2568
|
+
queued = null;
|
|
2569
|
+
running = null;
|
|
2570
|
+
held = [];
|
|
2571
|
+
timer = null;
|
|
2572
|
+
live = false;
|
|
2573
|
+
isClaimed(coinId) {
|
|
2574
|
+
return this.deps.claims.issuerOf(coinId) !== null;
|
|
2575
|
+
}
|
|
2576
|
+
standing(tokenId, coinId) {
|
|
2577
|
+
if (!this.isClaimed(coinId) || this.verified.has(tokenId)) return "trusted";
|
|
2578
|
+
return this.refused.has(tokenId) ? "refused" : "pending";
|
|
2579
|
+
}
|
|
2580
|
+
trusts(tokenId, coinId) {
|
|
2581
|
+
return this.standing(tokenId, coinId) === "trusted";
|
|
2582
|
+
}
|
|
2583
|
+
hydrate() {
|
|
2584
|
+
this.hydration ??= this.load().catch((error) => {
|
|
2585
|
+
this.hydration = null;
|
|
2586
|
+
throw error;
|
|
2587
|
+
});
|
|
2588
|
+
return this.hydration;
|
|
2589
|
+
}
|
|
2590
|
+
start() {
|
|
2591
|
+
this.live = true;
|
|
2592
|
+
return this.hydrate();
|
|
2593
|
+
}
|
|
2594
|
+
stop() {
|
|
2595
|
+
this.live = false;
|
|
2596
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
2597
|
+
this.timer = null;
|
|
2598
|
+
}
|
|
2599
|
+
async accept(token) {
|
|
2600
|
+
if (honoursClaims(this.deps.claims, token)) await this.remember(token.blob.tokenId);
|
|
2601
|
+
}
|
|
2602
|
+
async recordSplit(sourceTokenId, outputTokenId) {
|
|
2603
|
+
await this.hydrate();
|
|
2604
|
+
if (this.verified.has(sourceTokenId)) await this.remember(outputTokenId);
|
|
2605
|
+
}
|
|
2606
|
+
review(holders) {
|
|
2607
|
+
this.queued = holders;
|
|
2608
|
+
this.running ??= this.drain();
|
|
2609
|
+
return this.running;
|
|
2610
|
+
}
|
|
2611
|
+
async drain() {
|
|
2612
|
+
try {
|
|
2613
|
+
while (this.queued !== null) {
|
|
2614
|
+
const holders = this.queued;
|
|
2615
|
+
this.queued = null;
|
|
2616
|
+
await this.reviewOnce(holders);
|
|
2617
|
+
}
|
|
2618
|
+
} finally {
|
|
2619
|
+
this.running = null;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
async reviewOnce(holders) {
|
|
2623
|
+
await this.hydrate();
|
|
2624
|
+
this.held = holders;
|
|
2625
|
+
const due = holders.filter((tokenId) => this.isDue(tokenId));
|
|
2626
|
+
const verdicts = due.length === 0 ? /* @__PURE__ */ new Map() : await this.check(due);
|
|
2627
|
+
for (const tokenId of due) this.settle(tokenId, verdicts.get(tokenId) ?? "retry");
|
|
2628
|
+
if (due.some((tokenId) => verdicts.get(tokenId) === "verified")) {
|
|
2629
|
+
await this.persist();
|
|
2630
|
+
this.deps.changed();
|
|
2631
|
+
}
|
|
2632
|
+
this.scheduleRetry();
|
|
2633
|
+
}
|
|
2634
|
+
isDue(tokenId) {
|
|
2635
|
+
if (this.verified.has(tokenId) || this.refused.has(tokenId)) return false;
|
|
2636
|
+
return (this.retries.get(tokenId)?.atMs ?? 0) <= this.now();
|
|
2637
|
+
}
|
|
2638
|
+
async check(due) {
|
|
2639
|
+
const verdicts = /* @__PURE__ */ new Map();
|
|
2640
|
+
let blobs;
|
|
2641
|
+
try {
|
|
2642
|
+
blobs = await this.deps.getBlobs([...due]);
|
|
2643
|
+
} catch {
|
|
2644
|
+
return verdicts;
|
|
2645
|
+
}
|
|
2646
|
+
for (const tokenId of due) {
|
|
2647
|
+
const bytes = blobs.get(tokenId);
|
|
2648
|
+
if (bytes !== void 0) verdicts.set(tokenId, await this.verdictOf(tokenId, bytes));
|
|
2649
|
+
}
|
|
2650
|
+
return verdicts;
|
|
2651
|
+
}
|
|
2652
|
+
async verdictOf(tokenId, bytes) {
|
|
2653
|
+
let engine;
|
|
2654
|
+
try {
|
|
2655
|
+
engine = this.deps.engine();
|
|
2656
|
+
} catch {
|
|
2657
|
+
return "retry";
|
|
2658
|
+
}
|
|
2659
|
+
let token;
|
|
2660
|
+
try {
|
|
2661
|
+
token = await engine.decodeToken({ tokenId, token: bytes });
|
|
2662
|
+
} catch {
|
|
2663
|
+
return "refused";
|
|
2664
|
+
}
|
|
2665
|
+
if (token.blob.tokenId !== tokenId || !honoursClaims(this.deps.claims, token)) return "refused";
|
|
2666
|
+
try {
|
|
2667
|
+
return (await engine.verify(token)).ok ? "verified" : "refused";
|
|
2668
|
+
} catch {
|
|
2669
|
+
return "retry";
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
settle(tokenId, verdict) {
|
|
2673
|
+
if (verdict === "retry") {
|
|
2674
|
+
this.retries.set(tokenId, this.nextRetry(this.retries.get(tokenId)));
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
this.retries.delete(tokenId);
|
|
2678
|
+
(verdict === "verified" ? this.verified : this.refused).add(tokenId);
|
|
2679
|
+
}
|
|
2680
|
+
nextRetry(previous) {
|
|
2681
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
2682
|
+
const delay = Math.min(VERDICT_RETRY_MS * 2 ** (attempts - 1), VERDICT_RETRY_MAX_MS);
|
|
2683
|
+
return { attempts, atMs: this.now() + delay };
|
|
2684
|
+
}
|
|
2685
|
+
scheduleRetry() {
|
|
2686
|
+
if (!this.live || this.timer !== null) return;
|
|
2687
|
+
const due = this.held.flatMap((tokenId) => this.retries.get(tokenId)?.atMs ?? []);
|
|
2688
|
+
if (due.length === 0) return;
|
|
2689
|
+
const delay = Math.max(0, due.reduce((a, b) => Math.min(a, b)) - this.now());
|
|
2690
|
+
this.timer = setTimeout(() => {
|
|
2691
|
+
this.timer = null;
|
|
2692
|
+
void this.review(this.held).catch(() => void 0);
|
|
2693
|
+
}, delay);
|
|
2694
|
+
}
|
|
2695
|
+
async remember(tokenId) {
|
|
2696
|
+
await this.hydrate();
|
|
2697
|
+
if (this.verified.has(tokenId)) return;
|
|
2698
|
+
this.verified.add(tokenId);
|
|
2699
|
+
this.refused.delete(tokenId);
|
|
2700
|
+
this.retries.delete(tokenId);
|
|
2701
|
+
await this.persist();
|
|
2702
|
+
}
|
|
2703
|
+
persist() {
|
|
2704
|
+
const remembered = { fingerprint: this.deps.claims.fingerprint(), tokenIds: [...this.verified] };
|
|
2705
|
+
return this.writes.enqueue(() => this.deps.kv.set(STORE_KEYS.verifiedTokens, remembered));
|
|
2706
|
+
}
|
|
2707
|
+
async load() {
|
|
2708
|
+
const remembered = await this.deps.kv.get(STORE_KEYS.verifiedTokens);
|
|
2709
|
+
if (remembered?.fingerprint !== this.deps.claims.fingerprint()) return;
|
|
2710
|
+
for (const tokenId of remembered.tokenIds) this.verified.add(tokenId);
|
|
2711
|
+
}
|
|
2712
|
+
now() {
|
|
2713
|
+
return (this.deps.now ?? Date.now)();
|
|
2714
|
+
}
|
|
2715
|
+
};
|
|
2716
|
+
|
|
2493
2717
|
// token-engine/value-envelope.ts
|
|
2494
2718
|
function isCoinlessEnvelope(envelope) {
|
|
2495
2719
|
return envelope.startsWith("none_");
|
|
@@ -2504,6 +2728,8 @@ function isRetryableAckError(err) {
|
|
|
2504
2728
|
var ACK_BATCH_SIZE = 200;
|
|
2505
2729
|
var REFRESH_INTERVAL_MS = 2500;
|
|
2506
2730
|
var POLL_INTERVAL_MS = 3e4;
|
|
2731
|
+
var RECHECK_BASE_MS = POLL_INTERVAL_MS;
|
|
2732
|
+
var RECHECK_MAX_MS = 60 * 60 * 1e3;
|
|
2507
2733
|
var ATTENTION_CLAIM_CONFLICT = "claim:conflict";
|
|
2508
2734
|
function receivedDedupKey(tokenId, stateHash) {
|
|
2509
2735
|
return `RECEIVED:${tokenId.toLowerCase()}:${stateHash.toLowerCase()}`;
|
|
@@ -2520,6 +2746,7 @@ var Receive = class {
|
|
|
2520
2746
|
pollTimer = null;
|
|
2521
2747
|
unsubscribeWake = null;
|
|
2522
2748
|
lastRefreshAt = 0;
|
|
2749
|
+
rechecks = /* @__PURE__ */ new Map();
|
|
2523
2750
|
drainOnce() {
|
|
2524
2751
|
return this.drainFlight.run(() => this.doDrain());
|
|
2525
2752
|
}
|
|
@@ -2578,6 +2805,7 @@ var Receive = class {
|
|
|
2578
2805
|
const pending = [];
|
|
2579
2806
|
const pageEpoch = () => deps.delivery.incomingEpoch() ?? deps.syncEpoch();
|
|
2580
2807
|
const claimable = { n: 0 };
|
|
2808
|
+
const deferred = { any: false };
|
|
2581
2809
|
const finish = () => {
|
|
2582
2810
|
if (claimable.n > 0) deps.refreshView?.();
|
|
2583
2811
|
return stored;
|
|
@@ -2586,7 +2814,7 @@ var Receive = class {
|
|
|
2586
2814
|
const record = await deps.kv.get(STORE_KEYS.streamCursor("mailbox"));
|
|
2587
2815
|
let basis = record !== null && record.syncEpoch === deps.syncEpoch() ? record : null;
|
|
2588
2816
|
for (let pass = 0; pass < 2; pass++) {
|
|
2589
|
-
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable }, basis);
|
|
2817
|
+
await this.drainPages({ deps, engine, pending, stored, pageEpoch, claimable, deferred }, basis);
|
|
2590
2818
|
const served = deps.delivery.incomingEpoch();
|
|
2591
2819
|
if (basis === null || served === null || served === basis.syncEpoch) break;
|
|
2592
2820
|
await deps.kv.remove(STORE_KEYS.streamCursor("mailbox"));
|
|
@@ -2603,20 +2831,41 @@ var Receive = class {
|
|
|
2603
2831
|
return finish();
|
|
2604
2832
|
}
|
|
2605
2833
|
async processEntry(ctx, entry) {
|
|
2606
|
-
const { deps, engine,
|
|
2607
|
-
const screened = await screen(deps, engine, entry);
|
|
2834
|
+
const { deps, engine, stored, claimable } = ctx;
|
|
2835
|
+
const screened = this.recheckDue(entry.deliveryId) ? await screen(deps, engine, entry) : { kind: "defer" };
|
|
2836
|
+
this.scheduleRecheck(entry.deliveryId, screened.kind === "defer");
|
|
2837
|
+
if (screened.kind === "defer") {
|
|
2838
|
+
ctx.deferred.any = true;
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2608
2841
|
if (screened.kind === "ack") {
|
|
2609
|
-
|
|
2842
|
+
queueAck(ctx, screened.ack);
|
|
2610
2843
|
if (screened.ack.disposition === "claimed") claimable.n += 1;
|
|
2611
2844
|
return;
|
|
2612
2845
|
}
|
|
2846
|
+
await deps.accepted?.(screened.token);
|
|
2613
2847
|
await deps.view.store(screened.record);
|
|
2614
|
-
|
|
2848
|
+
queueAck(ctx, claimAck(entry));
|
|
2615
2849
|
claimable.n += 1;
|
|
2616
2850
|
const transfer = await announce(deps, entry, screened.record);
|
|
2617
2851
|
stored.push(transfer);
|
|
2618
2852
|
deps.emit("transfer:incoming", transfer);
|
|
2619
2853
|
}
|
|
2854
|
+
recheckDue(deliveryId) {
|
|
2855
|
+
const recheck = this.rechecks.get(deliveryId);
|
|
2856
|
+
return recheck === void 0 || (this.deps.now?.() ?? Date.now()) >= recheck.atMs;
|
|
2857
|
+
}
|
|
2858
|
+
scheduleRecheck(deliveryId, deferred) {
|
|
2859
|
+
if (!deferred) {
|
|
2860
|
+
this.rechecks.delete(deliveryId);
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
const previous = this.rechecks.get(deliveryId);
|
|
2864
|
+
if (previous !== void 0 && (this.deps.now?.() ?? Date.now()) < previous.atMs) return;
|
|
2865
|
+
const attempts = (previous?.attempts ?? 0) + 1;
|
|
2866
|
+
const delay = Math.min(RECHECK_BASE_MS * 2 ** (attempts - 1), RECHECK_MAX_MS);
|
|
2867
|
+
this.rechecks.set(deliveryId, { attempts, atMs: (this.deps.now?.() ?? Date.now()) + delay });
|
|
2868
|
+
}
|
|
2620
2869
|
};
|
|
2621
2870
|
async function screen(deps, engine, entry) {
|
|
2622
2871
|
const blobBytes = await entry.fetchBlob();
|
|
@@ -2627,7 +2876,8 @@ async function screen(deps, engine, entry) {
|
|
|
2627
2876
|
logger.warn("PaymentsV2", `receive: undecodable blob rejected \u2014 ${String(err)}`);
|
|
2628
2877
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2629
2878
|
}
|
|
2630
|
-
const verdict = await engine
|
|
2879
|
+
const verdict = await verdictOf(engine, token);
|
|
2880
|
+
if (verdict === null) return { kind: "defer" };
|
|
2631
2881
|
if (!verdict.ok) return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2632
2882
|
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
2633
2883
|
return { kind: "ack", ack: rejectAck(entry, "not-owned") };
|
|
@@ -2638,38 +2888,43 @@ async function screen(deps, engine, entry) {
|
|
|
2638
2888
|
if (held !== null && await engine.isSpent(token)) {
|
|
2639
2889
|
return { kind: "ack", ack: rejectAck(entry, "invalid") };
|
|
2640
2890
|
}
|
|
2891
|
+
const unverifiedCoinIds = unverifiedCoinsOf(deps, token);
|
|
2641
2892
|
return {
|
|
2642
2893
|
kind: "accept",
|
|
2894
|
+
token,
|
|
2643
2895
|
record: {
|
|
2644
2896
|
tokenId: keys.tokenId,
|
|
2645
2897
|
stateHash: keys.stateHash,
|
|
2646
2898
|
assets: toAssetAmounts(token),
|
|
2647
|
-
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {}
|
|
2899
|
+
...isCoinlessEnvelope(token.valueEnvelope) ? { tokenType: token.tokenType } : {},
|
|
2900
|
+
...unverifiedCoinIds.length > 0 ? { unverifiedCoinIds } : {}
|
|
2648
2901
|
}
|
|
2649
2902
|
};
|
|
2650
2903
|
}
|
|
2651
|
-
|
|
2652
|
-
const
|
|
2904
|
+
function unverifiedCoinsOf(deps, token) {
|
|
2905
|
+
const claims = deps.claims;
|
|
2906
|
+
if (claims === void 0 || honoursClaims(claims, token)) return [];
|
|
2907
|
+
return (token.value?.assets ?? []).map((asset) => asset.coinId).filter((coinId) => claims.issuerOf(coinId) !== null);
|
|
2908
|
+
}
|
|
2909
|
+
async function verdictOf(engine, token) {
|
|
2653
2910
|
try {
|
|
2654
|
-
await
|
|
2655
|
-
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
2656
|
-
tokenId: record.tokenId,
|
|
2657
|
-
stateHash: record.stateHash,
|
|
2658
|
-
assets: record.assets,
|
|
2659
|
-
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
2660
|
-
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
2661
|
-
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2662
|
-
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2663
|
-
receivedAt
|
|
2664
|
-
});
|
|
2911
|
+
return await engine.verify(token);
|
|
2665
2912
|
} catch (err) {
|
|
2666
|
-
|
|
2913
|
+
if (isSphereError(err) && err.code === "MINT_REASON_UNVERIFIABLE") return null;
|
|
2914
|
+
throw err;
|
|
2667
2915
|
}
|
|
2916
|
+
}
|
|
2917
|
+
async function announce(deps, entry, record) {
|
|
2918
|
+
const receivedAt = (deps.now ?? Date.now)();
|
|
2919
|
+
await recordArrival(deps, entry, record, receivedAt);
|
|
2920
|
+
const refused2 = record.assets.filter((asset) => record.unverifiedCoinIds?.includes(asset.coinId));
|
|
2921
|
+
const counted = record.assets.filter((asset) => !refused2.includes(asset));
|
|
2668
2922
|
return {
|
|
2669
2923
|
id: record.tokenId,
|
|
2670
2924
|
senderPubkey: entry.senderPubkey ?? "",
|
|
2671
2925
|
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2672
|
-
tokens:
|
|
2926
|
+
tokens: counted.map((asset) => toUiToken(record.tokenId, asset, deps.registry, receivedAt)),
|
|
2927
|
+
...refused2.length > 0 ? { unverifiedTokens: refused2.map((asset) => ({ ...toUiToken(record.tokenId, asset, deps.registry, receivedAt), unverified: "refused" })) } : {},
|
|
2673
2928
|
// #777: named here rather than mapped from assets, which announced an EMPTY list.
|
|
2674
2929
|
...record.tokenType !== void 0 ? {
|
|
2675
2930
|
coinless: [
|
|
@@ -2687,6 +2942,25 @@ async function announce(deps, entry, record) {
|
|
|
2687
2942
|
receivedAt
|
|
2688
2943
|
};
|
|
2689
2944
|
}
|
|
2945
|
+
async function recordArrival(deps, entry, record, receivedAt) {
|
|
2946
|
+
const assets = record.assets.filter((asset) => !record.unverifiedCoinIds?.includes(asset.coinId));
|
|
2947
|
+
if (assets.length === 0 && record.unverifiedCoinIds !== void 0) return;
|
|
2948
|
+
try {
|
|
2949
|
+
await deps.recordReceived({
|
|
2950
|
+
dedupKey: receivedDedupKey(record.tokenId, record.stateHash),
|
|
2951
|
+
tokenId: record.tokenId,
|
|
2952
|
+
stateHash: record.stateHash,
|
|
2953
|
+
assets,
|
|
2954
|
+
...record.tokenType !== void 0 ? { tokenType: record.tokenType } : {},
|
|
2955
|
+
...entry.senderPubkey !== void 0 ? { senderPubkey: entry.senderPubkey } : {},
|
|
2956
|
+
...entry.senderNametag !== void 0 ? { senderNametag: entry.senderNametag } : {},
|
|
2957
|
+
...entry.memo !== void 0 ? { memo: entry.memo } : {},
|
|
2958
|
+
receivedAt
|
|
2959
|
+
});
|
|
2960
|
+
} catch (err) {
|
|
2961
|
+
logger.debug("PaymentsV2", "RECEIVED history hook failed (money path unaffected):", err);
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2690
2964
|
async function flushAcks(deps, pending, epochOf) {
|
|
2691
2965
|
if (pending.length === 0) return { progressed: true };
|
|
2692
2966
|
const before = pending.length;
|
|
@@ -2694,7 +2968,7 @@ async function flushAcks(deps, pending, epochOf) {
|
|
|
2694
2968
|
try {
|
|
2695
2969
|
const settled = await settleAcks(deps, pending);
|
|
2696
2970
|
let i = 0;
|
|
2697
|
-
while (i < pending.length && settled.has(pending[i].deliveryId)) {
|
|
2971
|
+
while (i < pending.length && settled.has(pending[i].deliveryId) && pending[i].holdsCursor !== true) {
|
|
2698
2972
|
lastAcked = pending[i].cursor;
|
|
2699
2973
|
i += 1;
|
|
2700
2974
|
}
|
|
@@ -2770,6 +3044,9 @@ async function ackOne(deps, ack) {
|
|
|
2770
3044
|
await rejectStaleClaim(deps, ack.deliveryId, ack.transferId);
|
|
2771
3045
|
}
|
|
2772
3046
|
}
|
|
3047
|
+
function queueAck(ctx, ack) {
|
|
3048
|
+
ctx.pending.push(ctx.deferred.any ? { ...ack, holdsCursor: true } : ack);
|
|
3049
|
+
}
|
|
2773
3050
|
function claimAck(entry) {
|
|
2774
3051
|
return {
|
|
2775
3052
|
deliveryId: entry.deliveryId,
|
|
@@ -3670,6 +3947,7 @@ var TransferMachine = class {
|
|
|
3670
3947
|
const key = keys.get(digest);
|
|
3671
3948
|
if (key === void 0) throw new SphereError(`no upload key returned for ${digest}`, "STORAGE_ERROR");
|
|
3672
3949
|
const { tokenId } = await engine.deliveryKeys(o.changeBlob);
|
|
3950
|
+
await this.deps.recordSplit?.(o.op.sourceTokenId, tokenId);
|
|
3673
3951
|
added.push({ tokenId, key });
|
|
3674
3952
|
}
|
|
3675
3953
|
return added;
|
|
@@ -4065,7 +4343,7 @@ var Converger = class {
|
|
|
4065
4343
|
this.deps.stores.burnJournal.list()
|
|
4066
4344
|
]);
|
|
4067
4345
|
const mintsOpen = mints.length > 0 || nftMints.length > 0 || customMints.length > 0;
|
|
4068
|
-
const burnsOpen = burns.some(
|
|
4346
|
+
const burnsOpen = burns.some(isLiveBurn);
|
|
4069
4347
|
return backstop.length > 0 || journal.some((e) => e.undeliverable !== true) || mintsOpen || burnsOpen;
|
|
4070
4348
|
}
|
|
4071
4349
|
async resumePass() {
|
|
@@ -4152,6 +4430,9 @@ async function deriveOpenIntentHolds(stores, decryptPayload) {
|
|
|
4152
4430
|
const split = typeof payload?.split?.tokenId === "string" ? [payload.split.tokenId] : [];
|
|
4153
4431
|
holds.set(entry.transferId, [...direct, ...split]);
|
|
4154
4432
|
}
|
|
4433
|
+
for (const burn of await stores.burnJournal.list()) {
|
|
4434
|
+
if (isLiveBurn(burn)) holds.set(burn.burnId, [burn.tokenId]);
|
|
4435
|
+
}
|
|
4155
4436
|
return { open: holds, complete };
|
|
4156
4437
|
}
|
|
4157
4438
|
function groupJournal(journal) {
|
|
@@ -4641,6 +4922,176 @@ function parseAmount(raw) {
|
|
|
4641
4922
|
return amount;
|
|
4642
4923
|
}
|
|
4643
4924
|
|
|
4925
|
+
// modules/payments-v2/burn.ts
|
|
4926
|
+
var HEX32 = /^[0-9a-f]{64}$/;
|
|
4927
|
+
async function heldToken(deps, tokenId) {
|
|
4928
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
4929
|
+
if (bytes === void 0) {
|
|
4930
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage; it is not held here`, "STORAGE_ERROR");
|
|
4931
|
+
}
|
|
4932
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
4933
|
+
}
|
|
4934
|
+
function assertBurnable(engine, token, tokenId) {
|
|
4935
|
+
if (!engine.isOwnedBy(token, engine.getIdentity().chainPubkey)) {
|
|
4936
|
+
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
4937
|
+
}
|
|
4938
|
+
if (token.valueEnvelope === "bare_collection") {
|
|
4939
|
+
throw new SphereError(
|
|
4940
|
+
`Token ${tokenId} carries a value envelope this SDK cannot read, so its coins cannot be accounted for; it cannot be burned`,
|
|
4941
|
+
"VALIDATION_ERROR"
|
|
4942
|
+
);
|
|
4943
|
+
}
|
|
4944
|
+
}
|
|
4945
|
+
function assetsOf(token) {
|
|
4946
|
+
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
4947
|
+
}
|
|
4948
|
+
async function runBurnUnderJournal(deps, input) {
|
|
4949
|
+
const { burnId } = input;
|
|
4950
|
+
const { tokenId, reasonBytes } = input.request;
|
|
4951
|
+
if (!HEX32.test(tokenId) || reasonBytes.length === 0) {
|
|
4952
|
+
return { success: false, burnId, tokenId, error: "burn: tokenId must be 64 hex and reasonBytes non-empty", errorCode: "VALIDATION_ERROR" };
|
|
4953
|
+
}
|
|
4954
|
+
let token;
|
|
4955
|
+
try {
|
|
4956
|
+
token = await heldToken(deps, tokenId);
|
|
4957
|
+
assertBurnable(deps.engine, token, tokenId);
|
|
4958
|
+
} catch (err) {
|
|
4959
|
+
return refused(burnId, tokenId, err);
|
|
4960
|
+
}
|
|
4961
|
+
const entry = {
|
|
4962
|
+
burnId,
|
|
4963
|
+
tokenId,
|
|
4964
|
+
reasonHex: bytesToHex(reasonBytes),
|
|
4965
|
+
burnedTokenHex: null,
|
|
4966
|
+
settled: false,
|
|
4967
|
+
assets: assetsOf(token),
|
|
4968
|
+
createdAt: deps.now()
|
|
4969
|
+
};
|
|
4970
|
+
await deps.burnJournal.upsert(entry);
|
|
4971
|
+
try {
|
|
4972
|
+
const settled = await burnJournaled(deps, entry, token);
|
|
4973
|
+
return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
|
|
4974
|
+
} catch (err) {
|
|
4975
|
+
const failure = failureOf(err);
|
|
4976
|
+
if (isTerminal(err)) await deps.burnJournal.upsert({ ...entry, failure });
|
|
4977
|
+
else deps.armHeartbeat();
|
|
4978
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
async function replayBurns(deps) {
|
|
4982
|
+
let progressed = 0;
|
|
4983
|
+
for (const entry of await deps.burnJournal.list()) {
|
|
4984
|
+
if (!isLiveBurn(entry) || deps.isActiveOp(entry.burnId)) continue;
|
|
4985
|
+
try {
|
|
4986
|
+
const token = entry.burnedTokenHex === null ? await heldToken(deps, entry.tokenId) : null;
|
|
4987
|
+
await burnJournaled(deps, entry, token);
|
|
4988
|
+
progressed += 1;
|
|
4989
|
+
} catch (err) {
|
|
4990
|
+
if (!isTerminal(err)) continue;
|
|
4991
|
+
await deps.burnJournal.upsert({ ...entry, failure: failureOf(err) });
|
|
4992
|
+
progressed += 1;
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
return progressed;
|
|
4996
|
+
}
|
|
4997
|
+
async function pendingBurns(deps) {
|
|
4998
|
+
return (await deps.burnJournal.list()).map((entry) => ({
|
|
4999
|
+
burnId: entry.burnId,
|
|
5000
|
+
tokenId: entry.tokenId,
|
|
5001
|
+
reasonBytes: hexToBytes(entry.reasonHex),
|
|
5002
|
+
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
|
|
5003
|
+
settled: entry.settled,
|
|
5004
|
+
createdAt: entry.createdAt,
|
|
5005
|
+
...entry.failure !== void 0 ? { failure: entry.failure } : {}
|
|
5006
|
+
}));
|
|
5007
|
+
}
|
|
5008
|
+
async function acknowledgeBurn(deps, burnId) {
|
|
5009
|
+
const entry = await deps.burnJournal.getByKey(burnId);
|
|
5010
|
+
if (entry === void 0) return;
|
|
5011
|
+
if (isLiveBurn(entry)) {
|
|
5012
|
+
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
5013
|
+
}
|
|
5014
|
+
await deps.burnJournal.removeByKey(burnId);
|
|
5015
|
+
}
|
|
5016
|
+
function refused(burnId, tokenId, err) {
|
|
5017
|
+
const failure = failureOf(err);
|
|
5018
|
+
return { success: false, burnId, tokenId, error: failure.message, errorCode: failure.code };
|
|
5019
|
+
}
|
|
5020
|
+
function isTerminal(err) {
|
|
5021
|
+
return classifyError(err) === "conflict";
|
|
5022
|
+
}
|
|
5023
|
+
function failureOf(err) {
|
|
5024
|
+
const code = err !== null && typeof err === "object" ? err.code : void 0;
|
|
5025
|
+
return { code: typeof code === "string" ? code : "UNKNOWN", message: messageOf(err) };
|
|
5026
|
+
}
|
|
5027
|
+
async function burnJournaled(deps, entry, token) {
|
|
5028
|
+
let current = entry;
|
|
5029
|
+
if (current.burnedTokenHex === null) {
|
|
5030
|
+
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
5031
|
+
const burned = await deps.engine.burn(
|
|
5032
|
+
{ token, reasonBytes: hexToBytes(current.reasonHex) },
|
|
5033
|
+
{ transferId: current.burnId }
|
|
5034
|
+
);
|
|
5035
|
+
if (burned.blob.tokenId !== current.tokenId) {
|
|
5036
|
+
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
5037
|
+
}
|
|
5038
|
+
current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
|
|
5039
|
+
await deps.burnJournal.upsert(current);
|
|
5040
|
+
}
|
|
5041
|
+
await deps.storagePort.applyDelta({ transferId: current.burnId, spent: [current.tokenId], added: [], externalDelivery: true });
|
|
5042
|
+
await deps.recordSent({ transferId: current.burnId, assets: current.assets, tokenId: current.tokenId });
|
|
5043
|
+
current = { ...current, settled: true };
|
|
5044
|
+
await deps.burnJournal.upsert(current);
|
|
5045
|
+
deps.refreshView();
|
|
5046
|
+
return current;
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
// modules/payments-v2/burn-hold.ts
|
|
5050
|
+
var BurnHold = class {
|
|
5051
|
+
constructor(deps) {
|
|
5052
|
+
this.deps = deps;
|
|
5053
|
+
}
|
|
5054
|
+
async run(burnId, tokenId, burn) {
|
|
5055
|
+
try {
|
|
5056
|
+
this.deps.queue.planWhole(burnId, tokenId);
|
|
5057
|
+
} catch (err) {
|
|
5058
|
+
return refused(burnId, tokenId, err);
|
|
5059
|
+
}
|
|
5060
|
+
this.deps.view.markInFlightMany([tokenId]);
|
|
5061
|
+
let burned = false;
|
|
5062
|
+
try {
|
|
5063
|
+
const result = await burn();
|
|
5064
|
+
burned = result.success;
|
|
5065
|
+
return result;
|
|
5066
|
+
} finally {
|
|
5067
|
+
await this.settle(burnId, tokenId, burned);
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
async settle(burnId, tokenId, burned) {
|
|
5071
|
+
if (burned) {
|
|
5072
|
+
this.deps.ledger.commit(burnId);
|
|
5073
|
+
this.deps.track(this.refreshThenRelease(tokenId));
|
|
5074
|
+
return;
|
|
5075
|
+
}
|
|
5076
|
+
const entry = await this.deps.burnJournal.getByKey(burnId).catch(() => null);
|
|
5077
|
+
if (entry === void 0 || entry !== null && !isLiveBurn(entry)) {
|
|
5078
|
+
this.deps.ledger.cancel(burnId);
|
|
5079
|
+
this.deps.view.releaseMany([tokenId]);
|
|
5080
|
+
this.deps.queue.notifyChange("");
|
|
5081
|
+
return;
|
|
5082
|
+
}
|
|
5083
|
+
this.deps.pins.adopt(burnId);
|
|
5084
|
+
}
|
|
5085
|
+
async refreshThenRelease(tokenId) {
|
|
5086
|
+
try {
|
|
5087
|
+
await this.deps.view.delta();
|
|
5088
|
+
this.deps.view.releaseMany([tokenId]);
|
|
5089
|
+
} finally {
|
|
5090
|
+
this.deps.queue.notifyChange("");
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
};
|
|
5094
|
+
|
|
4644
5095
|
// modules/payments-v2/compose.ts
|
|
4645
5096
|
function supportsDeterministicMint(engine) {
|
|
4646
5097
|
return engine.deterministicMint === true;
|
|
@@ -4648,16 +5099,8 @@ function supportsDeterministicMint(engine) {
|
|
|
4648
5099
|
function composeFacadeParts(deps, hooks) {
|
|
4649
5100
|
const ownPubkeyBytes = hexToBytes(deps.ownPubkey);
|
|
4650
5101
|
const ledger = new ReservationLedger();
|
|
4651
|
-
const
|
|
4652
|
-
|
|
4653
|
-
kv: deps.kv,
|
|
4654
|
-
emit: (event) => deps.emit(event, {}),
|
|
4655
|
-
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
4656
|
-
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
4657
|
-
// must not call a token spendable that the queue is about to refuse to spend.
|
|
4658
|
-
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
4659
|
-
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
4660
|
-
});
|
|
5102
|
+
const verdicts = buildVerdicts(deps, hooks);
|
|
5103
|
+
const view = buildView(deps, hooks, ledger, verdicts);
|
|
4661
5104
|
const queue = new SpendQueue({
|
|
4662
5105
|
ledger,
|
|
4663
5106
|
getPool: (coinId) => view.pool(coinId),
|
|
@@ -4672,27 +5115,57 @@ function composeFacadeParts(deps, hooks) {
|
|
|
4672
5115
|
...deps.now !== void 0 ? { now: deps.now } : {},
|
|
4673
5116
|
...deps.newId !== void 0 ? { newId: deps.newId } : {}
|
|
4674
5117
|
});
|
|
4675
|
-
const machineDeps = buildMachineDeps(deps, hooks, historyStore);
|
|
5118
|
+
const machineDeps = buildMachineDeps(deps, hooks, historyStore, verdicts);
|
|
4676
5119
|
const machineStores = createMachineStores(deps.kv);
|
|
4677
5120
|
const heldStates = /* @__PURE__ */ new Map();
|
|
4678
5121
|
const refreshView = coalesced(() => view.delta(), hooks.track);
|
|
5122
|
+
const pins = buildPins(deps, hooks, { ledger, view, machineStores, machineDeps });
|
|
4679
5123
|
return {
|
|
4680
5124
|
ownPubkeyBytes,
|
|
4681
5125
|
view,
|
|
5126
|
+
verdicts,
|
|
4682
5127
|
refreshView,
|
|
4683
5128
|
ledger,
|
|
4684
|
-
pins
|
|
5129
|
+
pins,
|
|
4685
5130
|
queue,
|
|
5131
|
+
burnHold: new BurnHold({ queue, ledger, view, pins, burnJournal: machineStores.burnJournal, track: hooks.track }),
|
|
4686
5132
|
historyStore,
|
|
4687
5133
|
machineStores,
|
|
4688
5134
|
machineDeps,
|
|
4689
5135
|
machine: new TransferMachine(machineDeps),
|
|
4690
5136
|
heldStates,
|
|
4691
|
-
receiveLoop: buildReceive(deps, hooks, historyStore, heldStates, refreshView),
|
|
5137
|
+
receiveLoop: buildReceive(deps, hooks, { historyStore, heldStates, verdicts }, refreshView),
|
|
4692
5138
|
requests: buildRequests(deps, hooks),
|
|
4693
5139
|
restoreDeps: buildRestoreDeps(deps, machineDeps, machineStores, view)
|
|
4694
5140
|
};
|
|
4695
5141
|
}
|
|
5142
|
+
function buildView(deps, hooks, ledger, verdicts) {
|
|
5143
|
+
const view = new InventoryView({
|
|
5144
|
+
port: deps.storagePort,
|
|
5145
|
+
kv: deps.kv,
|
|
5146
|
+
trust: verdicts,
|
|
5147
|
+
emit: (event) => {
|
|
5148
|
+
deps.emit(event, {});
|
|
5149
|
+
hooks.track(verdicts.review(view.claimHolders()));
|
|
5150
|
+
},
|
|
5151
|
+
// #737: a reserved token is unselectable, so it is never confirmed balance.
|
|
5152
|
+
// #738: while the held-set is unproven, EVERY token reads pinned — the report
|
|
5153
|
+
// must not call a token spendable that the queue is about to refuse to spend.
|
|
5154
|
+
isPinned: (tokenId) => ledger.unprovenReason() !== null || ledger.holderOf(tokenId) !== void 0,
|
|
5155
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
5156
|
+
});
|
|
5157
|
+
return view;
|
|
5158
|
+
}
|
|
5159
|
+
function buildVerdicts(deps, hooks) {
|
|
5160
|
+
return new TokenVerdicts({
|
|
5161
|
+
kv: deps.kv,
|
|
5162
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
5163
|
+
engine: () => hooks.engine(),
|
|
5164
|
+
getBlobs: (tokenIds) => deps.storagePort.getBlobs(tokenIds),
|
|
5165
|
+
changed: () => deps.emit("inventory:updated", {}),
|
|
5166
|
+
...deps.now !== void 0 ? { now: deps.now } : {}
|
|
5167
|
+
});
|
|
5168
|
+
}
|
|
4696
5169
|
function buildPins(deps, hooks, parts) {
|
|
4697
5170
|
return new IntentPins({
|
|
4698
5171
|
ledger: parts.ledger,
|
|
@@ -4720,8 +5193,9 @@ function buildRestoreDeps(deps, machineDeps, machineStores, view) {
|
|
|
4720
5193
|
}
|
|
4721
5194
|
};
|
|
4722
5195
|
}
|
|
4723
|
-
function buildMachineDeps(deps, hooks, historyStore) {
|
|
5196
|
+
function buildMachineDeps(deps, hooks, historyStore, verdicts) {
|
|
4724
5197
|
return {
|
|
5198
|
+
recordSplit: (sourceTokenId, outputTokenId) => verdicts.recordSplit(sourceTokenId, outputTokenId),
|
|
4725
5199
|
engine: () => hooks.engine(),
|
|
4726
5200
|
storage: deps.storagePort,
|
|
4727
5201
|
delivery: deps.deliveryPort,
|
|
@@ -4754,10 +5228,13 @@ function buildMachineDeps(deps, hooks, historyStore) {
|
|
|
4754
5228
|
}
|
|
4755
5229
|
};
|
|
4756
5230
|
}
|
|
4757
|
-
function buildReceive(deps, hooks,
|
|
5231
|
+
function buildReceive(deps, hooks, parts, refreshView) {
|
|
5232
|
+
const { historyStore, heldStates, verdicts } = parts;
|
|
4758
5233
|
return new Receive({
|
|
4759
5234
|
delivery: deps.deliveryPort,
|
|
4760
5235
|
engine: () => hooks.engine(),
|
|
5236
|
+
claims: deps.claims ?? new CoinClaims(),
|
|
5237
|
+
accepted: (token) => verdicts.accept(token),
|
|
4761
5238
|
view: {
|
|
4762
5239
|
heldState: (tokenId) => heldStates.get(tokenId) ?? null,
|
|
4763
5240
|
store: async (entry) => {
|
|
@@ -4876,6 +5353,16 @@ async function tokenInServerInventory(storagePort, tokenId) {
|
|
|
4876
5353
|
}
|
|
4877
5354
|
return false;
|
|
4878
5355
|
}
|
|
5356
|
+
async function seedHeldStates(storagePort, heldStates) {
|
|
5357
|
+
let page = await storagePort.listInventory();
|
|
5358
|
+
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
5359
|
+
for (const item of page.items) {
|
|
5360
|
+
if (item.status === "active") heldStates.set(item.tokenId, item.stateHash);
|
|
5361
|
+
}
|
|
5362
|
+
if (!page.more) return;
|
|
5363
|
+
page = await storagePort.listInventory(page.cursor);
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
4879
5366
|
async function replayCoinMints(deps) {
|
|
4880
5367
|
let resolved = 0;
|
|
4881
5368
|
for (const entry of await deps.mintJournal.list()) {
|
|
@@ -4979,98 +5466,9 @@ async function mintJournaled(deps, entry) {
|
|
|
4979
5466
|
await finalizeMint(deps, { mintId: entry.mintId, token, assets: [], journal: deps.nftMintJournal });
|
|
4980
5467
|
}
|
|
4981
5468
|
|
|
4982
|
-
// modules/payments-v2/inventory/nft-read.ts
|
|
4983
|
-
var NFT_CACHE_MAX = 256;
|
|
4984
|
-
var NftCache = class {
|
|
4985
|
-
constructor(max = NFT_CACHE_MAX) {
|
|
4986
|
-
this.max = max;
|
|
4987
|
-
}
|
|
4988
|
-
entries = /* @__PURE__ */ new Map();
|
|
4989
|
-
/** `undefined` = not cached. A hit becomes the most recently used entry. */
|
|
4990
|
-
get(tokenId) {
|
|
4991
|
-
const view = this.entries.get(tokenId);
|
|
4992
|
-
if (view === void 0) return void 0;
|
|
4993
|
-
this.entries.delete(tokenId);
|
|
4994
|
-
this.entries.set(tokenId, view);
|
|
4995
|
-
return view;
|
|
4996
|
-
}
|
|
4997
|
-
set(tokenId, view) {
|
|
4998
|
-
this.entries.delete(tokenId);
|
|
4999
|
-
this.entries.set(tokenId, view);
|
|
5000
|
-
const oldest = this.entries.keys().next().value;
|
|
5001
|
-
if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
|
|
5002
|
-
}
|
|
5003
|
-
};
|
|
5004
|
-
async function readNft(deps, tokenId) {
|
|
5005
|
-
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5006
|
-
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5007
|
-
}
|
|
5008
|
-
const cached = deps.cache.get(tokenId);
|
|
5009
|
-
if (cached !== void 0) return cached;
|
|
5010
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5011
|
-
if (bytes === void 0) {
|
|
5012
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5013
|
-
}
|
|
5014
|
-
return decodeView(deps, tokenId, bytes);
|
|
5015
|
-
}
|
|
5016
|
-
async function readNfts(deps, tokenIds) {
|
|
5017
|
-
const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
|
|
5018
|
-
const views = /* @__PURE__ */ new Map();
|
|
5019
|
-
const misses = [];
|
|
5020
|
-
for (const tokenId of held) {
|
|
5021
|
-
const cached = deps.cache.get(tokenId);
|
|
5022
|
-
if (cached === void 0) misses.push(tokenId);
|
|
5023
|
-
else views.set(tokenId, cached);
|
|
5024
|
-
}
|
|
5025
|
-
const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
|
|
5026
|
-
for (const tokenId of misses) {
|
|
5027
|
-
const bytes = blobs.get(tokenId);
|
|
5028
|
-
if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
|
|
5029
|
-
}
|
|
5030
|
-
const out = /* @__PURE__ */ new Map();
|
|
5031
|
-
for (const tokenId of held) {
|
|
5032
|
-
const view = views.get(tokenId);
|
|
5033
|
-
if (view) out.set(tokenId, view);
|
|
5034
|
-
}
|
|
5035
|
-
return out;
|
|
5036
|
-
}
|
|
5037
|
-
async function decodeView(deps, tokenId, bytes) {
|
|
5038
|
-
let view;
|
|
5039
|
-
try {
|
|
5040
|
-
const token = await deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5041
|
-
if (token.blob.tokenId !== tokenId) {
|
|
5042
|
-
throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
|
|
5043
|
-
}
|
|
5044
|
-
const reading = await deps.engine.readNft(token);
|
|
5045
|
-
view = reading === null ? null : { tokenId, ...reading };
|
|
5046
|
-
} catch (err) {
|
|
5047
|
-
logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
|
|
5048
|
-
throw err;
|
|
5049
|
-
}
|
|
5050
|
-
deps.cache.set(tokenId, view);
|
|
5051
|
-
return view;
|
|
5052
|
-
}
|
|
5053
|
-
|
|
5054
|
-
// modules/payments-v2/inventory/token-data.ts
|
|
5055
|
-
async function readTokenData(deps, tokenId) {
|
|
5056
|
-
return deps.engine.readTokenData(await heldToken(deps, tokenId));
|
|
5057
|
-
}
|
|
5058
|
-
async function readTokenJustification(deps, tokenId) {
|
|
5059
|
-
return deps.engine.readTokenJustification(await heldToken(deps, tokenId));
|
|
5060
|
-
}
|
|
5061
|
-
async function heldToken(deps, tokenId) {
|
|
5062
|
-
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5063
|
-
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5064
|
-
}
|
|
5065
|
-
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5066
|
-
if (bytes === void 0) {
|
|
5067
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5068
|
-
}
|
|
5069
|
-
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5070
|
-
}
|
|
5071
|
-
|
|
5072
5469
|
// modules/payments-v2/mint-custom.ts
|
|
5073
|
-
var
|
|
5470
|
+
var HEX322 = /^[0-9a-f]{64}$/;
|
|
5471
|
+
var CUSTOM_MINT_MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
5074
5472
|
function assertRequest(request) {
|
|
5075
5473
|
if (request.tokenType.length !== 32) {
|
|
5076
5474
|
throw new SphereError("mintCustom: tokenType must be 32 bytes", "VALIDATION_ERROR");
|
|
@@ -5081,19 +5479,27 @@ function assertRequest(request) {
|
|
|
5081
5479
|
if (request.data.length === 0) {
|
|
5082
5480
|
throw new SphereError("mintCustom: data must not be empty", "VALIDATION_ERROR");
|
|
5083
5481
|
}
|
|
5482
|
+
if (request.justification !== void 0 && request.justification.length === 0) {
|
|
5483
|
+
throw new SphereError("mintCustom: justification must be omitted, not empty", "VALIDATION_ERROR");
|
|
5484
|
+
}
|
|
5485
|
+
if (request.data.length + (request.justification?.length ?? 0) > CUSTOM_MINT_MAX_PAYLOAD_BYTES) {
|
|
5486
|
+
throw new SphereError(`mintCustom: data and justification exceed ${CUSTOM_MINT_MAX_PAYLOAD_BYTES} bytes`, "VALIDATION_ERROR");
|
|
5487
|
+
}
|
|
5084
5488
|
for (const asset of request.assets) {
|
|
5085
|
-
if (!
|
|
5489
|
+
if (!HEX322.test(asset.coinId) || asset.amount <= 0n) {
|
|
5086
5490
|
throw new SphereError("mintCustom: assets need a 64-hex coinId and a positive amount", "VALIDATION_ERROR");
|
|
5087
5491
|
}
|
|
5088
5492
|
}
|
|
5089
5493
|
}
|
|
5090
5494
|
async function runCustomMintUnderJournal(deps, input) {
|
|
5495
|
+
let entry;
|
|
5091
5496
|
try {
|
|
5092
5497
|
assertRequest(input.request);
|
|
5498
|
+
entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
5499
|
+
deps.engine.assertMintable(mintParamsOf(deps, entry, input.request.mintJustificationVerifiers));
|
|
5093
5500
|
} catch (err) {
|
|
5094
5501
|
return { success: false, error: messageOf(err) };
|
|
5095
5502
|
}
|
|
5096
|
-
const entry = journalEntryOf2(input.mintId, input.request, deps.now());
|
|
5097
5503
|
await deps.customMintJournal.upsert(entry);
|
|
5098
5504
|
let tokenId;
|
|
5099
5505
|
try {
|
|
@@ -5133,24 +5539,25 @@ function journalEntryOf2(mintId, request, createdAt) {
|
|
|
5133
5539
|
createdAt
|
|
5134
5540
|
};
|
|
5135
5541
|
}
|
|
5542
|
+
function mintParamsOf(deps, entry, mintJustificationVerifiers) {
|
|
5543
|
+
return {
|
|
5544
|
+
recipientPubkey: deps.ownPubkeyBytes,
|
|
5545
|
+
data: hexToBytes(entry.dataHex),
|
|
5546
|
+
tokenType: hexToBytes(entry.tokenTypeHex),
|
|
5547
|
+
salt: hexToBytes(entry.saltHex),
|
|
5548
|
+
...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
|
|
5549
|
+
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
5550
|
+
};
|
|
5551
|
+
}
|
|
5136
5552
|
async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
5137
|
-
const token = await deps.engine.mintDataToken(
|
|
5138
|
-
{
|
|
5139
|
-
recipientPubkey: deps.ownPubkeyBytes,
|
|
5140
|
-
data: hexToBytes(entry.dataHex),
|
|
5141
|
-
tokenType: hexToBytes(entry.tokenTypeHex),
|
|
5142
|
-
salt: hexToBytes(entry.saltHex),
|
|
5143
|
-
...entry.justificationHex !== null ? { justification: hexToBytes(entry.justificationHex) } : {},
|
|
5144
|
-
...mintJustificationVerifiers ? { mintJustificationVerifiers } : {}
|
|
5145
|
-
},
|
|
5146
|
-
{ transferId: entry.mintId }
|
|
5147
|
-
);
|
|
5553
|
+
const token = await deps.engine.mintDataToken(mintParamsOf(deps, entry, mintJustificationVerifiers), { transferId: entry.mintId });
|
|
5148
5554
|
if (entry.tokenId !== "" && token.blob.tokenId !== entry.tokenId) {
|
|
5149
5555
|
throw new SphereError(`minted token ${token.blob.tokenId}, but the journal names ${entry.tokenId}`, "VALIDATION_ERROR");
|
|
5150
5556
|
}
|
|
5151
5557
|
if (entry.tokenId === "") {
|
|
5152
5558
|
await deps.customMintJournal.upsert({ ...entry, tokenId: token.blob.tokenId });
|
|
5153
5559
|
}
|
|
5560
|
+
if (mintJustificationVerifiers === void 0) await deps.accepted?.(token);
|
|
5154
5561
|
await finalizeMint(deps, {
|
|
5155
5562
|
mintId: entry.mintId,
|
|
5156
5563
|
token,
|
|
@@ -5160,106 +5567,94 @@ async function mintJournaled2(deps, entry, mintJustificationVerifiers) {
|
|
|
5160
5567
|
return token.blob.tokenId;
|
|
5161
5568
|
}
|
|
5162
5569
|
|
|
5163
|
-
// modules/payments-v2/
|
|
5164
|
-
var
|
|
5165
|
-
|
|
5570
|
+
// modules/payments-v2/inventory/nft-read.ts
|
|
5571
|
+
var NFT_CACHE_MAX = 256;
|
|
5572
|
+
var NftCache = class {
|
|
5573
|
+
constructor(max = NFT_CACHE_MAX) {
|
|
5574
|
+
this.max = max;
|
|
5575
|
+
}
|
|
5576
|
+
entries = /* @__PURE__ */ new Map();
|
|
5577
|
+
/** `undefined` = not cached. A hit becomes the most recently used entry. */
|
|
5578
|
+
get(tokenId) {
|
|
5579
|
+
const view = this.entries.get(tokenId);
|
|
5580
|
+
if (view === void 0) return void 0;
|
|
5581
|
+
this.entries.delete(tokenId);
|
|
5582
|
+
this.entries.set(tokenId, view);
|
|
5583
|
+
return view;
|
|
5584
|
+
}
|
|
5585
|
+
set(tokenId, view) {
|
|
5586
|
+
this.entries.delete(tokenId);
|
|
5587
|
+
this.entries.set(tokenId, view);
|
|
5588
|
+
const oldest = this.entries.keys().next().value;
|
|
5589
|
+
if (this.entries.size > this.max && oldest !== void 0) this.entries.delete(oldest);
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5592
|
+
async function readNft(deps, tokenId) {
|
|
5593
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5594
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5595
|
+
}
|
|
5596
|
+
const cached = deps.cache.get(tokenId);
|
|
5597
|
+
if (cached !== void 0) return cached;
|
|
5166
5598
|
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5167
5599
|
if (bytes === void 0) {
|
|
5168
|
-
throw new SphereError(`Token ${tokenId} has no blob in storage
|
|
5600
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5169
5601
|
}
|
|
5170
|
-
return deps
|
|
5171
|
-
}
|
|
5172
|
-
function assetsOf(token) {
|
|
5173
|
-
return (token.value?.assets ?? []).map((a) => ({ coinId: a.coinId, amount: a.amount.toString() }));
|
|
5602
|
+
return decodeView(deps, tokenId, bytes);
|
|
5174
5603
|
}
|
|
5175
|
-
async function
|
|
5176
|
-
const
|
|
5177
|
-
const
|
|
5178
|
-
|
|
5179
|
-
|
|
5604
|
+
async function readNfts(deps, tokenIds) {
|
|
5605
|
+
const held = [...new Set(tokenIds)].filter((tokenId) => deps.view.stateHashOf(tokenId) !== void 0);
|
|
5606
|
+
const views = /* @__PURE__ */ new Map();
|
|
5607
|
+
const misses = [];
|
|
5608
|
+
for (const tokenId of held) {
|
|
5609
|
+
const cached = deps.cache.get(tokenId);
|
|
5610
|
+
if (cached === void 0) misses.push(tokenId);
|
|
5611
|
+
else views.set(tokenId, cached);
|
|
5180
5612
|
}
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
if (
|
|
5185
|
-
throw new SphereError(`Token ${tokenId} is not owned by this wallet`, "VALIDATION_ERROR");
|
|
5186
|
-
}
|
|
5187
|
-
} catch (err) {
|
|
5188
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
5613
|
+
const blobs = misses.length > 0 ? await deps.storagePort.getBlobs(misses) : /* @__PURE__ */ new Map();
|
|
5614
|
+
for (const tokenId of misses) {
|
|
5615
|
+
const bytes = blobs.get(tokenId);
|
|
5616
|
+
if (bytes !== void 0) views.set(tokenId, await decodeView(deps, tokenId, bytes).catch(() => null));
|
|
5189
5617
|
}
|
|
5190
|
-
const
|
|
5191
|
-
|
|
5192
|
-
tokenId
|
|
5193
|
-
|
|
5194
|
-
burnedTokenHex: null,
|
|
5195
|
-
settled: false,
|
|
5196
|
-
assets: assetsOf(token),
|
|
5197
|
-
createdAt: deps.now()
|
|
5198
|
-
};
|
|
5199
|
-
await deps.burnJournal.upsert(entry);
|
|
5200
|
-
try {
|
|
5201
|
-
const settled = await burnJournaled(deps, entry, token, { applied: false });
|
|
5202
|
-
return { success: true, burnId, tokenId, burnedToken: hexToBytes(settled.burnedTokenHex ?? "") };
|
|
5203
|
-
} catch (err) {
|
|
5204
|
-
deps.armHeartbeat();
|
|
5205
|
-
return { success: false, burnId, tokenId, error: messageOf(err) };
|
|
5618
|
+
const out = /* @__PURE__ */ new Map();
|
|
5619
|
+
for (const tokenId of held) {
|
|
5620
|
+
const view = views.get(tokenId);
|
|
5621
|
+
if (view) out.set(tokenId, view);
|
|
5206
5622
|
}
|
|
5623
|
+
return out;
|
|
5207
5624
|
}
|
|
5208
|
-
async function
|
|
5209
|
-
let
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
const applied = entry.burnedTokenHex !== null && !await deps.tokenInServerInventory(entry.tokenId);
|
|
5215
|
-
await burnJournaled(deps, entry, token, { applied });
|
|
5216
|
-
progressed += 1;
|
|
5217
|
-
} catch {
|
|
5218
|
-
continue;
|
|
5625
|
+
async function decodeView(deps, tokenId, bytes) {
|
|
5626
|
+
let view;
|
|
5627
|
+
try {
|
|
5628
|
+
const token = await deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5629
|
+
if (token.blob.tokenId !== tokenId) {
|
|
5630
|
+
throw new SphereError(`the blob served for ${tokenId} decodes as ${token.blob.tokenId}`, "STORAGE_ERROR");
|
|
5219
5631
|
}
|
|
5632
|
+
const reading = await deps.engine.readNft(token);
|
|
5633
|
+
view = reading === null ? null : { tokenId, ...reading };
|
|
5634
|
+
} catch (err) {
|
|
5635
|
+
logger.warn("PaymentsV2", `NFT read: token ${tokenId} did not decode \u2014 ${String(err)}`);
|
|
5636
|
+
throw err;
|
|
5220
5637
|
}
|
|
5221
|
-
|
|
5638
|
+
deps.cache.set(tokenId, view);
|
|
5639
|
+
return view;
|
|
5222
5640
|
}
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
reasonBytes: hexToBytes(entry.reasonHex),
|
|
5228
|
-
burnedToken: entry.burnedTokenHex === null ? null : hexToBytes(entry.burnedTokenHex),
|
|
5229
|
-
settled: entry.settled,
|
|
5230
|
-
createdAt: entry.createdAt
|
|
5231
|
-
}));
|
|
5641
|
+
|
|
5642
|
+
// modules/payments-v2/inventory/token-data.ts
|
|
5643
|
+
async function readTokenData(deps, tokenId) {
|
|
5644
|
+
return deps.engine.readTokenData(await heldToken2(deps, tokenId));
|
|
5232
5645
|
}
|
|
5233
|
-
async function
|
|
5234
|
-
|
|
5235
|
-
if (entry === void 0) return;
|
|
5236
|
-
if (!entry.settled) {
|
|
5237
|
-
throw new SphereError(`Burn ${burnId} is not settled yet; it cannot be acknowledged`, "VALIDATION_ERROR");
|
|
5238
|
-
}
|
|
5239
|
-
await deps.burnJournal.removeByKey(burnId);
|
|
5646
|
+
async function readTokenJustification(deps, tokenId) {
|
|
5647
|
+
return deps.engine.readTokenJustification(await heldToken2(deps, tokenId));
|
|
5240
5648
|
}
|
|
5241
|
-
async function
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
if (token === null) throw new SphereError("burn replay needs the held token to certify", "VALIDATION_ERROR");
|
|
5245
|
-
const burned = await deps.engine.burn(
|
|
5246
|
-
{ token, reasonBytes: hexToBytes(current.reasonHex) },
|
|
5247
|
-
{ transferId: current.burnId }
|
|
5248
|
-
);
|
|
5249
|
-
if (burned.blob.tokenId !== current.tokenId) {
|
|
5250
|
-
throw new SphereError(`burned token ${burned.blob.tokenId}, but the journal names ${current.tokenId}`, "VALIDATION_ERROR");
|
|
5251
|
-
}
|
|
5252
|
-
current = { ...current, burnedTokenHex: bytesToHex(burned.blob.token) };
|
|
5253
|
-
await deps.burnJournal.upsert(current);
|
|
5649
|
+
async function heldToken2(deps, tokenId) {
|
|
5650
|
+
if (deps.view.stateHashOf(tokenId) === void 0) {
|
|
5651
|
+
throw new SphereError(`Token ${tokenId} is not in inventory`, "VALIDATION_ERROR");
|
|
5254
5652
|
}
|
|
5255
|
-
|
|
5256
|
-
|
|
5653
|
+
const bytes = (await deps.storagePort.getBlobs([tokenId])).get(tokenId);
|
|
5654
|
+
if (bytes === void 0) {
|
|
5655
|
+
throw new SphereError(`Token ${tokenId} has no blob in storage`, "STORAGE_ERROR");
|
|
5257
5656
|
}
|
|
5258
|
-
|
|
5259
|
-
current = { ...current, settled: true };
|
|
5260
|
-
await deps.burnJournal.upsert(current);
|
|
5261
|
-
deps.refreshView();
|
|
5262
|
-
return current;
|
|
5657
|
+
return deps.engine.decodeToken({ tokenId, token: bytes });
|
|
5263
5658
|
}
|
|
5264
5659
|
|
|
5265
5660
|
// modules/payments-v2/send-whole.ts
|
|
@@ -5457,6 +5852,7 @@ var PaymentsFacade = class {
|
|
|
5457
5852
|
track: (op) => this.trackTail(op)
|
|
5458
5853
|
});
|
|
5459
5854
|
this.view = parts.view;
|
|
5855
|
+
this.verdicts = parts.verdicts;
|
|
5460
5856
|
this.wakeRefresh = parts.refreshView;
|
|
5461
5857
|
this.ledger = parts.ledger;
|
|
5462
5858
|
this.queue = parts.queue;
|
|
@@ -5470,6 +5866,7 @@ var PaymentsFacade = class {
|
|
|
5470
5866
|
this.restoreDeps = parts.restoreDeps;
|
|
5471
5867
|
this.requests = parts.requests;
|
|
5472
5868
|
this.pins = parts.pins;
|
|
5869
|
+
this.burnHold = parts.burnHold;
|
|
5473
5870
|
deps.deliveryPort.bindDeliveryKeys((blob) => this.engine().deliveryKeys(blob));
|
|
5474
5871
|
this.heartbeat = new ConvergenceHeartbeat({
|
|
5475
5872
|
now: () => this.nowMs(),
|
|
@@ -5493,6 +5890,7 @@ var PaymentsFacade = class {
|
|
|
5493
5890
|
wakeRefresh;
|
|
5494
5891
|
prewarmed = new PrewarmCache();
|
|
5495
5892
|
view;
|
|
5893
|
+
verdicts;
|
|
5496
5894
|
ledger;
|
|
5497
5895
|
queue;
|
|
5498
5896
|
machine;
|
|
@@ -5519,6 +5917,7 @@ var PaymentsFacade = class {
|
|
|
5519
5917
|
activeMoneyOps = /* @__PURE__ */ new Set();
|
|
5520
5918
|
/** #737: the ledger holds exactly the sources of the still-open intents. */
|
|
5521
5919
|
pins;
|
|
5920
|
+
burnHold;
|
|
5522
5921
|
/** Per facade, so per address: an NFT reading is never served to another wallet. */
|
|
5523
5922
|
nftCache = new NftCache();
|
|
5524
5923
|
// ── lifecycle ──────────────────────────────────────────────────────────────
|
|
@@ -5543,7 +5942,8 @@ var PaymentsFacade = class {
|
|
|
5543
5942
|
await this.deps.session.start();
|
|
5544
5943
|
await this.pins.sync().catch(() => void 0);
|
|
5545
5944
|
this.trackTail(this.runConvergencePass());
|
|
5546
|
-
this.trackTail(this.
|
|
5945
|
+
this.trackTail(seedHeldStates(this.deps.storagePort, this.heldStates));
|
|
5946
|
+
await this.verdicts.start().catch(() => void 0);
|
|
5547
5947
|
await this.view.fullPull().catch(() => void 0);
|
|
5548
5948
|
this.receiveLoop.start(this.deps.receivePollMs);
|
|
5549
5949
|
}
|
|
@@ -5552,6 +5952,7 @@ var PaymentsFacade = class {
|
|
|
5552
5952
|
this.started = false;
|
|
5553
5953
|
this.heartbeat.stop();
|
|
5554
5954
|
this.receiveLoop.stop();
|
|
5955
|
+
this.verdicts.stop();
|
|
5555
5956
|
for (const unsubscribe of this.unsubscribers) unsubscribe();
|
|
5556
5957
|
this.unsubscribers = [];
|
|
5557
5958
|
await this.deps.session.stop();
|
|
@@ -5568,12 +5969,17 @@ var PaymentsFacade = class {
|
|
|
5568
5969
|
}
|
|
5569
5970
|
// ── reads ──────────────────────────────────────────────────────────────────
|
|
5570
5971
|
async assets(coinId) {
|
|
5972
|
+
return (await this.heldAssets(coinId)).filter((asset) => asset.unverified === void 0);
|
|
5973
|
+
}
|
|
5974
|
+
unverifiedAssets = async (coinId) => (await this.heldAssets(coinId)).filter((a) => a.unverified !== void 0);
|
|
5975
|
+
async heldAssets(coinId) {
|
|
5571
5976
|
const all = await this.view.assets(this.deps.registry, this.deps.price);
|
|
5572
5977
|
return coinId === void 0 ? all : all.filter((asset) => asset.coinId === coinId);
|
|
5573
5978
|
}
|
|
5574
5979
|
tokens(filter) {
|
|
5575
|
-
return this.view.tokens(this.deps.registry, filter);
|
|
5980
|
+
return this.view.tokens(this.deps.registry, filter).filter((token) => token.unverified === void 0);
|
|
5576
5981
|
}
|
|
5982
|
+
unverifiedTokens = (filter) => this.view.tokens(this.deps.registry, filter).filter((t) => t.unverified !== void 0);
|
|
5577
5983
|
coinless() {
|
|
5578
5984
|
return this.view.coinless(this.deps.registry);
|
|
5579
5985
|
}
|
|
@@ -5634,9 +6040,9 @@ var PaymentsFacade = class {
|
|
|
5634
6040
|
return this.track(this.ownedOp((mintId) => runNftMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
5635
6041
|
}
|
|
5636
6042
|
mintCustom = (request) => this.track(this.ownedOp((mintId) => runCustomMintUnderJournal(this.mintDeps(), { mintId, request })));
|
|
5637
|
-
burn = (request) => this.track(this.ownedOp((burnId) => runBurnUnderJournal(this.mintDeps(), { burnId, request })));
|
|
5638
|
-
pendingBurns = () => this.track(pendingBurns(this.
|
|
5639
|
-
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.
|
|
6043
|
+
burn = (request) => this.track(this.ownedOp((burnId) => this.burnHold.run(burnId, request.tokenId, () => runBurnUnderJournal(this.mintDeps(), { burnId, request }))));
|
|
6044
|
+
pendingBurns = () => this.track(pendingBurns(this.machineStores));
|
|
6045
|
+
acknowledgeBurn = (burnId) => this.track(acknowledgeBurn(this.machineStores, burnId));
|
|
5640
6046
|
// §4 retry verb: coalesces onto the running pass — NEVER re-issue send() (#631/#676).
|
|
5641
6047
|
async resumeNow() {
|
|
5642
6048
|
if (!this.started) return;
|
|
@@ -5679,7 +6085,8 @@ var PaymentsFacade = class {
|
|
|
5679
6085
|
isActiveOp: (mintId) => this.activeMoneyOps.has(mintId),
|
|
5680
6086
|
tokenInServerInventory: (tokenId) => tokenInServerInventory(this.deps.storagePort, tokenId),
|
|
5681
6087
|
emit: this.deps.emit,
|
|
5682
|
-
now: () => this.nowMs()
|
|
6088
|
+
now: () => this.nowMs(),
|
|
6089
|
+
accepted: (token) => this.verdicts.accept(token)
|
|
5683
6090
|
};
|
|
5684
6091
|
}
|
|
5685
6092
|
// ── send policy (§5.5 + old-loop parity; the machine stays policy-free) ────
|
|
@@ -6017,16 +6424,6 @@ var PaymentsFacade = class {
|
|
|
6017
6424
|
newId() {
|
|
6018
6425
|
return (this.deps.newId ?? randomUUID)();
|
|
6019
6426
|
}
|
|
6020
|
-
async seedHeldStates() {
|
|
6021
|
-
let page = await this.deps.storagePort.listInventory();
|
|
6022
|
-
for (let i = 0; i < INVENTORY_SCAN_PAGE_LIMIT; i++) {
|
|
6023
|
-
for (const item of page.items) {
|
|
6024
|
-
if (item.status === "active") this.heldStates.set(item.tokenId, item.stateHash);
|
|
6025
|
-
}
|
|
6026
|
-
if (!page.more) return;
|
|
6027
|
-
page = await this.deps.storagePort.listInventory(page.cursor);
|
|
6028
|
-
}
|
|
6029
|
-
}
|
|
6030
6427
|
track(op) {
|
|
6031
6428
|
const settled = op.then(() => void 0, () => void 0);
|
|
6032
6429
|
this.pendingOps.add(settled);
|
|
@@ -6042,6 +6439,7 @@ var PaymentsFacade = class {
|
|
|
6042
6439
|
ATTENTION_MINT_UNRESOLVED,
|
|
6043
6440
|
ATTENTION_RECIPIENT_NETWORK_UNVERIFIED,
|
|
6044
6441
|
ATTENTION_RESEED_REJECTED,
|
|
6442
|
+
CUSTOM_MINT_MAX_PAYLOAD_BYTES,
|
|
6045
6443
|
HEARTBEAT_CAP_MS,
|
|
6046
6444
|
HEARTBEAT_SEED_MS,
|
|
6047
6445
|
MAX_RESELECT,
|
|
@@ -6055,6 +6453,7 @@ var PaymentsFacade = class {
|
|
|
6055
6453
|
STORE_KEYS,
|
|
6056
6454
|
createScopedKV,
|
|
6057
6455
|
encodeNftContent,
|
|
6456
|
+
isLiveBurn,
|
|
6058
6457
|
isNftDocumentLink,
|
|
6059
6458
|
isRetryableAckError,
|
|
6060
6459
|
parseNftDocument,
|