@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/dist/index.cjs CHANGED
@@ -498,6 +498,7 @@ __export(index_exports, {
498
498
  TokenRegistry: () => TokenRegistry,
499
499
  TokenValidator: () => TokenValidator,
500
500
  archivedKeyFromTokenId: () => archivedKeyFromTokenId,
501
+ areSameNametag: () => import_nostr_js_sdk3.areSameNametag,
501
502
  base58Decode: () => base58Decode,
502
503
  base58Encode: () => base58Encode2,
503
504
  buildTxfStorageData: () => buildTxfStorageData,
@@ -545,6 +546,7 @@ __export(index_exports, {
545
546
  hasUncommittedTransactions: () => hasUncommittedTransactions,
546
547
  hasValidTxfData: () => hasValidTxfData,
547
548
  hash160: () => hash160,
549
+ hashNametag: () => import_nostr_js_sdk3.hashNametag,
548
550
  hexToBytes: () => hexToBytes,
549
551
  identityFromMnemonicSync: () => identityFromMnemonicSync,
550
552
  initSphere: () => initSphere,
@@ -556,10 +558,12 @@ __export(index_exports, {
556
558
  isKnownToken: () => isKnownToken,
557
559
  isPaymentSessionTerminal: () => isPaymentSessionTerminal,
558
560
  isPaymentSessionTimedOut: () => isPaymentSessionTimedOut,
561
+ isPhoneNumber: () => import_nostr_js_sdk3.isPhoneNumber,
559
562
  isSQLiteDatabase: () => isSQLiteDatabase,
560
563
  isTextWalletEncrypted: () => isTextWalletEncrypted,
561
564
  isTokenKey: () => isTokenKey,
562
565
  isValidBech32: () => isValidBech32,
566
+ isValidNametag: () => isValidNametag,
563
567
  isValidPrivateKey: () => isValidPrivateKey,
564
568
  isValidTokenId: () => isValidTokenId,
565
569
  isWalletDatEncrypted: () => isWalletDatEncrypted,
@@ -567,6 +571,7 @@ __export(index_exports, {
567
571
  keyFromTokenId: () => keyFromTokenId,
568
572
  loadSphere: () => loadSphere,
569
573
  mnemonicToSeedSync: () => mnemonicToSeedSync2,
574
+ normalizeNametag: () => import_nostr_js_sdk3.normalizeNametag,
570
575
  normalizeSdkTokenToStorage: () => normalizeSdkTokenToStorage,
571
576
  objectToTxf: () => objectToTxf,
572
577
  parseAndDecryptWalletDat: () => parseAndDecryptWalletDat,
@@ -1754,7 +1759,7 @@ var L1PaymentsModule = class {
1754
1759
  _transport;
1755
1760
  constructor(config) {
1756
1761
  this._config = {
1757
- electrumUrl: config?.electrumUrl ?? "wss://fulcrum.alpha.unicity.network:50004",
1762
+ electrumUrl: config?.electrumUrl ?? "wss://fulcrum.unicity.network:50004",
1758
1763
  network: config?.network ?? "mainnet",
1759
1764
  defaultFeeRate: config?.defaultFeeRate ?? 10,
1760
1765
  enableVesting: config?.enableVesting ?? true
@@ -1786,10 +1791,17 @@ var L1PaymentsModule = class {
1786
1791
  });
1787
1792
  }
1788
1793
  }
1789
- if (this._config.electrumUrl) {
1794
+ this._initialized = true;
1795
+ }
1796
+ /**
1797
+ * Ensure the Fulcrum WebSocket is connected. Called lazily before any
1798
+ * operation that needs the network. If the singleton is already connected
1799
+ * (e.g. by the address scanner), this is a no-op.
1800
+ */
1801
+ async ensureConnected() {
1802
+ if (!isWebSocketConnected() && this._config.electrumUrl) {
1790
1803
  await connect(this._config.electrumUrl);
1791
1804
  }
1792
- this._initialized = true;
1793
1805
  }
1794
1806
  destroy() {
1795
1807
  if (isWebSocketConnected()) {
@@ -1847,6 +1859,7 @@ var L1PaymentsModule = class {
1847
1859
  }
1848
1860
  async send(request) {
1849
1861
  this.ensureInitialized();
1862
+ await this.ensureConnected();
1850
1863
  if (!this._wallet || !this._identity) {
1851
1864
  return { success: false, error: "No wallet available" };
1852
1865
  }
@@ -1881,6 +1894,7 @@ var L1PaymentsModule = class {
1881
1894
  }
1882
1895
  async getBalance() {
1883
1896
  this.ensureInitialized();
1897
+ await this.ensureConnected();
1884
1898
  const addresses = this._getWatchedAddresses();
1885
1899
  let totalAlpha = 0;
1886
1900
  let vestedSats = BigInt(0);
@@ -1912,6 +1926,7 @@ var L1PaymentsModule = class {
1912
1926
  }
1913
1927
  async getUtxos() {
1914
1928
  this.ensureInitialized();
1929
+ await this.ensureConnected();
1915
1930
  const result = [];
1916
1931
  const currentHeight = await getCurrentBlockHeight();
1917
1932
  const allUtxos = await this._getAllUtxos();
@@ -1947,42 +1962,73 @@ var L1PaymentsModule = class {
1947
1962
  return result;
1948
1963
  }
1949
1964
  async getHistory(limit) {
1965
+ await this.ensureConnected();
1950
1966
  this.ensureInitialized();
1951
1967
  const addresses = this._getWatchedAddresses();
1952
1968
  const transactions = [];
1953
1969
  const seenTxids = /* @__PURE__ */ new Set();
1954
1970
  const currentHeight = await getCurrentBlockHeight();
1971
+ const txCache = /* @__PURE__ */ new Map();
1972
+ const fetchTx = async (txid) => {
1973
+ if (txCache.has(txid)) return txCache.get(txid);
1974
+ const detail = await getTransaction(txid);
1975
+ txCache.set(txid, detail);
1976
+ return detail;
1977
+ };
1978
+ const addressSet = new Set(addresses.map((a) => a.toLowerCase()));
1955
1979
  for (const address of addresses) {
1956
1980
  const history = await getTransactionHistory(address);
1957
1981
  for (const item of history) {
1958
1982
  if (seenTxids.has(item.tx_hash)) continue;
1959
1983
  seenTxids.add(item.tx_hash);
1960
- const tx = await getTransaction(item.tx_hash);
1984
+ const tx = await fetchTx(item.tx_hash);
1961
1985
  if (!tx) continue;
1962
- const isSend = tx.vin?.some(
1963
- (vin) => addresses.includes(vin.txid ?? "")
1964
- );
1965
- let amount = "0";
1986
+ let isSend = false;
1987
+ for (const vin of tx.vin ?? []) {
1988
+ if (!vin.txid) continue;
1989
+ const prevTx = await fetchTx(vin.txid);
1990
+ if (prevTx?.vout?.[vin.vout]) {
1991
+ const prevOut = prevTx.vout[vin.vout];
1992
+ const prevAddrs = [
1993
+ ...prevOut.scriptPubKey?.addresses ?? [],
1994
+ ...prevOut.scriptPubKey?.address ? [prevOut.scriptPubKey.address] : []
1995
+ ];
1996
+ if (prevAddrs.some((a) => addressSet.has(a.toLowerCase()))) {
1997
+ isSend = true;
1998
+ break;
1999
+ }
2000
+ }
2001
+ }
2002
+ let amountToUs = 0;
2003
+ let amountToOthers = 0;
1966
2004
  let txAddress = address;
2005
+ let externalAddress = "";
1967
2006
  if (tx.vout) {
1968
2007
  for (const vout of tx.vout) {
1969
- const voutAddresses = vout.scriptPubKey?.addresses ?? [];
1970
- if (vout.scriptPubKey?.address) {
1971
- voutAddresses.push(vout.scriptPubKey.address);
1972
- }
1973
- const matchedAddr = voutAddresses.find((a) => addresses.includes(a));
1974
- if (matchedAddr) {
1975
- amount = Math.floor((vout.value ?? 0) * 1e8).toString();
1976
- txAddress = matchedAddr;
1977
- break;
2008
+ const voutAddresses = [
2009
+ ...vout.scriptPubKey?.addresses ?? [],
2010
+ ...vout.scriptPubKey?.address ? [vout.scriptPubKey.address] : []
2011
+ ];
2012
+ const isOurs = voutAddresses.some((a) => addressSet.has(a.toLowerCase()));
2013
+ const valueSats = Math.floor((vout.value ?? 0) * 1e8);
2014
+ if (isOurs) {
2015
+ amountToUs += valueSats;
2016
+ if (!txAddress) txAddress = voutAddresses[0];
2017
+ } else {
2018
+ amountToOthers += valueSats;
2019
+ if (!externalAddress && voutAddresses.length > 0) {
2020
+ externalAddress = voutAddresses[0];
2021
+ }
1978
2022
  }
1979
2023
  }
1980
2024
  }
2025
+ const amount = isSend ? amountToOthers.toString() : amountToUs.toString();
2026
+ const displayAddress = isSend ? externalAddress || txAddress : txAddress;
1981
2027
  transactions.push({
1982
2028
  txid: item.tx_hash,
1983
2029
  type: isSend ? "send" : "receive",
1984
2030
  amount,
1985
- address: txAddress,
2031
+ address: displayAddress,
1986
2032
  confirmations: item.height > 0 ? currentHeight - item.height : 0,
1987
2033
  timestamp: tx.time ? tx.time * 1e3 : Date.now(),
1988
2034
  blockHeight: item.height > 0 ? item.height : void 0
@@ -1994,6 +2040,7 @@ var L1PaymentsModule = class {
1994
2040
  }
1995
2041
  async getTransaction(txid) {
1996
2042
  this.ensureInitialized();
2043
+ await this.ensureConnected();
1997
2044
  const tx = await getTransaction(txid);
1998
2045
  if (!tx) return null;
1999
2046
  const addresses = this._getWatchedAddresses();
@@ -2029,6 +2076,7 @@ var L1PaymentsModule = class {
2029
2076
  }
2030
2077
  async estimateFee(to, amount) {
2031
2078
  this.ensureInitialized();
2079
+ await this.ensureConnected();
2032
2080
  if (!this._wallet) {
2033
2081
  return { fee: "0", feeRate: this._config.defaultFeeRate ?? 10 };
2034
2082
  }
@@ -2371,6 +2419,7 @@ var import_MintCommitment = require("@unicitylabs/state-transition-sdk/lib/trans
2371
2419
  var import_HashAlgorithm2 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
2372
2420
  var import_UnmaskedPredicate2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
2373
2421
  var import_InclusionProofUtils2 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
2422
+ var import_nostr_js_sdk = require("@unicitylabs/nostr-js-sdk");
2374
2423
  var UNICITY_TOKEN_TYPE_HEX = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
2375
2424
  var NametagMinter = class {
2376
2425
  client;
@@ -2395,7 +2444,8 @@ var NametagMinter = class {
2395
2444
  */
2396
2445
  async isNametagAvailable(nametag) {
2397
2446
  try {
2398
- const cleanNametag = nametag.replace("@", "").trim();
2447
+ const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
2448
+ const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
2399
2449
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
2400
2450
  const isMinted = await this.client.isMinted(this.trustBase, nametagTokenId);
2401
2451
  return !isMinted;
@@ -2412,7 +2462,8 @@ var NametagMinter = class {
2412
2462
  * @returns MintNametagResult with token if successful
2413
2463
  */
2414
2464
  async mintNametag(nametag, ownerAddress) {
2415
- const cleanNametag = nametag.replace("@", "").trim();
2465
+ const stripped = nametag.startsWith("@") ? nametag.slice(1) : nametag;
2466
+ const cleanNametag = (0, import_nostr_js_sdk.normalizeNametag)(stripped);
2416
2467
  this.log(`Starting mint for nametag: ${cleanNametag}`);
2417
2468
  try {
2418
2469
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
@@ -2552,7 +2603,9 @@ var STORAGE_KEYS_GLOBAL = {
2552
2603
  /** Nametag cache per address (separate from tracked addresses registry) */
2553
2604
  ADDRESS_NAMETAGS: "address_nametags",
2554
2605
  /** Active addresses registry (JSON: TrackedAddressesStorage) */
2555
- TRACKED_ADDRESSES: "tracked_addresses"
2606
+ TRACKED_ADDRESSES: "tracked_addresses",
2607
+ /** Last processed Nostr wallet event timestamp (unix seconds), keyed per pubkey */
2608
+ LAST_WALLET_EVENT_TS: "last_wallet_event_ts"
2556
2609
  };
2557
2610
  var STORAGE_KEYS_ADDRESS = {
2558
2611
  /** Pending transfers for this address */
@@ -2564,7 +2617,9 @@ var STORAGE_KEYS_ADDRESS = {
2564
2617
  /** Messages for this address */
2565
2618
  MESSAGES: "messages",
2566
2619
  /** Transaction history for this address */
2567
- TRANSACTION_HISTORY: "transaction_history"
2620
+ TRANSACTION_HISTORY: "transaction_history",
2621
+ /** Pending V5 finalization tokens (unconfirmed instant split tokens) */
2622
+ PENDING_V5_TOKENS: "pending_v5_tokens"
2568
2623
  };
2569
2624
  var STORAGE_KEYS = {
2570
2625
  ...STORAGE_KEYS_GLOBAL,
@@ -2979,6 +3034,18 @@ function parseTxfStorageData(data) {
2979
3034
  result.validationErrors.push(`Forked token ${parsed.tokenId}: invalid structure`);
2980
3035
  }
2981
3036
  }
3037
+ } else if (key.startsWith("token-")) {
3038
+ try {
3039
+ const entry = storageData[key];
3040
+ const txfToken = entry?.token;
3041
+ if (txfToken?.genesis?.data?.tokenId) {
3042
+ const tokenId = txfToken.genesis.data.tokenId;
3043
+ const token = txfToToken(tokenId, txfToken);
3044
+ result.tokens.push(token);
3045
+ }
3046
+ } catch (err) {
3047
+ result.validationErrors.push(`Token ${key}: ${err}`);
3048
+ }
2982
3049
  }
2983
3050
  }
2984
3051
  return result;
@@ -3554,8 +3621,9 @@ var InstantSplitExecutor = class {
3554
3621
  const criticalPathDuration = performance.now() - startTime;
3555
3622
  console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
3556
3623
  options?.onNostrDelivered?.(nostrEventId);
3624
+ let backgroundPromise;
3557
3625
  if (!options?.skipBackground) {
3558
- this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
3626
+ backgroundPromise = this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
3559
3627
  signingService: this.signingService,
3560
3628
  tokenType: tokenToSplit.type,
3561
3629
  coinId,
@@ -3571,7 +3639,8 @@ var InstantSplitExecutor = class {
3571
3639
  nostrEventId,
3572
3640
  splitGroupId,
3573
3641
  criticalPathDurationMs: criticalPathDuration,
3574
- backgroundStarted: !options?.skipBackground
3642
+ backgroundStarted: !options?.skipBackground,
3643
+ backgroundPromise
3575
3644
  };
3576
3645
  } catch (error) {
3577
3646
  const duration = performance.now() - startTime;
@@ -3633,7 +3702,7 @@ var InstantSplitExecutor = class {
3633
3702
  this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
3634
3703
  this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
3635
3704
  ]);
3636
- submissions.then(async (results) => {
3705
+ return submissions.then(async (results) => {
3637
3706
  const submitDuration = performance.now() - startTime;
3638
3707
  console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
3639
3708
  context.onProgress?.({
@@ -4098,6 +4167,11 @@ var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/addres
4098
4167
  var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
4099
4168
  var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
4100
4169
  var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
4170
+ var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4171
+ var import_MintCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
4172
+ var import_MintTransactionData3 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
4173
+ var import_InclusionProofUtils5 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
4174
+ var import_InclusionProof = require("@unicitylabs/state-transition-sdk/lib/transaction/InclusionProof");
4101
4175
  function enrichWithRegistry(info) {
4102
4176
  const registry = TokenRegistry.getInstance();
4103
4177
  const def = registry.getDefinition(info.coinId);
@@ -4125,7 +4199,7 @@ async function parseTokenInfo(tokenData) {
4125
4199
  try {
4126
4200
  const sdkToken = await import_Token6.Token.fromJSON(data);
4127
4201
  if (sdkToken.id) {
4128
- defaultInfo.tokenId = sdkToken.id.toString();
4202
+ defaultInfo.tokenId = sdkToken.id.toJSON();
4129
4203
  }
4130
4204
  if (sdkToken.coins && sdkToken.coins.coins) {
4131
4205
  const rawCoins = sdkToken.coins.coins;
@@ -4295,6 +4369,13 @@ function extractTokenStateKey(token) {
4295
4369
  if (!tokenId || !stateHash) return null;
4296
4370
  return createTokenStateKey(tokenId, stateHash);
4297
4371
  }
4372
+ function fromHex4(hex) {
4373
+ const bytes = new Uint8Array(hex.length / 2);
4374
+ for (let i = 0; i < hex.length; i += 2) {
4375
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
4376
+ }
4377
+ return bytes;
4378
+ }
4298
4379
  function hasSameGenesisTokenId(t1, t2) {
4299
4380
  const id1 = extractTokenIdFromSdkData(t1.sdkData);
4300
4381
  const id2 = extractTokenIdFromSdkData(t2.sdkData);
@@ -4384,6 +4465,7 @@ var PaymentsModule = class _PaymentsModule {
4384
4465
  // Token State
4385
4466
  tokens = /* @__PURE__ */ new Map();
4386
4467
  pendingTransfers = /* @__PURE__ */ new Map();
4468
+ pendingBackgroundTasks = [];
4387
4469
  // Repository State (tombstones, archives, forked, history)
4388
4470
  tombstones = [];
4389
4471
  archivedTokens = /* @__PURE__ */ new Map();
@@ -4408,6 +4490,12 @@ var PaymentsModule = class _PaymentsModule {
4408
4490
  // Poll every 2s
4409
4491
  static PROOF_POLLING_MAX_ATTEMPTS = 30;
4410
4492
  // Max 30 attempts (~60s)
4493
+ // Storage event subscriptions (push-based sync)
4494
+ storageEventUnsubscribers = [];
4495
+ syncDebounceTimer = null;
4496
+ static SYNC_DEBOUNCE_MS = 500;
4497
+ /** Sync coalescing: concurrent sync() calls share the same operation */
4498
+ _syncInProgress = null;
4411
4499
  constructor(config) {
4412
4500
  this.moduleConfig = {
4413
4501
  autoSync: config?.autoSync ?? true,
@@ -4416,10 +4504,13 @@ var PaymentsModule = class _PaymentsModule {
4416
4504
  maxRetries: config?.maxRetries ?? 3,
4417
4505
  debug: config?.debug ?? false
4418
4506
  };
4419
- const l1Enabled = config?.l1?.electrumUrl && config.l1.electrumUrl.length > 0;
4420
- this.l1 = l1Enabled ? new L1PaymentsModule(config?.l1) : null;
4507
+ this.l1 = config?.l1 === null ? null : new L1PaymentsModule(config?.l1);
4421
4508
  }
4422
- /** Get module configuration */
4509
+ /**
4510
+ * Get the current module configuration (excluding L1 config).
4511
+ *
4512
+ * @returns Resolved configuration with all defaults applied.
4513
+ */
4423
4514
  getConfig() {
4424
4515
  return this.moduleConfig;
4425
4516
  }
@@ -4460,9 +4551,9 @@ var PaymentsModule = class _PaymentsModule {
4460
4551
  transport: deps.transport
4461
4552
  });
4462
4553
  }
4463
- this.unsubscribeTransfers = deps.transport.onTokenTransfer((transfer) => {
4464
- this.handleIncomingTransfer(transfer);
4465
- });
4554
+ this.unsubscribeTransfers = deps.transport.onTokenTransfer(
4555
+ (transfer) => this.handleIncomingTransfer(transfer)
4556
+ );
4466
4557
  if (deps.transport.onPaymentRequest) {
4467
4558
  this.unsubscribePaymentRequests = deps.transport.onPaymentRequest((request) => {
4468
4559
  this.handleIncomingPaymentRequest(request);
@@ -4473,9 +4564,14 @@ var PaymentsModule = class _PaymentsModule {
4473
4564
  this.handlePaymentRequestResponse(response);
4474
4565
  });
4475
4566
  }
4567
+ this.subscribeToStorageEvents();
4476
4568
  }
4477
4569
  /**
4478
- * Load tokens from storage
4570
+ * Load all token data from storage providers and restore wallet state.
4571
+ *
4572
+ * Loads tokens, nametag data, transaction history, and pending transfers
4573
+ * from configured storage providers. Restores pending V5 tokens and
4574
+ * triggers a fire-and-forget {@link resolveUnconfirmed} call.
4479
4575
  */
4480
4576
  async load() {
4481
4577
  this.ensureInitialized();
@@ -4492,6 +4588,7 @@ var PaymentsModule = class _PaymentsModule {
4492
4588
  console.error(`[Payments] Failed to load from provider ${id}:`, err);
4493
4589
  }
4494
4590
  }
4591
+ await this.loadPendingV5Tokens();
4495
4592
  await this.loadTokensFromFileStorage();
4496
4593
  await this.loadNametagFromFileStorage();
4497
4594
  const historyData = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.TRANSACTION_HISTORY);
@@ -4509,9 +4606,14 @@ var PaymentsModule = class _PaymentsModule {
4509
4606
  this.pendingTransfers.set(transfer.id, transfer);
4510
4607
  }
4511
4608
  }
4609
+ this.resolveUnconfirmed().catch(() => {
4610
+ });
4512
4611
  }
4513
4612
  /**
4514
- * Cleanup resources
4613
+ * Cleanup all subscriptions, polling jobs, and pending resolvers.
4614
+ *
4615
+ * Should be called when the wallet is being shut down or the module is
4616
+ * no longer needed. Also destroys the L1 sub-module if present.
4515
4617
  */
4516
4618
  destroy() {
4517
4619
  this.unsubscribeTransfers?.();
@@ -4529,6 +4631,7 @@ var PaymentsModule = class _PaymentsModule {
4529
4631
  resolver.reject(new Error("Module destroyed"));
4530
4632
  }
4531
4633
  this.pendingResponseResolvers.clear();
4634
+ this.unsubscribeStorageEvents();
4532
4635
  if (this.l1) {
4533
4636
  this.l1.destroy();
4534
4637
  }
@@ -4545,7 +4648,8 @@ var PaymentsModule = class _PaymentsModule {
4545
4648
  const result = {
4546
4649
  id: crypto.randomUUID(),
4547
4650
  status: "pending",
4548
- tokens: []
4651
+ tokens: [],
4652
+ tokenTransfers: []
4549
4653
  };
4550
4654
  try {
4551
4655
  const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
@@ -4582,69 +4686,147 @@ var PaymentsModule = class _PaymentsModule {
4582
4686
  await this.saveToOutbox(result, recipientPubkey);
4583
4687
  result.status = "submitted";
4584
4688
  const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4689
+ const transferMode = request.transferMode ?? "instant";
4585
4690
  if (splitPlan.requiresSplit && splitPlan.tokenToSplit) {
4586
- this.log("Executing token split...");
4587
- const executor = new TokenSplitExecutor({
4588
- stateTransitionClient: stClient,
4589
- trustBase,
4590
- signingService
4591
- });
4592
- const splitResult = await executor.executeSplit(
4593
- splitPlan.tokenToSplit.sdkToken,
4594
- splitPlan.splitAmount,
4595
- splitPlan.remainderAmount,
4596
- splitPlan.coinId,
4597
- recipientAddress
4598
- );
4599
- const changeTokenData = splitResult.tokenForSender.toJSON();
4600
- const changeToken = {
4601
- id: crypto.randomUUID(),
4602
- coinId: request.coinId,
4603
- symbol: this.getCoinSymbol(request.coinId),
4604
- name: this.getCoinName(request.coinId),
4605
- decimals: this.getCoinDecimals(request.coinId),
4606
- iconUrl: this.getCoinIconUrl(request.coinId),
4607
- amount: splitPlan.remainderAmount.toString(),
4608
- status: "confirmed",
4609
- createdAt: Date.now(),
4610
- updatedAt: Date.now(),
4611
- sdkData: JSON.stringify(changeTokenData)
4612
- };
4613
- await this.addToken(changeToken, true);
4614
- this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
4615
- console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
4616
- await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4617
- sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
4618
- transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
4619
- memo: request.memo
4620
- });
4621
- console.log(`[Payments] Split token sent successfully`);
4622
- await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
4623
- result.txHash = "split-" + Date.now().toString(16);
4624
- this.log(`Split transfer completed`);
4691
+ if (transferMode === "conservative") {
4692
+ this.log("Executing conservative split...");
4693
+ const splitExecutor = new TokenSplitExecutor({
4694
+ stateTransitionClient: stClient,
4695
+ trustBase,
4696
+ signingService
4697
+ });
4698
+ const splitResult = await splitExecutor.executeSplit(
4699
+ splitPlan.tokenToSplit.sdkToken,
4700
+ splitPlan.splitAmount,
4701
+ splitPlan.remainderAmount,
4702
+ splitPlan.coinId,
4703
+ recipientAddress
4704
+ );
4705
+ const changeTokenData = splitResult.tokenForSender.toJSON();
4706
+ const changeUiToken = {
4707
+ id: crypto.randomUUID(),
4708
+ coinId: request.coinId,
4709
+ symbol: this.getCoinSymbol(request.coinId),
4710
+ name: this.getCoinName(request.coinId),
4711
+ decimals: this.getCoinDecimals(request.coinId),
4712
+ iconUrl: this.getCoinIconUrl(request.coinId),
4713
+ amount: splitPlan.remainderAmount.toString(),
4714
+ status: "confirmed",
4715
+ createdAt: Date.now(),
4716
+ updatedAt: Date.now(),
4717
+ sdkData: JSON.stringify(changeTokenData)
4718
+ };
4719
+ await this.addToken(changeUiToken, true);
4720
+ this.log(`Conservative split: change token saved: ${changeUiToken.id}`);
4721
+ await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4722
+ sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
4723
+ transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
4724
+ memo: request.memo
4725
+ });
4726
+ const splitCommitmentRequestId = splitResult.recipientTransferTx?.data?.requestId ?? splitResult.recipientTransferTx?.requestId;
4727
+ const splitRequestIdHex = splitCommitmentRequestId instanceof Uint8Array ? Array.from(splitCommitmentRequestId).map((b) => b.toString(16).padStart(2, "0")).join("") : splitCommitmentRequestId ? String(splitCommitmentRequestId) : void 0;
4728
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag, true);
4729
+ result.tokenTransfers.push({
4730
+ sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4731
+ method: "split",
4732
+ requestIdHex: splitRequestIdHex
4733
+ });
4734
+ this.log(`Conservative split transfer completed`);
4735
+ } else {
4736
+ this.log("Executing instant split...");
4737
+ const devMode = this.deps.oracle.isDevMode?.() ?? false;
4738
+ const executor = new InstantSplitExecutor({
4739
+ stateTransitionClient: stClient,
4740
+ trustBase,
4741
+ signingService,
4742
+ devMode
4743
+ });
4744
+ const instantResult = await executor.executeSplitInstant(
4745
+ splitPlan.tokenToSplit.sdkToken,
4746
+ splitPlan.splitAmount,
4747
+ splitPlan.remainderAmount,
4748
+ splitPlan.coinId,
4749
+ recipientAddress,
4750
+ this.deps.transport,
4751
+ recipientPubkey,
4752
+ {
4753
+ onChangeTokenCreated: async (changeToken) => {
4754
+ const changeTokenData = changeToken.toJSON();
4755
+ const uiToken = {
4756
+ id: crypto.randomUUID(),
4757
+ coinId: request.coinId,
4758
+ symbol: this.getCoinSymbol(request.coinId),
4759
+ name: this.getCoinName(request.coinId),
4760
+ decimals: this.getCoinDecimals(request.coinId),
4761
+ iconUrl: this.getCoinIconUrl(request.coinId),
4762
+ amount: splitPlan.remainderAmount.toString(),
4763
+ status: "confirmed",
4764
+ createdAt: Date.now(),
4765
+ updatedAt: Date.now(),
4766
+ sdkData: JSON.stringify(changeTokenData)
4767
+ };
4768
+ await this.addToken(uiToken, true);
4769
+ this.log(`Change token saved via background: ${uiToken.id}`);
4770
+ },
4771
+ onStorageSync: async () => {
4772
+ await this.save();
4773
+ return true;
4774
+ }
4775
+ }
4776
+ );
4777
+ if (!instantResult.success) {
4778
+ throw new Error(instantResult.error || "Instant split failed");
4779
+ }
4780
+ if (instantResult.backgroundPromise) {
4781
+ this.pendingBackgroundTasks.push(instantResult.backgroundPromise);
4782
+ }
4783
+ await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
4784
+ result.tokenTransfers.push({
4785
+ sourceTokenId: splitPlan.tokenToSplit.uiToken.id,
4786
+ method: "split",
4787
+ splitGroupId: instantResult.splitGroupId,
4788
+ nostrEventId: instantResult.nostrEventId
4789
+ });
4790
+ this.log(`Instant split transfer completed`);
4791
+ }
4625
4792
  }
4626
4793
  for (const tokenWithAmount of splitPlan.tokensToTransferDirectly) {
4627
4794
  const token = tokenWithAmount.uiToken;
4628
4795
  const commitment = await this.createSdkCommitment(token, recipientAddress, signingService);
4629
- const response = await stClient.submitTransferCommitment(commitment);
4630
- if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
4631
- throw new Error(`Transfer commitment failed: ${response.status}`);
4632
- }
4633
- if (!this.deps.oracle.waitForProofSdk) {
4634
- throw new Error("Oracle provider must implement waitForProofSdk()");
4796
+ if (transferMode === "conservative") {
4797
+ console.log(`[Payments] CONSERVATIVE: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
4798
+ const submitResponse = await stClient.submitTransferCommitment(commitment);
4799
+ if (submitResponse.status !== "SUCCESS" && submitResponse.status !== "REQUEST_ID_EXISTS") {
4800
+ throw new Error(`Transfer commitment failed: ${submitResponse.status}`);
4801
+ }
4802
+ const inclusionProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment);
4803
+ const transferTx = commitment.toTransaction(inclusionProof);
4804
+ await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4805
+ sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
4806
+ transferTx: JSON.stringify(transferTx.toJSON()),
4807
+ memo: request.memo
4808
+ });
4809
+ console.log(`[Payments] CONSERVATIVE: Direct token sent successfully`);
4810
+ } else {
4811
+ console.log(`[Payments] NOSTR-FIRST: Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}...`);
4812
+ await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4813
+ sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
4814
+ commitmentData: JSON.stringify(commitment.toJSON()),
4815
+ memo: request.memo
4816
+ });
4817
+ console.log(`[Payments] NOSTR-FIRST: Direct token sent successfully`);
4818
+ stClient.submitTransferCommitment(commitment).catch(
4819
+ (err) => console.error("[Payments] Background commitment submit failed:", err)
4820
+ );
4635
4821
  }
4636
- const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
4637
- const transferTx = commitment.toTransaction(inclusionProof);
4638
4822
  const requestIdBytes = commitment.requestId;
4639
- result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
4640
- console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
4641
- await this.deps.transport.sendTokenTransfer(recipientPubkey, {
4642
- sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
4643
- transferTx: JSON.stringify(transferTx.toJSON()),
4644
- memo: request.memo
4823
+ const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
4824
+ result.tokenTransfers.push({
4825
+ sourceTokenId: token.id,
4826
+ method: "direct",
4827
+ requestIdHex
4645
4828
  });
4646
- console.log(`[Payments] Direct token sent successfully`);
4647
- this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
4829
+ this.log(`Token ${token.id} sent via ${transferMode.toUpperCase()}, requestId: ${requestIdHex}`);
4648
4830
  await this.removeToken(token.id, recipientNametag, true);
4649
4831
  }
4650
4832
  result.status = "delivered";
@@ -4657,7 +4839,8 @@ var PaymentsModule = class _PaymentsModule {
4657
4839
  coinId: request.coinId,
4658
4840
  symbol: this.getCoinSymbol(request.coinId),
4659
4841
  timestamp: Date.now(),
4660
- recipientNametag
4842
+ recipientNametag,
4843
+ transferId: result.id
4661
4844
  });
4662
4845
  this.deps.emitEvent("transfer:confirmed", result);
4663
4846
  return result;
@@ -4793,6 +4976,9 @@ var PaymentsModule = class _PaymentsModule {
4793
4976
  }
4794
4977
  );
4795
4978
  if (result.success) {
4979
+ if (result.backgroundPromise) {
4980
+ this.pendingBackgroundTasks.push(result.backgroundPromise);
4981
+ }
4796
4982
  const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4797
4983
  await this.removeToken(tokenToSplit.id, recipientNametag, true);
4798
4984
  await this.addToHistory({
@@ -4834,6 +5020,63 @@ var PaymentsModule = class _PaymentsModule {
4834
5020
  */
4835
5021
  async processInstantSplitBundle(bundle, senderPubkey) {
4836
5022
  this.ensureInitialized();
5023
+ if (!isInstantSplitBundleV5(bundle)) {
5024
+ return this.processInstantSplitBundleSync(bundle, senderPubkey);
5025
+ }
5026
+ try {
5027
+ const deterministicId = `v5split_${bundle.splitGroupId}`;
5028
+ if (this.tokens.has(deterministicId)) {
5029
+ this.log(`V5 bundle ${deterministicId.slice(0, 16)}... already exists, skipping duplicate`);
5030
+ return { success: true, durationMs: 0 };
5031
+ }
5032
+ const registry = TokenRegistry.getInstance();
5033
+ const pendingData = {
5034
+ type: "v5_bundle",
5035
+ stage: "RECEIVED",
5036
+ bundleJson: JSON.stringify(bundle),
5037
+ senderPubkey,
5038
+ savedAt: Date.now(),
5039
+ attemptCount: 0
5040
+ };
5041
+ const uiToken = {
5042
+ id: deterministicId,
5043
+ coinId: bundle.coinId,
5044
+ symbol: registry.getSymbol(bundle.coinId) || bundle.coinId,
5045
+ name: registry.getName(bundle.coinId) || bundle.coinId,
5046
+ decimals: registry.getDecimals(bundle.coinId) ?? 8,
5047
+ amount: bundle.amount,
5048
+ status: "submitted",
5049
+ // UNCONFIRMED
5050
+ createdAt: Date.now(),
5051
+ updatedAt: Date.now(),
5052
+ sdkData: JSON.stringify({ _pendingFinalization: pendingData })
5053
+ };
5054
+ await this.addToken(uiToken, false);
5055
+ this.log(`V5 bundle saved as unconfirmed: ${uiToken.id.slice(0, 8)}...`);
5056
+ this.deps.emitEvent("transfer:incoming", {
5057
+ id: bundle.splitGroupId,
5058
+ senderPubkey,
5059
+ tokens: [uiToken],
5060
+ receivedAt: Date.now()
5061
+ });
5062
+ await this.save();
5063
+ this.resolveUnconfirmed().catch(() => {
5064
+ });
5065
+ return { success: true, durationMs: 0 };
5066
+ } catch (error) {
5067
+ const errorMessage = error instanceof Error ? error.message : String(error);
5068
+ return {
5069
+ success: false,
5070
+ error: errorMessage,
5071
+ durationMs: 0
5072
+ };
5073
+ }
5074
+ }
5075
+ /**
5076
+ * Synchronous V4 bundle processing (dev mode only).
5077
+ * Kept for backward compatibility with V4 bundles.
5078
+ */
5079
+ async processInstantSplitBundleSync(bundle, senderPubkey) {
4837
5080
  try {
4838
5081
  const signingService = await this.createSigningService();
4839
5082
  const stClient = this.deps.oracle.getStateTransitionClient?.();
@@ -4919,7 +5162,10 @@ var PaymentsModule = class _PaymentsModule {
4919
5162
  }
4920
5163
  }
4921
5164
  /**
4922
- * Check if a payload is an instant split bundle
5165
+ * Type-guard: check whether a payload is a valid {@link InstantSplitBundle} (V4 or V5).
5166
+ *
5167
+ * @param payload - The object to test.
5168
+ * @returns `true` if the payload matches the InstantSplitBundle shape.
4923
5169
  */
4924
5170
  isInstantSplitBundle(payload) {
4925
5171
  return isInstantSplitBundle(payload);
@@ -5000,39 +5246,57 @@ var PaymentsModule = class _PaymentsModule {
5000
5246
  return [...this.paymentRequests];
5001
5247
  }
5002
5248
  /**
5003
- * Get pending payment requests count
5249
+ * Get the count of payment requests with status `'pending'`.
5250
+ *
5251
+ * @returns Number of pending incoming payment requests.
5004
5252
  */
5005
5253
  getPendingPaymentRequestsCount() {
5006
5254
  return this.paymentRequests.filter((r) => r.status === "pending").length;
5007
5255
  }
5008
5256
  /**
5009
- * Accept a payment request (marks it as accepted, user should then call send())
5257
+ * Accept a payment request and notify the requester.
5258
+ *
5259
+ * Marks the request as `'accepted'` and sends a response via transport.
5260
+ * The caller should subsequently call {@link send} to fulfill the payment.
5261
+ *
5262
+ * @param requestId - ID of the incoming payment request to accept.
5010
5263
  */
5011
5264
  async acceptPaymentRequest(requestId2) {
5012
5265
  this.updatePaymentRequestStatus(requestId2, "accepted");
5013
5266
  await this.sendPaymentRequestResponse(requestId2, "accepted");
5014
5267
  }
5015
5268
  /**
5016
- * Reject a payment request
5269
+ * Reject a payment request and notify the requester.
5270
+ *
5271
+ * @param requestId - ID of the incoming payment request to reject.
5017
5272
  */
5018
5273
  async rejectPaymentRequest(requestId2) {
5019
5274
  this.updatePaymentRequestStatus(requestId2, "rejected");
5020
5275
  await this.sendPaymentRequestResponse(requestId2, "rejected");
5021
5276
  }
5022
5277
  /**
5023
- * Mark a payment request as paid (after successful transfer)
5278
+ * Mark a payment request as paid (local status update only).
5279
+ *
5280
+ * Typically called after a successful {@link send} to record that the
5281
+ * request has been fulfilled.
5282
+ *
5283
+ * @param requestId - ID of the incoming payment request to mark as paid.
5024
5284
  */
5025
5285
  markPaymentRequestPaid(requestId2) {
5026
5286
  this.updatePaymentRequestStatus(requestId2, "paid");
5027
5287
  }
5028
5288
  /**
5029
- * Clear processed (non-pending) payment requests
5289
+ * Remove all non-pending incoming payment requests from memory.
5290
+ *
5291
+ * Keeps only requests with status `'pending'`.
5030
5292
  */
5031
5293
  clearProcessedPaymentRequests() {
5032
5294
  this.paymentRequests = this.paymentRequests.filter((r) => r.status === "pending");
5033
5295
  }
5034
5296
  /**
5035
- * Remove a specific payment request
5297
+ * Remove a specific incoming payment request by ID.
5298
+ *
5299
+ * @param requestId - ID of the payment request to remove.
5036
5300
  */
5037
5301
  removePaymentRequest(requestId2) {
5038
5302
  this.paymentRequests = this.paymentRequests.filter((r) => r.id !== requestId2);
@@ -5079,13 +5343,16 @@ var PaymentsModule = class _PaymentsModule {
5079
5343
  if (this.paymentRequests.find((r) => r.id === transportRequest.id)) {
5080
5344
  return;
5081
5345
  }
5346
+ const coinId = transportRequest.request.coinId;
5347
+ const registry = TokenRegistry.getInstance();
5348
+ const coinDef = registry.getDefinition(coinId);
5082
5349
  const request = {
5083
5350
  id: transportRequest.id,
5084
5351
  senderPubkey: transportRequest.senderTransportPubkey,
5352
+ senderNametag: transportRequest.senderNametag,
5085
5353
  amount: transportRequest.request.amount,
5086
- coinId: transportRequest.request.coinId,
5087
- symbol: transportRequest.request.coinId,
5088
- // Use coinId as symbol for now
5354
+ coinId,
5355
+ symbol: coinDef?.symbol || coinId.slice(0, 8),
5089
5356
  message: transportRequest.request.message,
5090
5357
  recipientNametag: transportRequest.request.recipientNametag,
5091
5358
  requestId: transportRequest.request.requestId,
@@ -5154,7 +5421,11 @@ var PaymentsModule = class _PaymentsModule {
5154
5421
  });
5155
5422
  }
5156
5423
  /**
5157
- * Cancel waiting for a payment response
5424
+ * Cancel an active {@link waitForPaymentResponse} call.
5425
+ *
5426
+ * The pending promise is rejected with a `'Cancelled'` error.
5427
+ *
5428
+ * @param requestId - The outgoing request ID whose wait should be cancelled.
5158
5429
  */
5159
5430
  cancelWaitForPaymentResponse(requestId2) {
5160
5431
  const resolver = this.pendingResponseResolvers.get(requestId2);
@@ -5165,14 +5436,16 @@ var PaymentsModule = class _PaymentsModule {
5165
5436
  }
5166
5437
  }
5167
5438
  /**
5168
- * Remove an outgoing payment request
5439
+ * Remove an outgoing payment request and cancel any pending wait.
5440
+ *
5441
+ * @param requestId - ID of the outgoing request to remove.
5169
5442
  */
5170
5443
  removeOutgoingPaymentRequest(requestId2) {
5171
5444
  this.outgoingPaymentRequests.delete(requestId2);
5172
5445
  this.cancelWaitForPaymentResponse(requestId2);
5173
5446
  }
5174
5447
  /**
5175
- * Clear completed/expired outgoing payment requests
5448
+ * Remove all outgoing payment requests that are `'paid'`, `'rejected'`, or `'expired'`.
5176
5449
  */
5177
5450
  clearCompletedOutgoingPaymentRequests() {
5178
5451
  for (const [id, request] of this.outgoingPaymentRequests) {
@@ -5244,6 +5517,71 @@ var PaymentsModule = class _PaymentsModule {
5244
5517
  }
5245
5518
  }
5246
5519
  // ===========================================================================
5520
+ // Public API - Receive
5521
+ // ===========================================================================
5522
+ /**
5523
+ * Fetch and process pending incoming transfers from the transport layer.
5524
+ *
5525
+ * Performs a one-shot query to fetch all pending events, processes them
5526
+ * through the existing pipeline, and resolves after all stored events
5527
+ * are handled. Useful for batch/CLI apps that need explicit receive.
5528
+ *
5529
+ * When `finalize` is true, polls resolveUnconfirmed() + load() until all
5530
+ * tokens are confirmed or the timeout expires. Otherwise calls
5531
+ * resolveUnconfirmed() once to submit pending commitments.
5532
+ *
5533
+ * @param options - Optional receive options including finalization control
5534
+ * @param callback - Optional callback invoked for each newly received transfer
5535
+ * @returns ReceiveResult with transfers and finalization metadata
5536
+ */
5537
+ async receive(options, callback) {
5538
+ this.ensureInitialized();
5539
+ if (!this.deps.transport.fetchPendingEvents) {
5540
+ throw new Error("Transport provider does not support fetchPendingEvents");
5541
+ }
5542
+ const opts = options ?? {};
5543
+ const tokensBefore = new Set(this.tokens.keys());
5544
+ await this.deps.transport.fetchPendingEvents();
5545
+ await this.load();
5546
+ const received = [];
5547
+ for (const [tokenId, token] of this.tokens) {
5548
+ if (!tokensBefore.has(tokenId)) {
5549
+ const transfer = {
5550
+ id: tokenId,
5551
+ senderPubkey: "",
5552
+ tokens: [token],
5553
+ receivedAt: Date.now()
5554
+ };
5555
+ received.push(transfer);
5556
+ if (callback) callback(transfer);
5557
+ }
5558
+ }
5559
+ const result = { transfers: received };
5560
+ if (opts.finalize) {
5561
+ const timeout = opts.timeout ?? 6e4;
5562
+ const pollInterval = opts.pollInterval ?? 2e3;
5563
+ const startTime = Date.now();
5564
+ while (Date.now() - startTime < timeout) {
5565
+ const resolution = await this.resolveUnconfirmed();
5566
+ result.finalization = resolution;
5567
+ if (opts.onProgress) opts.onProgress(resolution);
5568
+ const stillUnconfirmed = Array.from(this.tokens.values()).some(
5569
+ (t) => t.status === "submitted" || t.status === "pending"
5570
+ );
5571
+ if (!stillUnconfirmed) break;
5572
+ await new Promise((r) => setTimeout(r, pollInterval));
5573
+ await this.load();
5574
+ }
5575
+ result.finalizationDurationMs = Date.now() - startTime;
5576
+ result.timedOut = Array.from(this.tokens.values()).some(
5577
+ (t) => t.status === "submitted" || t.status === "pending"
5578
+ );
5579
+ } else {
5580
+ result.finalization = await this.resolveUnconfirmed();
5581
+ }
5582
+ return result;
5583
+ }
5584
+ // ===========================================================================
5247
5585
  // Public API - Balance & Tokens
5248
5586
  // ===========================================================================
5249
5587
  /**
@@ -5253,10 +5591,20 @@ var PaymentsModule = class _PaymentsModule {
5253
5591
  this.priceProvider = provider;
5254
5592
  }
5255
5593
  /**
5256
- * Get total portfolio value in USD
5257
- * Returns null if PriceProvider is not configured
5594
+ * Wait for all pending background operations (e.g., instant split change token creation).
5595
+ * Call this before process exit to ensure all tokens are saved.
5258
5596
  */
5259
- async getBalance() {
5597
+ async waitForPendingOperations() {
5598
+ if (this.pendingBackgroundTasks.length > 0) {
5599
+ await Promise.allSettled(this.pendingBackgroundTasks);
5600
+ this.pendingBackgroundTasks = [];
5601
+ }
5602
+ }
5603
+ /**
5604
+ * Get total portfolio value in USD.
5605
+ * Returns null if PriceProvider is not configured.
5606
+ */
5607
+ async getFiatBalance() {
5260
5608
  const assets = await this.getAssets();
5261
5609
  if (!this.priceProvider) {
5262
5610
  return null;
@@ -5272,19 +5620,95 @@ var PaymentsModule = class _PaymentsModule {
5272
5620
  return hasAnyPrice ? total : null;
5273
5621
  }
5274
5622
  /**
5275
- * Get aggregated assets (tokens grouped by coinId) with price data
5276
- * Only includes confirmed tokens
5623
+ * Get token balances grouped by coin type.
5624
+ *
5625
+ * Returns an array of {@link Asset} objects, one per coin type held.
5626
+ * Each entry includes confirmed and unconfirmed breakdowns. Tokens with
5627
+ * status `'spent'`, `'invalid'`, or `'transferring'` are excluded.
5628
+ *
5629
+ * This is synchronous — no price data is included. Use {@link getAssets}
5630
+ * for the async version with fiat pricing.
5631
+ *
5632
+ * @param coinId - Optional coin ID to filter by (e.g. hex string). When omitted, all coin types are returned.
5633
+ * @returns Array of balance summaries (synchronous — no await needed).
5634
+ */
5635
+ getBalance(coinId) {
5636
+ return this.aggregateTokens(coinId);
5637
+ }
5638
+ /**
5639
+ * Get aggregated assets (tokens grouped by coinId) with price data.
5640
+ * Includes both confirmed and unconfirmed tokens with breakdown.
5277
5641
  */
5278
5642
  async getAssets(coinId) {
5643
+ const rawAssets = this.aggregateTokens(coinId);
5644
+ if (!this.priceProvider || rawAssets.length === 0) {
5645
+ return rawAssets;
5646
+ }
5647
+ try {
5648
+ const registry = TokenRegistry.getInstance();
5649
+ const nameToCoins = /* @__PURE__ */ new Map();
5650
+ for (const asset of rawAssets) {
5651
+ const def = registry.getDefinition(asset.coinId);
5652
+ if (def?.name) {
5653
+ const existing = nameToCoins.get(def.name);
5654
+ if (existing) {
5655
+ existing.push(asset.coinId);
5656
+ } else {
5657
+ nameToCoins.set(def.name, [asset.coinId]);
5658
+ }
5659
+ }
5660
+ }
5661
+ if (nameToCoins.size > 0) {
5662
+ const tokenNames = Array.from(nameToCoins.keys());
5663
+ const prices = await this.priceProvider.getPrices(tokenNames);
5664
+ return rawAssets.map((raw) => {
5665
+ const def = registry.getDefinition(raw.coinId);
5666
+ const price = def?.name ? prices.get(def.name) : void 0;
5667
+ let fiatValueUsd = null;
5668
+ let fiatValueEur = null;
5669
+ if (price) {
5670
+ const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
5671
+ fiatValueUsd = humanAmount * price.priceUsd;
5672
+ if (price.priceEur != null) {
5673
+ fiatValueEur = humanAmount * price.priceEur;
5674
+ }
5675
+ }
5676
+ return {
5677
+ ...raw,
5678
+ priceUsd: price?.priceUsd ?? null,
5679
+ priceEur: price?.priceEur ?? null,
5680
+ change24h: price?.change24h ?? null,
5681
+ fiatValueUsd,
5682
+ fiatValueEur
5683
+ };
5684
+ });
5685
+ }
5686
+ } catch (error) {
5687
+ console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
5688
+ }
5689
+ return rawAssets;
5690
+ }
5691
+ /**
5692
+ * Aggregate tokens by coinId with confirmed/unconfirmed breakdown.
5693
+ * Excludes tokens with status 'spent', 'invalid', or 'transferring'.
5694
+ */
5695
+ aggregateTokens(coinId) {
5279
5696
  const assetsMap = /* @__PURE__ */ new Map();
5280
5697
  for (const token of this.tokens.values()) {
5281
- if (token.status !== "confirmed") continue;
5698
+ if (token.status === "spent" || token.status === "invalid" || token.status === "transferring") continue;
5282
5699
  if (coinId && token.coinId !== coinId) continue;
5283
5700
  const key = token.coinId;
5701
+ const amount = BigInt(token.amount);
5702
+ const isConfirmed = token.status === "confirmed";
5284
5703
  const existing = assetsMap.get(key);
5285
5704
  if (existing) {
5286
- existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
5287
- existing.tokenCount++;
5705
+ if (isConfirmed) {
5706
+ existing.confirmedAmount += amount;
5707
+ existing.confirmedTokenCount++;
5708
+ } else {
5709
+ existing.unconfirmedAmount += amount;
5710
+ existing.unconfirmedTokenCount++;
5711
+ }
5288
5712
  } else {
5289
5713
  assetsMap.set(key, {
5290
5714
  coinId: token.coinId,
@@ -5292,78 +5716,42 @@ var PaymentsModule = class _PaymentsModule {
5292
5716
  name: token.name,
5293
5717
  decimals: token.decimals,
5294
5718
  iconUrl: token.iconUrl,
5295
- totalAmount: token.amount,
5296
- tokenCount: 1
5719
+ confirmedAmount: isConfirmed ? amount : 0n,
5720
+ unconfirmedAmount: isConfirmed ? 0n : amount,
5721
+ confirmedTokenCount: isConfirmed ? 1 : 0,
5722
+ unconfirmedTokenCount: isConfirmed ? 0 : 1
5297
5723
  });
5298
5724
  }
5299
5725
  }
5300
- const rawAssets = Array.from(assetsMap.values());
5301
- let priceMap = null;
5302
- if (this.priceProvider && rawAssets.length > 0) {
5303
- try {
5304
- const registry = TokenRegistry.getInstance();
5305
- const nameToCoins = /* @__PURE__ */ new Map();
5306
- for (const asset of rawAssets) {
5307
- const def = registry.getDefinition(asset.coinId);
5308
- if (def?.name) {
5309
- const existing = nameToCoins.get(def.name);
5310
- if (existing) {
5311
- existing.push(asset.coinId);
5312
- } else {
5313
- nameToCoins.set(def.name, [asset.coinId]);
5314
- }
5315
- }
5316
- }
5317
- if (nameToCoins.size > 0) {
5318
- const tokenNames = Array.from(nameToCoins.keys());
5319
- const prices = await this.priceProvider.getPrices(tokenNames);
5320
- priceMap = /* @__PURE__ */ new Map();
5321
- for (const [name, coinIds] of nameToCoins) {
5322
- const price = prices.get(name);
5323
- if (price) {
5324
- for (const cid of coinIds) {
5325
- priceMap.set(cid, {
5326
- priceUsd: price.priceUsd,
5327
- priceEur: price.priceEur,
5328
- change24h: price.change24h
5329
- });
5330
- }
5331
- }
5332
- }
5333
- }
5334
- } catch (error) {
5335
- console.warn("[Payments] Failed to fetch prices, returning assets without price data:", error);
5336
- }
5337
- }
5338
- return rawAssets.map((raw) => {
5339
- const price = priceMap?.get(raw.coinId);
5340
- let fiatValueUsd = null;
5341
- let fiatValueEur = null;
5342
- if (price) {
5343
- const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
5344
- fiatValueUsd = humanAmount * price.priceUsd;
5345
- if (price.priceEur != null) {
5346
- fiatValueEur = humanAmount * price.priceEur;
5347
- }
5348
- }
5726
+ return Array.from(assetsMap.values()).map((raw) => {
5727
+ const totalAmount = (raw.confirmedAmount + raw.unconfirmedAmount).toString();
5349
5728
  return {
5350
5729
  coinId: raw.coinId,
5351
5730
  symbol: raw.symbol,
5352
5731
  name: raw.name,
5353
5732
  decimals: raw.decimals,
5354
5733
  iconUrl: raw.iconUrl,
5355
- totalAmount: raw.totalAmount,
5356
- tokenCount: raw.tokenCount,
5357
- priceUsd: price?.priceUsd ?? null,
5358
- priceEur: price?.priceEur ?? null,
5359
- change24h: price?.change24h ?? null,
5360
- fiatValueUsd,
5361
- fiatValueEur
5734
+ totalAmount,
5735
+ tokenCount: raw.confirmedTokenCount + raw.unconfirmedTokenCount,
5736
+ confirmedAmount: raw.confirmedAmount.toString(),
5737
+ unconfirmedAmount: raw.unconfirmedAmount.toString(),
5738
+ confirmedTokenCount: raw.confirmedTokenCount,
5739
+ unconfirmedTokenCount: raw.unconfirmedTokenCount,
5740
+ priceUsd: null,
5741
+ priceEur: null,
5742
+ change24h: null,
5743
+ fiatValueUsd: null,
5744
+ fiatValueEur: null
5362
5745
  };
5363
5746
  });
5364
5747
  }
5365
5748
  /**
5366
- * Get all tokens
5749
+ * Get all tokens, optionally filtered by coin type and/or status.
5750
+ *
5751
+ * @param filter - Optional filter criteria.
5752
+ * @param filter.coinId - Return only tokens of this coin type.
5753
+ * @param filter.status - Return only tokens with this status (e.g. `'submitted'` for unconfirmed).
5754
+ * @returns Array of matching {@link Token} objects (synchronous).
5367
5755
  */
5368
5756
  getTokens(filter) {
5369
5757
  let tokens = Array.from(this.tokens.values());
@@ -5376,19 +5764,327 @@ var PaymentsModule = class _PaymentsModule {
5376
5764
  return tokens;
5377
5765
  }
5378
5766
  /**
5379
- * Get single token
5767
+ * Get a single token by its local ID.
5768
+ *
5769
+ * @param id - The local UUID assigned when the token was added.
5770
+ * @returns The token, or `undefined` if not found.
5380
5771
  */
5381
5772
  getToken(id) {
5382
5773
  return this.tokens.get(id);
5383
5774
  }
5384
5775
  // ===========================================================================
5776
+ // Public API - Unconfirmed Token Resolution
5777
+ // ===========================================================================
5778
+ /**
5779
+ * Attempt to resolve unconfirmed (status `'submitted'`) tokens by acquiring
5780
+ * their missing aggregator proofs.
5781
+ *
5782
+ * Each unconfirmed V5 token progresses through stages:
5783
+ * `RECEIVED` → `MINT_SUBMITTED` → `MINT_PROVEN` → `TRANSFER_SUBMITTED` → `FINALIZED`
5784
+ *
5785
+ * Uses 500 ms quick-timeouts per proof check so the call returns quickly even
5786
+ * when proofs are not yet available. Tokens that exceed 50 failed attempts are
5787
+ * marked `'invalid'`.
5788
+ *
5789
+ * Automatically called (fire-and-forget) by {@link load}.
5790
+ *
5791
+ * @returns Summary with counts of resolved, still-pending, and failed tokens plus per-token details.
5792
+ */
5793
+ async resolveUnconfirmed() {
5794
+ this.ensureInitialized();
5795
+ const result = {
5796
+ resolved: 0,
5797
+ stillPending: 0,
5798
+ failed: 0,
5799
+ details: []
5800
+ };
5801
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
5802
+ const trustBase = this.deps.oracle.getTrustBase?.();
5803
+ if (!stClient || !trustBase) return result;
5804
+ const signingService = await this.createSigningService();
5805
+ for (const [tokenId, token] of this.tokens) {
5806
+ if (token.status !== "submitted") continue;
5807
+ const pending2 = this.parsePendingFinalization(token.sdkData);
5808
+ if (!pending2) {
5809
+ result.stillPending++;
5810
+ continue;
5811
+ }
5812
+ if (pending2.type === "v5_bundle") {
5813
+ const progress = await this.resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService);
5814
+ result.details.push({ tokenId, stage: pending2.stage, status: progress });
5815
+ if (progress === "resolved") result.resolved++;
5816
+ else if (progress === "failed") result.failed++;
5817
+ else result.stillPending++;
5818
+ }
5819
+ }
5820
+ if (result.resolved > 0 || result.failed > 0) {
5821
+ await this.save();
5822
+ }
5823
+ return result;
5824
+ }
5825
+ // ===========================================================================
5826
+ // Private - V5 Lazy Resolution Helpers
5827
+ // ===========================================================================
5828
+ /**
5829
+ * Process a single V5 token through its finalization stages with quick-timeout proof checks.
5830
+ */
5831
+ async resolveV5Token(tokenId, token, pending2, stClient, trustBase, signingService) {
5832
+ const bundle = JSON.parse(pending2.bundleJson);
5833
+ pending2.attemptCount++;
5834
+ pending2.lastAttemptAt = Date.now();
5835
+ try {
5836
+ if (pending2.stage === "RECEIVED") {
5837
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
5838
+ const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5839
+ const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5840
+ const mintResponse = await stClient.submitMintCommitment(mintCommitment);
5841
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
5842
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
5843
+ }
5844
+ pending2.stage = "MINT_SUBMITTED";
5845
+ this.updatePendingFinalization(token, pending2);
5846
+ }
5847
+ if (pending2.stage === "MINT_SUBMITTED") {
5848
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
5849
+ const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5850
+ const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5851
+ const proof = await this.quickProofCheck(stClient, trustBase, mintCommitment);
5852
+ if (!proof) {
5853
+ this.updatePendingFinalization(token, pending2);
5854
+ return "pending";
5855
+ }
5856
+ pending2.mintProofJson = JSON.stringify(proof);
5857
+ pending2.stage = "MINT_PROVEN";
5858
+ this.updatePendingFinalization(token, pending2);
5859
+ }
5860
+ if (pending2.stage === "MINT_PROVEN") {
5861
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5862
+ const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5863
+ const transferResponse = await stClient.submitTransferCommitment(transferCommitment);
5864
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
5865
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
5866
+ }
5867
+ pending2.stage = "TRANSFER_SUBMITTED";
5868
+ this.updatePendingFinalization(token, pending2);
5869
+ }
5870
+ if (pending2.stage === "TRANSFER_SUBMITTED") {
5871
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5872
+ const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5873
+ const proof = await this.quickProofCheck(stClient, trustBase, transferCommitment);
5874
+ if (!proof) {
5875
+ this.updatePendingFinalization(token, pending2);
5876
+ return "pending";
5877
+ }
5878
+ const finalizedToken = await this.finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase);
5879
+ const confirmedToken = {
5880
+ id: token.id,
5881
+ coinId: token.coinId,
5882
+ symbol: token.symbol,
5883
+ name: token.name,
5884
+ decimals: token.decimals,
5885
+ iconUrl: token.iconUrl,
5886
+ amount: token.amount,
5887
+ status: "confirmed",
5888
+ createdAt: token.createdAt,
5889
+ updatedAt: Date.now(),
5890
+ sdkData: JSON.stringify(finalizedToken.toJSON())
5891
+ };
5892
+ this.tokens.set(tokenId, confirmedToken);
5893
+ await this.saveTokenToFileStorage(confirmedToken);
5894
+ await this.addToHistory({
5895
+ type: "RECEIVED",
5896
+ amount: confirmedToken.amount,
5897
+ coinId: confirmedToken.coinId,
5898
+ symbol: confirmedToken.symbol || "UNK",
5899
+ timestamp: Date.now(),
5900
+ senderPubkey: pending2.senderPubkey
5901
+ });
5902
+ this.log(`V5 token resolved: ${tokenId.slice(0, 8)}...`);
5903
+ return "resolved";
5904
+ }
5905
+ return "pending";
5906
+ } catch (error) {
5907
+ console.error(`[Payments] resolveV5Token failed for ${tokenId.slice(0, 8)}:`, error);
5908
+ if (pending2.attemptCount > 50) {
5909
+ token.status = "invalid";
5910
+ token.updatedAt = Date.now();
5911
+ this.tokens.set(tokenId, token);
5912
+ return "failed";
5913
+ }
5914
+ this.updatePendingFinalization(token, pending2);
5915
+ return "pending";
5916
+ }
5917
+ }
5918
+ /**
5919
+ * Non-blocking proof check with 500ms timeout.
5920
+ */
5921
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5922
+ async quickProofCheck(stClient, trustBase, commitment, timeoutMs = 500) {
5923
+ try {
5924
+ const proof = await Promise.race([
5925
+ (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, commitment),
5926
+ new Promise((resolve) => setTimeout(() => resolve(null), timeoutMs))
5927
+ ]);
5928
+ return proof;
5929
+ } catch {
5930
+ return null;
5931
+ }
5932
+ }
5933
+ /**
5934
+ * Perform V5 bundle finalization from stored bundle data and proofs.
5935
+ * Extracted from InstantSplitProcessor.processV5Bundle() steps 4-10.
5936
+ */
5937
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5938
+ async finalizeFromV5Bundle(bundle, pending2, signingService, stClient, trustBase) {
5939
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
5940
+ const mintData = await import_MintTransactionData3.MintTransactionData.fromJSON(mintDataJson);
5941
+ const mintCommitment = await import_MintCommitment3.MintCommitment.create(mintData);
5942
+ const mintProofJson = JSON.parse(pending2.mintProofJson);
5943
+ const mintProof = import_InclusionProof.InclusionProof.fromJSON(mintProofJson);
5944
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
5945
+ const tokenType = new import_TokenType3.TokenType(fromHex4(bundle.tokenTypeHex));
5946
+ const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
5947
+ const tokenJson = {
5948
+ version: "2.0",
5949
+ state: senderMintedStateJson,
5950
+ genesis: mintTransaction.toJSON(),
5951
+ transactions: [],
5952
+ nametags: []
5953
+ };
5954
+ const mintedToken = await import_Token6.Token.fromJSON(tokenJson);
5955
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
5956
+ const transferCommitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferCommitmentJson);
5957
+ const transferProof = await (0, import_InclusionProofUtils5.waitInclusionProof)(trustBase, stClient, transferCommitment);
5958
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
5959
+ const transferSalt = fromHex4(bundle.transferSaltHex);
5960
+ const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
5961
+ mintData.tokenId,
5962
+ tokenType,
5963
+ signingService,
5964
+ import_HashAlgorithm5.HashAlgorithm.SHA256,
5965
+ transferSalt
5966
+ );
5967
+ const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
5968
+ let nametagTokens = [];
5969
+ const recipientAddressStr = bundle.recipientAddressJson;
5970
+ if (recipientAddressStr.startsWith("PROXY://")) {
5971
+ if (bundle.nametagTokenJson) {
5972
+ try {
5973
+ const nametagToken = await import_Token6.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
5974
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
5975
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
5976
+ if (proxy.address === recipientAddressStr) {
5977
+ nametagTokens = [nametagToken];
5978
+ }
5979
+ } catch {
5980
+ }
5981
+ }
5982
+ if (nametagTokens.length === 0 && this.nametag?.token) {
5983
+ try {
5984
+ const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
5985
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
5986
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
5987
+ if (proxy.address === recipientAddressStr) {
5988
+ nametagTokens = [nametagToken];
5989
+ }
5990
+ } catch {
5991
+ }
5992
+ }
5993
+ }
5994
+ return stClient.finalizeTransaction(trustBase, mintedToken, recipientState, transferTransaction, nametagTokens);
5995
+ }
5996
+ /**
5997
+ * Parse pending finalization metadata from token's sdkData.
5998
+ */
5999
+ parsePendingFinalization(sdkData) {
6000
+ if (!sdkData) return null;
6001
+ try {
6002
+ const data = JSON.parse(sdkData);
6003
+ if (data._pendingFinalization && data._pendingFinalization.type === "v5_bundle") {
6004
+ return data._pendingFinalization;
6005
+ }
6006
+ return null;
6007
+ } catch {
6008
+ return null;
6009
+ }
6010
+ }
6011
+ /**
6012
+ * Update pending finalization metadata in token's sdkData.
6013
+ * Creates a new token object since sdkData is readonly.
6014
+ */
6015
+ updatePendingFinalization(token, pending2) {
6016
+ const updated = {
6017
+ id: token.id,
6018
+ coinId: token.coinId,
6019
+ symbol: token.symbol,
6020
+ name: token.name,
6021
+ decimals: token.decimals,
6022
+ iconUrl: token.iconUrl,
6023
+ amount: token.amount,
6024
+ status: token.status,
6025
+ createdAt: token.createdAt,
6026
+ updatedAt: Date.now(),
6027
+ sdkData: JSON.stringify({ _pendingFinalization: pending2 })
6028
+ };
6029
+ this.tokens.set(token.id, updated);
6030
+ }
6031
+ /**
6032
+ * Save pending V5 tokens to key-value storage.
6033
+ * These tokens can't be serialized to TXF format (no genesis/state),
6034
+ * so we persist them separately and restore on load().
6035
+ */
6036
+ async savePendingV5Tokens() {
6037
+ const pendingTokens = [];
6038
+ for (const token of this.tokens.values()) {
6039
+ if (this.parsePendingFinalization(token.sdkData)) {
6040
+ pendingTokens.push(token);
6041
+ }
6042
+ }
6043
+ if (pendingTokens.length > 0) {
6044
+ await this.deps.storage.set(
6045
+ STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS,
6046
+ JSON.stringify(pendingTokens)
6047
+ );
6048
+ } else {
6049
+ await this.deps.storage.set(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS, "");
6050
+ }
6051
+ }
6052
+ /**
6053
+ * Load pending V5 tokens from key-value storage and merge into tokens map.
6054
+ * Called during load() to restore tokens that TXF format can't represent.
6055
+ */
6056
+ async loadPendingV5Tokens() {
6057
+ const data = await this.deps.storage.get(STORAGE_KEYS_ADDRESS.PENDING_V5_TOKENS);
6058
+ if (!data) return;
6059
+ try {
6060
+ const pendingTokens = JSON.parse(data);
6061
+ for (const token of pendingTokens) {
6062
+ if (!this.tokens.has(token.id)) {
6063
+ this.tokens.set(token.id, token);
6064
+ }
6065
+ }
6066
+ if (pendingTokens.length > 0) {
6067
+ this.log(`Restored ${pendingTokens.length} pending V5 token(s)`);
6068
+ }
6069
+ } catch {
6070
+ }
6071
+ }
6072
+ // ===========================================================================
5385
6073
  // Public API - Token Operations
5386
6074
  // ===========================================================================
5387
6075
  /**
5388
- * Add a token
5389
- * Tokens are uniquely identified by (tokenId, stateHash) composite key.
5390
- * Multiple historic states of the same token can coexist.
5391
- * @returns false if exact duplicate (same tokenId AND same stateHash)
6076
+ * Add a token to the wallet.
6077
+ *
6078
+ * Tokens are uniquely identified by a `(tokenId, stateHash)` composite key.
6079
+ * Duplicate detection:
6080
+ * - **Tombstoned** — rejected if the exact `(tokenId, stateHash)` pair has a tombstone.
6081
+ * - **Exact duplicate** — rejected if a token with the same composite key already exists.
6082
+ * - **State replacement** — if the same `tokenId` exists with a *different* `stateHash`,
6083
+ * the old state is archived and replaced with the incoming one.
6084
+ *
6085
+ * @param token - The token to add.
6086
+ * @param skipHistory - When `true`, do not create a `RECEIVED` transaction history entry (default `false`).
6087
+ * @returns `true` if the token was added, `false` if rejected as duplicate or tombstoned.
5392
6088
  */
5393
6089
  async addToken(token, skipHistory = false) {
5394
6090
  this.ensureInitialized();
@@ -5446,7 +6142,9 @@ var PaymentsModule = class _PaymentsModule {
5446
6142
  });
5447
6143
  }
5448
6144
  await this.save();
5449
- await this.saveTokenToFileStorage(token);
6145
+ if (!this.parsePendingFinalization(token.sdkData)) {
6146
+ await this.saveTokenToFileStorage(token);
6147
+ }
5450
6148
  this.log(`Added token ${token.id}, total: ${this.tokens.size}`);
5451
6149
  return true;
5452
6150
  }
@@ -5503,6 +6201,9 @@ var PaymentsModule = class _PaymentsModule {
5503
6201
  const data = fileData;
5504
6202
  const tokenJson = data.token;
5505
6203
  if (!tokenJson) continue;
6204
+ if (typeof tokenJson === "object" && tokenJson !== null && "_pendingFinalization" in tokenJson) {
6205
+ continue;
6206
+ }
5506
6207
  let sdkTokenId;
5507
6208
  if (typeof tokenJson === "object" && tokenJson !== null) {
5508
6209
  const tokenObj = tokenJson;
@@ -5554,7 +6255,12 @@ var PaymentsModule = class _PaymentsModule {
5554
6255
  this.log(`Loaded ${this.tokens.size} tokens from file storage`);
5555
6256
  }
5556
6257
  /**
5557
- * Update an existing token
6258
+ * Update an existing token or add it if not found.
6259
+ *
6260
+ * Looks up the token by genesis `tokenId` (from `sdkData`) first, then by
6261
+ * `token.id`. If no match is found, falls back to {@link addToken}.
6262
+ *
6263
+ * @param token - The token with updated data. Must include a valid `id`.
5558
6264
  */
5559
6265
  async updateToken(token) {
5560
6266
  this.ensureInitialized();
@@ -5578,7 +6284,15 @@ var PaymentsModule = class _PaymentsModule {
5578
6284
  this.log(`Updated token ${token.id}`);
5579
6285
  }
5580
6286
  /**
5581
- * Remove a token by ID
6287
+ * Remove a token from the wallet.
6288
+ *
6289
+ * The token is archived first, then a tombstone `(tokenId, stateHash)` is
6290
+ * created to prevent re-addition via Nostr re-delivery. A `SENT` history
6291
+ * entry is created unless `skipHistory` is `true`.
6292
+ *
6293
+ * @param tokenId - Local UUID of the token to remove.
6294
+ * @param recipientNametag - Optional nametag of the transfer recipient (for history).
6295
+ * @param skipHistory - When `true`, skip creating a transaction history entry (default `false`).
5582
6296
  */
5583
6297
  async removeToken(tokenId, recipientNametag, skipHistory = false) {
5584
6298
  this.ensureInitialized();
@@ -5640,13 +6354,22 @@ var PaymentsModule = class _PaymentsModule {
5640
6354
  // Public API - Tombstones
5641
6355
  // ===========================================================================
5642
6356
  /**
5643
- * Get all tombstones
6357
+ * Get all tombstone entries.
6358
+ *
6359
+ * Each tombstone is keyed by `(tokenId, stateHash)` and prevents a spent
6360
+ * token state from being re-added (e.g. via Nostr re-delivery).
6361
+ *
6362
+ * @returns A shallow copy of the tombstone array.
5644
6363
  */
5645
6364
  getTombstones() {
5646
6365
  return [...this.tombstones];
5647
6366
  }
5648
6367
  /**
5649
- * Check if token state is tombstoned
6368
+ * Check whether a specific `(tokenId, stateHash)` combination is tombstoned.
6369
+ *
6370
+ * @param tokenId - The genesis token ID.
6371
+ * @param stateHash - The state hash of the token version to check.
6372
+ * @returns `true` if the exact combination has been tombstoned.
5650
6373
  */
5651
6374
  isStateTombstoned(tokenId, stateHash) {
5652
6375
  return this.tombstones.some(
@@ -5654,8 +6377,13 @@ var PaymentsModule = class _PaymentsModule {
5654
6377
  );
5655
6378
  }
5656
6379
  /**
5657
- * Merge remote tombstones
5658
- * @returns number of local tokens removed
6380
+ * Merge tombstones received from a remote sync source.
6381
+ *
6382
+ * Any local token whose `(tokenId, stateHash)` matches a remote tombstone is
6383
+ * removed. The remote tombstones are then added to the local set (union merge).
6384
+ *
6385
+ * @param remoteTombstones - Tombstone entries from the remote source.
6386
+ * @returns Number of local tokens that were removed.
5659
6387
  */
5660
6388
  async mergeTombstones(remoteTombstones) {
5661
6389
  this.ensureInitialized();
@@ -5691,7 +6419,9 @@ var PaymentsModule = class _PaymentsModule {
5691
6419
  return removedCount;
5692
6420
  }
5693
6421
  /**
5694
- * Prune old tombstones
6422
+ * Remove tombstones older than `maxAge` and cap the list at 100 entries.
6423
+ *
6424
+ * @param maxAge - Maximum age in milliseconds (default: 30 days).
5695
6425
  */
5696
6426
  async pruneTombstones(maxAge) {
5697
6427
  const originalCount = this.tombstones.length;
@@ -5705,20 +6435,38 @@ var PaymentsModule = class _PaymentsModule {
5705
6435
  // Public API - Archives
5706
6436
  // ===========================================================================
5707
6437
  /**
5708
- * Get archived tokens
6438
+ * Get all archived (spent/superseded) tokens in TXF format.
6439
+ *
6440
+ * Archived tokens are kept for recovery and sync purposes. The map key is
6441
+ * the genesis token ID.
6442
+ *
6443
+ * @returns A shallow copy of the archived token map.
5709
6444
  */
5710
6445
  getArchivedTokens() {
5711
6446
  return new Map(this.archivedTokens);
5712
6447
  }
5713
6448
  /**
5714
- * Get best archived version of a token
6449
+ * Get the best (most committed transactions) archived version of a token.
6450
+ *
6451
+ * Searches both archived and forked token maps and returns the version with
6452
+ * the highest number of committed transactions.
6453
+ *
6454
+ * @param tokenId - The genesis token ID to look up.
6455
+ * @returns The best TXF token version, or `null` if not found.
5715
6456
  */
5716
6457
  getBestArchivedVersion(tokenId) {
5717
6458
  return findBestTokenVersion(tokenId, this.archivedTokens, this.forkedTokens);
5718
6459
  }
5719
6460
  /**
5720
- * Merge remote archived tokens
5721
- * @returns number of tokens updated/added
6461
+ * Merge archived tokens from a remote sync source.
6462
+ *
6463
+ * For each remote token:
6464
+ * - If missing locally, it is added.
6465
+ * - If the remote version is an incremental update of the local, it replaces it.
6466
+ * - If the histories diverge (fork), the remote version is stored via {@link storeForkedToken}.
6467
+ *
6468
+ * @param remoteArchived - Map of genesis token ID → TXF token from remote.
6469
+ * @returns Number of tokens that were updated or added locally.
5722
6470
  */
5723
6471
  async mergeArchivedTokens(remoteArchived) {
5724
6472
  let mergedCount = 0;
@@ -5741,7 +6489,11 @@ var PaymentsModule = class _PaymentsModule {
5741
6489
  return mergedCount;
5742
6490
  }
5743
6491
  /**
5744
- * Prune archived tokens
6492
+ * Prune archived tokens to keep at most `maxCount` entries.
6493
+ *
6494
+ * Oldest entries (by insertion order) are removed first.
6495
+ *
6496
+ * @param maxCount - Maximum number of archived tokens to retain (default: 100).
5745
6497
  */
5746
6498
  async pruneArchivedTokens(maxCount = 100) {
5747
6499
  if (this.archivedTokens.size <= maxCount) return;
@@ -5754,13 +6506,24 @@ var PaymentsModule = class _PaymentsModule {
5754
6506
  // Public API - Forked Tokens
5755
6507
  // ===========================================================================
5756
6508
  /**
5757
- * Get forked tokens
6509
+ * Get all forked token versions.
6510
+ *
6511
+ * Forked tokens represent alternative histories detected during sync.
6512
+ * The map key is `{tokenId}_{stateHash}`.
6513
+ *
6514
+ * @returns A shallow copy of the forked tokens map.
5758
6515
  */
5759
6516
  getForkedTokens() {
5760
6517
  return new Map(this.forkedTokens);
5761
6518
  }
5762
6519
  /**
5763
- * Store a forked token
6520
+ * Store a forked token version (alternative history).
6521
+ *
6522
+ * No-op if the exact `(tokenId, stateHash)` key already exists.
6523
+ *
6524
+ * @param tokenId - Genesis token ID.
6525
+ * @param stateHash - State hash of this forked version.
6526
+ * @param txfToken - The TXF token data to store.
5764
6527
  */
5765
6528
  async storeForkedToken(tokenId, stateHash, txfToken) {
5766
6529
  const key = `${tokenId}_${stateHash}`;
@@ -5770,8 +6533,10 @@ var PaymentsModule = class _PaymentsModule {
5770
6533
  await this.save();
5771
6534
  }
5772
6535
  /**
5773
- * Merge remote forked tokens
5774
- * @returns number of tokens added
6536
+ * Merge forked tokens from a remote sync source. Only new keys are added.
6537
+ *
6538
+ * @param remoteForked - Map of `{tokenId}_{stateHash}` → TXF token from remote.
6539
+ * @returns Number of new forked tokens added.
5775
6540
  */
5776
6541
  async mergeForkedTokens(remoteForked) {
5777
6542
  let addedCount = 0;
@@ -5787,7 +6552,9 @@ var PaymentsModule = class _PaymentsModule {
5787
6552
  return addedCount;
5788
6553
  }
5789
6554
  /**
5790
- * Prune forked tokens
6555
+ * Prune forked tokens to keep at most `maxCount` entries.
6556
+ *
6557
+ * @param maxCount - Maximum number of forked tokens to retain (default: 50).
5791
6558
  */
5792
6559
  async pruneForkedTokens(maxCount = 50) {
5793
6560
  if (this.forkedTokens.size <= maxCount) return;
@@ -5800,13 +6567,19 @@ var PaymentsModule = class _PaymentsModule {
5800
6567
  // Public API - Transaction History
5801
6568
  // ===========================================================================
5802
6569
  /**
5803
- * Get transaction history
6570
+ * Get the transaction history sorted newest-first.
6571
+ *
6572
+ * @returns Array of {@link TransactionHistoryEntry} objects in descending timestamp order.
5804
6573
  */
5805
6574
  getHistory() {
5806
6575
  return [...this.transactionHistory].sort((a, b) => b.timestamp - a.timestamp);
5807
6576
  }
5808
6577
  /**
5809
- * Add to transaction history
6578
+ * Append an entry to the transaction history.
6579
+ *
6580
+ * A unique `id` is auto-generated. The entry is immediately persisted to storage.
6581
+ *
6582
+ * @param entry - History entry fields (without `id`).
5810
6583
  */
5811
6584
  async addToHistory(entry) {
5812
6585
  this.ensureInitialized();
@@ -5824,7 +6597,11 @@ var PaymentsModule = class _PaymentsModule {
5824
6597
  // Public API - Nametag
5825
6598
  // ===========================================================================
5826
6599
  /**
5827
- * Set nametag for current identity
6600
+ * Set the nametag data for the current identity.
6601
+ *
6602
+ * Persists to both key-value storage and file storage (lottery compatibility).
6603
+ *
6604
+ * @param nametag - The nametag data including minted token JSON.
5828
6605
  */
5829
6606
  async setNametag(nametag) {
5830
6607
  this.ensureInitialized();
@@ -5834,19 +6611,23 @@ var PaymentsModule = class _PaymentsModule {
5834
6611
  this.log(`Nametag set: ${nametag.name}`);
5835
6612
  }
5836
6613
  /**
5837
- * Get nametag
6614
+ * Get the current nametag data.
6615
+ *
6616
+ * @returns The nametag data, or `null` if no nametag is set.
5838
6617
  */
5839
6618
  getNametag() {
5840
6619
  return this.nametag;
5841
6620
  }
5842
6621
  /**
5843
- * Check if has nametag
6622
+ * Check whether a nametag is currently set.
6623
+ *
6624
+ * @returns `true` if nametag data is present.
5844
6625
  */
5845
6626
  hasNametag() {
5846
6627
  return this.nametag !== null;
5847
6628
  }
5848
6629
  /**
5849
- * Clear nametag
6630
+ * Remove the current nametag data from memory and storage.
5850
6631
  */
5851
6632
  async clearNametag() {
5852
6633
  this.ensureInitialized();
@@ -5940,9 +6721,9 @@ var PaymentsModule = class _PaymentsModule {
5940
6721
  try {
5941
6722
  const signingService = await this.createSigningService();
5942
6723
  const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5943
- const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6724
+ const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5944
6725
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
5945
- const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
6726
+ const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5946
6727
  const addressRef = await UnmaskedPredicateReference4.create(
5947
6728
  tokenType,
5948
6729
  signingService.algorithm,
@@ -6003,11 +6784,27 @@ var PaymentsModule = class _PaymentsModule {
6003
6784
  // Public API - Sync & Validate
6004
6785
  // ===========================================================================
6005
6786
  /**
6006
- * Sync with all token storage providers (IPFS, MongoDB, etc.)
6007
- * Syncs with each provider and merges results
6787
+ * Sync local token state with all configured token storage providers (IPFS, file, etc.).
6788
+ *
6789
+ * For each provider, the local data is packaged into TXF storage format, sent
6790
+ * to the provider's `sync()` method, and the merged result is applied locally.
6791
+ * Emits `sync:started`, `sync:completed`, and `sync:error` events.
6792
+ *
6793
+ * @returns Summary with counts of tokens added and removed during sync.
6008
6794
  */
6009
6795
  async sync() {
6010
6796
  this.ensureInitialized();
6797
+ if (this._syncInProgress) {
6798
+ return this._syncInProgress;
6799
+ }
6800
+ this._syncInProgress = this._doSync();
6801
+ try {
6802
+ return await this._syncInProgress;
6803
+ } finally {
6804
+ this._syncInProgress = null;
6805
+ }
6806
+ }
6807
+ async _doSync() {
6011
6808
  this.deps.emitEvent("sync:started", { source: "payments" });
6012
6809
  try {
6013
6810
  const providers = this.getTokenStorageProviders();
@@ -6045,6 +6842,9 @@ var PaymentsModule = class _PaymentsModule {
6045
6842
  });
6046
6843
  }
6047
6844
  }
6845
+ if (totalAdded > 0 || totalRemoved > 0) {
6846
+ await this.save();
6847
+ }
6048
6848
  this.deps.emitEvent("sync:completed", {
6049
6849
  source: "payments",
6050
6850
  count: this.tokens.size
@@ -6058,6 +6858,66 @@ var PaymentsModule = class _PaymentsModule {
6058
6858
  throw error;
6059
6859
  }
6060
6860
  }
6861
+ // ===========================================================================
6862
+ // Storage Event Subscription (Push-Based Sync)
6863
+ // ===========================================================================
6864
+ /**
6865
+ * Subscribe to 'storage:remote-updated' events from all token storage providers.
6866
+ * When a provider emits this event, a debounced sync is triggered.
6867
+ */
6868
+ subscribeToStorageEvents() {
6869
+ this.unsubscribeStorageEvents();
6870
+ const providers = this.getTokenStorageProviders();
6871
+ for (const [providerId, provider] of providers) {
6872
+ if (provider.onEvent) {
6873
+ const unsub = provider.onEvent((event) => {
6874
+ if (event.type === "storage:remote-updated") {
6875
+ this.log("Remote update detected from provider", providerId, event.data);
6876
+ this.debouncedSyncFromRemoteUpdate(providerId, event.data);
6877
+ }
6878
+ });
6879
+ this.storageEventUnsubscribers.push(unsub);
6880
+ }
6881
+ }
6882
+ }
6883
+ /**
6884
+ * Unsubscribe from all storage provider events and clear debounce timer.
6885
+ */
6886
+ unsubscribeStorageEvents() {
6887
+ for (const unsub of this.storageEventUnsubscribers) {
6888
+ unsub();
6889
+ }
6890
+ this.storageEventUnsubscribers = [];
6891
+ if (this.syncDebounceTimer) {
6892
+ clearTimeout(this.syncDebounceTimer);
6893
+ this.syncDebounceTimer = null;
6894
+ }
6895
+ }
6896
+ /**
6897
+ * Debounced sync triggered by a storage:remote-updated event.
6898
+ * Waits 500ms to batch rapid updates, then performs sync.
6899
+ */
6900
+ debouncedSyncFromRemoteUpdate(providerId, eventData) {
6901
+ if (this.syncDebounceTimer) {
6902
+ clearTimeout(this.syncDebounceTimer);
6903
+ }
6904
+ this.syncDebounceTimer = setTimeout(() => {
6905
+ this.syncDebounceTimer = null;
6906
+ this.sync().then((result) => {
6907
+ const data = eventData;
6908
+ this.deps?.emitEvent("sync:remote-update", {
6909
+ providerId,
6910
+ name: data?.name ?? "",
6911
+ sequence: data?.sequence ?? 0,
6912
+ cid: data?.cid ?? "",
6913
+ added: result.added,
6914
+ removed: result.removed
6915
+ });
6916
+ }).catch((err) => {
6917
+ this.log("Auto-sync from remote update failed:", err);
6918
+ });
6919
+ }, _PaymentsModule.SYNC_DEBOUNCE_MS);
6920
+ }
6061
6921
  /**
6062
6922
  * Get all active token storage providers
6063
6923
  */
@@ -6073,15 +6933,24 @@ var PaymentsModule = class _PaymentsModule {
6073
6933
  return /* @__PURE__ */ new Map();
6074
6934
  }
6075
6935
  /**
6076
- * Update token storage providers (called when providers are added/removed dynamically)
6936
+ * Replace the set of token storage providers at runtime.
6937
+ *
6938
+ * Use when providers are added or removed dynamically (e.g. IPFS node started).
6939
+ *
6940
+ * @param providers - New map of provider ID → TokenStorageProvider.
6077
6941
  */
6078
6942
  updateTokenStorageProviders(providers) {
6079
6943
  if (this.deps) {
6080
6944
  this.deps.tokenStorageProviders = providers;
6945
+ this.subscribeToStorageEvents();
6081
6946
  }
6082
6947
  }
6083
6948
  /**
6084
- * Validate tokens with aggregator
6949
+ * Validate all tokens against the aggregator (oracle provider).
6950
+ *
6951
+ * Tokens that fail validation or are detected as spent are marked `'invalid'`.
6952
+ *
6953
+ * @returns Object with arrays of valid and invalid tokens.
6085
6954
  */
6086
6955
  async validate() {
6087
6956
  this.ensureInitialized();
@@ -6102,7 +6971,9 @@ var PaymentsModule = class _PaymentsModule {
6102
6971
  return { valid, invalid };
6103
6972
  }
6104
6973
  /**
6105
- * Get pending transfers
6974
+ * Get all in-progress (pending) outgoing transfers.
6975
+ *
6976
+ * @returns Array of {@link TransferResult} objects for transfers that have not yet completed.
6106
6977
  */
6107
6978
  getPendingTransfers() {
6108
6979
  return Array.from(this.pendingTransfers.values());
@@ -6166,9 +7037,9 @@ var PaymentsModule = class _PaymentsModule {
6166
7037
  */
6167
7038
  async createDirectAddressFromPubkey(pubkeyHex) {
6168
7039
  const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
6169
- const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
7040
+ const { TokenType: TokenType6 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6170
7041
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
6171
- const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
7042
+ const tokenType = new TokenType6(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
6172
7043
  const pubkeyBytes = new Uint8Array(
6173
7044
  pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
6174
7045
  );
@@ -6380,7 +7251,8 @@ var PaymentsModule = class _PaymentsModule {
6380
7251
  this.deps.emitEvent("transfer:confirmed", {
6381
7252
  id: crypto.randomUUID(),
6382
7253
  status: "completed",
6383
- tokens: [finalizedToken]
7254
+ tokens: [finalizedToken],
7255
+ tokenTransfers: []
6384
7256
  });
6385
7257
  await this.addToHistory({
6386
7258
  type: "RECEIVED",
@@ -6403,14 +7275,26 @@ var PaymentsModule = class _PaymentsModule {
6403
7275
  async handleIncomingTransfer(transfer) {
6404
7276
  try {
6405
7277
  const payload = transfer.payload;
7278
+ let instantBundle = null;
6406
7279
  if (isInstantSplitBundle(payload)) {
7280
+ instantBundle = payload;
7281
+ } else if (payload.token) {
7282
+ try {
7283
+ const inner = typeof payload.token === "string" ? JSON.parse(payload.token) : payload.token;
7284
+ if (isInstantSplitBundle(inner)) {
7285
+ instantBundle = inner;
7286
+ }
7287
+ } catch {
7288
+ }
7289
+ }
7290
+ if (instantBundle) {
6407
7291
  this.log("Processing INSTANT_SPLIT bundle...");
6408
7292
  try {
6409
7293
  if (!this.nametag) {
6410
7294
  await this.loadNametagFromFileStorage();
6411
7295
  }
6412
7296
  const result = await this.processInstantSplitBundle(
6413
- payload,
7297
+ instantBundle,
6414
7298
  transfer.senderTransportPubkey
6415
7299
  );
6416
7300
  if (result.success) {
@@ -6423,6 +7307,11 @@ var PaymentsModule = class _PaymentsModule {
6423
7307
  }
6424
7308
  return;
6425
7309
  }
7310
+ if (payload.sourceToken && payload.commitmentData && !payload.transferTx) {
7311
+ this.log("Processing NOSTR-FIRST commitment-only transfer...");
7312
+ await this.handleCommitmentOnlyTransfer(transfer, payload);
7313
+ return;
7314
+ }
6426
7315
  let tokenData;
6427
7316
  let finalizedSdkToken = null;
6428
7317
  if (payload.sourceToken && payload.transferTx) {
@@ -6578,6 +7467,7 @@ var PaymentsModule = class _PaymentsModule {
6578
7467
  console.error(`[Payments] Failed to save to provider ${id}:`, err);
6579
7468
  }
6580
7469
  }
7470
+ await this.savePendingV5Tokens();
6581
7471
  }
6582
7472
  async saveToOutbox(transfer, recipient) {
6583
7473
  const outbox = await this.loadOutbox();
@@ -6595,8 +7485,7 @@ var PaymentsModule = class _PaymentsModule {
6595
7485
  }
6596
7486
  async createStorageData() {
6597
7487
  return await buildTxfStorageData(
6598
- [],
6599
- // Empty - active tokens stored as token-xxx files
7488
+ Array.from(this.tokens.values()),
6600
7489
  {
6601
7490
  version: 1,
6602
7491
  address: this.deps.identity.l1Address,
@@ -6781,7 +7670,7 @@ function createPaymentsModule(config) {
6781
7670
  // modules/payments/TokenRecoveryService.ts
6782
7671
  var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
6783
7672
  var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
6784
- var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
7673
+ var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6785
7674
  var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
6786
7675
  var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
6787
7676
  var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
@@ -7845,15 +8734,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
7845
8734
 
7846
8735
  // core/Sphere.ts
7847
8736
  var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
7848
- var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
8737
+ var import_TokenType5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
7849
8738
  var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
7850
8739
  var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
8740
+ var import_nostr_js_sdk2 = require("@unicitylabs/nostr-js-sdk");
8741
+ function isValidNametag(nametag) {
8742
+ if ((0, import_nostr_js_sdk2.isPhoneNumber)(nametag)) return true;
8743
+ return /^[a-z0-9_-]{3,20}$/.test(nametag);
8744
+ }
7851
8745
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
7852
8746
  async function deriveL3PredicateAddress(privateKey) {
7853
8747
  const secret = Buffer.from(privateKey, "hex");
7854
8748
  const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
7855
8749
  const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
7856
- const tokenType = new import_TokenType4.TokenType(tokenTypeBytes);
8750
+ const tokenType = new import_TokenType5.TokenType(tokenTypeBytes);
7857
8751
  const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
7858
8752
  tokenType,
7859
8753
  signingService.algorithm,
@@ -8019,8 +8913,8 @@ var Sphere = class _Sphere {
8019
8913
  if (options.nametag) {
8020
8914
  await sphere.registerNametag(options.nametag);
8021
8915
  } else {
8022
- await sphere.syncIdentityWithTransport();
8023
8916
  await sphere.recoverNametagFromTransport();
8917
+ await sphere.syncIdentityWithTransport();
8024
8918
  }
8025
8919
  return sphere;
8026
8920
  }
@@ -8067,9 +8961,14 @@ var Sphere = class _Sphere {
8067
8961
  if (!options.mnemonic && !options.masterKey) {
8068
8962
  throw new Error("Either mnemonic or masterKey is required");
8069
8963
  }
8964
+ console.log("[Sphere.import] Starting import...");
8965
+ console.log("[Sphere.import] Clearing existing wallet data...");
8070
8966
  await _Sphere.clear({ storage: options.storage, tokenStorage: options.tokenStorage });
8967
+ console.log("[Sphere.import] Clear done");
8071
8968
  if (!options.storage.isConnected()) {
8969
+ console.log("[Sphere.import] Reconnecting storage...");
8072
8970
  await options.storage.connect();
8971
+ console.log("[Sphere.import] Storage reconnected");
8073
8972
  }
8074
8973
  const sphere = new _Sphere(
8075
8974
  options.storage,
@@ -8083,9 +8982,12 @@ var Sphere = class _Sphere {
8083
8982
  if (!_Sphere.validateMnemonic(options.mnemonic)) {
8084
8983
  throw new Error("Invalid mnemonic");
8085
8984
  }
8985
+ console.log("[Sphere.import] Storing mnemonic...");
8086
8986
  await sphere.storeMnemonic(options.mnemonic, options.derivationPath, options.basePath);
8987
+ console.log("[Sphere.import] Initializing identity from mnemonic...");
8087
8988
  await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
8088
8989
  } else if (options.masterKey) {
8990
+ console.log("[Sphere.import] Storing master key...");
8089
8991
  await sphere.storeMasterKey(
8090
8992
  options.masterKey,
8091
8993
  options.chainCode,
@@ -8093,24 +8995,43 @@ var Sphere = class _Sphere {
8093
8995
  options.basePath,
8094
8996
  options.derivationMode
8095
8997
  );
8998
+ console.log("[Sphere.import] Initializing identity from master key...");
8096
8999
  await sphere.initializeIdentityFromMasterKey(
8097
9000
  options.masterKey,
8098
9001
  options.chainCode,
8099
9002
  options.derivationPath
8100
9003
  );
8101
9004
  }
9005
+ console.log("[Sphere.import] Initializing providers...");
8102
9006
  await sphere.initializeProviders();
9007
+ console.log("[Sphere.import] Providers initialized. Initializing modules...");
8103
9008
  await sphere.initializeModules();
9009
+ console.log("[Sphere.import] Modules initialized");
8104
9010
  if (!options.nametag) {
9011
+ console.log("[Sphere.import] Recovering nametag from transport...");
8105
9012
  await sphere.recoverNametagFromTransport();
9013
+ console.log("[Sphere.import] Nametag recovery done");
9014
+ await sphere.syncIdentityWithTransport();
8106
9015
  }
9016
+ console.log("[Sphere.import] Finalizing wallet creation...");
8107
9017
  await sphere.finalizeWalletCreation();
8108
9018
  sphere._initialized = true;
8109
9019
  _Sphere.instance = sphere;
9020
+ console.log("[Sphere.import] Tracking address 0...");
8110
9021
  await sphere.ensureAddressTracked(0);
8111
9022
  if (options.nametag) {
9023
+ console.log("[Sphere.import] Registering nametag...");
8112
9024
  await sphere.registerNametag(options.nametag);
8113
9025
  }
9026
+ if (sphere._tokenStorageProviders.size > 0) {
9027
+ try {
9028
+ const syncResult = await sphere._payments.sync();
9029
+ console.log(`[Sphere.import] Auto-sync: +${syncResult.added} -${syncResult.removed}`);
9030
+ } catch (err) {
9031
+ console.warn("[Sphere.import] Auto-sync failed (non-fatal):", err);
9032
+ }
9033
+ }
9034
+ console.log("[Sphere.import] Import complete");
8114
9035
  return sphere;
8115
9036
  }
8116
9037
  /**
@@ -8135,6 +9056,10 @@ var Sphere = class _Sphere {
8135
9056
  static async clear(storageOrOptions) {
8136
9057
  const storage = "get" in storageOrOptions ? storageOrOptions : storageOrOptions.storage;
8137
9058
  const tokenStorage = "get" in storageOrOptions ? void 0 : storageOrOptions.tokenStorage;
9059
+ if (!storage.isConnected()) {
9060
+ await storage.connect();
9061
+ }
9062
+ console.log("[Sphere.clear] Removing storage keys...");
8138
9063
  await storage.remove(STORAGE_KEYS_GLOBAL.MNEMONIC);
8139
9064
  await storage.remove(STORAGE_KEYS_GLOBAL.MASTER_KEY);
8140
9065
  await storage.remove(STORAGE_KEYS_GLOBAL.CHAIN_CODE);
@@ -8147,12 +9072,30 @@ var Sphere = class _Sphere {
8147
9072
  await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
8148
9073
  await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
8149
9074
  await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
9075
+ console.log("[Sphere.clear] Storage keys removed");
8150
9076
  if (tokenStorage?.clear) {
8151
- await tokenStorage.clear();
9077
+ console.log("[Sphere.clear] Clearing token storage...");
9078
+ try {
9079
+ await Promise.race([
9080
+ tokenStorage.clear(),
9081
+ new Promise(
9082
+ (_, reject) => setTimeout(() => reject(new Error("tokenStorage.clear() timed out after 2s")), 2e3)
9083
+ )
9084
+ ]);
9085
+ console.log("[Sphere.clear] Token storage cleared");
9086
+ } catch (err) {
9087
+ console.warn("[Sphere.clear] Token storage clear failed/timed out:", err);
9088
+ }
8152
9089
  }
9090
+ console.log("[Sphere.clear] Destroying vesting classifier...");
8153
9091
  await vestingClassifier.destroy();
9092
+ console.log("[Sphere.clear] Vesting classifier destroyed");
8154
9093
  if (_Sphere.instance) {
9094
+ console.log("[Sphere.clear] Destroying Sphere instance...");
8155
9095
  await _Sphere.instance.destroy();
9096
+ console.log("[Sphere.clear] Sphere instance destroyed");
9097
+ } else {
9098
+ console.log("[Sphere.clear] No Sphere instance to destroy");
8156
9099
  }
8157
9100
  }
8158
9101
  /**
@@ -8533,7 +9476,8 @@ var Sphere = class _Sphere {
8533
9476
  storage: options.storage,
8534
9477
  transport: options.transport,
8535
9478
  oracle: options.oracle,
8536
- tokenStorage: options.tokenStorage
9479
+ tokenStorage: options.tokenStorage,
9480
+ l1: options.l1
8537
9481
  });
8538
9482
  return { success: true, mnemonic };
8539
9483
  }
@@ -8546,7 +9490,8 @@ var Sphere = class _Sphere {
8546
9490
  storage: options.storage,
8547
9491
  transport: options.transport,
8548
9492
  oracle: options.oracle,
8549
- tokenStorage: options.tokenStorage
9493
+ tokenStorage: options.tokenStorage,
9494
+ l1: options.l1
8550
9495
  });
8551
9496
  return { success: true };
8552
9497
  }
@@ -8605,7 +9550,8 @@ var Sphere = class _Sphere {
8605
9550
  transport: options.transport,
8606
9551
  oracle: options.oracle,
8607
9552
  tokenStorage: options.tokenStorage,
8608
- nametag: options.nametag
9553
+ nametag: options.nametag,
9554
+ l1: options.l1
8609
9555
  });
8610
9556
  return { success: true, sphere, mnemonic };
8611
9557
  }
@@ -8634,7 +9580,8 @@ var Sphere = class _Sphere {
8634
9580
  transport: options.transport,
8635
9581
  oracle: options.oracle,
8636
9582
  tokenStorage: options.tokenStorage,
8637
- nametag: options.nametag
9583
+ nametag: options.nametag,
9584
+ l1: options.l1
8638
9585
  });
8639
9586
  return { success: true, sphere };
8640
9587
  }
@@ -8665,7 +9612,8 @@ var Sphere = class _Sphere {
8665
9612
  transport: options.transport,
8666
9613
  oracle: options.oracle,
8667
9614
  tokenStorage: options.tokenStorage,
8668
- nametag: options.nametag
9615
+ nametag: options.nametag,
9616
+ l1: options.l1
8669
9617
  });
8670
9618
  return { success: true, sphere };
8671
9619
  }
@@ -8684,7 +9632,8 @@ var Sphere = class _Sphere {
8684
9632
  storage: options.storage,
8685
9633
  transport: options.transport,
8686
9634
  oracle: options.oracle,
8687
- tokenStorage: options.tokenStorage
9635
+ tokenStorage: options.tokenStorage,
9636
+ l1: options.l1
8688
9637
  });
8689
9638
  if (result.success) {
8690
9639
  const sphere2 = _Sphere.getInstance();
@@ -8733,7 +9682,8 @@ var Sphere = class _Sphere {
8733
9682
  transport: options.transport,
8734
9683
  oracle: options.oracle,
8735
9684
  tokenStorage: options.tokenStorage,
8736
- nametag: options.nametag
9685
+ nametag: options.nametag,
9686
+ l1: options.l1
8737
9687
  });
8738
9688
  return { success: true, sphere: sphere2, mnemonic };
8739
9689
  }
@@ -8746,7 +9696,8 @@ var Sphere = class _Sphere {
8746
9696
  transport: options.transport,
8747
9697
  oracle: options.oracle,
8748
9698
  tokenStorage: options.tokenStorage,
8749
- nametag: options.nametag
9699
+ nametag: options.nametag,
9700
+ l1: options.l1
8750
9701
  });
8751
9702
  return { success: true, sphere };
8752
9703
  }
@@ -8950,9 +9901,9 @@ var Sphere = class _Sphere {
8950
9901
  if (index < 0) {
8951
9902
  throw new Error("Address index must be non-negative");
8952
9903
  }
8953
- const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
8954
- if (newNametag && !this.validateNametag(newNametag)) {
8955
- throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
9904
+ const newNametag = options?.nametag ? this.cleanNametag(options.nametag) : void 0;
9905
+ if (newNametag && !isValidNametag(newNametag)) {
9906
+ throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
8956
9907
  }
8957
9908
  const addressInfo = this.deriveAddress(index, false);
8958
9909
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
@@ -9336,9 +10287,9 @@ var Sphere = class _Sphere {
9336
10287
  */
9337
10288
  async registerNametag(nametag) {
9338
10289
  this.ensureReady();
9339
- const cleanNametag = nametag.startsWith("@") ? nametag.slice(1) : nametag;
9340
- if (!this.validateNametag(cleanNametag)) {
9341
- throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
10290
+ const cleanNametag = this.cleanNametag(nametag);
10291
+ if (!isValidNametag(cleanNametag)) {
10292
+ throw new Error("Invalid nametag format. Use lowercase alphanumeric, underscore, or hyphen (3-20 chars), or a valid phone number.");
9342
10293
  }
9343
10294
  if (this._identity?.nametag) {
9344
10295
  throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
@@ -9609,46 +10560,49 @@ var Sphere = class _Sphere {
9609
10560
  if (this._identity?.nametag) {
9610
10561
  return;
9611
10562
  }
9612
- if (!this._transport.recoverNametag) {
10563
+ let recoveredNametag = null;
10564
+ if (this._transport.recoverNametag) {
10565
+ try {
10566
+ recoveredNametag = await this._transport.recoverNametag();
10567
+ } catch {
10568
+ }
10569
+ }
10570
+ if (!recoveredNametag && this._transport.resolveAddressInfo && this._identity?.l1Address) {
10571
+ try {
10572
+ const info = await this._transport.resolveAddressInfo(this._identity.l1Address);
10573
+ if (info?.nametag) {
10574
+ recoveredNametag = info.nametag;
10575
+ }
10576
+ } catch {
10577
+ }
10578
+ }
10579
+ if (!recoveredNametag) {
9613
10580
  return;
9614
10581
  }
9615
10582
  try {
9616
- const recoveredNametag = await this._transport.recoverNametag();
9617
- if (recoveredNametag) {
9618
- if (this._identity) {
9619
- this._identity.nametag = recoveredNametag;
9620
- await this._updateCachedProxyAddress();
9621
- }
9622
- const entry = await this.ensureAddressTracked(this._currentAddressIndex);
9623
- let nametags = this._addressNametags.get(entry.addressId);
9624
- if (!nametags) {
9625
- nametags = /* @__PURE__ */ new Map();
9626
- this._addressNametags.set(entry.addressId, nametags);
9627
- }
9628
- const nextIndex = nametags.size;
9629
- nametags.set(nextIndex, recoveredNametag);
9630
- await this.persistAddressNametags();
9631
- if (this._transport.publishIdentityBinding) {
9632
- await this._transport.publishIdentityBinding(
9633
- this._identity.chainPubkey,
9634
- this._identity.l1Address,
9635
- this._identity.directAddress || "",
9636
- recoveredNametag
9637
- );
9638
- }
9639
- this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
10583
+ if (this._identity) {
10584
+ this._identity.nametag = recoveredNametag;
10585
+ await this._updateCachedProxyAddress();
10586
+ }
10587
+ const entry = await this.ensureAddressTracked(this._currentAddressIndex);
10588
+ let nametags = this._addressNametags.get(entry.addressId);
10589
+ if (!nametags) {
10590
+ nametags = /* @__PURE__ */ new Map();
10591
+ this._addressNametags.set(entry.addressId, nametags);
9640
10592
  }
10593
+ const nextIndex = nametags.size;
10594
+ nametags.set(nextIndex, recoveredNametag);
10595
+ await this.persistAddressNametags();
10596
+ this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
9641
10597
  } catch {
9642
10598
  }
9643
10599
  }
9644
10600
  /**
9645
- * Validate nametag format
10601
+ * Strip @ prefix and normalize a nametag (lowercase, phone E.164, strip @unicity suffix).
9646
10602
  */
9647
- validateNametag(nametag) {
9648
- const pattern = new RegExp(
9649
- `^[a-zA-Z0-9_-]{${LIMITS.NAMETAG_MIN_LENGTH},${LIMITS.NAMETAG_MAX_LENGTH}}$`
9650
- );
9651
- return pattern.test(nametag);
10603
+ cleanNametag(raw) {
10604
+ const stripped = raw.startsWith("@") ? raw.slice(1) : raw;
10605
+ return (0, import_nostr_js_sdk2.normalizeNametag)(stripped);
9652
10606
  }
9653
10607
  // ===========================================================================
9654
10608
  // Public Methods - Lifecycle
@@ -9846,8 +10800,12 @@ var Sphere = class _Sphere {
9846
10800
  for (const provider of this._tokenStorageProviders.values()) {
9847
10801
  provider.setIdentity(this._identity);
9848
10802
  }
9849
- await this._storage.connect();
9850
- await this._transport.connect();
10803
+ if (!this._storage.isConnected()) {
10804
+ await this._storage.connect();
10805
+ }
10806
+ if (!this._transport.isConnected()) {
10807
+ await this._transport.connect();
10808
+ }
9851
10809
  await this._oracle.initialize();
9852
10810
  for (const provider of this._tokenStorageProviders.values()) {
9853
10811
  await provider.initialize();
@@ -10344,6 +11302,9 @@ function createTokenValidator(options) {
10344
11302
  return new TokenValidator(options);
10345
11303
  }
10346
11304
 
11305
+ // index.ts
11306
+ var import_nostr_js_sdk3 = require("@unicitylabs/nostr-js-sdk");
11307
+
10347
11308
  // price/CoinGeckoPriceProvider.ts
10348
11309
  var CoinGeckoPriceProvider = class {
10349
11310
  platform = "coingecko";
@@ -10480,6 +11441,7 @@ function createPriceProvider(config) {
10480
11441
  TokenRegistry,
10481
11442
  TokenValidator,
10482
11443
  archivedKeyFromTokenId,
11444
+ areSameNametag,
10483
11445
  base58Decode,
10484
11446
  base58Encode,
10485
11447
  buildTxfStorageData,
@@ -10527,6 +11489,7 @@ function createPriceProvider(config) {
10527
11489
  hasUncommittedTransactions,
10528
11490
  hasValidTxfData,
10529
11491
  hash160,
11492
+ hashNametag,
10530
11493
  hexToBytes,
10531
11494
  identityFromMnemonicSync,
10532
11495
  initSphere,
@@ -10538,10 +11501,12 @@ function createPriceProvider(config) {
10538
11501
  isKnownToken,
10539
11502
  isPaymentSessionTerminal,
10540
11503
  isPaymentSessionTimedOut,
11504
+ isPhoneNumber,
10541
11505
  isSQLiteDatabase,
10542
11506
  isTextWalletEncrypted,
10543
11507
  isTokenKey,
10544
11508
  isValidBech32,
11509
+ isValidNametag,
10545
11510
  isValidPrivateKey,
10546
11511
  isValidTokenId,
10547
11512
  isWalletDatEncrypted,
@@ -10549,6 +11514,7 @@ function createPriceProvider(config) {
10549
11514
  keyFromTokenId,
10550
11515
  loadSphere,
10551
11516
  mnemonicToSeedSync,
11517
+ normalizeNametag,
10552
11518
  normalizeSdkTokenToStorage,
10553
11519
  objectToTxf,
10554
11520
  parseAndDecryptWalletDat,