@unicitylabs/sphere-sdk 0.2.2 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -79
- package/dist/core/index.cjs +1220 -275
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +422 -221
- package/dist/core/index.d.ts +422 -221
- package/dist/core/index.js +1219 -275
- package/dist/core/index.js.map +1 -1
- package/dist/impl/browser/index.cjs +2077 -14
- package/dist/impl/browser/index.cjs.map +1 -1
- package/dist/impl/browser/index.js +2077 -14
- package/dist/impl/browser/index.js.map +1 -1
- package/dist/impl/browser/ipfs.cjs +1877 -513
- package/dist/impl/browser/ipfs.cjs.map +1 -1
- package/dist/impl/browser/ipfs.js +1877 -513
- package/dist/impl/browser/ipfs.js.map +1 -1
- package/dist/impl/nodejs/index.cjs +2222 -172
- package/dist/impl/nodejs/index.cjs.map +1 -1
- package/dist/impl/nodejs/index.d.cts +84 -3
- package/dist/impl/nodejs/index.d.ts +84 -3
- package/dist/impl/nodejs/index.js +2222 -172
- package/dist/impl/nodejs/index.js.map +1 -1
- package/dist/index.cjs +1231 -265
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +440 -67
- package/dist/index.d.ts +440 -67
- package/dist/index.js +1231 -265
- package/dist/index.js.map +1 -1
- package/package.json +25 -5
package/dist/core/index.js
CHANGED
|
@@ -1461,7 +1461,7 @@ var L1PaymentsModule = class {
|
|
|
1461
1461
|
_transport;
|
|
1462
1462
|
constructor(config) {
|
|
1463
1463
|
this._config = {
|
|
1464
|
-
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.
|
|
1464
|
+
electrumUrl: config?.electrumUrl ?? "wss://fulcrum.unicity.network:50004",
|
|
1465
1465
|
network: config?.network ?? "mainnet",
|
|
1466
1466
|
defaultFeeRate: config?.defaultFeeRate ?? 10,
|
|
1467
1467
|
enableVesting: config?.enableVesting ?? true
|
|
@@ -1493,10 +1493,17 @@ var L1PaymentsModule = class {
|
|
|
1493
1493
|
});
|
|
1494
1494
|
}
|
|
1495
1495
|
}
|
|
1496
|
-
|
|
1496
|
+
this._initialized = true;
|
|
1497
|
+
}
|
|
1498
|
+
/**
|
|
1499
|
+
* Ensure the Fulcrum WebSocket is connected. Called lazily before any
|
|
1500
|
+
* operation that needs the network. If the singleton is already connected
|
|
1501
|
+
* (e.g. by the address scanner), this is a no-op.
|
|
1502
|
+
*/
|
|
1503
|
+
async ensureConnected() {
|
|
1504
|
+
if (!isWebSocketConnected() && this._config.electrumUrl) {
|
|
1497
1505
|
await connect(this._config.electrumUrl);
|
|
1498
1506
|
}
|
|
1499
|
-
this._initialized = true;
|
|
1500
1507
|
}
|
|
1501
1508
|
destroy() {
|
|
1502
1509
|
if (isWebSocketConnected()) {
|
|
@@ -1554,6 +1561,7 @@ var L1PaymentsModule = class {
|
|
|
1554
1561
|
}
|
|
1555
1562
|
async send(request) {
|
|
1556
1563
|
this.ensureInitialized();
|
|
1564
|
+
await this.ensureConnected();
|
|
1557
1565
|
if (!this._wallet || !this._identity) {
|
|
1558
1566
|
return { success: false, error: "No wallet available" };
|
|
1559
1567
|
}
|
|
@@ -1588,6 +1596,7 @@ var L1PaymentsModule = class {
|
|
|
1588
1596
|
}
|
|
1589
1597
|
async getBalance() {
|
|
1590
1598
|
this.ensureInitialized();
|
|
1599
|
+
await this.ensureConnected();
|
|
1591
1600
|
const addresses = this._getWatchedAddresses();
|
|
1592
1601
|
let totalAlpha = 0;
|
|
1593
1602
|
let vestedSats = BigInt(0);
|
|
@@ -1619,6 +1628,7 @@ var L1PaymentsModule = class {
|
|
|
1619
1628
|
}
|
|
1620
1629
|
async getUtxos() {
|
|
1621
1630
|
this.ensureInitialized();
|
|
1631
|
+
await this.ensureConnected();
|
|
1622
1632
|
const result = [];
|
|
1623
1633
|
const currentHeight = await getCurrentBlockHeight();
|
|
1624
1634
|
const allUtxos = await this._getAllUtxos();
|
|
@@ -1654,42 +1664,73 @@ var L1PaymentsModule = class {
|
|
|
1654
1664
|
return result;
|
|
1655
1665
|
}
|
|
1656
1666
|
async getHistory(limit) {
|
|
1667
|
+
await this.ensureConnected();
|
|
1657
1668
|
this.ensureInitialized();
|
|
1658
1669
|
const addresses = this._getWatchedAddresses();
|
|
1659
1670
|
const transactions = [];
|
|
1660
1671
|
const seenTxids = /* @__PURE__ */ new Set();
|
|
1661
1672
|
const currentHeight = await getCurrentBlockHeight();
|
|
1673
|
+
const txCache = /* @__PURE__ */ new Map();
|
|
1674
|
+
const fetchTx = async (txid) => {
|
|
1675
|
+
if (txCache.has(txid)) return txCache.get(txid);
|
|
1676
|
+
const detail = await getTransaction(txid);
|
|
1677
|
+
txCache.set(txid, detail);
|
|
1678
|
+
return detail;
|
|
1679
|
+
};
|
|
1680
|
+
const addressSet = new Set(addresses.map((a) => a.toLowerCase()));
|
|
1662
1681
|
for (const address of addresses) {
|
|
1663
1682
|
const history = await getTransactionHistory(address);
|
|
1664
1683
|
for (const item of history) {
|
|
1665
1684
|
if (seenTxids.has(item.tx_hash)) continue;
|
|
1666
1685
|
seenTxids.add(item.tx_hash);
|
|
1667
|
-
const tx = await
|
|
1686
|
+
const tx = await fetchTx(item.tx_hash);
|
|
1668
1687
|
if (!tx) continue;
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1688
|
+
let isSend = false;
|
|
1689
|
+
for (const vin of tx.vin ?? []) {
|
|
1690
|
+
if (!vin.txid) continue;
|
|
1691
|
+
const prevTx = await fetchTx(vin.txid);
|
|
1692
|
+
if (prevTx?.vout?.[vin.vout]) {
|
|
1693
|
+
const prevOut = prevTx.vout[vin.vout];
|
|
1694
|
+
const prevAddrs = [
|
|
1695
|
+
...prevOut.scriptPubKey?.addresses ?? [],
|
|
1696
|
+
...prevOut.scriptPubKey?.address ? [prevOut.scriptPubKey.address] : []
|
|
1697
|
+
];
|
|
1698
|
+
if (prevAddrs.some((a) => addressSet.has(a.toLowerCase()))) {
|
|
1699
|
+
isSend = true;
|
|
1700
|
+
break;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
let amountToUs = 0;
|
|
1705
|
+
let amountToOthers = 0;
|
|
1673
1706
|
let txAddress = address;
|
|
1707
|
+
let externalAddress = "";
|
|
1674
1708
|
if (tx.vout) {
|
|
1675
1709
|
for (const vout of tx.vout) {
|
|
1676
|
-
const voutAddresses =
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1710
|
+
const voutAddresses = [
|
|
1711
|
+
...vout.scriptPubKey?.addresses ?? [],
|
|
1712
|
+
...vout.scriptPubKey?.address ? [vout.scriptPubKey.address] : []
|
|
1713
|
+
];
|
|
1714
|
+
const isOurs = voutAddresses.some((a) => addressSet.has(a.toLowerCase()));
|
|
1715
|
+
const valueSats = Math.floor((vout.value ?? 0) * 1e8);
|
|
1716
|
+
if (isOurs) {
|
|
1717
|
+
amountToUs += valueSats;
|
|
1718
|
+
if (!txAddress) txAddress = voutAddresses[0];
|
|
1719
|
+
} else {
|
|
1720
|
+
amountToOthers += valueSats;
|
|
1721
|
+
if (!externalAddress && voutAddresses.length > 0) {
|
|
1722
|
+
externalAddress = voutAddresses[0];
|
|
1723
|
+
}
|
|
1685
1724
|
}
|
|
1686
1725
|
}
|
|
1687
1726
|
}
|
|
1727
|
+
const amount = isSend ? amountToOthers.toString() : amountToUs.toString();
|
|
1728
|
+
const displayAddress = isSend ? externalAddress || txAddress : txAddress;
|
|
1688
1729
|
transactions.push({
|
|
1689
1730
|
txid: item.tx_hash,
|
|
1690
1731
|
type: isSend ? "send" : "receive",
|
|
1691
1732
|
amount,
|
|
1692
|
-
address:
|
|
1733
|
+
address: displayAddress,
|
|
1693
1734
|
confirmations: item.height > 0 ? currentHeight - item.height : 0,
|
|
1694
1735
|
timestamp: tx.time ? tx.time * 1e3 : Date.now(),
|
|
1695
1736
|
blockHeight: item.height > 0 ? item.height : void 0
|
|
@@ -1701,6 +1742,7 @@ var L1PaymentsModule = class {
|
|
|
1701
1742
|
}
|
|
1702
1743
|
async getTransaction(txid) {
|
|
1703
1744
|
this.ensureInitialized();
|
|
1745
|
+
await this.ensureConnected();
|
|
1704
1746
|
const tx = await getTransaction(txid);
|
|
1705
1747
|
if (!tx) return null;
|
|
1706
1748
|
const addresses = this._getWatchedAddresses();
|
|
@@ -1736,6 +1778,7 @@ var L1PaymentsModule = class {
|
|
|
1736
1778
|
}
|
|
1737
1779
|
async estimateFee(to, amount) {
|
|
1738
1780
|
this.ensureInitialized();
|
|
1781
|
+
await this.ensureConnected();
|
|
1739
1782
|
if (!this._wallet) {
|
|
1740
1783
|
return { fee: "0", feeRate: this._config.defaultFeeRate ?? 10 };
|
|
1741
1784
|
}
|
|
@@ -2075,6 +2118,7 @@ import { MintCommitment } from "@unicitylabs/state-transition-sdk/lib/transactio
|
|
|
2075
2118
|
import { HashAlgorithm as HashAlgorithm2 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
2076
2119
|
import { UnmaskedPredicate as UnmaskedPredicate2 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
2077
2120
|
import { waitInclusionProof as waitInclusionProof2 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
2121
|
+
import { normalizeNametag } from "@unicitylabs/nostr-js-sdk";
|
|
2078
2122
|
var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
2079
2123
|
var NametagMinter = class {
|
|
2080
2124
|
client;
|
|
@@ -2099,7 +2143,8 @@ var NametagMinter = class {
|
|
|
2099
2143
|
*/
|
|
2100
2144
|
async isNametagAvailable(nametag) {
|
|
2101
2145
|
try {
|
|
2102
|
-
const
|
|
2146
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2147
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2103
2148
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
2104
2149
|
const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
|
|
2105
2150
|
return !isMinted;
|
|
@@ -2116,7 +2161,8 @@ var NametagMinter = class {
|
|
|
2116
2161
|
* @returns MintNametagResult with token if successful
|
|
2117
2162
|
*/
|
|
2118
2163
|
async mintNametag(nametag, ownerAddress) {
|
|
2119
|
-
const
|
|
2164
|
+
const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
|
|
2165
|
+
const cleanNametag = normalizeNametag(stripped);
|
|
2120
2166
|
this.log(`Starting mint for nametag: ${cleanNametag}`);
|
|
2121
2167
|
try {
|
|
2122
2168
|
const nametagTokenId = await TokenId2.fromNameTag(cleanNametag);
|
|
@@ -2255,7 +2301,9 @@ var STORAGE_KEYS_GLOBAL = {
|
|
|
2255
2301
|
/** Nametag cache per address (separate from tracked addresses registry) */
|
|
2256
2302
|
ADDRESS_NAMETAGS: "address_nametags",
|
|
2257
2303
|
/** Active addresses registry (JSON: TrackedAddressesStorage) */
|
|
2258
|
-
TRACKED_ADDRESSES: "tracked_addresses"
|
|
2304
|
+
TRACKED_ADDRESSES: "tracked_addresses",
|
|
2305
|
+
/** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
|
|
2306
|
+
LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
|
|
2259
2307
|
};
|
|
2260
2308
|
var STORAGE_KEYS_ADDRESS = {
|
|
2261
2309
|
/** Pending transfers for this address */
|
|
@@ -2267,7 +2315,9 @@ var STORAGE_KEYS_ADDRESS = {
|
|
|
2267
2315
|
/** Messages for this address */
|
|
2268
2316
|
MESSAGES: "messages",
|
|
2269
2317
|
/** Transaction history for this address */
|
|
2270
|
-
TRANSACTION_HISTORY: "transaction_history"
|
|
2318
|
+
TRANSACTION_HISTORY: "transaction_history",
|
|
2319
|
+
/** Pending V5 finalization tokens (unconfirmed instant split tokens) */
|
|
2320
|
+
PENDING_V5_TOKENS: "pending_v5_tokens"
|
|
2271
2321
|
};
|
|
2272
2322
|
var STORAGE_KEYS = {
|
|
2273
2323
|
...STORAGE_KEYS_GLOBAL,
|
|
@@ -2286,16 +2336,6 @@ function getAddressId(directAddress) {
|
|
|
2286
2336
|
}
|
|
2287
2337
|
var DEFAULT_BASE_PATH = "m/44'/0'/0'";
|
|
2288
2338
|
var DEFAULT_DERIVATION_PATH2 = `${DEFAULT_BASE_PATH}/0/0`;
|
|
2289
|
-
var LIMITS = {
|
|
2290
|
-
/** Min nametag length */
|
|
2291
|
-
NAMETAG_MIN_LENGTH: 3,
|
|
2292
|
-
/** Max nametag length */
|
|
2293
|
-
NAMETAG_MAX_LENGTH: 20,
|
|
2294
|
-
/** Max memo length */
|
|
2295
|
-
MEMO_MAX_LENGTH: 500,
|
|
2296
|
-
/** Max message length */
|
|
2297
|
-
MESSAGE_MAX_LENGTH: 1e4
|
|
2298
|
-
};
|
|
2299
2339
|
|
|
2300
2340
|
// types/txf.ts
|
|
2301
2341
|
var ARCHIVED_PREFIX = "archived-";
|
|
@@ -2588,6 +2628,18 @@ function parseTxfStorageData(data) {
|
|
|
2588
2628
|
result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
|
|
2589
2629
|
}
|
|
2590
2630
|
}
|
|
2631
|
+
} else if (key.startsWith("token-")) {
|
|
2632
|
+
try {
|
|
2633
|
+
const entry = storageData[key];
|
|
2634
|
+
const txfToken = entry?.token;
|
|
2635
|
+
if (txfToken?.genesis?.data?.tokenId) {
|
|
2636
|
+
const tokenId = txfToken.genesis.data.tokenId;
|
|
2637
|
+
const token = txfToToken(tokenId, txfToken);
|
|
2638
|
+
result.tokens.push(token);
|
|
2639
|
+
}
|
|
2640
|
+
} catch (err) {
|
|
2641
|
+
result.validationErrors.push(`Token ${key}: ${err}`);
|
|
2642
|
+
}
|
|
2591
2643
|
}
|
|
2592
2644
|
}
|
|
2593
2645
|
return result;
|
|
@@ -3088,8 +3140,9 @@ var InstantSplitExecutor = class {
|
|
|
3088
3140
|
const criticalPathDuration = performance.now() - startTime;
|
|
3089
3141
|
console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
|
|
3090
3142
|
options?.onNostrDelivered?.(nostrEventId);
|
|
3143
|
+
let backgroundPromise;
|
|
3091
3144
|
if (!options?.skipBackground) {
|
|
3092
|
-
this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3145
|
+
backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
|
|
3093
3146
|
signingService: this.signingService,
|
|
3094
3147
|
tokenType: tokenToSplit.type,
|
|
3095
3148
|
coinId,
|
|
@@ -3105,7 +3158,8 @@ var InstantSplitExecutor = class {
|
|
|
3105
3158
|
nostrEventId,
|
|
3106
3159
|
splitGroupId,
|
|
3107
3160
|
criticalPathDurationMs: criticalPathDuration,
|
|
3108
|
-
backgroundStarted: !options?.skipBackground
|
|
3161
|
+
backgroundStarted: !options?.skipBackground,
|
|
3162
|
+
backgroundPromise
|
|
3109
3163
|
};
|
|
3110
3164
|
} catch (error) {
|
|
3111
3165
|
const duration = performance.now() - startTime;
|
|
@@ -3167,7 +3221,7 @@ var InstantSplitExecutor = class {
|
|
|
3167
3221
|
this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
|
|
3168
3222
|
this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
|
|
3169
3223
|
]);
|
|
3170
|
-
submissions.then(async (results) => {
|
|
3224
|
+
return submissions.then(async (results) => {
|
|
3171
3225
|
const submitDuration = performance.now() - startTime;
|
|
3172
3226
|
console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
|
|
3173
3227
|
context.onProgress?.({
|
|
@@ -3632,6 +3686,11 @@ import { AddressScheme } from "@unicitylabs/state-transition-sdk/lib/address/Add
|
|
|
3632
3686
|
import { UnmaskedPredicate as UnmaskedPredicate5 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
3633
3687
|
import { TokenState as TokenState5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
3634
3688
|
import { HashAlgorithm as HashAlgorithm5 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
3689
|
+
import { TokenType as TokenType3 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
3690
|
+
import { MintCommitment as MintCommitment3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment";
|
|
3691
|
+
import { MintTransactionData as MintTransactionData3 } from "@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData";
|
|
3692
|
+
import { waitInclusionProof as waitInclusionProof5 } from "@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils";
|
|
3693
|
+
import { InclusionProof } from "@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof";
|
|
3635
3694
|
function enrichWithRegistry(info) {
|
|
3636
3695
|
const registry = TokenRegistry.getInstance();
|
|
3637
3696
|
const def = registry.getDefinition(info.coinId);
|
|
@@ -3659,7 +3718,7 @@ async function parseTokenInfo(tokenData) {
|
|
|
3659
3718
|
try {
|
|
3660
3719
|
const sdkToken = await SdkToken2.fromJSON(data);
|
|
3661
3720
|
if (sdkToken.id) {
|
|
3662
|
-
defaultInfo.tokenId = sdkToken.id.
|
|
3721
|
+
defaultInfo.tokenId = sdkToken.id.toJSON();
|
|
3663
3722
|
}
|
|
3664
3723
|
if (sdkToken.coins && sdkToken.coins.coins) {
|
|
3665
3724
|
const rawCoins = sdkToken.coins.coins;
|
|
@@ -3829,6 +3888,13 @@ function extractTokenStateKey(token) {
|
|
|
3829
3888
|
if (!tokenId || !stateHash) return null;
|
|
3830
3889
|
return createTokenStateKey(tokenId, stateHash);
|
|
3831
3890
|
}
|
|
3891
|
+
function fromHex4(hex) {
|
|
3892
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
3893
|
+
for (let i = 0; i < hex.length; i += 2) {
|
|
3894
|
+
bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
3895
|
+
}
|
|
3896
|
+
return bytes;
|
|
3897
|
+
}
|
|
3832
3898
|
function hasSameGenesisTokenId(t1, t2) {
|
|
3833
3899
|
const id1 = extractTokenIdFromSdkData(t1.sdkData);
|
|
3834
3900
|
const id2 = extractTokenIdFromSdkData(t2.sdkData);
|
|
@@ -3918,6 +3984,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3918
3984
|
// Token State
|
|
3919
3985
|
tokens = /* @__PURE__ */ new Map();
|
|
3920
3986
|
pendingTransfers = /* @__PURE__ */ new Map();
|
|
3987
|
+
pendingBackgroundTasks = [];
|
|
3921
3988
|
// Repository State (tombstones, archives, forked, history)
|
|
3922
3989
|
tombstones = [];
|
|
3923
3990
|
archivedTokens = /* @__PURE__ */ new Map();
|
|
@@ -3942,6 +4009,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3942
4009
|
// Poll every 2s
|
|
3943
4010
|
static PROOF_POLLING_MAX_ATTEMPTS = 30;
|
|
3944
4011
|
// Max 30 attempts (~60s)
|
|
4012
|
+
// Storage event subscriptions (push-based sync)
|
|
4013
|
+
storageEventUnsubscribers = [];
|
|
4014
|
+
syncDebounceTimer = null;
|
|
4015
|
+
static SYNC_DEBOUNCE_MS = 500;
|
|
4016
|
+
/** Sync coalescing: concurrent sync() calls share the same operation */
|
|
4017
|
+
_syncInProgress = null;
|
|
3945
4018
|
constructor(config) {
|
|
3946
4019
|
this.moduleConfig = {
|
|
3947
4020
|
autoSync: config?.autoSync ?? true,
|
|
@@ -3950,10 +4023,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3950
4023
|
maxRetries: config?.maxRetries ?? 3,
|
|
3951
4024
|
debug: config?.debug ?? false
|
|
3952
4025
|
};
|
|
3953
|
-
|
|
3954
|
-
this.l1 = l1Enabled ? new L1PaymentsModule(config?.l1) : null;
|
|
4026
|
+
this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
|
|
3955
4027
|
}
|
|
3956
|
-
/**
|
|
4028
|
+
/**
|
|
4029
|
+
* Get the current module configuration (excluding L1 config).
|
|
4030
|
+
*
|
|
4031
|
+
* @returns Resolved configuration with all defaults applied.
|
|
4032
|
+
*/
|
|
3957
4033
|
getConfig() {
|
|
3958
4034
|
return this.moduleConfig;
|
|
3959
4035
|
}
|
|
@@ -3994,9 +4070,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
3994
4070
|
transport: deps.transport
|
|
3995
4071
|
});
|
|
3996
4072
|
}
|
|
3997
|
-
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
3998
|
-
this.handleIncomingTransfer(transfer)
|
|
3999
|
-
|
|
4073
|
+
this.unsubscribeTransfers = deps.transport.onTokenTransfer(
|
|
4074
|
+
(transfer) => this.handleIncomingTransfer(transfer)
|
|
4075
|
+
);
|
|
4000
4076
|
if (deps.transport.onPaymentRequest) {
|
|
4001
4077
|
this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
|
|
4002
4078
|
this.handleIncomingPaymentRequest(request);
|
|
@@ -4007,9 +4083,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4007
4083
|
this.handlePaymentRequestResponse(response);
|
|
4008
4084
|
});
|
|
4009
4085
|
}
|
|
4086
|
+
this.subscribeToStorageEvents();
|
|
4010
4087
|
}
|
|
4011
4088
|
/**
|
|
4012
|
-
* Load
|
|
4089
|
+
* Load all token data from storage providers and restore wallet state.
|
|
4090
|
+
*
|
|
4091
|
+
* Loads tokens, nametag data, transaction history, and pending transfers
|
|
4092
|
+
* from configured storage providers. Restores pending V5 tokens and
|
|
4093
|
+
* triggers a fire-and-forget {@link resolveUnconfirmed} call.
|
|
4013
4094
|
*/
|
|
4014
4095
|
async load() {
|
|
4015
4096
|
this.ensureInitialized();
|
|
@@ -4026,6 +4107,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4026
4107
|
console.error(`[Payments] Failed to load from provider ${id}:`, err);
|
|
4027
4108
|
}
|
|
4028
4109
|
}
|
|
4110
|
+
await this.loadPendingV5Tokens();
|
|
4029
4111
|
await this.loadTokensFromFileStorage();
|
|
4030
4112
|
await this.loadNametagFromFileStorage();
|
|
4031
4113
|
const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
|
|
@@ -4043,9 +4125,14 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4043
4125
|
this.pendingTransfers.set(transfer.id, transfer);
|
|
4044
4126
|
}
|
|
4045
4127
|
}
|
|
4128
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4129
|
+
});
|
|
4046
4130
|
}
|
|
4047
4131
|
/**
|
|
4048
|
-
* Cleanup
|
|
4132
|
+
* Cleanup all subscriptions, polling jobs, and pending resolvers.
|
|
4133
|
+
*
|
|
4134
|
+
* Should be called when the wallet is being shut down or the module is
|
|
4135
|
+
* no longer needed. Also destroys the L1 sub-module if present.
|
|
4049
4136
|
*/
|
|
4050
4137
|
destroy() {
|
|
4051
4138
|
this.unsubscribeTransfers?.();
|
|
@@ -4063,6 +4150,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4063
4150
|
resolver.reject(new Error("Module destroyed"));
|
|
4064
4151
|
}
|
|
4065
4152
|
this.pendingResponseResolvers.clear();
|
|
4153
|
+
this.unsubscribeStorageEvents();
|
|
4066
4154
|
if (this.l1) {
|
|
4067
4155
|
this.l1.destroy();
|
|
4068
4156
|
}
|
|
@@ -4079,7 +4167,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4079
4167
|
const result = {
|
|
4080
4168
|
id: crypto.randomUUID(),
|
|
4081
4169
|
status: "pending",
|
|
4082
|
-
tokens: []
|
|
4170
|
+
tokens: [],
|
|
4171
|
+
tokenTransfers: []
|
|
4083
4172
|
};
|
|
4084
4173
|
try {
|
|
4085
4174
|
const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
|
|
@@ -4116,69 +4205,147 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4116
4205
|
await this.saveToOutbox(result, recipientPubkey);
|
|
4117
4206
|
result.status = "submitted";
|
|
4118
4207
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4208
|
+
const transferMode = request.transferMode ?? "instant";
|
|
4119
4209
|
if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4210
|
+
if (transferMode === "conservative") {
|
|
4211
|
+
this.log("Executing conservative split...");
|
|
4212
|
+
const splitExecutor = new TokenSplitExecutor({
|
|
4213
|
+
stateTransitionClient: stClient,
|
|
4214
|
+
trustBase,
|
|
4215
|
+
signingService
|
|
4216
|
+
});
|
|
4217
|
+
const splitResult = await splitExecutor.executeSplit(
|
|
4218
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4219
|
+
splitPlan.splitAmount,
|
|
4220
|
+
splitPlan.remainderAmount,
|
|
4221
|
+
splitPlan.coinId,
|
|
4222
|
+
recipientAddress
|
|
4223
|
+
);
|
|
4224
|
+
const changeTokenData = splitResult.tokenForSender.toJSON();
|
|
4225
|
+
const changeUiToken = {
|
|
4226
|
+
id: crypto.randomUUID(),
|
|
4227
|
+
coinId: request.coinId,
|
|
4228
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4229
|
+
name: this.getCoinName(request.coinId),
|
|
4230
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4231
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4232
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4233
|
+
status: "confirmed",
|
|
4234
|
+
createdAt: Date.now(),
|
|
4235
|
+
updatedAt: Date.now(),
|
|
4236
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4237
|
+
};
|
|
4238
|
+
await this.addToken(changeUiToken, true);
|
|
4239
|
+
this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
|
|
4240
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4241
|
+
sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
|
|
4242
|
+
transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
|
|
4243
|
+
memo: request.memo
|
|
4244
|
+
});
|
|
4245
|
+
const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
|
|
4246
|
+
const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
|
|
4247
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
|
|
4248
|
+
result.tokenTransfers.push({
|
|
4249
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4250
|
+
method: "split",
|
|
4251
|
+
requestIdHex: splitRequestIdHex
|
|
4252
|
+
});
|
|
4253
|
+
this.log(`Conservative split transfer completed`);
|
|
4254
|
+
} else {
|
|
4255
|
+
this.log("Executing instant split...");
|
|
4256
|
+
const devMode = this.deps.oracle.isDevMode?.() ?? false;
|
|
4257
|
+
const executor = new InstantSplitExecutor({
|
|
4258
|
+
stateTransitionClient: stClient,
|
|
4259
|
+
trustBase,
|
|
4260
|
+
signingService,
|
|
4261
|
+
devMode
|
|
4262
|
+
});
|
|
4263
|
+
const instantResult = await executor.executeSplitInstant(
|
|
4264
|
+
splitPlan.tokenToSplit.sdkToken,
|
|
4265
|
+
splitPlan.splitAmount,
|
|
4266
|
+
splitPlan.remainderAmount,
|
|
4267
|
+
splitPlan.coinId,
|
|
4268
|
+
recipientAddress,
|
|
4269
|
+
this.deps.transport,
|
|
4270
|
+
recipientPubkey,
|
|
4271
|
+
{
|
|
4272
|
+
onChangeTokenCreated: async (changeToken) => {
|
|
4273
|
+
const changeTokenData = changeToken.toJSON();
|
|
4274
|
+
const uiToken = {
|
|
4275
|
+
id: crypto.randomUUID(),
|
|
4276
|
+
coinId: request.coinId,
|
|
4277
|
+
symbol: this.getCoinSymbol(request.coinId),
|
|
4278
|
+
name: this.getCoinName(request.coinId),
|
|
4279
|
+
decimals: this.getCoinDecimals(request.coinId),
|
|
4280
|
+
iconUrl: this.getCoinIconUrl(request.coinId),
|
|
4281
|
+
amount: splitPlan.remainderAmount.toString(),
|
|
4282
|
+
status: "confirmed",
|
|
4283
|
+
createdAt: Date.now(),
|
|
4284
|
+
updatedAt: Date.now(),
|
|
4285
|
+
sdkData: JSON.stringify(changeTokenData)
|
|
4286
|
+
};
|
|
4287
|
+
await this.addToken(uiToken, true);
|
|
4288
|
+
this.log(`Change token saved via background: ${uiToken.id}`);
|
|
4289
|
+
},
|
|
4290
|
+
onStorageSync: async () => {
|
|
4291
|
+
await this.save();
|
|
4292
|
+
return true;
|
|
4293
|
+
}
|
|
4294
|
+
}
|
|
4295
|
+
);
|
|
4296
|
+
if (!instantResult.success) {
|
|
4297
|
+
throw new Error(instantResult.error || "Instant split failed");
|
|
4298
|
+
}
|
|
4299
|
+
if (instantResult.backgroundPromise) {
|
|
4300
|
+
this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
|
|
4301
|
+
}
|
|
4302
|
+
await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
|
|
4303
|
+
result.tokenTransfers.push({
|
|
4304
|
+
sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
|
|
4305
|
+
method: "split",
|
|
4306
|
+
splitGroupId: instantResult.splitGroupId,
|
|
4307
|
+
nostrEventId: instantResult.nostrEventId
|
|
4308
|
+
});
|
|
4309
|
+
this.log(`Instant split transfer completed`);
|
|
4310
|
+
}
|
|
4159
4311
|
}
|
|
4160
4312
|
for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
|
|
4161
4313
|
const token = tokenWithAmount.uiToken;
|
|
4162
4314
|
const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4315
|
+
if (transferMode === "conservative") {
|
|
4316
|
+
console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4317
|
+
const submitResponse = await stClient.submitTransferCommitment(commitment);
|
|
4318
|
+
if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
|
|
4319
|
+
throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
|
|
4320
|
+
}
|
|
4321
|
+
const inclusionProof = await waitInclusionProof5(trustBase, stClient, commitment);
|
|
4322
|
+
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4323
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4324
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4325
|
+
transferTx: JSON.stringify(transferTx.toJSON()),
|
|
4326
|
+
memo: request.memo
|
|
4327
|
+
});
|
|
4328
|
+
console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
|
|
4329
|
+
} else {
|
|
4330
|
+
console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
|
|
4331
|
+
await this.deps.transport.sendTokenTransfer(recipientPubkey, {
|
|
4332
|
+
sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
|
|
4333
|
+
commitmentData: JSON.stringify(commitment.toJSON()),
|
|
4334
|
+
memo: request.memo
|
|
4335
|
+
});
|
|
4336
|
+
console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
|
|
4337
|
+
stClient.submitTransferCommitment(commitment).catch(
|
|
4338
|
+
(err) => console.error("[Payments] Background commitment submit failed:", err)
|
|
4339
|
+
);
|
|
4169
4340
|
}
|
|
4170
|
-
const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
|
|
4171
|
-
const transferTx = commitment.toTransaction(inclusionProof);
|
|
4172
4341
|
const requestIdBytes = commitment.requestId;
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
memo: request.memo
|
|
4342
|
+
const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
|
|
4343
|
+
result.tokenTransfers.push({
|
|
4344
|
+
sourceTokenId: token.id,
|
|
4345
|
+
method: "direct",
|
|
4346
|
+
requestIdHex
|
|
4179
4347
|
});
|
|
4180
|
-
|
|
4181
|
-
this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
|
|
4348
|
+
this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
|
|
4182
4349
|
await this.removeToken(token.id, recipientNametag, true);
|
|
4183
4350
|
}
|
|
4184
4351
|
result.status = "delivered";
|
|
@@ -4191,7 +4358,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4191
4358
|
coinId: request.coinId,
|
|
4192
4359
|
symbol: this.getCoinSymbol(request.coinId),
|
|
4193
4360
|
timestamp: Date.now(),
|
|
4194
|
-
recipientNametag
|
|
4361
|
+
recipientNametag,
|
|
4362
|
+
transferId: result.id
|
|
4195
4363
|
});
|
|
4196
4364
|
this.deps.emitEvent("transfer:confirmed", result);
|
|
4197
4365
|
return result;
|
|
@@ -4327,6 +4495,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4327
4495
|
}
|
|
4328
4496
|
);
|
|
4329
4497
|
if (result.success) {
|
|
4498
|
+
if (result.backgroundPromise) {
|
|
4499
|
+
this.pendingBackgroundTasks.push(result.backgroundPromise);
|
|
4500
|
+
}
|
|
4330
4501
|
const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
|
|
4331
4502
|
await this.removeToken(tokenToSplit.id, recipientNametag, true);
|
|
4332
4503
|
await this.addToHistory({
|
|
@@ -4368,6 +4539,63 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4368
4539
|
*/
|
|
4369
4540
|
async processInstantSplitBundle(bundle, senderPubkey) {
|
|
4370
4541
|
this.ensureInitialized();
|
|
4542
|
+
if (!isInstantSplitBundleV5(bundle)) {
|
|
4543
|
+
return this.processInstantSplitBundleSync(bundle, senderPubkey);
|
|
4544
|
+
}
|
|
4545
|
+
try {
|
|
4546
|
+
const deterministicId = `v5split_${bundle.splitGroupId}`;
|
|
4547
|
+
if (this.tokens.has(deterministicId)) {
|
|
4548
|
+
this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
|
|
4549
|
+
return { success: true, durationMs: 0 };
|
|
4550
|
+
}
|
|
4551
|
+
const registry = TokenRegistry.getInstance();
|
|
4552
|
+
const pendingData = {
|
|
4553
|
+
type: "v5_bundle",
|
|
4554
|
+
stage: "RECEIVED",
|
|
4555
|
+
bundleJson: JSON.stringify(bundle),
|
|
4556
|
+
senderPubkey,
|
|
4557
|
+
savedAt: Date.now(),
|
|
4558
|
+
attemptCount: 0
|
|
4559
|
+
};
|
|
4560
|
+
const uiToken = {
|
|
4561
|
+
id: deterministicId,
|
|
4562
|
+
coinId: bundle.coinId,
|
|
4563
|
+
symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
|
|
4564
|
+
name: registry.getName(bundle.coinId) || bundle.coinId,
|
|
4565
|
+
decimals: registry.getDecimals(bundle.coinId) ?? 8,
|
|
4566
|
+
amount: bundle.amount,
|
|
4567
|
+
status: "submitted",
|
|
4568
|
+
// UNCONFIRMED
|
|
4569
|
+
createdAt: Date.now(),
|
|
4570
|
+
updatedAt: Date.now(),
|
|
4571
|
+
sdkData: JSON.stringify({ _pendingFinalization: pendingData })
|
|
4572
|
+
};
|
|
4573
|
+
await this.addToken(uiToken, false);
|
|
4574
|
+
this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
|
|
4575
|
+
this.deps.emitEvent("transfer:incoming", {
|
|
4576
|
+
id: bundle.splitGroupId,
|
|
4577
|
+
senderPubkey,
|
|
4578
|
+
tokens: [uiToken],
|
|
4579
|
+
receivedAt: Date.now()
|
|
4580
|
+
});
|
|
4581
|
+
await this.save();
|
|
4582
|
+
this.resolveUnconfirmed().catch(() => {
|
|
4583
|
+
});
|
|
4584
|
+
return { success: true, durationMs: 0 };
|
|
4585
|
+
} catch (error) {
|
|
4586
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
4587
|
+
return {
|
|
4588
|
+
success: false,
|
|
4589
|
+
error: errorMessage,
|
|
4590
|
+
durationMs: 0
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* Synchronous V4 bundle processing (dev mode only).
|
|
4596
|
+
* Kept for backward compatibility with V4 bundles.
|
|
4597
|
+
*/
|
|
4598
|
+
async processInstantSplitBundleSync(bundle, senderPubkey) {
|
|
4371
4599
|
try {
|
|
4372
4600
|
const signingService = await this.createSigningService();
|
|
4373
4601
|
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
@@ -4453,7 +4681,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4453
4681
|
}
|
|
4454
4682
|
}
|
|
4455
4683
|
/**
|
|
4456
|
-
*
|
|
4684
|
+
* Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
|
|
4685
|
+
*
|
|
4686
|
+
* @param payload - The object to test.
|
|
4687
|
+
* @returns `true` if the payload matches the InstantSplitBundle shape.
|
|
4457
4688
|
*/
|
|
4458
4689
|
isInstantSplitBundle(payload) {
|
|
4459
4690
|
return isInstantSplitBundle(payload);
|
|
@@ -4534,39 +4765,57 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4534
4765
|
return [...this.paymentRequests];
|
|
4535
4766
|
}
|
|
4536
4767
|
/**
|
|
4537
|
-
* Get
|
|
4768
|
+
* Get the count of payment requests with status `'pending'`.
|
|
4769
|
+
*
|
|
4770
|
+
* @returns Number of pending incoming payment requests.
|
|
4538
4771
|
*/
|
|
4539
4772
|
getPendingPaymentRequestsCount() {
|
|
4540
4773
|
return this.paymentRequests.filter((r) => r.status === "pending").length;
|
|
4541
4774
|
}
|
|
4542
4775
|
/**
|
|
4543
|
-
* Accept a payment request
|
|
4776
|
+
* Accept a payment request and notify the requester.
|
|
4777
|
+
*
|
|
4778
|
+
* Marks the request as `'accepted'` and sends a response via transport.
|
|
4779
|
+
* The caller should subsequently call {@link send} to fulfill the payment.
|
|
4780
|
+
*
|
|
4781
|
+
* @param requestId - ID of the incoming payment request to accept.
|
|
4544
4782
|
*/
|
|
4545
4783
|
async acceptPaymentRequest(requestId2) {
|
|
4546
4784
|
this.updatePaymentRequestStatus(requestId2, "accepted");
|
|
4547
4785
|
await this.sendPaymentRequestResponse(requestId2, "accepted");
|
|
4548
4786
|
}
|
|
4549
4787
|
/**
|
|
4550
|
-
* Reject a payment request
|
|
4788
|
+
* Reject a payment request and notify the requester.
|
|
4789
|
+
*
|
|
4790
|
+
* @param requestId - ID of the incoming payment request to reject.
|
|
4551
4791
|
*/
|
|
4552
4792
|
async rejectPaymentRequest(requestId2) {
|
|
4553
4793
|
this.updatePaymentRequestStatus(requestId2, "rejected");
|
|
4554
4794
|
await this.sendPaymentRequestResponse(requestId2, "rejected");
|
|
4555
4795
|
}
|
|
4556
4796
|
/**
|
|
4557
|
-
* Mark a payment request as paid (
|
|
4797
|
+
* Mark a payment request as paid (local status update only).
|
|
4798
|
+
*
|
|
4799
|
+
* Typically called after a successful {@link send} to record that the
|
|
4800
|
+
* request has been fulfilled.
|
|
4801
|
+
*
|
|
4802
|
+
* @param requestId - ID of the incoming payment request to mark as paid.
|
|
4558
4803
|
*/
|
|
4559
4804
|
markPaymentRequestPaid(requestId2) {
|
|
4560
4805
|
this.updatePaymentRequestStatus(requestId2, "paid");
|
|
4561
4806
|
}
|
|
4562
4807
|
/**
|
|
4563
|
-
*
|
|
4808
|
+
* Remove all non-pending incoming payment requests from memory.
|
|
4809
|
+
*
|
|
4810
|
+
* Keeps only requests with status `'pending'`.
|
|
4564
4811
|
*/
|
|
4565
4812
|
clearProcessedPaymentRequests() {
|
|
4566
4813
|
this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
|
|
4567
4814
|
}
|
|
4568
4815
|
/**
|
|
4569
|
-
* Remove a specific payment request
|
|
4816
|
+
* Remove a specific incoming payment request by ID.
|
|
4817
|
+
*
|
|
4818
|
+
* @param requestId - ID of the payment request to remove.
|
|
4570
4819
|
*/
|
|
4571
4820
|
removePaymentRequest(requestId2) {
|
|
4572
4821
|
this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
|
|
@@ -4613,13 +4862,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4613
4862
|
if (this.paymentRequests.find((r) => r.id === transportRequest.id)) {
|
|
4614
4863
|
return;
|
|
4615
4864
|
}
|
|
4865
|
+
const coinId = transportRequest.request.coinId;
|
|
4866
|
+
const registry = TokenRegistry.getInstance();
|
|
4867
|
+
const coinDef = registry.getDefinition(coinId);
|
|
4616
4868
|
const request = {
|
|
4617
4869
|
id: transportRequest.id,
|
|
4618
4870
|
senderPubkey: transportRequest.senderTransportPubkey,
|
|
4871
|
+
senderNametag: transportRequest.senderNametag,
|
|
4619
4872
|
amount: transportRequest.request.amount,
|
|
4620
|
-
coinId
|
|
4621
|
-
symbol:
|
|
4622
|
-
// Use coinId as symbol for now
|
|
4873
|
+
coinId,
|
|
4874
|
+
symbol: coinDef?.symbol || coinId.slice(0, 8),
|
|
4623
4875
|
message: transportRequest.request.message,
|
|
4624
4876
|
recipientNametag: transportRequest.request.recipientNametag,
|
|
4625
4877
|
requestId: transportRequest.request.requestId,
|
|
@@ -4688,7 +4940,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4688
4940
|
});
|
|
4689
4941
|
}
|
|
4690
4942
|
/**
|
|
4691
|
-
* Cancel
|
|
4943
|
+
* Cancel an active {@link waitForPaymentResponse} call.
|
|
4944
|
+
*
|
|
4945
|
+
* The pending promise is rejected with a `'Cancelled'` error.
|
|
4946
|
+
*
|
|
4947
|
+
* @param requestId - The outgoing request ID whose wait should be cancelled.
|
|
4692
4948
|
*/
|
|
4693
4949
|
cancelWaitForPaymentResponse(requestId2) {
|
|
4694
4950
|
const resolver = this.pendingResponseResolvers.get(requestId2);
|
|
@@ -4699,14 +4955,16 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4699
4955
|
}
|
|
4700
4956
|
}
|
|
4701
4957
|
/**
|
|
4702
|
-
* Remove an outgoing payment request
|
|
4958
|
+
* Remove an outgoing payment request and cancel any pending wait.
|
|
4959
|
+
*
|
|
4960
|
+
* @param requestId - ID of the outgoing request to remove.
|
|
4703
4961
|
*/
|
|
4704
4962
|
removeOutgoingPaymentRequest(requestId2) {
|
|
4705
4963
|
this.outgoingPaymentRequests.delete(requestId2);
|
|
4706
4964
|
this.cancelWaitForPaymentResponse(requestId2);
|
|
4707
4965
|
}
|
|
4708
4966
|
/**
|
|
4709
|
-
*
|
|
4967
|
+
* Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
|
|
4710
4968
|
*/
|
|
4711
4969
|
clearCompletedOutgoingPaymentRequests() {
|
|
4712
4970
|
for (const [id, request] of this.outgoingPaymentRequests) {
|
|
@@ -4778,6 +5036,71 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4778
5036
|
}
|
|
4779
5037
|
}
|
|
4780
5038
|
// ===========================================================================
|
|
5039
|
+
// Public API - Receive
|
|
5040
|
+
// ===========================================================================
|
|
5041
|
+
/**
|
|
5042
|
+
* Fetch and process pending incoming transfers from the transport layer.
|
|
5043
|
+
*
|
|
5044
|
+
* Performs a one-shot query to fetch all pending events, processes them
|
|
5045
|
+
* through the existing pipeline, and resolves after all stored events
|
|
5046
|
+
* are handled. Useful for batch/CLI apps that need explicit receive.
|
|
5047
|
+
*
|
|
5048
|
+
* When `finalize` is true, polls resolveUnconfirmed() + load() until all
|
|
5049
|
+
* tokens are confirmed or the timeout expires. Otherwise calls
|
|
5050
|
+
* resolveUnconfirmed() once to submit pending commitments.
|
|
5051
|
+
*
|
|
5052
|
+
* @param options - Optional receive options including finalization control
|
|
5053
|
+
* @param callback - Optional callback invoked for each newly received transfer
|
|
5054
|
+
* @returns ReceiveResult with transfers and finalization metadata
|
|
5055
|
+
*/
|
|
5056
|
+
async receive(options, callback) {
|
|
5057
|
+
this.ensureInitialized();
|
|
5058
|
+
if (!this.deps.transport.fetchPendingEvents) {
|
|
5059
|
+
throw new Error("Transport provider does not support fetchPendingEvents");
|
|
5060
|
+
}
|
|
5061
|
+
const opts = options ?? {};
|
|
5062
|
+
const tokensBefore = new Set(this.tokens.keys());
|
|
5063
|
+
await this.deps.transport.fetchPendingEvents();
|
|
5064
|
+
await this.load();
|
|
5065
|
+
const received = [];
|
|
5066
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5067
|
+
if (!tokensBefore.has(tokenId)) {
|
|
5068
|
+
const transfer = {
|
|
5069
|
+
id: tokenId,
|
|
5070
|
+
senderPubkey: "",
|
|
5071
|
+
tokens: [token],
|
|
5072
|
+
receivedAt: Date.now()
|
|
5073
|
+
};
|
|
5074
|
+
received.push(transfer);
|
|
5075
|
+
if (callback) callback(transfer);
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
const result = { transfers: received };
|
|
5079
|
+
if (opts.finalize) {
|
|
5080
|
+
const timeout = opts.timeout ?? 6e4;
|
|
5081
|
+
const pollInterval = opts.pollInterval ?? 2e3;
|
|
5082
|
+
const startTime = Date.now();
|
|
5083
|
+
while (Date.now() - startTime < timeout) {
|
|
5084
|
+
const resolution = await this.resolveUnconfirmed();
|
|
5085
|
+
result.finalization = resolution;
|
|
5086
|
+
if (opts.onProgress) opts.onProgress(resolution);
|
|
5087
|
+
const stillUnconfirmed = Array.from(this.tokens.values()).some(
|
|
5088
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5089
|
+
);
|
|
5090
|
+
if (!stillUnconfirmed) break;
|
|
5091
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
5092
|
+
await this.load();
|
|
5093
|
+
}
|
|
5094
|
+
result.finalizationDurationMs = Date.now() - startTime;
|
|
5095
|
+
result.timedOut = Array.from(this.tokens.values()).some(
|
|
5096
|
+
(t) => t.status === "submitted" || t.status === "pending"
|
|
5097
|
+
);
|
|
5098
|
+
} else {
|
|
5099
|
+
result.finalization = await this.resolveUnconfirmed();
|
|
5100
|
+
}
|
|
5101
|
+
return result;
|
|
5102
|
+
}
|
|
5103
|
+
// ===========================================================================
|
|
4781
5104
|
// Public API - Balance & Tokens
|
|
4782
5105
|
// ===========================================================================
|
|
4783
5106
|
/**
|
|
@@ -4787,10 +5110,20 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4787
5110
|
this.priceProvider = provider;
|
|
4788
5111
|
}
|
|
4789
5112
|
/**
|
|
4790
|
-
*
|
|
4791
|
-
*
|
|
5113
|
+
* Wait for all pending background operations (e.g., instant split change token creation).
|
|
5114
|
+
* Call this before process exit to ensure all tokens are saved.
|
|
4792
5115
|
*/
|
|
4793
|
-
async
|
|
5116
|
+
async waitForPendingOperations() {
|
|
5117
|
+
if (this.pendingBackgroundTasks.length > 0) {
|
|
5118
|
+
await Promise.allSettled(this.pendingBackgroundTasks);
|
|
5119
|
+
this.pendingBackgroundTasks = [];
|
|
5120
|
+
}
|
|
5121
|
+
}
|
|
5122
|
+
/**
|
|
5123
|
+
* Get total portfolio value in USD.
|
|
5124
|
+
* Returns null if PriceProvider is not configured.
|
|
5125
|
+
*/
|
|
5126
|
+
async getFiatBalance() {
|
|
4794
5127
|
const assets = await this.getAssets();
|
|
4795
5128
|
if (!this.priceProvider) {
|
|
4796
5129
|
return null;
|
|
@@ -4806,19 +5139,95 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4806
5139
|
return hasAnyPrice ? total : null;
|
|
4807
5140
|
}
|
|
4808
5141
|
/**
|
|
4809
|
-
* Get
|
|
4810
|
-
*
|
|
5142
|
+
* Get token balances grouped by coin type.
|
|
5143
|
+
*
|
|
5144
|
+
* Returns an array of {@link Asset} objects, one per coin type held.
|
|
5145
|
+
* Each entry includes confirmed and unconfirmed breakdowns. Tokens with
|
|
5146
|
+
* status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
|
|
5147
|
+
*
|
|
5148
|
+
* This is synchronous — no price data is included. Use {@link getAssets}
|
|
5149
|
+
* for the async version with fiat pricing.
|
|
5150
|
+
*
|
|
5151
|
+
* @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
|
|
5152
|
+
* @returns Array of balance summaries (synchronous — no await needed).
|
|
5153
|
+
*/
|
|
5154
|
+
getBalance(coinId) {
|
|
5155
|
+
return this.aggregateTokens(coinId);
|
|
5156
|
+
}
|
|
5157
|
+
/**
|
|
5158
|
+
* Get aggregated assets (tokens grouped by coinId) with price data.
|
|
5159
|
+
* Includes both confirmed and unconfirmed tokens with breakdown.
|
|
4811
5160
|
*/
|
|
4812
5161
|
async getAssets(coinId) {
|
|
5162
|
+
const rawAssets = this.aggregateTokens(coinId);
|
|
5163
|
+
if (!this.priceProvider || rawAssets.length === 0) {
|
|
5164
|
+
return rawAssets;
|
|
5165
|
+
}
|
|
5166
|
+
try {
|
|
5167
|
+
const registry = TokenRegistry.getInstance();
|
|
5168
|
+
const nameToCoins = /* @__PURE__ */ new Map();
|
|
5169
|
+
for (const asset of rawAssets) {
|
|
5170
|
+
const def = registry.getDefinition(asset.coinId);
|
|
5171
|
+
if (def?.name) {
|
|
5172
|
+
const existing = nameToCoins.get(def.name);
|
|
5173
|
+
if (existing) {
|
|
5174
|
+
existing.push(asset.coinId);
|
|
5175
|
+
} else {
|
|
5176
|
+
nameToCoins.set(def.name, [asset.coinId]);
|
|
5177
|
+
}
|
|
5178
|
+
}
|
|
5179
|
+
}
|
|
5180
|
+
if (nameToCoins.size > 0) {
|
|
5181
|
+
const tokenNames = Array.from(nameToCoins.keys());
|
|
5182
|
+
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
5183
|
+
return rawAssets.map((raw) => {
|
|
5184
|
+
const def = registry.getDefinition(raw.coinId);
|
|
5185
|
+
const price = def?.name ? prices.get(def.name) : void 0;
|
|
5186
|
+
let fiatValueUsd = null;
|
|
5187
|
+
let fiatValueEur = null;
|
|
5188
|
+
if (price) {
|
|
5189
|
+
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
5190
|
+
fiatValueUsd = humanAmount * price.priceUsd;
|
|
5191
|
+
if (price.priceEur != null) {
|
|
5192
|
+
fiatValueEur = humanAmount * price.priceEur;
|
|
5193
|
+
}
|
|
5194
|
+
}
|
|
5195
|
+
return {
|
|
5196
|
+
...raw,
|
|
5197
|
+
priceUsd: price?.priceUsd ?? null,
|
|
5198
|
+
priceEur: price?.priceEur ?? null,
|
|
5199
|
+
change24h: price?.change24h ?? null,
|
|
5200
|
+
fiatValueUsd,
|
|
5201
|
+
fiatValueEur
|
|
5202
|
+
};
|
|
5203
|
+
});
|
|
5204
|
+
}
|
|
5205
|
+
} catch (error) {
|
|
5206
|
+
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
5207
|
+
}
|
|
5208
|
+
return rawAssets;
|
|
5209
|
+
}
|
|
5210
|
+
/**
|
|
5211
|
+
* Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
|
|
5212
|
+
* Excludes tokens with status 'spent', 'invalid', or 'transferring'.
|
|
5213
|
+
*/
|
|
5214
|
+
aggregateTokens(coinId) {
|
|
4813
5215
|
const assetsMap = /* @__PURE__ */ new Map();
|
|
4814
5216
|
for (const token of this.tokens.values()) {
|
|
4815
|
-
if (token.status
|
|
5217
|
+
if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
|
|
4816
5218
|
if (coinId && token.coinId !== coinId) continue;
|
|
4817
5219
|
const key = token.coinId;
|
|
5220
|
+
const amount = BigInt(token.amount);
|
|
5221
|
+
const isConfirmed = token.status === "confirmed";
|
|
4818
5222
|
const existing = assetsMap.get(key);
|
|
4819
5223
|
if (existing) {
|
|
4820
|
-
|
|
4821
|
-
|
|
5224
|
+
if (isConfirmed) {
|
|
5225
|
+
existing.confirmedAmount += amount;
|
|
5226
|
+
existing.confirmedTokenCount++;
|
|
5227
|
+
} else {
|
|
5228
|
+
existing.unconfirmedAmount += amount;
|
|
5229
|
+
existing.unconfirmedTokenCount++;
|
|
5230
|
+
}
|
|
4822
5231
|
} else {
|
|
4823
5232
|
assetsMap.set(key, {
|
|
4824
5233
|
coinId: token.coinId,
|
|
@@ -4826,78 +5235,42 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4826
5235
|
name: token.name,
|
|
4827
5236
|
decimals: token.decimals,
|
|
4828
5237
|
iconUrl: token.iconUrl,
|
|
4829
|
-
|
|
4830
|
-
|
|
5238
|
+
confirmedAmount: isConfirmed ? amount : 0n,
|
|
5239
|
+
unconfirmedAmount: isConfirmed ? 0n : amount,
|
|
5240
|
+
confirmedTokenCount: isConfirmed ? 1 : 0,
|
|
5241
|
+
unconfirmedTokenCount: isConfirmed ? 0 : 1
|
|
4831
5242
|
});
|
|
4832
5243
|
}
|
|
4833
5244
|
}
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
if (this.priceProvider && rawAssets.length > 0) {
|
|
4837
|
-
try {
|
|
4838
|
-
const registry = TokenRegistry.getInstance();
|
|
4839
|
-
const nameToCoins = /* @__PURE__ */ new Map();
|
|
4840
|
-
for (const asset of rawAssets) {
|
|
4841
|
-
const def = registry.getDefinition(asset.coinId);
|
|
4842
|
-
if (def?.name) {
|
|
4843
|
-
const existing = nameToCoins.get(def.name);
|
|
4844
|
-
if (existing) {
|
|
4845
|
-
existing.push(asset.coinId);
|
|
4846
|
-
} else {
|
|
4847
|
-
nameToCoins.set(def.name, [asset.coinId]);
|
|
4848
|
-
}
|
|
4849
|
-
}
|
|
4850
|
-
}
|
|
4851
|
-
if (nameToCoins.size > 0) {
|
|
4852
|
-
const tokenNames = Array.from(nameToCoins.keys());
|
|
4853
|
-
const prices = await this.priceProvider.getPrices(tokenNames);
|
|
4854
|
-
priceMap = /* @__PURE__ */ new Map();
|
|
4855
|
-
for (const [name, coinIds] of nameToCoins) {
|
|
4856
|
-
const price = prices.get(name);
|
|
4857
|
-
if (price) {
|
|
4858
|
-
for (const cid of coinIds) {
|
|
4859
|
-
priceMap.set(cid, {
|
|
4860
|
-
priceUsd: price.priceUsd,
|
|
4861
|
-
priceEur: price.priceEur,
|
|
4862
|
-
change24h: price.change24h
|
|
4863
|
-
});
|
|
4864
|
-
}
|
|
4865
|
-
}
|
|
4866
|
-
}
|
|
4867
|
-
}
|
|
4868
|
-
} catch (error) {
|
|
4869
|
-
console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
|
|
4870
|
-
}
|
|
4871
|
-
}
|
|
4872
|
-
return rawAssets.map((raw) => {
|
|
4873
|
-
const price = priceMap?.get(raw.coinId);
|
|
4874
|
-
let fiatValueUsd = null;
|
|
4875
|
-
let fiatValueEur = null;
|
|
4876
|
-
if (price) {
|
|
4877
|
-
const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
|
|
4878
|
-
fiatValueUsd = humanAmount * price.priceUsd;
|
|
4879
|
-
if (price.priceEur != null) {
|
|
4880
|
-
fiatValueEur = humanAmount * price.priceEur;
|
|
4881
|
-
}
|
|
4882
|
-
}
|
|
5245
|
+
return Array.from(assetsMap.values()).map((raw) => {
|
|
5246
|
+
const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
|
|
4883
5247
|
return {
|
|
4884
5248
|
coinId: raw.coinId,
|
|
4885
5249
|
symbol: raw.symbol,
|
|
4886
5250
|
name: raw.name,
|
|
4887
5251
|
decimals: raw.decimals,
|
|
4888
5252
|
iconUrl: raw.iconUrl,
|
|
4889
|
-
totalAmount
|
|
4890
|
-
tokenCount: raw.
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
5253
|
+
totalAmount,
|
|
5254
|
+
tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
|
|
5255
|
+
confirmedAmount: raw.confirmedAmount.toString(),
|
|
5256
|
+
unconfirmedAmount: raw.unconfirmedAmount.toString(),
|
|
5257
|
+
confirmedTokenCount: raw.confirmedTokenCount,
|
|
5258
|
+
unconfirmedTokenCount: raw.unconfirmedTokenCount,
|
|
5259
|
+
priceUsd: null,
|
|
5260
|
+
priceEur: null,
|
|
5261
|
+
change24h: null,
|
|
5262
|
+
fiatValueUsd: null,
|
|
5263
|
+
fiatValueEur: null
|
|
4896
5264
|
};
|
|
4897
5265
|
});
|
|
4898
5266
|
}
|
|
4899
5267
|
/**
|
|
4900
|
-
* Get all tokens
|
|
5268
|
+
* Get all tokens, optionally filtered by coin type and/or status.
|
|
5269
|
+
*
|
|
5270
|
+
* @param filter - Optional filter criteria.
|
|
5271
|
+
* @param filter.coinId - Return only tokens of this coin type.
|
|
5272
|
+
* @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
|
|
5273
|
+
* @returns Array of matching {@link Token} objects (synchronous).
|
|
4901
5274
|
*/
|
|
4902
5275
|
getTokens(filter) {
|
|
4903
5276
|
let tokens = Array.from(this.tokens.values());
|
|
@@ -4910,19 +5283,327 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4910
5283
|
return tokens;
|
|
4911
5284
|
}
|
|
4912
5285
|
/**
|
|
4913
|
-
* Get single token
|
|
5286
|
+
* Get a single token by its local ID.
|
|
5287
|
+
*
|
|
5288
|
+
* @param id - The local UUID assigned when the token was added.
|
|
5289
|
+
* @returns The token, or `undefined` if not found.
|
|
4914
5290
|
*/
|
|
4915
5291
|
getToken(id) {
|
|
4916
5292
|
return this.tokens.get(id);
|
|
4917
5293
|
}
|
|
4918
5294
|
// ===========================================================================
|
|
5295
|
+
// Public API - Unconfirmed Token Resolution
|
|
5296
|
+
// ===========================================================================
|
|
5297
|
+
/**
|
|
5298
|
+
* Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
|
|
5299
|
+
* their missing aggregator proofs.
|
|
5300
|
+
*
|
|
5301
|
+
* Each unconfirmed V5 token progresses through stages:
|
|
5302
|
+
* `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
|
|
5303
|
+
*
|
|
5304
|
+
* Uses 500 ms quick-timeouts per proof check so the call returns quickly even
|
|
5305
|
+
* when proofs are not yet available. Tokens that exceed 50 failed attempts are
|
|
5306
|
+
* marked `'invalid'`.
|
|
5307
|
+
*
|
|
5308
|
+
* Automatically called (fire-and-forget) by {@link load}.
|
|
5309
|
+
*
|
|
5310
|
+
* @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
|
|
5311
|
+
*/
|
|
5312
|
+
async resolveUnconfirmed() {
|
|
5313
|
+
this.ensureInitialized();
|
|
5314
|
+
const result = {
|
|
5315
|
+
resolved: 0,
|
|
5316
|
+
stillPending: 0,
|
|
5317
|
+
failed: 0,
|
|
5318
|
+
details: []
|
|
5319
|
+
};
|
|
5320
|
+
const stClient = this.deps.oracle.getStateTransitionClient?.();
|
|
5321
|
+
const trustBase = this.deps.oracle.getTrustBase?.();
|
|
5322
|
+
if (!stClient || !trustBase) return result;
|
|
5323
|
+
const signingService = await this.createSigningService();
|
|
5324
|
+
for (const [tokenId, token] of this.tokens) {
|
|
5325
|
+
if (token.status !== "submitted") continue;
|
|
5326
|
+
const pending2 = this.parsePendingFinalization(token.sdkData);
|
|
5327
|
+
if (!pending2) {
|
|
5328
|
+
result.stillPending++;
|
|
5329
|
+
continue;
|
|
5330
|
+
}
|
|
5331
|
+
if (pending2.type === "v5_bundle") {
|
|
5332
|
+
const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
|
|
5333
|
+
result.details.push({ tokenId, stage: pending2.stage, status: progress });
|
|
5334
|
+
if (progress === "resolved") result.resolved++;
|
|
5335
|
+
else if (progress === "failed") result.failed++;
|
|
5336
|
+
else result.stillPending++;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
if (result.resolved > 0 || result.failed > 0) {
|
|
5340
|
+
await this.save();
|
|
5341
|
+
}
|
|
5342
|
+
return result;
|
|
5343
|
+
}
|
|
5344
|
+
// ===========================================================================
|
|
5345
|
+
// Private - V5 Lazy Resolution Helpers
|
|
5346
|
+
// ===========================================================================
|
|
5347
|
+
/**
|
|
5348
|
+
* Process a single V5 token through its finalization stages with quick-timeout proof checks.
|
|
5349
|
+
*/
|
|
5350
|
+
async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
|
|
5351
|
+
const bundle = JSON.parse(pending2.bundleJson);
|
|
5352
|
+
pending2.attemptCount++;
|
|
5353
|
+
pending2.lastAttemptAt = Date.now();
|
|
5354
|
+
try {
|
|
5355
|
+
if (pending2.stage === "RECEIVED") {
|
|
5356
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5357
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5358
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5359
|
+
const mintResponse = await stClient.submitMintCommitment(mintCommitment);
|
|
5360
|
+
if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5361
|
+
throw new Error(`Mint submission failed: ${mintResponse.status}`);
|
|
5362
|
+
}
|
|
5363
|
+
pending2.stage = "MINT_SUBMITTED";
|
|
5364
|
+
this.updatePendingFinalization(token, pending2);
|
|
5365
|
+
}
|
|
5366
|
+
if (pending2.stage === "MINT_SUBMITTED") {
|
|
5367
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5368
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5369
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5370
|
+
const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
|
|
5371
|
+
if (!proof) {
|
|
5372
|
+
this.updatePendingFinalization(token, pending2);
|
|
5373
|
+
return "pending";
|
|
5374
|
+
}
|
|
5375
|
+
pending2.mintProofJson = JSON.stringify(proof);
|
|
5376
|
+
pending2.stage = "MINT_PROVEN";
|
|
5377
|
+
this.updatePendingFinalization(token, pending2);
|
|
5378
|
+
}
|
|
5379
|
+
if (pending2.stage === "MINT_PROVEN") {
|
|
5380
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5381
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5382
|
+
const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
|
|
5383
|
+
if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
|
|
5384
|
+
throw new Error(`Transfer submission failed: ${transferResponse.status}`);
|
|
5385
|
+
}
|
|
5386
|
+
pending2.stage = "TRANSFER_SUBMITTED";
|
|
5387
|
+
this.updatePendingFinalization(token, pending2);
|
|
5388
|
+
}
|
|
5389
|
+
if (pending2.stage === "TRANSFER_SUBMITTED") {
|
|
5390
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5391
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5392
|
+
const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
|
|
5393
|
+
if (!proof) {
|
|
5394
|
+
this.updatePendingFinalization(token, pending2);
|
|
5395
|
+
return "pending";
|
|
5396
|
+
}
|
|
5397
|
+
const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
|
|
5398
|
+
const confirmedToken = {
|
|
5399
|
+
id: token.id,
|
|
5400
|
+
coinId: token.coinId,
|
|
5401
|
+
symbol: token.symbol,
|
|
5402
|
+
name: token.name,
|
|
5403
|
+
decimals: token.decimals,
|
|
5404
|
+
iconUrl: token.iconUrl,
|
|
5405
|
+
amount: token.amount,
|
|
5406
|
+
status: "confirmed",
|
|
5407
|
+
createdAt: token.createdAt,
|
|
5408
|
+
updatedAt: Date.now(),
|
|
5409
|
+
sdkData: JSON.stringify(finalizedToken.toJSON())
|
|
5410
|
+
};
|
|
5411
|
+
this.tokens.set(tokenId, confirmedToken);
|
|
5412
|
+
await this.saveTokenToFileStorage(confirmedToken);
|
|
5413
|
+
await this.addToHistory({
|
|
5414
|
+
type: "RECEIVED",
|
|
5415
|
+
amount: confirmedToken.amount,
|
|
5416
|
+
coinId: confirmedToken.coinId,
|
|
5417
|
+
symbol: confirmedToken.symbol || "UNK",
|
|
5418
|
+
timestamp: Date.now(),
|
|
5419
|
+
senderPubkey: pending2.senderPubkey
|
|
5420
|
+
});
|
|
5421
|
+
this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
|
|
5422
|
+
return "resolved";
|
|
5423
|
+
}
|
|
5424
|
+
return "pending";
|
|
5425
|
+
} catch (error) {
|
|
5426
|
+
console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
|
|
5427
|
+
if (pending2.attemptCount > 50) {
|
|
5428
|
+
token.status = "invalid";
|
|
5429
|
+
token.updatedAt = Date.now();
|
|
5430
|
+
this.tokens.set(tokenId, token);
|
|
5431
|
+
return "failed";
|
|
5432
|
+
}
|
|
5433
|
+
this.updatePendingFinalization(token, pending2);
|
|
5434
|
+
return "pending";
|
|
5435
|
+
}
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
5438
|
+
* Non-blocking proof check with 500ms timeout.
|
|
5439
|
+
*/
|
|
5440
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5441
|
+
async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
|
|
5442
|
+
try {
|
|
5443
|
+
const proof = await Promise.race([
|
|
5444
|
+
waitInclusionProof5(trustBase, stClient, commitment),
|
|
5445
|
+
new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
|
|
5446
|
+
]);
|
|
5447
|
+
return proof;
|
|
5448
|
+
} catch {
|
|
5449
|
+
return null;
|
|
5450
|
+
}
|
|
5451
|
+
}
|
|
5452
|
+
/**
|
|
5453
|
+
* Perform V5 bundle finalization from stored bundle data and proofs.
|
|
5454
|
+
* Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
|
|
5455
|
+
*/
|
|
5456
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5457
|
+
async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
|
|
5458
|
+
const mintDataJson = JSON.parse(bundle.recipientMintData);
|
|
5459
|
+
const mintData = await MintTransactionData3.fromJSON(mintDataJson);
|
|
5460
|
+
const mintCommitment = await MintCommitment3.create(mintData);
|
|
5461
|
+
const mintProofJson = JSON.parse(pending2.mintProofJson);
|
|
5462
|
+
const mintProof = InclusionProof.fromJSON(mintProofJson);
|
|
5463
|
+
const mintTransaction = mintCommitment.toTransaction(mintProof);
|
|
5464
|
+
const tokenType = new TokenType3(fromHex4(bundle.tokenTypeHex));
|
|
5465
|
+
const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
|
|
5466
|
+
const tokenJson = {
|
|
5467
|
+
version: "2.0",
|
|
5468
|
+
state: senderMintedStateJson,
|
|
5469
|
+
genesis: mintTransaction.toJSON(),
|
|
5470
|
+
transactions: [],
|
|
5471
|
+
nametags: []
|
|
5472
|
+
};
|
|
5473
|
+
const mintedToken = await SdkToken2.fromJSON(tokenJson);
|
|
5474
|
+
const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
|
|
5475
|
+
const transferCommitment = await TransferCommitment4.fromJSON(transferCommitmentJson);
|
|
5476
|
+
const transferProof = await waitInclusionProof5(trustBase, stClient, transferCommitment);
|
|
5477
|
+
const transferTransaction = transferCommitment.toTransaction(transferProof);
|
|
5478
|
+
const transferSalt = fromHex4(bundle.transferSaltHex);
|
|
5479
|
+
const recipientPredicate = await UnmaskedPredicate5.create(
|
|
5480
|
+
mintData.tokenId,
|
|
5481
|
+
tokenType,
|
|
5482
|
+
signingService,
|
|
5483
|
+
HashAlgorithm5.SHA256,
|
|
5484
|
+
transferSalt
|
|
5485
|
+
);
|
|
5486
|
+
const recipientState = new TokenState5(recipientPredicate, null);
|
|
5487
|
+
let nametagTokens = [];
|
|
5488
|
+
const recipientAddressStr = bundle.recipientAddressJson;
|
|
5489
|
+
if (recipientAddressStr.startsWith("PROXY://")) {
|
|
5490
|
+
if (bundle.nametagTokenJson) {
|
|
5491
|
+
try {
|
|
5492
|
+
const nametagToken = await SdkToken2.fromJSON(JSON.parse(bundle.nametagTokenJson));
|
|
5493
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5494
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5495
|
+
if (proxy.address === recipientAddressStr) {
|
|
5496
|
+
nametagTokens = [nametagToken];
|
|
5497
|
+
}
|
|
5498
|
+
} catch {
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
if (nametagTokens.length === 0 && this.nametag?.token) {
|
|
5502
|
+
try {
|
|
5503
|
+
const nametagToken = await SdkToken2.fromJSON(this.nametag.token);
|
|
5504
|
+
const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
|
|
5505
|
+
const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
|
|
5506
|
+
if (proxy.address === recipientAddressStr) {
|
|
5507
|
+
nametagTokens = [nametagToken];
|
|
5508
|
+
}
|
|
5509
|
+
} catch {
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
}
|
|
5513
|
+
return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
|
|
5514
|
+
}
|
|
5515
|
+
/**
|
|
5516
|
+
* Parse pending finalization metadata from token's sdkData.
|
|
5517
|
+
*/
|
|
5518
|
+
parsePendingFinalization(sdkData) {
|
|
5519
|
+
if (!sdkData) return null;
|
|
5520
|
+
try {
|
|
5521
|
+
const data = JSON.parse(sdkData);
|
|
5522
|
+
if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
|
|
5523
|
+
return data._pendingFinalization;
|
|
5524
|
+
}
|
|
5525
|
+
return null;
|
|
5526
|
+
} catch {
|
|
5527
|
+
return null;
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
/**
|
|
5531
|
+
* Update pending finalization metadata in token's sdkData.
|
|
5532
|
+
* Creates a new token object since sdkData is readonly.
|
|
5533
|
+
*/
|
|
5534
|
+
updatePendingFinalization(token, pending2) {
|
|
5535
|
+
const updated = {
|
|
5536
|
+
id: token.id,
|
|
5537
|
+
coinId: token.coinId,
|
|
5538
|
+
symbol: token.symbol,
|
|
5539
|
+
name: token.name,
|
|
5540
|
+
decimals: token.decimals,
|
|
5541
|
+
iconUrl: token.iconUrl,
|
|
5542
|
+
amount: token.amount,
|
|
5543
|
+
status: token.status,
|
|
5544
|
+
createdAt: token.createdAt,
|
|
5545
|
+
updatedAt: Date.now(),
|
|
5546
|
+
sdkData: JSON.stringify({ _pendingFinalization: pending2 })
|
|
5547
|
+
};
|
|
5548
|
+
this.tokens.set(token.id, updated);
|
|
5549
|
+
}
|
|
5550
|
+
/**
|
|
5551
|
+
* Save pending V5 tokens to key-value storage.
|
|
5552
|
+
* These tokens can't be serialized to TXF format (no genesis/state),
|
|
5553
|
+
* so we persist them separately and restore on load().
|
|
5554
|
+
*/
|
|
5555
|
+
async savePendingV5Tokens() {
|
|
5556
|
+
const pendingTokens = [];
|
|
5557
|
+
for (const token of this.tokens.values()) {
|
|
5558
|
+
if (this.parsePendingFinalization(token.sdkData)) {
|
|
5559
|
+
pendingTokens.push(token);
|
|
5560
|
+
}
|
|
5561
|
+
}
|
|
5562
|
+
if (pendingTokens.length > 0) {
|
|
5563
|
+
await this.deps.storage.set(
|
|
5564
|
+
STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
|
|
5565
|
+
JSON.stringify(pendingTokens)
|
|
5566
|
+
);
|
|
5567
|
+
} else {
|
|
5568
|
+
await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
|
|
5569
|
+
}
|
|
5570
|
+
}
|
|
5571
|
+
/**
|
|
5572
|
+
* Load pending V5 tokens from key-value storage and merge into tokens map.
|
|
5573
|
+
* Called during load() to restore tokens that TXF format can't represent.
|
|
5574
|
+
*/
|
|
5575
|
+
async loadPendingV5Tokens() {
|
|
5576
|
+
const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
|
|
5577
|
+
if (!data) return;
|
|
5578
|
+
try {
|
|
5579
|
+
const pendingTokens = JSON.parse(data);
|
|
5580
|
+
for (const token of pendingTokens) {
|
|
5581
|
+
if (!this.tokens.has(token.id)) {
|
|
5582
|
+
this.tokens.set(token.id, token);
|
|
5583
|
+
}
|
|
5584
|
+
}
|
|
5585
|
+
if (pendingTokens.length > 0) {
|
|
5586
|
+
this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
|
|
5587
|
+
}
|
|
5588
|
+
} catch {
|
|
5589
|
+
}
|
|
5590
|
+
}
|
|
5591
|
+
// ===========================================================================
|
|
4919
5592
|
// Public API - Token Operations
|
|
4920
5593
|
// ===========================================================================
|
|
4921
5594
|
/**
|
|
4922
|
-
* Add a token
|
|
4923
|
-
*
|
|
4924
|
-
*
|
|
4925
|
-
*
|
|
5595
|
+
* Add a token to the wallet.
|
|
5596
|
+
*
|
|
5597
|
+
* Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
|
|
5598
|
+
* Duplicate detection:
|
|
5599
|
+
* - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
|
|
5600
|
+
* - **Exact duplicate** — rejected if a token with the same composite key already exists.
|
|
5601
|
+
* - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
|
|
5602
|
+
* the old state is archived and replaced with the incoming one.
|
|
5603
|
+
*
|
|
5604
|
+
* @param token - The token to add.
|
|
5605
|
+
* @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
|
|
5606
|
+
* @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
|
|
4926
5607
|
*/
|
|
4927
5608
|
async addToken(token, skipHistory = false) {
|
|
4928
5609
|
this.ensureInitialized();
|
|
@@ -4980,7 +5661,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
4980
5661
|
});
|
|
4981
5662
|
}
|
|
4982
5663
|
await this.save();
|
|
4983
|
-
|
|
5664
|
+
if (!this.parsePendingFinalization(token.sdkData)) {
|
|
5665
|
+
await this.saveTokenToFileStorage(token);
|
|
5666
|
+
}
|
|
4984
5667
|
this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
|
|
4985
5668
|
return true;
|
|
4986
5669
|
}
|
|
@@ -5037,6 +5720,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5037
5720
|
const data = fileData;
|
|
5038
5721
|
const tokenJson = data.token;
|
|
5039
5722
|
if (!tokenJson) continue;
|
|
5723
|
+
if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
|
|
5724
|
+
continue;
|
|
5725
|
+
}
|
|
5040
5726
|
let sdkTokenId;
|
|
5041
5727
|
if (typeof tokenJson === "object" && tokenJson !== null) {
|
|
5042
5728
|
const tokenObj = tokenJson;
|
|
@@ -5088,7 +5774,12 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5088
5774
|
this.log(`Loaded ${this.tokens.size} tokens from file storage`);
|
|
5089
5775
|
}
|
|
5090
5776
|
/**
|
|
5091
|
-
* Update an existing token
|
|
5777
|
+
* Update an existing token or add it if not found.
|
|
5778
|
+
*
|
|
5779
|
+
* Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
|
|
5780
|
+
* `token.id`. If no match is found, falls back to {@link addToken}.
|
|
5781
|
+
*
|
|
5782
|
+
* @param token - The token with updated data. Must include a valid `id`.
|
|
5092
5783
|
*/
|
|
5093
5784
|
async updateToken(token) {
|
|
5094
5785
|
this.ensureInitialized();
|
|
@@ -5112,7 +5803,15 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5112
5803
|
this.log(`Updated token ${token.id}`);
|
|
5113
5804
|
}
|
|
5114
5805
|
/**
|
|
5115
|
-
* Remove a token
|
|
5806
|
+
* Remove a token from the wallet.
|
|
5807
|
+
*
|
|
5808
|
+
* The token is archived first, then a tombstone `(tokenId, stateHash)` is
|
|
5809
|
+
* created to prevent re-addition via Nostr re-delivery. A `SENT` history
|
|
5810
|
+
* entry is created unless `skipHistory` is `true`.
|
|
5811
|
+
*
|
|
5812
|
+
* @param tokenId - Local UUID of the token to remove.
|
|
5813
|
+
* @param recipientNametag - Optional nametag of the transfer recipient (for history).
|
|
5814
|
+
* @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
|
|
5116
5815
|
*/
|
|
5117
5816
|
async removeToken(tokenId, recipientNametag, skipHistory = false) {
|
|
5118
5817
|
this.ensureInitialized();
|
|
@@ -5174,13 +5873,22 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5174
5873
|
// Public API - Tombstones
|
|
5175
5874
|
// ===========================================================================
|
|
5176
5875
|
/**
|
|
5177
|
-
* Get all
|
|
5876
|
+
* Get all tombstone entries.
|
|
5877
|
+
*
|
|
5878
|
+
* Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
|
|
5879
|
+
* token state from being re-added (e.g. via Nostr re-delivery).
|
|
5880
|
+
*
|
|
5881
|
+
* @returns A shallow copy of the tombstone array.
|
|
5178
5882
|
*/
|
|
5179
5883
|
getTombstones() {
|
|
5180
5884
|
return [...this.tombstones];
|
|
5181
5885
|
}
|
|
5182
5886
|
/**
|
|
5183
|
-
* Check
|
|
5887
|
+
* Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
|
|
5888
|
+
*
|
|
5889
|
+
* @param tokenId - The genesis token ID.
|
|
5890
|
+
* @param stateHash - The state hash of the token version to check.
|
|
5891
|
+
* @returns `true` if the exact combination has been tombstoned.
|
|
5184
5892
|
*/
|
|
5185
5893
|
isStateTombstoned(tokenId, stateHash) {
|
|
5186
5894
|
return this.tombstones.some(
|
|
@@ -5188,8 +5896,13 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5188
5896
|
);
|
|
5189
5897
|
}
|
|
5190
5898
|
/**
|
|
5191
|
-
* Merge remote
|
|
5192
|
-
*
|
|
5899
|
+
* Merge tombstones received from a remote sync source.
|
|
5900
|
+
*
|
|
5901
|
+
* Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
|
|
5902
|
+
* removed. The remote tombstones are then added to the local set (union merge).
|
|
5903
|
+
*
|
|
5904
|
+
* @param remoteTombstones - Tombstone entries from the remote source.
|
|
5905
|
+
* @returns Number of local tokens that were removed.
|
|
5193
5906
|
*/
|
|
5194
5907
|
async mergeTombstones(remoteTombstones) {
|
|
5195
5908
|
this.ensureInitialized();
|
|
@@ -5225,7 +5938,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5225
5938
|
return removedCount;
|
|
5226
5939
|
}
|
|
5227
5940
|
/**
|
|
5228
|
-
*
|
|
5941
|
+
* Remove tombstones older than `maxAge` and cap the list at 100 entries.
|
|
5942
|
+
*
|
|
5943
|
+
* @param maxAge - Maximum age in milliseconds (default: 30 days).
|
|
5229
5944
|
*/
|
|
5230
5945
|
async pruneTombstones(maxAge) {
|
|
5231
5946
|
const originalCount = this.tombstones.length;
|
|
@@ -5239,20 +5954,38 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5239
5954
|
// Public API - Archives
|
|
5240
5955
|
// ===========================================================================
|
|
5241
5956
|
/**
|
|
5242
|
-
* Get archived tokens
|
|
5957
|
+
* Get all archived (spent/superseded) tokens in TXF format.
|
|
5958
|
+
*
|
|
5959
|
+
* Archived tokens are kept for recovery and sync purposes. The map key is
|
|
5960
|
+
* the genesis token ID.
|
|
5961
|
+
*
|
|
5962
|
+
* @returns A shallow copy of the archived token map.
|
|
5243
5963
|
*/
|
|
5244
5964
|
getArchivedTokens() {
|
|
5245
5965
|
return new Map(this.archivedTokens);
|
|
5246
5966
|
}
|
|
5247
5967
|
/**
|
|
5248
|
-
* Get best archived version of a token
|
|
5968
|
+
* Get the best (most committed transactions) archived version of a token.
|
|
5969
|
+
*
|
|
5970
|
+
* Searches both archived and forked token maps and returns the version with
|
|
5971
|
+
* the highest number of committed transactions.
|
|
5972
|
+
*
|
|
5973
|
+
* @param tokenId - The genesis token ID to look up.
|
|
5974
|
+
* @returns The best TXF token version, or `null` if not found.
|
|
5249
5975
|
*/
|
|
5250
5976
|
getBestArchivedVersion(tokenId) {
|
|
5251
5977
|
return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
|
|
5252
5978
|
}
|
|
5253
5979
|
/**
|
|
5254
|
-
* Merge remote
|
|
5255
|
-
*
|
|
5980
|
+
* Merge archived tokens from a remote sync source.
|
|
5981
|
+
*
|
|
5982
|
+
* For each remote token:
|
|
5983
|
+
* - If missing locally, it is added.
|
|
5984
|
+
* - If the remote version is an incremental update of the local, it replaces it.
|
|
5985
|
+
* - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
|
|
5986
|
+
*
|
|
5987
|
+
* @param remoteArchived - Map of genesis token ID → TXF token from remote.
|
|
5988
|
+
* @returns Number of tokens that were updated or added locally.
|
|
5256
5989
|
*/
|
|
5257
5990
|
async mergeArchivedTokens(remoteArchived) {
|
|
5258
5991
|
let mergedCount = 0;
|
|
@@ -5275,7 +6008,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5275
6008
|
return mergedCount;
|
|
5276
6009
|
}
|
|
5277
6010
|
/**
|
|
5278
|
-
* Prune archived tokens
|
|
6011
|
+
* Prune archived tokens to keep at most `maxCount` entries.
|
|
6012
|
+
*
|
|
6013
|
+
* Oldest entries (by insertion order) are removed first.
|
|
6014
|
+
*
|
|
6015
|
+
* @param maxCount - Maximum number of archived tokens to retain (default: 100).
|
|
5279
6016
|
*/
|
|
5280
6017
|
async pruneArchivedTokens(maxCount = 100) {
|
|
5281
6018
|
if (this.archivedTokens.size <= maxCount) return;
|
|
@@ -5288,13 +6025,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5288
6025
|
// Public API - Forked Tokens
|
|
5289
6026
|
// ===========================================================================
|
|
5290
6027
|
/**
|
|
5291
|
-
* Get forked
|
|
6028
|
+
* Get all forked token versions.
|
|
6029
|
+
*
|
|
6030
|
+
* Forked tokens represent alternative histories detected during sync.
|
|
6031
|
+
* The map key is `{tokenId}_{stateHash}`.
|
|
6032
|
+
*
|
|
6033
|
+
* @returns A shallow copy of the forked tokens map.
|
|
5292
6034
|
*/
|
|
5293
6035
|
getForkedTokens() {
|
|
5294
6036
|
return new Map(this.forkedTokens);
|
|
5295
6037
|
}
|
|
5296
6038
|
/**
|
|
5297
|
-
* Store a forked token
|
|
6039
|
+
* Store a forked token version (alternative history).
|
|
6040
|
+
*
|
|
6041
|
+
* No-op if the exact `(tokenId, stateHash)` key already exists.
|
|
6042
|
+
*
|
|
6043
|
+
* @param tokenId - Genesis token ID.
|
|
6044
|
+
* @param stateHash - State hash of this forked version.
|
|
6045
|
+
* @param txfToken - The TXF token data to store.
|
|
5298
6046
|
*/
|
|
5299
6047
|
async storeForkedToken(tokenId, stateHash, txfToken) {
|
|
5300
6048
|
const key = `${tokenId}_${stateHash}`;
|
|
@@ -5304,8 +6052,10 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5304
6052
|
await this.save();
|
|
5305
6053
|
}
|
|
5306
6054
|
/**
|
|
5307
|
-
* Merge remote
|
|
5308
|
-
*
|
|
6055
|
+
* Merge forked tokens from a remote sync source. Only new keys are added.
|
|
6056
|
+
*
|
|
6057
|
+
* @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
|
|
6058
|
+
* @returns Number of new forked tokens added.
|
|
5309
6059
|
*/
|
|
5310
6060
|
async mergeForkedTokens(remoteForked) {
|
|
5311
6061
|
let addedCount = 0;
|
|
@@ -5321,7 +6071,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5321
6071
|
return addedCount;
|
|
5322
6072
|
}
|
|
5323
6073
|
/**
|
|
5324
|
-
* Prune forked tokens
|
|
6074
|
+
* Prune forked tokens to keep at most `maxCount` entries.
|
|
6075
|
+
*
|
|
6076
|
+
* @param maxCount - Maximum number of forked tokens to retain (default: 50).
|
|
5325
6077
|
*/
|
|
5326
6078
|
async pruneForkedTokens(maxCount = 50) {
|
|
5327
6079
|
if (this.forkedTokens.size <= maxCount) return;
|
|
@@ -5334,13 +6086,19 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5334
6086
|
// Public API - Transaction History
|
|
5335
6087
|
// ===========================================================================
|
|
5336
6088
|
/**
|
|
5337
|
-
* Get transaction history
|
|
6089
|
+
* Get the transaction history sorted newest-first.
|
|
6090
|
+
*
|
|
6091
|
+
* @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
|
|
5338
6092
|
*/
|
|
5339
6093
|
getHistory() {
|
|
5340
6094
|
return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
|
|
5341
6095
|
}
|
|
5342
6096
|
/**
|
|
5343
|
-
*
|
|
6097
|
+
* Append an entry to the transaction history.
|
|
6098
|
+
*
|
|
6099
|
+
* A unique `id` is auto-generated. The entry is immediately persisted to storage.
|
|
6100
|
+
*
|
|
6101
|
+
* @param entry - History entry fields (without `id`).
|
|
5344
6102
|
*/
|
|
5345
6103
|
async addToHistory(entry) {
|
|
5346
6104
|
this.ensureInitialized();
|
|
@@ -5358,7 +6116,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5358
6116
|
// Public API - Nametag
|
|
5359
6117
|
// ===========================================================================
|
|
5360
6118
|
/**
|
|
5361
|
-
* Set nametag for current identity
|
|
6119
|
+
* Set the nametag data for the current identity.
|
|
6120
|
+
*
|
|
6121
|
+
* Persists to both key-value storage and file storage (lottery compatibility).
|
|
6122
|
+
*
|
|
6123
|
+
* @param nametag - The nametag data including minted token JSON.
|
|
5362
6124
|
*/
|
|
5363
6125
|
async setNametag(nametag) {
|
|
5364
6126
|
this.ensureInitialized();
|
|
@@ -5368,19 +6130,23 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5368
6130
|
this.log(`Nametag set: ${nametag.name}`);
|
|
5369
6131
|
}
|
|
5370
6132
|
/**
|
|
5371
|
-
* Get nametag
|
|
6133
|
+
* Get the current nametag data.
|
|
6134
|
+
*
|
|
6135
|
+
* @returns The nametag data, or `null` if no nametag is set.
|
|
5372
6136
|
*/
|
|
5373
6137
|
getNametag() {
|
|
5374
6138
|
return this.nametag;
|
|
5375
6139
|
}
|
|
5376
6140
|
/**
|
|
5377
|
-
* Check
|
|
6141
|
+
* Check whether a nametag is currently set.
|
|
6142
|
+
*
|
|
6143
|
+
* @returns `true` if nametag data is present.
|
|
5378
6144
|
*/
|
|
5379
6145
|
hasNametag() {
|
|
5380
6146
|
return this.nametag !== null;
|
|
5381
6147
|
}
|
|
5382
6148
|
/**
|
|
5383
|
-
*
|
|
6149
|
+
* Remove the current nametag data from memory and storage.
|
|
5384
6150
|
*/
|
|
5385
6151
|
async clearNametag() {
|
|
5386
6152
|
this.ensureInitialized();
|
|
@@ -5474,9 +6240,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5474
6240
|
try {
|
|
5475
6241
|
const signingService = await this.createSigningService();
|
|
5476
6242
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5477
|
-
const { TokenType:
|
|
6243
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5478
6244
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5479
|
-
const tokenType = new
|
|
6245
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5480
6246
|
const addressRef = await UnmaskedPredicateReference4.create(
|
|
5481
6247
|
tokenType,
|
|
5482
6248
|
signingService.algorithm,
|
|
@@ -5537,11 +6303,27 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5537
6303
|
// Public API - Sync & Validate
|
|
5538
6304
|
// ===========================================================================
|
|
5539
6305
|
/**
|
|
5540
|
-
* Sync with all token storage providers (IPFS,
|
|
5541
|
-
*
|
|
6306
|
+
* Sync local token state with all configured token storage providers (IPFS, file, etc.).
|
|
6307
|
+
*
|
|
6308
|
+
* For each provider, the local data is packaged into TXF storage format, sent
|
|
6309
|
+
* to the provider's `sync()` method, and the merged result is applied locally.
|
|
6310
|
+
* Emits `sync:started`, `sync:completed`, and `sync:error` events.
|
|
6311
|
+
*
|
|
6312
|
+
* @returns Summary with counts of tokens added and removed during sync.
|
|
5542
6313
|
*/
|
|
5543
6314
|
async sync() {
|
|
5544
6315
|
this.ensureInitialized();
|
|
6316
|
+
if (this._syncInProgress) {
|
|
6317
|
+
return this._syncInProgress;
|
|
6318
|
+
}
|
|
6319
|
+
this._syncInProgress = this._doSync();
|
|
6320
|
+
try {
|
|
6321
|
+
return await this._syncInProgress;
|
|
6322
|
+
} finally {
|
|
6323
|
+
this._syncInProgress = null;
|
|
6324
|
+
}
|
|
6325
|
+
}
|
|
6326
|
+
async _doSync() {
|
|
5545
6327
|
this.deps.emitEvent("sync:started", { source: "payments" });
|
|
5546
6328
|
try {
|
|
5547
6329
|
const providers = this.getTokenStorageProviders();
|
|
@@ -5579,6 +6361,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5579
6361
|
});
|
|
5580
6362
|
}
|
|
5581
6363
|
}
|
|
6364
|
+
if (totalAdded > 0 || totalRemoved > 0) {
|
|
6365
|
+
await this.save();
|
|
6366
|
+
}
|
|
5582
6367
|
this.deps.emitEvent("sync:completed", {
|
|
5583
6368
|
source: "payments",
|
|
5584
6369
|
count: this.tokens.size
|
|
@@ -5592,6 +6377,66 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5592
6377
|
throw error;
|
|
5593
6378
|
}
|
|
5594
6379
|
}
|
|
6380
|
+
// ===========================================================================
|
|
6381
|
+
// Storage Event Subscription (Push-Based Sync)
|
|
6382
|
+
// ===========================================================================
|
|
6383
|
+
/**
|
|
6384
|
+
* Subscribe to 'storage:remote-updated' events from all token storage providers.
|
|
6385
|
+
* When a provider emits this event, a debounced sync is triggered.
|
|
6386
|
+
*/
|
|
6387
|
+
subscribeToStorageEvents() {
|
|
6388
|
+
this.unsubscribeStorageEvents();
|
|
6389
|
+
const providers = this.getTokenStorageProviders();
|
|
6390
|
+
for (const [providerId, provider] of providers) {
|
|
6391
|
+
if (provider.onEvent) {
|
|
6392
|
+
const unsub = provider.onEvent((event) => {
|
|
6393
|
+
if (event.type === "storage:remote-updated") {
|
|
6394
|
+
this.log("Remote update detected from provider", providerId, event.data);
|
|
6395
|
+
this.debouncedSyncFromRemoteUpdate(providerId, event.data);
|
|
6396
|
+
}
|
|
6397
|
+
});
|
|
6398
|
+
this.storageEventUnsubscribers.push(unsub);
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
/**
|
|
6403
|
+
* Unsubscribe from all storage provider events and clear debounce timer.
|
|
6404
|
+
*/
|
|
6405
|
+
unsubscribeStorageEvents() {
|
|
6406
|
+
for (const unsub of this.storageEventUnsubscribers) {
|
|
6407
|
+
unsub();
|
|
6408
|
+
}
|
|
6409
|
+
this.storageEventUnsubscribers = [];
|
|
6410
|
+
if (this.syncDebounceTimer) {
|
|
6411
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6412
|
+
this.syncDebounceTimer = null;
|
|
6413
|
+
}
|
|
6414
|
+
}
|
|
6415
|
+
/**
|
|
6416
|
+
* Debounced sync triggered by a storage:remote-updated event.
|
|
6417
|
+
* Waits 500ms to batch rapid updates, then performs sync.
|
|
6418
|
+
*/
|
|
6419
|
+
debouncedSyncFromRemoteUpdate(providerId, eventData) {
|
|
6420
|
+
if (this.syncDebounceTimer) {
|
|
6421
|
+
clearTimeout(this.syncDebounceTimer);
|
|
6422
|
+
}
|
|
6423
|
+
this.syncDebounceTimer = setTimeout(() => {
|
|
6424
|
+
this.syncDebounceTimer = null;
|
|
6425
|
+
this.sync().then((result) => {
|
|
6426
|
+
const data = eventData;
|
|
6427
|
+
this.deps?.emitEvent("sync:remote-update", {
|
|
6428
|
+
providerId,
|
|
6429
|
+
name: data?.name ?? "",
|
|
6430
|
+
sequence: data?.sequence ?? 0,
|
|
6431
|
+
cid: data?.cid ?? "",
|
|
6432
|
+
added: result.added,
|
|
6433
|
+
removed: result.removed
|
|
6434
|
+
});
|
|
6435
|
+
}).catch((err) => {
|
|
6436
|
+
this.log("Auto-sync from remote update failed:", err);
|
|
6437
|
+
});
|
|
6438
|
+
}, _PaymentsModule.SYNC_DEBOUNCE_MS);
|
|
6439
|
+
}
|
|
5595
6440
|
/**
|
|
5596
6441
|
* Get all active token storage providers
|
|
5597
6442
|
*/
|
|
@@ -5607,15 +6452,24 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5607
6452
|
return /* @__PURE__ */ new Map();
|
|
5608
6453
|
}
|
|
5609
6454
|
/**
|
|
5610
|
-
*
|
|
6455
|
+
* Replace the set of token storage providers at runtime.
|
|
6456
|
+
*
|
|
6457
|
+
* Use when providers are added or removed dynamically (e.g. IPFS node started).
|
|
6458
|
+
*
|
|
6459
|
+
* @param providers - New map of provider ID → TokenStorageProvider.
|
|
5611
6460
|
*/
|
|
5612
6461
|
updateTokenStorageProviders(providers) {
|
|
5613
6462
|
if (this.deps) {
|
|
5614
6463
|
this.deps.tokenStorageProviders = providers;
|
|
6464
|
+
this.subscribeToStorageEvents();
|
|
5615
6465
|
}
|
|
5616
6466
|
}
|
|
5617
6467
|
/**
|
|
5618
|
-
* Validate tokens
|
|
6468
|
+
* Validate all tokens against the aggregator (oracle provider).
|
|
6469
|
+
*
|
|
6470
|
+
* Tokens that fail validation or are detected as spent are marked `'invalid'`.
|
|
6471
|
+
*
|
|
6472
|
+
* @returns Object with arrays of valid and invalid tokens.
|
|
5619
6473
|
*/
|
|
5620
6474
|
async validate() {
|
|
5621
6475
|
this.ensureInitialized();
|
|
@@ -5636,7 +6490,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5636
6490
|
return { valid, invalid };
|
|
5637
6491
|
}
|
|
5638
6492
|
/**
|
|
5639
|
-
* Get pending transfers
|
|
6493
|
+
* Get all in-progress (pending) outgoing transfers.
|
|
6494
|
+
*
|
|
6495
|
+
* @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
|
|
5640
6496
|
*/
|
|
5641
6497
|
getPendingTransfers() {
|
|
5642
6498
|
return Array.from(this.pendingTransfers.values());
|
|
@@ -5700,9 +6556,9 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5700
6556
|
*/
|
|
5701
6557
|
async createDirectAddressFromPubkey(pubkeyHex) {
|
|
5702
6558
|
const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
|
|
5703
|
-
const { TokenType:
|
|
6559
|
+
const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
|
|
5704
6560
|
const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
5705
|
-
const tokenType = new
|
|
6561
|
+
const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
|
|
5706
6562
|
const pubkeyBytes = new Uint8Array(
|
|
5707
6563
|
pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
|
|
5708
6564
|
);
|
|
@@ -5914,7 +6770,8 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5914
6770
|
this.deps.emitEvent("transfer:confirmed", {
|
|
5915
6771
|
id: crypto.randomUUID(),
|
|
5916
6772
|
status: "completed",
|
|
5917
|
-
tokens: [finalizedToken]
|
|
6773
|
+
tokens: [finalizedToken],
|
|
6774
|
+
tokenTransfers: []
|
|
5918
6775
|
});
|
|
5919
6776
|
await this.addToHistory({
|
|
5920
6777
|
type: "RECEIVED",
|
|
@@ -5937,14 +6794,26 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5937
6794
|
async handleIncomingTransfer(transfer) {
|
|
5938
6795
|
try {
|
|
5939
6796
|
const payload = transfer.payload;
|
|
6797
|
+
let instantBundle = null;
|
|
5940
6798
|
if (isInstantSplitBundle(payload)) {
|
|
6799
|
+
instantBundle = payload;
|
|
6800
|
+
} else if (payload.token) {
|
|
6801
|
+
try {
|
|
6802
|
+
const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
|
|
6803
|
+
if (isInstantSplitBundle(inner)) {
|
|
6804
|
+
instantBundle = inner;
|
|
6805
|
+
}
|
|
6806
|
+
} catch {
|
|
6807
|
+
}
|
|
6808
|
+
}
|
|
6809
|
+
if (instantBundle) {
|
|
5941
6810
|
this.log("Processing INSTANT_SPLIT bundle...");
|
|
5942
6811
|
try {
|
|
5943
6812
|
if (!this.nametag) {
|
|
5944
6813
|
await this.loadNametagFromFileStorage();
|
|
5945
6814
|
}
|
|
5946
6815
|
const result = await this.processInstantSplitBundle(
|
|
5947
|
-
|
|
6816
|
+
instantBundle,
|
|
5948
6817
|
transfer.senderTransportPubkey
|
|
5949
6818
|
);
|
|
5950
6819
|
if (result.success) {
|
|
@@ -5957,6 +6826,11 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
5957
6826
|
}
|
|
5958
6827
|
return;
|
|
5959
6828
|
}
|
|
6829
|
+
if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
|
|
6830
|
+
this.log("Processing NOSTR-FIRST commitment-only transfer...");
|
|
6831
|
+
await this.handleCommitmentOnlyTransfer(transfer, payload);
|
|
6832
|
+
return;
|
|
6833
|
+
}
|
|
5960
6834
|
let tokenData;
|
|
5961
6835
|
let finalizedSdkToken = null;
|
|
5962
6836
|
if (payload.sourceToken && payload.transferTx) {
|
|
@@ -6112,6 +6986,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6112
6986
|
console.error(`[Payments] Failed to save to provider ${id}:`, err);
|
|
6113
6987
|
}
|
|
6114
6988
|
}
|
|
6989
|
+
await this.savePendingV5Tokens();
|
|
6115
6990
|
}
|
|
6116
6991
|
async saveToOutbox(transfer, recipient) {
|
|
6117
6992
|
const outbox = await this.loadOutbox();
|
|
@@ -6129,8 +7004,7 @@ var PaymentsModule = class _PaymentsModule {
|
|
|
6129
7004
|
}
|
|
6130
7005
|
async createStorageData() {
|
|
6131
7006
|
return await buildTxfStorageData(
|
|
6132
|
-
|
|
6133
|
-
// Empty - active tokens stored as token-xxx files
|
|
7007
|
+
Array.from(this.tokens.values()),
|
|
6134
7008
|
{
|
|
6135
7009
|
version: 1,
|
|
6136
7010
|
address: this.deps.identity.l1Address,
|
|
@@ -6315,7 +7189,7 @@ function createPaymentsModule(config) {
|
|
|
6315
7189
|
// modules/payments/TokenRecoveryService.ts
|
|
6316
7190
|
import { TokenId as TokenId4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenId";
|
|
6317
7191
|
import { TokenState as TokenState6 } from "@unicitylabs/state-transition-sdk/lib/token/TokenState";
|
|
6318
|
-
import { TokenType as
|
|
7192
|
+
import { TokenType as TokenType4 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
6319
7193
|
import { CoinId as CoinId5 } from "@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId";
|
|
6320
7194
|
import { HashAlgorithm as HashAlgorithm6 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
6321
7195
|
import { UnmaskedPredicate as UnmaskedPredicate6 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate";
|
|
@@ -7464,15 +8338,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
|
|
|
7464
8338
|
|
|
7465
8339
|
// core/Sphere.ts
|
|
7466
8340
|
import { SigningService as SigningService2 } from "@unicitylabs/state-transition-sdk/lib/sign/SigningService";
|
|
7467
|
-
import { TokenType as
|
|
8341
|
+
import { TokenType as TokenType5 } from "@unicitylabs/state-transition-sdk/lib/token/TokenType";
|
|
7468
8342
|
import { HashAlgorithm as HashAlgorithm7 } from "@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm";
|
|
7469
8343
|
import { UnmaskedPredicateReference as UnmaskedPredicateReference3 } from "@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference";
|
|
8344
|
+
import { normalizeNametag as normalizeNametag2, isPhoneNumber } from "@unicitylabs/nostr-js-sdk";
|
|
8345
|
+
function isValidNametag(nametag) {
|
|
8346
|
+
if (isPhoneNumber(nametag)) return true;
|
|
8347
|
+
return /^[a-z0-9_-]{3,20}$/.test(nametag);
|
|
8348
|
+
}
|
|
7470
8349
|
var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
|
|
7471
8350
|
async function deriveL3PredicateAddress(privateKey) {
|
|
7472
8351
|
const secret = Buffer.from(privateKey, "hex");
|
|
7473
8352
|
const signingService = await SigningService2.createFromSecret(secret);
|
|
7474
8353
|
const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
|
|
7475
|
-
const tokenType = new
|
|
8354
|
+
const tokenType = new TokenType5(tokenTypeBytes);
|
|
7476
8355
|
const predicateRef = UnmaskedPredicateReference3.create(
|
|
7477
8356
|
tokenType,
|
|
7478
8357
|
signingService.algorithm,
|
|
@@ -7638,8 +8517,8 @@ var Sphere = class _Sphere {
|
|
|
7638
8517
|
if (options.nametag) {
|
|
7639
8518
|
await sphere.registerNametag(options.nametag);
|
|
7640
8519
|
} else {
|
|
7641
|
-
await sphere.syncIdentityWithTransport();
|
|
7642
8520
|
await sphere.recoverNametagFromTransport();
|
|
8521
|
+
await sphere.syncIdentityWithTransport();
|
|
7643
8522
|
}
|
|
7644
8523
|
return sphere;
|
|
7645
8524
|
}
|
|
@@ -7686,9 +8565,14 @@ var Sphere = class _Sphere {
|
|
|
7686
8565
|
if (!options.mnemonic && !options.masterKey) {
|
|
7687
8566
|
throw new Error("Either mnemonic or masterKey is required");
|
|
7688
8567
|
}
|
|
8568
|
+
console.log("[Sphere.import] Starting import...");
|
|
8569
|
+
console.log("[Sphere.import] Clearing existing wallet data...");
|
|
7689
8570
|
await _Sphere.clear({ storage: options.storage, tokenStorage: options.tokenStorage });
|
|
8571
|
+
console.log("[Sphere.import] Clear done");
|
|
7690
8572
|
if (!options.storage.isConnected()) {
|
|
8573
|
+
console.log("[Sphere.import] Reconnecting storage...");
|
|
7691
8574
|
await options.storage.connect();
|
|
8575
|
+
console.log("[Sphere.import] Storage reconnected");
|
|
7692
8576
|
}
|
|
7693
8577
|
const sphere = new _Sphere(
|
|
7694
8578
|
options.storage,
|
|
@@ -7702,9 +8586,12 @@ var Sphere = class _Sphere {
|
|
|
7702
8586
|
if (!_Sphere.validateMnemonic(options.mnemonic)) {
|
|
7703
8587
|
throw new Error("Invalid mnemonic");
|
|
7704
8588
|
}
|
|
8589
|
+
console.log("[Sphere.import] Storing mnemonic...");
|
|
7705
8590
|
await sphere.storeMnemonic(options.mnemonic, options.derivationPath, options.basePath);
|
|
8591
|
+
console.log("[Sphere.import] Initializing identity from mnemonic...");
|
|
7706
8592
|
await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
|
|
7707
8593
|
} else if (options.masterKey) {
|
|
8594
|
+
console.log("[Sphere.import] Storing master key...");
|
|
7708
8595
|
await sphere.storeMasterKey(
|
|
7709
8596
|
options.masterKey,
|
|
7710
8597
|
options.chainCode,
|
|
@@ -7712,24 +8599,43 @@ var Sphere = class _Sphere {
|
|
|
7712
8599
|
options.basePath,
|
|
7713
8600
|
options.derivationMode
|
|
7714
8601
|
);
|
|
8602
|
+
console.log("[Sphere.import] Initializing identity from master key...");
|
|
7715
8603
|
await sphere.initializeIdentityFromMasterKey(
|
|
7716
8604
|
options.masterKey,
|
|
7717
8605
|
options.chainCode,
|
|
7718
8606
|
options.derivationPath
|
|
7719
8607
|
);
|
|
7720
8608
|
}
|
|
8609
|
+
console.log("[Sphere.import] Initializing providers...");
|
|
7721
8610
|
await sphere.initializeProviders();
|
|
8611
|
+
console.log("[Sphere.import] Providers initialized. Initializing modules...");
|
|
7722
8612
|
await sphere.initializeModules();
|
|
8613
|
+
console.log("[Sphere.import] Modules initialized");
|
|
7723
8614
|
if (!options.nametag) {
|
|
8615
|
+
console.log("[Sphere.import] Recovering nametag from transport...");
|
|
7724
8616
|
await sphere.recoverNametagFromTransport();
|
|
8617
|
+
console.log("[Sphere.import] Nametag recovery done");
|
|
8618
|
+
await sphere.syncIdentityWithTransport();
|
|
7725
8619
|
}
|
|
8620
|
+
console.log("[Sphere.import] Finalizing wallet creation...");
|
|
7726
8621
|
await sphere.finalizeWalletCreation();
|
|
7727
8622
|
sphere._initialized = true;
|
|
7728
8623
|
_Sphere.instance = sphere;
|
|
8624
|
+
console.log("[Sphere.import] Tracking address 0...");
|
|
7729
8625
|
await sphere.ensureAddressTracked(0);
|
|
7730
8626
|
if (options.nametag) {
|
|
8627
|
+
console.log("[Sphere.import] Registering nametag...");
|
|
7731
8628
|
await sphere.registerNametag(options.nametag);
|
|
7732
8629
|
}
|
|
8630
|
+
if (sphere._tokenStorageProviders.size > 0) {
|
|
8631
|
+
try {
|
|
8632
|
+
const syncResult = await sphere._payments.sync();
|
|
8633
|
+
console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
|
|
8634
|
+
} catch (err) {
|
|
8635
|
+
console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
|
|
8636
|
+
}
|
|
8637
|
+
}
|
|
8638
|
+
console.log("[Sphere.import] Import complete");
|
|
7733
8639
|
return sphere;
|
|
7734
8640
|
}
|
|
7735
8641
|
/**
|
|
@@ -7754,6 +8660,10 @@ var Sphere = class _Sphere {
|
|
|
7754
8660
|
static async clear(storageOrOptions) {
|
|
7755
8661
|
const storage = "get" in storageOrOptions ? storageOrOptions : storageOrOptions.storage;
|
|
7756
8662
|
const tokenStorage = "get" in storageOrOptions ? void 0 : storageOrOptions.tokenStorage;
|
|
8663
|
+
if (!storage.isConnected()) {
|
|
8664
|
+
await storage.connect();
|
|
8665
|
+
}
|
|
8666
|
+
console.log("[Sphere.clear] Removing storage keys...");
|
|
7757
8667
|
await storage.remove(STORAGE_KEYS_GLOBAL.MNEMONIC);
|
|
7758
8668
|
await storage.remove(STORAGE_KEYS_GLOBAL.MASTER_KEY);
|
|
7759
8669
|
await storage.remove(STORAGE_KEYS_GLOBAL.CHAIN_CODE);
|
|
@@ -7766,12 +8676,30 @@ var Sphere = class _Sphere {
|
|
|
7766
8676
|
await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
|
|
7767
8677
|
await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
|
|
7768
8678
|
await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
|
|
8679
|
+
console.log("[Sphere.clear] Storage keys removed");
|
|
7769
8680
|
if (tokenStorage?.clear) {
|
|
7770
|
-
|
|
8681
|
+
console.log("[Sphere.clear] Clearing token storage...");
|
|
8682
|
+
try {
|
|
8683
|
+
await Promise.race([
|
|
8684
|
+
tokenStorage.clear(),
|
|
8685
|
+
new Promise(
|
|
8686
|
+
(_, reject) => setTimeout(() => reject(new Error("tokenStorage.clear() timed out after 2s")), 2e3)
|
|
8687
|
+
)
|
|
8688
|
+
]);
|
|
8689
|
+
console.log("[Sphere.clear] Token storage cleared");
|
|
8690
|
+
} catch (err) {
|
|
8691
|
+
console.warn("[Sphere.clear] Token storage clear failed/timed out:", err);
|
|
8692
|
+
}
|
|
7771
8693
|
}
|
|
8694
|
+
console.log("[Sphere.clear] Destroying vesting classifier...");
|
|
7772
8695
|
await vestingClassifier.destroy();
|
|
8696
|
+
console.log("[Sphere.clear] Vesting classifier destroyed");
|
|
7773
8697
|
if (_Sphere.instance) {
|
|
8698
|
+
console.log("[Sphere.clear] Destroying Sphere instance...");
|
|
7774
8699
|
await _Sphere.instance.destroy();
|
|
8700
|
+
console.log("[Sphere.clear] Sphere instance destroyed");
|
|
8701
|
+
} else {
|
|
8702
|
+
console.log("[Sphere.clear] No Sphere instance to destroy");
|
|
7775
8703
|
}
|
|
7776
8704
|
}
|
|
7777
8705
|
/**
|
|
@@ -8152,7 +9080,8 @@ var Sphere = class _Sphere {
|
|
|
8152
9080
|
storage: options.storage,
|
|
8153
9081
|
transport: options.transport,
|
|
8154
9082
|
oracle: options.oracle,
|
|
8155
|
-
tokenStorage: options.tokenStorage
|
|
9083
|
+
tokenStorage: options.tokenStorage,
|
|
9084
|
+
l1: options.l1
|
|
8156
9085
|
});
|
|
8157
9086
|
return { success: true, mnemonic };
|
|
8158
9087
|
}
|
|
@@ -8165,7 +9094,8 @@ var Sphere = class _Sphere {
|
|
|
8165
9094
|
storage: options.storage,
|
|
8166
9095
|
transport: options.transport,
|
|
8167
9096
|
oracle: options.oracle,
|
|
8168
|
-
tokenStorage: options.tokenStorage
|
|
9097
|
+
tokenStorage: options.tokenStorage,
|
|
9098
|
+
l1: options.l1
|
|
8169
9099
|
});
|
|
8170
9100
|
return { success: true };
|
|
8171
9101
|
}
|
|
@@ -8224,7 +9154,8 @@ var Sphere = class _Sphere {
|
|
|
8224
9154
|
transport: options.transport,
|
|
8225
9155
|
oracle: options.oracle,
|
|
8226
9156
|
tokenStorage: options.tokenStorage,
|
|
8227
|
-
nametag: options.nametag
|
|
9157
|
+
nametag: options.nametag,
|
|
9158
|
+
l1: options.l1
|
|
8228
9159
|
});
|
|
8229
9160
|
return { success: true, sphere, mnemonic };
|
|
8230
9161
|
}
|
|
@@ -8253,7 +9184,8 @@ var Sphere = class _Sphere {
|
|
|
8253
9184
|
transport: options.transport,
|
|
8254
9185
|
oracle: options.oracle,
|
|
8255
9186
|
tokenStorage: options.tokenStorage,
|
|
8256
|
-
nametag: options.nametag
|
|
9187
|
+
nametag: options.nametag,
|
|
9188
|
+
l1: options.l1
|
|
8257
9189
|
});
|
|
8258
9190
|
return { success: true, sphere };
|
|
8259
9191
|
}
|
|
@@ -8284,7 +9216,8 @@ var Sphere = class _Sphere {
|
|
|
8284
9216
|
transport: options.transport,
|
|
8285
9217
|
oracle: options.oracle,
|
|
8286
9218
|
tokenStorage: options.tokenStorage,
|
|
8287
|
-
nametag: options.nametag
|
|
9219
|
+
nametag: options.nametag,
|
|
9220
|
+
l1: options.l1
|
|
8288
9221
|
});
|
|
8289
9222
|
return { success: true, sphere };
|
|
8290
9223
|
}
|
|
@@ -8303,7 +9236,8 @@ var Sphere = class _Sphere {
|
|
|
8303
9236
|
storage: options.storage,
|
|
8304
9237
|
transport: options.transport,
|
|
8305
9238
|
oracle: options.oracle,
|
|
8306
|
-
tokenStorage: options.tokenStorage
|
|
9239
|
+
tokenStorage: options.tokenStorage,
|
|
9240
|
+
l1: options.l1
|
|
8307
9241
|
});
|
|
8308
9242
|
if (result.success) {
|
|
8309
9243
|
const sphere2 = _Sphere.getInstance();
|
|
@@ -8352,7 +9286,8 @@ var Sphere = class _Sphere {
|
|
|
8352
9286
|
transport: options.transport,
|
|
8353
9287
|
oracle: options.oracle,
|
|
8354
9288
|
tokenStorage: options.tokenStorage,
|
|
8355
|
-
nametag: options.nametag
|
|
9289
|
+
nametag: options.nametag,
|
|
9290
|
+
l1: options.l1
|
|
8356
9291
|
});
|
|
8357
9292
|
return { success: true, sphere: sphere2, mnemonic };
|
|
8358
9293
|
}
|
|
@@ -8365,7 +9300,8 @@ var Sphere = class _Sphere {
|
|
|
8365
9300
|
transport: options.transport,
|
|
8366
9301
|
oracle: options.oracle,
|
|
8367
9302
|
tokenStorage: options.tokenStorage,
|
|
8368
|
-
nametag: options.nametag
|
|
9303
|
+
nametag: options.nametag,
|
|
9304
|
+
l1: options.l1
|
|
8369
9305
|
});
|
|
8370
9306
|
return { success: true, sphere };
|
|
8371
9307
|
}
|
|
@@ -8569,9 +9505,9 @@ var Sphere = class _Sphere {
|
|
|
8569
9505
|
if (index < 0) {
|
|
8570
9506
|
throw new Error("Address index must be non-negative");
|
|
8571
9507
|
}
|
|
8572
|
-
const newNametag = options?.nametag
|
|
8573
|
-
if (newNametag && !
|
|
8574
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9508
|
+
const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
|
|
9509
|
+
if (newNametag && !isValidNametag(newNametag)) {
|
|
9510
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8575
9511
|
}
|
|
8576
9512
|
const addressInfo = this.deriveAddress(index, false);
|
|
8577
9513
|
const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
|
|
@@ -8955,9 +9891,9 @@ var Sphere = class _Sphere {
|
|
|
8955
9891
|
*/
|
|
8956
9892
|
async registerNametag(nametag) {
|
|
8957
9893
|
this.ensureReady();
|
|
8958
|
-
const cleanNametag =
|
|
8959
|
-
if (!
|
|
8960
|
-
throw new Error("Invalid nametag format. Use alphanumeric
|
|
9894
|
+
const cleanNametag = this.cleanNametag(nametag);
|
|
9895
|
+
if (!isValidNametag(cleanNametag)) {
|
|
9896
|
+
throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
|
|
8961
9897
|
}
|
|
8962
9898
|
if (this._identity?.nametag) {
|
|
8963
9899
|
throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
|
|
@@ -9228,46 +10164,49 @@ var Sphere = class _Sphere {
|
|
|
9228
10164
|
if (this._identity?.nametag) {
|
|
9229
10165
|
return;
|
|
9230
10166
|
}
|
|
9231
|
-
|
|
10167
|
+
let recoveredNametag = null;
|
|
10168
|
+
if (this._transport.recoverNametag) {
|
|
10169
|
+
try {
|
|
10170
|
+
recoveredNametag = await this._transport.recoverNametag();
|
|
10171
|
+
} catch {
|
|
10172
|
+
}
|
|
10173
|
+
}
|
|
10174
|
+
if (!recoveredNametag && this._transport.resolveAddressInfo && this._identity?.l1Address) {
|
|
10175
|
+
try {
|
|
10176
|
+
const info = await this._transport.resolveAddressInfo(this._identity.l1Address);
|
|
10177
|
+
if (info?.nametag) {
|
|
10178
|
+
recoveredNametag = info.nametag;
|
|
10179
|
+
}
|
|
10180
|
+
} catch {
|
|
10181
|
+
}
|
|
10182
|
+
}
|
|
10183
|
+
if (!recoveredNametag) {
|
|
9232
10184
|
return;
|
|
9233
10185
|
}
|
|
9234
10186
|
try {
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
9244
|
-
nametags = /* @__PURE__ */ new Map();
|
|
9245
|
-
this._addressNametags.set(entry.addressId, nametags);
|
|
9246
|
-
}
|
|
9247
|
-
const nextIndex = nametags.size;
|
|
9248
|
-
nametags.set(nextIndex, recoveredNametag);
|
|
9249
|
-
await this.persistAddressNametags();
|
|
9250
|
-
if (this._transport.publishIdentityBinding) {
|
|
9251
|
-
await this._transport.publishIdentityBinding(
|
|
9252
|
-
this._identity.chainPubkey,
|
|
9253
|
-
this._identity.l1Address,
|
|
9254
|
-
this._identity.directAddress || "",
|
|
9255
|
-
recoveredNametag
|
|
9256
|
-
);
|
|
9257
|
-
}
|
|
9258
|
-
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
10187
|
+
if (this._identity) {
|
|
10188
|
+
this._identity.nametag = recoveredNametag;
|
|
10189
|
+
await this._updateCachedProxyAddress();
|
|
10190
|
+
}
|
|
10191
|
+
const entry = await this.ensureAddressTracked(this._currentAddressIndex);
|
|
10192
|
+
let nametags = this._addressNametags.get(entry.addressId);
|
|
10193
|
+
if (!nametags) {
|
|
10194
|
+
nametags = /* @__PURE__ */ new Map();
|
|
10195
|
+
this._addressNametags.set(entry.addressId, nametags);
|
|
9259
10196
|
}
|
|
10197
|
+
const nextIndex = nametags.size;
|
|
10198
|
+
nametags.set(nextIndex, recoveredNametag);
|
|
10199
|
+
await this.persistAddressNametags();
|
|
10200
|
+
this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
|
|
9260
10201
|
} catch {
|
|
9261
10202
|
}
|
|
9262
10203
|
}
|
|
9263
10204
|
/**
|
|
9264
|
-
*
|
|
10205
|
+
* Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
|
|
9265
10206
|
*/
|
|
9266
|
-
|
|
9267
|
-
const
|
|
9268
|
-
|
|
9269
|
-
);
|
|
9270
|
-
return pattern.test(nametag);
|
|
10207
|
+
cleanNametag(raw) {
|
|
10208
|
+
const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
|
|
10209
|
+
return normalizeNametag2(stripped);
|
|
9271
10210
|
}
|
|
9272
10211
|
// ===========================================================================
|
|
9273
10212
|
// Public Methods - Lifecycle
|
|
@@ -9465,8 +10404,12 @@ var Sphere = class _Sphere {
|
|
|
9465
10404
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9466
10405
|
provider.setIdentity(this._identity);
|
|
9467
10406
|
}
|
|
9468
|
-
|
|
9469
|
-
|
|
10407
|
+
if (!this._storage.isConnected()) {
|
|
10408
|
+
await this._storage.connect();
|
|
10409
|
+
}
|
|
10410
|
+
if (!this._transport.isConnected()) {
|
|
10411
|
+
await this._transport.connect();
|
|
10412
|
+
}
|
|
9470
10413
|
await this._oracle.initialize();
|
|
9471
10414
|
for (const provider of this._tokenStorageProviders.values()) {
|
|
9472
10415
|
await provider.initialize();
|
|
@@ -9623,6 +10566,7 @@ export {
|
|
|
9623
10566
|
initSphere,
|
|
9624
10567
|
isEncryptedData,
|
|
9625
10568
|
isValidBech32,
|
|
10569
|
+
isValidNametag,
|
|
9626
10570
|
isValidPrivateKey,
|
|
9627
10571
|
loadSphere,
|
|
9628
10572
|
mnemonicToEntropy2 as mnemonicToEntropy,
|