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