@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/index.js
CHANGED
|
@@ -1612,7 +1612,7 @@ var L1PaymentsModule = class {
|
|
|
1612
1612
|
_transport;
|
|
1613
1613
|
constructor(config) {
|
|
1614
1614
|
this._config = {
|
|
1615
|
-
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.
|
|
1615
|
+
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.unicity.network:50004",
|
|
1616
1616
|
network: config?.network ?? "mainnet",
|
|
1617
1617
|
defaultFeeRate: config?.defaultFeeRate ?? 10,
|
|
1618
1618
|
enableVesting: config?.enableVesting ?? true
|
|
@@ -1644,10 +1644,17 @@ var L1PaymentsModule = class {
|
|
|
1644
1644
|
});
|
|
1645
1645
|
}
|
|
1646
1646
|
}
|
|
1647
|
-
|
|
1647
|
+
this._initialized = true;
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Ensure the Fulcrum WebSocket is connected. Called lazily before any
|
|
1651
|
+
* operation that needs the network. If the singleton is already connected
|
|
1652
|
+
* (e.g. by the address scanner), this is a no-op.
|
|
1653
|
+
*/
|
|
1654
|
+
async ensureConnected() {
|
|
1655
|
+
if (!isWebSocketConnected() && this._config.electrumUrl) {
|
|
1648
1656
|
await connect(this._config.electrumUrl);
|
|
1649
1657
|
}
|
|
1650
|
-
this._initialized = true;
|
|
1651
1658
|
}
|
|
1652
1659
|
destroy() {
|
|
1653
1660
|
if (isWebSocketConnected()) {
|
|
@@ -1705,6 +1712,7 @@ var L1PaymentsModule = class {
|
|
|
1705
1712
|
}
|
|
1706
1713
|
async send(request) {
|
|
1707
1714
|
this.ensureInitialized();
|
|
1715
|
+
await this.ensureConnected();
|
|
1708
1716
|
if (!this._wallet || !this._identity) {
|
|
1709
1717
|
return { success: false, error: "No wallet available" };
|
|
1710
1718
|
}
|
|
@@ -1739,6 +1747,7 @@ var L1PaymentsModule = class {
|
|
|
1739
1747
|
}
|
|
1740
1748
|
async getBalance() {
|
|
1741
1749
|
this.ensureInitialized();
|
|
1750
|
+
await this.ensureConnected();
|
|
1742
1751
|
const addresses = this._getWatchedAddresses();
|
|
1743
1752
|
let totalAlpha = 0;
|
|
1744
1753
|
let vestedSats = BigInt(0);
|
|
@@ -1770,6 +1779,7 @@ var L1PaymentsModule = class {
|
|
|
1770
1779
|
}
|
|
1771
1780
|
async getUtxos() {
|
|
1772
1781
|
this.ensureInitialized();
|
|
1782
|
+
await this.ensureConnected();
|
|
1773
1783
|
const result = [];
|
|
1774
1784
|
const currentHeight = await getCurrentBlockHeight();
|
|
1775
1785
|
const allUtxos = await this._getAllUtxos();
|
|
@@ -1805,42 +1815,73 @@ var L1PaymentsModule = class {
|
|
|
1805
1815
|
return result;
|
|
1806
1816
|
}
|
|
1807
1817
|
async getHistory(limit) {
|
|
1818
|
+
await this.ensureConnected();
|
|
1808
1819
|
this.ensureInitialized();
|
|
1809
1820
|
const addresses = this._getWatchedAddresses();
|
|
1810
1821
|
const transactions = [];
|
|
1811
1822
|
const seenTxids = /* @__PURE__ */ new Set();
|
|
1812
1823
|
const currentHeight = await getCurrentBlockHeight();
|
|
1824
|
+
const txCache = /* @__PURE__ */ new Map();
|
|
1825
|
+
const fetchTx = async (txid) => {
|
|
1826
|
+
if (txCache.has(txid)) return txCache.get(txid);
|
|
1827
|
+
const detail = await getTransaction(txid);
|
|
1828
|
+
txCache.set(txid, detail);
|
|
1829
|
+
return detail;
|
|
1830
|
+
};
|
|
1831
|
+
const addressSet = new Set(addresses.map((a) => a.toLowerCase()));
|
|
1813
1832
|
for (const address of addresses) {
|
|
1814
1833
|
const history = await getTransactionHistory(address);
|
|
1815
1834
|
for (const item of history) {
|
|
1816
1835
|
if (seenTxids.has(item.tx_hash)) continue;
|
|
1817
1836
|
seenTxids.add(item.tx_hash);
|
|
1818
|
-
const tx = await
|
|
1837
|
+
const tx = await fetchTx(item.tx_hash);
|
|
1819
1838
|
if (!tx) continue;
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1839
|
+
let isSend = false;
|
|
1840
|
+
for (const vin of tx.vin ?? []) {
|
|
1841
|
+
if (!vin.txid) continue;
|
|
1842
|
+
const prevTx = await fetchTx(vin.txid);
|
|
1843
|
+
if (prevTx?.vout?.[vin.vout]) {
|
|
1844
|
+
const prevOut = prevTx.vout[vin.vout];
|
|
1845
|
+
const prevAddrs = [
|
|
1846
|
+
...prevOut.scriptPubKey?.addresses ?? [],
|
|
1847
|
+
...prevOut.scriptPubKey?.address ? [prevOut.scriptPubKey.address] : []
|
|
1848
|
+
];
|
|
1849
|
+
if (prevAddrs.some((a) => addressSet.has(a.toLowerCase()))) {
|
|
1850
|
+
isSend = true;
|
|
1851
|
+
break;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
let amountToUs = 0;
|
|
1856
|
+
let amountToOthers = 0;
|
|
1824
1857
|
let txAddress = address;
|
|
1858
|
+
let externalAddress = "";
|
|
1825
1859
|
if (tx.vout) {
|
|
1826
1860
|
for (const vout of tx.vout) {
|
|
1827
|
-
const voutAddresses =
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
const
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1861
|
+
const voutAddresses = [
|
|
1862
|
+
...vout.scriptPubKey?.addresses ?? [],
|
|
1863
|
+
...vout.scriptPubKey?.address ? [vout.scriptPubKey.address] : []
|
|
1864
|
+
];
|
|
1865
|
+
const isOurs = voutAddresses.some((a) => addressSet.has(a.toLowerCase()));
|
|
1866
|
+
const valueSats = Math.floor((vout.value ?? 0) * 1e8);
|
|
1867
|
+
if (isOurs) {
|
|
1868
|
+
amountToUs += valueSats;
|
|
1869
|
+
if (!txAddress) txAddress = voutAddresses[0];
|
|
1870
|
+
} else {
|
|
1871
|
+
amountToOthers += valueSats;
|
|
1872
|
+
if (!externalAddress && voutAddresses.length > 0) {
|
|
1873
|
+
externalAddress = voutAddresses[0];
|
|
1874
|
+
}
|
|
1836
1875
|
}
|
|
1837
1876
|
}
|
|
1838
1877
|
}
|
|
1878
|
+
const amount = isSend ? amountToOthers.toString() : amountToUs.toString();
|
|
1879
|
+
const displayAddress = isSend ? externalAddress || txAddress : txAddress;
|
|
1839
1880
|
transactions.push({
|
|
1840
1881
|
txid: item.tx_hash,
|
|
1841
1882
|
type: isSend ? "send" : "receive",
|
|
1842
1883
|
amount,
|
|
1843
|
-
address:
|
|
1884
|
+
address: displayAddress,
|
|
1844
1885
|
confirmations: item.height > 0 ? currentHeight - item.height : 0,
|
|
1845
1886
|
timestamp: tx.time ? tx.time * 1e3 : Date.now(),
|
|
1846
1887
|
blockHeight: item.height > 0 ? item.height : void 0
|
|
@@ -1852,6 +1893,7 @@ var L1PaymentsModule = class {
|
|
|
1852
1893
|
}
|
|
1853
1894
|
async getTransaction(txid) {
|
|
1854
1895
|
this.ensureInitialized();
|
|
1896
|
+
await this.ensureConnected();
|
|
1855
1897
|
const tx = await getTransaction(txid);
|
|
1856
1898
|
if (!tx) return null;
|
|
1857
1899
|
const addresses = this._getWatchedAddresses();
|
|
@@ -1887,6 +1929,7 @@ var L1PaymentsModule = class {
|
|
|
1887
1929
|
}
|
|
1888
1930
|
async estimateFee(to, amount) {
|
|
1889
1931
|
this.ensureInitialized();
|
|
1932
|
+
await this.ensureConnected();
|
|
1890
1933
|
if (!this._wallet) {
|
|
1891
1934
|
return { fee: "0", feeRate: this._config.defaultFeeRate ?? 10 };
|
|
1892
1935
|
}
|
|
@@ -2229,6 +2272,7 @@ import { MintCommitment } from "@unicitylabs/state-transition-sdk/lib/transactio
|
|
|
2229
2272
|
import { HashAlgorithm as HashAlgorithm2 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
2230
2273
|
import { UnmaskedPredicate as UnmaskedPredicate2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
2231
2274
|
import { waitInclusionProof as waitInclusionProof2 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
2275
|
+
import { normalizeNametag } from "@unicitylabs/nostr-js-sdk";
|
|
2232
2276
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2233
2277
|
var NametagMinter = class {
|
|
2234
2278
|
client;
|
|
@@ -2253,7 +2297,8 @@ var NametagMinter = class {
|
|
|
2253
2297
|
*/
|
|
2254
2298
|
async isNametagAvailable(nametag) {
|
|
2255
2299
|
try {
|
|
2256
|
-
const
|
|
2300
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2301
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2257
2302
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
2258
2303
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2259
2304
|
return !isMinted;
|
|
@@ -2270,7 +2315,8 @@ var NametagMinter = class {
|
|
|
2270
2315
|
* @returns MintNametagResult with token if successful
|
|
2271
2316
|
*/
|
|
2272
2317
|
async mintNametag(nametag, ownerAddress) {
|
|
2273
|
-
const
|
|
2318
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2319
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2274
2320
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2275
2321
|
try {
|
|
2276
2322
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
@@ -2410,7 +2456,9 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2410
2456
|
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2411
2457
|
ADDRESS_NAMETAGS: "address_nametags",
|
|
2412
2458
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2413
|
-
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2459
|
+
TRACKED_ADDRESSES: "tracked_addresses",
|
|
2460
|
+
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
2461
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
2414
2462
|
};
|
|
2415
2463
|
var STORAGE_KEYS_ADDRESS = {
|
|
2416
2464
|
/** Pending transfers for this address */
|
|
@@ -2422,7 +2470,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2422
2470
|
/** Messages for this address */
|
|
2423
2471
|
MESSAGES: "messages",
|
|
2424
2472
|
/** Transaction history for this address */
|
|
2425
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2473
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2474
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2475
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2426
2476
|
};
|
|
2427
2477
|
var STORAGE_KEYS = {
|
|
2428
2478
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2837,6 +2887,18 @@ function parseTxfStorageData(data) {
|
|
|
2837
2887
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2838
2888
|
}
|
|
2839
2889
|
}
|
|
2890
|
+
} else if (key.startsWith("token-")) {
|
|
2891
|
+
try {
|
|
2892
|
+
const entry = storageData[key];
|
|
2893
|
+
const txfToken = entry?.token;
|
|
2894
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2895
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2896
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2897
|
+
result.tokens.push(token);
|
|
2898
|
+
}
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2901
|
+
}
|
|
2840
2902
|
}
|
|
2841
2903
|
}
|
|
2842
2904
|
return result;
|
|
@@ -3412,8 +3474,9 @@ var InstantSplitExecutor = class {
|
|
|
3412
3474
|
const criticalPathDuration = performance.now() - startTime;
|
|
3413
3475
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3414
3476
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3477
|
+
let backgroundPromise;
|
|
3415
3478
|
if (!options?.skipBackground) {
|
|
3416
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3479
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3417
3480
|
signingService: this.signingService,
|
|
3418
3481
|
tokenType: tokenToSplit.type,
|
|
3419
3482
|
coinId,
|
|
@@ -3429,7 +3492,8 @@ var InstantSplitExecutor = class {
|
|
|
3429
3492
|
nostrEventId,
|
|
3430
3493
|
splitGroupId,
|
|
3431
3494
|
criticalPathDurationMs: criticalPathDuration,
|
|
3432
|
-
backgroundStarted: !options?.skipBackground
|
|
3495
|
+
backgroundStarted: !options?.skipBackground,
|
|
3496
|
+
backgroundPromise
|
|
3433
3497
|
};
|
|
3434
3498
|
} catch (error) {
|
|
3435
3499
|
const duration = performance.now() - startTime;
|
|
@@ -3491,7 +3555,7 @@ var InstantSplitExecutor = class {
|
|
|
3491
3555
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3492
3556
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3493
3557
|
]);
|
|
3494
|
-
submissions.then(async (results) => {
|
|
3558
|
+
return submissions.then(async (results) => {
|
|
3495
3559
|
const submitDuration = performance.now() - startTime;
|
|
3496
3560
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3497
3561
|
context.onProgress?.({
|
|
@@ -3956,6 +4020,11 @@ import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/Add
|
|
|
3956
4020
|
import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3957
4021
|
import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3958
4022
|
import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
4023
|
+
import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
4024
|
+
import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
|
|
4025
|
+
import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
4026
|
+
import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
4027
|
+
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
|
|
3959
4028
|
function enrichWithRegistry(info) {
|
|
3960
4029
|
const registry = TokenRegistry.getInstance();
|
|
3961
4030
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3983,7 +4052,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3983
4052
|
try {
|
|
3984
4053
|
const sdkToken = await SdkToken2.fromJSON(data);
|
|
3985
4054
|
if (sdkToken.id) {
|
|
3986
|
-
defaultInfo.tokenId = sdkToken.id.
|
|
4055
|
+
defaultInfo.tokenId = sdkToken.id.toJSON();
|
|
3987
4056
|
}
|
|
3988
4057
|
if (sdkToken.coins && sdkToken.coins.coins) {
|
|
3989
4058
|
const rawCoins = sdkToken.coins.coins;
|
|
@@ -4153,6 +4222,13 @@ function extractTokenStateKey(token) {
|
|
|
4153
4222
|
if (!tokenId || !stateHash) return null;
|
|
4154
4223
|
return createTokenStateKey(tokenId, stateHash);
|
|
4155
4224
|
}
|
|
4225
|
+
function fromHex4(hex) {
|
|
4226
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
4227
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
4228
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
4229
|
+
}
|
|
4230
|
+
return bytes;
|
|
4231
|
+
}
|
|
4156
4232
|
function hasSameGenesisTokenId(t1, t2) {
|
|
4157
4233
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
4158
4234
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -4242,6 +4318,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4242
4318
|
// Token State
|
|
4243
4319
|
tokens = /* @__PURE__ */ new Map();
|
|
4244
4320
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
4321
|
+
pendingBackgroundTasks = [];
|
|
4245
4322
|
// Repository State (tombstones, archives, forked, history)
|
|
4246
4323
|
tombstones = [];
|
|
4247
4324
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -4266,6 +4343,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4266
4343
|
// Poll every 2s
|
|
4267
4344
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
4268
4345
|
// Max 30 attempts (~60s)
|
|
4346
|
+
// Storage event subscriptions (push-based sync)
|
|
4347
|
+
storageEventUnsubscribers = [];
|
|
4348
|
+
syncDebounceTimer = null;
|
|
4349
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4350
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4351
|
+
_syncInProgress = null;
|
|
4269
4352
|
constructor(config) {
|
|
4270
4353
|
this.moduleConfig = {
|
|
4271
4354
|
autoSync: config?.autoSync ?? true,
|
|
@@ -4274,10 +4357,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4274
4357
|
maxRetries: config?.maxRetries ?? 3,
|
|
4275
4358
|
debug: config?.debug ?? false
|
|
4276
4359
|
};
|
|
4277
|
-
|
|
4278
|
-
this.l1 = l1Enabled ? new L1PaymentsModule(config?.l1) : null;
|
|
4360
|
+
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
4279
4361
|
}
|
|
4280
|
-
/**
|
|
4362
|
+
/**
|
|
4363
|
+
* Get the current module configuration (excluding L1 config).
|
|
4364
|
+
*
|
|
4365
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4366
|
+
*/
|
|
4281
4367
|
getConfig() {
|
|
4282
4368
|
return this.moduleConfig;
|
|
4283
4369
|
}
|
|
@@ -4318,9 +4404,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4318
4404
|
transport: deps.transport
|
|
4319
4405
|
});
|
|
4320
4406
|
}
|
|
4321
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4322
|
-
this.handleIncomingTransfer(transfer)
|
|
4323
|
-
|
|
4407
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4408
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4409
|
+
);
|
|
4324
4410
|
if (deps.transport.onPaymentRequest) {
|
|
4325
4411
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4326
4412
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4331,9 +4417,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4331
4417
|
this.handlePaymentRequestResponse(response);
|
|
4332
4418
|
});
|
|
4333
4419
|
}
|
|
4420
|
+
this.subscribeToStorageEvents();
|
|
4334
4421
|
}
|
|
4335
4422
|
/**
|
|
4336
|
-
* Load
|
|
4423
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4424
|
+
*
|
|
4425
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4426
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4427
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4337
4428
|
*/
|
|
4338
4429
|
async load() {
|
|
4339
4430
|
this.ensureInitialized();
|
|
@@ -4350,6 +4441,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4350
4441
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4351
4442
|
}
|
|
4352
4443
|
}
|
|
4444
|
+
await this.loadPendingV5Tokens();
|
|
4353
4445
|
await this.loadTokensFromFileStorage();
|
|
4354
4446
|
await this.loadNametagFromFileStorage();
|
|
4355
4447
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4367,9 +4459,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4367
4459
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4368
4460
|
}
|
|
4369
4461
|
}
|
|
4462
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4463
|
+
});
|
|
4370
4464
|
}
|
|
4371
4465
|
/**
|
|
4372
|
-
* Cleanup
|
|
4466
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4467
|
+
*
|
|
4468
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4469
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4373
4470
|
*/
|
|
4374
4471
|
destroy() {
|
|
4375
4472
|
this.unsubscribeTransfers?.();
|
|
@@ -4387,6 +4484,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4387
4484
|
resolver.reject(new Error("Module destroyed"));
|
|
4388
4485
|
}
|
|
4389
4486
|
this.pendingResponseResolvers.clear();
|
|
4487
|
+
this.unsubscribeStorageEvents();
|
|
4390
4488
|
if (this.l1) {
|
|
4391
4489
|
this.l1.destroy();
|
|
4392
4490
|
}
|
|
@@ -4403,7 +4501,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4403
4501
|
const result = {
|
|
4404
4502
|
id: crypto.randomUUID(),
|
|
4405
4503
|
status: "pending",
|
|
4406
|
-
tokens: []
|
|
4504
|
+
tokens: [],
|
|
4505
|
+
tokenTransfers: []
|
|
4407
4506
|
};
|
|
4408
4507
|
try {
|
|
4409
4508
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4440,69 +4539,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4440
4539
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4441
4540
|
result.status = "submitted";
|
|
4442
4541
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4542
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4443
4543
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4544
|
+
if (transferMode === "conservative") {
|
|
4545
|
+
this.log("Executing conservative split...");
|
|
4546
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4547
|
+
stateTransitionClient: stClient,
|
|
4548
|
+
trustBase,
|
|
4549
|
+
signingService
|
|
4550
|
+
});
|
|
4551
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4552
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4553
|
+
splitPlan.splitAmount,
|
|
4554
|
+
splitPlan.remainderAmount,
|
|
4555
|
+
splitPlan.coinId,
|
|
4556
|
+
recipientAddress
|
|
4557
|
+
);
|
|
4558
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4559
|
+
const changeUiToken = {
|
|
4560
|
+
id: crypto.randomUUID(),
|
|
4561
|
+
coinId: request.coinId,
|
|
4562
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4563
|
+
name: this.getCoinName(request.coinId),
|
|
4564
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4565
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4566
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4567
|
+
status: "confirmed",
|
|
4568
|
+
createdAt: Date.now(),
|
|
4569
|
+
updatedAt: Date.now(),
|
|
4570
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4571
|
+
};
|
|
4572
|
+
await this.addToken(changeUiToken, true);
|
|
4573
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4574
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4575
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4576
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4577
|
+
memo: request.memo
|
|
4578
|
+
});
|
|
4579
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4580
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4581
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4582
|
+
result.tokenTransfers.push({
|
|
4583
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4584
|
+
method: "split",
|
|
4585
|
+
requestIdHex: splitRequestIdHex
|
|
4586
|
+
});
|
|
4587
|
+
this.log(`Conservative split transfer completed`);
|
|
4588
|
+
} else {
|
|
4589
|
+
this.log("Executing instant split...");
|
|
4590
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4591
|
+
const executor = new InstantSplitExecutor({
|
|
4592
|
+
stateTransitionClient: stClient,
|
|
4593
|
+
trustBase,
|
|
4594
|
+
signingService,
|
|
4595
|
+
devMode
|
|
4596
|
+
});
|
|
4597
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4598
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4599
|
+
splitPlan.splitAmount,
|
|
4600
|
+
splitPlan.remainderAmount,
|
|
4601
|
+
splitPlan.coinId,
|
|
4602
|
+
recipientAddress,
|
|
4603
|
+
this.deps.transport,
|
|
4604
|
+
recipientPubkey,
|
|
4605
|
+
{
|
|
4606
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4607
|
+
const changeTokenData = changeToken.toJSON();
|
|
4608
|
+
const uiToken = {
|
|
4609
|
+
id: crypto.randomUUID(),
|
|
4610
|
+
coinId: request.coinId,
|
|
4611
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4612
|
+
name: this.getCoinName(request.coinId),
|
|
4613
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4614
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4615
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4616
|
+
status: "confirmed",
|
|
4617
|
+
createdAt: Date.now(),
|
|
4618
|
+
updatedAt: Date.now(),
|
|
4619
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4620
|
+
};
|
|
4621
|
+
await this.addToken(uiToken, true);
|
|
4622
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4623
|
+
},
|
|
4624
|
+
onStorageSync: async () => {
|
|
4625
|
+
await this.save();
|
|
4626
|
+
return true;
|
|
4627
|
+
}
|
|
4628
|
+
}
|
|
4629
|
+
);
|
|
4630
|
+
if (!instantResult.success) {
|
|
4631
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4632
|
+
}
|
|
4633
|
+
if (instantResult.backgroundPromise) {
|
|
4634
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4635
|
+
}
|
|
4636
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4637
|
+
result.tokenTransfers.push({
|
|
4638
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4639
|
+
method: "split",
|
|
4640
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4641
|
+
nostrEventId: instantResult.nostrEventId
|
|
4642
|
+
});
|
|
4643
|
+
this.log(`Instant split transfer completed`);
|
|
4644
|
+
}
|
|
4483
4645
|
}
|
|
4484
4646
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4485
4647
|
const token = tokenWithAmount.uiToken;
|
|
4486
4648
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4649
|
+
if (transferMode === "conservative") {
|
|
4650
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4651
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4652
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4653
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4654
|
+
}
|
|
4655
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4656
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4657
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4658
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4659
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4660
|
+
memo: request.memo
|
|
4661
|
+
});
|
|
4662
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4663
|
+
} else {
|
|
4664
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4665
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4666
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4667
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4668
|
+
memo: request.memo
|
|
4669
|
+
});
|
|
4670
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4671
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4672
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4673
|
+
);
|
|
4493
4674
|
}
|
|
4494
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4495
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4496
4675
|
const requestIdBytes = commitment.requestId;
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
memo: request.memo
|
|
4676
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4677
|
+
result.tokenTransfers.push({
|
|
4678
|
+
sourceTokenId: token.id,
|
|
4679
|
+
method: "direct",
|
|
4680
|
+
requestIdHex
|
|
4503
4681
|
});
|
|
4504
|
-
|
|
4505
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4682
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4506
4683
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4507
4684
|
}
|
|
4508
4685
|
result.status = "delivered";
|
|
@@ -4515,7 +4692,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4515
4692
|
coinId: request.coinId,
|
|
4516
4693
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4517
4694
|
timestamp: Date.now(),
|
|
4518
|
-
recipientNametag
|
|
4695
|
+
recipientNametag,
|
|
4696
|
+
transferId: result.id
|
|
4519
4697
|
});
|
|
4520
4698
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4521
4699
|
return result;
|
|
@@ -4651,6 +4829,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4651
4829
|
}
|
|
4652
4830
|
);
|
|
4653
4831
|
if (result.success) {
|
|
4832
|
+
if (result.backgroundPromise) {
|
|
4833
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4834
|
+
}
|
|
4654
4835
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4655
4836
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4656
4837
|
await this.addToHistory({
|
|
@@ -4692,6 +4873,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4692
4873
|
*/
|
|
4693
4874
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4694
4875
|
this.ensureInitialized();
|
|
4876
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4877
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4878
|
+
}
|
|
4879
|
+
try {
|
|
4880
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4881
|
+
if (this.tokens.has(deterministicId)) {
|
|
4882
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4883
|
+
return { success: true, durationMs: 0 };
|
|
4884
|
+
}
|
|
4885
|
+
const registry = TokenRegistry.getInstance();
|
|
4886
|
+
const pendingData = {
|
|
4887
|
+
type: "v5_bundle",
|
|
4888
|
+
stage: "RECEIVED",
|
|
4889
|
+
bundleJson: JSON.stringify(bundle),
|
|
4890
|
+
senderPubkey,
|
|
4891
|
+
savedAt: Date.now(),
|
|
4892
|
+
attemptCount: 0
|
|
4893
|
+
};
|
|
4894
|
+
const uiToken = {
|
|
4895
|
+
id: deterministicId,
|
|
4896
|
+
coinId: bundle.coinId,
|
|
4897
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4898
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4899
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4900
|
+
amount: bundle.amount,
|
|
4901
|
+
status: "submitted",
|
|
4902
|
+
// UNCONFIRMED
|
|
4903
|
+
createdAt: Date.now(),
|
|
4904
|
+
updatedAt: Date.now(),
|
|
4905
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4906
|
+
};
|
|
4907
|
+
await this.addToken(uiToken, false);
|
|
4908
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4909
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4910
|
+
id: bundle.splitGroupId,
|
|
4911
|
+
senderPubkey,
|
|
4912
|
+
tokens: [uiToken],
|
|
4913
|
+
receivedAt: Date.now()
|
|
4914
|
+
});
|
|
4915
|
+
await this.save();
|
|
4916
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4917
|
+
});
|
|
4918
|
+
return { success: true, durationMs: 0 };
|
|
4919
|
+
} catch (error) {
|
|
4920
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4921
|
+
return {
|
|
4922
|
+
success: false,
|
|
4923
|
+
error: errorMessage,
|
|
4924
|
+
durationMs: 0
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
/**
|
|
4929
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4930
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4931
|
+
*/
|
|
4932
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4695
4933
|
try {
|
|
4696
4934
|
const signingService = await this.createSigningService();
|
|
4697
4935
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4777,7 +5015,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4777
5015
|
}
|
|
4778
5016
|
}
|
|
4779
5017
|
/**
|
|
4780
|
-
*
|
|
5018
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
5019
|
+
*
|
|
5020
|
+
* @param payload - The object to test.
|
|
5021
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4781
5022
|
*/
|
|
4782
5023
|
isInstantSplitBundle(payload) {
|
|
4783
5024
|
return isInstantSplitBundle(payload);
|
|
@@ -4858,39 +5099,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4858
5099
|
return [...this.paymentRequests];
|
|
4859
5100
|
}
|
|
4860
5101
|
/**
|
|
4861
|
-
* Get
|
|
5102
|
+
* Get the count of payment requests with status `'pending'`.
|
|
5103
|
+
*
|
|
5104
|
+
* @returns Number of pending incoming payment requests.
|
|
4862
5105
|
*/
|
|
4863
5106
|
getPendingPaymentRequestsCount() {
|
|
4864
5107
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4865
5108
|
}
|
|
4866
5109
|
/**
|
|
4867
|
-
* Accept a payment request
|
|
5110
|
+
* Accept a payment request and notify the requester.
|
|
5111
|
+
*
|
|
5112
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
5113
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
5114
|
+
*
|
|
5115
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4868
5116
|
*/
|
|
4869
5117
|
async acceptPaymentRequest(requestId2) {
|
|
4870
5118
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4871
5119
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4872
5120
|
}
|
|
4873
5121
|
/**
|
|
4874
|
-
* Reject a payment request
|
|
5122
|
+
* Reject a payment request and notify the requester.
|
|
5123
|
+
*
|
|
5124
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4875
5125
|
*/
|
|
4876
5126
|
async rejectPaymentRequest(requestId2) {
|
|
4877
5127
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4878
5128
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4879
5129
|
}
|
|
4880
5130
|
/**
|
|
4881
|
-
* Mark a payment request as paid (
|
|
5131
|
+
* Mark a payment request as paid (local status update only).
|
|
5132
|
+
*
|
|
5133
|
+
* Typically called after a successful {@link send} to record that the
|
|
5134
|
+
* request has been fulfilled.
|
|
5135
|
+
*
|
|
5136
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4882
5137
|
*/
|
|
4883
5138
|
markPaymentRequestPaid(requestId2) {
|
|
4884
5139
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4885
5140
|
}
|
|
4886
5141
|
/**
|
|
4887
|
-
*
|
|
5142
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
5143
|
+
*
|
|
5144
|
+
* Keeps only requests with status `'pending'`.
|
|
4888
5145
|
*/
|
|
4889
5146
|
clearProcessedPaymentRequests() {
|
|
4890
5147
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4891
5148
|
}
|
|
4892
5149
|
/**
|
|
4893
|
-
* Remove a specific payment request
|
|
5150
|
+
* Remove a specific incoming payment request by ID.
|
|
5151
|
+
*
|
|
5152
|
+
* @param requestId - ID of the payment request to remove.
|
|
4894
5153
|
*/
|
|
4895
5154
|
removePaymentRequest(requestId2) {
|
|
4896
5155
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -4937,13 +5196,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4937
5196
|
if (this.paymentRequests.find((r) => r.id === transportRequest.id)) {
|
|
4938
5197
|
return;
|
|
4939
5198
|
}
|
|
5199
|
+
const coinId = transportRequest.request.coinId;
|
|
5200
|
+
const registry = TokenRegistry.getInstance();
|
|
5201
|
+
const coinDef = registry.getDefinition(coinId);
|
|
4940
5202
|
const request = {
|
|
4941
5203
|
id: transportRequest.id,
|
|
4942
5204
|
senderPubkey: transportRequest.senderTransportPubkey,
|
|
5205
|
+
senderNametag: transportRequest.senderNametag,
|
|
4943
5206
|
amount: transportRequest.request.amount,
|
|
4944
|
-
coinId
|
|
4945
|
-
symbol:
|
|
4946
|
-
// Use coinId as symbol for now
|
|
5207
|
+
coinId,
|
|
5208
|
+
symbol: coinDef?.symbol || coinId.slice(0, 8),
|
|
4947
5209
|
message: transportRequest.request.message,
|
|
4948
5210
|
recipientNametag: transportRequest.request.recipientNametag,
|
|
4949
5211
|
requestId: transportRequest.request.requestId,
|
|
@@ -5012,7 +5274,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5012
5274
|
});
|
|
5013
5275
|
}
|
|
5014
5276
|
/**
|
|
5015
|
-
* Cancel
|
|
5277
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
5278
|
+
*
|
|
5279
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
5280
|
+
*
|
|
5281
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
5016
5282
|
*/
|
|
5017
5283
|
cancelWaitForPaymentResponse(requestId2) {
|
|
5018
5284
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -5023,14 +5289,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5023
5289
|
}
|
|
5024
5290
|
}
|
|
5025
5291
|
/**
|
|
5026
|
-
* Remove an outgoing payment request
|
|
5292
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
5293
|
+
*
|
|
5294
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
5027
5295
|
*/
|
|
5028
5296
|
removeOutgoingPaymentRequest(requestId2) {
|
|
5029
5297
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
5030
5298
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
5031
5299
|
}
|
|
5032
5300
|
/**
|
|
5033
|
-
*
|
|
5301
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
5034
5302
|
*/
|
|
5035
5303
|
clearCompletedOutgoingPaymentRequests() {
|
|
5036
5304
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -5102,6 +5370,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5102
5370
|
}
|
|
5103
5371
|
}
|
|
5104
5372
|
// ===========================================================================
|
|
5373
|
+
// Public API - Receive
|
|
5374
|
+
// ===========================================================================
|
|
5375
|
+
/**
|
|
5376
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5377
|
+
*
|
|
5378
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5379
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5380
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5381
|
+
*
|
|
5382
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5383
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5384
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5385
|
+
*
|
|
5386
|
+
* @param options - Optional receive options including finalization control
|
|
5387
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5388
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5389
|
+
*/
|
|
5390
|
+
async receive(options, callback) {
|
|
5391
|
+
this.ensureInitialized();
|
|
5392
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5393
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5394
|
+
}
|
|
5395
|
+
const opts = options ?? {};
|
|
5396
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5397
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5398
|
+
await this.load();
|
|
5399
|
+
const received = [];
|
|
5400
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5401
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5402
|
+
const transfer = {
|
|
5403
|
+
id: tokenId,
|
|
5404
|
+
senderPubkey: "",
|
|
5405
|
+
tokens: [token],
|
|
5406
|
+
receivedAt: Date.now()
|
|
5407
|
+
};
|
|
5408
|
+
received.push(transfer);
|
|
5409
|
+
if (callback) callback(transfer);
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
const result = { transfers: received };
|
|
5413
|
+
if (opts.finalize) {
|
|
5414
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5415
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5416
|
+
const startTime = Date.now();
|
|
5417
|
+
while (Date.now() - startTime < timeout) {
|
|
5418
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5419
|
+
result.finalization = resolution;
|
|
5420
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5421
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5422
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5423
|
+
);
|
|
5424
|
+
if (!stillUnconfirmed) break;
|
|
5425
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5426
|
+
await this.load();
|
|
5427
|
+
}
|
|
5428
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5429
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5430
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5431
|
+
);
|
|
5432
|
+
} else {
|
|
5433
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5434
|
+
}
|
|
5435
|
+
return result;
|
|
5436
|
+
}
|
|
5437
|
+
// ===========================================================================
|
|
5105
5438
|
// Public API - Balance & Tokens
|
|
5106
5439
|
// ===========================================================================
|
|
5107
5440
|
/**
|
|
@@ -5111,10 +5444,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5111
5444
|
this.priceProvider = provider;
|
|
5112
5445
|
}
|
|
5113
5446
|
/**
|
|
5114
|
-
*
|
|
5115
|
-
*
|
|
5447
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5448
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
5116
5449
|
*/
|
|
5117
|
-
async
|
|
5450
|
+
async waitForPendingOperations() {
|
|
5451
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5452
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5453
|
+
this.pendingBackgroundTasks = [];
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
/**
|
|
5457
|
+
* Get total portfolio value in USD.
|
|
5458
|
+
* Returns null if PriceProvider is not configured.
|
|
5459
|
+
*/
|
|
5460
|
+
async getFiatBalance() {
|
|
5118
5461
|
const assets = await this.getAssets();
|
|
5119
5462
|
if (!this.priceProvider) {
|
|
5120
5463
|
return null;
|
|
@@ -5130,19 +5473,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5130
5473
|
return hasAnyPrice ? total : null;
|
|
5131
5474
|
}
|
|
5132
5475
|
/**
|
|
5133
|
-
* Get
|
|
5134
|
-
*
|
|
5476
|
+
* Get token balances grouped by coin type.
|
|
5477
|
+
*
|
|
5478
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5479
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5480
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5481
|
+
*
|
|
5482
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5483
|
+
* for the async version with fiat pricing.
|
|
5484
|
+
*
|
|
5485
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5486
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5487
|
+
*/
|
|
5488
|
+
getBalance(coinId) {
|
|
5489
|
+
return this.aggregateTokens(coinId);
|
|
5490
|
+
}
|
|
5491
|
+
/**
|
|
5492
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5493
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
5135
5494
|
*/
|
|
5136
5495
|
async getAssets(coinId) {
|
|
5496
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5497
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5498
|
+
return rawAssets;
|
|
5499
|
+
}
|
|
5500
|
+
try {
|
|
5501
|
+
const registry = TokenRegistry.getInstance();
|
|
5502
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5503
|
+
for (const asset of rawAssets) {
|
|
5504
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5505
|
+
if (def?.name) {
|
|
5506
|
+
const existing = nameToCoins.get(def.name);
|
|
5507
|
+
if (existing) {
|
|
5508
|
+
existing.push(asset.coinId);
|
|
5509
|
+
} else {
|
|
5510
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
}
|
|
5514
|
+
if (nameToCoins.size > 0) {
|
|
5515
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5516
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5517
|
+
return rawAssets.map((raw) => {
|
|
5518
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5519
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5520
|
+
let fiatValueUsd = null;
|
|
5521
|
+
let fiatValueEur = null;
|
|
5522
|
+
if (price) {
|
|
5523
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5524
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5525
|
+
if (price.priceEur != null) {
|
|
5526
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5527
|
+
}
|
|
5528
|
+
}
|
|
5529
|
+
return {
|
|
5530
|
+
...raw,
|
|
5531
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5532
|
+
priceEur: price?.priceEur ?? null,
|
|
5533
|
+
change24h: price?.change24h ?? null,
|
|
5534
|
+
fiatValueUsd,
|
|
5535
|
+
fiatValueEur
|
|
5536
|
+
};
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
} catch (error) {
|
|
5540
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5541
|
+
}
|
|
5542
|
+
return rawAssets;
|
|
5543
|
+
}
|
|
5544
|
+
/**
|
|
5545
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5546
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5547
|
+
*/
|
|
5548
|
+
aggregateTokens(coinId) {
|
|
5137
5549
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
5138
5550
|
for (const token of this.tokens.values()) {
|
|
5139
|
-
if (token.status
|
|
5551
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
5140
5552
|
if (coinId && token.coinId !== coinId) continue;
|
|
5141
5553
|
const key = token.coinId;
|
|
5554
|
+
const amount = BigInt(token.amount);
|
|
5555
|
+
const isConfirmed = token.status === "confirmed";
|
|
5142
5556
|
const existing = assetsMap.get(key);
|
|
5143
5557
|
if (existing) {
|
|
5144
|
-
|
|
5145
|
-
|
|
5558
|
+
if (isConfirmed) {
|
|
5559
|
+
existing.confirmedAmount += amount;
|
|
5560
|
+
existing.confirmedTokenCount++;
|
|
5561
|
+
} else {
|
|
5562
|
+
existing.unconfirmedAmount += amount;
|
|
5563
|
+
existing.unconfirmedTokenCount++;
|
|
5564
|
+
}
|
|
5146
5565
|
} else {
|
|
5147
5566
|
assetsMap.set(key, {
|
|
5148
5567
|
coinId: token.coinId,
|
|
@@ -5150,78 +5569,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5150
5569
|
name: token.name,
|
|
5151
5570
|
decimals: token.decimals,
|
|
5152
5571
|
iconUrl: token.iconUrl,
|
|
5153
|
-
|
|
5154
|
-
|
|
5572
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5573
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5574
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5575
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
5155
5576
|
});
|
|
5156
5577
|
}
|
|
5157
5578
|
}
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
5161
|
-
try {
|
|
5162
|
-
const registry = TokenRegistry.getInstance();
|
|
5163
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5164
|
-
for (const asset of rawAssets) {
|
|
5165
|
-
const def = registry.getDefinition(asset.coinId);
|
|
5166
|
-
if (def?.name) {
|
|
5167
|
-
const existing = nameToCoins.get(def.name);
|
|
5168
|
-
if (existing) {
|
|
5169
|
-
existing.push(asset.coinId);
|
|
5170
|
-
} else {
|
|
5171
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
5172
|
-
}
|
|
5173
|
-
}
|
|
5174
|
-
}
|
|
5175
|
-
if (nameToCoins.size > 0) {
|
|
5176
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
5177
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5178
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
5179
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
5180
|
-
const price = prices.get(name);
|
|
5181
|
-
if (price) {
|
|
5182
|
-
for (const cid of coinIds) {
|
|
5183
|
-
priceMap.set(cid, {
|
|
5184
|
-
priceUsd: price.priceUsd,
|
|
5185
|
-
priceEur: price.priceEur,
|
|
5186
|
-
change24h: price.change24h
|
|
5187
|
-
});
|
|
5188
|
-
}
|
|
5189
|
-
}
|
|
5190
|
-
}
|
|
5191
|
-
}
|
|
5192
|
-
} catch (error) {
|
|
5193
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5194
|
-
}
|
|
5195
|
-
}
|
|
5196
|
-
return rawAssets.map((raw) => {
|
|
5197
|
-
const price = priceMap?.get(raw.coinId);
|
|
5198
|
-
let fiatValueUsd = null;
|
|
5199
|
-
let fiatValueEur = null;
|
|
5200
|
-
if (price) {
|
|
5201
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5202
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5203
|
-
if (price.priceEur != null) {
|
|
5204
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
5205
|
-
}
|
|
5206
|
-
}
|
|
5579
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5580
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
5207
5581
|
return {
|
|
5208
5582
|
coinId: raw.coinId,
|
|
5209
5583
|
symbol: raw.symbol,
|
|
5210
5584
|
name: raw.name,
|
|
5211
5585
|
decimals: raw.decimals,
|
|
5212
5586
|
iconUrl: raw.iconUrl,
|
|
5213
|
-
totalAmount
|
|
5214
|
-
tokenCount: raw.
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5587
|
+
totalAmount,
|
|
5588
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5589
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5590
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5591
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5592
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5593
|
+
priceUsd: null,
|
|
5594
|
+
priceEur: null,
|
|
5595
|
+
change24h: null,
|
|
5596
|
+
fiatValueUsd: null,
|
|
5597
|
+
fiatValueEur: null
|
|
5220
5598
|
};
|
|
5221
5599
|
});
|
|
5222
5600
|
}
|
|
5223
5601
|
/**
|
|
5224
|
-
* Get all tokens
|
|
5602
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5603
|
+
*
|
|
5604
|
+
* @param filter - Optional filter criteria.
|
|
5605
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5606
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5607
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
5225
5608
|
*/
|
|
5226
5609
|
getTokens(filter) {
|
|
5227
5610
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -5234,19 +5617,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5234
5617
|
return tokens;
|
|
5235
5618
|
}
|
|
5236
5619
|
/**
|
|
5237
|
-
* Get single token
|
|
5620
|
+
* Get a single token by its local ID.
|
|
5621
|
+
*
|
|
5622
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5623
|
+
* @returns The token, or `undefined` if not found.
|
|
5238
5624
|
*/
|
|
5239
5625
|
getToken(id) {
|
|
5240
5626
|
return this.tokens.get(id);
|
|
5241
5627
|
}
|
|
5242
5628
|
// ===========================================================================
|
|
5629
|
+
// Public API - Unconfirmed Token Resolution
|
|
5630
|
+
// ===========================================================================
|
|
5631
|
+
/**
|
|
5632
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5633
|
+
* their missing aggregator proofs.
|
|
5634
|
+
*
|
|
5635
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5636
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5637
|
+
*
|
|
5638
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5639
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5640
|
+
* marked `'invalid'`.
|
|
5641
|
+
*
|
|
5642
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5643
|
+
*
|
|
5644
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5645
|
+
*/
|
|
5646
|
+
async resolveUnconfirmed() {
|
|
5647
|
+
this.ensureInitialized();
|
|
5648
|
+
const result = {
|
|
5649
|
+
resolved: 0,
|
|
5650
|
+
stillPending: 0,
|
|
5651
|
+
failed: 0,
|
|
5652
|
+
details: []
|
|
5653
|
+
};
|
|
5654
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5655
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5656
|
+
if (!stClient || !trustBase) return result;
|
|
5657
|
+
const signingService = await this.createSigningService();
|
|
5658
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5659
|
+
if (token.status !== "submitted") continue;
|
|
5660
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5661
|
+
if (!pending2) {
|
|
5662
|
+
result.stillPending++;
|
|
5663
|
+
continue;
|
|
5664
|
+
}
|
|
5665
|
+
if (pending2.type === "v5_bundle") {
|
|
5666
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5667
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5668
|
+
if (progress === "resolved") result.resolved++;
|
|
5669
|
+
else if (progress === "failed") result.failed++;
|
|
5670
|
+
else result.stillPending++;
|
|
5671
|
+
}
|
|
5672
|
+
}
|
|
5673
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5674
|
+
await this.save();
|
|
5675
|
+
}
|
|
5676
|
+
return result;
|
|
5677
|
+
}
|
|
5678
|
+
// ===========================================================================
|
|
5679
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5680
|
+
// ===========================================================================
|
|
5681
|
+
/**
|
|
5682
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5683
|
+
*/
|
|
5684
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5685
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5686
|
+
pending2.attemptCount++;
|
|
5687
|
+
pending2.lastAttemptAt = Date.now();
|
|
5688
|
+
try {
|
|
5689
|
+
if (pending2.stage === "RECEIVED") {
|
|
5690
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5691
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5692
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5693
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5694
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5695
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5696
|
+
}
|
|
5697
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5698
|
+
this.updatePendingFinalization(token, pending2);
|
|
5699
|
+
}
|
|
5700
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5701
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5702
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5703
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5704
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5705
|
+
if (!proof) {
|
|
5706
|
+
this.updatePendingFinalization(token, pending2);
|
|
5707
|
+
return "pending";
|
|
5708
|
+
}
|
|
5709
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5710
|
+
pending2.stage = "MINT_PROVEN";
|
|
5711
|
+
this.updatePendingFinalization(token, pending2);
|
|
5712
|
+
}
|
|
5713
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5714
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5715
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5716
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5717
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5718
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5719
|
+
}
|
|
5720
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5721
|
+
this.updatePendingFinalization(token, pending2);
|
|
5722
|
+
}
|
|
5723
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5724
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5725
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5726
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5727
|
+
if (!proof) {
|
|
5728
|
+
this.updatePendingFinalization(token, pending2);
|
|
5729
|
+
return "pending";
|
|
5730
|
+
}
|
|
5731
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5732
|
+
const confirmedToken = {
|
|
5733
|
+
id: token.id,
|
|
5734
|
+
coinId: token.coinId,
|
|
5735
|
+
symbol: token.symbol,
|
|
5736
|
+
name: token.name,
|
|
5737
|
+
decimals: token.decimals,
|
|
5738
|
+
iconUrl: token.iconUrl,
|
|
5739
|
+
amount: token.amount,
|
|
5740
|
+
status: "confirmed",
|
|
5741
|
+
createdAt: token.createdAt,
|
|
5742
|
+
updatedAt: Date.now(),
|
|
5743
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5744
|
+
};
|
|
5745
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5746
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5747
|
+
await this.addToHistory({
|
|
5748
|
+
type: "RECEIVED",
|
|
5749
|
+
amount: confirmedToken.amount,
|
|
5750
|
+
coinId: confirmedToken.coinId,
|
|
5751
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5752
|
+
timestamp: Date.now(),
|
|
5753
|
+
senderPubkey: pending2.senderPubkey
|
|
5754
|
+
});
|
|
5755
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5756
|
+
return "resolved";
|
|
5757
|
+
}
|
|
5758
|
+
return "pending";
|
|
5759
|
+
} catch (error) {
|
|
5760
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5761
|
+
if (pending2.attemptCount > 50) {
|
|
5762
|
+
token.status = "invalid";
|
|
5763
|
+
token.updatedAt = Date.now();
|
|
5764
|
+
this.tokens.set(tokenId, token);
|
|
5765
|
+
return "failed";
|
|
5766
|
+
}
|
|
5767
|
+
this.updatePendingFinalization(token, pending2);
|
|
5768
|
+
return "pending";
|
|
5769
|
+
}
|
|
5770
|
+
}
|
|
5771
|
+
/**
|
|
5772
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5773
|
+
*/
|
|
5774
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5775
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5776
|
+
try {
|
|
5777
|
+
const proof = await Promise.race([
|
|
5778
|
+
waitInclusionProof5(trustBase, stClient, commitment),
|
|
5779
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5780
|
+
]);
|
|
5781
|
+
return proof;
|
|
5782
|
+
} catch {
|
|
5783
|
+
return null;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
/**
|
|
5787
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5788
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5789
|
+
*/
|
|
5790
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5791
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5792
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5793
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5794
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5795
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5796
|
+
const mintProof = InclusionProof.fromJSON(mintProofJson);
|
|
5797
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5798
|
+
const tokenType = new TokenType3(fromHex4(bundle.tokenTypeHex));
|
|
5799
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5800
|
+
const tokenJson = {
|
|
5801
|
+
version: "2.0",
|
|
5802
|
+
state: senderMintedStateJson,
|
|
5803
|
+
genesis: mintTransaction.toJSON(),
|
|
5804
|
+
transactions: [],
|
|
5805
|
+
nametags: []
|
|
5806
|
+
};
|
|
5807
|
+
const mintedToken = await SdkToken2.fromJSON(tokenJson);
|
|
5808
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5809
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5810
|
+
const transferProof = await waitInclusionProof5(trustBase, stClient, transferCommitment);
|
|
5811
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5812
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5813
|
+
const recipientPredicate = await UnmaskedPredicate5.create(
|
|
5814
|
+
mintData.tokenId,
|
|
5815
|
+
tokenType,
|
|
5816
|
+
signingService,
|
|
5817
|
+
HashAlgorithm5.SHA256,
|
|
5818
|
+
transferSalt
|
|
5819
|
+
);
|
|
5820
|
+
const recipientState = new TokenState5(recipientPredicate, null);
|
|
5821
|
+
let nametagTokens = [];
|
|
5822
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5823
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5824
|
+
if (bundle.nametagTokenJson) {
|
|
5825
|
+
try {
|
|
5826
|
+
const nametagToken = await SdkToken2.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5827
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5828
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5829
|
+
if (proxy.address === recipientAddressStr) {
|
|
5830
|
+
nametagTokens = [nametagToken];
|
|
5831
|
+
}
|
|
5832
|
+
} catch {
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5836
|
+
try {
|
|
5837
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
5838
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5839
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5840
|
+
if (proxy.address === recipientAddressStr) {
|
|
5841
|
+
nametagTokens = [nametagToken];
|
|
5842
|
+
}
|
|
5843
|
+
} catch {
|
|
5844
|
+
}
|
|
5845
|
+
}
|
|
5846
|
+
}
|
|
5847
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5848
|
+
}
|
|
5849
|
+
/**
|
|
5850
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5851
|
+
*/
|
|
5852
|
+
parsePendingFinalization(sdkData) {
|
|
5853
|
+
if (!sdkData) return null;
|
|
5854
|
+
try {
|
|
5855
|
+
const data = JSON.parse(sdkData);
|
|
5856
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5857
|
+
return data._pendingFinalization;
|
|
5858
|
+
}
|
|
5859
|
+
return null;
|
|
5860
|
+
} catch {
|
|
5861
|
+
return null;
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
/**
|
|
5865
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5866
|
+
* Creates a new token object since sdkData is readonly.
|
|
5867
|
+
*/
|
|
5868
|
+
updatePendingFinalization(token, pending2) {
|
|
5869
|
+
const updated = {
|
|
5870
|
+
id: token.id,
|
|
5871
|
+
coinId: token.coinId,
|
|
5872
|
+
symbol: token.symbol,
|
|
5873
|
+
name: token.name,
|
|
5874
|
+
decimals: token.decimals,
|
|
5875
|
+
iconUrl: token.iconUrl,
|
|
5876
|
+
amount: token.amount,
|
|
5877
|
+
status: token.status,
|
|
5878
|
+
createdAt: token.createdAt,
|
|
5879
|
+
updatedAt: Date.now(),
|
|
5880
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5881
|
+
};
|
|
5882
|
+
this.tokens.set(token.id, updated);
|
|
5883
|
+
}
|
|
5884
|
+
/**
|
|
5885
|
+
* Save pending V5 tokens to key-value storage.
|
|
5886
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5887
|
+
* so we persist them separately and restore on load().
|
|
5888
|
+
*/
|
|
5889
|
+
async savePendingV5Tokens() {
|
|
5890
|
+
const pendingTokens = [];
|
|
5891
|
+
for (const token of this.tokens.values()) {
|
|
5892
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5893
|
+
pendingTokens.push(token);
|
|
5894
|
+
}
|
|
5895
|
+
}
|
|
5896
|
+
if (pendingTokens.length > 0) {
|
|
5897
|
+
await this.deps.storage.set(
|
|
5898
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5899
|
+
JSON.stringify(pendingTokens)
|
|
5900
|
+
);
|
|
5901
|
+
} else {
|
|
5902
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5903
|
+
}
|
|
5904
|
+
}
|
|
5905
|
+
/**
|
|
5906
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5907
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5908
|
+
*/
|
|
5909
|
+
async loadPendingV5Tokens() {
|
|
5910
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5911
|
+
if (!data) return;
|
|
5912
|
+
try {
|
|
5913
|
+
const pendingTokens = JSON.parse(data);
|
|
5914
|
+
for (const token of pendingTokens) {
|
|
5915
|
+
if (!this.tokens.has(token.id)) {
|
|
5916
|
+
this.tokens.set(token.id, token);
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
if (pendingTokens.length > 0) {
|
|
5920
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5921
|
+
}
|
|
5922
|
+
} catch {
|
|
5923
|
+
}
|
|
5924
|
+
}
|
|
5925
|
+
// ===========================================================================
|
|
5243
5926
|
// Public API - Token Operations
|
|
5244
5927
|
// ===========================================================================
|
|
5245
5928
|
/**
|
|
5246
|
-
* Add a token
|
|
5247
|
-
*
|
|
5248
|
-
*
|
|
5249
|
-
*
|
|
5929
|
+
* Add a token to the wallet.
|
|
5930
|
+
*
|
|
5931
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5932
|
+
* Duplicate detection:
|
|
5933
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5934
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5935
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5936
|
+
* the old state is archived and replaced with the incoming one.
|
|
5937
|
+
*
|
|
5938
|
+
* @param token - The token to add.
|
|
5939
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5940
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
5250
5941
|
*/
|
|
5251
5942
|
async addToken(token, skipHistory = false) {
|
|
5252
5943
|
this.ensureInitialized();
|
|
@@ -5304,7 +5995,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5304
5995
|
});
|
|
5305
5996
|
}
|
|
5306
5997
|
await this.save();
|
|
5307
|
-
|
|
5998
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5999
|
+
await this.saveTokenToFileStorage(token);
|
|
6000
|
+
}
|
|
5308
6001
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
5309
6002
|
return true;
|
|
5310
6003
|
}
|
|
@@ -5361,6 +6054,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5361
6054
|
const data = fileData;
|
|
5362
6055
|
const tokenJson = data.token;
|
|
5363
6056
|
if (!tokenJson) continue;
|
|
6057
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
6058
|
+
continue;
|
|
6059
|
+
}
|
|
5364
6060
|
let sdkTokenId;
|
|
5365
6061
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5366
6062
|
const tokenObj = tokenJson;
|
|
@@ -5412,7 +6108,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5412
6108
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5413
6109
|
}
|
|
5414
6110
|
/**
|
|
5415
|
-
* Update an existing token
|
|
6111
|
+
* Update an existing token or add it if not found.
|
|
6112
|
+
*
|
|
6113
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
6114
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
6115
|
+
*
|
|
6116
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5416
6117
|
*/
|
|
5417
6118
|
async updateToken(token) {
|
|
5418
6119
|
this.ensureInitialized();
|
|
@@ -5436,7 +6137,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5436
6137
|
this.log(`Updated token ${token.id}`);
|
|
5437
6138
|
}
|
|
5438
6139
|
/**
|
|
5439
|
-
* Remove a token
|
|
6140
|
+
* Remove a token from the wallet.
|
|
6141
|
+
*
|
|
6142
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
6143
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
6144
|
+
* entry is created unless `skipHistory` is `true`.
|
|
6145
|
+
*
|
|
6146
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
6147
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
6148
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5440
6149
|
*/
|
|
5441
6150
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5442
6151
|
this.ensureInitialized();
|
|
@@ -5498,13 +6207,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5498
6207
|
// Public API - Tombstones
|
|
5499
6208
|
// ===========================================================================
|
|
5500
6209
|
/**
|
|
5501
|
-
* Get all
|
|
6210
|
+
* Get all tombstone entries.
|
|
6211
|
+
*
|
|
6212
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
6213
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
6214
|
+
*
|
|
6215
|
+
* @returns A shallow copy of the tombstone array.
|
|
5502
6216
|
*/
|
|
5503
6217
|
getTombstones() {
|
|
5504
6218
|
return [...this.tombstones];
|
|
5505
6219
|
}
|
|
5506
6220
|
/**
|
|
5507
|
-
* Check
|
|
6221
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
6222
|
+
*
|
|
6223
|
+
* @param tokenId - The genesis token ID.
|
|
6224
|
+
* @param stateHash - The state hash of the token version to check.
|
|
6225
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5508
6226
|
*/
|
|
5509
6227
|
isStateTombstoned(tokenId, stateHash) {
|
|
5510
6228
|
return this.tombstones.some(
|
|
@@ -5512,8 +6230,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5512
6230
|
);
|
|
5513
6231
|
}
|
|
5514
6232
|
/**
|
|
5515
|
-
* Merge remote
|
|
5516
|
-
*
|
|
6233
|
+
* Merge tombstones received from a remote sync source.
|
|
6234
|
+
*
|
|
6235
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
6236
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
6237
|
+
*
|
|
6238
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
6239
|
+
* @returns Number of local tokens that were removed.
|
|
5517
6240
|
*/
|
|
5518
6241
|
async mergeTombstones(remoteTombstones) {
|
|
5519
6242
|
this.ensureInitialized();
|
|
@@ -5549,7 +6272,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5549
6272
|
return removedCount;
|
|
5550
6273
|
}
|
|
5551
6274
|
/**
|
|
5552
|
-
*
|
|
6275
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
6276
|
+
*
|
|
6277
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5553
6278
|
*/
|
|
5554
6279
|
async pruneTombstones(maxAge) {
|
|
5555
6280
|
const originalCount = this.tombstones.length;
|
|
@@ -5563,20 +6288,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5563
6288
|
// Public API - Archives
|
|
5564
6289
|
// ===========================================================================
|
|
5565
6290
|
/**
|
|
5566
|
-
* Get archived tokens
|
|
6291
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
6292
|
+
*
|
|
6293
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
6294
|
+
* the genesis token ID.
|
|
6295
|
+
*
|
|
6296
|
+
* @returns A shallow copy of the archived token map.
|
|
5567
6297
|
*/
|
|
5568
6298
|
getArchivedTokens() {
|
|
5569
6299
|
return new Map(this.archivedTokens);
|
|
5570
6300
|
}
|
|
5571
6301
|
/**
|
|
5572
|
-
* Get best archived version of a token
|
|
6302
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
6303
|
+
*
|
|
6304
|
+
* Searches both archived and forked token maps and returns the version with
|
|
6305
|
+
* the highest number of committed transactions.
|
|
6306
|
+
*
|
|
6307
|
+
* @param tokenId - The genesis token ID to look up.
|
|
6308
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5573
6309
|
*/
|
|
5574
6310
|
getBestArchivedVersion(tokenId) {
|
|
5575
6311
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5576
6312
|
}
|
|
5577
6313
|
/**
|
|
5578
|
-
* Merge remote
|
|
5579
|
-
*
|
|
6314
|
+
* Merge archived tokens from a remote sync source.
|
|
6315
|
+
*
|
|
6316
|
+
* For each remote token:
|
|
6317
|
+
* - If missing locally, it is added.
|
|
6318
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
6319
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
6320
|
+
*
|
|
6321
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
6322
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5580
6323
|
*/
|
|
5581
6324
|
async mergeArchivedTokens(remoteArchived) {
|
|
5582
6325
|
let mergedCount = 0;
|
|
@@ -5599,7 +6342,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5599
6342
|
return mergedCount;
|
|
5600
6343
|
}
|
|
5601
6344
|
/**
|
|
5602
|
-
* Prune archived tokens
|
|
6345
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6346
|
+
*
|
|
6347
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6348
|
+
*
|
|
6349
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5603
6350
|
*/
|
|
5604
6351
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5605
6352
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5612,13 +6359,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5612
6359
|
// Public API - Forked Tokens
|
|
5613
6360
|
// ===========================================================================
|
|
5614
6361
|
/**
|
|
5615
|
-
* Get forked
|
|
6362
|
+
* Get all forked token versions.
|
|
6363
|
+
*
|
|
6364
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6365
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6366
|
+
*
|
|
6367
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5616
6368
|
*/
|
|
5617
6369
|
getForkedTokens() {
|
|
5618
6370
|
return new Map(this.forkedTokens);
|
|
5619
6371
|
}
|
|
5620
6372
|
/**
|
|
5621
|
-
* Store a forked token
|
|
6373
|
+
* Store a forked token version (alternative history).
|
|
6374
|
+
*
|
|
6375
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6376
|
+
*
|
|
6377
|
+
* @param tokenId - Genesis token ID.
|
|
6378
|
+
* @param stateHash - State hash of this forked version.
|
|
6379
|
+
* @param txfToken - The TXF token data to store.
|
|
5622
6380
|
*/
|
|
5623
6381
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5624
6382
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5628,8 +6386,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5628
6386
|
await this.save();
|
|
5629
6387
|
}
|
|
5630
6388
|
/**
|
|
5631
|
-
* Merge remote
|
|
5632
|
-
*
|
|
6389
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6390
|
+
*
|
|
6391
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6392
|
+
* @returns Number of new forked tokens added.
|
|
5633
6393
|
*/
|
|
5634
6394
|
async mergeForkedTokens(remoteForked) {
|
|
5635
6395
|
let addedCount = 0;
|
|
@@ -5645,7 +6405,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5645
6405
|
return addedCount;
|
|
5646
6406
|
}
|
|
5647
6407
|
/**
|
|
5648
|
-
* Prune forked tokens
|
|
6408
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6409
|
+
*
|
|
6410
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5649
6411
|
*/
|
|
5650
6412
|
async pruneForkedTokens(maxCount = 50) {
|
|
5651
6413
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5658,13 +6420,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5658
6420
|
// Public API - Transaction History
|
|
5659
6421
|
// ===========================================================================
|
|
5660
6422
|
/**
|
|
5661
|
-
* Get transaction history
|
|
6423
|
+
* Get the transaction history sorted newest-first.
|
|
6424
|
+
*
|
|
6425
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5662
6426
|
*/
|
|
5663
6427
|
getHistory() {
|
|
5664
6428
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5665
6429
|
}
|
|
5666
6430
|
/**
|
|
5667
|
-
*
|
|
6431
|
+
* Append an entry to the transaction history.
|
|
6432
|
+
*
|
|
6433
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6434
|
+
*
|
|
6435
|
+
* @param entry - History entry fields (without `id`).
|
|
5668
6436
|
*/
|
|
5669
6437
|
async addToHistory(entry) {
|
|
5670
6438
|
this.ensureInitialized();
|
|
@@ -5682,7 +6450,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5682
6450
|
// Public API - Nametag
|
|
5683
6451
|
// ===========================================================================
|
|
5684
6452
|
/**
|
|
5685
|
-
* Set nametag for current identity
|
|
6453
|
+
* Set the nametag data for the current identity.
|
|
6454
|
+
*
|
|
6455
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6456
|
+
*
|
|
6457
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5686
6458
|
*/
|
|
5687
6459
|
async setNametag(nametag) {
|
|
5688
6460
|
this.ensureInitialized();
|
|
@@ -5692,19 +6464,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5692
6464
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5693
6465
|
}
|
|
5694
6466
|
/**
|
|
5695
|
-
* Get nametag
|
|
6467
|
+
* Get the current nametag data.
|
|
6468
|
+
*
|
|
6469
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5696
6470
|
*/
|
|
5697
6471
|
getNametag() {
|
|
5698
6472
|
return this.nametag;
|
|
5699
6473
|
}
|
|
5700
6474
|
/**
|
|
5701
|
-
* Check
|
|
6475
|
+
* Check whether a nametag is currently set.
|
|
6476
|
+
*
|
|
6477
|
+
* @returns `true` if nametag data is present.
|
|
5702
6478
|
*/
|
|
5703
6479
|
hasNametag() {
|
|
5704
6480
|
return this.nametag !== null;
|
|
5705
6481
|
}
|
|
5706
6482
|
/**
|
|
5707
|
-
*
|
|
6483
|
+
* Remove the current nametag data from memory and storage.
|
|
5708
6484
|
*/
|
|
5709
6485
|
async clearNametag() {
|
|
5710
6486
|
this.ensureInitialized();
|
|
@@ -5798,9 +6574,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5798
6574
|
try {
|
|
5799
6575
|
const signingService = await this.createSigningService();
|
|
5800
6576
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5801
|
-
const { TokenType:
|
|
6577
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5802
6578
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5803
|
-
const tokenType = new
|
|
6579
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5804
6580
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5805
6581
|
tokenType,
|
|
5806
6582
|
signingService.algorithm,
|
|
@@ -5861,11 +6637,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5861
6637
|
// Public API - Sync & Validate
|
|
5862
6638
|
// ===========================================================================
|
|
5863
6639
|
/**
|
|
5864
|
-
* Sync with all token storage providers (IPFS,
|
|
5865
|
-
*
|
|
6640
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6641
|
+
*
|
|
6642
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6643
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6644
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6645
|
+
*
|
|
6646
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5866
6647
|
*/
|
|
5867
6648
|
async sync() {
|
|
5868
6649
|
this.ensureInitialized();
|
|
6650
|
+
if (this._syncInProgress) {
|
|
6651
|
+
return this._syncInProgress;
|
|
6652
|
+
}
|
|
6653
|
+
this._syncInProgress = this._doSync();
|
|
6654
|
+
try {
|
|
6655
|
+
return await this._syncInProgress;
|
|
6656
|
+
} finally {
|
|
6657
|
+
this._syncInProgress = null;
|
|
6658
|
+
}
|
|
6659
|
+
}
|
|
6660
|
+
async _doSync() {
|
|
5869
6661
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5870
6662
|
try {
|
|
5871
6663
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5903,6 +6695,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5903
6695
|
});
|
|
5904
6696
|
}
|
|
5905
6697
|
}
|
|
6698
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6699
|
+
await this.save();
|
|
6700
|
+
}
|
|
5906
6701
|
this.deps.emitEvent("sync:completed", {
|
|
5907
6702
|
source: "payments",
|
|
5908
6703
|
count: this.tokens.size
|
|
@@ -5916,6 +6711,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5916
6711
|
throw error;
|
|
5917
6712
|
}
|
|
5918
6713
|
}
|
|
6714
|
+
// ===========================================================================
|
|
6715
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6716
|
+
// ===========================================================================
|
|
6717
|
+
/**
|
|
6718
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6719
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6720
|
+
*/
|
|
6721
|
+
subscribeToStorageEvents() {
|
|
6722
|
+
this.unsubscribeStorageEvents();
|
|
6723
|
+
const providers = this.getTokenStorageProviders();
|
|
6724
|
+
for (const [providerId, provider] of providers) {
|
|
6725
|
+
if (provider.onEvent) {
|
|
6726
|
+
const unsub = provider.onEvent((event) => {
|
|
6727
|
+
if (event.type === "storage:remote-updated") {
|
|
6728
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6729
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6730
|
+
}
|
|
6731
|
+
});
|
|
6732
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6733
|
+
}
|
|
6734
|
+
}
|
|
6735
|
+
}
|
|
6736
|
+
/**
|
|
6737
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6738
|
+
*/
|
|
6739
|
+
unsubscribeStorageEvents() {
|
|
6740
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6741
|
+
unsub();
|
|
6742
|
+
}
|
|
6743
|
+
this.storageEventUnsubscribers = [];
|
|
6744
|
+
if (this.syncDebounceTimer) {
|
|
6745
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6746
|
+
this.syncDebounceTimer = null;
|
|
6747
|
+
}
|
|
6748
|
+
}
|
|
6749
|
+
/**
|
|
6750
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6751
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6752
|
+
*/
|
|
6753
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6754
|
+
if (this.syncDebounceTimer) {
|
|
6755
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6756
|
+
}
|
|
6757
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6758
|
+
this.syncDebounceTimer = null;
|
|
6759
|
+
this.sync().then((result) => {
|
|
6760
|
+
const data = eventData;
|
|
6761
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6762
|
+
providerId,
|
|
6763
|
+
name: data?.name ?? "",
|
|
6764
|
+
sequence: data?.sequence ?? 0,
|
|
6765
|
+
cid: data?.cid ?? "",
|
|
6766
|
+
added: result.added,
|
|
6767
|
+
removed: result.removed
|
|
6768
|
+
});
|
|
6769
|
+
}).catch((err) => {
|
|
6770
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6771
|
+
});
|
|
6772
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6773
|
+
}
|
|
5919
6774
|
/**
|
|
5920
6775
|
* Get all active token storage providers
|
|
5921
6776
|
*/
|
|
@@ -5931,15 +6786,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5931
6786
|
return /* @__PURE__ */ new Map();
|
|
5932
6787
|
}
|
|
5933
6788
|
/**
|
|
5934
|
-
*
|
|
6789
|
+
* Replace the set of token storage providers at runtime.
|
|
6790
|
+
*
|
|
6791
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6792
|
+
*
|
|
6793
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5935
6794
|
*/
|
|
5936
6795
|
updateTokenStorageProviders(providers) {
|
|
5937
6796
|
if (this.deps) {
|
|
5938
6797
|
this.deps.tokenStorageProviders = providers;
|
|
6798
|
+
this.subscribeToStorageEvents();
|
|
5939
6799
|
}
|
|
5940
6800
|
}
|
|
5941
6801
|
/**
|
|
5942
|
-
* Validate tokens
|
|
6802
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6803
|
+
*
|
|
6804
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6805
|
+
*
|
|
6806
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5943
6807
|
*/
|
|
5944
6808
|
async validate() {
|
|
5945
6809
|
this.ensureInitialized();
|
|
@@ -5960,7 +6824,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5960
6824
|
return { valid, invalid };
|
|
5961
6825
|
}
|
|
5962
6826
|
/**
|
|
5963
|
-
* Get pending transfers
|
|
6827
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6828
|
+
*
|
|
6829
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
5964
6830
|
*/
|
|
5965
6831
|
getPendingTransfers() {
|
|
5966
6832
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -6024,9 +6890,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6024
6890
|
*/
|
|
6025
6891
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
6026
6892
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
6027
|
-
const { TokenType:
|
|
6893
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
6028
6894
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
6029
|
-
const tokenType = new
|
|
6895
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
6030
6896
|
const pubkeyBytes = new Uint8Array(
|
|
6031
6897
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
6032
6898
|
);
|
|
@@ -6238,7 +7104,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6238
7104
|
this.deps.emitEvent("transfer:confirmed", {
|
|
6239
7105
|
id: crypto.randomUUID(),
|
|
6240
7106
|
status: "completed",
|
|
6241
|
-
tokens: [finalizedToken]
|
|
7107
|
+
tokens: [finalizedToken],
|
|
7108
|
+
tokenTransfers: []
|
|
6242
7109
|
});
|
|
6243
7110
|
await this.addToHistory({
|
|
6244
7111
|
type: "RECEIVED",
|
|
@@ -6261,14 +7128,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6261
7128
|
async handleIncomingTransfer(transfer) {
|
|
6262
7129
|
try {
|
|
6263
7130
|
const payload = transfer.payload;
|
|
7131
|
+
let instantBundle = null;
|
|
6264
7132
|
if (isInstantSplitBundle(payload)) {
|
|
7133
|
+
instantBundle = payload;
|
|
7134
|
+
} else if (payload.token) {
|
|
7135
|
+
try {
|
|
7136
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
7137
|
+
if (isInstantSplitBundle(inner)) {
|
|
7138
|
+
instantBundle = inner;
|
|
7139
|
+
}
|
|
7140
|
+
} catch {
|
|
7141
|
+
}
|
|
7142
|
+
}
|
|
7143
|
+
if (instantBundle) {
|
|
6265
7144
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
6266
7145
|
try {
|
|
6267
7146
|
if (!this.nametag) {
|
|
6268
7147
|
await this.loadNametagFromFileStorage();
|
|
6269
7148
|
}
|
|
6270
7149
|
const result = await this.processInstantSplitBundle(
|
|
6271
|
-
|
|
7150
|
+
instantBundle,
|
|
6272
7151
|
transfer.senderTransportPubkey
|
|
6273
7152
|
);
|
|
6274
7153
|
if (result.success) {
|
|
@@ -6281,6 +7160,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6281
7160
|
}
|
|
6282
7161
|
return;
|
|
6283
7162
|
}
|
|
7163
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
7164
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
7165
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
7166
|
+
return;
|
|
7167
|
+
}
|
|
6284
7168
|
let tokenData;
|
|
6285
7169
|
let finalizedSdkToken = null;
|
|
6286
7170
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6436,6 +7320,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6436
7320
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6437
7321
|
}
|
|
6438
7322
|
}
|
|
7323
|
+
await this.savePendingV5Tokens();
|
|
6439
7324
|
}
|
|
6440
7325
|
async saveToOutbox(transfer, recipient) {
|
|
6441
7326
|
const outbox = await this.loadOutbox();
|
|
@@ -6453,8 +7338,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6453
7338
|
}
|
|
6454
7339
|
async createStorageData() {
|
|
6455
7340
|
return await buildTxfStorageData(
|
|
6456
|
-
|
|
6457
|
-
// Empty - active tokens stored as token-xxx files
|
|
7341
|
+
Array.from(this.tokens.values()),
|
|
6458
7342
|
{
|
|
6459
7343
|
version: 1,
|
|
6460
7344
|
address: this.deps.identity.l1Address,
|
|
@@ -6639,7 +7523,7 @@ function createPaymentsModule(config) {
|
|
|
6639
7523
|
// modules/payments/TokenRecoveryService.ts
|
|
6640
7524
|
import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
6641
7525
|
import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
6642
|
-
import { TokenType as
|
|
7526
|
+
import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
6643
7527
|
import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
6644
7528
|
import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
6645
7529
|
import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
@@ -7703,15 +8587,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7703
8587
|
|
|
7704
8588
|
// core/Sphere.ts
|
|
7705
8589
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
7706
|
-
import { TokenType as
|
|
8590
|
+
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
7707
8591
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
7708
8592
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
8593
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
8594
|
+
function isValidNametag(nametag) {
|
|
8595
|
+
if (isPhoneNumber(nametag)) return true;
|
|
8596
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8597
|
+
}
|
|
7709
8598
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7710
8599
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7711
8600
|
const secret = Buffer.from(privateKey, "hex");
|
|
7712
8601
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
7713
8602
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7714
|
-
const tokenType = new
|
|
8603
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
7715
8604
|
const predicateRef = UnmaskedPredicateReference3.create(
|
|
7716
8605
|
tokenType,
|
|
7717
8606
|
signingService.algorithm,
|
|
@@ -7877,8 +8766,8 @@ var Sphere = class _Sphere {
|
|
|
7877
8766
|
if (options.nametag) {
|
|
7878
8767
|
await sphere.registerNametag(options.nametag);
|
|
7879
8768
|
} else {
|
|
7880
|
-
await sphere.syncIdentityWithTransport();
|
|
7881
8769
|
await sphere.recoverNametagFromTransport();
|
|
8770
|
+
await sphere.syncIdentityWithTransport();
|
|
7882
8771
|
}
|
|
7883
8772
|
return sphere;
|
|
7884
8773
|
}
|
|
@@ -7925,9 +8814,14 @@ var Sphere = class _Sphere {
|
|
|
7925
8814
|
if (!options.mnemonic && !options.masterKey) {
|
|
7926
8815
|
throw new Error("Either mnemonic or masterKey is required");
|
|
7927
8816
|
}
|
|
8817
|
+
console.log("[Sphere.import] Starting import...");
|
|
8818
|
+
console.log("[Sphere.import] Clearing existing wallet data...");
|
|
7928
8819
|
await _Sphere.clear({ storage: options.storage, tokenStorage: options.tokenStorage });
|
|
8820
|
+
console.log("[Sphere.import] Clear done");
|
|
7929
8821
|
if (!options.storage.isConnected()) {
|
|
8822
|
+
console.log("[Sphere.import] Reconnecting storage...");
|
|
7930
8823
|
await options.storage.connect();
|
|
8824
|
+
console.log("[Sphere.import] Storage reconnected");
|
|
7931
8825
|
}
|
|
7932
8826
|
const sphere = new _Sphere(
|
|
7933
8827
|
options.storage,
|
|
@@ -7941,9 +8835,12 @@ var Sphere = class _Sphere {
|
|
|
7941
8835
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
7942
8836
|
throw new Error("Invalid mnemonic");
|
|
7943
8837
|
}
|
|
8838
|
+
console.log("[Sphere.import] Storing mnemonic...");
|
|
7944
8839
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath, options.basePath);
|
|
8840
|
+
console.log("[Sphere.import] Initializing identity from mnemonic...");
|
|
7945
8841
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
7946
8842
|
} else if (options.masterKey) {
|
|
8843
|
+
console.log("[Sphere.import] Storing master key...");
|
|
7947
8844
|
await sphere.storeMasterKey(
|
|
7948
8845
|
options.masterKey,
|
|
7949
8846
|
options.chainCode,
|
|
@@ -7951,24 +8848,43 @@ var Sphere = class _Sphere {
|
|
|
7951
8848
|
options.basePath,
|
|
7952
8849
|
options.derivationMode
|
|
7953
8850
|
);
|
|
8851
|
+
console.log("[Sphere.import] Initializing identity from master key...");
|
|
7954
8852
|
await sphere.initializeIdentityFromMasterKey(
|
|
7955
8853
|
options.masterKey,
|
|
7956
8854
|
options.chainCode,
|
|
7957
8855
|
options.derivationPath
|
|
7958
8856
|
);
|
|
7959
8857
|
}
|
|
8858
|
+
console.log("[Sphere.import] Initializing providers...");
|
|
7960
8859
|
await sphere.initializeProviders();
|
|
8860
|
+
console.log("[Sphere.import] Providers initialized. Initializing modules...");
|
|
7961
8861
|
await sphere.initializeModules();
|
|
8862
|
+
console.log("[Sphere.import] Modules initialized");
|
|
7962
8863
|
if (!options.nametag) {
|
|
8864
|
+
console.log("[Sphere.import] Recovering nametag from transport...");
|
|
7963
8865
|
await sphere.recoverNametagFromTransport();
|
|
8866
|
+
console.log("[Sphere.import] Nametag recovery done");
|
|
8867
|
+
await sphere.syncIdentityWithTransport();
|
|
7964
8868
|
}
|
|
8869
|
+
console.log("[Sphere.import] Finalizing wallet creation...");
|
|
7965
8870
|
await sphere.finalizeWalletCreation();
|
|
7966
8871
|
sphere._initialized = true;
|
|
7967
8872
|
_Sphere.instance = sphere;
|
|
8873
|
+
console.log("[Sphere.import] Tracking address 0...");
|
|
7968
8874
|
await sphere.ensureAddressTracked(0);
|
|
7969
8875
|
if (options.nametag) {
|
|
8876
|
+
console.log("[Sphere.import] Registering nametag...");
|
|
7970
8877
|
await sphere.registerNametag(options.nametag);
|
|
7971
8878
|
}
|
|
8879
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8880
|
+
try {
|
|
8881
|
+
const syncResult = await sphere._payments.sync();
|
|
8882
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8883
|
+
} catch (err) {
|
|
8884
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8885
|
+
}
|
|
8886
|
+
}
|
|
8887
|
+
console.log("[Sphere.import] Import complete");
|
|
7972
8888
|
return sphere;
|
|
7973
8889
|
}
|
|
7974
8890
|
/**
|
|
@@ -7993,6 +8909,10 @@ var Sphere = class _Sphere {
|
|
|
7993
8909
|
static async clear(storageOrOptions) {
|
|
7994
8910
|
const storage = "get" in storageOrOptions ? storageOrOptions : storageOrOptions.storage;
|
|
7995
8911
|
const tokenStorage = "get" in storageOrOptions ? void 0 : storageOrOptions.tokenStorage;
|
|
8912
|
+
if (!storage.isConnected()) {
|
|
8913
|
+
await storage.connect();
|
|
8914
|
+
}
|
|
8915
|
+
console.log("[Sphere.clear] Removing storage keys...");
|
|
7996
8916
|
await storage.remove(STORAGE_KEYS_GLOBAL.MNEMONIC);
|
|
7997
8917
|
await storage.remove(STORAGE_KEYS_GLOBAL.MASTER_KEY);
|
|
7998
8918
|
await storage.remove(STORAGE_KEYS_GLOBAL.CHAIN_CODE);
|
|
@@ -8005,12 +8925,30 @@ var Sphere = class _Sphere {
|
|
|
8005
8925
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
8006
8926
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
8007
8927
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
8928
|
+
console.log("[Sphere.clear] Storage keys removed");
|
|
8008
8929
|
if (tokenStorage?.clear) {
|
|
8009
|
-
|
|
8930
|
+
console.log("[Sphere.clear] Clearing token storage...");
|
|
8931
|
+
try {
|
|
8932
|
+
await Promise.race([
|
|
8933
|
+
tokenStorage.clear(),
|
|
8934
|
+
new Promise(
|
|
8935
|
+
(_, reject) => setTimeout(() => reject(new Error("tokenStorage.clear() timed out after 2s")), 2e3)
|
|
8936
|
+
)
|
|
8937
|
+
]);
|
|
8938
|
+
console.log("[Sphere.clear] Token storage cleared");
|
|
8939
|
+
} catch (err) {
|
|
8940
|
+
console.warn("[Sphere.clear] Token storage clear failed/timed out:", err);
|
|
8941
|
+
}
|
|
8010
8942
|
}
|
|
8943
|
+
console.log("[Sphere.clear] Destroying vesting classifier...");
|
|
8011
8944
|
await vestingClassifier.destroy();
|
|
8945
|
+
console.log("[Sphere.clear] Vesting classifier destroyed");
|
|
8012
8946
|
if (_Sphere.instance) {
|
|
8947
|
+
console.log("[Sphere.clear] Destroying Sphere instance...");
|
|
8013
8948
|
await _Sphere.instance.destroy();
|
|
8949
|
+
console.log("[Sphere.clear] Sphere instance destroyed");
|
|
8950
|
+
} else {
|
|
8951
|
+
console.log("[Sphere.clear] No Sphere instance to destroy");
|
|
8014
8952
|
}
|
|
8015
8953
|
}
|
|
8016
8954
|
/**
|
|
@@ -8391,7 +9329,8 @@ var Sphere = class _Sphere {
|
|
|
8391
9329
|
storage: options.storage,
|
|
8392
9330
|
transport: options.transport,
|
|
8393
9331
|
oracle: options.oracle,
|
|
8394
|
-
tokenStorage: options.tokenStorage
|
|
9332
|
+
tokenStorage: options.tokenStorage,
|
|
9333
|
+
l1: options.l1
|
|
8395
9334
|
});
|
|
8396
9335
|
return { success: true, mnemonic };
|
|
8397
9336
|
}
|
|
@@ -8404,7 +9343,8 @@ var Sphere = class _Sphere {
|
|
|
8404
9343
|
storage: options.storage,
|
|
8405
9344
|
transport: options.transport,
|
|
8406
9345
|
oracle: options.oracle,
|
|
8407
|
-
tokenStorage: options.tokenStorage
|
|
9346
|
+
tokenStorage: options.tokenStorage,
|
|
9347
|
+
l1: options.l1
|
|
8408
9348
|
});
|
|
8409
9349
|
return { success: true };
|
|
8410
9350
|
}
|
|
@@ -8463,7 +9403,8 @@ var Sphere = class _Sphere {
|
|
|
8463
9403
|
transport: options.transport,
|
|
8464
9404
|
oracle: options.oracle,
|
|
8465
9405
|
tokenStorage: options.tokenStorage,
|
|
8466
|
-
nametag: options.nametag
|
|
9406
|
+
nametag: options.nametag,
|
|
9407
|
+
l1: options.l1
|
|
8467
9408
|
});
|
|
8468
9409
|
return { success: true, sphere, mnemonic };
|
|
8469
9410
|
}
|
|
@@ -8492,7 +9433,8 @@ var Sphere = class _Sphere {
|
|
|
8492
9433
|
transport: options.transport,
|
|
8493
9434
|
oracle: options.oracle,
|
|
8494
9435
|
tokenStorage: options.tokenStorage,
|
|
8495
|
-
nametag: options.nametag
|
|
9436
|
+
nametag: options.nametag,
|
|
9437
|
+
l1: options.l1
|
|
8496
9438
|
});
|
|
8497
9439
|
return { success: true, sphere };
|
|
8498
9440
|
}
|
|
@@ -8523,7 +9465,8 @@ var Sphere = class _Sphere {
|
|
|
8523
9465
|
transport: options.transport,
|
|
8524
9466
|
oracle: options.oracle,
|
|
8525
9467
|
tokenStorage: options.tokenStorage,
|
|
8526
|
-
nametag: options.nametag
|
|
9468
|
+
nametag: options.nametag,
|
|
9469
|
+
l1: options.l1
|
|
8527
9470
|
});
|
|
8528
9471
|
return { success: true, sphere };
|
|
8529
9472
|
}
|
|
@@ -8542,7 +9485,8 @@ var Sphere = class _Sphere {
|
|
|
8542
9485
|
storage: options.storage,
|
|
8543
9486
|
transport: options.transport,
|
|
8544
9487
|
oracle: options.oracle,
|
|
8545
|
-
tokenStorage: options.tokenStorage
|
|
9488
|
+
tokenStorage: options.tokenStorage,
|
|
9489
|
+
l1: options.l1
|
|
8546
9490
|
});
|
|
8547
9491
|
if (result.success) {
|
|
8548
9492
|
const sphere2 = _Sphere.getInstance();
|
|
@@ -8591,7 +9535,8 @@ var Sphere = class _Sphere {
|
|
|
8591
9535
|
transport: options.transport,
|
|
8592
9536
|
oracle: options.oracle,
|
|
8593
9537
|
tokenStorage: options.tokenStorage,
|
|
8594
|
-
nametag: options.nametag
|
|
9538
|
+
nametag: options.nametag,
|
|
9539
|
+
l1: options.l1
|
|
8595
9540
|
});
|
|
8596
9541
|
return { success: true, sphere: sphere2, mnemonic };
|
|
8597
9542
|
}
|
|
@@ -8604,7 +9549,8 @@ var Sphere = class _Sphere {
|
|
|
8604
9549
|
transport: options.transport,
|
|
8605
9550
|
oracle: options.oracle,
|
|
8606
9551
|
tokenStorage: options.tokenStorage,
|
|
8607
|
-
nametag: options.nametag
|
|
9552
|
+
nametag: options.nametag,
|
|
9553
|
+
l1: options.l1
|
|
8608
9554
|
});
|
|
8609
9555
|
return { success: true, sphere };
|
|
8610
9556
|
}
|
|
@@ -8808,9 +9754,9 @@ var Sphere = class _Sphere {
|
|
|
8808
9754
|
if (index < 0) {
|
|
8809
9755
|
throw new Error("Address index must be non-negative");
|
|
8810
9756
|
}
|
|
8811
|
-
const newNametag = options?.nametag
|
|
8812
|
-
if (newNametag && !
|
|
8813
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9757
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9758
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9759
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8814
9760
|
}
|
|
8815
9761
|
const addressInfo = this.deriveAddress(index, false);
|
|
8816
9762
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -9194,9 +10140,9 @@ var Sphere = class _Sphere {
|
|
|
9194
10140
|
*/
|
|
9195
10141
|
async registerNametag(nametag) {
|
|
9196
10142
|
this.ensureReady();
|
|
9197
|
-
const cleanNametag =
|
|
9198
|
-
if (!
|
|
9199
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
10143
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
10144
|
+
if (!isValidNametag(cleanNametag)) {
|
|
10145
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
9200
10146
|
}
|
|
9201
10147
|
if (this._identity?.nametag) {
|
|
9202
10148
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9467,46 +10413,49 @@ var Sphere = class _Sphere {
|
|
|
9467
10413
|
if (this._identity?.nametag) {
|
|
9468
10414
|
return;
|
|
9469
10415
|
}
|
|
9470
|
-
|
|
10416
|
+
let recoveredNametag = null;
|
|
10417
|
+
if (this._transport.recoverNametag) {
|
|
10418
|
+
try {
|
|
10419
|
+
recoveredNametag = await this._transport.recoverNametag();
|
|
10420
|
+
} catch {
|
|
10421
|
+
}
|
|
10422
|
+
}
|
|
10423
|
+
if (!recoveredNametag && this._transport.resolveAddressInfo && this._identity?.l1Address) {
|
|
10424
|
+
try {
|
|
10425
|
+
const info = await this._transport.resolveAddressInfo(this._identity.l1Address);
|
|
10426
|
+
if (info?.nametag) {
|
|
10427
|
+
recoveredNametag = info.nametag;
|
|
10428
|
+
}
|
|
10429
|
+
} catch {
|
|
10430
|
+
}
|
|
10431
|
+
}
|
|
10432
|
+
if (!recoveredNametag) {
|
|
9471
10433
|
return;
|
|
9472
10434
|
}
|
|
9473
10435
|
try {
|
|
9474
|
-
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
nametags = /* @__PURE__ */ new Map();
|
|
9484
|
-
this._addressNametags.set(entry.addressId, nametags);
|
|
9485
|
-
}
|
|
9486
|
-
const nextIndex = nametags.size;
|
|
9487
|
-
nametags.set(nextIndex, recoveredNametag);
|
|
9488
|
-
await this.persistAddressNametags();
|
|
9489
|
-
if (this._transport.publishIdentityBinding) {
|
|
9490
|
-
await this._transport.publishIdentityBinding(
|
|
9491
|
-
this._identity.chainPubkey,
|
|
9492
|
-
this._identity.l1Address,
|
|
9493
|
-
this._identity.directAddress || "",
|
|
9494
|
-
recoveredNametag
|
|
9495
|
-
);
|
|
9496
|
-
}
|
|
9497
|
-
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
10436
|
+
if (this._identity) {
|
|
10437
|
+
this._identity.nametag = recoveredNametag;
|
|
10438
|
+
await this._updateCachedProxyAddress();
|
|
10439
|
+
}
|
|
10440
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
10441
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
10442
|
+
if (!nametags) {
|
|
10443
|
+
nametags = /* @__PURE__ */ new Map();
|
|
10444
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
9498
10445
|
}
|
|
10446
|
+
const nextIndex = nametags.size;
|
|
10447
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
10448
|
+
await this.persistAddressNametags();
|
|
10449
|
+
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
9499
10450
|
} catch {
|
|
9500
10451
|
}
|
|
9501
10452
|
}
|
|
9502
10453
|
/**
|
|
9503
|
-
*
|
|
10454
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9504
10455
|
*/
|
|
9505
|
-
|
|
9506
|
-
const
|
|
9507
|
-
|
|
9508
|
-
);
|
|
9509
|
-
return pattern.test(nametag);
|
|
10456
|
+
cleanNametag(raw) {
|
|
10457
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10458
|
+
return normalizeNametag2(stripped);
|
|
9510
10459
|
}
|
|
9511
10460
|
// ===========================================================================
|
|
9512
10461
|
// Public Methods - Lifecycle
|
|
@@ -9704,8 +10653,12 @@ var Sphere = class _Sphere {
|
|
|
9704
10653
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9705
10654
|
provider.setIdentity(this._identity);
|
|
9706
10655
|
}
|
|
9707
|
-
|
|
9708
|
-
|
|
10656
|
+
if (!this._storage.isConnected()) {
|
|
10657
|
+
await this._storage.connect();
|
|
10658
|
+
}
|
|
10659
|
+
if (!this._transport.isConnected()) {
|
|
10660
|
+
await this._transport.connect();
|
|
10661
|
+
}
|
|
9709
10662
|
await this._oracle.initialize();
|
|
9710
10663
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9711
10664
|
await provider.initialize();
|
|
@@ -10202,6 +11155,14 @@ function createTokenValidator(options) {
|
|
|
10202
11155
|
return new TokenValidator(options);
|
|
10203
11156
|
}
|
|
10204
11157
|
|
|
11158
|
+
// index.ts
|
|
11159
|
+
import {
|
|
11160
|
+
normalizeNametag as normalizeNametag3,
|
|
11161
|
+
isPhoneNumber as isPhoneNumber2,
|
|
11162
|
+
hashNametag,
|
|
11163
|
+
areSameNametag
|
|
11164
|
+
} from "@unicitylabs/nostr-js-sdk";
|
|
11165
|
+
|
|
10205
11166
|
// price/CoinGeckoPriceProvider.ts
|
|
10206
11167
|
var CoinGeckoPriceProvider = class {
|
|
10207
11168
|
platform = "coingecko";
|
|
@@ -10337,6 +11298,7 @@ export {
|
|
|
10337
11298
|
TokenRegistry,
|
|
10338
11299
|
TokenValidator,
|
|
10339
11300
|
archivedKeyFromTokenId,
|
|
11301
|
+
areSameNametag,
|
|
10340
11302
|
base58Decode,
|
|
10341
11303
|
base58Encode2 as base58Encode,
|
|
10342
11304
|
buildTxfStorageData,
|
|
@@ -10384,6 +11346,7 @@ export {
|
|
|
10384
11346
|
hasUncommittedTransactions,
|
|
10385
11347
|
hasValidTxfData,
|
|
10386
11348
|
hash160,
|
|
11349
|
+
hashNametag,
|
|
10387
11350
|
hexToBytes,
|
|
10388
11351
|
identityFromMnemonicSync,
|
|
10389
11352
|
initSphere,
|
|
@@ -10395,10 +11358,12 @@ export {
|
|
|
10395
11358
|
isKnownToken,
|
|
10396
11359
|
isPaymentSessionTerminal,
|
|
10397
11360
|
isPaymentSessionTimedOut,
|
|
11361
|
+
isPhoneNumber2 as isPhoneNumber,
|
|
10398
11362
|
isSQLiteDatabase,
|
|
10399
11363
|
isTextWalletEncrypted,
|
|
10400
11364
|
isTokenKey,
|
|
10401
11365
|
isValidBech32,
|
|
11366
|
+
isValidNametag,
|
|
10402
11367
|
isValidPrivateKey,
|
|
10403
11368
|
isValidTokenId,
|
|
10404
11369
|
isWalletDatEncrypted,
|
|
@@ -10406,6 +11371,7 @@ export {
|
|
|
10406
11371
|
keyFromTokenId,
|
|
10407
11372
|
loadSphere,
|
|
10408
11373
|
mnemonicToSeedSync2 as mnemonicToSeedSync,
|
|
11374
|
+
normalizeNametag3 as normalizeNametag,
|
|
10409
11375
|
normalizeSdkTokenToStorage,
|
|
10410
11376
|
objectToTxf,
|
|
10411
11377
|
parseAndDecryptWalletDat,
|