@unicitylabs/sphere-sdk 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -79
- package/dist/core/index.cjs +1220 -275
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +422 -221
- package/dist/core/index.d.ts +422 -221
- package/dist/core/index.js +1219 -275
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +2077 -14
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +2077 -14
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +1877 -513
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +1877 -513
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +2222 -172
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +84 -3
- package/dist/impl/nodejs/index.d.ts +84 -3
- package/dist/impl/nodejs/index.js +2222 -172
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +1231 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +440 -67
- package/dist/index.d.ts +440 -67
- package/dist/index.js +1231 -265
- package/dist/index.js.map +1 -1
- package/package.json +25 -5
package/dist/core/index.cjs
CHANGED
|
@@ -520,6 +520,7 @@ __export(core_exports, {
|
|
|
520
520
|
initSphere: () => initSphere,
|
|
521
521
|
isEncryptedData: () => isEncryptedData,
|
|
522
522
|
isValidBech32: () => isValidBech32,
|
|
523
|
+
isValidNametag: () => isValidNametag,
|
|
523
524
|
isValidPrivateKey: () => isValidPrivateKey,
|
|
524
525
|
loadSphere: () => loadSphere,
|
|
525
526
|
mnemonicToEntropy: () => mnemonicToEntropy2,
|
|
@@ -1553,7 +1554,7 @@ var L1PaymentsModule = class {
|
|
|
1553
1554
|
_transport;
|
|
1554
1555
|
constructor(config) {
|
|
1555
1556
|
this._config = {
|
|
1556
|
-
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.
|
|
1557
|
+
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.unicity.network:50004",
|
|
1557
1558
|
network: config?.network ?? "mainnet",
|
|
1558
1559
|
defaultFeeRate: config?.defaultFeeRate ?? 10,
|
|
1559
1560
|
enableVesting: config?.enableVesting ?? true
|
|
@@ -1585,10 +1586,17 @@ var L1PaymentsModule = class {
|
|
|
1585
1586
|
});
|
|
1586
1587
|
}
|
|
1587
1588
|
}
|
|
1588
|
-
|
|
1589
|
+
this._initialized = true;
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Ensure the Fulcrum WebSocket is connected. Called lazily before any
|
|
1593
|
+
* operation that needs the network. If the singleton is already connected
|
|
1594
|
+
* (e.g. by the address scanner), this is a no-op.
|
|
1595
|
+
*/
|
|
1596
|
+
async ensureConnected() {
|
|
1597
|
+
if (!isWebSocketConnected() && this._config.electrumUrl) {
|
|
1589
1598
|
await connect(this._config.electrumUrl);
|
|
1590
1599
|
}
|
|
1591
|
-
this._initialized = true;
|
|
1592
1600
|
}
|
|
1593
1601
|
destroy() {
|
|
1594
1602
|
if (isWebSocketConnected()) {
|
|
@@ -1646,6 +1654,7 @@ var L1PaymentsModule = class {
|
|
|
1646
1654
|
}
|
|
1647
1655
|
async send(request) {
|
|
1648
1656
|
this.ensureInitialized();
|
|
1657
|
+
await this.ensureConnected();
|
|
1649
1658
|
if (!this._wallet || !this._identity) {
|
|
1650
1659
|
return { success: false, error: "No wallet available" };
|
|
1651
1660
|
}
|
|
@@ -1680,6 +1689,7 @@ var L1PaymentsModule = class {
|
|
|
1680
1689
|
}
|
|
1681
1690
|
async getBalance() {
|
|
1682
1691
|
this.ensureInitialized();
|
|
1692
|
+
await this.ensureConnected();
|
|
1683
1693
|
const addresses = this._getWatchedAddresses();
|
|
1684
1694
|
let totalAlpha = 0;
|
|
1685
1695
|
let vestedSats = BigInt(0);
|
|
@@ -1711,6 +1721,7 @@ var L1PaymentsModule = class {
|
|
|
1711
1721
|
}
|
|
1712
1722
|
async getUtxos() {
|
|
1713
1723
|
this.ensureInitialized();
|
|
1724
|
+
await this.ensureConnected();
|
|
1714
1725
|
const result = [];
|
|
1715
1726
|
const currentHeight = await getCurrentBlockHeight();
|
|
1716
1727
|
const allUtxos = await this._getAllUtxos();
|
|
@@ -1746,42 +1757,73 @@ var L1PaymentsModule = class {
|
|
|
1746
1757
|
return result;
|
|
1747
1758
|
}
|
|
1748
1759
|
async getHistory(limit) {
|
|
1760
|
+
await this.ensureConnected();
|
|
1749
1761
|
this.ensureInitialized();
|
|
1750
1762
|
const addresses = this._getWatchedAddresses();
|
|
1751
1763
|
const transactions = [];
|
|
1752
1764
|
const seenTxids = /* @__PURE__ */ new Set();
|
|
1753
1765
|
const currentHeight = await getCurrentBlockHeight();
|
|
1766
|
+
const txCache = /* @__PURE__ */ new Map();
|
|
1767
|
+
const fetchTx = async (txid) => {
|
|
1768
|
+
if (txCache.has(txid)) return txCache.get(txid);
|
|
1769
|
+
const detail = await getTransaction(txid);
|
|
1770
|
+
txCache.set(txid, detail);
|
|
1771
|
+
return detail;
|
|
1772
|
+
};
|
|
1773
|
+
const addressSet = new Set(addresses.map((a) => a.toLowerCase()));
|
|
1754
1774
|
for (const address of addresses) {
|
|
1755
1775
|
const history = await getTransactionHistory(address);
|
|
1756
1776
|
for (const item of history) {
|
|
1757
1777
|
if (seenTxids.has(item.tx_hash)) continue;
|
|
1758
1778
|
seenTxids.add(item.tx_hash);
|
|
1759
|
-
const tx = await
|
|
1779
|
+
const tx = await fetchTx(item.tx_hash);
|
|
1760
1780
|
if (!tx) continue;
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1781
|
+
let isSend = false;
|
|
1782
|
+
for (const vin of tx.vin ?? []) {
|
|
1783
|
+
if (!vin.txid) continue;
|
|
1784
|
+
const prevTx = await fetchTx(vin.txid);
|
|
1785
|
+
if (prevTx?.vout?.[vin.vout]) {
|
|
1786
|
+
const prevOut = prevTx.vout[vin.vout];
|
|
1787
|
+
const prevAddrs = [
|
|
1788
|
+
...prevOut.scriptPubKey?.addresses ?? [],
|
|
1789
|
+
...prevOut.scriptPubKey?.address ? [prevOut.scriptPubKey.address] : []
|
|
1790
|
+
];
|
|
1791
|
+
if (prevAddrs.some((a) => addressSet.has(a.toLowerCase()))) {
|
|
1792
|
+
isSend = true;
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
let amountToUs = 0;
|
|
1798
|
+
let amountToOthers = 0;
|
|
1765
1799
|
let txAddress = address;
|
|
1800
|
+
let externalAddress = "";
|
|
1766
1801
|
if (tx.vout) {
|
|
1767
1802
|
for (const vout of tx.vout) {
|
|
1768
|
-
const voutAddresses =
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
const
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1803
|
+
const voutAddresses = [
|
|
1804
|
+
...vout.scriptPubKey?.addresses ?? [],
|
|
1805
|
+
...vout.scriptPubKey?.address ? [vout.scriptPubKey.address] : []
|
|
1806
|
+
];
|
|
1807
|
+
const isOurs = voutAddresses.some((a) => addressSet.has(a.toLowerCase()));
|
|
1808
|
+
const valueSats = Math.floor((vout.value ?? 0) * 1e8);
|
|
1809
|
+
if (isOurs) {
|
|
1810
|
+
amountToUs += valueSats;
|
|
1811
|
+
if (!txAddress) txAddress = voutAddresses[0];
|
|
1812
|
+
} else {
|
|
1813
|
+
amountToOthers += valueSats;
|
|
1814
|
+
if (!externalAddress && voutAddresses.length > 0) {
|
|
1815
|
+
externalAddress = voutAddresses[0];
|
|
1816
|
+
}
|
|
1777
1817
|
}
|
|
1778
1818
|
}
|
|
1779
1819
|
}
|
|
1820
|
+
const amount = isSend ? amountToOthers.toString() : amountToUs.toString();
|
|
1821
|
+
const displayAddress = isSend ? externalAddress || txAddress : txAddress;
|
|
1780
1822
|
transactions.push({
|
|
1781
1823
|
txid: item.tx_hash,
|
|
1782
1824
|
type: isSend ? "send" : "receive",
|
|
1783
1825
|
amount,
|
|
1784
|
-
address:
|
|
1826
|
+
address: displayAddress,
|
|
1785
1827
|
confirmations: item.height > 0 ? currentHeight - item.height : 0,
|
|
1786
1828
|
timestamp: tx.time ? tx.time * 1e3 : Date.now(),
|
|
1787
1829
|
blockHeight: item.height > 0 ? item.height : void 0
|
|
@@ -1793,6 +1835,7 @@ var L1PaymentsModule = class {
|
|
|
1793
1835
|
}
|
|
1794
1836
|
async getTransaction(txid) {
|
|
1795
1837
|
this.ensureInitialized();
|
|
1838
|
+
await this.ensureConnected();
|
|
1796
1839
|
const tx = await getTransaction(txid);
|
|
1797
1840
|
if (!tx) return null;
|
|
1798
1841
|
const addresses = this._getWatchedAddresses();
|
|
@@ -1828,6 +1871,7 @@ var L1PaymentsModule = class {
|
|
|
1828
1871
|
}
|
|
1829
1872
|
async estimateFee(to, amount) {
|
|
1830
1873
|
this.ensureInitialized();
|
|
1874
|
+
await this.ensureConnected();
|
|
1831
1875
|
if (!this._wallet) {
|
|
1832
1876
|
return { fee: "0", feeRate: this._config.defaultFeeRate ?? 10 };
|
|
1833
1877
|
}
|
|
@@ -2167,6 +2211,7 @@ var import_MintCommitment = require("@unicitylabs/state-transition-sdk/lib/trans
|
|
|
2167
2211
|
var import_HashAlgorithm2 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
2168
2212
|
var import_UnmaskedPredicate2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
2169
2213
|
var import_InclusionProofUtils2 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
2214
|
+
var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
|
|
2170
2215
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2171
2216
|
var NametagMinter = class {
|
|
2172
2217
|
client;
|
|
@@ -2191,7 +2236,8 @@ var NametagMinter = class {
|
|
|
2191
2236
|
*/
|
|
2192
2237
|
async isNametagAvailable(nametag) {
|
|
2193
2238
|
try {
|
|
2194
|
-
const
|
|
2239
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2240
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2195
2241
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
2196
2242
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2197
2243
|
return !isMinted;
|
|
@@ -2208,7 +2254,8 @@ var NametagMinter = class {
|
|
|
2208
2254
|
* @returns MintNametagResult with token if successful
|
|
2209
2255
|
*/
|
|
2210
2256
|
async mintNametag(nametag, ownerAddress) {
|
|
2211
|
-
const
|
|
2257
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2258
|
+
const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
|
|
2212
2259
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2213
2260
|
try {
|
|
2214
2261
|
const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
|
|
@@ -2347,7 +2394,9 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2347
2394
|
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2348
2395
|
ADDRESS_NAMETAGS: "address_nametags",
|
|
2349
2396
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2350
|
-
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2397
|
+
TRACKED_ADDRESSES: "tracked_addresses",
|
|
2398
|
+
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
2399
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
2351
2400
|
};
|
|
2352
2401
|
var STORAGE_KEYS_ADDRESS = {
|
|
2353
2402
|
/** Pending transfers for this address */
|
|
@@ -2359,7 +2408,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2359
2408
|
/** Messages for this address */
|
|
2360
2409
|
MESSAGES: "messages",
|
|
2361
2410
|
/** Transaction history for this address */
|
|
2362
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2411
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2412
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2413
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2363
2414
|
};
|
|
2364
2415
|
var STORAGE_KEYS = {
|
|
2365
2416
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2378,16 +2429,6 @@ function getAddressId(directAddress) {
|
|
|
2378
2429
|
}
|
|
2379
2430
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2380
2431
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2381
|
-
var LIMITS = {
|
|
2382
|
-
/** Min nametag length */
|
|
2383
|
-
NAMETAG_MIN_LENGTH: 3,
|
|
2384
|
-
/** Max nametag length */
|
|
2385
|
-
NAMETAG_MAX_LENGTH: 20,
|
|
2386
|
-
/** Max memo length */
|
|
2387
|
-
MEMO_MAX_LENGTH: 500,
|
|
2388
|
-
/** Max message length */
|
|
2389
|
-
MESSAGE_MAX_LENGTH: 1e4
|
|
2390
|
-
};
|
|
2391
2432
|
|
|
2392
2433
|
// types/txf.ts
|
|
2393
2434
|
var ARCHIVED_PREFIX = "archived-";
|
|
@@ -2680,6 +2721,18 @@ function parseTxfStorageData(data) {
|
|
|
2680
2721
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2681
2722
|
}
|
|
2682
2723
|
}
|
|
2724
|
+
} else if (key.startsWith("token-")) {
|
|
2725
|
+
try {
|
|
2726
|
+
const entry = storageData[key];
|
|
2727
|
+
const txfToken = entry?.token;
|
|
2728
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2729
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2730
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2731
|
+
result.tokens.push(token);
|
|
2732
|
+
}
|
|
2733
|
+
} catch (err) {
|
|
2734
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2735
|
+
}
|
|
2683
2736
|
}
|
|
2684
2737
|
}
|
|
2685
2738
|
return result;
|
|
@@ -3180,8 +3233,9 @@ var InstantSplitExecutor = class {
|
|
|
3180
3233
|
const criticalPathDuration = performance.now() - startTime;
|
|
3181
3234
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3182
3235
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3236
|
+
let backgroundPromise;
|
|
3183
3237
|
if (!options?.skipBackground) {
|
|
3184
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3238
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3185
3239
|
signingService: this.signingService,
|
|
3186
3240
|
tokenType: tokenToSplit.type,
|
|
3187
3241
|
coinId,
|
|
@@ -3197,7 +3251,8 @@ var InstantSplitExecutor = class {
|
|
|
3197
3251
|
nostrEventId,
|
|
3198
3252
|
splitGroupId,
|
|
3199
3253
|
criticalPathDurationMs: criticalPathDuration,
|
|
3200
|
-
backgroundStarted: !options?.skipBackground
|
|
3254
|
+
backgroundStarted: !options?.skipBackground,
|
|
3255
|
+
backgroundPromise
|
|
3201
3256
|
};
|
|
3202
3257
|
} catch (error) {
|
|
3203
3258
|
const duration = performance.now() - startTime;
|
|
@@ -3259,7 +3314,7 @@ var InstantSplitExecutor = class {
|
|
|
3259
3314
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3260
3315
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3261
3316
|
]);
|
|
3262
|
-
submissions.then(async (results) => {
|
|
3317
|
+
return submissions.then(async (results) => {
|
|
3263
3318
|
const submitDuration = performance.now() - startTime;
|
|
3264
3319
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3265
3320
|
context.onProgress?.({
|
|
@@ -3724,6 +3779,11 @@ var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/addres
|
|
|
3724
3779
|
var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
3725
3780
|
var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
3726
3781
|
var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
3782
|
+
var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
3783
|
+
var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
|
|
3784
|
+
var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
|
|
3785
|
+
var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
|
|
3786
|
+
var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
|
|
3727
3787
|
function enrichWithRegistry(info) {
|
|
3728
3788
|
const registry = TokenRegistry.getInstance();
|
|
3729
3789
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3751,7 +3811,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3751
3811
|
try {
|
|
3752
3812
|
const sdkToken = await import_Token6.Token.fromJSON(data);
|
|
3753
3813
|
if (sdkToken.id) {
|
|
3754
|
-
defaultInfo.tokenId = sdkToken.id.
|
|
3814
|
+
defaultInfo.tokenId = sdkToken.id.toJSON();
|
|
3755
3815
|
}
|
|
3756
3816
|
if (sdkToken.coins && sdkToken.coins.coins) {
|
|
3757
3817
|
const rawCoins = sdkToken.coins.coins;
|
|
@@ -3921,6 +3981,13 @@ function extractTokenStateKey(token) {
|
|
|
3921
3981
|
if (!tokenId || !stateHash) return null;
|
|
3922
3982
|
return createTokenStateKey(tokenId, stateHash);
|
|
3923
3983
|
}
|
|
3984
|
+
function fromHex4(hex) {
|
|
3985
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3986
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3987
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3988
|
+
}
|
|
3989
|
+
return bytes;
|
|
3990
|
+
}
|
|
3924
3991
|
function hasSameGenesisTokenId(t1, t2) {
|
|
3925
3992
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3926
3993
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -4010,6 +4077,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4010
4077
|
// Token State
|
|
4011
4078
|
tokens = /* @__PURE__ */ new Map();
|
|
4012
4079
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
4080
|
+
pendingBackgroundTasks = [];
|
|
4013
4081
|
// Repository State (tombstones, archives, forked, history)
|
|
4014
4082
|
tombstones = [];
|
|
4015
4083
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -4034,6 +4102,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4034
4102
|
// Poll every 2s
|
|
4035
4103
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4036
4104
|
// Max 30 attempts (~60s)
|
|
4105
|
+
// Storage event subscriptions (push-based sync)
|
|
4106
|
+
storageEventUnsubscribers = [];
|
|
4107
|
+
syncDebounceTimer = null;
|
|
4108
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4109
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4110
|
+
_syncInProgress = null;
|
|
4037
4111
|
constructor(config) {
|
|
4038
4112
|
this.moduleConfig = {
|
|
4039
4113
|
autoSync: config?.autoSync ?? true,
|
|
@@ -4042,10 +4116,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4042
4116
|
maxRetries: config?.maxRetries ?? 3,
|
|
4043
4117
|
debug: config?.debug ?? false
|
|
4044
4118
|
};
|
|
4045
|
-
|
|
4046
|
-
this.l1 = l1Enabled ? new L1PaymentsModule(config?.l1) : null;
|
|
4119
|
+
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
4047
4120
|
}
|
|
4048
|
-
/**
|
|
4121
|
+
/**
|
|
4122
|
+
* Get the current module configuration (excluding L1 config).
|
|
4123
|
+
*
|
|
4124
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4125
|
+
*/
|
|
4049
4126
|
getConfig() {
|
|
4050
4127
|
return this.moduleConfig;
|
|
4051
4128
|
}
|
|
@@ -4086,9 +4163,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4086
4163
|
transport: deps.transport
|
|
4087
4164
|
});
|
|
4088
4165
|
}
|
|
4089
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4090
|
-
this.handleIncomingTransfer(transfer)
|
|
4091
|
-
|
|
4166
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4167
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4168
|
+
);
|
|
4092
4169
|
if (deps.transport.onPaymentRequest) {
|
|
4093
4170
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4094
4171
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4099,9 +4176,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4099
4176
|
this.handlePaymentRequestResponse(response);
|
|
4100
4177
|
});
|
|
4101
4178
|
}
|
|
4179
|
+
this.subscribeToStorageEvents();
|
|
4102
4180
|
}
|
|
4103
4181
|
/**
|
|
4104
|
-
* Load
|
|
4182
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4183
|
+
*
|
|
4184
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4185
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4186
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4105
4187
|
*/
|
|
4106
4188
|
async load() {
|
|
4107
4189
|
this.ensureInitialized();
|
|
@@ -4118,6 +4200,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4118
4200
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4119
4201
|
}
|
|
4120
4202
|
}
|
|
4203
|
+
await this.loadPendingV5Tokens();
|
|
4121
4204
|
await this.loadTokensFromFileStorage();
|
|
4122
4205
|
await this.loadNametagFromFileStorage();
|
|
4123
4206
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4135,9 +4218,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4135
4218
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4136
4219
|
}
|
|
4137
4220
|
}
|
|
4221
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4222
|
+
});
|
|
4138
4223
|
}
|
|
4139
4224
|
/**
|
|
4140
|
-
* Cleanup
|
|
4225
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4226
|
+
*
|
|
4227
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4228
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4141
4229
|
*/
|
|
4142
4230
|
destroy() {
|
|
4143
4231
|
this.unsubscribeTransfers?.();
|
|
@@ -4155,6 +4243,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4155
4243
|
resolver.reject(new Error("Module destroyed"));
|
|
4156
4244
|
}
|
|
4157
4245
|
this.pendingResponseResolvers.clear();
|
|
4246
|
+
this.unsubscribeStorageEvents();
|
|
4158
4247
|
if (this.l1) {
|
|
4159
4248
|
this.l1.destroy();
|
|
4160
4249
|
}
|
|
@@ -4171,7 +4260,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4171
4260
|
const result = {
|
|
4172
4261
|
id: crypto.randomUUID(),
|
|
4173
4262
|
status: "pending",
|
|
4174
|
-
tokens: []
|
|
4263
|
+
tokens: [],
|
|
4264
|
+
tokenTransfers: []
|
|
4175
4265
|
};
|
|
4176
4266
|
try {
|
|
4177
4267
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4208,69 +4298,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4208
4298
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4209
4299
|
result.status = "submitted";
|
|
4210
4300
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4301
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4211
4302
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4303
|
+
if (transferMode === "conservative") {
|
|
4304
|
+
this.log("Executing conservative split...");
|
|
4305
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4306
|
+
stateTransitionClient: stClient,
|
|
4307
|
+
trustBase,
|
|
4308
|
+
signingService
|
|
4309
|
+
});
|
|
4310
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4311
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4312
|
+
splitPlan.splitAmount,
|
|
4313
|
+
splitPlan.remainderAmount,
|
|
4314
|
+
splitPlan.coinId,
|
|
4315
|
+
recipientAddress
|
|
4316
|
+
);
|
|
4317
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4318
|
+
const changeUiToken = {
|
|
4319
|
+
id: crypto.randomUUID(),
|
|
4320
|
+
coinId: request.coinId,
|
|
4321
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4322
|
+
name: this.getCoinName(request.coinId),
|
|
4323
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4324
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4325
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4326
|
+
status: "confirmed",
|
|
4327
|
+
createdAt: Date.now(),
|
|
4328
|
+
updatedAt: Date.now(),
|
|
4329
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4330
|
+
};
|
|
4331
|
+
await this.addToken(changeUiToken, true);
|
|
4332
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4333
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4334
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4335
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4336
|
+
memo: request.memo
|
|
4337
|
+
});
|
|
4338
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4339
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4340
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4341
|
+
result.tokenTransfers.push({
|
|
4342
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4343
|
+
method: "split",
|
|
4344
|
+
requestIdHex: splitRequestIdHex
|
|
4345
|
+
});
|
|
4346
|
+
this.log(`Conservative split transfer completed`);
|
|
4347
|
+
} else {
|
|
4348
|
+
this.log("Executing instant split...");
|
|
4349
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4350
|
+
const executor = new InstantSplitExecutor({
|
|
4351
|
+
stateTransitionClient: stClient,
|
|
4352
|
+
trustBase,
|
|
4353
|
+
signingService,
|
|
4354
|
+
devMode
|
|
4355
|
+
});
|
|
4356
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4357
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4358
|
+
splitPlan.splitAmount,
|
|
4359
|
+
splitPlan.remainderAmount,
|
|
4360
|
+
splitPlan.coinId,
|
|
4361
|
+
recipientAddress,
|
|
4362
|
+
this.deps.transport,
|
|
4363
|
+
recipientPubkey,
|
|
4364
|
+
{
|
|
4365
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4366
|
+
const changeTokenData = changeToken.toJSON();
|
|
4367
|
+
const uiToken = {
|
|
4368
|
+
id: crypto.randomUUID(),
|
|
4369
|
+
coinId: request.coinId,
|
|
4370
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4371
|
+
name: this.getCoinName(request.coinId),
|
|
4372
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4373
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4374
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4375
|
+
status: "confirmed",
|
|
4376
|
+
createdAt: Date.now(),
|
|
4377
|
+
updatedAt: Date.now(),
|
|
4378
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4379
|
+
};
|
|
4380
|
+
await this.addToken(uiToken, true);
|
|
4381
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4382
|
+
},
|
|
4383
|
+
onStorageSync: async () => {
|
|
4384
|
+
await this.save();
|
|
4385
|
+
return true;
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
);
|
|
4389
|
+
if (!instantResult.success) {
|
|
4390
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4391
|
+
}
|
|
4392
|
+
if (instantResult.backgroundPromise) {
|
|
4393
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4394
|
+
}
|
|
4395
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4396
|
+
result.tokenTransfers.push({
|
|
4397
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4398
|
+
method: "split",
|
|
4399
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4400
|
+
nostrEventId: instantResult.nostrEventId
|
|
4401
|
+
});
|
|
4402
|
+
this.log(`Instant split transfer completed`);
|
|
4403
|
+
}
|
|
4251
4404
|
}
|
|
4252
4405
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4253
4406
|
const token = tokenWithAmount.uiToken;
|
|
4254
4407
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4408
|
+
if (transferMode === "conservative") {
|
|
4409
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4410
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4411
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4412
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4413
|
+
}
|
|
4414
|
+
const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
|
|
4415
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4416
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4417
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4418
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4419
|
+
memo: request.memo
|
|
4420
|
+
});
|
|
4421
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4422
|
+
} else {
|
|
4423
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4424
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4425
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4426
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4427
|
+
memo: request.memo
|
|
4428
|
+
});
|
|
4429
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4430
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4431
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4432
|
+
);
|
|
4261
4433
|
}
|
|
4262
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4263
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4264
4434
|
const requestIdBytes = commitment.requestId;
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
memo: request.memo
|
|
4435
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4436
|
+
result.tokenTransfers.push({
|
|
4437
|
+
sourceTokenId: token.id,
|
|
4438
|
+
method: "direct",
|
|
4439
|
+
requestIdHex
|
|
4271
4440
|
});
|
|
4272
|
-
|
|
4273
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4441
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4274
4442
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4275
4443
|
}
|
|
4276
4444
|
result.status = "delivered";
|
|
@@ -4283,7 +4451,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4283
4451
|
coinId: request.coinId,
|
|
4284
4452
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4285
4453
|
timestamp: Date.now(),
|
|
4286
|
-
recipientNametag
|
|
4454
|
+
recipientNametag,
|
|
4455
|
+
transferId: result.id
|
|
4287
4456
|
});
|
|
4288
4457
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4289
4458
|
return result;
|
|
@@ -4419,6 +4588,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4419
4588
|
}
|
|
4420
4589
|
);
|
|
4421
4590
|
if (result.success) {
|
|
4591
|
+
if (result.backgroundPromise) {
|
|
4592
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4593
|
+
}
|
|
4422
4594
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4423
4595
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4424
4596
|
await this.addToHistory({
|
|
@@ -4460,6 +4632,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4460
4632
|
*/
|
|
4461
4633
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4462
4634
|
this.ensureInitialized();
|
|
4635
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4636
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4637
|
+
}
|
|
4638
|
+
try {
|
|
4639
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4640
|
+
if (this.tokens.has(deterministicId)) {
|
|
4641
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4642
|
+
return { success: true, durationMs: 0 };
|
|
4643
|
+
}
|
|
4644
|
+
const registry = TokenRegistry.getInstance();
|
|
4645
|
+
const pendingData = {
|
|
4646
|
+
type: "v5_bundle",
|
|
4647
|
+
stage: "RECEIVED",
|
|
4648
|
+
bundleJson: JSON.stringify(bundle),
|
|
4649
|
+
senderPubkey,
|
|
4650
|
+
savedAt: Date.now(),
|
|
4651
|
+
attemptCount: 0
|
|
4652
|
+
};
|
|
4653
|
+
const uiToken = {
|
|
4654
|
+
id: deterministicId,
|
|
4655
|
+
coinId: bundle.coinId,
|
|
4656
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4657
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4658
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4659
|
+
amount: bundle.amount,
|
|
4660
|
+
status: "submitted",
|
|
4661
|
+
// UNCONFIRMED
|
|
4662
|
+
createdAt: Date.now(),
|
|
4663
|
+
updatedAt: Date.now(),
|
|
4664
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4665
|
+
};
|
|
4666
|
+
await this.addToken(uiToken, false);
|
|
4667
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4668
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4669
|
+
id: bundle.splitGroupId,
|
|
4670
|
+
senderPubkey,
|
|
4671
|
+
tokens: [uiToken],
|
|
4672
|
+
receivedAt: Date.now()
|
|
4673
|
+
});
|
|
4674
|
+
await this.save();
|
|
4675
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4676
|
+
});
|
|
4677
|
+
return { success: true, durationMs: 0 };
|
|
4678
|
+
} catch (error) {
|
|
4679
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4680
|
+
return {
|
|
4681
|
+
success: false,
|
|
4682
|
+
error: errorMessage,
|
|
4683
|
+
durationMs: 0
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
/**
|
|
4688
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4689
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4690
|
+
*/
|
|
4691
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4463
4692
|
try {
|
|
4464
4693
|
const signingService = await this.createSigningService();
|
|
4465
4694
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4545,7 +4774,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4545
4774
|
}
|
|
4546
4775
|
}
|
|
4547
4776
|
/**
|
|
4548
|
-
*
|
|
4777
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
4778
|
+
*
|
|
4779
|
+
* @param payload - The object to test.
|
|
4780
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4549
4781
|
*/
|
|
4550
4782
|
isInstantSplitBundle(payload) {
|
|
4551
4783
|
return isInstantSplitBundle(payload);
|
|
@@ -4626,39 +4858,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4626
4858
|
return [...this.paymentRequests];
|
|
4627
4859
|
}
|
|
4628
4860
|
/**
|
|
4629
|
-
* Get
|
|
4861
|
+
* Get the count of payment requests with status `'pending'`.
|
|
4862
|
+
*
|
|
4863
|
+
* @returns Number of pending incoming payment requests.
|
|
4630
4864
|
*/
|
|
4631
4865
|
getPendingPaymentRequestsCount() {
|
|
4632
4866
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4633
4867
|
}
|
|
4634
4868
|
/**
|
|
4635
|
-
* Accept a payment request
|
|
4869
|
+
* Accept a payment request and notify the requester.
|
|
4870
|
+
*
|
|
4871
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
4872
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
4873
|
+
*
|
|
4874
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4636
4875
|
*/
|
|
4637
4876
|
async acceptPaymentRequest(requestId2) {
|
|
4638
4877
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4639
4878
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4640
4879
|
}
|
|
4641
4880
|
/**
|
|
4642
|
-
* Reject a payment request
|
|
4881
|
+
* Reject a payment request and notify the requester.
|
|
4882
|
+
*
|
|
4883
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4643
4884
|
*/
|
|
4644
4885
|
async rejectPaymentRequest(requestId2) {
|
|
4645
4886
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4646
4887
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4647
4888
|
}
|
|
4648
4889
|
/**
|
|
4649
|
-
* Mark a payment request as paid (
|
|
4890
|
+
* Mark a payment request as paid (local status update only).
|
|
4891
|
+
*
|
|
4892
|
+
* Typically called after a successful {@link send} to record that the
|
|
4893
|
+
* request has been fulfilled.
|
|
4894
|
+
*
|
|
4895
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4650
4896
|
*/
|
|
4651
4897
|
markPaymentRequestPaid(requestId2) {
|
|
4652
4898
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4653
4899
|
}
|
|
4654
4900
|
/**
|
|
4655
|
-
*
|
|
4901
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
4902
|
+
*
|
|
4903
|
+
* Keeps only requests with status `'pending'`.
|
|
4656
4904
|
*/
|
|
4657
4905
|
clearProcessedPaymentRequests() {
|
|
4658
4906
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4659
4907
|
}
|
|
4660
4908
|
/**
|
|
4661
|
-
* Remove a specific payment request
|
|
4909
|
+
* Remove a specific incoming payment request by ID.
|
|
4910
|
+
*
|
|
4911
|
+
* @param requestId - ID of the payment request to remove.
|
|
4662
4912
|
*/
|
|
4663
4913
|
removePaymentRequest(requestId2) {
|
|
4664
4914
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -4705,13 +4955,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4705
4955
|
if (this.paymentRequests.find((r) => r.id === transportRequest.id)) {
|
|
4706
4956
|
return;
|
|
4707
4957
|
}
|
|
4958
|
+
const coinId = transportRequest.request.coinId;
|
|
4959
|
+
const registry = TokenRegistry.getInstance();
|
|
4960
|
+
const coinDef = registry.getDefinition(coinId);
|
|
4708
4961
|
const request = {
|
|
4709
4962
|
id: transportRequest.id,
|
|
4710
4963
|
senderPubkey: transportRequest.senderTransportPubkey,
|
|
4964
|
+
senderNametag: transportRequest.senderNametag,
|
|
4711
4965
|
amount: transportRequest.request.amount,
|
|
4712
|
-
coinId
|
|
4713
|
-
symbol:
|
|
4714
|
-
// Use coinId as symbol for now
|
|
4966
|
+
coinId,
|
|
4967
|
+
symbol: coinDef?.symbol || coinId.slice(0, 8),
|
|
4715
4968
|
message: transportRequest.request.message,
|
|
4716
4969
|
recipientNametag: transportRequest.request.recipientNametag,
|
|
4717
4970
|
requestId: transportRequest.request.requestId,
|
|
@@ -4780,7 +5033,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4780
5033
|
});
|
|
4781
5034
|
}
|
|
4782
5035
|
/**
|
|
4783
|
-
* Cancel
|
|
5036
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
5037
|
+
*
|
|
5038
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
5039
|
+
*
|
|
5040
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
4784
5041
|
*/
|
|
4785
5042
|
cancelWaitForPaymentResponse(requestId2) {
|
|
4786
5043
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -4791,14 +5048,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4791
5048
|
}
|
|
4792
5049
|
}
|
|
4793
5050
|
/**
|
|
4794
|
-
* Remove an outgoing payment request
|
|
5051
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
5052
|
+
*
|
|
5053
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
4795
5054
|
*/
|
|
4796
5055
|
removeOutgoingPaymentRequest(requestId2) {
|
|
4797
5056
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
4798
5057
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
4799
5058
|
}
|
|
4800
5059
|
/**
|
|
4801
|
-
*
|
|
5060
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
4802
5061
|
*/
|
|
4803
5062
|
clearCompletedOutgoingPaymentRequests() {
|
|
4804
5063
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -4870,6 +5129,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4870
5129
|
}
|
|
4871
5130
|
}
|
|
4872
5131
|
// ===========================================================================
|
|
5132
|
+
// Public API - Receive
|
|
5133
|
+
// ===========================================================================
|
|
5134
|
+
/**
|
|
5135
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5136
|
+
*
|
|
5137
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5138
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5139
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5140
|
+
*
|
|
5141
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5142
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5143
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5144
|
+
*
|
|
5145
|
+
* @param options - Optional receive options including finalization control
|
|
5146
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5147
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5148
|
+
*/
|
|
5149
|
+
async receive(options, callback) {
|
|
5150
|
+
this.ensureInitialized();
|
|
5151
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5152
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5153
|
+
}
|
|
5154
|
+
const opts = options ?? {};
|
|
5155
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5156
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5157
|
+
await this.load();
|
|
5158
|
+
const received = [];
|
|
5159
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5160
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5161
|
+
const transfer = {
|
|
5162
|
+
id: tokenId,
|
|
5163
|
+
senderPubkey: "",
|
|
5164
|
+
tokens: [token],
|
|
5165
|
+
receivedAt: Date.now()
|
|
5166
|
+
};
|
|
5167
|
+
received.push(transfer);
|
|
5168
|
+
if (callback) callback(transfer);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
const result = { transfers: received };
|
|
5172
|
+
if (opts.finalize) {
|
|
5173
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5174
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5175
|
+
const startTime = Date.now();
|
|
5176
|
+
while (Date.now() - startTime < timeout) {
|
|
5177
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5178
|
+
result.finalization = resolution;
|
|
5179
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5180
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5181
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5182
|
+
);
|
|
5183
|
+
if (!stillUnconfirmed) break;
|
|
5184
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5185
|
+
await this.load();
|
|
5186
|
+
}
|
|
5187
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5188
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5189
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5190
|
+
);
|
|
5191
|
+
} else {
|
|
5192
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5193
|
+
}
|
|
5194
|
+
return result;
|
|
5195
|
+
}
|
|
5196
|
+
// ===========================================================================
|
|
4873
5197
|
// Public API - Balance & Tokens
|
|
4874
5198
|
// ===========================================================================
|
|
4875
5199
|
/**
|
|
@@ -4879,10 +5203,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4879
5203
|
this.priceProvider = provider;
|
|
4880
5204
|
}
|
|
4881
5205
|
/**
|
|
4882
|
-
*
|
|
4883
|
-
*
|
|
5206
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5207
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
4884
5208
|
*/
|
|
4885
|
-
async
|
|
5209
|
+
async waitForPendingOperations() {
|
|
5210
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5211
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5212
|
+
this.pendingBackgroundTasks = [];
|
|
5213
|
+
}
|
|
5214
|
+
}
|
|
5215
|
+
/**
|
|
5216
|
+
* Get total portfolio value in USD.
|
|
5217
|
+
* Returns null if PriceProvider is not configured.
|
|
5218
|
+
*/
|
|
5219
|
+
async getFiatBalance() {
|
|
4886
5220
|
const assets = await this.getAssets();
|
|
4887
5221
|
if (!this.priceProvider) {
|
|
4888
5222
|
return null;
|
|
@@ -4898,19 +5232,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4898
5232
|
return hasAnyPrice ? total : null;
|
|
4899
5233
|
}
|
|
4900
5234
|
/**
|
|
4901
|
-
* Get
|
|
4902
|
-
*
|
|
5235
|
+
* Get token balances grouped by coin type.
|
|
5236
|
+
*
|
|
5237
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5238
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5239
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5240
|
+
*
|
|
5241
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5242
|
+
* for the async version with fiat pricing.
|
|
5243
|
+
*
|
|
5244
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5245
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5246
|
+
*/
|
|
5247
|
+
getBalance(coinId) {
|
|
5248
|
+
return this.aggregateTokens(coinId);
|
|
5249
|
+
}
|
|
5250
|
+
/**
|
|
5251
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5252
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
4903
5253
|
*/
|
|
4904
5254
|
async getAssets(coinId) {
|
|
5255
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5256
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5257
|
+
return rawAssets;
|
|
5258
|
+
}
|
|
5259
|
+
try {
|
|
5260
|
+
const registry = TokenRegistry.getInstance();
|
|
5261
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5262
|
+
for (const asset of rawAssets) {
|
|
5263
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5264
|
+
if (def?.name) {
|
|
5265
|
+
const existing = nameToCoins.get(def.name);
|
|
5266
|
+
if (existing) {
|
|
5267
|
+
existing.push(asset.coinId);
|
|
5268
|
+
} else {
|
|
5269
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5270
|
+
}
|
|
5271
|
+
}
|
|
5272
|
+
}
|
|
5273
|
+
if (nameToCoins.size > 0) {
|
|
5274
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5275
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5276
|
+
return rawAssets.map((raw) => {
|
|
5277
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5278
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5279
|
+
let fiatValueUsd = null;
|
|
5280
|
+
let fiatValueEur = null;
|
|
5281
|
+
if (price) {
|
|
5282
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5283
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5284
|
+
if (price.priceEur != null) {
|
|
5285
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
return {
|
|
5289
|
+
...raw,
|
|
5290
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5291
|
+
priceEur: price?.priceEur ?? null,
|
|
5292
|
+
change24h: price?.change24h ?? null,
|
|
5293
|
+
fiatValueUsd,
|
|
5294
|
+
fiatValueEur
|
|
5295
|
+
};
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
} catch (error) {
|
|
5299
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5300
|
+
}
|
|
5301
|
+
return rawAssets;
|
|
5302
|
+
}
|
|
5303
|
+
/**
|
|
5304
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5305
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5306
|
+
*/
|
|
5307
|
+
aggregateTokens(coinId) {
|
|
4905
5308
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
4906
5309
|
for (const token of this.tokens.values()) {
|
|
4907
|
-
if (token.status
|
|
5310
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
4908
5311
|
if (coinId && token.coinId !== coinId) continue;
|
|
4909
5312
|
const key = token.coinId;
|
|
5313
|
+
const amount = BigInt(token.amount);
|
|
5314
|
+
const isConfirmed = token.status === "confirmed";
|
|
4910
5315
|
const existing = assetsMap.get(key);
|
|
4911
5316
|
if (existing) {
|
|
4912
|
-
|
|
4913
|
-
|
|
5317
|
+
if (isConfirmed) {
|
|
5318
|
+
existing.confirmedAmount += amount;
|
|
5319
|
+
existing.confirmedTokenCount++;
|
|
5320
|
+
} else {
|
|
5321
|
+
existing.unconfirmedAmount += amount;
|
|
5322
|
+
existing.unconfirmedTokenCount++;
|
|
5323
|
+
}
|
|
4914
5324
|
} else {
|
|
4915
5325
|
assetsMap.set(key, {
|
|
4916
5326
|
coinId: token.coinId,
|
|
@@ -4918,78 +5328,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4918
5328
|
name: token.name,
|
|
4919
5329
|
decimals: token.decimals,
|
|
4920
5330
|
iconUrl: token.iconUrl,
|
|
4921
|
-
|
|
4922
|
-
|
|
5331
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5332
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5333
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5334
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
4923
5335
|
});
|
|
4924
5336
|
}
|
|
4925
5337
|
}
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
4929
|
-
try {
|
|
4930
|
-
const registry = TokenRegistry.getInstance();
|
|
4931
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
4932
|
-
for (const asset of rawAssets) {
|
|
4933
|
-
const def = registry.getDefinition(asset.coinId);
|
|
4934
|
-
if (def?.name) {
|
|
4935
|
-
const existing = nameToCoins.get(def.name);
|
|
4936
|
-
if (existing) {
|
|
4937
|
-
existing.push(asset.coinId);
|
|
4938
|
-
} else {
|
|
4939
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
4940
|
-
}
|
|
4941
|
-
}
|
|
4942
|
-
}
|
|
4943
|
-
if (nameToCoins.size > 0) {
|
|
4944
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
4945
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
4946
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
4947
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
4948
|
-
const price = prices.get(name);
|
|
4949
|
-
if (price) {
|
|
4950
|
-
for (const cid of coinIds) {
|
|
4951
|
-
priceMap.set(cid, {
|
|
4952
|
-
priceUsd: price.priceUsd,
|
|
4953
|
-
priceEur: price.priceEur,
|
|
4954
|
-
change24h: price.change24h
|
|
4955
|
-
});
|
|
4956
|
-
}
|
|
4957
|
-
}
|
|
4958
|
-
}
|
|
4959
|
-
}
|
|
4960
|
-
} catch (error) {
|
|
4961
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
4962
|
-
}
|
|
4963
|
-
}
|
|
4964
|
-
return rawAssets.map((raw) => {
|
|
4965
|
-
const price = priceMap?.get(raw.coinId);
|
|
4966
|
-
let fiatValueUsd = null;
|
|
4967
|
-
let fiatValueEur = null;
|
|
4968
|
-
if (price) {
|
|
4969
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
4970
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
4971
|
-
if (price.priceEur != null) {
|
|
4972
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
4973
|
-
}
|
|
4974
|
-
}
|
|
5338
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5339
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
4975
5340
|
return {
|
|
4976
5341
|
coinId: raw.coinId,
|
|
4977
5342
|
symbol: raw.symbol,
|
|
4978
5343
|
name: raw.name,
|
|
4979
5344
|
decimals: raw.decimals,
|
|
4980
5345
|
iconUrl: raw.iconUrl,
|
|
4981
|
-
totalAmount
|
|
4982
|
-
tokenCount: raw.
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
5346
|
+
totalAmount,
|
|
5347
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5348
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5349
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5350
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5351
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5352
|
+
priceUsd: null,
|
|
5353
|
+
priceEur: null,
|
|
5354
|
+
change24h: null,
|
|
5355
|
+
fiatValueUsd: null,
|
|
5356
|
+
fiatValueEur: null
|
|
4988
5357
|
};
|
|
4989
5358
|
});
|
|
4990
5359
|
}
|
|
4991
5360
|
/**
|
|
4992
|
-
* Get all tokens
|
|
5361
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5362
|
+
*
|
|
5363
|
+
* @param filter - Optional filter criteria.
|
|
5364
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5365
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5366
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
4993
5367
|
*/
|
|
4994
5368
|
getTokens(filter) {
|
|
4995
5369
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -5002,19 +5376,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5002
5376
|
return tokens;
|
|
5003
5377
|
}
|
|
5004
5378
|
/**
|
|
5005
|
-
* Get single token
|
|
5379
|
+
* Get a single token by its local ID.
|
|
5380
|
+
*
|
|
5381
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5382
|
+
* @returns The token, or `undefined` if not found.
|
|
5006
5383
|
*/
|
|
5007
5384
|
getToken(id) {
|
|
5008
5385
|
return this.tokens.get(id);
|
|
5009
5386
|
}
|
|
5010
5387
|
// ===========================================================================
|
|
5388
|
+
// Public API - Unconfirmed Token Resolution
|
|
5389
|
+
// ===========================================================================
|
|
5390
|
+
/**
|
|
5391
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5392
|
+
* their missing aggregator proofs.
|
|
5393
|
+
*
|
|
5394
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5395
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5396
|
+
*
|
|
5397
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5398
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5399
|
+
* marked `'invalid'`.
|
|
5400
|
+
*
|
|
5401
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5402
|
+
*
|
|
5403
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5404
|
+
*/
|
|
5405
|
+
async resolveUnconfirmed() {
|
|
5406
|
+
this.ensureInitialized();
|
|
5407
|
+
const result = {
|
|
5408
|
+
resolved: 0,
|
|
5409
|
+
stillPending: 0,
|
|
5410
|
+
failed: 0,
|
|
5411
|
+
details: []
|
|
5412
|
+
};
|
|
5413
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5414
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5415
|
+
if (!stClient || !trustBase) return result;
|
|
5416
|
+
const signingService = await this.createSigningService();
|
|
5417
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5418
|
+
if (token.status !== "submitted") continue;
|
|
5419
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5420
|
+
if (!pending2) {
|
|
5421
|
+
result.stillPending++;
|
|
5422
|
+
continue;
|
|
5423
|
+
}
|
|
5424
|
+
if (pending2.type === "v5_bundle") {
|
|
5425
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5426
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5427
|
+
if (progress === "resolved") result.resolved++;
|
|
5428
|
+
else if (progress === "failed") result.failed++;
|
|
5429
|
+
else result.stillPending++;
|
|
5430
|
+
}
|
|
5431
|
+
}
|
|
5432
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5433
|
+
await this.save();
|
|
5434
|
+
}
|
|
5435
|
+
return result;
|
|
5436
|
+
}
|
|
5437
|
+
// ===========================================================================
|
|
5438
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5439
|
+
// ===========================================================================
|
|
5440
|
+
/**
|
|
5441
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5442
|
+
*/
|
|
5443
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5444
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5445
|
+
pending2.attemptCount++;
|
|
5446
|
+
pending2.lastAttemptAt = Date.now();
|
|
5447
|
+
try {
|
|
5448
|
+
if (pending2.stage === "RECEIVED") {
|
|
5449
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5450
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5451
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5452
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5453
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5454
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5455
|
+
}
|
|
5456
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5457
|
+
this.updatePendingFinalization(token, pending2);
|
|
5458
|
+
}
|
|
5459
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5460
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5461
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5462
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5463
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5464
|
+
if (!proof) {
|
|
5465
|
+
this.updatePendingFinalization(token, pending2);
|
|
5466
|
+
return "pending";
|
|
5467
|
+
}
|
|
5468
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5469
|
+
pending2.stage = "MINT_PROVEN";
|
|
5470
|
+
this.updatePendingFinalization(token, pending2);
|
|
5471
|
+
}
|
|
5472
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5473
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5474
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5475
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5476
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5477
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5478
|
+
}
|
|
5479
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5480
|
+
this.updatePendingFinalization(token, pending2);
|
|
5481
|
+
}
|
|
5482
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5483
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5484
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5485
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5486
|
+
if (!proof) {
|
|
5487
|
+
this.updatePendingFinalization(token, pending2);
|
|
5488
|
+
return "pending";
|
|
5489
|
+
}
|
|
5490
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5491
|
+
const confirmedToken = {
|
|
5492
|
+
id: token.id,
|
|
5493
|
+
coinId: token.coinId,
|
|
5494
|
+
symbol: token.symbol,
|
|
5495
|
+
name: token.name,
|
|
5496
|
+
decimals: token.decimals,
|
|
5497
|
+
iconUrl: token.iconUrl,
|
|
5498
|
+
amount: token.amount,
|
|
5499
|
+
status: "confirmed",
|
|
5500
|
+
createdAt: token.createdAt,
|
|
5501
|
+
updatedAt: Date.now(),
|
|
5502
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5503
|
+
};
|
|
5504
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5505
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5506
|
+
await this.addToHistory({
|
|
5507
|
+
type: "RECEIVED",
|
|
5508
|
+
amount: confirmedToken.amount,
|
|
5509
|
+
coinId: confirmedToken.coinId,
|
|
5510
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5511
|
+
timestamp: Date.now(),
|
|
5512
|
+
senderPubkey: pending2.senderPubkey
|
|
5513
|
+
});
|
|
5514
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5515
|
+
return "resolved";
|
|
5516
|
+
}
|
|
5517
|
+
return "pending";
|
|
5518
|
+
} catch (error) {
|
|
5519
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5520
|
+
if (pending2.attemptCount > 50) {
|
|
5521
|
+
token.status = "invalid";
|
|
5522
|
+
token.updatedAt = Date.now();
|
|
5523
|
+
this.tokens.set(tokenId, token);
|
|
5524
|
+
return "failed";
|
|
5525
|
+
}
|
|
5526
|
+
this.updatePendingFinalization(token, pending2);
|
|
5527
|
+
return "pending";
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5532
|
+
*/
|
|
5533
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5534
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5535
|
+
try {
|
|
5536
|
+
const proof = await Promise.race([
|
|
5537
|
+
(0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment),
|
|
5538
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5539
|
+
]);
|
|
5540
|
+
return proof;
|
|
5541
|
+
} catch {
|
|
5542
|
+
return null;
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
/**
|
|
5546
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5547
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5548
|
+
*/
|
|
5549
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5550
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5551
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5552
|
+
const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
|
|
5553
|
+
const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
|
|
5554
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5555
|
+
const mintProof = import_InclusionProof.InclusionProof.fromJSON(mintProofJson);
|
|
5556
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5557
|
+
const tokenType = new import_TokenType3.TokenType(fromHex4(bundle.tokenTypeHex));
|
|
5558
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5559
|
+
const tokenJson = {
|
|
5560
|
+
version: "2.0",
|
|
5561
|
+
state: senderMintedStateJson,
|
|
5562
|
+
genesis: mintTransaction.toJSON(),
|
|
5563
|
+
transactions: [],
|
|
5564
|
+
nametags: []
|
|
5565
|
+
};
|
|
5566
|
+
const mintedToken = await import_Token6.Token.fromJSON(tokenJson);
|
|
5567
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5568
|
+
const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
|
|
5569
|
+
const transferProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, transferCommitment);
|
|
5570
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5571
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5572
|
+
const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
|
|
5573
|
+
mintData.tokenId,
|
|
5574
|
+
tokenType,
|
|
5575
|
+
signingService,
|
|
5576
|
+
import_HashAlgorithm5.HashAlgorithm.SHA256,
|
|
5577
|
+
transferSalt
|
|
5578
|
+
);
|
|
5579
|
+
const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
|
|
5580
|
+
let nametagTokens = [];
|
|
5581
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5582
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5583
|
+
if (bundle.nametagTokenJson) {
|
|
5584
|
+
try {
|
|
5585
|
+
const nametagToken = await import_Token6.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5586
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5587
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5588
|
+
if (proxy.address === recipientAddressStr) {
|
|
5589
|
+
nametagTokens = [nametagToken];
|
|
5590
|
+
}
|
|
5591
|
+
} catch {
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5595
|
+
try {
|
|
5596
|
+
const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
|
|
5597
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5598
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5599
|
+
if (proxy.address === recipientAddressStr) {
|
|
5600
|
+
nametagTokens = [nametagToken];
|
|
5601
|
+
}
|
|
5602
|
+
} catch {
|
|
5603
|
+
}
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5607
|
+
}
|
|
5608
|
+
/**
|
|
5609
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5610
|
+
*/
|
|
5611
|
+
parsePendingFinalization(sdkData) {
|
|
5612
|
+
if (!sdkData) return null;
|
|
5613
|
+
try {
|
|
5614
|
+
const data = JSON.parse(sdkData);
|
|
5615
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5616
|
+
return data._pendingFinalization;
|
|
5617
|
+
}
|
|
5618
|
+
return null;
|
|
5619
|
+
} catch {
|
|
5620
|
+
return null;
|
|
5621
|
+
}
|
|
5622
|
+
}
|
|
5623
|
+
/**
|
|
5624
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5625
|
+
* Creates a new token object since sdkData is readonly.
|
|
5626
|
+
*/
|
|
5627
|
+
updatePendingFinalization(token, pending2) {
|
|
5628
|
+
const updated = {
|
|
5629
|
+
id: token.id,
|
|
5630
|
+
coinId: token.coinId,
|
|
5631
|
+
symbol: token.symbol,
|
|
5632
|
+
name: token.name,
|
|
5633
|
+
decimals: token.decimals,
|
|
5634
|
+
iconUrl: token.iconUrl,
|
|
5635
|
+
amount: token.amount,
|
|
5636
|
+
status: token.status,
|
|
5637
|
+
createdAt: token.createdAt,
|
|
5638
|
+
updatedAt: Date.now(),
|
|
5639
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5640
|
+
};
|
|
5641
|
+
this.tokens.set(token.id, updated);
|
|
5642
|
+
}
|
|
5643
|
+
/**
|
|
5644
|
+
* Save pending V5 tokens to key-value storage.
|
|
5645
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5646
|
+
* so we persist them separately and restore on load().
|
|
5647
|
+
*/
|
|
5648
|
+
async savePendingV5Tokens() {
|
|
5649
|
+
const pendingTokens = [];
|
|
5650
|
+
for (const token of this.tokens.values()) {
|
|
5651
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5652
|
+
pendingTokens.push(token);
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
if (pendingTokens.length > 0) {
|
|
5656
|
+
await this.deps.storage.set(
|
|
5657
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5658
|
+
JSON.stringify(pendingTokens)
|
|
5659
|
+
);
|
|
5660
|
+
} else {
|
|
5661
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5662
|
+
}
|
|
5663
|
+
}
|
|
5664
|
+
/**
|
|
5665
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5666
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5667
|
+
*/
|
|
5668
|
+
async loadPendingV5Tokens() {
|
|
5669
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5670
|
+
if (!data) return;
|
|
5671
|
+
try {
|
|
5672
|
+
const pendingTokens = JSON.parse(data);
|
|
5673
|
+
for (const token of pendingTokens) {
|
|
5674
|
+
if (!this.tokens.has(token.id)) {
|
|
5675
|
+
this.tokens.set(token.id, token);
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
if (pendingTokens.length > 0) {
|
|
5679
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5680
|
+
}
|
|
5681
|
+
} catch {
|
|
5682
|
+
}
|
|
5683
|
+
}
|
|
5684
|
+
// ===========================================================================
|
|
5011
5685
|
// Public API - Token Operations
|
|
5012
5686
|
// ===========================================================================
|
|
5013
5687
|
/**
|
|
5014
|
-
* Add a token
|
|
5015
|
-
*
|
|
5016
|
-
*
|
|
5017
|
-
*
|
|
5688
|
+
* Add a token to the wallet.
|
|
5689
|
+
*
|
|
5690
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5691
|
+
* Duplicate detection:
|
|
5692
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5693
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5694
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5695
|
+
* the old state is archived and replaced with the incoming one.
|
|
5696
|
+
*
|
|
5697
|
+
* @param token - The token to add.
|
|
5698
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5699
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5018
5700
|
*/
|
|
5019
5701
|
async addToken(token, skipHistory = false) {
|
|
5020
5702
|
this.ensureInitialized();
|
|
@@ -5072,7 +5754,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5072
5754
|
});
|
|
5073
5755
|
}
|
|
5074
5756
|
await this.save();
|
|
5075
|
-
|
|
5757
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5758
|
+
await this.saveTokenToFileStorage(token);
|
|
5759
|
+
}
|
|
5076
5760
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5077
5761
|
return true;
|
|
5078
5762
|
}
|
|
@@ -5129,6 +5813,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5129
5813
|
const data = fileData;
|
|
5130
5814
|
const tokenJson = data.token;
|
|
5131
5815
|
if (!tokenJson) continue;
|
|
5816
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5817
|
+
continue;
|
|
5818
|
+
}
|
|
5132
5819
|
let sdkTokenId;
|
|
5133
5820
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5134
5821
|
const tokenObj = tokenJson;
|
|
@@ -5180,7 +5867,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5180
5867
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5181
5868
|
}
|
|
5182
5869
|
/**
|
|
5183
|
-
* Update an existing token
|
|
5870
|
+
* Update an existing token or add it if not found.
|
|
5871
|
+
*
|
|
5872
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5873
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5874
|
+
*
|
|
5875
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5184
5876
|
*/
|
|
5185
5877
|
async updateToken(token) {
|
|
5186
5878
|
this.ensureInitialized();
|
|
@@ -5204,7 +5896,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5204
5896
|
this.log(`Updated token ${token.id}`);
|
|
5205
5897
|
}
|
|
5206
5898
|
/**
|
|
5207
|
-
* Remove a token
|
|
5899
|
+
* Remove a token from the wallet.
|
|
5900
|
+
*
|
|
5901
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
5902
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
5903
|
+
* entry is created unless `skipHistory` is `true`.
|
|
5904
|
+
*
|
|
5905
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
5906
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
5907
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5208
5908
|
*/
|
|
5209
5909
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5210
5910
|
this.ensureInitialized();
|
|
@@ -5266,13 +5966,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5266
5966
|
// Public API - Tombstones
|
|
5267
5967
|
// ===========================================================================
|
|
5268
5968
|
/**
|
|
5269
|
-
* Get all
|
|
5969
|
+
* Get all tombstone entries.
|
|
5970
|
+
*
|
|
5971
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
5972
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
5973
|
+
*
|
|
5974
|
+
* @returns A shallow copy of the tombstone array.
|
|
5270
5975
|
*/
|
|
5271
5976
|
getTombstones() {
|
|
5272
5977
|
return [...this.tombstones];
|
|
5273
5978
|
}
|
|
5274
5979
|
/**
|
|
5275
|
-
* Check
|
|
5980
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
5981
|
+
*
|
|
5982
|
+
* @param tokenId - The genesis token ID.
|
|
5983
|
+
* @param stateHash - The state hash of the token version to check.
|
|
5984
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5276
5985
|
*/
|
|
5277
5986
|
isStateTombstoned(tokenId, stateHash) {
|
|
5278
5987
|
return this.tombstones.some(
|
|
@@ -5280,8 +5989,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5280
5989
|
);
|
|
5281
5990
|
}
|
|
5282
5991
|
/**
|
|
5283
|
-
* Merge remote
|
|
5284
|
-
*
|
|
5992
|
+
* Merge tombstones received from a remote sync source.
|
|
5993
|
+
*
|
|
5994
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
5995
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
5996
|
+
*
|
|
5997
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
5998
|
+
* @returns Number of local tokens that were removed.
|
|
5285
5999
|
*/
|
|
5286
6000
|
async mergeTombstones(remoteTombstones) {
|
|
5287
6001
|
this.ensureInitialized();
|
|
@@ -5317,7 +6031,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5317
6031
|
return removedCount;
|
|
5318
6032
|
}
|
|
5319
6033
|
/**
|
|
5320
|
-
*
|
|
6034
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
6035
|
+
*
|
|
6036
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5321
6037
|
*/
|
|
5322
6038
|
async pruneTombstones(maxAge) {
|
|
5323
6039
|
const originalCount = this.tombstones.length;
|
|
@@ -5331,20 +6047,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5331
6047
|
// Public API - Archives
|
|
5332
6048
|
// ===========================================================================
|
|
5333
6049
|
/**
|
|
5334
|
-
* Get archived tokens
|
|
6050
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
6051
|
+
*
|
|
6052
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
6053
|
+
* the genesis token ID.
|
|
6054
|
+
*
|
|
6055
|
+
* @returns A shallow copy of the archived token map.
|
|
5335
6056
|
*/
|
|
5336
6057
|
getArchivedTokens() {
|
|
5337
6058
|
return new Map(this.archivedTokens);
|
|
5338
6059
|
}
|
|
5339
6060
|
/**
|
|
5340
|
-
* Get best archived version of a token
|
|
6061
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
6062
|
+
*
|
|
6063
|
+
* Searches both archived and forked token maps and returns the version with
|
|
6064
|
+
* the highest number of committed transactions.
|
|
6065
|
+
*
|
|
6066
|
+
* @param tokenId - The genesis token ID to look up.
|
|
6067
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5341
6068
|
*/
|
|
5342
6069
|
getBestArchivedVersion(tokenId) {
|
|
5343
6070
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5344
6071
|
}
|
|
5345
6072
|
/**
|
|
5346
|
-
* Merge remote
|
|
5347
|
-
*
|
|
6073
|
+
* Merge archived tokens from a remote sync source.
|
|
6074
|
+
*
|
|
6075
|
+
* For each remote token:
|
|
6076
|
+
* - If missing locally, it is added.
|
|
6077
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
6078
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
6079
|
+
*
|
|
6080
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
6081
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5348
6082
|
*/
|
|
5349
6083
|
async mergeArchivedTokens(remoteArchived) {
|
|
5350
6084
|
let mergedCount = 0;
|
|
@@ -5367,7 +6101,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5367
6101
|
return mergedCount;
|
|
5368
6102
|
}
|
|
5369
6103
|
/**
|
|
5370
|
-
* Prune archived tokens
|
|
6104
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6105
|
+
*
|
|
6106
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6107
|
+
*
|
|
6108
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5371
6109
|
*/
|
|
5372
6110
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5373
6111
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5380,13 +6118,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5380
6118
|
// Public API - Forked Tokens
|
|
5381
6119
|
// ===========================================================================
|
|
5382
6120
|
/**
|
|
5383
|
-
* Get forked
|
|
6121
|
+
* Get all forked token versions.
|
|
6122
|
+
*
|
|
6123
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6124
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6125
|
+
*
|
|
6126
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5384
6127
|
*/
|
|
5385
6128
|
getForkedTokens() {
|
|
5386
6129
|
return new Map(this.forkedTokens);
|
|
5387
6130
|
}
|
|
5388
6131
|
/**
|
|
5389
|
-
* Store a forked token
|
|
6132
|
+
* Store a forked token version (alternative history).
|
|
6133
|
+
*
|
|
6134
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6135
|
+
*
|
|
6136
|
+
* @param tokenId - Genesis token ID.
|
|
6137
|
+
* @param stateHash - State hash of this forked version.
|
|
6138
|
+
* @param txfToken - The TXF token data to store.
|
|
5390
6139
|
*/
|
|
5391
6140
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5392
6141
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5396,8 +6145,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5396
6145
|
await this.save();
|
|
5397
6146
|
}
|
|
5398
6147
|
/**
|
|
5399
|
-
* Merge remote
|
|
5400
|
-
*
|
|
6148
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6149
|
+
*
|
|
6150
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6151
|
+
* @returns Number of new forked tokens added.
|
|
5401
6152
|
*/
|
|
5402
6153
|
async mergeForkedTokens(remoteForked) {
|
|
5403
6154
|
let addedCount = 0;
|
|
@@ -5413,7 +6164,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5413
6164
|
return addedCount;
|
|
5414
6165
|
}
|
|
5415
6166
|
/**
|
|
5416
|
-
* Prune forked tokens
|
|
6167
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6168
|
+
*
|
|
6169
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5417
6170
|
*/
|
|
5418
6171
|
async pruneForkedTokens(maxCount = 50) {
|
|
5419
6172
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5426,13 +6179,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5426
6179
|
// Public API - Transaction History
|
|
5427
6180
|
// ===========================================================================
|
|
5428
6181
|
/**
|
|
5429
|
-
* Get transaction history
|
|
6182
|
+
* Get the transaction history sorted newest-first.
|
|
6183
|
+
*
|
|
6184
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5430
6185
|
*/
|
|
5431
6186
|
getHistory() {
|
|
5432
6187
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5433
6188
|
}
|
|
5434
6189
|
/**
|
|
5435
|
-
*
|
|
6190
|
+
* Append an entry to the transaction history.
|
|
6191
|
+
*
|
|
6192
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6193
|
+
*
|
|
6194
|
+
* @param entry - History entry fields (without `id`).
|
|
5436
6195
|
*/
|
|
5437
6196
|
async addToHistory(entry) {
|
|
5438
6197
|
this.ensureInitialized();
|
|
@@ -5450,7 +6209,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5450
6209
|
// Public API - Nametag
|
|
5451
6210
|
// ===========================================================================
|
|
5452
6211
|
/**
|
|
5453
|
-
* Set nametag for current identity
|
|
6212
|
+
* Set the nametag data for the current identity.
|
|
6213
|
+
*
|
|
6214
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6215
|
+
*
|
|
6216
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5454
6217
|
*/
|
|
5455
6218
|
async setNametag(nametag) {
|
|
5456
6219
|
this.ensureInitialized();
|
|
@@ -5460,19 +6223,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5460
6223
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5461
6224
|
}
|
|
5462
6225
|
/**
|
|
5463
|
-
* Get nametag
|
|
6226
|
+
* Get the current nametag data.
|
|
6227
|
+
*
|
|
6228
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5464
6229
|
*/
|
|
5465
6230
|
getNametag() {
|
|
5466
6231
|
return this.nametag;
|
|
5467
6232
|
}
|
|
5468
6233
|
/**
|
|
5469
|
-
* Check
|
|
6234
|
+
* Check whether a nametag is currently set.
|
|
6235
|
+
*
|
|
6236
|
+
* @returns `true` if nametag data is present.
|
|
5470
6237
|
*/
|
|
5471
6238
|
hasNametag() {
|
|
5472
6239
|
return this.nametag !== null;
|
|
5473
6240
|
}
|
|
5474
6241
|
/**
|
|
5475
|
-
*
|
|
6242
|
+
* Remove the current nametag data from memory and storage.
|
|
5476
6243
|
*/
|
|
5477
6244
|
async clearNametag() {
|
|
5478
6245
|
this.ensureInitialized();
|
|
@@ -5566,9 +6333,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5566
6333
|
try {
|
|
5567
6334
|
const signingService = await this.createSigningService();
|
|
5568
6335
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5569
|
-
const { TokenType:
|
|
6336
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5570
6337
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5571
|
-
const tokenType = new
|
|
6338
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5572
6339
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5573
6340
|
tokenType,
|
|
5574
6341
|
signingService.algorithm,
|
|
@@ -5629,11 +6396,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5629
6396
|
// Public API - Sync & Validate
|
|
5630
6397
|
// ===========================================================================
|
|
5631
6398
|
/**
|
|
5632
|
-
* Sync with all token storage providers (IPFS,
|
|
5633
|
-
*
|
|
6399
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6400
|
+
*
|
|
6401
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6402
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6403
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6404
|
+
*
|
|
6405
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5634
6406
|
*/
|
|
5635
6407
|
async sync() {
|
|
5636
6408
|
this.ensureInitialized();
|
|
6409
|
+
if (this._syncInProgress) {
|
|
6410
|
+
return this._syncInProgress;
|
|
6411
|
+
}
|
|
6412
|
+
this._syncInProgress = this._doSync();
|
|
6413
|
+
try {
|
|
6414
|
+
return await this._syncInProgress;
|
|
6415
|
+
} finally {
|
|
6416
|
+
this._syncInProgress = null;
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
6419
|
+
async _doSync() {
|
|
5637
6420
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5638
6421
|
try {
|
|
5639
6422
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5671,6 +6454,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5671
6454
|
});
|
|
5672
6455
|
}
|
|
5673
6456
|
}
|
|
6457
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6458
|
+
await this.save();
|
|
6459
|
+
}
|
|
5674
6460
|
this.deps.emitEvent("sync:completed", {
|
|
5675
6461
|
source: "payments",
|
|
5676
6462
|
count: this.tokens.size
|
|
@@ -5684,6 +6470,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5684
6470
|
throw error;
|
|
5685
6471
|
}
|
|
5686
6472
|
}
|
|
6473
|
+
// ===========================================================================
|
|
6474
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6475
|
+
// ===========================================================================
|
|
6476
|
+
/**
|
|
6477
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6478
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6479
|
+
*/
|
|
6480
|
+
subscribeToStorageEvents() {
|
|
6481
|
+
this.unsubscribeStorageEvents();
|
|
6482
|
+
const providers = this.getTokenStorageProviders();
|
|
6483
|
+
for (const [providerId, provider] of providers) {
|
|
6484
|
+
if (provider.onEvent) {
|
|
6485
|
+
const unsub = provider.onEvent((event) => {
|
|
6486
|
+
if (event.type === "storage:remote-updated") {
|
|
6487
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6488
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6489
|
+
}
|
|
6490
|
+
});
|
|
6491
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6494
|
+
}
|
|
6495
|
+
/**
|
|
6496
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6497
|
+
*/
|
|
6498
|
+
unsubscribeStorageEvents() {
|
|
6499
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6500
|
+
unsub();
|
|
6501
|
+
}
|
|
6502
|
+
this.storageEventUnsubscribers = [];
|
|
6503
|
+
if (this.syncDebounceTimer) {
|
|
6504
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6505
|
+
this.syncDebounceTimer = null;
|
|
6506
|
+
}
|
|
6507
|
+
}
|
|
6508
|
+
/**
|
|
6509
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6510
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6511
|
+
*/
|
|
6512
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6513
|
+
if (this.syncDebounceTimer) {
|
|
6514
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6515
|
+
}
|
|
6516
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6517
|
+
this.syncDebounceTimer = null;
|
|
6518
|
+
this.sync().then((result) => {
|
|
6519
|
+
const data = eventData;
|
|
6520
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6521
|
+
providerId,
|
|
6522
|
+
name: data?.name ?? "",
|
|
6523
|
+
sequence: data?.sequence ?? 0,
|
|
6524
|
+
cid: data?.cid ?? "",
|
|
6525
|
+
added: result.added,
|
|
6526
|
+
removed: result.removed
|
|
6527
|
+
});
|
|
6528
|
+
}).catch((err) => {
|
|
6529
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6530
|
+
});
|
|
6531
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6532
|
+
}
|
|
5687
6533
|
/**
|
|
5688
6534
|
* Get all active token storage providers
|
|
5689
6535
|
*/
|
|
@@ -5699,15 +6545,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5699
6545
|
return /* @__PURE__ */ new Map();
|
|
5700
6546
|
}
|
|
5701
6547
|
/**
|
|
5702
|
-
*
|
|
6548
|
+
* Replace the set of token storage providers at runtime.
|
|
6549
|
+
*
|
|
6550
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6551
|
+
*
|
|
6552
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5703
6553
|
*/
|
|
5704
6554
|
updateTokenStorageProviders(providers) {
|
|
5705
6555
|
if (this.deps) {
|
|
5706
6556
|
this.deps.tokenStorageProviders = providers;
|
|
6557
|
+
this.subscribeToStorageEvents();
|
|
5707
6558
|
}
|
|
5708
6559
|
}
|
|
5709
6560
|
/**
|
|
5710
|
-
* Validate tokens
|
|
6561
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6562
|
+
*
|
|
6563
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6564
|
+
*
|
|
6565
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5711
6566
|
*/
|
|
5712
6567
|
async validate() {
|
|
5713
6568
|
this.ensureInitialized();
|
|
@@ -5728,7 +6583,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5728
6583
|
return { valid, invalid };
|
|
5729
6584
|
}
|
|
5730
6585
|
/**
|
|
5731
|
-
* Get pending transfers
|
|
6586
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6587
|
+
*
|
|
6588
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
5732
6589
|
*/
|
|
5733
6590
|
getPendingTransfers() {
|
|
5734
6591
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -5792,9 +6649,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5792
6649
|
*/
|
|
5793
6650
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
5794
6651
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5795
|
-
const { TokenType:
|
|
6652
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5796
6653
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5797
|
-
const tokenType = new
|
|
6654
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5798
6655
|
const pubkeyBytes = new Uint8Array(
|
|
5799
6656
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
5800
6657
|
);
|
|
@@ -6006,7 +6863,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6006
6863
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6007
6864
|
id: crypto.randomUUID(),
|
|
6008
6865
|
status: "completed",
|
|
6009
|
-
tokens: [finalizedToken]
|
|
6866
|
+
tokens: [finalizedToken],
|
|
6867
|
+
tokenTransfers: []
|
|
6010
6868
|
});
|
|
6011
6869
|
await this.addToHistory({
|
|
6012
6870
|
type: "RECEIVED",
|
|
@@ -6029,14 +6887,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6029
6887
|
async handleIncomingTransfer(transfer) {
|
|
6030
6888
|
try {
|
|
6031
6889
|
const payload = transfer.payload;
|
|
6890
|
+
let instantBundle = null;
|
|
6032
6891
|
if (isInstantSplitBundle(payload)) {
|
|
6892
|
+
instantBundle = payload;
|
|
6893
|
+
} else if (payload.token) {
|
|
6894
|
+
try {
|
|
6895
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
6896
|
+
if (isInstantSplitBundle(inner)) {
|
|
6897
|
+
instantBundle = inner;
|
|
6898
|
+
}
|
|
6899
|
+
} catch {
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
if (instantBundle) {
|
|
6033
6903
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6034
6904
|
try {
|
|
6035
6905
|
if (!this.nametag) {
|
|
6036
6906
|
await this.loadNametagFromFileStorage();
|
|
6037
6907
|
}
|
|
6038
6908
|
const result = await this.processInstantSplitBundle(
|
|
6039
|
-
|
|
6909
|
+
instantBundle,
|
|
6040
6910
|
transfer.senderTransportPubkey
|
|
6041
6911
|
);
|
|
6042
6912
|
if (result.success) {
|
|
@@ -6049,6 +6919,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6049
6919
|
}
|
|
6050
6920
|
return;
|
|
6051
6921
|
}
|
|
6922
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
6923
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
6924
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
6925
|
+
return;
|
|
6926
|
+
}
|
|
6052
6927
|
let tokenData;
|
|
6053
6928
|
let finalizedSdkToken = null;
|
|
6054
6929
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6204,6 +7079,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6204
7079
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6205
7080
|
}
|
|
6206
7081
|
}
|
|
7082
|
+
await this.savePendingV5Tokens();
|
|
6207
7083
|
}
|
|
6208
7084
|
async saveToOutbox(transfer, recipient) {
|
|
6209
7085
|
const outbox = await this.loadOutbox();
|
|
@@ -6221,8 +7097,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6221
7097
|
}
|
|
6222
7098
|
async createStorageData() {
|
|
6223
7099
|
return await buildTxfStorageData(
|
|
6224
|
-
|
|
6225
|
-
// Empty - active tokens stored as token-xxx files
|
|
7100
|
+
Array.from(this.tokens.values()),
|
|
6226
7101
|
{
|
|
6227
7102
|
version: 1,
|
|
6228
7103
|
address: this.deps.identity.l1Address,
|
|
@@ -6407,7 +7282,7 @@ function createPaymentsModule(config) {
|
|
|
6407
7282
|
// modules/payments/TokenRecoveryService.ts
|
|
6408
7283
|
var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
|
|
6409
7284
|
var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
|
|
6410
|
-
var
|
|
7285
|
+
var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6411
7286
|
var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
|
|
6412
7287
|
var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
6413
7288
|
var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
|
|
@@ -7556,15 +8431,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7556
8431
|
|
|
7557
8432
|
// core/Sphere.ts
|
|
7558
8433
|
var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
|
|
7559
|
-
var
|
|
8434
|
+
var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
7560
8435
|
var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
|
|
7561
8436
|
var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
8437
|
+
var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
|
|
8438
|
+
function isValidNametag(nametag) {
|
|
8439
|
+
if ((0, import_nostr_js_sdk2.isPhoneNumber)(nametag)) return true;
|
|
8440
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8441
|
+
}
|
|
7562
8442
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7563
8443
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7564
8444
|
const secret = Buffer.from(privateKey, "hex");
|
|
7565
8445
|
const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
|
|
7566
8446
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7567
|
-
const tokenType = new
|
|
8447
|
+
const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
|
|
7568
8448
|
const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
|
|
7569
8449
|
tokenType,
|
|
7570
8450
|
signingService.algorithm,
|
|
@@ -7730,8 +8610,8 @@ var Sphere = class _Sphere {
|
|
|
7730
8610
|
if (options.nametag) {
|
|
7731
8611
|
await sphere.registerNametag(options.nametag);
|
|
7732
8612
|
} else {
|
|
7733
|
-
await sphere.syncIdentityWithTransport();
|
|
7734
8613
|
await sphere.recoverNametagFromTransport();
|
|
8614
|
+
await sphere.syncIdentityWithTransport();
|
|
7735
8615
|
}
|
|
7736
8616
|
return sphere;
|
|
7737
8617
|
}
|
|
@@ -7778,9 +8658,14 @@ var Sphere = class _Sphere {
|
|
|
7778
8658
|
if (!options.mnemonic && !options.masterKey) {
|
|
7779
8659
|
throw new Error("Either mnemonic or masterKey is required");
|
|
7780
8660
|
}
|
|
8661
|
+
console.log("[Sphere.import] Starting import...");
|
|
8662
|
+
console.log("[Sphere.import] Clearing existing wallet data...");
|
|
7781
8663
|
await _Sphere.clear({ storage: options.storage, tokenStorage: options.tokenStorage });
|
|
8664
|
+
console.log("[Sphere.import] Clear done");
|
|
7782
8665
|
if (!options.storage.isConnected()) {
|
|
8666
|
+
console.log("[Sphere.import] Reconnecting storage...");
|
|
7783
8667
|
await options.storage.connect();
|
|
8668
|
+
console.log("[Sphere.import] Storage reconnected");
|
|
7784
8669
|
}
|
|
7785
8670
|
const sphere = new _Sphere(
|
|
7786
8671
|
options.storage,
|
|
@@ -7794,9 +8679,12 @@ var Sphere = class _Sphere {
|
|
|
7794
8679
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
7795
8680
|
throw new Error("Invalid mnemonic");
|
|
7796
8681
|
}
|
|
8682
|
+
console.log("[Sphere.import] Storing mnemonic...");
|
|
7797
8683
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath, options.basePath);
|
|
8684
|
+
console.log("[Sphere.import] Initializing identity from mnemonic...");
|
|
7798
8685
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
7799
8686
|
} else if (options.masterKey) {
|
|
8687
|
+
console.log("[Sphere.import] Storing master key...");
|
|
7800
8688
|
await sphere.storeMasterKey(
|
|
7801
8689
|
options.masterKey,
|
|
7802
8690
|
options.chainCode,
|
|
@@ -7804,24 +8692,43 @@ var Sphere = class _Sphere {
|
|
|
7804
8692
|
options.basePath,
|
|
7805
8693
|
options.derivationMode
|
|
7806
8694
|
);
|
|
8695
|
+
console.log("[Sphere.import] Initializing identity from master key...");
|
|
7807
8696
|
await sphere.initializeIdentityFromMasterKey(
|
|
7808
8697
|
options.masterKey,
|
|
7809
8698
|
options.chainCode,
|
|
7810
8699
|
options.derivationPath
|
|
7811
8700
|
);
|
|
7812
8701
|
}
|
|
8702
|
+
console.log("[Sphere.import] Initializing providers...");
|
|
7813
8703
|
await sphere.initializeProviders();
|
|
8704
|
+
console.log("[Sphere.import] Providers initialized. Initializing modules...");
|
|
7814
8705
|
await sphere.initializeModules();
|
|
8706
|
+
console.log("[Sphere.import] Modules initialized");
|
|
7815
8707
|
if (!options.nametag) {
|
|
8708
|
+
console.log("[Sphere.import] Recovering nametag from transport...");
|
|
7816
8709
|
await sphere.recoverNametagFromTransport();
|
|
8710
|
+
console.log("[Sphere.import] Nametag recovery done");
|
|
8711
|
+
await sphere.syncIdentityWithTransport();
|
|
7817
8712
|
}
|
|
8713
|
+
console.log("[Sphere.import] Finalizing wallet creation...");
|
|
7818
8714
|
await sphere.finalizeWalletCreation();
|
|
7819
8715
|
sphere._initialized = true;
|
|
7820
8716
|
_Sphere.instance = sphere;
|
|
8717
|
+
console.log("[Sphere.import] Tracking address 0...");
|
|
7821
8718
|
await sphere.ensureAddressTracked(0);
|
|
7822
8719
|
if (options.nametag) {
|
|
8720
|
+
console.log("[Sphere.import] Registering nametag...");
|
|
7823
8721
|
await sphere.registerNametag(options.nametag);
|
|
7824
8722
|
}
|
|
8723
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8724
|
+
try {
|
|
8725
|
+
const syncResult = await sphere._payments.sync();
|
|
8726
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8727
|
+
} catch (err) {
|
|
8728
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
console.log("[Sphere.import] Import complete");
|
|
7825
8732
|
return sphere;
|
|
7826
8733
|
}
|
|
7827
8734
|
/**
|
|
@@ -7846,6 +8753,10 @@ var Sphere = class _Sphere {
|
|
|
7846
8753
|
static async clear(storageOrOptions) {
|
|
7847
8754
|
const storage = "get" in storageOrOptions ? storageOrOptions : storageOrOptions.storage;
|
|
7848
8755
|
const tokenStorage = "get" in storageOrOptions ? void 0 : storageOrOptions.tokenStorage;
|
|
8756
|
+
if (!storage.isConnected()) {
|
|
8757
|
+
await storage.connect();
|
|
8758
|
+
}
|
|
8759
|
+
console.log("[Sphere.clear] Removing storage keys...");
|
|
7849
8760
|
await storage.remove(STORAGE_KEYS_GLOBAL.MNEMONIC);
|
|
7850
8761
|
await storage.remove(STORAGE_KEYS_GLOBAL.MASTER_KEY);
|
|
7851
8762
|
await storage.remove(STORAGE_KEYS_GLOBAL.CHAIN_CODE);
|
|
@@ -7858,12 +8769,30 @@ var Sphere = class _Sphere {
|
|
|
7858
8769
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
7859
8770
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
7860
8771
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
8772
|
+
console.log("[Sphere.clear] Storage keys removed");
|
|
7861
8773
|
if (tokenStorage?.clear) {
|
|
7862
|
-
|
|
8774
|
+
console.log("[Sphere.clear] Clearing token storage...");
|
|
8775
|
+
try {
|
|
8776
|
+
await Promise.race([
|
|
8777
|
+
tokenStorage.clear(),
|
|
8778
|
+
new Promise(
|
|
8779
|
+
(_, reject) => setTimeout(() => reject(new Error("tokenStorage.clear() timed out after 2s")), 2e3)
|
|
8780
|
+
)
|
|
8781
|
+
]);
|
|
8782
|
+
console.log("[Sphere.clear] Token storage cleared");
|
|
8783
|
+
} catch (err) {
|
|
8784
|
+
console.warn("[Sphere.clear] Token storage clear failed/timed out:", err);
|
|
8785
|
+
}
|
|
7863
8786
|
}
|
|
8787
|
+
console.log("[Sphere.clear] Destroying vesting classifier...");
|
|
7864
8788
|
await vestingClassifier.destroy();
|
|
8789
|
+
console.log("[Sphere.clear] Vesting classifier destroyed");
|
|
7865
8790
|
if (_Sphere.instance) {
|
|
8791
|
+
console.log("[Sphere.clear] Destroying Sphere instance...");
|
|
7866
8792
|
await _Sphere.instance.destroy();
|
|
8793
|
+
console.log("[Sphere.clear] Sphere instance destroyed");
|
|
8794
|
+
} else {
|
|
8795
|
+
console.log("[Sphere.clear] No Sphere instance to destroy");
|
|
7867
8796
|
}
|
|
7868
8797
|
}
|
|
7869
8798
|
/**
|
|
@@ -8244,7 +9173,8 @@ var Sphere = class _Sphere {
|
|
|
8244
9173
|
storage: options.storage,
|
|
8245
9174
|
transport: options.transport,
|
|
8246
9175
|
oracle: options.oracle,
|
|
8247
|
-
tokenStorage: options.tokenStorage
|
|
9176
|
+
tokenStorage: options.tokenStorage,
|
|
9177
|
+
l1: options.l1
|
|
8248
9178
|
});
|
|
8249
9179
|
return { success: true, mnemonic };
|
|
8250
9180
|
}
|
|
@@ -8257,7 +9187,8 @@ var Sphere = class _Sphere {
|
|
|
8257
9187
|
storage: options.storage,
|
|
8258
9188
|
transport: options.transport,
|
|
8259
9189
|
oracle: options.oracle,
|
|
8260
|
-
tokenStorage: options.tokenStorage
|
|
9190
|
+
tokenStorage: options.tokenStorage,
|
|
9191
|
+
l1: options.l1
|
|
8261
9192
|
});
|
|
8262
9193
|
return { success: true };
|
|
8263
9194
|
}
|
|
@@ -8316,7 +9247,8 @@ var Sphere = class _Sphere {
|
|
|
8316
9247
|
transport: options.transport,
|
|
8317
9248
|
oracle: options.oracle,
|
|
8318
9249
|
tokenStorage: options.tokenStorage,
|
|
8319
|
-
nametag: options.nametag
|
|
9250
|
+
nametag: options.nametag,
|
|
9251
|
+
l1: options.l1
|
|
8320
9252
|
});
|
|
8321
9253
|
return { success: true, sphere, mnemonic };
|
|
8322
9254
|
}
|
|
@@ -8345,7 +9277,8 @@ var Sphere = class _Sphere {
|
|
|
8345
9277
|
transport: options.transport,
|
|
8346
9278
|
oracle: options.oracle,
|
|
8347
9279
|
tokenStorage: options.tokenStorage,
|
|
8348
|
-
nametag: options.nametag
|
|
9280
|
+
nametag: options.nametag,
|
|
9281
|
+
l1: options.l1
|
|
8349
9282
|
});
|
|
8350
9283
|
return { success: true, sphere };
|
|
8351
9284
|
}
|
|
@@ -8376,7 +9309,8 @@ var Sphere = class _Sphere {
|
|
|
8376
9309
|
transport: options.transport,
|
|
8377
9310
|
oracle: options.oracle,
|
|
8378
9311
|
tokenStorage: options.tokenStorage,
|
|
8379
|
-
nametag: options.nametag
|
|
9312
|
+
nametag: options.nametag,
|
|
9313
|
+
l1: options.l1
|
|
8380
9314
|
});
|
|
8381
9315
|
return { success: true, sphere };
|
|
8382
9316
|
}
|
|
@@ -8395,7 +9329,8 @@ var Sphere = class _Sphere {
|
|
|
8395
9329
|
storage: options.storage,
|
|
8396
9330
|
transport: options.transport,
|
|
8397
9331
|
oracle: options.oracle,
|
|
8398
|
-
tokenStorage: options.tokenStorage
|
|
9332
|
+
tokenStorage: options.tokenStorage,
|
|
9333
|
+
l1: options.l1
|
|
8399
9334
|
});
|
|
8400
9335
|
if (result.success) {
|
|
8401
9336
|
const sphere2 = _Sphere.getInstance();
|
|
@@ -8444,7 +9379,8 @@ var Sphere = class _Sphere {
|
|
|
8444
9379
|
transport: options.transport,
|
|
8445
9380
|
oracle: options.oracle,
|
|
8446
9381
|
tokenStorage: options.tokenStorage,
|
|
8447
|
-
nametag: options.nametag
|
|
9382
|
+
nametag: options.nametag,
|
|
9383
|
+
l1: options.l1
|
|
8448
9384
|
});
|
|
8449
9385
|
return { success: true, sphere: sphere2, mnemonic };
|
|
8450
9386
|
}
|
|
@@ -8457,7 +9393,8 @@ var Sphere = class _Sphere {
|
|
|
8457
9393
|
transport: options.transport,
|
|
8458
9394
|
oracle: options.oracle,
|
|
8459
9395
|
tokenStorage: options.tokenStorage,
|
|
8460
|
-
nametag: options.nametag
|
|
9396
|
+
nametag: options.nametag,
|
|
9397
|
+
l1: options.l1
|
|
8461
9398
|
});
|
|
8462
9399
|
return { success: true, sphere };
|
|
8463
9400
|
}
|
|
@@ -8661,9 +9598,9 @@ var Sphere = class _Sphere {
|
|
|
8661
9598
|
if (index < 0) {
|
|
8662
9599
|
throw new Error("Address index must be non-negative");
|
|
8663
9600
|
}
|
|
8664
|
-
const newNametag = options?.nametag
|
|
8665
|
-
if (newNametag && !
|
|
8666
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9601
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9602
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9603
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8667
9604
|
}
|
|
8668
9605
|
const addressInfo = this.deriveAddress(index, false);
|
|
8669
9606
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9047,9 +9984,9 @@ var Sphere = class _Sphere {
|
|
|
9047
9984
|
*/
|
|
9048
9985
|
async registerNametag(nametag) {
|
|
9049
9986
|
this.ensureReady();
|
|
9050
|
-
const cleanNametag =
|
|
9051
|
-
if (!
|
|
9052
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9987
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
9988
|
+
if (!isValidNametag(cleanNametag)) {
|
|
9989
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9053
9990
|
}
|
|
9054
9991
|
if (this._identity?.nametag) {
|
|
9055
9992
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9320,46 +10257,49 @@ var Sphere = class _Sphere {
|
|
|
9320
10257
|
if (this._identity?.nametag) {
|
|
9321
10258
|
return;
|
|
9322
10259
|
}
|
|
9323
|
-
|
|
10260
|
+
let recoveredNametag = null;
|
|
10261
|
+
if (this._transport.recoverNametag) {
|
|
10262
|
+
try {
|
|
10263
|
+
recoveredNametag = await this._transport.recoverNametag();
|
|
10264
|
+
} catch {
|
|
10265
|
+
}
|
|
10266
|
+
}
|
|
10267
|
+
if (!recoveredNametag && this._transport.resolveAddressInfo && this._identity?.l1Address) {
|
|
10268
|
+
try {
|
|
10269
|
+
const info = await this._transport.resolveAddressInfo(this._identity.l1Address);
|
|
10270
|
+
if (info?.nametag) {
|
|
10271
|
+
recoveredNametag = info.nametag;
|
|
10272
|
+
}
|
|
10273
|
+
} catch {
|
|
10274
|
+
}
|
|
10275
|
+
}
|
|
10276
|
+
if (!recoveredNametag) {
|
|
9324
10277
|
return;
|
|
9325
10278
|
}
|
|
9326
10279
|
try {
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
nametags = /* @__PURE__ */ new Map();
|
|
9337
|
-
this._addressNametags.set(entry.addressId, nametags);
|
|
9338
|
-
}
|
|
9339
|
-
const nextIndex = nametags.size;
|
|
9340
|
-
nametags.set(nextIndex, recoveredNametag);
|
|
9341
|
-
await this.persistAddressNametags();
|
|
9342
|
-
if (this._transport.publishIdentityBinding) {
|
|
9343
|
-
await this._transport.publishIdentityBinding(
|
|
9344
|
-
this._identity.chainPubkey,
|
|
9345
|
-
this._identity.l1Address,
|
|
9346
|
-
this._identity.directAddress || "",
|
|
9347
|
-
recoveredNametag
|
|
9348
|
-
);
|
|
9349
|
-
}
|
|
9350
|
-
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
10280
|
+
if (this._identity) {
|
|
10281
|
+
this._identity.nametag = recoveredNametag;
|
|
10282
|
+
await this._updateCachedProxyAddress();
|
|
10283
|
+
}
|
|
10284
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
10285
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
10286
|
+
if (!nametags) {
|
|
10287
|
+
nametags = /* @__PURE__ */ new Map();
|
|
10288
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
9351
10289
|
}
|
|
10290
|
+
const nextIndex = nametags.size;
|
|
10291
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
10292
|
+
await this.persistAddressNametags();
|
|
10293
|
+
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
9352
10294
|
} catch {
|
|
9353
10295
|
}
|
|
9354
10296
|
}
|
|
9355
10297
|
/**
|
|
9356
|
-
*
|
|
10298
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9357
10299
|
*/
|
|
9358
|
-
|
|
9359
|
-
const
|
|
9360
|
-
|
|
9361
|
-
);
|
|
9362
|
-
return pattern.test(nametag);
|
|
10300
|
+
cleanNametag(raw) {
|
|
10301
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10302
|
+
return (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
|
|
9363
10303
|
}
|
|
9364
10304
|
// ===========================================================================
|
|
9365
10305
|
// Public Methods - Lifecycle
|
|
@@ -9557,8 +10497,12 @@ var Sphere = class _Sphere {
|
|
|
9557
10497
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9558
10498
|
provider.setIdentity(this._identity);
|
|
9559
10499
|
}
|
|
9560
|
-
|
|
9561
|
-
|
|
10500
|
+
if (!this._storage.isConnected()) {
|
|
10501
|
+
await this._storage.connect();
|
|
10502
|
+
}
|
|
10503
|
+
if (!this._transport.isConnected()) {
|
|
10504
|
+
await this._transport.connect();
|
|
10505
|
+
}
|
|
9562
10506
|
await this._oracle.initialize();
|
|
9563
10507
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9564
10508
|
await provider.initialize();
|
|
@@ -9716,6 +10660,7 @@ init_bech32();
|
|
|
9716
10660
|
initSphere,
|
|
9717
10661
|
isEncryptedData,
|
|
9718
10662
|
isValidBech32,
|
|
10663
|
+
isValidNametag,
|
|
9719
10664
|
isValidPrivateKey,
|
|
9720
10665
|
loadSphere,
|
|
9721
10666
|
mnemonicToEntropy,
|