@unicitylabs/sphere-sdk 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  COIN_TYPES: () => COIN_TYPES,
34
+ CoinGeckoPriceProvider: () => CoinGeckoPriceProvider,
34
35
  CommunicationsModule: () => CommunicationsModule,
35
36
  DEFAULT_AGGREGATOR_TIMEOUT: () => DEFAULT_AGGREGATOR_TIMEOUT,
36
37
  DEFAULT_AGGREGATOR_URL: () => DEFAULT_AGGREGATOR_URL,
@@ -66,8 +67,12 @@ __export(index_exports, {
66
67
  createCommunicationsModule: () => createCommunicationsModule,
67
68
  createKeyPair: () => createKeyPair,
68
69
  createL1PaymentsModule: () => createL1PaymentsModule,
70
+ createPaymentSession: () => createPaymentSession,
71
+ createPaymentSessionError: () => createPaymentSessionError,
69
72
  createPaymentsModule: () => createPaymentsModule,
73
+ createPriceProvider: () => createPriceProvider,
70
74
  createSphere: () => createSphere,
75
+ createSplitPaymentSession: () => createSplitPaymentSession,
71
76
  createTokenValidator: () => createTokenValidator,
72
77
  decodeBech32: () => decodeBech32,
73
78
  decryptCMasterKey: () => decryptCMasterKey,
@@ -105,7 +110,12 @@ __export(index_exports, {
105
110
  initSphere: () => initSphere,
106
111
  isArchivedKey: () => isArchivedKey,
107
112
  isForkedKey: () => isForkedKey,
113
+ isInstantSplitBundle: () => isInstantSplitBundle,
114
+ isInstantSplitBundleV4: () => isInstantSplitBundleV4,
115
+ isInstantSplitBundleV5: () => isInstantSplitBundleV5,
108
116
  isKnownToken: () => isKnownToken,
117
+ isPaymentSessionTerminal: () => isPaymentSessionTerminal,
118
+ isPaymentSessionTimedOut: () => isPaymentSessionTimedOut,
109
119
  isSQLiteDatabase: () => isSQLiteDatabase,
110
120
  isTextWalletEncrypted: () => isTextWalletEncrypted,
111
121
  isTokenKey: () => isTokenKey,
@@ -1671,7 +1681,6 @@ var L1PaymentsModule = class {
1671
1681
  _initialized = false;
1672
1682
  _config;
1673
1683
  _identity;
1674
- _chainCode;
1675
1684
  _addresses = [];
1676
1685
  _wallet;
1677
1686
  _transport;
@@ -1685,7 +1694,6 @@ var L1PaymentsModule = class {
1685
1694
  }
1686
1695
  async initialize(deps) {
1687
1696
  this._identity = deps.identity;
1688
- this._chainCode = deps.chainCode;
1689
1697
  this._addresses = deps.addresses ?? [];
1690
1698
  this._transport = deps.transport;
1691
1699
  this._wallet = {
@@ -1721,7 +1729,6 @@ var L1PaymentsModule = class {
1721
1729
  }
1722
1730
  this._initialized = false;
1723
1731
  this._identity = void 0;
1724
- this._chainCode = void 0;
1725
1732
  this._addresses = [];
1726
1733
  this._wallet = void 0;
1727
1734
  }
@@ -1756,10 +1763,10 @@ var L1PaymentsModule = class {
1756
1763
  * Resolve nametag to L1 address using transport provider
1757
1764
  */
1758
1765
  async resolveNametagToL1Address(nametag) {
1759
- if (!this._transport?.resolveNametagInfo) {
1760
- throw new Error("Transport provider does not support nametag resolution");
1766
+ if (!this._transport?.resolve) {
1767
+ throw new Error("Transport provider does not support resolution");
1761
1768
  }
1762
- const info = await this._transport.resolveNametagInfo(nametag);
1769
+ const info = await this._transport.resolve(nametag);
1763
1770
  if (!info) {
1764
1771
  throw new Error(`Nametag not found: ${nametag}`);
1765
1772
  }
@@ -2036,25 +2043,11 @@ var TokenSplitCalculator = class {
2036
2043
  * 3. If no exact match, determine which token to split
2037
2044
  */
2038
2045
  async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
2039
- console.log(
2040
- `[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
2041
- );
2042
- console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
2043
2046
  const candidates = [];
2044
2047
  for (const t of availableTokens) {
2045
- console.log(`[SplitCalculator] Token ${t.id}: coinId=${t.coinId}, status=${t.status}, hasSdkData=${!!t.sdkData}`);
2046
- if (t.coinId !== targetCoinIdHex) {
2047
- console.log(`[SplitCalculator] Skipping token ${t.id}: coinId mismatch (${t.coinId} !== ${targetCoinIdHex})`);
2048
- continue;
2049
- }
2050
- if (t.status !== "confirmed") {
2051
- console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
2052
- continue;
2053
- }
2054
- if (!t.sdkData) {
2055
- console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
2056
- continue;
2057
- }
2048
+ if (t.coinId !== targetCoinIdHex) continue;
2049
+ if (t.status !== "confirmed") continue;
2050
+ if (!t.sdkData) continue;
2058
2051
  try {
2059
2052
  const parsed = JSON.parse(t.sdkData);
2060
2053
  const sdkToken = await import_Token.Token.fromJSON(parsed);
@@ -2082,14 +2075,12 @@ var TokenSplitCalculator = class {
2082
2075
  }
2083
2076
  const exactMatch = candidates.find((t) => t.amount === targetAmount);
2084
2077
  if (exactMatch) {
2085
- console.log("[SplitCalculator] Found exact match token");
2086
2078
  return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
2087
2079
  }
2088
2080
  const maxCombinationSize = Math.min(5, candidates.length);
2089
2081
  for (let size = 2; size <= maxCombinationSize; size++) {
2090
2082
  const combo = this.findCombinationOfSize(candidates, targetAmount, size);
2091
2083
  if (combo) {
2092
- console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
2093
2084
  return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
2094
2085
  }
2095
2086
  }
@@ -2106,9 +2097,6 @@ var TokenSplitCalculator = class {
2106
2097
  } else {
2107
2098
  const neededFromThisToken = targetAmount - currentSum;
2108
2099
  const remainderForSender = candidate.amount - neededFromThisToken;
2109
- console.log(
2110
- `[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
2111
- );
2112
2100
  return {
2113
2101
  tokensToTransferDirectly: toTransfer,
2114
2102
  tokenToSplit: candidate,
@@ -2127,16 +2115,10 @@ var TokenSplitCalculator = class {
2127
2115
  */
2128
2116
  getTokenBalance(sdkToken, coinIdHex) {
2129
2117
  try {
2130
- if (!sdkToken.coins) {
2131
- console.log("[SplitCalculator] Token has no coins");
2132
- return 0n;
2133
- }
2118
+ if (!sdkToken.coins) return 0n;
2134
2119
  const coinId = import_CoinId.CoinId.fromJSON(coinIdHex);
2135
- const balance = sdkToken.coins.get(coinId);
2136
- console.log(`[SplitCalculator] Token balance for ${coinIdHex.slice(0, 8)}...: ${balance ?? 0n}`);
2137
- return balance ?? 0n;
2138
- } catch (e) {
2139
- console.error("[SplitCalculator] Error getting token balance:", e);
2120
+ return sdkToken.coins.get(coinId) ?? 0n;
2121
+ } catch {
2140
2122
  return 0n;
2141
2123
  }
2142
2124
  }
@@ -2365,20 +2347,18 @@ var NametagMinter = class {
2365
2347
  const cleanNametag = nametag.replace("@", "").trim();
2366
2348
  this.log(`Starting mint for nametag: ${cleanNametag}`);
2367
2349
  try {
2368
- const isAvailable = await this.isNametagAvailable(cleanNametag);
2369
- if (!isAvailable) {
2370
- return {
2371
- success: false,
2372
- error: `Nametag "${cleanNametag}" is already taken`
2373
- };
2374
- }
2375
2350
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
2376
2351
  const nametagTokenType = new import_TokenType.TokenType(
2377
2352
  Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
2378
2353
  );
2379
- const salt = new Uint8Array(32);
2380
- crypto.getRandomValues(salt);
2381
- this.log("Generated salt");
2354
+ const nametagBytes = new TextEncoder().encode(cleanNametag);
2355
+ const pubKey = this.signingService.publicKey;
2356
+ const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
2357
+ saltInput.set(pubKey, 0);
2358
+ saltInput.set(nametagBytes, pubKey.length);
2359
+ const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
2360
+ const salt = new Uint8Array(saltBuffer);
2361
+ this.log("Generated deterministic salt");
2382
2362
  const mintData = await import_MintTransactionData.MintTransactionData.createFromNametag(
2383
2363
  cleanNametag,
2384
2364
  nametagTokenType,
@@ -2501,8 +2481,10 @@ var STORAGE_KEYS_GLOBAL = {
2501
2481
  WALLET_EXISTS: "wallet_exists",
2502
2482
  /** Current active address index */
2503
2483
  CURRENT_ADDRESS_INDEX: "current_address_index",
2504
- /** Index of address nametags (JSON: { "0": "alice", "1": "bob" }) - for discovery */
2505
- ADDRESS_NAMETAGS: "address_nametags"
2484
+ /** Nametag cache per address (separate from tracked addresses registry) */
2485
+ ADDRESS_NAMETAGS: "address_nametags",
2486
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
2487
+ TRACKED_ADDRESSES: "tracked_addresses"
2506
2488
  };
2507
2489
  var STORAGE_KEYS_ADDRESS = {
2508
2490
  /** Pending transfers for this address */
@@ -2951,11 +2933,16 @@ function getCurrentStateHash(txf) {
2951
2933
  if (lastTx?.newStateHash) {
2952
2934
  return lastTx.newStateHash;
2953
2935
  }
2954
- return void 0;
2936
+ if (lastTx?.inclusionProof?.authenticator?.stateHash) {
2937
+ return lastTx.inclusionProof.authenticator.stateHash;
2938
+ }
2955
2939
  }
2956
2940
  if (txf._integrity?.currentStateHash) {
2957
2941
  return txf._integrity.currentStateHash;
2958
2942
  }
2943
+ if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
2944
+ return txf.genesis.inclusionProof.authenticator.stateHash;
2945
+ }
2959
2946
  return void 0;
2960
2947
  }
2961
2948
  function hasValidTxfData(token) {
@@ -3316,16 +3303,733 @@ function getCoinIdByName(name) {
3316
3303
  return TokenRegistry.getInstance().getCoinIdByName(name);
3317
3304
  }
3318
3305
 
3319
- // modules/payments/PaymentsModule.ts
3306
+ // modules/payments/InstantSplitExecutor.ts
3320
3307
  var import_Token4 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
3308
+ var import_TokenId3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
3309
+ var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
3321
3310
  var import_CoinId3 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
3311
+ var import_TokenCoinData2 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
3312
+ var import_TokenSplitBuilder2 = require("@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder");
3313
+ var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
3314
+ var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
3315
+ var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
3322
3316
  var import_TransferCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
3317
+ var import_InclusionProofUtils3 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
3318
+ async function sha2563(input) {
3319
+ const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
3320
+ const buffer = new ArrayBuffer(data.length);
3321
+ new Uint8Array(buffer).set(data);
3322
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
3323
+ return new Uint8Array(hashBuffer);
3324
+ }
3325
+ function toHex2(bytes) {
3326
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3327
+ }
3328
+ function fromHex2(hex) {
3329
+ const bytes = new Uint8Array(hex.length / 2);
3330
+ for (let i = 0; i < hex.length; i += 2) {
3331
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3332
+ }
3333
+ return bytes;
3334
+ }
3335
+ var InstantSplitExecutor = class {
3336
+ client;
3337
+ trustBase;
3338
+ signingService;
3339
+ devMode;
3340
+ constructor(config) {
3341
+ this.client = config.stateTransitionClient;
3342
+ this.trustBase = config.trustBase;
3343
+ this.signingService = config.signingService;
3344
+ this.devMode = config.devMode ?? false;
3345
+ }
3346
+ /**
3347
+ * Execute an instant split transfer with V5 optimized flow.
3348
+ *
3349
+ * Critical path (~2.3s):
3350
+ * 1. Create and submit burn commitment
3351
+ * 2. Wait for burn proof
3352
+ * 3. Create mint commitments with SplitMintReason
3353
+ * 4. Create transfer commitment (no mint proof needed)
3354
+ * 5. Send bundle via transport
3355
+ *
3356
+ * @param tokenToSplit - The SDK token to split
3357
+ * @param splitAmount - Amount to send to recipient
3358
+ * @param remainderAmount - Amount to keep as change
3359
+ * @param coinIdHex - Coin ID in hex format
3360
+ * @param recipientAddress - Recipient's address (PROXY or DIRECT)
3361
+ * @param transport - Transport provider for sending the bundle
3362
+ * @param recipientPubkey - Recipient's transport public key
3363
+ * @param options - Optional configuration
3364
+ * @returns InstantSplitResult with success status and timing info
3365
+ */
3366
+ async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
3367
+ const startTime = performance.now();
3368
+ const splitGroupId = crypto.randomUUID();
3369
+ const tokenIdHex = toHex2(tokenToSplit.id.bytes);
3370
+ console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
3371
+ try {
3372
+ const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
3373
+ const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
3374
+ const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
3375
+ const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
3376
+ const recipientSalt = await sha2563(seedString + "_recipient_salt");
3377
+ const senderSalt = await sha2563(seedString + "_sender_salt");
3378
+ const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
3379
+ tokenToSplit.type,
3380
+ this.signingService.algorithm,
3381
+ this.signingService.publicKey,
3382
+ import_HashAlgorithm3.HashAlgorithm.SHA256
3383
+ );
3384
+ const senderAddress = await senderAddressRef.toAddress();
3385
+ const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
3386
+ const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
3387
+ builder.createToken(
3388
+ recipientTokenId,
3389
+ tokenToSplit.type,
3390
+ new Uint8Array(0),
3391
+ coinDataA,
3392
+ senderAddress,
3393
+ // Mint to sender first, then transfer
3394
+ recipientSalt,
3395
+ null
3396
+ );
3397
+ const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
3398
+ builder.createToken(
3399
+ senderTokenId,
3400
+ tokenToSplit.type,
3401
+ new Uint8Array(0),
3402
+ coinDataB,
3403
+ senderAddress,
3404
+ senderSalt,
3405
+ null
3406
+ );
3407
+ const split = await builder.build(tokenToSplit);
3408
+ console.log("[InstantSplit] Step 1: Creating and submitting burn...");
3409
+ const burnSalt = await sha2563(seedString + "_burn_salt");
3410
+ const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
3411
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3412
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3413
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3414
+ }
3415
+ console.log("[InstantSplit] Step 2: Waiting for burn proof...");
3416
+ const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
3417
+ const burnTransaction = burnCommitment.toTransaction(burnProof);
3418
+ const burnDuration = performance.now() - startTime;
3419
+ console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
3420
+ options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
3421
+ console.log("[InstantSplit] Step 3: Creating mint commitments...");
3422
+ const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
3423
+ const recipientIdHex = toHex2(recipientTokenId.bytes);
3424
+ const senderIdHex = toHex2(senderTokenId.bytes);
3425
+ const recipientMintCommitment = mintCommitments.find(
3426
+ (c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
3427
+ );
3428
+ const senderMintCommitment = mintCommitments.find(
3429
+ (c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
3430
+ );
3431
+ if (!recipientMintCommitment || !senderMintCommitment) {
3432
+ throw new Error("Failed to find expected mint commitments");
3433
+ }
3434
+ console.log("[InstantSplit] Step 4: Creating transfer commitment...");
3435
+ const transferSalt = await sha2563(seedString + "_transfer_salt");
3436
+ const transferCommitment = await this.createTransferCommitmentFromMintData(
3437
+ recipientMintCommitment.transactionData,
3438
+ recipientAddress,
3439
+ transferSalt,
3440
+ this.signingService
3441
+ );
3442
+ const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3443
+ recipientTokenId,
3444
+ tokenToSplit.type,
3445
+ this.signingService,
3446
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3447
+ recipientSalt
3448
+ );
3449
+ const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
3450
+ console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
3451
+ const senderPubkey = toHex2(this.signingService.publicKey);
3452
+ let nametagTokenJson;
3453
+ const recipientAddressStr = recipientAddress.toString();
3454
+ if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
3455
+ nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
3456
+ }
3457
+ const bundle = {
3458
+ version: "5.0",
3459
+ type: "INSTANT_SPLIT",
3460
+ burnTransaction: JSON.stringify(burnTransaction.toJSON()),
3461
+ recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
3462
+ transferCommitment: JSON.stringify(transferCommitment.toJSON()),
3463
+ amount: splitAmount.toString(),
3464
+ coinId: coinIdHex,
3465
+ tokenTypeHex: toHex2(tokenToSplit.type.bytes),
3466
+ splitGroupId,
3467
+ senderPubkey,
3468
+ recipientSaltHex: toHex2(recipientSalt),
3469
+ transferSaltHex: toHex2(transferSalt),
3470
+ mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
3471
+ finalRecipientStateJson: "",
3472
+ // Recipient creates their own
3473
+ recipientAddressJson: recipientAddressStr,
3474
+ nametagTokenJson
3475
+ };
3476
+ console.log("[InstantSplit] Step 6: Sending via transport...");
3477
+ const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
3478
+ token: JSON.stringify(bundle),
3479
+ proof: null,
3480
+ // Proof is included in the bundle
3481
+ memo: "INSTANT_SPLIT_V5",
3482
+ sender: {
3483
+ transportPubkey: senderPubkey
3484
+ }
3485
+ });
3486
+ const criticalPathDuration = performance.now() - startTime;
3487
+ console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
3488
+ options?.onNostrDelivered?.(nostrEventId);
3489
+ if (!options?.skipBackground) {
3490
+ this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
3491
+ signingService: this.signingService,
3492
+ tokenType: tokenToSplit.type,
3493
+ coinId,
3494
+ senderTokenId,
3495
+ senderSalt,
3496
+ onProgress: options?.onBackgroundProgress,
3497
+ onChangeTokenCreated: options?.onChangeTokenCreated,
3498
+ onStorageSync: options?.onStorageSync
3499
+ });
3500
+ }
3501
+ return {
3502
+ success: true,
3503
+ nostrEventId,
3504
+ splitGroupId,
3505
+ criticalPathDurationMs: criticalPathDuration,
3506
+ backgroundStarted: !options?.skipBackground
3507
+ };
3508
+ } catch (error) {
3509
+ const duration = performance.now() - startTime;
3510
+ const errorMessage = error instanceof Error ? error.message : String(error);
3511
+ console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
3512
+ return {
3513
+ success: false,
3514
+ splitGroupId,
3515
+ criticalPathDurationMs: duration,
3516
+ error: errorMessage,
3517
+ backgroundStarted: false
3518
+ };
3519
+ }
3520
+ }
3521
+ /**
3522
+ * Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
3523
+ *
3524
+ * Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
3525
+ * It does NOT need the genesis transaction or mint proof.
3526
+ */
3527
+ async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
3528
+ const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3529
+ mintData.tokenId,
3530
+ mintData.tokenType,
3531
+ signingService,
3532
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3533
+ mintData.salt
3534
+ );
3535
+ const state = new import_TokenState3.TokenState(predicate, null);
3536
+ const minimalToken = {
3537
+ state,
3538
+ nametagTokens: nametagTokens || [],
3539
+ id: mintData.tokenId,
3540
+ type: mintData.tokenType
3541
+ };
3542
+ const transferCommitment = await import_TransferCommitment2.TransferCommitment.create(
3543
+ minimalToken,
3544
+ recipientAddress,
3545
+ transferSalt,
3546
+ null,
3547
+ // recipientData
3548
+ null,
3549
+ // recipientDataHash
3550
+ signingService
3551
+ );
3552
+ return transferCommitment;
3553
+ }
3554
+ /**
3555
+ * V5 background submission.
3556
+ *
3557
+ * Submits mint commitments to aggregator in PARALLEL after transport delivery.
3558
+ * Then waits for sender's mint proof, reconstructs change token, and saves it.
3559
+ */
3560
+ submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
3561
+ console.log("[InstantSplit] Background: Starting parallel mint submission...");
3562
+ const startTime = performance.now();
3563
+ const submissions = Promise.all([
3564
+ this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
3565
+ this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
3566
+ this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
3567
+ ]);
3568
+ submissions.then(async (results) => {
3569
+ const submitDuration = performance.now() - startTime;
3570
+ console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
3571
+ context.onProgress?.({
3572
+ stage: "MINTS_SUBMITTED",
3573
+ message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
3574
+ });
3575
+ const senderMintResult = results.find((r) => r.type === "senderMint");
3576
+ if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
3577
+ console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
3578
+ context.onProgress?.({
3579
+ stage: "FAILED",
3580
+ message: "Sender mint submission failed",
3581
+ error: String(senderMintResult?.error)
3582
+ });
3583
+ return;
3584
+ }
3585
+ console.log("[InstantSplit] Background: Waiting for sender mint proof...");
3586
+ const proofStartTime = performance.now();
3587
+ try {
3588
+ const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, senderMintCommitment);
3589
+ const proofDuration = performance.now() - proofStartTime;
3590
+ console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
3591
+ context.onProgress?.({
3592
+ stage: "MINTS_PROVEN",
3593
+ message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
3594
+ });
3595
+ const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
3596
+ const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3597
+ context.senderTokenId,
3598
+ context.tokenType,
3599
+ context.signingService,
3600
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3601
+ context.senderSalt
3602
+ );
3603
+ const state = new import_TokenState3.TokenState(predicate, null);
3604
+ const changeToken = await import_Token4.Token.mint(this.trustBase, state, mintTransaction);
3605
+ if (!this.devMode) {
3606
+ const verification = await changeToken.verify(this.trustBase);
3607
+ if (!verification.isSuccessful) {
3608
+ throw new Error(`Change token verification failed`);
3609
+ }
3610
+ }
3611
+ console.log("[InstantSplit] Background: Change token created");
3612
+ context.onProgress?.({
3613
+ stage: "CHANGE_TOKEN_SAVED",
3614
+ message: "Change token created and verified"
3615
+ });
3616
+ if (context.onChangeTokenCreated) {
3617
+ await context.onChangeTokenCreated(changeToken);
3618
+ console.log("[InstantSplit] Background: Change token saved");
3619
+ }
3620
+ if (context.onStorageSync) {
3621
+ try {
3622
+ const syncSuccess = await context.onStorageSync();
3623
+ console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
3624
+ context.onProgress?.({
3625
+ stage: "STORAGE_SYNCED",
3626
+ message: syncSuccess ? "Storage synchronized" : "Sync deferred"
3627
+ });
3628
+ } catch (syncError) {
3629
+ console.warn("[InstantSplit] Background: Storage sync error:", syncError);
3630
+ }
3631
+ }
3632
+ const totalDuration = performance.now() - startTime;
3633
+ console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
3634
+ context.onProgress?.({
3635
+ stage: "COMPLETED",
3636
+ message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
3637
+ });
3638
+ } catch (proofError) {
3639
+ console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
3640
+ context.onProgress?.({
3641
+ stage: "FAILED",
3642
+ message: "Failed to get mint proof",
3643
+ error: String(proofError)
3644
+ });
3645
+ }
3646
+ }).catch((err) => {
3647
+ console.error("[InstantSplit] Background: Submission batch failed:", err);
3648
+ context.onProgress?.({
3649
+ stage: "FAILED",
3650
+ message: "Background submission failed",
3651
+ error: String(err)
3652
+ });
3653
+ });
3654
+ }
3655
+ /**
3656
+ * Dev mode bypass for waitInclusionProof.
3657
+ * In dev mode, we create a mock proof for testing.
3658
+ */
3659
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
3660
+ if (this.devMode) {
3661
+ try {
3662
+ return await Promise.race([
3663
+ (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment),
3664
+ new Promise(
3665
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
3666
+ )
3667
+ ]);
3668
+ } catch {
3669
+ console.log("[InstantSplit] Dev mode: Using mock proof");
3670
+ return {
3671
+ toJSON: () => ({ mock: true })
3672
+ };
3673
+ }
3674
+ }
3675
+ return (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment);
3676
+ }
3677
+ };
3678
+
3679
+ // modules/payments/InstantSplitProcessor.ts
3680
+ var import_Token5 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
3681
+ var import_TokenState4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
3682
+ var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
3683
+ var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
3684
+ var import_UnmaskedPredicate4 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
3685
+ var import_TransferCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
3323
3686
  var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
3687
+ var import_MintCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
3688
+ var import_MintTransactionData2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
3689
+ var import_InclusionProofUtils4 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
3690
+
3691
+ // types/instant-split.ts
3692
+ function isInstantSplitBundle(obj) {
3693
+ if (typeof obj !== "object" || obj === null) {
3694
+ return false;
3695
+ }
3696
+ const bundle = obj;
3697
+ if (bundle.type !== "INSTANT_SPLIT") return false;
3698
+ if (typeof bundle.recipientMintData !== "string") return false;
3699
+ if (typeof bundle.transferCommitment !== "string") return false;
3700
+ if (typeof bundle.amount !== "string") return false;
3701
+ if (typeof bundle.coinId !== "string") return false;
3702
+ if (typeof bundle.splitGroupId !== "string") return false;
3703
+ if (typeof bundle.senderPubkey !== "string") return false;
3704
+ if (typeof bundle.recipientSaltHex !== "string") return false;
3705
+ if (typeof bundle.transferSaltHex !== "string") return false;
3706
+ if (bundle.version === "4.0") {
3707
+ return typeof bundle.burnCommitment === "string";
3708
+ } else if (bundle.version === "5.0") {
3709
+ return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
3710
+ }
3711
+ return false;
3712
+ }
3713
+ function isInstantSplitBundleV4(obj) {
3714
+ return isInstantSplitBundle(obj) && obj.version === "4.0";
3715
+ }
3716
+ function isInstantSplitBundleV5(obj) {
3717
+ return isInstantSplitBundle(obj) && obj.version === "5.0";
3718
+ }
3719
+
3720
+ // modules/payments/InstantSplitProcessor.ts
3721
+ function fromHex3(hex) {
3722
+ const bytes = new Uint8Array(hex.length / 2);
3723
+ for (let i = 0; i < hex.length; i += 2) {
3724
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3725
+ }
3726
+ return bytes;
3727
+ }
3728
+ var InstantSplitProcessor = class {
3729
+ client;
3730
+ trustBase;
3731
+ devMode;
3732
+ constructor(config) {
3733
+ this.client = config.stateTransitionClient;
3734
+ this.trustBase = config.trustBase;
3735
+ this.devMode = config.devMode ?? false;
3736
+ }
3737
+ /**
3738
+ * Process a received INSTANT_SPLIT bundle.
3739
+ *
3740
+ * @param bundle - The received bundle (V4 or V5)
3741
+ * @param signingService - Recipient's signing service
3742
+ * @param senderPubkey - Sender's public key (for verification)
3743
+ * @param options - Processing options
3744
+ * @returns Processing result with finalized token if successful
3745
+ */
3746
+ async processReceivedBundle(bundle, signingService, senderPubkey, options) {
3747
+ if (isInstantSplitBundleV5(bundle)) {
3748
+ return this.processV5Bundle(bundle, signingService, senderPubkey, options);
3749
+ } else if (isInstantSplitBundleV4(bundle)) {
3750
+ return this.processV4Bundle(bundle, signingService, senderPubkey, options);
3751
+ }
3752
+ return {
3753
+ success: false,
3754
+ error: `Unknown bundle version: ${bundle.version}`,
3755
+ durationMs: 0
3756
+ };
3757
+ }
3758
+ /**
3759
+ * Process a V5 bundle (production mode).
3760
+ *
3761
+ * V5 Flow:
3762
+ * 1. Burn transaction already has proof (just validate)
3763
+ * 2. Submit mint commitment -> wait for proof
3764
+ * 3. Reconstruct minted token (use sender's state from bundle)
3765
+ * 4. Submit transfer commitment -> wait for proof
3766
+ * 5. Create recipient's final state and finalize token
3767
+ */
3768
+ async processV5Bundle(bundle, signingService, senderPubkey, options) {
3769
+ console.log("[InstantSplitProcessor] Processing V5 bundle...");
3770
+ const startTime = performance.now();
3771
+ try {
3772
+ if (bundle.senderPubkey !== senderPubkey) {
3773
+ console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
3774
+ }
3775
+ const burnTxJson = JSON.parse(bundle.burnTransaction);
3776
+ const burnTransaction = await import_TransferTransaction.TransferTransaction.fromJSON(burnTxJson);
3777
+ console.log("[InstantSplitProcessor] Burn transaction validated");
3778
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3779
+ const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
3780
+ const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
3781
+ console.log("[InstantSplitProcessor] Mint commitment recreated");
3782
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3783
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3784
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3785
+ }
3786
+ console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
3787
+ const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, mintCommitment);
3788
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3789
+ console.log("[InstantSplitProcessor] Mint proof received");
3790
+ const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
3791
+ const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
3792
+ const tokenJson = {
3793
+ version: "2.0",
3794
+ state: senderMintedStateJson,
3795
+ genesis: mintTransaction.toJSON(),
3796
+ transactions: [],
3797
+ nametags: []
3798
+ };
3799
+ const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
3800
+ console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
3801
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3802
+ const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
3803
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3804
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3805
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3806
+ }
3807
+ console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
3808
+ const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, transferCommitment);
3809
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3810
+ console.log("[InstantSplitProcessor] Transfer proof received");
3811
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3812
+ const finalRecipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3813
+ mintData.tokenId,
3814
+ tokenType,
3815
+ signingService,
3816
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3817
+ transferSalt
3818
+ );
3819
+ const finalRecipientState = new import_TokenState4.TokenState(finalRecipientPredicate, null);
3820
+ console.log("[InstantSplitProcessor] Final recipient state created");
3821
+ let nametagTokens = [];
3822
+ const recipientAddressStr = bundle.recipientAddressJson;
3823
+ if (recipientAddressStr.startsWith("PROXY://")) {
3824
+ console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
3825
+ if (bundle.nametagTokenJson) {
3826
+ try {
3827
+ const nametagToken = await import_Token5.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
3828
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
3829
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
3830
+ if (proxy.address !== recipientAddressStr) {
3831
+ console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
3832
+ } else {
3833
+ nametagTokens = [nametagToken];
3834
+ console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
3835
+ }
3836
+ } catch (err) {
3837
+ console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
3838
+ }
3839
+ }
3840
+ if (nametagTokens.length === 0 && options?.findNametagToken) {
3841
+ const token = await options.findNametagToken(recipientAddressStr);
3842
+ if (token) {
3843
+ nametagTokens = [token];
3844
+ console.log("[InstantSplitProcessor] Found nametag token via callback");
3845
+ }
3846
+ }
3847
+ if (nametagTokens.length === 0 && !this.devMode) {
3848
+ throw new Error(
3849
+ `PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
3850
+ );
3851
+ }
3852
+ }
3853
+ let finalToken;
3854
+ if (this.devMode) {
3855
+ console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
3856
+ const tokenJson2 = mintedToken.toJSON();
3857
+ tokenJson2.state = finalRecipientState.toJSON();
3858
+ tokenJson2.transactions = [transferTransaction.toJSON()];
3859
+ finalToken = await import_Token5.Token.fromJSON(tokenJson2);
3860
+ } else {
3861
+ finalToken = await this.client.finalizeTransaction(
3862
+ this.trustBase,
3863
+ mintedToken,
3864
+ finalRecipientState,
3865
+ transferTransaction,
3866
+ nametagTokens
3867
+ );
3868
+ }
3869
+ console.log("[InstantSplitProcessor] Token finalized");
3870
+ if (!this.devMode) {
3871
+ const verification = await finalToken.verify(this.trustBase);
3872
+ if (!verification.isSuccessful) {
3873
+ throw new Error(`Token verification failed`);
3874
+ }
3875
+ console.log("[InstantSplitProcessor] Token verified");
3876
+ }
3877
+ const duration = performance.now() - startTime;
3878
+ console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
3879
+ return {
3880
+ success: true,
3881
+ token: finalToken,
3882
+ durationMs: duration
3883
+ };
3884
+ } catch (error) {
3885
+ const duration = performance.now() - startTime;
3886
+ const errorMessage = error instanceof Error ? error.message : String(error);
3887
+ console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
3888
+ return {
3889
+ success: false,
3890
+ error: errorMessage,
3891
+ durationMs: duration
3892
+ };
3893
+ }
3894
+ }
3895
+ /**
3896
+ * Process a V4 bundle (dev mode only).
3897
+ *
3898
+ * V4 Flow:
3899
+ * 1. Submit burn commitment -> wait for proof
3900
+ * 2. Submit mint commitment -> wait for proof
3901
+ * 3. Reconstruct minted token
3902
+ * 4. Submit transfer commitment -> wait for proof
3903
+ * 5. Finalize token
3904
+ */
3905
+ async processV4Bundle(bundle, signingService, _senderPubkey, options) {
3906
+ if (!this.devMode) {
3907
+ return {
3908
+ success: false,
3909
+ error: "INSTANT_SPLIT V4 is only supported in dev mode",
3910
+ durationMs: 0
3911
+ };
3912
+ }
3913
+ console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
3914
+ const startTime = performance.now();
3915
+ try {
3916
+ const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
3917
+ const burnCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(burnCommitmentJson);
3918
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3919
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3920
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3921
+ }
3922
+ await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
3923
+ console.log("[InstantSplitProcessor] V4: Burn proof received");
3924
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3925
+ const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
3926
+ const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
3927
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3928
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3929
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3930
+ }
3931
+ const mintProof = await this.waitInclusionProofWithDevBypass(
3932
+ mintCommitment,
3933
+ options?.proofTimeoutMs
3934
+ );
3935
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3936
+ console.log("[InstantSplitProcessor] V4: Mint proof received");
3937
+ const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
3938
+ const recipientSalt = fromHex3(bundle.recipientSaltHex);
3939
+ const recipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3940
+ mintData.tokenId,
3941
+ tokenType,
3942
+ signingService,
3943
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3944
+ recipientSalt
3945
+ );
3946
+ const recipientState = new import_TokenState4.TokenState(recipientPredicate, null);
3947
+ const tokenJson = {
3948
+ version: "2.0",
3949
+ state: recipientState.toJSON(),
3950
+ genesis: mintTransaction.toJSON(),
3951
+ transactions: [],
3952
+ nametags: []
3953
+ };
3954
+ const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
3955
+ console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
3956
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3957
+ const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
3958
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3959
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3960
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3961
+ }
3962
+ const transferProof = await this.waitInclusionProofWithDevBypass(
3963
+ transferCommitment,
3964
+ options?.proofTimeoutMs
3965
+ );
3966
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3967
+ console.log("[InstantSplitProcessor] V4: Transfer proof received");
3968
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3969
+ const finalPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3970
+ mintData.tokenId,
3971
+ tokenType,
3972
+ signingService,
3973
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3974
+ transferSalt
3975
+ );
3976
+ const finalState = new import_TokenState4.TokenState(finalPredicate, null);
3977
+ const finalTokenJson = mintedToken.toJSON();
3978
+ finalTokenJson.state = finalState.toJSON();
3979
+ finalTokenJson.transactions = [transferTransaction.toJSON()];
3980
+ const finalToken = await import_Token5.Token.fromJSON(finalTokenJson);
3981
+ console.log("[InstantSplitProcessor] V4: Token finalized");
3982
+ const duration = performance.now() - startTime;
3983
+ console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
3984
+ return {
3985
+ success: true,
3986
+ token: finalToken,
3987
+ durationMs: duration
3988
+ };
3989
+ } catch (error) {
3990
+ const duration = performance.now() - startTime;
3991
+ const errorMessage = error instanceof Error ? error.message : String(error);
3992
+ console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
3993
+ return {
3994
+ success: false,
3995
+ error: errorMessage,
3996
+ durationMs: duration
3997
+ };
3998
+ }
3999
+ }
4000
+ /**
4001
+ * Dev mode bypass for waitInclusionProof.
4002
+ */
4003
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
4004
+ if (this.devMode) {
4005
+ try {
4006
+ return await Promise.race([
4007
+ (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment),
4008
+ new Promise(
4009
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
4010
+ )
4011
+ ]);
4012
+ } catch {
4013
+ console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
4014
+ return {
4015
+ toJSON: () => ({ mock: true })
4016
+ };
4017
+ }
4018
+ }
4019
+ return (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment);
4020
+ }
4021
+ };
4022
+
4023
+ // modules/payments/PaymentsModule.ts
4024
+ var import_Token6 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
4025
+ var import_CoinId4 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
4026
+ var import_TransferCommitment4 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
4027
+ var import_TransferTransaction2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
3324
4028
  var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
3325
4029
  var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/address/AddressScheme");
3326
- var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
3327
- var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
3328
- var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
4030
+ var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
4031
+ var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
4032
+ var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
3329
4033
  function enrichWithRegistry(info) {
3330
4034
  const registry = TokenRegistry.getInstance();
3331
4035
  const def = registry.getDefinition(info.coinId);
@@ -3351,7 +4055,7 @@ async function parseTokenInfo(tokenData) {
3351
4055
  try {
3352
4056
  const data = typeof tokenData === "string" ? JSON.parse(tokenData) : tokenData;
3353
4057
  try {
3354
- const sdkToken = await import_Token4.Token.fromJSON(data);
4058
+ const sdkToken = await import_Token6.Token.fromJSON(data);
3355
4059
  if (sdkToken.id) {
3356
4060
  defaultInfo.tokenId = sdkToken.id.toString();
3357
4061
  }
@@ -3364,7 +4068,7 @@ async function parseTokenInfo(tokenData) {
3364
4068
  if (Array.isArray(firstCoin) && firstCoin.length === 2) {
3365
4069
  [coinIdObj, amount] = firstCoin;
3366
4070
  }
3367
- if (coinIdObj instanceof import_CoinId3.CoinId) {
4071
+ if (coinIdObj instanceof import_CoinId4.CoinId) {
3368
4072
  const coinIdHex = coinIdObj.toJSON();
3369
4073
  return enrichWithRegistry({
3370
4074
  coinId: coinIdHex,
@@ -3497,21 +4201,48 @@ function extractStateHashFromSdkData(sdkData) {
3497
4201
  if (!sdkData) return "";
3498
4202
  try {
3499
4203
  const txf = JSON.parse(sdkData);
3500
- return getCurrentStateHash(txf) || "";
4204
+ const stateHash = getCurrentStateHash(txf);
4205
+ if (!stateHash) {
4206
+ if (txf.state?.hash) {
4207
+ return txf.state.hash;
4208
+ }
4209
+ if (txf.stateHash) {
4210
+ return txf.stateHash;
4211
+ }
4212
+ if (txf.currentStateHash) {
4213
+ return txf.currentStateHash;
4214
+ }
4215
+ }
4216
+ return stateHash || "";
3501
4217
  } catch {
3502
4218
  return "";
3503
4219
  }
3504
4220
  }
3505
- function isSameToken(t1, t2) {
3506
- if (t1.id === t2.id) return true;
4221
+ function createTokenStateKey(tokenId, stateHash) {
4222
+ return `${tokenId}_${stateHash}`;
4223
+ }
4224
+ function extractTokenStateKey(token) {
4225
+ const tokenId = extractTokenIdFromSdkData(token.sdkData);
4226
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
4227
+ if (!tokenId || !stateHash) return null;
4228
+ return createTokenStateKey(tokenId, stateHash);
4229
+ }
4230
+ function hasSameGenesisTokenId(t1, t2) {
3507
4231
  const id1 = extractTokenIdFromSdkData(t1.sdkData);
3508
4232
  const id2 = extractTokenIdFromSdkData(t2.sdkData);
3509
4233
  return !!(id1 && id2 && id1 === id2);
3510
4234
  }
4235
+ function isSameTokenState(t1, t2) {
4236
+ const key1 = extractTokenStateKey(t1);
4237
+ const key2 = extractTokenStateKey(t2);
4238
+ return !!(key1 && key2 && key1 === key2);
4239
+ }
3511
4240
  function createTombstoneFromToken(token) {
3512
4241
  const tokenId = extractTokenIdFromSdkData(token.sdkData);
3513
- if (!tokenId) return null;
3514
4242
  const stateHash = extractStateHashFromSdkData(token.sdkData);
4243
+ if (!tokenId || !stateHash) {
4244
+ return null;
4245
+ }
3515
4246
  return {
3516
4247
  tokenId,
3517
4248
  stateHash,
@@ -3577,7 +4308,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
3577
4308
  candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
3578
4309
  return candidates[0];
3579
4310
  }
3580
- var PaymentsModule = class {
4311
+ var PaymentsModule = class _PaymentsModule {
3581
4312
  moduleConfig;
3582
4313
  deps = null;
3583
4314
  /** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
@@ -3602,6 +4333,13 @@ var PaymentsModule = class {
3602
4333
  unsubscribeTransfers = null;
3603
4334
  unsubscribePaymentRequests = null;
3604
4335
  unsubscribePaymentRequestResponses = null;
4336
+ // NOSTR-FIRST proof polling (background proof verification)
4337
+ proofPollingJobs = /* @__PURE__ */ new Map();
4338
+ proofPollingInterval = null;
4339
+ static PROOF_POLLING_INTERVAL_MS = 2e3;
4340
+ // Poll every 2s
4341
+ static PROOF_POLLING_MAX_ATTEMPTS = 30;
4342
+ // Max 30 attempts (~60s)
3605
4343
  constructor(config) {
3606
4344
  this.moduleConfig = {
3607
4345
  autoSync: config?.autoSync ?? true,
@@ -3617,6 +4355,8 @@ var PaymentsModule = class {
3617
4355
  getConfig() {
3618
4356
  return this.moduleConfig;
3619
4357
  }
4358
+ /** Price provider (optional) */
4359
+ priceProvider = null;
3620
4360
  log(...args) {
3621
4361
  if (this.moduleConfig.debug) {
3622
4362
  console.log("[PaymentsModule]", ...args);
@@ -3629,7 +4369,21 @@ var PaymentsModule = class {
3629
4369
  * Initialize module with dependencies
3630
4370
  */
3631
4371
  initialize(deps) {
4372
+ this.unsubscribeTransfers?.();
4373
+ this.unsubscribeTransfers = null;
4374
+ this.unsubscribePaymentRequests?.();
4375
+ this.unsubscribePaymentRequests = null;
4376
+ this.unsubscribePaymentRequestResponses?.();
4377
+ this.unsubscribePaymentRequestResponses = null;
4378
+ this.tokens.clear();
4379
+ this.pendingTransfers.clear();
4380
+ this.tombstones = [];
4381
+ this.archivedTokens.clear();
4382
+ this.forkedTokens.clear();
4383
+ this.transactionHistory = [];
4384
+ this.nametag = null;
3632
4385
  this.deps = deps;
4386
+ this.priceProvider = deps.price ?? null;
3633
4387
  if (this.l1) {
3634
4388
  this.l1.initialize({
3635
4389
  identity: deps.identity,
@@ -3700,6 +4454,8 @@ var PaymentsModule = class {
3700
4454
  this.unsubscribePaymentRequestResponses = null;
3701
4455
  this.paymentRequestHandlers.clear();
3702
4456
  this.paymentRequestResponseHandlers.clear();
4457
+ this.stopProofPolling();
4458
+ this.proofPollingJobs.clear();
3703
4459
  for (const [, resolver] of this.pendingResponseResolvers) {
3704
4460
  clearTimeout(resolver.timeout);
3705
4461
  resolver.reject(new Error("Module destroyed"));
@@ -3724,8 +4480,9 @@ var PaymentsModule = class {
3724
4480
  tokens: []
3725
4481
  };
3726
4482
  try {
3727
- const recipientPubkey = await this.resolveRecipient(request.recipient);
3728
- const recipientAddress = await this.resolveRecipientAddress(request.recipient);
4483
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4484
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4485
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
3729
4486
  const signingService = await this.createSigningService();
3730
4487
  const stClient = this.deps.oracle.getStateTransitionClient?.();
3731
4488
  if (!stClient) {
@@ -3745,7 +4502,6 @@ var PaymentsModule = class {
3745
4502
  if (!splitPlan) {
3746
4503
  throw new Error("Insufficient balance");
3747
4504
  }
3748
- this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
3749
4505
  const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
3750
4506
  if (splitPlan.tokenToSplit) {
3751
4507
  tokensToSend.push(splitPlan.tokenToSplit.uiToken);
@@ -3788,11 +4544,13 @@ var PaymentsModule = class {
3788
4544
  };
3789
4545
  await this.addToken(changeToken, true);
3790
4546
  this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
4547
+ console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3791
4548
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3792
4549
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
3793
4550
  transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
3794
4551
  memo: request.memo
3795
4552
  });
4553
+ console.log(`[Payments] Split token sent successfully`);
3796
4554
  await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
3797
4555
  result.txHash = "split-" + Date.now().toString(16);
3798
4556
  this.log(`Split transfer completed`);
@@ -3811,11 +4569,13 @@ var PaymentsModule = class {
3811
4569
  const transferTx = commitment.toTransaction(inclusionProof);
3812
4570
  const requestIdBytes = commitment.requestId;
3813
4571
  result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
4572
+ console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3814
4573
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3815
4574
  sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
3816
4575
  transferTx: JSON.stringify(transferTx.toJSON()),
3817
4576
  memo: request.memo
3818
4577
  });
4578
+ console.log(`[Payments] Direct token sent successfully`);
3819
4579
  this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
3820
4580
  await this.removeToken(token.id, recipientNametag);
3821
4581
  }
@@ -3869,28 +4629,257 @@ var PaymentsModule = class {
3869
4629
  return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
3870
4630
  }
3871
4631
  // ===========================================================================
3872
- // Public API - Payment Requests
4632
+ // Public API - Instant Split (V5 Optimized)
3873
4633
  // ===========================================================================
3874
4634
  /**
3875
- * Send a payment request to someone
3876
- * @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
3877
- * @param request - Payment request details
3878
- * @returns Result with event ID
4635
+ * Send tokens using INSTANT_SPLIT V5 optimized flow.
4636
+ *
4637
+ * This achieves ~2.3s critical path latency instead of ~42s by:
4638
+ * 1. Waiting only for burn proof (required)
4639
+ * 2. Creating transfer commitment from mint data (no mint proof needed)
4640
+ * 3. Sending bundle via Nostr immediately
4641
+ * 4. Processing mints in background
4642
+ *
4643
+ * @param request - Transfer request with recipient, amount, and coinId
4644
+ * @param options - Optional instant split configuration
4645
+ * @returns InstantSplitResult with timing info
3879
4646
  */
3880
- async sendPaymentRequest(recipientPubkeyOrNametag, request) {
4647
+ async sendInstant(request, options) {
3881
4648
  this.ensureInitialized();
3882
- if (!this.deps.transport.sendPaymentRequest) {
3883
- return {
3884
- success: false,
3885
- error: "Transport provider does not support payment requests"
3886
- };
3887
- }
4649
+ const startTime = performance.now();
3888
4650
  try {
3889
- const recipientPubkey = await this.resolveRecipient(recipientPubkeyOrNametag);
3890
- const payload = {
3891
- amount: request.amount,
3892
- coinId: request.coinId,
3893
- message: request.message,
4651
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4652
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4653
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
4654
+ const signingService = await this.createSigningService();
4655
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4656
+ if (!stClient) {
4657
+ throw new Error("State transition client not available");
4658
+ }
4659
+ const trustBase = this.deps.oracle.getTrustBase?.();
4660
+ if (!trustBase) {
4661
+ throw new Error("Trust base not available");
4662
+ }
4663
+ const calculator = new TokenSplitCalculator();
4664
+ const availableTokens = Array.from(this.tokens.values());
4665
+ const splitPlan = await calculator.calculateOptimalSplit(
4666
+ availableTokens,
4667
+ BigInt(request.amount),
4668
+ request.coinId
4669
+ );
4670
+ if (!splitPlan) {
4671
+ throw new Error("Insufficient balance");
4672
+ }
4673
+ if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
4674
+ this.log("No split required, falling back to standard send()");
4675
+ const result2 = await this.send(request);
4676
+ return {
4677
+ success: result2.status === "completed",
4678
+ criticalPathDurationMs: performance.now() - startTime,
4679
+ error: result2.error
4680
+ };
4681
+ }
4682
+ this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
4683
+ const tokenToSplit = splitPlan.tokenToSplit.uiToken;
4684
+ tokenToSplit.status = "transferring";
4685
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4686
+ const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
4687
+ const executor = new InstantSplitExecutor({
4688
+ stateTransitionClient: stClient,
4689
+ trustBase,
4690
+ signingService,
4691
+ devMode
4692
+ });
4693
+ const result = await executor.executeSplitInstant(
4694
+ splitPlan.tokenToSplit.sdkToken,
4695
+ splitPlan.splitAmount,
4696
+ splitPlan.remainderAmount,
4697
+ splitPlan.coinId,
4698
+ recipientAddress,
4699
+ this.deps.transport,
4700
+ recipientPubkey,
4701
+ {
4702
+ ...options,
4703
+ onChangeTokenCreated: async (changeToken) => {
4704
+ const changeTokenData = changeToken.toJSON();
4705
+ const uiToken = {
4706
+ id: crypto.randomUUID(),
4707
+ coinId: request.coinId,
4708
+ symbol: this.getCoinSymbol(request.coinId),
4709
+ name: this.getCoinName(request.coinId),
4710
+ decimals: this.getCoinDecimals(request.coinId),
4711
+ iconUrl: this.getCoinIconUrl(request.coinId),
4712
+ amount: splitPlan.remainderAmount.toString(),
4713
+ status: "confirmed",
4714
+ createdAt: Date.now(),
4715
+ updatedAt: Date.now(),
4716
+ sdkData: JSON.stringify(changeTokenData)
4717
+ };
4718
+ await this.addToken(uiToken, true);
4719
+ this.log(`Change token saved via background: ${uiToken.id}`);
4720
+ },
4721
+ onStorageSync: async () => {
4722
+ await this.save();
4723
+ return true;
4724
+ }
4725
+ }
4726
+ );
4727
+ if (result.success) {
4728
+ const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4729
+ await this.removeToken(tokenToSplit.id, recipientNametag);
4730
+ await this.addToHistory({
4731
+ type: "SENT",
4732
+ amount: request.amount,
4733
+ coinId: request.coinId,
4734
+ symbol: this.getCoinSymbol(request.coinId),
4735
+ timestamp: Date.now(),
4736
+ recipientNametag
4737
+ });
4738
+ await this.save();
4739
+ } else {
4740
+ tokenToSplit.status = "confirmed";
4741
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4742
+ }
4743
+ return result;
4744
+ } catch (error) {
4745
+ const errorMessage = error instanceof Error ? error.message : String(error);
4746
+ return {
4747
+ success: false,
4748
+ criticalPathDurationMs: performance.now() - startTime,
4749
+ error: errorMessage
4750
+ };
4751
+ }
4752
+ }
4753
+ /**
4754
+ * Process a received INSTANT_SPLIT bundle.
4755
+ *
4756
+ * This should be called when receiving an instant split bundle via transport.
4757
+ * It handles the recipient-side processing:
4758
+ * 1. Validate burn transaction
4759
+ * 2. Submit and wait for mint proof
4760
+ * 3. Submit and wait for transfer proof
4761
+ * 4. Finalize and save the token
4762
+ *
4763
+ * @param bundle - The received InstantSplitBundle (V4 or V5)
4764
+ * @param senderPubkey - Sender's public key for verification
4765
+ * @returns Processing result with finalized token
4766
+ */
4767
+ async processInstantSplitBundle(bundle, senderPubkey) {
4768
+ this.ensureInitialized();
4769
+ try {
4770
+ const signingService = await this.createSigningService();
4771
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4772
+ if (!stClient) {
4773
+ throw new Error("State transition client not available");
4774
+ }
4775
+ const trustBase = this.deps.oracle.getTrustBase?.();
4776
+ if (!trustBase) {
4777
+ throw new Error("Trust base not available");
4778
+ }
4779
+ const devMode = this.deps.oracle.isDevMode?.() ?? false;
4780
+ const processor = new InstantSplitProcessor({
4781
+ stateTransitionClient: stClient,
4782
+ trustBase,
4783
+ devMode
4784
+ });
4785
+ const result = await processor.processReceivedBundle(
4786
+ bundle,
4787
+ signingService,
4788
+ senderPubkey,
4789
+ {
4790
+ findNametagToken: async (proxyAddress) => {
4791
+ if (this.nametag?.token) {
4792
+ try {
4793
+ const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
4794
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
4795
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
4796
+ if (proxy.address === proxyAddress) {
4797
+ return nametagToken;
4798
+ }
4799
+ this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
4800
+ return null;
4801
+ } catch (err) {
4802
+ this.log("Failed to parse nametag token:", err);
4803
+ return null;
4804
+ }
4805
+ }
4806
+ return null;
4807
+ }
4808
+ }
4809
+ );
4810
+ if (result.success && result.token) {
4811
+ const tokenData = result.token.toJSON();
4812
+ const info = await parseTokenInfo(tokenData);
4813
+ const uiToken = {
4814
+ id: crypto.randomUUID(),
4815
+ coinId: info.coinId,
4816
+ symbol: info.symbol,
4817
+ name: info.name,
4818
+ decimals: info.decimals,
4819
+ iconUrl: info.iconUrl,
4820
+ amount: bundle.amount,
4821
+ status: "confirmed",
4822
+ createdAt: Date.now(),
4823
+ updatedAt: Date.now(),
4824
+ sdkData: JSON.stringify(tokenData)
4825
+ };
4826
+ await this.addToken(uiToken);
4827
+ await this.addToHistory({
4828
+ type: "RECEIVED",
4829
+ amount: bundle.amount,
4830
+ coinId: info.coinId,
4831
+ symbol: info.symbol,
4832
+ timestamp: Date.now(),
4833
+ senderPubkey
4834
+ });
4835
+ await this.save();
4836
+ this.deps.emitEvent("transfer:incoming", {
4837
+ id: bundle.splitGroupId,
4838
+ senderPubkey,
4839
+ tokens: [uiToken],
4840
+ receivedAt: Date.now()
4841
+ });
4842
+ }
4843
+ return result;
4844
+ } catch (error) {
4845
+ const errorMessage = error instanceof Error ? error.message : String(error);
4846
+ return {
4847
+ success: false,
4848
+ error: errorMessage,
4849
+ durationMs: 0
4850
+ };
4851
+ }
4852
+ }
4853
+ /**
4854
+ * Check if a payload is an instant split bundle
4855
+ */
4856
+ isInstantSplitBundle(payload) {
4857
+ return isInstantSplitBundle(payload);
4858
+ }
4859
+ // ===========================================================================
4860
+ // Public API - Payment Requests
4861
+ // ===========================================================================
4862
+ /**
4863
+ * Send a payment request to someone
4864
+ * @param recipientPubkeyOrNametag - Recipient's pubkey or @nametag
4865
+ * @param request - Payment request details
4866
+ * @returns Result with event ID
4867
+ */
4868
+ async sendPaymentRequest(recipientPubkeyOrNametag, request) {
4869
+ this.ensureInitialized();
4870
+ if (!this.deps.transport.sendPaymentRequest) {
4871
+ return {
4872
+ success: false,
4873
+ error: "Transport provider does not support payment requests"
4874
+ };
4875
+ }
4876
+ try {
4877
+ const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
4878
+ const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
4879
+ const payload = {
4880
+ amount: request.amount,
4881
+ coinId: request.coinId,
4882
+ message: request.message,
3894
4883
  recipientNametag: request.recipientNametag,
3895
4884
  metadata: request.metadata
3896
4885
  };
@@ -4190,47 +5179,46 @@ var PaymentsModule = class {
4190
5179
  // Public API - Balance & Tokens
4191
5180
  // ===========================================================================
4192
5181
  /**
4193
- * Get balance for coin type
5182
+ * Set or update price provider
4194
5183
  */
4195
- getBalance(coinId) {
4196
- const balances = /* @__PURE__ */ new Map();
4197
- for (const token of this.tokens.values()) {
4198
- if (token.status !== "confirmed") continue;
4199
- if (coinId && token.coinId !== coinId) continue;
4200
- const key = token.coinId;
4201
- const existing = balances.get(key);
4202
- if (existing) {
4203
- existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
4204
- existing.tokenCount++;
4205
- } else {
4206
- balances.set(key, {
4207
- coinId: token.coinId,
4208
- symbol: token.symbol,
4209
- name: token.name,
4210
- totalAmount: token.amount,
4211
- tokenCount: 1,
4212
- decimals: 8
4213
- });
5184
+ setPriceProvider(provider) {
5185
+ this.priceProvider = provider;
5186
+ }
5187
+ /**
5188
+ * Get total portfolio value in USD
5189
+ * Returns null if PriceProvider is not configured
5190
+ */
5191
+ async getBalance() {
5192
+ const assets = await this.getAssets();
5193
+ if (!this.priceProvider) {
5194
+ return null;
5195
+ }
5196
+ let total = 0;
5197
+ let hasAnyPrice = false;
5198
+ for (const asset of assets) {
5199
+ if (asset.fiatValueUsd != null) {
5200
+ total += asset.fiatValueUsd;
5201
+ hasAnyPrice = true;
4214
5202
  }
4215
5203
  }
4216
- return Array.from(balances.values());
5204
+ return hasAnyPrice ? total : null;
4217
5205
  }
4218
5206
  /**
4219
- * Get aggregated assets (tokens grouped by coinId)
5207
+ * Get aggregated assets (tokens grouped by coinId) with price data
4220
5208
  * Only includes confirmed tokens
4221
5209
  */
4222
- getAssets(coinId) {
4223
- const assets = /* @__PURE__ */ new Map();
5210
+ async getAssets(coinId) {
5211
+ const assetsMap = /* @__PURE__ */ new Map();
4224
5212
  for (const token of this.tokens.values()) {
4225
5213
  if (token.status !== "confirmed") continue;
4226
5214
  if (coinId && token.coinId !== coinId) continue;
4227
5215
  const key = token.coinId;
4228
- const existing = assets.get(key);
5216
+ const existing = assetsMap.get(key);
4229
5217
  if (existing) {
4230
5218
  existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
4231
5219
  existing.tokenCount++;
4232
5220
  } else {
4233
- assets.set(key, {
5221
+ assetsMap.set(key, {
4234
5222
  coinId: token.coinId,
4235
5223
  symbol: token.symbol,
4236
5224
  name: token.name,
@@ -4241,7 +5229,66 @@ var PaymentsModule = class {
4241
5229
  });
4242
5230
  }
4243
5231
  }
4244
- return Array.from(assets.values());
5232
+ const rawAssets = Array.from(assetsMap.values());
5233
+ let priceMap = null;
5234
+ if (this.priceProvider && rawAssets.length > 0) {
5235
+ const registry = TokenRegistry.getInstance();
5236
+ const nameToCoins = /* @__PURE__ */ new Map();
5237
+ for (const asset of rawAssets) {
5238
+ const def = registry.getDefinition(asset.coinId);
5239
+ if (def?.name) {
5240
+ const existing = nameToCoins.get(def.name);
5241
+ if (existing) {
5242
+ existing.push(asset.coinId);
5243
+ } else {
5244
+ nameToCoins.set(def.name, [asset.coinId]);
5245
+ }
5246
+ }
5247
+ }
5248
+ if (nameToCoins.size > 0) {
5249
+ const tokenNames = Array.from(nameToCoins.keys());
5250
+ const prices = await this.priceProvider.getPrices(tokenNames);
5251
+ priceMap = /* @__PURE__ */ new Map();
5252
+ for (const [name, coinIds] of nameToCoins) {
5253
+ const price = prices.get(name);
5254
+ if (price) {
5255
+ for (const cid of coinIds) {
5256
+ priceMap.set(cid, {
5257
+ priceUsd: price.priceUsd,
5258
+ priceEur: price.priceEur,
5259
+ change24h: price.change24h
5260
+ });
5261
+ }
5262
+ }
5263
+ }
5264
+ }
5265
+ }
5266
+ return rawAssets.map((raw) => {
5267
+ const price = priceMap?.get(raw.coinId);
5268
+ let fiatValueUsd = null;
5269
+ let fiatValueEur = null;
5270
+ if (price) {
5271
+ const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
5272
+ fiatValueUsd = humanAmount * price.priceUsd;
5273
+ if (price.priceEur != null) {
5274
+ fiatValueEur = humanAmount * price.priceEur;
5275
+ }
5276
+ }
5277
+ return {
5278
+ coinId: raw.coinId,
5279
+ symbol: raw.symbol,
5280
+ name: raw.name,
5281
+ decimals: raw.decimals,
5282
+ iconUrl: raw.iconUrl,
5283
+ totalAmount: raw.totalAmount,
5284
+ tokenCount: raw.tokenCount,
5285
+ priceUsd: price?.priceUsd ?? null,
5286
+ priceEur: price?.priceEur ?? null,
5287
+ change24h: price?.change24h ?? null,
5288
+ fiatValueUsd,
5289
+ fiatValueEur
5290
+ };
5291
+ });
4245
5292
  }
4246
5293
  /**
4247
5294
  * Get all tokens
@@ -4267,14 +5314,52 @@ var PaymentsModule = class {
4267
5314
  // ===========================================================================
4268
5315
  /**
4269
5316
  * Add a token
4270
- * @returns false if duplicate
5317
+ * Tokens are uniquely identified by (tokenId, stateHash) composite key.
5318
+ * Multiple historic states of the same token can coexist.
5319
+ * @returns false if exact duplicate (same tokenId AND same stateHash)
4271
5320
  */
4272
5321
  async addToken(token, skipHistory = false) {
4273
5322
  this.ensureInitialized();
4274
- for (const existing of this.tokens.values()) {
4275
- if (isSameToken(existing, token)) {
4276
- this.log(`Duplicate token detected: ${token.id}`);
4277
- return false;
5323
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
5324
+ const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
5325
+ const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
5326
+ if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
5327
+ this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
5328
+ return false;
5329
+ }
5330
+ if (incomingStateKey) {
5331
+ for (const [existingId, existing] of this.tokens) {
5332
+ if (isSameTokenState(existing, token)) {
5333
+ this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
5334
+ return false;
5335
+ }
5336
+ }
5337
+ }
5338
+ for (const [existingId, existing] of this.tokens) {
5339
+ if (hasSameGenesisTokenId(existing, token)) {
5340
+ const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
5341
+ if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
5342
+ continue;
5343
+ }
5344
+ if (existing.status === "spent" || existing.status === "invalid") {
5345
+ this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
5346
+ this.tokens.delete(existingId);
5347
+ break;
5348
+ }
5349
+ if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
5350
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
5351
+ await this.archiveToken(existing);
5352
+ this.tokens.delete(existingId);
5353
+ break;
5354
+ }
5355
+ if (!incomingStateHash || !existingStateHash) {
5356
+ if (existingId !== token.id) {
5357
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
5358
+ await this.archiveToken(existing);
5359
+ this.tokens.delete(existingId);
5360
+ break;
5361
+ }
5362
+ }
4278
5363
  }
4279
5364
  }
4280
5365
  this.tokens.set(token.id, token);
@@ -4429,8 +5514,10 @@ var PaymentsModule = class {
4429
5514
  );
4430
5515
  if (!alreadyTombstoned) {
4431
5516
  this.tombstones.push(tombstone);
4432
- this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
5517
+ this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
4433
5518
  }
5519
+ } else {
5520
+ this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
4434
5521
  }
4435
5522
  this.tokens.delete(tokenId);
4436
5523
  if (!skipHistory && token.coinId && token.amount) {
@@ -4748,15 +5835,15 @@ var PaymentsModule = class {
4748
5835
  }
4749
5836
  try {
4750
5837
  const signingService = await this.createSigningService();
4751
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4752
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5838
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5839
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4753
5840
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4754
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4755
- const addressRef = await UnmaskedPredicateReference3.create(
5841
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5842
+ const addressRef = await UnmaskedPredicateReference4.create(
4756
5843
  tokenType,
4757
5844
  signingService.algorithm,
4758
5845
  signingService.publicKey,
4759
- import_HashAlgorithm3.HashAlgorithm.SHA256
5846
+ import_HashAlgorithm5.HashAlgorithm.SHA256
4760
5847
  );
4761
5848
  const ownerAddress = await addressRef.toAddress();
4762
5849
  const minter = new NametagMinter({
@@ -4923,40 +6010,22 @@ var PaymentsModule = class {
4923
6010
  * Detect if a string is an L3 address (not a nametag)
4924
6011
  * Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
4925
6012
  */
4926
- isL3Address(value) {
4927
- if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
4928
- return true;
4929
- }
4930
- if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
4931
- return true;
4932
- }
4933
- return false;
4934
- }
4935
6013
  /**
4936
- * Resolve recipient to Nostr pubkey for messaging
4937
- * Supports: nametag (with or without @), hex pubkey
6014
+ * Resolve recipient to transport pubkey for messaging.
6015
+ * Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
4938
6016
  */
4939
- async resolveRecipient(recipient) {
4940
- if (recipient.startsWith("@")) {
4941
- const nametag = recipient.slice(1);
4942
- const pubkey = await this.deps.transport.resolveNametag?.(nametag);
4943
- if (!pubkey) {
4944
- throw new Error(`Nametag not found: ${nametag}`);
4945
- }
4946
- return pubkey;
6017
+ resolveTransportPubkey(recipient, peerInfo) {
6018
+ if (peerInfo?.transportPubkey) {
6019
+ return peerInfo.transportPubkey;
4947
6020
  }
4948
- if (this.isL3Address(recipient)) {
4949
- return recipient;
4950
- }
4951
- if (this.deps?.transport.resolveNametag) {
4952
- const pubkey = await this.deps.transport.resolveNametag(recipient);
4953
- if (pubkey) {
4954
- this.log(`Resolved "${recipient}" as nametag to pubkey`);
4955
- return pubkey;
6021
+ if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
6022
+ if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
6023
+ return recipient.slice(2);
4956
6024
  }
6025
+ return recipient;
4957
6026
  }
4958
6027
  throw new Error(
4959
- `Recipient "${recipient}" is not a valid nametag or address. Use @nametag for explicit nametag or a valid hex pubkey/PROXY:/DIRECT: address.`
6028
+ `Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
4960
6029
  );
4961
6030
  }
4962
6031
  /**
@@ -4964,9 +6033,9 @@ var PaymentsModule = class {
4964
6033
  */
4965
6034
  async createSdkCommitment(token, recipientAddress, signingService) {
4966
6035
  const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
4967
- const sdkToken = await import_Token4.Token.fromJSON(tokenData);
6036
+ const sdkToken = await import_Token6.Token.fromJSON(tokenData);
4968
6037
  const salt = crypto.getRandomValues(new Uint8Array(32));
4969
- const commitment = await import_TransferCommitment2.TransferCommitment.create(
6038
+ const commitment = await import_TransferCommitment4.TransferCommitment.create(
4970
6039
  sdkToken,
4971
6040
  recipientAddress,
4972
6041
  salt,
@@ -4992,75 +6061,264 @@ var PaymentsModule = class {
4992
6061
  * Create DirectAddress from a public key using UnmaskedPredicateReference
4993
6062
  */
4994
6063
  async createDirectAddressFromPubkey(pubkeyHex) {
4995
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4996
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6064
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
6065
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4997
6066
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4998
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
6067
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4999
6068
  const pubkeyBytes = new Uint8Array(
5000
6069
  pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
5001
6070
  );
5002
- const addressRef = await UnmaskedPredicateReference3.create(
6071
+ const addressRef = await UnmaskedPredicateReference4.create(
5003
6072
  tokenType,
5004
6073
  "secp256k1",
5005
6074
  pubkeyBytes,
5006
- import_HashAlgorithm3.HashAlgorithm.SHA256
6075
+ import_HashAlgorithm5.HashAlgorithm.SHA256
5007
6076
  );
5008
6077
  return addressRef.toAddress();
5009
6078
  }
5010
6079
  /**
5011
- * Resolve nametag to 33-byte compressed public key using resolveNametagInfo
5012
- * Returns null if nametag not found or publicKey not available
6080
+ * Resolve recipient to IAddress for L3 transfers.
6081
+ * Uses pre-resolved PeerInfo when available to avoid redundant network queries.
5013
6082
  */
5014
- async resolveNametagToPublicKey(nametag) {
5015
- if (!this.deps?.transport.resolveNametagInfo) {
5016
- this.log("resolveNametagInfo not available on transport");
5017
- return null;
6083
+ async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
6084
+ const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
6085
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
6086
+ if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
6087
+ return AddressFactory.createAddress(recipient);
5018
6088
  }
5019
- const info = await this.deps.transport.resolveNametagInfo(nametag);
6089
+ if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
6090
+ this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
6091
+ return this.createDirectAddressFromPubkey(recipient);
6092
+ }
6093
+ const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
5020
6094
  if (!info) {
5021
- this.log(`Nametag "${nametag}" not found`);
5022
- return null;
6095
+ throw new Error(
6096
+ `Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
6097
+ );
5023
6098
  }
5024
- if (!info.chainPubkey) {
5025
- this.log(`Nametag "${nametag}" has no 33-byte chainPubkey (legacy event)`);
5026
- return null;
6099
+ const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
6100
+ if (addressMode === "proxy") {
6101
+ console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
6102
+ return ProxyAddress.fromNameTag(nametag);
6103
+ }
6104
+ if (addressMode === "direct") {
6105
+ if (!info.directAddress) {
6106
+ throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
6107
+ }
6108
+ console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
6109
+ return AddressFactory.createAddress(info.directAddress);
6110
+ }
6111
+ if (info.directAddress) {
6112
+ this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
6113
+ return AddressFactory.createAddress(info.directAddress);
5027
6114
  }
5028
- return info.chainPubkey;
6115
+ this.log(`Using PROXY address for legacy nametag "${nametag}"`);
6116
+ return ProxyAddress.fromNameTag(nametag);
5029
6117
  }
5030
6118
  /**
5031
- * Resolve recipient to IAddress for L3 transfers
5032
- * Supports: nametag (with or without @), PROXY:, DIRECT:, hex pubkey
6119
+ * Handle NOSTR-FIRST commitment-only transfer (recipient side)
6120
+ * This is called when receiving a transfer with only commitmentData and no proof yet.
6121
+ * We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
5033
6122
  */
5034
- async resolveRecipientAddress(recipient) {
5035
- const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
5036
- if (recipient.startsWith("@")) {
5037
- const nametag = recipient.slice(1);
5038
- const publicKey2 = await this.resolveNametagToPublicKey(nametag);
5039
- if (publicKey2) {
5040
- this.log(`Resolved @${nametag} to 33-byte publicKey for DirectAddress`);
5041
- return this.createDirectAddressFromPubkey(publicKey2);
6123
+ async handleCommitmentOnlyTransfer(transfer, payload) {
6124
+ try {
6125
+ const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
6126
+ const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
6127
+ if (!sourceTokenInput || !commitmentInput) {
6128
+ console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
6129
+ return;
5042
6130
  }
5043
- throw new Error(`Nametag "${nametag}" not found or missing publicKey`);
5044
- }
5045
- if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
5046
- return AddressFactory.createAddress(recipient);
5047
- }
5048
- if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
5049
- this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
5050
- return this.createDirectAddressFromPubkey(recipient);
6131
+ const tokenInfo = await parseTokenInfo(sourceTokenInput);
6132
+ const token = {
6133
+ id: tokenInfo.tokenId ?? crypto.randomUUID(),
6134
+ coinId: tokenInfo.coinId,
6135
+ symbol: tokenInfo.symbol,
6136
+ name: tokenInfo.name,
6137
+ decimals: tokenInfo.decimals,
6138
+ iconUrl: tokenInfo.iconUrl,
6139
+ amount: tokenInfo.amount,
6140
+ status: "submitted",
6141
+ // NOSTR-FIRST: unconfirmed until proof
6142
+ createdAt: Date.now(),
6143
+ updatedAt: Date.now(),
6144
+ sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
6145
+ };
6146
+ const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
6147
+ const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
6148
+ if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
6149
+ this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
6150
+ return;
6151
+ }
6152
+ this.tokens.set(token.id, token);
6153
+ await this.save();
6154
+ this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
6155
+ const incomingTransfer = {
6156
+ id: transfer.id,
6157
+ senderPubkey: transfer.senderTransportPubkey,
6158
+ tokens: [token],
6159
+ memo: payload.memo,
6160
+ receivedAt: transfer.timestamp
6161
+ };
6162
+ this.deps.emitEvent("transfer:incoming", incomingTransfer);
6163
+ try {
6164
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
6165
+ const requestIdBytes = commitment.requestId;
6166
+ const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
6167
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6168
+ if (stClient) {
6169
+ const response = await stClient.submitTransferCommitment(commitment);
6170
+ this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
6171
+ }
6172
+ this.addProofPollingJob({
6173
+ tokenId: token.id,
6174
+ requestIdHex,
6175
+ commitmentJson: JSON.stringify(commitmentInput),
6176
+ startedAt: Date.now(),
6177
+ attemptCount: 0,
6178
+ lastAttemptAt: 0,
6179
+ onProofReceived: async (tokenId) => {
6180
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
6181
+ }
6182
+ });
6183
+ } catch (err) {
6184
+ console.error("[Payments] Failed to parse commitment for proof polling:", err);
6185
+ }
6186
+ } catch (error) {
6187
+ console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
5051
6188
  }
5052
- const publicKey = await this.resolveNametagToPublicKey(recipient);
5053
- if (publicKey) {
5054
- this.log(`Resolved "${recipient}" as nametag to 33-byte publicKey for DirectAddress`);
5055
- return this.createDirectAddressFromPubkey(publicKey);
6189
+ }
6190
+ /**
6191
+ * Shared finalization logic for received transfers.
6192
+ * Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
6193
+ */
6194
+ async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
6195
+ const recipientAddress = transferTx.data.recipient;
6196
+ const addressScheme = recipientAddress.scheme;
6197
+ const signingService = await this.createSigningService();
6198
+ const transferSalt = transferTx.data.salt;
6199
+ const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
6200
+ sourceToken.id,
6201
+ sourceToken.type,
6202
+ signingService,
6203
+ import_HashAlgorithm5.HashAlgorithm.SHA256,
6204
+ transferSalt
6205
+ );
6206
+ const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
6207
+ let nametagTokens = [];
6208
+ if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
6209
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
6210
+ if (!this.nametag?.token) {
6211
+ throw new Error("Cannot finalize PROXY transfer - no nametag token");
6212
+ }
6213
+ const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
6214
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
6215
+ if (proxy.address !== recipientAddress.address) {
6216
+ throw new Error(
6217
+ `PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
6218
+ );
6219
+ }
6220
+ nametagTokens = [nametagToken];
5056
6221
  }
5057
- throw new Error(
5058
- `Recipient "${recipient}" is not a valid nametag or L3 address. Use @nametag for explicit nametag or a valid 33-byte hex pubkey/PROXY:/DIRECT: address.`
6222
+ return stClient.finalizeTransaction(
6223
+ trustBase,
6224
+ sourceToken,
6225
+ recipientState,
6226
+ transferTx,
6227
+ nametagTokens
5059
6228
  );
5060
6229
  }
6230
+ /**
6231
+ * Finalize a received token after proof is available
6232
+ */
6233
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
6234
+ try {
6235
+ const token = this.tokens.get(tokenId);
6236
+ if (!token) {
6237
+ this.log(`Token ${tokenId} not found for finalization`);
6238
+ return;
6239
+ }
6240
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
6241
+ if (!this.deps.oracle.waitForProofSdk) {
6242
+ this.log("Cannot finalize - no waitForProofSdk");
6243
+ token.status = "confirmed";
6244
+ token.updatedAt = Date.now();
6245
+ await this.save();
6246
+ return;
6247
+ }
6248
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6249
+ const transferTx = commitment.toTransaction(inclusionProof);
6250
+ const sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
6251
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6252
+ const trustBase = this.deps.oracle.getTrustBase?.();
6253
+ if (!stClient || !trustBase) {
6254
+ this.log("Cannot finalize - missing state transition client or trust base");
6255
+ token.status = "confirmed";
6256
+ token.updatedAt = Date.now();
6257
+ await this.save();
6258
+ return;
6259
+ }
6260
+ const finalizedSdkToken = await this.finalizeTransferToken(
6261
+ sourceToken,
6262
+ transferTx,
6263
+ stClient,
6264
+ trustBase
6265
+ );
6266
+ const finalizedToken = {
6267
+ ...token,
6268
+ status: "confirmed",
6269
+ updatedAt: Date.now(),
6270
+ sdkData: JSON.stringify(finalizedSdkToken.toJSON())
6271
+ };
6272
+ this.tokens.set(tokenId, finalizedToken);
6273
+ await this.save();
6274
+ await this.saveTokenToFileStorage(finalizedToken);
6275
+ this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
6276
+ this.deps.emitEvent("transfer:confirmed", {
6277
+ id: crypto.randomUUID(),
6278
+ status: "completed",
6279
+ tokens: [finalizedToken]
6280
+ });
6281
+ await this.addToHistory({
6282
+ type: "RECEIVED",
6283
+ amount: finalizedToken.amount,
6284
+ coinId: finalizedToken.coinId,
6285
+ symbol: finalizedToken.symbol,
6286
+ timestamp: Date.now(),
6287
+ senderPubkey
6288
+ });
6289
+ } catch (error) {
6290
+ console.error("[Payments] Failed to finalize received token:", error);
6291
+ const token = this.tokens.get(tokenId);
6292
+ if (token && token.status === "submitted") {
6293
+ token.status = "confirmed";
6294
+ token.updatedAt = Date.now();
6295
+ await this.save();
6296
+ }
6297
+ }
6298
+ }
5061
6299
  async handleIncomingTransfer(transfer) {
5062
6300
  try {
5063
6301
  const payload = transfer.payload;
6302
+ if (isInstantSplitBundle(payload)) {
6303
+ this.log("Processing INSTANT_SPLIT bundle...");
6304
+ try {
6305
+ if (!this.nametag) {
6306
+ await this.loadNametagFromFileStorage();
6307
+ }
6308
+ const result = await this.processInstantSplitBundle(
6309
+ payload,
6310
+ transfer.senderTransportPubkey
6311
+ );
6312
+ if (result.success) {
6313
+ this.log("INSTANT_SPLIT processed successfully");
6314
+ } else {
6315
+ console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
6316
+ }
6317
+ } catch (err) {
6318
+ console.error("[Payments] INSTANT_SPLIT processing error:", err);
6319
+ }
6320
+ return;
6321
+ }
5064
6322
  let tokenData;
5065
6323
  let finalizedSdkToken = null;
5066
6324
  if (payload.sourceToken && payload.transferTx) {
@@ -5071,82 +6329,71 @@ var PaymentsModule = class {
5071
6329
  console.warn("[Payments] Invalid Sphere wallet transfer format");
5072
6330
  return;
5073
6331
  }
5074
- const sourceToken = await import_Token4.Token.fromJSON(sourceTokenInput);
5075
- const transferTx = await import_TransferTransaction.TransferTransaction.fromJSON(transferTxInput);
5076
- const recipientAddress = transferTx.data.recipient;
5077
- const addressScheme = recipientAddress.scheme;
5078
- if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
5079
- if (!this.nametag?.token) {
5080
- console.error("[Payments] Cannot finalize PROXY transfer - no nametag token. Token rejected.");
5081
- return;
5082
- }
5083
- {
5084
- try {
5085
- const nametagToken = await import_Token4.Token.fromJSON(this.nametag.token);
5086
- const signingService = await this.createSigningService();
5087
- const transferSalt = transferTx.data.salt;
5088
- const recipientPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
5089
- sourceToken.id,
5090
- sourceToken.type,
5091
- signingService,
5092
- import_HashAlgorithm3.HashAlgorithm.SHA256,
5093
- transferSalt
5094
- );
5095
- const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
5096
- const stClient = this.deps.oracle.getStateTransitionClient?.();
5097
- const trustBase = this.deps.oracle.getTrustBase?.();
5098
- if (!stClient || !trustBase) {
5099
- console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
5100
- return;
5101
- }
5102
- finalizedSdkToken = await stClient.finalizeTransaction(
5103
- trustBase,
5104
- sourceToken,
5105
- recipientState,
5106
- transferTx,
5107
- [nametagToken]
5108
- );
5109
- tokenData = finalizedSdkToken.toJSON();
5110
- this.log("Token finalized successfully");
5111
- } catch (finalizeError) {
5112
- console.error("[Payments] Finalization failed:", finalizeError);
6332
+ let sourceToken;
6333
+ let transferTx;
6334
+ try {
6335
+ sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
6336
+ } catch (err) {
6337
+ console.error("[Payments] Failed to parse sourceToken:", err);
6338
+ return;
6339
+ }
6340
+ try {
6341
+ const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
6342
+ const hasData = transferTxInput.data !== void 0;
6343
+ const hasTransactionData = transferTxInput.transactionData !== void 0;
6344
+ const hasAuthenticator = transferTxInput.authenticator !== void 0;
6345
+ if (hasData && hasInclusionProof) {
6346
+ transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
6347
+ } else if (hasTransactionData && hasAuthenticator) {
6348
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
6349
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6350
+ if (!stClient) {
6351
+ console.error("[Payments] Cannot process commitment - no state transition client");
5113
6352
  return;
5114
6353
  }
5115
- }
5116
- } else {
5117
- this.log("Finalizing DIRECT address transfer for state tracking...");
5118
- try {
5119
- const signingService = await this.createSigningService();
5120
- const transferSalt = transferTx.data.salt;
5121
- const recipientPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
5122
- sourceToken.id,
5123
- sourceToken.type,
5124
- signingService,
5125
- import_HashAlgorithm3.HashAlgorithm.SHA256,
5126
- transferSalt
5127
- );
5128
- const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
5129
- const stClient = this.deps.oracle.getStateTransitionClient?.();
5130
- const trustBase = this.deps.oracle.getTrustBase?.();
5131
- if (!stClient || !trustBase) {
5132
- this.log("Cannot finalize DIRECT transfer - missing client, using source token");
5133
- tokenData = sourceTokenInput;
5134
- } else {
5135
- finalizedSdkToken = await stClient.finalizeTransaction(
5136
- trustBase,
5137
- sourceToken,
5138
- recipientState,
5139
- transferTx,
5140
- []
5141
- // No nametag tokens needed for DIRECT
5142
- );
5143
- tokenData = finalizedSdkToken.toJSON();
5144
- this.log("DIRECT transfer finalized successfully");
6354
+ const response = await stClient.submitTransferCommitment(commitment);
6355
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
6356
+ console.error("[Payments] Transfer commitment submission failed:", response.status);
6357
+ return;
6358
+ }
6359
+ if (!this.deps.oracle.waitForProofSdk) {
6360
+ console.error("[Payments] Cannot wait for proof - missing oracle method");
6361
+ return;
6362
+ }
6363
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6364
+ transferTx = commitment.toTransaction(inclusionProof);
6365
+ } else {
6366
+ try {
6367
+ transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
6368
+ } catch {
6369
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
6370
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6371
+ if (!stClient || !this.deps.oracle.waitForProofSdk) {
6372
+ throw new Error("Cannot submit commitment - missing oracle methods");
6373
+ }
6374
+ await stClient.submitTransferCommitment(commitment);
6375
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
6376
+ transferTx = commitment.toTransaction(inclusionProof);
5145
6377
  }
5146
- } catch (finalizeError) {
5147
- this.log("DIRECT finalization failed, using source token:", finalizeError);
5148
- tokenData = sourceTokenInput;
5149
6378
  }
6379
+ } catch (err) {
6380
+ console.error("[Payments] Failed to parse transferTx:", err);
6381
+ return;
6382
+ }
6383
+ try {
6384
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6385
+ const trustBase = this.deps.oracle.getTrustBase?.();
6386
+ if (!stClient || !trustBase) {
6387
+ console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
6388
+ return;
6389
+ }
6390
+ finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
6391
+ tokenData = finalizedSdkToken.toJSON();
6392
+ const addressScheme = transferTx.data.recipient.scheme;
6393
+ this.log(`${addressScheme === import_AddressScheme.AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
6394
+ } catch (finalizeError) {
6395
+ console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
6396
+ return;
5150
6397
  }
5151
6398
  } else if (payload.token) {
5152
6399
  tokenData = payload.token;
@@ -5173,12 +6420,6 @@ var PaymentsModule = class {
5173
6420
  updatedAt: Date.now(),
5174
6421
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
5175
6422
  };
5176
- const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
5177
- const stateHash = extractStateHashFromSdkData(token.sdkData);
5178
- if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
5179
- this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
5180
- return;
5181
- }
5182
6423
  await this.addToken(token);
5183
6424
  const incomingTransfer = {
5184
6425
  id: transfer.id,
@@ -5266,14 +6507,159 @@ var PaymentsModule = class {
5266
6507
  }
5267
6508
  loadFromStorageData(data) {
5268
6509
  const parsed = parseTxfStorageData(data);
6510
+ this.tombstones = parsed.tombstones;
5269
6511
  this.tokens.clear();
5270
6512
  for (const token of parsed.tokens) {
6513
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6514
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6515
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6516
+ this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
6517
+ continue;
6518
+ }
5271
6519
  this.tokens.set(token.id, token);
5272
6520
  }
5273
- this.tombstones = parsed.tombstones;
5274
6521
  this.archivedTokens = parsed.archivedTokens;
5275
6522
  this.forkedTokens = parsed.forkedTokens;
5276
- this.nametag = parsed.nametag;
6523
+ if (parsed.nametag !== null) {
6524
+ this.nametag = parsed.nametag;
6525
+ }
6526
+ }
6527
+ // ===========================================================================
6528
+ // Private: NOSTR-FIRST Proof Polling
6529
+ // ===========================================================================
6530
+ /**
6531
+ * Submit commitment to aggregator and start background proof polling
6532
+ * (NOSTR-FIRST pattern: fire-and-forget submission)
6533
+ */
6534
+ async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
6535
+ try {
6536
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6537
+ if (!stClient) {
6538
+ this.log("Cannot submit commitment - no state transition client");
6539
+ return;
6540
+ }
6541
+ const response = await stClient.submitTransferCommitment(commitment);
6542
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
6543
+ this.log(`Transfer commitment submission failed: ${response.status}`);
6544
+ const token = this.tokens.get(tokenId);
6545
+ if (token) {
6546
+ token.status = "invalid";
6547
+ token.updatedAt = Date.now();
6548
+ this.tokens.set(tokenId, token);
6549
+ await this.save();
6550
+ }
6551
+ return;
6552
+ }
6553
+ this.addProofPollingJob({
6554
+ tokenId,
6555
+ requestIdHex,
6556
+ commitmentJson: JSON.stringify(commitment.toJSON()),
6557
+ startedAt: Date.now(),
6558
+ attemptCount: 0,
6559
+ lastAttemptAt: 0,
6560
+ onProofReceived
6561
+ });
6562
+ } catch (error) {
6563
+ this.log("submitAndPollForProof error:", error);
6564
+ }
6565
+ }
6566
+ /**
6567
+ * Add a proof polling job to the queue
6568
+ */
6569
+ addProofPollingJob(job) {
6570
+ this.proofPollingJobs.set(job.tokenId, job);
6571
+ this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
6572
+ this.startProofPolling();
6573
+ }
6574
+ /**
6575
+ * Start the proof polling interval if not already running
6576
+ */
6577
+ startProofPolling() {
6578
+ if (this.proofPollingInterval) return;
6579
+ if (this.proofPollingJobs.size === 0) return;
6580
+ this.log("Starting proof polling...");
6581
+ this.proofPollingInterval = setInterval(
6582
+ () => this.processProofPollingQueue(),
6583
+ _PaymentsModule.PROOF_POLLING_INTERVAL_MS
6584
+ );
6585
+ }
6586
+ /**
6587
+ * Stop the proof polling interval
6588
+ */
6589
+ stopProofPolling() {
6590
+ if (this.proofPollingInterval) {
6591
+ clearInterval(this.proofPollingInterval);
6592
+ this.proofPollingInterval = null;
6593
+ this.log("Stopped proof polling");
6594
+ }
6595
+ }
6596
+ /**
6597
+ * Process all pending proof polling jobs
6598
+ */
6599
+ async processProofPollingQueue() {
6600
+ if (this.proofPollingJobs.size === 0) {
6601
+ this.stopProofPolling();
6602
+ return;
6603
+ }
6604
+ const completedJobs = [];
6605
+ for (const [tokenId, job] of this.proofPollingJobs) {
6606
+ try {
6607
+ job.attemptCount++;
6608
+ job.lastAttemptAt = Date.now();
6609
+ if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
6610
+ this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
6611
+ const token2 = this.tokens.get(tokenId);
6612
+ if (token2 && token2.status === "submitted") {
6613
+ token2.status = "invalid";
6614
+ token2.updatedAt = Date.now();
6615
+ this.tokens.set(tokenId, token2);
6616
+ }
6617
+ completedJobs.push(tokenId);
6618
+ continue;
6619
+ }
6620
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(JSON.parse(job.commitmentJson));
6621
+ let inclusionProof = null;
6622
+ try {
6623
+ const abortController = new AbortController();
6624
+ const timeoutId = setTimeout(() => abortController.abort(), 500);
6625
+ if (this.deps.oracle.waitForProofSdk) {
6626
+ inclusionProof = await Promise.race([
6627
+ this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
6628
+ new Promise((resolve) => setTimeout(() => resolve(null), 500))
6629
+ ]);
6630
+ } else {
6631
+ const proof = await this.deps.oracle.getProof(job.requestIdHex);
6632
+ if (proof) {
6633
+ inclusionProof = proof;
6634
+ }
6635
+ }
6636
+ clearTimeout(timeoutId);
6637
+ } catch (err) {
6638
+ continue;
6639
+ }
6640
+ if (!inclusionProof) {
6641
+ continue;
6642
+ }
6643
+ const token = this.tokens.get(tokenId);
6644
+ if (token) {
6645
+ token.status = "spent";
6646
+ token.updatedAt = Date.now();
6647
+ this.tokens.set(tokenId, token);
6648
+ await this.save();
6649
+ this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
6650
+ }
6651
+ job.onProofReceived?.(tokenId);
6652
+ completedJobs.push(tokenId);
6653
+ } catch (error) {
6654
+ this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
6655
+ }
6656
+ }
6657
+ for (const tokenId of completedJobs) {
6658
+ this.proofPollingJobs.delete(tokenId);
6659
+ }
6660
+ if (this.proofPollingJobs.size === 0) {
6661
+ this.stopProofPolling();
6662
+ }
5277
6663
  }
5278
6664
  // ===========================================================================
5279
6665
  // Private: Helpers
@@ -5288,6 +6674,14 @@ function createPaymentsModule(config) {
5288
6674
  return new PaymentsModule(config);
5289
6675
  }
5290
6676
 
6677
+ // modules/payments/TokenRecoveryService.ts
6678
+ var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
6679
+ var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
6680
+ var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6681
+ var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
6682
+ var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
6683
+ var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
6684
+
5291
6685
  // modules/communications/CommunicationsModule.ts
5292
6686
  var CommunicationsModule = class {
5293
6687
  config;
@@ -6256,20 +7650,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
6256
7650
 
6257
7651
  // core/Sphere.ts
6258
7652
  var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
6259
- var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6260
- var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
6261
- var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
7653
+ var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
7654
+ var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
7655
+ var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
6262
7656
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
6263
7657
  async function deriveL3PredicateAddress(privateKey) {
6264
7658
  const secret = Buffer.from(privateKey, "hex");
6265
7659
  const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
6266
7660
  const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
6267
- const tokenType = new import_TokenType2.TokenType(tokenTypeBytes);
6268
- const predicateRef = import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
7661
+ const tokenType = new import_TokenType4.TokenType(tokenTypeBytes);
7662
+ const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
6269
7663
  tokenType,
6270
7664
  signingService.algorithm,
6271
7665
  signingService.publicKey,
6272
- import_HashAlgorithm4.HashAlgorithm.SHA256
7666
+ import_HashAlgorithm7.HashAlgorithm.SHA256
6273
7667
  );
6274
7668
  return (await (await predicateRef).toAddress()).toString();
6275
7669
  }
@@ -6285,7 +7679,11 @@ var Sphere = class _Sphere {
6285
7679
  _derivationMode = "bip32";
6286
7680
  _basePath = DEFAULT_BASE_PATH;
6287
7681
  _currentAddressIndex = 0;
6288
- /** Map of addressId -> (nametagIndex -> nametag). Supports multiple nametags per address (e.g., from Nostr recovery) */
7682
+ /** Registry of all tracked (activated) addresses, keyed by HD index */
7683
+ _trackedAddresses = /* @__PURE__ */ new Map();
7684
+ /** Reverse lookup: addressId -> HD index */
7685
+ _addressIdToIndex = /* @__PURE__ */ new Map();
7686
+ /** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
6289
7687
  _addressNametags = /* @__PURE__ */ new Map();
6290
7688
  /** Cached PROXY address (computed once when nametag is set) */
6291
7689
  _cachedProxyAddress = void 0;
@@ -6294,6 +7692,7 @@ var Sphere = class _Sphere {
6294
7692
  _tokenStorageProviders = /* @__PURE__ */ new Map();
6295
7693
  _transport;
6296
7694
  _oracle;
7695
+ _priceProvider;
6297
7696
  // Modules
6298
7697
  _payments;
6299
7698
  _communications;
@@ -6302,10 +7701,11 @@ var Sphere = class _Sphere {
6302
7701
  // ===========================================================================
6303
7702
  // Constructor (private)
6304
7703
  // ===========================================================================
6305
- constructor(storage, transport, oracle, tokenStorage, l1Config) {
7704
+ constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
6306
7705
  this._storage = storage;
6307
7706
  this._transport = transport;
6308
7707
  this._oracle = oracle;
7708
+ this._priceProvider = priceProvider ?? null;
6309
7709
  if (tokenStorage) {
6310
7710
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
6311
7711
  }
@@ -6365,7 +7765,8 @@ var Sphere = class _Sphere {
6365
7765
  transport: options.transport,
6366
7766
  oracle: options.oracle,
6367
7767
  tokenStorage: options.tokenStorage,
6368
- l1: options.l1
7768
+ l1: options.l1,
7769
+ price: options.price
6369
7770
  });
6370
7771
  return { sphere: sphere2, created: false };
6371
7772
  }
@@ -6389,7 +7790,8 @@ var Sphere = class _Sphere {
6389
7790
  tokenStorage: options.tokenStorage,
6390
7791
  derivationPath: options.derivationPath,
6391
7792
  nametag: options.nametag,
6392
- l1: options.l1
7793
+ l1: options.l1,
7794
+ price: options.price
6393
7795
  });
6394
7796
  return { sphere, created: true, generatedMnemonic };
6395
7797
  }
@@ -6408,7 +7810,8 @@ var Sphere = class _Sphere {
6408
7810
  options.transport,
6409
7811
  options.oracle,
6410
7812
  options.tokenStorage,
6411
- options.l1
7813
+ options.l1,
7814
+ options.price
6412
7815
  );
6413
7816
  await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
6414
7817
  await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
@@ -6417,10 +7820,12 @@ var Sphere = class _Sphere {
6417
7820
  await sphere.finalizeWalletCreation();
6418
7821
  sphere._initialized = true;
6419
7822
  _Sphere.instance = sphere;
7823
+ await sphere.ensureAddressTracked(0);
6420
7824
  if (options.nametag) {
6421
7825
  await sphere.registerNametag(options.nametag);
6422
7826
  } else {
6423
- await sphere.recoverNametagFromNostr();
7827
+ await sphere.syncIdentityWithTransport();
7828
+ await sphere.recoverNametagFromTransport();
6424
7829
  }
6425
7830
  return sphere;
6426
7831
  }
@@ -6436,14 +7841,28 @@ var Sphere = class _Sphere {
6436
7841
  options.transport,
6437
7842
  options.oracle,
6438
7843
  options.tokenStorage,
6439
- options.l1
7844
+ options.l1,
7845
+ options.price
6440
7846
  );
6441
7847
  await sphere.loadIdentityFromStorage();
6442
7848
  await sphere.initializeProviders();
6443
7849
  await sphere.initializeModules();
6444
- await sphere.syncNametagWithNostr();
7850
+ await sphere.syncIdentityWithTransport();
6445
7851
  sphere._initialized = true;
6446
7852
  _Sphere.instance = sphere;
7853
+ if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
7854
+ console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
7855
+ try {
7856
+ const result = await sphere.mintNametag(sphere._identity.nametag);
7857
+ if (result.success) {
7858
+ console.log(`[Sphere] Nametag token minted successfully on load`);
7859
+ } else {
7860
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
7861
+ }
7862
+ } catch (err) {
7863
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
7864
+ }
7865
+ }
6447
7866
  return sphere;
6448
7867
  }
6449
7868
  /**
@@ -6459,7 +7878,8 @@ var Sphere = class _Sphere {
6459
7878
  options.transport,
6460
7879
  options.oracle,
6461
7880
  options.tokenStorage,
6462
- options.l1
7881
+ options.l1,
7882
+ options.price
6463
7883
  );
6464
7884
  if (options.mnemonic) {
6465
7885
  if (!_Sphere.validateMnemonic(options.mnemonic)) {
@@ -6484,11 +7904,12 @@ var Sphere = class _Sphere {
6484
7904
  await sphere.initializeProviders();
6485
7905
  await sphere.initializeModules();
6486
7906
  if (!options.nametag) {
6487
- await sphere.recoverNametagFromNostr();
7907
+ await sphere.recoverNametagFromTransport();
6488
7908
  }
6489
7909
  await sphere.finalizeWalletCreation();
6490
7910
  sphere._initialized = true;
6491
7911
  _Sphere.instance = sphere;
7912
+ await sphere.ensureAddressTracked(0);
6492
7913
  if (options.nametag) {
6493
7914
  await sphere.registerNametag(options.nametag);
6494
7915
  }
@@ -6524,6 +7945,7 @@ var Sphere = class _Sphere {
6524
7945
  await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
6525
7946
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
6526
7947
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
7948
+ await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
6527
7949
  await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
6528
7950
  await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
6529
7951
  await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
@@ -6648,6 +8070,13 @@ var Sphere = class _Sphere {
6648
8070
  hasTokenStorageProvider(providerId) {
6649
8071
  return this._tokenStorageProviders.has(providerId);
6650
8072
  }
8073
+ /**
8074
+ * Set or update the price provider after initialization
8075
+ */
8076
+ setPriceProvider(provider) {
8077
+ this._priceProvider = provider;
8078
+ this._payments.setPriceProvider(provider);
8079
+ }
6651
8080
  getTransport() {
6652
8081
  return this._transport;
6653
8082
  }
@@ -7135,10 +8564,9 @@ var Sphere = class _Sphere {
7135
8564
  * @returns Primary nametag (index 0) or undefined if not registered
7136
8565
  */
7137
8566
  getNametagForAddress(addressId) {
7138
- const id = addressId ?? this.getCurrentAddressId();
8567
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
7139
8568
  if (!id) return void 0;
7140
- const nametagsMap = this._addressNametags.get(id);
7141
- return nametagsMap?.get(0);
8569
+ return this._addressNametags.get(id)?.get(0);
7142
8570
  }
7143
8571
  /**
7144
8572
  * Get all nametags for a specific address
@@ -7147,29 +8575,89 @@ var Sphere = class _Sphere {
7147
8575
  * @returns Map of nametagIndex to nametag, or undefined if no nametags
7148
8576
  */
7149
8577
  getNametagsForAddress(addressId) {
7150
- const id = addressId ?? this.getCurrentAddressId();
8578
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
7151
8579
  if (!id) return void 0;
7152
- const nametagsMap = this._addressNametags.get(id);
7153
- return nametagsMap ? new Map(nametagsMap) : void 0;
8580
+ const nametags = this._addressNametags.get(id);
8581
+ return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
7154
8582
  }
7155
8583
  /**
7156
8584
  * Get all registered address nametags
7157
- *
8585
+ * @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
7158
8586
  * @returns Map of addressId to (nametagIndex -> nametag)
7159
8587
  */
7160
8588
  getAllAddressNametags() {
7161
8589
  const result = /* @__PURE__ */ new Map();
7162
- this._addressNametags.forEach((nametagsMap, addressId) => {
7163
- result.set(addressId, new Map(nametagsMap));
7164
- });
8590
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
8591
+ if (nametags.size > 0) {
8592
+ result.set(addressId, new Map(nametags));
8593
+ }
8594
+ }
7165
8595
  return result;
7166
8596
  }
7167
8597
  /**
7168
- * Get current address identifier (DIRECT://xxx format)
8598
+ * Get all active (non-hidden) tracked addresses.
8599
+ * Returns addresses that have been activated through create, switchToAddress,
8600
+ * registerNametag, or nametag recovery.
8601
+ *
8602
+ * @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
8603
+ */
8604
+ getActiveAddresses() {
8605
+ this.ensureReady();
8606
+ const result = [];
8607
+ for (const entry of this._trackedAddresses.values()) {
8608
+ if (!entry.hidden) {
8609
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8610
+ result.push({ ...entry, nametag });
8611
+ }
8612
+ }
8613
+ return result.sort((a, b) => a.index - b.index);
8614
+ }
8615
+ /**
8616
+ * Get all tracked addresses, including hidden ones.
8617
+ *
8618
+ * @returns Array of all TrackedAddress entries sorted by index
8619
+ */
8620
+ getAllTrackedAddresses() {
8621
+ this.ensureReady();
8622
+ const result = [];
8623
+ for (const entry of this._trackedAddresses.values()) {
8624
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8625
+ result.push({ ...entry, nametag });
8626
+ }
8627
+ return result.sort((a, b) => a.index - b.index);
8628
+ }
8629
+ /**
8630
+ * Get tracked address info by index.
8631
+ *
8632
+ * @param index - Address index
8633
+ * @returns TrackedAddress or undefined if not tracked
8634
+ */
8635
+ getTrackedAddress(index) {
8636
+ this.ensureReady();
8637
+ const entry = this._trackedAddresses.get(index);
8638
+ if (!entry) return void 0;
8639
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8640
+ return { ...entry, nametag };
8641
+ }
8642
+ /**
8643
+ * Set visibility of a tracked address.
8644
+ * Hidden addresses are not returned by getActiveAddresses() but remain tracked.
8645
+ *
8646
+ * @param index - Address index to hide/unhide
8647
+ * @param hidden - true to hide, false to show
8648
+ * @throws Error if address index is not tracked
7169
8649
  */
7170
- getCurrentAddressId() {
7171
- if (!this._identity?.directAddress) return void 0;
7172
- return getAddressId(this._identity.directAddress);
8650
+ async setAddressHidden(index, hidden) {
8651
+ this.ensureReady();
8652
+ const entry = this._trackedAddresses.get(index);
8653
+ if (!entry) {
8654
+ throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
8655
+ }
8656
+ if (entry.hidden === hidden) return;
8657
+ entry.hidden = hidden;
8658
+ await this.persistTrackedAddresses();
8659
+ const eventType = hidden ? "address:hidden" : "address:unhidden";
8660
+ this.emitEvent(eventType, { index, addressId: entry.addressId });
7173
8661
  }
7174
8662
  /**
7175
8663
  * Switch to a different address by index
@@ -7190,7 +8678,7 @@ var Sphere = class _Sphere {
7190
8678
  * await sphere.switchToAddress(0);
7191
8679
  * ```
7192
8680
  */
7193
- async switchToAddress(index) {
8681
+ async switchToAddress(index, options) {
7194
8682
  this.ensureReady();
7195
8683
  if (!this._masterKey) {
7196
8684
  throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
@@ -7198,12 +8686,28 @@ var Sphere = class _Sphere {
7198
8686
  if (index < 0) {
7199
8687
  throw new Error("Address index must be non-negative");
7200
8688
  }
8689
+ const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
8690
+ if (newNametag && !this.validateNametag(newNametag)) {
8691
+ throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
8692
+ }
7201
8693
  const addressInfo = this.deriveAddress(index, false);
7202
8694
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
7203
8695
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
8696
+ await this.ensureAddressTracked(index);
7204
8697
  const addressId = getAddressId(predicateAddress);
7205
- const nametagsMap = this._addressNametags.get(addressId);
7206
- const nametag = nametagsMap?.get(0);
8698
+ if (newNametag) {
8699
+ const existing = await this._transport.resolveNametag?.(newNametag);
8700
+ if (existing) {
8701
+ throw new Error(`Nametag @${newNametag} is already taken`);
8702
+ }
8703
+ let nametags = this._addressNametags.get(addressId);
8704
+ if (!nametags) {
8705
+ nametags = /* @__PURE__ */ new Map();
8706
+ this._addressNametags.set(addressId, nametags);
8707
+ }
8708
+ nametags.set(0, newNametag);
8709
+ }
8710
+ const nametag = this._addressNametags.get(addressId)?.get(0);
7207
8711
  this._identity = {
7208
8712
  privateKey: addressInfo.privateKey,
7209
8713
  chainPubkey: addressInfo.publicKey,
@@ -7216,11 +8720,47 @@ var Sphere = class _Sphere {
7216
8720
  await this._updateCachedProxyAddress();
7217
8721
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
7218
8722
  this._storage.setIdentity(this._identity);
7219
- this._transport.setIdentity(this._identity);
8723
+ await this._transport.setIdentity(this._identity);
7220
8724
  for (const provider of this._tokenStorageProviders.values()) {
7221
8725
  provider.setIdentity(this._identity);
8726
+ await provider.initialize();
7222
8727
  }
7223
8728
  await this.reinitializeModulesForNewAddress();
8729
+ if (this._identity.nametag) {
8730
+ await this.syncIdentityWithTransport();
8731
+ }
8732
+ if (newNametag) {
8733
+ await this.persistAddressNametags();
8734
+ if (!this._payments.hasNametag()) {
8735
+ console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
8736
+ try {
8737
+ const result = await this.mintNametag(newNametag);
8738
+ if (result.success) {
8739
+ console.log(`[Sphere] Nametag token minted successfully`);
8740
+ } else {
8741
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
8742
+ }
8743
+ } catch (err) {
8744
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
8745
+ }
8746
+ }
8747
+ this.emitEvent("nametag:registered", {
8748
+ nametag: newNametag,
8749
+ addressIndex: index
8750
+ });
8751
+ } else if (this._identity.nametag && !this._payments.hasNametag()) {
8752
+ console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
8753
+ try {
8754
+ const result = await this.mintNametag(this._identity.nametag);
8755
+ if (result.success) {
8756
+ console.log(`[Sphere] Nametag token minted successfully after switch`);
8757
+ } else {
8758
+ console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
8759
+ }
8760
+ } catch (err) {
8761
+ console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
8762
+ }
8763
+ }
7224
8764
  this.emitEvent("identity:changed", {
7225
8765
  l1Address: this._identity.l1Address,
7226
8766
  directAddress: this._identity.directAddress,
@@ -7242,7 +8782,8 @@ var Sphere = class _Sphere {
7242
8782
  transport: this._transport,
7243
8783
  oracle: this._oracle,
7244
8784
  emitEvent,
7245
- chainCode: this._masterKey?.chainCode
8785
+ chainCode: this._masterKey?.chainCode,
8786
+ price: this._priceProvider ?? void 0
7246
8787
  });
7247
8788
  this._communications.initialize({
7248
8789
  identity: this._identity,
@@ -7275,6 +8816,14 @@ var Sphere = class _Sphere {
7275
8816
  */
7276
8817
  deriveAddress(index, isChange = false) {
7277
8818
  this.ensureReady();
8819
+ return this._deriveAddressInternal(index, isChange);
8820
+ }
8821
+ /**
8822
+ * Internal address derivation without ensureReady() check.
8823
+ * Used during initialization (loadTrackedAddresses, ensureAddressTracked)
8824
+ * when _initialized is still false.
8825
+ */
8826
+ _deriveAddressInternal(index, isChange = false) {
7278
8827
  if (!this._masterKey) {
7279
8828
  throw new Error("HD derivation requires master key with chain code");
7280
8829
  }
@@ -7413,6 +8962,22 @@ var Sphere = class _Sphere {
7413
8962
  getProxyAddress() {
7414
8963
  return this._cachedProxyAddress;
7415
8964
  }
8965
+ /**
8966
+ * Resolve any identifier to full peer information.
8967
+ * Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
8968
+ *
8969
+ * @example
8970
+ * ```ts
8971
+ * const peer = await sphere.resolve('@alice');
8972
+ * const peer = await sphere.resolve('DIRECT://...');
8973
+ * const peer = await sphere.resolve('alpha1...');
8974
+ * const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
8975
+ * ```
8976
+ */
8977
+ async resolve(identifier) {
8978
+ this.ensureReady();
8979
+ return this._transport.resolve?.(identifier) ?? null;
8980
+ }
7416
8981
  /** Compute and cache the PROXY address from the current nametag */
7417
8982
  async _updateCachedProxyAddress() {
7418
8983
  const nametag = this._identity?.nametag;
@@ -7451,11 +9016,12 @@ var Sphere = class _Sphere {
7451
9016
  if (this._identity?.nametag) {
7452
9017
  throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
7453
9018
  }
7454
- if (this._transport.registerNametag) {
7455
- const success = await this._transport.registerNametag(
7456
- cleanNametag,
9019
+ if (this._transport.publishIdentityBinding) {
9020
+ const success = await this._transport.publishIdentityBinding(
7457
9021
  this._identity.chainPubkey,
7458
- this._identity.directAddress || ""
9022
+ this._identity.l1Address,
9023
+ this._identity.directAddress || "",
9024
+ cleanNametag
7459
9025
  );
7460
9026
  if (!success) {
7461
9027
  throw new Error("Failed to register nametag. It may already be taken.");
@@ -7463,14 +9029,14 @@ var Sphere = class _Sphere {
7463
9029
  }
7464
9030
  this._identity.nametag = cleanNametag;
7465
9031
  await this._updateCachedProxyAddress();
7466
- const addressId = this.getCurrentAddressId();
7467
- if (addressId) {
7468
- let nametagsMap = this._addressNametags.get(addressId);
7469
- if (!nametagsMap) {
7470
- nametagsMap = /* @__PURE__ */ new Map();
7471
- this._addressNametags.set(addressId, nametagsMap);
9032
+ const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
9033
+ if (currentAddressId) {
9034
+ let nametags = this._addressNametags.get(currentAddressId);
9035
+ if (!nametags) {
9036
+ nametags = /* @__PURE__ */ new Map();
9037
+ this._addressNametags.set(currentAddressId, nametags);
7472
9038
  }
7473
- nametagsMap.set(0, cleanNametag);
9039
+ nametags.set(0, cleanNametag);
7474
9040
  }
7475
9041
  await this.persistAddressNametags();
7476
9042
  if (!this._payments.hasNametag()) {
@@ -7489,19 +9055,19 @@ var Sphere = class _Sphere {
7489
9055
  console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
7490
9056
  }
7491
9057
  /**
7492
- * Persist address nametags to storage
7493
- * Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
9058
+ * Persist tracked addresses to storage (only minimal fields via StorageProvider)
7494
9059
  */
7495
- async persistAddressNametags() {
7496
- const result = {};
7497
- this._addressNametags.forEach((nametagsMap, addressId) => {
7498
- const innerObj = {};
7499
- nametagsMap.forEach((nametag, index) => {
7500
- innerObj[index.toString()] = nametag;
9060
+ async persistTrackedAddresses() {
9061
+ const entries = [];
9062
+ for (const entry of this._trackedAddresses.values()) {
9063
+ entries.push({
9064
+ index: entry.index,
9065
+ hidden: entry.hidden,
9066
+ createdAt: entry.createdAt,
9067
+ updatedAt: entry.updatedAt
7501
9068
  });
7502
- result[addressId] = innerObj;
7503
- });
7504
- await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
9069
+ }
9070
+ await this._storage.saveTrackedAddresses(entries);
7505
9071
  }
7506
9072
  /**
7507
9073
  * Mint a nametag token on-chain (like Sphere wallet and lottery)
@@ -7535,63 +9101,184 @@ var Sphere = class _Sphere {
7535
9101
  return this._payments.isNametagAvailable(nametag);
7536
9102
  }
7537
9103
  /**
7538
- * Load address nametags from storage
7539
- * Supports new format: { "DIRECT://abc...xyz": { "0": "alice" } }
7540
- * And legacy format: { "0": "alice" } (migrates to new format on save)
9104
+ * Load tracked addresses from storage.
9105
+ * Falls back to migrating from old ADDRESS_NAMETAGS format.
7541
9106
  */
7542
- async loadAddressNametags() {
9107
+ async loadTrackedAddresses() {
9108
+ this._trackedAddresses.clear();
9109
+ this._addressIdToIndex.clear();
7543
9110
  try {
7544
- const saved = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
7545
- if (saved) {
7546
- const parsed = JSON.parse(saved);
7547
- this._addressNametags.clear();
7548
- for (const [key, value] of Object.entries(parsed)) {
7549
- if (typeof value === "object" && value !== null) {
7550
- const nametagsMap = /* @__PURE__ */ new Map();
7551
- for (const [indexStr, nametag] of Object.entries(value)) {
7552
- nametagsMap.set(parseInt(indexStr, 10), nametag);
7553
- }
7554
- this._addressNametags.set(key, nametagsMap);
7555
- } else if (typeof value === "string") {
7556
- }
9111
+ const entries = await this._storage.loadTrackedAddresses();
9112
+ if (entries.length > 0) {
9113
+ for (const stored of entries) {
9114
+ const addrInfo = this._deriveAddressInternal(stored.index, false);
9115
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
9116
+ const addressId = getAddressId(directAddress);
9117
+ const entry = {
9118
+ ...stored,
9119
+ addressId,
9120
+ l1Address: addrInfo.address,
9121
+ directAddress,
9122
+ chainPubkey: addrInfo.publicKey
9123
+ };
9124
+ this._trackedAddresses.set(entry.index, entry);
9125
+ this._addressIdToIndex.set(addressId, entry.index);
7557
9126
  }
9127
+ return;
9128
+ }
9129
+ const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
9130
+ if (oldData) {
9131
+ const parsed = JSON.parse(oldData);
9132
+ await this.migrateFromOldNametagFormat(parsed);
9133
+ await this.persistTrackedAddresses();
7558
9134
  }
7559
9135
  } catch {
7560
9136
  }
7561
9137
  }
7562
9138
  /**
7563
- * Sync nametag with Nostr on wallet load
7564
- * If local nametag exists but not registered on Nostr, re-register it
9139
+ * Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
9140
+ * Scans HD indices 0..19 to match addressIds from the old format.
9141
+ * Populates both _trackedAddresses and _addressNametags.
7565
9142
  */
7566
- async syncNametagWithNostr() {
7567
- const nametag = this._identity?.nametag;
7568
- if (!nametag) {
7569
- return;
9143
+ async migrateFromOldNametagFormat(parsed) {
9144
+ const addressIdToNametags = /* @__PURE__ */ new Map();
9145
+ for (const [key, value] of Object.entries(parsed)) {
9146
+ if (typeof value === "object" && value !== null) {
9147
+ addressIdToNametags.set(key, value);
9148
+ }
9149
+ }
9150
+ if (addressIdToNametags.size === 0 || !this._masterKey) return;
9151
+ const SCAN_LIMIT = 20;
9152
+ for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
9153
+ try {
9154
+ const addrInfo = this._deriveAddressInternal(i, false);
9155
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
9156
+ const addressId = getAddressId(directAddress);
9157
+ if (addressIdToNametags.has(addressId)) {
9158
+ const nametagsObj = addressIdToNametags.get(addressId);
9159
+ const nametagMap = /* @__PURE__ */ new Map();
9160
+ for (const [idx, tag] of Object.entries(nametagsObj)) {
9161
+ nametagMap.set(parseInt(idx, 10), tag);
9162
+ }
9163
+ if (nametagMap.size > 0) {
9164
+ this._addressNametags.set(addressId, nametagMap);
9165
+ }
9166
+ const now = Date.now();
9167
+ const entry = {
9168
+ index: i,
9169
+ addressId,
9170
+ l1Address: addrInfo.address,
9171
+ directAddress,
9172
+ chainPubkey: addrInfo.publicKey,
9173
+ nametag: nametagMap.get(0),
9174
+ hidden: false,
9175
+ createdAt: now,
9176
+ updatedAt: now
9177
+ };
9178
+ this._trackedAddresses.set(i, entry);
9179
+ this._addressIdToIndex.set(addressId, i);
9180
+ addressIdToNametags.delete(addressId);
9181
+ }
9182
+ } catch {
9183
+ }
9184
+ }
9185
+ await this.persistAddressNametags();
9186
+ }
9187
+ /**
9188
+ * Ensure an address is tracked in the registry.
9189
+ * If not yet tracked, derives full info and creates the entry.
9190
+ */
9191
+ async ensureAddressTracked(index) {
9192
+ const existing = this._trackedAddresses.get(index);
9193
+ if (existing) return existing;
9194
+ const addrInfo = this._deriveAddressInternal(index, false);
9195
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
9196
+ const addressId = getAddressId(directAddress);
9197
+ const now = Date.now();
9198
+ const nametag = this._addressNametags.get(addressId)?.get(0);
9199
+ const entry = {
9200
+ index,
9201
+ addressId,
9202
+ l1Address: addrInfo.address,
9203
+ directAddress,
9204
+ chainPubkey: addrInfo.publicKey,
9205
+ nametag,
9206
+ hidden: false,
9207
+ createdAt: now,
9208
+ updatedAt: now
9209
+ };
9210
+ this._trackedAddresses.set(index, entry);
9211
+ this._addressIdToIndex.set(addressId, index);
9212
+ await this.persistTrackedAddresses();
9213
+ this.emitEvent("address:activated", { address: { ...entry } });
9214
+ return entry;
9215
+ }
9216
+ /**
9217
+ * Persist nametag cache to storage.
9218
+ * Format: { addressId: { "0": "alice", "1": "alice2" } }
9219
+ */
9220
+ async persistAddressNametags() {
9221
+ const result = {};
9222
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
9223
+ const obj = {};
9224
+ for (const [idx, tag] of nametags.entries()) {
9225
+ obj[idx.toString()] = tag;
9226
+ }
9227
+ result[addressId] = obj;
9228
+ }
9229
+ await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
9230
+ }
9231
+ /**
9232
+ * Load nametag cache from storage.
9233
+ */
9234
+ async loadAddressNametags() {
9235
+ this._addressNametags.clear();
9236
+ try {
9237
+ const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
9238
+ if (!data) return;
9239
+ const parsed = JSON.parse(data);
9240
+ for (const [addressId, nametags] of Object.entries(parsed)) {
9241
+ const map = /* @__PURE__ */ new Map();
9242
+ for (const [idx, tag] of Object.entries(nametags)) {
9243
+ map.set(parseInt(idx, 10), tag);
9244
+ }
9245
+ this._addressNametags.set(addressId, map);
9246
+ }
9247
+ } catch {
7570
9248
  }
7571
- if (!this._transport.resolveNametag || !this._transport.registerNametag) {
9249
+ }
9250
+ /**
9251
+ * Publish identity binding via transport.
9252
+ * Always publishes base identity (chainPubkey, l1Address, directAddress).
9253
+ * If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
9254
+ */
9255
+ async syncIdentityWithTransport() {
9256
+ if (!this._transport.publishIdentityBinding) {
7572
9257
  return;
7573
9258
  }
7574
9259
  try {
7575
- const success = await this._transport.registerNametag(
7576
- nametag,
9260
+ const nametag = this._identity?.nametag;
9261
+ const success = await this._transport.publishIdentityBinding(
7577
9262
  this._identity.chainPubkey,
7578
- this._identity.directAddress || ""
9263
+ this._identity.l1Address,
9264
+ this._identity.directAddress || "",
9265
+ nametag || void 0
7579
9266
  );
7580
9267
  if (success) {
7581
- console.log(`[Sphere] Nametag @${nametag} synced with Nostr`);
7582
- } else {
9268
+ console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
9269
+ } else if (nametag) {
7583
9270
  console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
7584
9271
  }
7585
9272
  } catch (error) {
7586
- console.warn(`[Sphere] Nametag sync failed:`, error);
9273
+ console.warn(`[Sphere] Identity binding sync failed:`, error);
7587
9274
  }
7588
9275
  }
7589
9276
  /**
7590
- * Recover nametag from Nostr after wallet import
9277
+ * Recover nametag from transport after wallet import.
7591
9278
  * Searches for encrypted nametag events authored by this wallet's pubkey
7592
- * and decrypts them to restore the nametag association
9279
+ * and decrypts them to restore the nametag association.
7593
9280
  */
7594
- async recoverNametagFromNostr() {
9281
+ async recoverNametagFromTransport() {
7595
9282
  if (this._identity?.nametag) {
7596
9283
  return;
7597
9284
  }
@@ -7605,22 +9292,21 @@ var Sphere = class _Sphere {
7605
9292
  this._identity.nametag = recoveredNametag;
7606
9293
  await this._updateCachedProxyAddress();
7607
9294
  }
7608
- const addressId = this.getCurrentAddressId();
7609
- if (addressId) {
7610
- let nametagsMap = this._addressNametags.get(addressId);
7611
- if (!nametagsMap) {
7612
- nametagsMap = /* @__PURE__ */ new Map();
7613
- this._addressNametags.set(addressId, nametagsMap);
7614
- }
7615
- const nextIndex = nametagsMap.size;
7616
- nametagsMap.set(nextIndex, recoveredNametag);
9295
+ const entry = await this.ensureAddressTracked(this._currentAddressIndex);
9296
+ let nametags = this._addressNametags.get(entry.addressId);
9297
+ if (!nametags) {
9298
+ nametags = /* @__PURE__ */ new Map();
9299
+ this._addressNametags.set(entry.addressId, nametags);
7617
9300
  }
9301
+ const nextIndex = nametags.size;
9302
+ nametags.set(nextIndex, recoveredNametag);
7618
9303
  await this.persistAddressNametags();
7619
- if (this._transport.registerNametag) {
7620
- await this._transport.registerNametag(
7621
- recoveredNametag,
9304
+ if (this._transport.publishIdentityBinding) {
9305
+ await this._transport.publishIdentityBinding(
7622
9306
  this._identity.chainPubkey,
7623
- this._identity.directAddress || ""
9307
+ this._identity.l1Address,
9308
+ this._identity.directAddress || "",
9309
+ recoveredNametag
7624
9310
  );
7625
9311
  }
7626
9312
  this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
@@ -7648,6 +9334,9 @@ var Sphere = class _Sphere {
7648
9334
  await this._oracle.disconnect();
7649
9335
  this._initialized = false;
7650
9336
  this._identity = null;
9337
+ this._trackedAddresses.clear();
9338
+ this._addressIdToIndex.clear();
9339
+ this._addressNametags.clear();
7651
9340
  this.eventHandlers.clear();
7652
9341
  if (_Sphere.instance === this) {
7653
9342
  _Sphere.instance = null;
@@ -7745,14 +9434,14 @@ var Sphere = class _Sphere {
7745
9434
  if (this._identity) {
7746
9435
  this._storage.setIdentity(this._identity);
7747
9436
  }
9437
+ await this.loadTrackedAddresses();
7748
9438
  await this.loadAddressNametags();
9439
+ const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
9440
+ const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
7749
9441
  if (this._currentAddressIndex > 0 && this._masterKey) {
7750
- const addressInfo = this.deriveAddress(this._currentAddressIndex, false);
9442
+ const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
7751
9443
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
7752
9444
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
7753
- const addressId = getAddressId(predicateAddress);
7754
- const nametagsMap = this._addressNametags.get(addressId);
7755
- const nametag = nametagsMap?.get(0);
7756
9445
  this._identity = {
7757
9446
  privateKey: addressInfo.privateKey,
7758
9447
  chainPubkey: addressInfo.publicKey,
@@ -7763,13 +9452,8 @@ var Sphere = class _Sphere {
7763
9452
  };
7764
9453
  this._storage.setIdentity(this._identity);
7765
9454
  console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
7766
- } else if (this._identity) {
7767
- const addressId = this.getCurrentAddressId();
7768
- const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
7769
- const nametag = nametagsMap?.get(0);
7770
- if (nametag) {
7771
- this._identity.nametag = nametag;
7772
- }
9455
+ } else if (this._identity && nametag) {
9456
+ this._identity.nametag = nametag;
7773
9457
  }
7774
9458
  await this._updateCachedProxyAddress();
7775
9459
  }
@@ -7827,7 +9511,7 @@ var Sphere = class _Sphere {
7827
9511
  // ===========================================================================
7828
9512
  async initializeProviders() {
7829
9513
  this._storage.setIdentity(this._identity);
7830
- this._transport.setIdentity(this._identity);
9514
+ await this._transport.setIdentity(this._identity);
7831
9515
  for (const provider of this._tokenStorageProviders.values()) {
7832
9516
  provider.setIdentity(this._identity);
7833
9517
  }
@@ -7848,7 +9532,8 @@ var Sphere = class _Sphere {
7848
9532
  oracle: this._oracle,
7849
9533
  emitEvent,
7850
9534
  // Pass chain code for L1 HD derivation
7851
- chainCode: this._masterKey?.chainCode
9535
+ chainCode: this._masterKey?.chainCode,
9536
+ price: this._priceProvider ?? void 0
7852
9537
  });
7853
9538
  this._communications.initialize({
7854
9539
  identity: this._identity,
@@ -7930,6 +9615,65 @@ function formatAmount(amount, options = {}) {
7930
9615
  return symbol ? `${readable} ${symbol}` : readable;
7931
9616
  }
7932
9617
 
9618
+ // types/payment-session.ts
9619
+ function createPaymentSession(params) {
9620
+ const now = Date.now();
9621
+ const deadlineMs = params.deadlineMs ?? 3e5;
9622
+ return {
9623
+ id: crypto.randomUUID(),
9624
+ direction: params.direction,
9625
+ status: "INITIATED",
9626
+ createdAt: now,
9627
+ updatedAt: now,
9628
+ deadline: now + deadlineMs,
9629
+ error: null,
9630
+ sourceTokenId: params.sourceTokenId,
9631
+ recipientNametag: params.recipientNametag,
9632
+ recipientPubkey: params.recipientPubkey,
9633
+ amount: params.amount,
9634
+ coinId: params.coinId,
9635
+ salt: params.salt
9636
+ };
9637
+ }
9638
+ function createSplitPaymentSession(params) {
9639
+ const now = Date.now();
9640
+ return {
9641
+ id: crypto.randomUUID(),
9642
+ direction: "SEND",
9643
+ sourceTokenId: params.sourceTokenId,
9644
+ paymentAmount: params.paymentAmount,
9645
+ changeAmount: params.changeAmount,
9646
+ recipientNametag: params.recipientNametag,
9647
+ recipientPubkey: params.recipientPubkey,
9648
+ splitGroupId: params.splitGroupId,
9649
+ phases: {
9650
+ burn: "PENDING",
9651
+ mints: "PENDING",
9652
+ transfer: "PENDING"
9653
+ },
9654
+ timing: {},
9655
+ createdAt: now,
9656
+ updatedAt: now,
9657
+ error: null
9658
+ };
9659
+ }
9660
+ function isPaymentSessionTimedOut(session) {
9661
+ if (!("deadline" in session) || !session.deadline) return false;
9662
+ return Date.now() > session.deadline;
9663
+ }
9664
+ function isPaymentSessionTerminal(session) {
9665
+ return session.status === "COMPLETED" || session.status === "FAILED" || session.status === "TIMED_OUT";
9666
+ }
9667
+ function createPaymentSessionError(code, message, recoverable = false, details) {
9668
+ return {
9669
+ code,
9670
+ message,
9671
+ timestamp: Date.now(),
9672
+ recoverable,
9673
+ details
9674
+ };
9675
+ }
9676
+
7933
9677
  // types/index.ts
7934
9678
  var SphereError = class extends Error {
7935
9679
  code;
@@ -8117,14 +9861,30 @@ var TokenValidator = class {
8117
9861
  }
8118
9862
  }
8119
9863
  /**
8120
- * Check which tokens are spent
9864
+ * Check which tokens are spent using SDK Token object to calculate state hash.
9865
+ *
9866
+ * Follows the same approach as the Sphere webgui TokenValidationService:
9867
+ * 1. Parse TXF using SDK's Token.fromJSON()
9868
+ * 2. Calculate CURRENT state hash via sdkToken.state.calculateHash()
9869
+ * 3. Create RequestId via RequestId.create(walletPubKey, calculatedHash)
9870
+ *
9871
+ * Uses wallet's own pubkey (not source state predicate key) because "spent" means
9872
+ * the CURRENT OWNER committed this state. Using the source state key would falsely
9873
+ * detect received tokens as "spent" (sender's commitment matches source state).
8121
9874
  */
8122
9875
  async checkSpentTokens(tokens, publicKey, options) {
8123
9876
  const spentTokens = [];
8124
9877
  const errors = [];
9878
+ if (!this.aggregatorClient) {
9879
+ errors.push("Aggregator client not available");
9880
+ return { spentTokens, errors };
9881
+ }
8125
9882
  const batchSize = options?.batchSize ?? 3;
8126
9883
  const total = tokens.length;
8127
9884
  let completed = 0;
9885
+ const { Token: SdkToken3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
9886
+ const { RequestId } = await import("@unicitylabs/state-transition-sdk/lib/api/RequestId");
9887
+ const pubKeyBytes = Buffer.from(publicKey, "hex");
8128
9888
  for (let i = 0; i < tokens.length; i += batchSize) {
8129
9889
  const batch = tokens.slice(i, i + batchSize);
8130
9890
  const batchResults = await Promise.allSettled(
@@ -8134,13 +9894,39 @@ var TokenValidator = class {
8134
9894
  if (!txf) {
8135
9895
  return { tokenId: token.id, localId: token.id, stateHash: "", spent: false, error: "Invalid TXF" };
8136
9896
  }
8137
- const tokenId = txf.genesis.data.tokenId;
8138
- const stateHash = getCurrentStateHash(txf);
8139
- if (!stateHash) {
8140
- return { tokenId, localId: token.id, stateHash: "", spent: false, error: "No state hash" };
9897
+ const tokenId = txf.genesis?.data?.tokenId || token.id;
9898
+ const sdkToken = await SdkToken3.fromJSON(txf);
9899
+ const calculatedStateHash = await sdkToken.state.calculateHash();
9900
+ const calculatedStateHashStr = calculatedStateHash.toJSON();
9901
+ const cacheKey = `${tokenId}:${calculatedStateHashStr}:${publicKey}`;
9902
+ const cached = this.spentStateCache.get(cacheKey);
9903
+ if (cached !== void 0) {
9904
+ if (cached.isSpent) {
9905
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: true };
9906
+ }
9907
+ if (Date.now() - cached.timestamp < this.UNSPENT_CACHE_TTL_MS) {
9908
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: false };
9909
+ }
9910
+ }
9911
+ const { DataHash } = await import("@unicitylabs/state-transition-sdk/lib/hash/DataHash");
9912
+ const stateHashObj = DataHash.fromJSON(calculatedStateHashStr);
9913
+ const requestId2 = await RequestId.create(pubKeyBytes, stateHashObj);
9914
+ const response = await this.aggregatorClient.getInclusionProof(requestId2);
9915
+ let isSpent = false;
9916
+ if (response.inclusionProof) {
9917
+ const proof = response.inclusionProof;
9918
+ const pathResult = await proof.merkleTreePath.verify(
9919
+ requestId2.toBitString().toBigInt()
9920
+ );
9921
+ if (pathResult.isPathValid && pathResult.isPathIncluded && proof.authenticator !== null) {
9922
+ isSpent = true;
9923
+ }
8141
9924
  }
8142
- const spent = await this.isTokenStateSpent(tokenId, stateHash, publicKey);
8143
- return { tokenId, localId: token.id, stateHash, spent };
9925
+ this.spentStateCache.set(cacheKey, {
9926
+ isSpent,
9927
+ timestamp: Date.now()
9928
+ });
9929
+ return { tokenId, localId: token.id, stateHash: calculatedStateHashStr, spent: isSpent };
8144
9930
  } catch (err) {
8145
9931
  return {
8146
9932
  tokenId: token.id,
@@ -8199,8 +9985,8 @@ var TokenValidator = class {
8199
9985
  }
8200
9986
  async verifyWithSdk(txfToken) {
8201
9987
  try {
8202
- const { Token: Token3 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
8203
- const sdkToken = await Token3.fromJSON(txfToken);
9988
+ const { Token: Token5 } = await import("@unicitylabs/state-transition-sdk/lib/token/Token");
9989
+ const sdkToken = await Token5.fromJSON(txfToken);
8204
9990
  if (!this.trustBase) {
8205
9991
  return { success: true };
8206
9992
  }
@@ -8223,9 +10009,117 @@ var TokenValidator = class {
8223
10009
  function createTokenValidator(options) {
8224
10010
  return new TokenValidator(options);
8225
10011
  }
10012
+
10013
+ // price/CoinGeckoPriceProvider.ts
10014
+ var CoinGeckoPriceProvider = class {
10015
+ platform = "coingecko";
10016
+ cache = /* @__PURE__ */ new Map();
10017
+ apiKey;
10018
+ cacheTtlMs;
10019
+ timeout;
10020
+ debug;
10021
+ baseUrl;
10022
+ constructor(config) {
10023
+ this.apiKey = config?.apiKey;
10024
+ this.cacheTtlMs = config?.cacheTtlMs ?? 6e4;
10025
+ this.timeout = config?.timeout ?? 1e4;
10026
+ this.debug = config?.debug ?? false;
10027
+ this.baseUrl = config?.baseUrl ?? (this.apiKey ? "https://pro-api.coingecko.com/api/v3" : "https://api.coingecko.com/api/v3");
10028
+ }
10029
+ async getPrices(tokenNames) {
10030
+ if (tokenNames.length === 0) {
10031
+ return /* @__PURE__ */ new Map();
10032
+ }
10033
+ const now = Date.now();
10034
+ const result = /* @__PURE__ */ new Map();
10035
+ const uncachedNames = [];
10036
+ for (const name of tokenNames) {
10037
+ const cached = this.cache.get(name);
10038
+ if (cached && cached.expiresAt > now) {
10039
+ if (cached.price !== null) {
10040
+ result.set(name, cached.price);
10041
+ }
10042
+ } else {
10043
+ uncachedNames.push(name);
10044
+ }
10045
+ }
10046
+ if (uncachedNames.length === 0) {
10047
+ return result;
10048
+ }
10049
+ try {
10050
+ const ids = uncachedNames.join(",");
10051
+ const url = `${this.baseUrl}/simple/price?ids=${encodeURIComponent(ids)}&vs_currencies=usd,eur&include_24hr_change=true`;
10052
+ const headers = { Accept: "application/json" };
10053
+ if (this.apiKey) {
10054
+ headers["x-cg-pro-api-key"] = this.apiKey;
10055
+ }
10056
+ if (this.debug) {
10057
+ console.log(`[CoinGecko] Fetching prices for: ${uncachedNames.join(", ")}`);
10058
+ }
10059
+ const response = await fetch(url, {
10060
+ headers,
10061
+ signal: AbortSignal.timeout(this.timeout)
10062
+ });
10063
+ if (!response.ok) {
10064
+ throw new Error(`CoinGecko API error: ${response.status} ${response.statusText}`);
10065
+ }
10066
+ const data = await response.json();
10067
+ for (const [name, values] of Object.entries(data)) {
10068
+ if (values && typeof values === "object") {
10069
+ const price = {
10070
+ tokenName: name,
10071
+ priceUsd: values.usd ?? 0,
10072
+ priceEur: values.eur,
10073
+ change24h: values.usd_24h_change,
10074
+ timestamp: now
10075
+ };
10076
+ this.cache.set(name, { price, expiresAt: now + this.cacheTtlMs });
10077
+ result.set(name, price);
10078
+ }
10079
+ }
10080
+ for (const name of uncachedNames) {
10081
+ if (!result.has(name)) {
10082
+ this.cache.set(name, { price: null, expiresAt: now + this.cacheTtlMs });
10083
+ }
10084
+ }
10085
+ if (this.debug) {
10086
+ console.log(`[CoinGecko] Fetched ${result.size} prices`);
10087
+ }
10088
+ } catch (error) {
10089
+ if (this.debug) {
10090
+ console.warn("[CoinGecko] Fetch failed, using stale cache:", error);
10091
+ }
10092
+ for (const name of uncachedNames) {
10093
+ const stale = this.cache.get(name);
10094
+ if (stale?.price) {
10095
+ result.set(name, stale.price);
10096
+ }
10097
+ }
10098
+ }
10099
+ return result;
10100
+ }
10101
+ async getPrice(tokenName) {
10102
+ const prices = await this.getPrices([tokenName]);
10103
+ return prices.get(tokenName) ?? null;
10104
+ }
10105
+ clearCache() {
10106
+ this.cache.clear();
10107
+ }
10108
+ };
10109
+
10110
+ // price/index.ts
10111
+ function createPriceProvider(config) {
10112
+ switch (config.platform) {
10113
+ case "coingecko":
10114
+ return new CoinGeckoPriceProvider(config);
10115
+ default:
10116
+ throw new Error(`Unsupported price platform: ${String(config.platform)}`);
10117
+ }
10118
+ }
8226
10119
  // Annotate the CommonJS export names for ESM import in node:
8227
10120
  0 && (module.exports = {
8228
10121
  COIN_TYPES,
10122
+ CoinGeckoPriceProvider,
8229
10123
  CommunicationsModule,
8230
10124
  DEFAULT_AGGREGATOR_TIMEOUT,
8231
10125
  DEFAULT_AGGREGATOR_URL,
@@ -8261,8 +10155,12 @@ function createTokenValidator(options) {
8261
10155
  createCommunicationsModule,
8262
10156
  createKeyPair,
8263
10157
  createL1PaymentsModule,
10158
+ createPaymentSession,
10159
+ createPaymentSessionError,
8264
10160
  createPaymentsModule,
10161
+ createPriceProvider,
8265
10162
  createSphere,
10163
+ createSplitPaymentSession,
8266
10164
  createTokenValidator,
8267
10165
  decodeBech32,
8268
10166
  decryptCMasterKey,
@@ -8300,7 +10198,12 @@ function createTokenValidator(options) {
8300
10198
  initSphere,
8301
10199
  isArchivedKey,
8302
10200
  isForkedKey,
10201
+ isInstantSplitBundle,
10202
+ isInstantSplitBundleV4,
10203
+ isInstantSplitBundleV5,
8303
10204
  isKnownToken,
10205
+ isPaymentSessionTerminal,
10206
+ isPaymentSessionTimedOut,
8304
10207
  isSQLiteDatabase,
8305
10208
  isTextWalletEncrypted,
8306
10209
  isTokenKey,