@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.
@@ -1442,7 +1442,6 @@ var L1PaymentsModule = class {
1442
1442
  _initialized = false;
1443
1443
  _config;
1444
1444
  _identity;
1445
- _chainCode;
1446
1445
  _addresses = [];
1447
1446
  _wallet;
1448
1447
  _transport;
@@ -1456,7 +1455,6 @@ var L1PaymentsModule = class {
1456
1455
  }
1457
1456
  async initialize(deps) {
1458
1457
  this._identity = deps.identity;
1459
- this._chainCode = deps.chainCode;
1460
1458
  this._addresses = deps.addresses ?? [];
1461
1459
  this._transport = deps.transport;
1462
1460
  this._wallet = {
@@ -1492,7 +1490,6 @@ var L1PaymentsModule = class {
1492
1490
  }
1493
1491
  this._initialized = false;
1494
1492
  this._identity = void 0;
1495
- this._chainCode = void 0;
1496
1493
  this._addresses = [];
1497
1494
  this._wallet = void 0;
1498
1495
  }
@@ -1527,10 +1524,10 @@ var L1PaymentsModule = class {
1527
1524
  * Resolve nametag to L1 address using transport provider
1528
1525
  */
1529
1526
  async resolveNametagToL1Address(nametag) {
1530
- if (!this._transport?.resolveNametagInfo) {
1531
- throw new Error("Transport provider does not support nametag resolution");
1527
+ if (!this._transport?.resolve) {
1528
+ throw new Error("Transport provider does not support resolution");
1532
1529
  }
1533
- const info = await this._transport.resolveNametagInfo(nametag);
1530
+ const info = await this._transport.resolve(nametag);
1534
1531
  if (!info) {
1535
1532
  throw new Error(`Nametag not found: ${nametag}`);
1536
1533
  }
@@ -1804,25 +1801,11 @@ var TokenSplitCalculator = class {
1804
1801
  * 3. If no exact match, determine which token to split
1805
1802
  */
1806
1803
  async calculateOptimalSplit(availableTokens, targetAmount, targetCoinIdHex) {
1807
- console.log(
1808
- `[SplitCalculator] Calculating split for ${targetAmount} of ${targetCoinIdHex}`
1809
- );
1810
- console.log(`[SplitCalculator] Available tokens: ${availableTokens.length}`);
1811
1804
  const candidates = [];
1812
1805
  for (const t of availableTokens) {
1813
- console.log(`[SplitCalculator] Token ${t.id}: coinId=${t.coinId}, status=${t.status}, hasSdkData=${!!t.sdkData}`);
1814
- if (t.coinId !== targetCoinIdHex) {
1815
- console.log(`[SplitCalculator] Skipping token ${t.id}: coinId mismatch (${t.coinId} !== ${targetCoinIdHex})`);
1816
- continue;
1817
- }
1818
- if (t.status !== "confirmed") {
1819
- console.log(`[SplitCalculator] Skipping token ${t.id}: status is ${t.status}`);
1820
- continue;
1821
- }
1822
- if (!t.sdkData) {
1823
- console.log(`[SplitCalculator] Skipping token ${t.id}: no sdkData`);
1824
- continue;
1825
- }
1806
+ if (t.coinId !== targetCoinIdHex) continue;
1807
+ if (t.status !== "confirmed") continue;
1808
+ if (!t.sdkData) continue;
1826
1809
  try {
1827
1810
  const parsed = JSON.parse(t.sdkData);
1828
1811
  const sdkToken = await import_Token.Token.fromJSON(parsed);
@@ -1850,14 +1833,12 @@ var TokenSplitCalculator = class {
1850
1833
  }
1851
1834
  const exactMatch = candidates.find((t) => t.amount === targetAmount);
1852
1835
  if (exactMatch) {
1853
- console.log("[SplitCalculator] Found exact match token");
1854
1836
  return this.createDirectPlan([exactMatch], targetAmount, targetCoinIdHex);
1855
1837
  }
1856
1838
  const maxCombinationSize = Math.min(5, candidates.length);
1857
1839
  for (let size = 2; size <= maxCombinationSize; size++) {
1858
1840
  const combo = this.findCombinationOfSize(candidates, targetAmount, size);
1859
1841
  if (combo) {
1860
- console.log(`[SplitCalculator] Found exact combination of ${size} tokens`);
1861
1842
  return this.createDirectPlan(combo, targetAmount, targetCoinIdHex);
1862
1843
  }
1863
1844
  }
@@ -1874,9 +1855,6 @@ var TokenSplitCalculator = class {
1874
1855
  } else {
1875
1856
  const neededFromThisToken = targetAmount - currentSum;
1876
1857
  const remainderForSender = candidate.amount - neededFromThisToken;
1877
- console.log(
1878
- `[SplitCalculator] Split required. Sending: ${neededFromThisToken}, Remainder: ${remainderForSender}`
1879
- );
1880
1858
  return {
1881
1859
  tokensToTransferDirectly: toTransfer,
1882
1860
  tokenToSplit: candidate,
@@ -1895,16 +1873,10 @@ var TokenSplitCalculator = class {
1895
1873
  */
1896
1874
  getTokenBalance(sdkToken, coinIdHex) {
1897
1875
  try {
1898
- if (!sdkToken.coins) {
1899
- console.log("[SplitCalculator] Token has no coins");
1900
- return 0n;
1901
- }
1876
+ if (!sdkToken.coins) return 0n;
1902
1877
  const coinId = import_CoinId.CoinId.fromJSON(coinIdHex);
1903
- const balance = sdkToken.coins.get(coinId);
1904
- console.log(`[SplitCalculator] Token balance for ${coinIdHex.slice(0, 8)}...: ${balance ?? 0n}`);
1905
- return balance ?? 0n;
1906
- } catch (e) {
1907
- console.error("[SplitCalculator] Error getting token balance:", e);
1878
+ return sdkToken.coins.get(coinId) ?? 0n;
1879
+ } catch {
1908
1880
  return 0n;
1909
1881
  }
1910
1882
  }
@@ -2133,20 +2105,18 @@ var NametagMinter = class {
2133
2105
  const cleanNametag = nametag.replace("@", "").trim();
2134
2106
  this.log(`Starting mint for nametag: ${cleanNametag}`);
2135
2107
  try {
2136
- const isAvailable = await this.isNametagAvailable(cleanNametag);
2137
- if (!isAvailable) {
2138
- return {
2139
- success: false,
2140
- error: `Nametag "${cleanNametag}" is already taken`
2141
- };
2142
- }
2143
2108
  const nametagTokenId = await import_TokenId2.TokenId.fromNameTag(cleanNametag);
2144
2109
  const nametagTokenType = new import_TokenType.TokenType(
2145
2110
  Buffer.from(UNICITY_TOKEN_TYPE_HEX, "hex")
2146
2111
  );
2147
- const salt = new Uint8Array(32);
2148
- crypto.getRandomValues(salt);
2149
- this.log("Generated salt");
2112
+ const nametagBytes = new TextEncoder().encode(cleanNametag);
2113
+ const pubKey = this.signingService.publicKey;
2114
+ const saltInput = new Uint8Array(pubKey.length + nametagBytes.length);
2115
+ saltInput.set(pubKey, 0);
2116
+ saltInput.set(nametagBytes, pubKey.length);
2117
+ const saltBuffer = await crypto.subtle.digest("SHA-256", saltInput);
2118
+ const salt = new Uint8Array(saltBuffer);
2119
+ this.log("Generated deterministic salt");
2150
2120
  const mintData = await import_MintTransactionData.MintTransactionData.createFromNametag(
2151
2121
  cleanNametag,
2152
2122
  nametagTokenType,
@@ -2268,8 +2238,10 @@ var STORAGE_KEYS_GLOBAL = {
2268
2238
  WALLET_EXISTS: "wallet_exists",
2269
2239
  /** Current active address index */
2270
2240
  CURRENT_ADDRESS_INDEX: "current_address_index",
2271
- /** Index of address nametags (JSON: { "0": "alice", "1": "bob" }) - for discovery */
2272
- ADDRESS_NAMETAGS: "address_nametags"
2241
+ /** Nametag cache per address (separate from tracked addresses registry) */
2242
+ ADDRESS_NAMETAGS: "address_nametags",
2243
+ /** Active addresses registry (JSON: TrackedAddressesStorage) */
2244
+ TRACKED_ADDRESSES: "tracked_addresses"
2273
2245
  };
2274
2246
  var STORAGE_KEYS_ADDRESS = {
2275
2247
  /** Pending transfers for this address */
@@ -2612,11 +2584,16 @@ function getCurrentStateHash(txf) {
2612
2584
  if (lastTx?.newStateHash) {
2613
2585
  return lastTx.newStateHash;
2614
2586
  }
2615
- return void 0;
2587
+ if (lastTx?.inclusionProof?.authenticator?.stateHash) {
2588
+ return lastTx.inclusionProof.authenticator.stateHash;
2589
+ }
2616
2590
  }
2617
2591
  if (txf._integrity?.currentStateHash) {
2618
2592
  return txf._integrity.currentStateHash;
2619
2593
  }
2594
+ if (txf.genesis?.inclusionProof?.authenticator?.stateHash) {
2595
+ return txf.genesis.inclusionProof.authenticator.stateHash;
2596
+ }
2620
2597
  return void 0;
2621
2598
  }
2622
2599
 
@@ -2914,16 +2891,733 @@ var TokenRegistry = class _TokenRegistry {
2914
2891
  }
2915
2892
  };
2916
2893
 
2917
- // modules/payments/PaymentsModule.ts
2894
+ // modules/payments/InstantSplitExecutor.ts
2918
2895
  var import_Token4 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
2896
+ var import_TokenId3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
2897
+ var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
2919
2898
  var import_CoinId3 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
2899
+ var import_TokenCoinData2 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/TokenCoinData");
2900
+ var import_TokenSplitBuilder2 = require("@unicitylabs/state-transition-sdk/lib/transaction/split/TokenSplitBuilder");
2901
+ var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
2902
+ var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
2903
+ var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
2920
2904
  var import_TransferCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
2905
+ var import_InclusionProofUtils3 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
2906
+ async function sha2563(input) {
2907
+ const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
2908
+ const buffer = new ArrayBuffer(data.length);
2909
+ new Uint8Array(buffer).set(data);
2910
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
2911
+ return new Uint8Array(hashBuffer);
2912
+ }
2913
+ function toHex2(bytes) {
2914
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2915
+ }
2916
+ function fromHex2(hex) {
2917
+ const bytes = new Uint8Array(hex.length / 2);
2918
+ for (let i = 0; i < hex.length; i += 2) {
2919
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
2920
+ }
2921
+ return bytes;
2922
+ }
2923
+ var InstantSplitExecutor = class {
2924
+ client;
2925
+ trustBase;
2926
+ signingService;
2927
+ devMode;
2928
+ constructor(config) {
2929
+ this.client = config.stateTransitionClient;
2930
+ this.trustBase = config.trustBase;
2931
+ this.signingService = config.signingService;
2932
+ this.devMode = config.devMode ?? false;
2933
+ }
2934
+ /**
2935
+ * Execute an instant split transfer with V5 optimized flow.
2936
+ *
2937
+ * Critical path (~2.3s):
2938
+ * 1. Create and submit burn commitment
2939
+ * 2. Wait for burn proof
2940
+ * 3. Create mint commitments with SplitMintReason
2941
+ * 4. Create transfer commitment (no mint proof needed)
2942
+ * 5. Send bundle via transport
2943
+ *
2944
+ * @param tokenToSplit - The SDK token to split
2945
+ * @param splitAmount - Amount to send to recipient
2946
+ * @param remainderAmount - Amount to keep as change
2947
+ * @param coinIdHex - Coin ID in hex format
2948
+ * @param recipientAddress - Recipient's address (PROXY or DIRECT)
2949
+ * @param transport - Transport provider for sending the bundle
2950
+ * @param recipientPubkey - Recipient's transport public key
2951
+ * @param options - Optional configuration
2952
+ * @returns InstantSplitResult with success status and timing info
2953
+ */
2954
+ async executeSplitInstant(tokenToSplit, splitAmount, remainderAmount, coinIdHex, recipientAddress, transport, recipientPubkey, options) {
2955
+ const startTime = performance.now();
2956
+ const splitGroupId = crypto.randomUUID();
2957
+ const tokenIdHex = toHex2(tokenToSplit.id.bytes);
2958
+ console.log(`[InstantSplit] Starting V5 split for token ${tokenIdHex.slice(0, 8)}...`);
2959
+ try {
2960
+ const coinId = new import_CoinId3.CoinId(fromHex2(coinIdHex));
2961
+ const seedString = `${tokenIdHex}_${splitAmount.toString()}_${remainderAmount.toString()}_${Date.now()}`;
2962
+ const recipientTokenId = new import_TokenId3.TokenId(await sha2563(seedString));
2963
+ const senderTokenId = new import_TokenId3.TokenId(await sha2563(seedString + "_sender"));
2964
+ const recipientSalt = await sha2563(seedString + "_recipient_salt");
2965
+ const senderSalt = await sha2563(seedString + "_sender_salt");
2966
+ const senderAddressRef = await import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
2967
+ tokenToSplit.type,
2968
+ this.signingService.algorithm,
2969
+ this.signingService.publicKey,
2970
+ import_HashAlgorithm3.HashAlgorithm.SHA256
2971
+ );
2972
+ const senderAddress = await senderAddressRef.toAddress();
2973
+ const builder = new import_TokenSplitBuilder2.TokenSplitBuilder();
2974
+ const coinDataA = import_TokenCoinData2.TokenCoinData.create([[coinId, splitAmount]]);
2975
+ builder.createToken(
2976
+ recipientTokenId,
2977
+ tokenToSplit.type,
2978
+ new Uint8Array(0),
2979
+ coinDataA,
2980
+ senderAddress,
2981
+ // Mint to sender first, then transfer
2982
+ recipientSalt,
2983
+ null
2984
+ );
2985
+ const coinDataB = import_TokenCoinData2.TokenCoinData.create([[coinId, remainderAmount]]);
2986
+ builder.createToken(
2987
+ senderTokenId,
2988
+ tokenToSplit.type,
2989
+ new Uint8Array(0),
2990
+ coinDataB,
2991
+ senderAddress,
2992
+ senderSalt,
2993
+ null
2994
+ );
2995
+ const split = await builder.build(tokenToSplit);
2996
+ console.log("[InstantSplit] Step 1: Creating and submitting burn...");
2997
+ const burnSalt = await sha2563(seedString + "_burn_salt");
2998
+ const burnCommitment = await split.createBurnCommitment(burnSalt, this.signingService);
2999
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3000
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3001
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3002
+ }
3003
+ console.log("[InstantSplit] Step 2: Waiting for burn proof...");
3004
+ const burnProof = this.devMode ? await this.waitInclusionProofWithDevBypass(burnCommitment, options?.burnProofTimeoutMs) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, burnCommitment);
3005
+ const burnTransaction = burnCommitment.toTransaction(burnProof);
3006
+ const burnDuration = performance.now() - startTime;
3007
+ console.log(`[InstantSplit] Burn proof received in ${burnDuration.toFixed(0)}ms`);
3008
+ options?.onBurnCompleted?.(JSON.stringify(burnTransaction.toJSON()));
3009
+ console.log("[InstantSplit] Step 3: Creating mint commitments...");
3010
+ const mintCommitments = await split.createSplitMintCommitments(this.trustBase, burnTransaction);
3011
+ const recipientIdHex = toHex2(recipientTokenId.bytes);
3012
+ const senderIdHex = toHex2(senderTokenId.bytes);
3013
+ const recipientMintCommitment = mintCommitments.find(
3014
+ (c) => toHex2(c.transactionData.tokenId.bytes) === recipientIdHex
3015
+ );
3016
+ const senderMintCommitment = mintCommitments.find(
3017
+ (c) => toHex2(c.transactionData.tokenId.bytes) === senderIdHex
3018
+ );
3019
+ if (!recipientMintCommitment || !senderMintCommitment) {
3020
+ throw new Error("Failed to find expected mint commitments");
3021
+ }
3022
+ console.log("[InstantSplit] Step 4: Creating transfer commitment...");
3023
+ const transferSalt = await sha2563(seedString + "_transfer_salt");
3024
+ const transferCommitment = await this.createTransferCommitmentFromMintData(
3025
+ recipientMintCommitment.transactionData,
3026
+ recipientAddress,
3027
+ transferSalt,
3028
+ this.signingService
3029
+ );
3030
+ const mintedPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3031
+ recipientTokenId,
3032
+ tokenToSplit.type,
3033
+ this.signingService,
3034
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3035
+ recipientSalt
3036
+ );
3037
+ const mintedState = new import_TokenState3.TokenState(mintedPredicate, null);
3038
+ console.log("[InstantSplit] Step 5: Packaging V5 bundle...");
3039
+ const senderPubkey = toHex2(this.signingService.publicKey);
3040
+ let nametagTokenJson;
3041
+ const recipientAddressStr = recipientAddress.toString();
3042
+ if (recipientAddressStr.startsWith("PROXY://") && tokenToSplit.nametagTokens?.length > 0) {
3043
+ nametagTokenJson = JSON.stringify(tokenToSplit.nametagTokens[0].toJSON());
3044
+ }
3045
+ const bundle = {
3046
+ version: "5.0",
3047
+ type: "INSTANT_SPLIT",
3048
+ burnTransaction: JSON.stringify(burnTransaction.toJSON()),
3049
+ recipientMintData: JSON.stringify(recipientMintCommitment.transactionData.toJSON()),
3050
+ transferCommitment: JSON.stringify(transferCommitment.toJSON()),
3051
+ amount: splitAmount.toString(),
3052
+ coinId: coinIdHex,
3053
+ tokenTypeHex: toHex2(tokenToSplit.type.bytes),
3054
+ splitGroupId,
3055
+ senderPubkey,
3056
+ recipientSaltHex: toHex2(recipientSalt),
3057
+ transferSaltHex: toHex2(transferSalt),
3058
+ mintedTokenStateJson: JSON.stringify(mintedState.toJSON()),
3059
+ finalRecipientStateJson: "",
3060
+ // Recipient creates their own
3061
+ recipientAddressJson: recipientAddressStr,
3062
+ nametagTokenJson
3063
+ };
3064
+ console.log("[InstantSplit] Step 6: Sending via transport...");
3065
+ const nostrEventId = await transport.sendTokenTransfer(recipientPubkey, {
3066
+ token: JSON.stringify(bundle),
3067
+ proof: null,
3068
+ // Proof is included in the bundle
3069
+ memo: "INSTANT_SPLIT_V5",
3070
+ sender: {
3071
+ transportPubkey: senderPubkey
3072
+ }
3073
+ });
3074
+ const criticalPathDuration = performance.now() - startTime;
3075
+ console.log(`[InstantSplit] V5 complete in ${criticalPathDuration.toFixed(0)}ms`);
3076
+ options?.onNostrDelivered?.(nostrEventId);
3077
+ if (!options?.skipBackground) {
3078
+ this.submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, {
3079
+ signingService: this.signingService,
3080
+ tokenType: tokenToSplit.type,
3081
+ coinId,
3082
+ senderTokenId,
3083
+ senderSalt,
3084
+ onProgress: options?.onBackgroundProgress,
3085
+ onChangeTokenCreated: options?.onChangeTokenCreated,
3086
+ onStorageSync: options?.onStorageSync
3087
+ });
3088
+ }
3089
+ return {
3090
+ success: true,
3091
+ nostrEventId,
3092
+ splitGroupId,
3093
+ criticalPathDurationMs: criticalPathDuration,
3094
+ backgroundStarted: !options?.skipBackground
3095
+ };
3096
+ } catch (error) {
3097
+ const duration = performance.now() - startTime;
3098
+ const errorMessage = error instanceof Error ? error.message : String(error);
3099
+ console.error(`[InstantSplit] Failed after ${duration.toFixed(0)}ms:`, error);
3100
+ return {
3101
+ success: false,
3102
+ splitGroupId,
3103
+ criticalPathDurationMs: duration,
3104
+ error: errorMessage,
3105
+ backgroundStarted: false
3106
+ };
3107
+ }
3108
+ }
3109
+ /**
3110
+ * Create a TransferCommitment from MintTransactionData WITHOUT waiting for mint proof.
3111
+ *
3112
+ * Key insight: TransferCommitment.create() only needs token.state and token.nametagTokens.
3113
+ * It does NOT need the genesis transaction or mint proof.
3114
+ */
3115
+ async createTransferCommitmentFromMintData(mintData, recipientAddress, transferSalt, signingService, nametagTokens) {
3116
+ const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3117
+ mintData.tokenId,
3118
+ mintData.tokenType,
3119
+ signingService,
3120
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3121
+ mintData.salt
3122
+ );
3123
+ const state = new import_TokenState3.TokenState(predicate, null);
3124
+ const minimalToken = {
3125
+ state,
3126
+ nametagTokens: nametagTokens || [],
3127
+ id: mintData.tokenId,
3128
+ type: mintData.tokenType
3129
+ };
3130
+ const transferCommitment = await import_TransferCommitment2.TransferCommitment.create(
3131
+ minimalToken,
3132
+ recipientAddress,
3133
+ transferSalt,
3134
+ null,
3135
+ // recipientData
3136
+ null,
3137
+ // recipientDataHash
3138
+ signingService
3139
+ );
3140
+ return transferCommitment;
3141
+ }
3142
+ /**
3143
+ * V5 background submission.
3144
+ *
3145
+ * Submits mint commitments to aggregator in PARALLEL after transport delivery.
3146
+ * Then waits for sender's mint proof, reconstructs change token, and saves it.
3147
+ */
3148
+ submitBackgroundV5(senderMintCommitment, recipientMintCommitment, transferCommitment, context) {
3149
+ console.log("[InstantSplit] Background: Starting parallel mint submission...");
3150
+ const startTime = performance.now();
3151
+ const submissions = Promise.all([
3152
+ this.client.submitMintCommitment(senderMintCommitment).then((res) => ({ type: "senderMint", status: res.status })).catch((err) => ({ type: "senderMint", status: "ERROR", error: err })),
3153
+ this.client.submitMintCommitment(recipientMintCommitment).then((res) => ({ type: "recipientMint", status: res.status })).catch((err) => ({ type: "recipientMint", status: "ERROR", error: err })),
3154
+ this.client.submitTransferCommitment(transferCommitment).then((res) => ({ type: "transfer", status: res.status })).catch((err) => ({ type: "transfer", status: "ERROR", error: err }))
3155
+ ]);
3156
+ submissions.then(async (results) => {
3157
+ const submitDuration = performance.now() - startTime;
3158
+ console.log(`[InstantSplit] Background: Submissions complete in ${submitDuration.toFixed(0)}ms`);
3159
+ context.onProgress?.({
3160
+ stage: "MINTS_SUBMITTED",
3161
+ message: `All commitments submitted in ${submitDuration.toFixed(0)}ms`
3162
+ });
3163
+ const senderMintResult = results.find((r) => r.type === "senderMint");
3164
+ if (senderMintResult?.status !== "SUCCESS" && senderMintResult?.status !== "REQUEST_ID_EXISTS") {
3165
+ console.error("[InstantSplit] Background: Sender mint failed - cannot save change token");
3166
+ context.onProgress?.({
3167
+ stage: "FAILED",
3168
+ message: "Sender mint submission failed",
3169
+ error: String(senderMintResult?.error)
3170
+ });
3171
+ return;
3172
+ }
3173
+ console.log("[InstantSplit] Background: Waiting for sender mint proof...");
3174
+ const proofStartTime = performance.now();
3175
+ try {
3176
+ const senderMintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(senderMintCommitment) : await (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, senderMintCommitment);
3177
+ const proofDuration = performance.now() - proofStartTime;
3178
+ console.log(`[InstantSplit] Background: Sender mint proof received in ${proofDuration.toFixed(0)}ms`);
3179
+ context.onProgress?.({
3180
+ stage: "MINTS_PROVEN",
3181
+ message: `Mint proof received in ${proofDuration.toFixed(0)}ms`
3182
+ });
3183
+ const mintTransaction = senderMintCommitment.toTransaction(senderMintProof);
3184
+ const predicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
3185
+ context.senderTokenId,
3186
+ context.tokenType,
3187
+ context.signingService,
3188
+ import_HashAlgorithm3.HashAlgorithm.SHA256,
3189
+ context.senderSalt
3190
+ );
3191
+ const state = new import_TokenState3.TokenState(predicate, null);
3192
+ const changeToken = await import_Token4.Token.mint(this.trustBase, state, mintTransaction);
3193
+ if (!this.devMode) {
3194
+ const verification = await changeToken.verify(this.trustBase);
3195
+ if (!verification.isSuccessful) {
3196
+ throw new Error(`Change token verification failed`);
3197
+ }
3198
+ }
3199
+ console.log("[InstantSplit] Background: Change token created");
3200
+ context.onProgress?.({
3201
+ stage: "CHANGE_TOKEN_SAVED",
3202
+ message: "Change token created and verified"
3203
+ });
3204
+ if (context.onChangeTokenCreated) {
3205
+ await context.onChangeTokenCreated(changeToken);
3206
+ console.log("[InstantSplit] Background: Change token saved");
3207
+ }
3208
+ if (context.onStorageSync) {
3209
+ try {
3210
+ const syncSuccess = await context.onStorageSync();
3211
+ console.log(`[InstantSplit] Background: Storage sync ${syncSuccess ? "completed" : "deferred"}`);
3212
+ context.onProgress?.({
3213
+ stage: "STORAGE_SYNCED",
3214
+ message: syncSuccess ? "Storage synchronized" : "Sync deferred"
3215
+ });
3216
+ } catch (syncError) {
3217
+ console.warn("[InstantSplit] Background: Storage sync error:", syncError);
3218
+ }
3219
+ }
3220
+ const totalDuration = performance.now() - startTime;
3221
+ console.log(`[InstantSplit] Background: Complete in ${totalDuration.toFixed(0)}ms`);
3222
+ context.onProgress?.({
3223
+ stage: "COMPLETED",
3224
+ message: `Background processing complete in ${totalDuration.toFixed(0)}ms`
3225
+ });
3226
+ } catch (proofError) {
3227
+ console.error("[InstantSplit] Background: Failed to get sender mint proof:", proofError);
3228
+ context.onProgress?.({
3229
+ stage: "FAILED",
3230
+ message: "Failed to get mint proof",
3231
+ error: String(proofError)
3232
+ });
3233
+ }
3234
+ }).catch((err) => {
3235
+ console.error("[InstantSplit] Background: Submission batch failed:", err);
3236
+ context.onProgress?.({
3237
+ stage: "FAILED",
3238
+ message: "Background submission failed",
3239
+ error: String(err)
3240
+ });
3241
+ });
3242
+ }
3243
+ /**
3244
+ * Dev mode bypass for waitInclusionProof.
3245
+ * In dev mode, we create a mock proof for testing.
3246
+ */
3247
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
3248
+ if (this.devMode) {
3249
+ try {
3250
+ return await Promise.race([
3251
+ (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment),
3252
+ new Promise(
3253
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
3254
+ )
3255
+ ]);
3256
+ } catch {
3257
+ console.log("[InstantSplit] Dev mode: Using mock proof");
3258
+ return {
3259
+ toJSON: () => ({ mock: true })
3260
+ };
3261
+ }
3262
+ }
3263
+ return (0, import_InclusionProofUtils3.waitInclusionProof)(this.trustBase, this.client, commitment);
3264
+ }
3265
+ };
3266
+
3267
+ // modules/payments/InstantSplitProcessor.ts
3268
+ var import_Token5 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
3269
+ var import_TokenState4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
3270
+ var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
3271
+ var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
3272
+ var import_UnmaskedPredicate4 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
3273
+ var import_TransferCommitment3 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
2921
3274
  var import_TransferTransaction = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
3275
+ var import_MintCommitment2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintCommitment");
3276
+ var import_MintTransactionData2 = require("@unicitylabs/state-transition-sdk/lib/transaction/MintTransactionData");
3277
+ var import_InclusionProofUtils4 = require("@unicitylabs/state-transition-sdk/lib/util/InclusionProofUtils");
3278
+
3279
+ // types/instant-split.ts
3280
+ function isInstantSplitBundle(obj) {
3281
+ if (typeof obj !== "object" || obj === null) {
3282
+ return false;
3283
+ }
3284
+ const bundle = obj;
3285
+ if (bundle.type !== "INSTANT_SPLIT") return false;
3286
+ if (typeof bundle.recipientMintData !== "string") return false;
3287
+ if (typeof bundle.transferCommitment !== "string") return false;
3288
+ if (typeof bundle.amount !== "string") return false;
3289
+ if (typeof bundle.coinId !== "string") return false;
3290
+ if (typeof bundle.splitGroupId !== "string") return false;
3291
+ if (typeof bundle.senderPubkey !== "string") return false;
3292
+ if (typeof bundle.recipientSaltHex !== "string") return false;
3293
+ if (typeof bundle.transferSaltHex !== "string") return false;
3294
+ if (bundle.version === "4.0") {
3295
+ return typeof bundle.burnCommitment === "string";
3296
+ } else if (bundle.version === "5.0") {
3297
+ return typeof bundle.burnTransaction === "string" && typeof bundle.mintedTokenStateJson === "string" && typeof bundle.finalRecipientStateJson === "string" && typeof bundle.recipientAddressJson === "string";
3298
+ }
3299
+ return false;
3300
+ }
3301
+ function isInstantSplitBundleV4(obj) {
3302
+ return isInstantSplitBundle(obj) && obj.version === "4.0";
3303
+ }
3304
+ function isInstantSplitBundleV5(obj) {
3305
+ return isInstantSplitBundle(obj) && obj.version === "5.0";
3306
+ }
3307
+
3308
+ // modules/payments/InstantSplitProcessor.ts
3309
+ function fromHex3(hex) {
3310
+ const bytes = new Uint8Array(hex.length / 2);
3311
+ for (let i = 0; i < hex.length; i += 2) {
3312
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3313
+ }
3314
+ return bytes;
3315
+ }
3316
+ var InstantSplitProcessor = class {
3317
+ client;
3318
+ trustBase;
3319
+ devMode;
3320
+ constructor(config) {
3321
+ this.client = config.stateTransitionClient;
3322
+ this.trustBase = config.trustBase;
3323
+ this.devMode = config.devMode ?? false;
3324
+ }
3325
+ /**
3326
+ * Process a received INSTANT_SPLIT bundle.
3327
+ *
3328
+ * @param bundle - The received bundle (V4 or V5)
3329
+ * @param signingService - Recipient's signing service
3330
+ * @param senderPubkey - Sender's public key (for verification)
3331
+ * @param options - Processing options
3332
+ * @returns Processing result with finalized token if successful
3333
+ */
3334
+ async processReceivedBundle(bundle, signingService, senderPubkey, options) {
3335
+ if (isInstantSplitBundleV5(bundle)) {
3336
+ return this.processV5Bundle(bundle, signingService, senderPubkey, options);
3337
+ } else if (isInstantSplitBundleV4(bundle)) {
3338
+ return this.processV4Bundle(bundle, signingService, senderPubkey, options);
3339
+ }
3340
+ return {
3341
+ success: false,
3342
+ error: `Unknown bundle version: ${bundle.version}`,
3343
+ durationMs: 0
3344
+ };
3345
+ }
3346
+ /**
3347
+ * Process a V5 bundle (production mode).
3348
+ *
3349
+ * V5 Flow:
3350
+ * 1. Burn transaction already has proof (just validate)
3351
+ * 2. Submit mint commitment -> wait for proof
3352
+ * 3. Reconstruct minted token (use sender's state from bundle)
3353
+ * 4. Submit transfer commitment -> wait for proof
3354
+ * 5. Create recipient's final state and finalize token
3355
+ */
3356
+ async processV5Bundle(bundle, signingService, senderPubkey, options) {
3357
+ console.log("[InstantSplitProcessor] Processing V5 bundle...");
3358
+ const startTime = performance.now();
3359
+ try {
3360
+ if (bundle.senderPubkey !== senderPubkey) {
3361
+ console.warn("[InstantSplitProcessor] Sender pubkey mismatch (non-fatal)");
3362
+ }
3363
+ const burnTxJson = JSON.parse(bundle.burnTransaction);
3364
+ const burnTransaction = await import_TransferTransaction.TransferTransaction.fromJSON(burnTxJson);
3365
+ console.log("[InstantSplitProcessor] Burn transaction validated");
3366
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3367
+ const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
3368
+ const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
3369
+ console.log("[InstantSplitProcessor] Mint commitment recreated");
3370
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3371
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3372
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3373
+ }
3374
+ console.log(`[InstantSplitProcessor] Mint submitted: ${mintResponse.status}`);
3375
+ const mintProof = this.devMode ? await this.waitInclusionProofWithDevBypass(mintCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, mintCommitment);
3376
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3377
+ console.log("[InstantSplitProcessor] Mint proof received");
3378
+ const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
3379
+ const senderMintedStateJson = JSON.parse(bundle.mintedTokenStateJson);
3380
+ const tokenJson = {
3381
+ version: "2.0",
3382
+ state: senderMintedStateJson,
3383
+ genesis: mintTransaction.toJSON(),
3384
+ transactions: [],
3385
+ nametags: []
3386
+ };
3387
+ const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
3388
+ console.log("[InstantSplitProcessor] Minted token reconstructed from sender state");
3389
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3390
+ const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
3391
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3392
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3393
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3394
+ }
3395
+ console.log(`[InstantSplitProcessor] Transfer submitted: ${transferResponse.status}`);
3396
+ const transferProof = this.devMode ? await this.waitInclusionProofWithDevBypass(transferCommitment, options?.proofTimeoutMs) : await (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, transferCommitment);
3397
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3398
+ console.log("[InstantSplitProcessor] Transfer proof received");
3399
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3400
+ const finalRecipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3401
+ mintData.tokenId,
3402
+ tokenType,
3403
+ signingService,
3404
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3405
+ transferSalt
3406
+ );
3407
+ const finalRecipientState = new import_TokenState4.TokenState(finalRecipientPredicate, null);
3408
+ console.log("[InstantSplitProcessor] Final recipient state created");
3409
+ let nametagTokens = [];
3410
+ const recipientAddressStr = bundle.recipientAddressJson;
3411
+ if (recipientAddressStr.startsWith("PROXY://")) {
3412
+ console.log("[InstantSplitProcessor] PROXY address detected, finding nametag token...");
3413
+ if (bundle.nametagTokenJson) {
3414
+ try {
3415
+ const nametagToken = await import_Token5.Token.fromJSON(JSON.parse(bundle.nametagTokenJson));
3416
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
3417
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
3418
+ if (proxy.address !== recipientAddressStr) {
3419
+ console.warn("[InstantSplitProcessor] Nametag PROXY address mismatch, ignoring bundle token");
3420
+ } else {
3421
+ nametagTokens = [nametagToken];
3422
+ console.log("[InstantSplitProcessor] Using nametag token from bundle (address validated)");
3423
+ }
3424
+ } catch (err) {
3425
+ console.warn("[InstantSplitProcessor] Failed to parse nametag token from bundle:", err);
3426
+ }
3427
+ }
3428
+ if (nametagTokens.length === 0 && options?.findNametagToken) {
3429
+ const token = await options.findNametagToken(recipientAddressStr);
3430
+ if (token) {
3431
+ nametagTokens = [token];
3432
+ console.log("[InstantSplitProcessor] Found nametag token via callback");
3433
+ }
3434
+ }
3435
+ if (nametagTokens.length === 0 && !this.devMode) {
3436
+ throw new Error(
3437
+ `PROXY address transfer requires nametag token for verification. Address: ${recipientAddressStr}`
3438
+ );
3439
+ }
3440
+ }
3441
+ let finalToken;
3442
+ if (this.devMode) {
3443
+ console.log("[InstantSplitProcessor] Dev mode: finalizing without verification");
3444
+ const tokenJson2 = mintedToken.toJSON();
3445
+ tokenJson2.state = finalRecipientState.toJSON();
3446
+ tokenJson2.transactions = [transferTransaction.toJSON()];
3447
+ finalToken = await import_Token5.Token.fromJSON(tokenJson2);
3448
+ } else {
3449
+ finalToken = await this.client.finalizeTransaction(
3450
+ this.trustBase,
3451
+ mintedToken,
3452
+ finalRecipientState,
3453
+ transferTransaction,
3454
+ nametagTokens
3455
+ );
3456
+ }
3457
+ console.log("[InstantSplitProcessor] Token finalized");
3458
+ if (!this.devMode) {
3459
+ const verification = await finalToken.verify(this.trustBase);
3460
+ if (!verification.isSuccessful) {
3461
+ throw new Error(`Token verification failed`);
3462
+ }
3463
+ console.log("[InstantSplitProcessor] Token verified");
3464
+ }
3465
+ const duration = performance.now() - startTime;
3466
+ console.log(`[InstantSplitProcessor] V5 bundle processed in ${duration.toFixed(0)}ms`);
3467
+ return {
3468
+ success: true,
3469
+ token: finalToken,
3470
+ durationMs: duration
3471
+ };
3472
+ } catch (error) {
3473
+ const duration = performance.now() - startTime;
3474
+ const errorMessage = error instanceof Error ? error.message : String(error);
3475
+ console.error(`[InstantSplitProcessor] V5 processing failed:`, error);
3476
+ return {
3477
+ success: false,
3478
+ error: errorMessage,
3479
+ durationMs: duration
3480
+ };
3481
+ }
3482
+ }
3483
+ /**
3484
+ * Process a V4 bundle (dev mode only).
3485
+ *
3486
+ * V4 Flow:
3487
+ * 1. Submit burn commitment -> wait for proof
3488
+ * 2. Submit mint commitment -> wait for proof
3489
+ * 3. Reconstruct minted token
3490
+ * 4. Submit transfer commitment -> wait for proof
3491
+ * 5. Finalize token
3492
+ */
3493
+ async processV4Bundle(bundle, signingService, _senderPubkey, options) {
3494
+ if (!this.devMode) {
3495
+ return {
3496
+ success: false,
3497
+ error: "INSTANT_SPLIT V4 is only supported in dev mode",
3498
+ durationMs: 0
3499
+ };
3500
+ }
3501
+ console.log("[InstantSplitProcessor] Processing V4 bundle (dev mode)...");
3502
+ const startTime = performance.now();
3503
+ try {
3504
+ const burnCommitmentJson = JSON.parse(bundle.burnCommitment);
3505
+ const burnCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(burnCommitmentJson);
3506
+ const burnResponse = await this.client.submitTransferCommitment(burnCommitment);
3507
+ if (burnResponse.status !== "SUCCESS" && burnResponse.status !== "REQUEST_ID_EXISTS") {
3508
+ throw new Error(`Burn submission failed: ${burnResponse.status}`);
3509
+ }
3510
+ await this.waitInclusionProofWithDevBypass(burnCommitment, options?.proofTimeoutMs);
3511
+ console.log("[InstantSplitProcessor] V4: Burn proof received");
3512
+ const mintDataJson = JSON.parse(bundle.recipientMintData);
3513
+ const mintData = await import_MintTransactionData2.MintTransactionData.fromJSON(mintDataJson);
3514
+ const mintCommitment = await import_MintCommitment2.MintCommitment.create(mintData);
3515
+ const mintResponse = await this.client.submitMintCommitment(mintCommitment);
3516
+ if (mintResponse.status !== "SUCCESS" && mintResponse.status !== "REQUEST_ID_EXISTS") {
3517
+ throw new Error(`Mint submission failed: ${mintResponse.status}`);
3518
+ }
3519
+ const mintProof = await this.waitInclusionProofWithDevBypass(
3520
+ mintCommitment,
3521
+ options?.proofTimeoutMs
3522
+ );
3523
+ const mintTransaction = mintCommitment.toTransaction(mintProof);
3524
+ console.log("[InstantSplitProcessor] V4: Mint proof received");
3525
+ const tokenType = new import_TokenType2.TokenType(fromHex3(bundle.tokenTypeHex));
3526
+ const recipientSalt = fromHex3(bundle.recipientSaltHex);
3527
+ const recipientPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3528
+ mintData.tokenId,
3529
+ tokenType,
3530
+ signingService,
3531
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3532
+ recipientSalt
3533
+ );
3534
+ const recipientState = new import_TokenState4.TokenState(recipientPredicate, null);
3535
+ const tokenJson = {
3536
+ version: "2.0",
3537
+ state: recipientState.toJSON(),
3538
+ genesis: mintTransaction.toJSON(),
3539
+ transactions: [],
3540
+ nametags: []
3541
+ };
3542
+ const mintedToken = await import_Token5.Token.fromJSON(tokenJson);
3543
+ console.log("[InstantSplitProcessor] V4: Minted token reconstructed");
3544
+ const transferCommitmentJson = JSON.parse(bundle.transferCommitment);
3545
+ const transferCommitment = await import_TransferCommitment3.TransferCommitment.fromJSON(transferCommitmentJson);
3546
+ const transferResponse = await this.client.submitTransferCommitment(transferCommitment);
3547
+ if (transferResponse.status !== "SUCCESS" && transferResponse.status !== "REQUEST_ID_EXISTS") {
3548
+ throw new Error(`Transfer submission failed: ${transferResponse.status}`);
3549
+ }
3550
+ const transferProof = await this.waitInclusionProofWithDevBypass(
3551
+ transferCommitment,
3552
+ options?.proofTimeoutMs
3553
+ );
3554
+ const transferTransaction = transferCommitment.toTransaction(transferProof);
3555
+ console.log("[InstantSplitProcessor] V4: Transfer proof received");
3556
+ const transferSalt = fromHex3(bundle.transferSaltHex);
3557
+ const finalPredicate = await import_UnmaskedPredicate4.UnmaskedPredicate.create(
3558
+ mintData.tokenId,
3559
+ tokenType,
3560
+ signingService,
3561
+ import_HashAlgorithm4.HashAlgorithm.SHA256,
3562
+ transferSalt
3563
+ );
3564
+ const finalState = new import_TokenState4.TokenState(finalPredicate, null);
3565
+ const finalTokenJson = mintedToken.toJSON();
3566
+ finalTokenJson.state = finalState.toJSON();
3567
+ finalTokenJson.transactions = [transferTransaction.toJSON()];
3568
+ const finalToken = await import_Token5.Token.fromJSON(finalTokenJson);
3569
+ console.log("[InstantSplitProcessor] V4: Token finalized");
3570
+ const duration = performance.now() - startTime;
3571
+ console.log(`[InstantSplitProcessor] V4 bundle processed in ${duration.toFixed(0)}ms`);
3572
+ return {
3573
+ success: true,
3574
+ token: finalToken,
3575
+ durationMs: duration
3576
+ };
3577
+ } catch (error) {
3578
+ const duration = performance.now() - startTime;
3579
+ const errorMessage = error instanceof Error ? error.message : String(error);
3580
+ console.error(`[InstantSplitProcessor] V4 processing failed:`, error);
3581
+ return {
3582
+ success: false,
3583
+ error: errorMessage,
3584
+ durationMs: duration
3585
+ };
3586
+ }
3587
+ }
3588
+ /**
3589
+ * Dev mode bypass for waitInclusionProof.
3590
+ */
3591
+ async waitInclusionProofWithDevBypass(commitment, timeoutMs = 6e4) {
3592
+ if (this.devMode) {
3593
+ try {
3594
+ return await Promise.race([
3595
+ (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment),
3596
+ new Promise(
3597
+ (_, reject) => setTimeout(() => reject(new Error("Dev mode timeout")), Math.min(timeoutMs, 5e3))
3598
+ )
3599
+ ]);
3600
+ } catch {
3601
+ console.log("[InstantSplitProcessor] Dev mode: Using mock proof");
3602
+ return {
3603
+ toJSON: () => ({ mock: true })
3604
+ };
3605
+ }
3606
+ }
3607
+ return (0, import_InclusionProofUtils4.waitInclusionProof)(this.trustBase, this.client, commitment);
3608
+ }
3609
+ };
3610
+
3611
+ // modules/payments/PaymentsModule.ts
3612
+ var import_Token6 = require("@unicitylabs/state-transition-sdk/lib/token/Token");
3613
+ var import_CoinId4 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
3614
+ var import_TransferCommitment4 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferCommitment");
3615
+ var import_TransferTransaction2 = require("@unicitylabs/state-transition-sdk/lib/transaction/TransferTransaction");
2922
3616
  var import_SigningService = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
2923
3617
  var import_AddressScheme = require("@unicitylabs/state-transition-sdk/lib/address/AddressScheme");
2924
- var import_UnmaskedPredicate3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
2925
- var import_TokenState3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
2926
- var import_HashAlgorithm3 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
3618
+ var import_UnmaskedPredicate5 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
3619
+ var import_TokenState5 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
3620
+ var import_HashAlgorithm5 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
2927
3621
  function enrichWithRegistry(info) {
2928
3622
  const registry = TokenRegistry.getInstance();
2929
3623
  const def = registry.getDefinition(info.coinId);
@@ -2949,7 +3643,7 @@ async function parseTokenInfo(tokenData) {
2949
3643
  try {
2950
3644
  const data = typeof tokenData === "string" ? JSON.parse(tokenData) : tokenData;
2951
3645
  try {
2952
- const sdkToken = await import_Token4.Token.fromJSON(data);
3646
+ const sdkToken = await import_Token6.Token.fromJSON(data);
2953
3647
  if (sdkToken.id) {
2954
3648
  defaultInfo.tokenId = sdkToken.id.toString();
2955
3649
  }
@@ -2962,7 +3656,7 @@ async function parseTokenInfo(tokenData) {
2962
3656
  if (Array.isArray(firstCoin) && firstCoin.length === 2) {
2963
3657
  [coinIdObj, amount] = firstCoin;
2964
3658
  }
2965
- if (coinIdObj instanceof import_CoinId3.CoinId) {
3659
+ if (coinIdObj instanceof import_CoinId4.CoinId) {
2966
3660
  const coinIdHex = coinIdObj.toJSON();
2967
3661
  return enrichWithRegistry({
2968
3662
  coinId: coinIdHex,
@@ -3095,21 +3789,48 @@ function extractStateHashFromSdkData(sdkData) {
3095
3789
  if (!sdkData) return "";
3096
3790
  try {
3097
3791
  const txf = JSON.parse(sdkData);
3098
- return getCurrentStateHash(txf) || "";
3792
+ const stateHash = getCurrentStateHash(txf);
3793
+ if (!stateHash) {
3794
+ if (txf.state?.hash) {
3795
+ return txf.state.hash;
3796
+ }
3797
+ if (txf.stateHash) {
3798
+ return txf.stateHash;
3799
+ }
3800
+ if (txf.currentStateHash) {
3801
+ return txf.currentStateHash;
3802
+ }
3803
+ }
3804
+ return stateHash || "";
3099
3805
  } catch {
3100
3806
  return "";
3101
3807
  }
3102
3808
  }
3103
- function isSameToken(t1, t2) {
3104
- if (t1.id === t2.id) return true;
3809
+ function createTokenStateKey(tokenId, stateHash) {
3810
+ return `${tokenId}_${stateHash}`;
3811
+ }
3812
+ function extractTokenStateKey(token) {
3813
+ const tokenId = extractTokenIdFromSdkData(token.sdkData);
3814
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
3815
+ if (!tokenId || !stateHash) return null;
3816
+ return createTokenStateKey(tokenId, stateHash);
3817
+ }
3818
+ function hasSameGenesisTokenId(t1, t2) {
3105
3819
  const id1 = extractTokenIdFromSdkData(t1.sdkData);
3106
3820
  const id2 = extractTokenIdFromSdkData(t2.sdkData);
3107
3821
  return !!(id1 && id2 && id1 === id2);
3108
3822
  }
3823
+ function isSameTokenState(t1, t2) {
3824
+ const key1 = extractTokenStateKey(t1);
3825
+ const key2 = extractTokenStateKey(t2);
3826
+ return !!(key1 && key2 && key1 === key2);
3827
+ }
3109
3828
  function createTombstoneFromToken(token) {
3110
3829
  const tokenId = extractTokenIdFromSdkData(token.sdkData);
3111
- if (!tokenId) return null;
3112
3830
  const stateHash = extractStateHashFromSdkData(token.sdkData);
3831
+ if (!tokenId || !stateHash) {
3832
+ return null;
3833
+ }
3113
3834
  return {
3114
3835
  tokenId,
3115
3836
  stateHash,
@@ -3175,7 +3896,7 @@ function findBestTokenVersion(tokenId, archivedTokens, forkedTokens) {
3175
3896
  candidates.sort((a, b) => countCommittedTxns(b) - countCommittedTxns(a));
3176
3897
  return candidates[0];
3177
3898
  }
3178
- var PaymentsModule = class {
3899
+ var PaymentsModule = class _PaymentsModule {
3179
3900
  moduleConfig;
3180
3901
  deps = null;
3181
3902
  /** L1 (ALPHA blockchain) payments sub-module (null if disabled) */
@@ -3200,6 +3921,13 @@ var PaymentsModule = class {
3200
3921
  unsubscribeTransfers = null;
3201
3922
  unsubscribePaymentRequests = null;
3202
3923
  unsubscribePaymentRequestResponses = null;
3924
+ // NOSTR-FIRST proof polling (background proof verification)
3925
+ proofPollingJobs = /* @__PURE__ */ new Map();
3926
+ proofPollingInterval = null;
3927
+ static PROOF_POLLING_INTERVAL_MS = 2e3;
3928
+ // Poll every 2s
3929
+ static PROOF_POLLING_MAX_ATTEMPTS = 30;
3930
+ // Max 30 attempts (~60s)
3203
3931
  constructor(config) {
3204
3932
  this.moduleConfig = {
3205
3933
  autoSync: config?.autoSync ?? true,
@@ -3215,6 +3943,8 @@ var PaymentsModule = class {
3215
3943
  getConfig() {
3216
3944
  return this.moduleConfig;
3217
3945
  }
3946
+ /** Price provider (optional) */
3947
+ priceProvider = null;
3218
3948
  log(...args) {
3219
3949
  if (this.moduleConfig.debug) {
3220
3950
  console.log("[PaymentsModule]", ...args);
@@ -3227,7 +3957,21 @@ var PaymentsModule = class {
3227
3957
  * Initialize module with dependencies
3228
3958
  */
3229
3959
  initialize(deps) {
3960
+ this.unsubscribeTransfers?.();
3961
+ this.unsubscribeTransfers = null;
3962
+ this.unsubscribePaymentRequests?.();
3963
+ this.unsubscribePaymentRequests = null;
3964
+ this.unsubscribePaymentRequestResponses?.();
3965
+ this.unsubscribePaymentRequestResponses = null;
3966
+ this.tokens.clear();
3967
+ this.pendingTransfers.clear();
3968
+ this.tombstones = [];
3969
+ this.archivedTokens.clear();
3970
+ this.forkedTokens.clear();
3971
+ this.transactionHistory = [];
3972
+ this.nametag = null;
3230
3973
  this.deps = deps;
3974
+ this.priceProvider = deps.price ?? null;
3231
3975
  if (this.l1) {
3232
3976
  this.l1.initialize({
3233
3977
  identity: deps.identity,
@@ -3298,6 +4042,8 @@ var PaymentsModule = class {
3298
4042
  this.unsubscribePaymentRequestResponses = null;
3299
4043
  this.paymentRequestHandlers.clear();
3300
4044
  this.paymentRequestResponseHandlers.clear();
4045
+ this.stopProofPolling();
4046
+ this.proofPollingJobs.clear();
3301
4047
  for (const [, resolver] of this.pendingResponseResolvers) {
3302
4048
  clearTimeout(resolver.timeout);
3303
4049
  resolver.reject(new Error("Module destroyed"));
@@ -3322,8 +4068,9 @@ var PaymentsModule = class {
3322
4068
  tokens: []
3323
4069
  };
3324
4070
  try {
3325
- const recipientPubkey = await this.resolveRecipient(request.recipient);
3326
- const recipientAddress = await this.resolveRecipientAddress(request.recipient);
4071
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4072
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4073
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
3327
4074
  const signingService = await this.createSigningService();
3328
4075
  const stClient = this.deps.oracle.getStateTransitionClient?.();
3329
4076
  if (!stClient) {
@@ -3343,7 +4090,6 @@ var PaymentsModule = class {
3343
4090
  if (!splitPlan) {
3344
4091
  throw new Error("Insufficient balance");
3345
4092
  }
3346
- this.log(`Split plan: requiresSplit=${splitPlan.requiresSplit}, directTokens=${splitPlan.tokensToTransferDirectly.length}`);
3347
4093
  const tokensToSend = splitPlan.tokensToTransferDirectly.map((t) => t.uiToken);
3348
4094
  if (splitPlan.tokenToSplit) {
3349
4095
  tokensToSend.push(splitPlan.tokenToSplit.uiToken);
@@ -3386,11 +4132,13 @@ var PaymentsModule = class {
3386
4132
  };
3387
4133
  await this.addToken(changeToken, true);
3388
4134
  this.log(`Change token saved: ${changeToken.id}, amount: ${changeToken.amount}`);
4135
+ console.log(`[Payments] Sending split token to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3389
4136
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3390
4137
  sourceToken: JSON.stringify(splitResult.tokenForRecipient.toJSON()),
3391
4138
  transferTx: JSON.stringify(splitResult.recipientTransferTx.toJSON()),
3392
4139
  memo: request.memo
3393
4140
  });
4141
+ console.log(`[Payments] Split token sent successfully`);
3394
4142
  await this.removeToken(splitPlan.tokenToSplit.uiToken.id, recipientNametag);
3395
4143
  result.txHash = "split-" + Date.now().toString(16);
3396
4144
  this.log(`Split transfer completed`);
@@ -3409,11 +4157,13 @@ var PaymentsModule = class {
3409
4157
  const transferTx = commitment.toTransaction(inclusionProof);
3410
4158
  const requestIdBytes = commitment.requestId;
3411
4159
  result.txHash = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
4160
+ console.log(`[Payments] Sending direct token ${token.id.slice(0, 8)}... to ${recipientPubkey.slice(0, 8)}... via Nostr`);
3412
4161
  await this.deps.transport.sendTokenTransfer(recipientPubkey, {
3413
4162
  sourceToken: JSON.stringify(tokenWithAmount.sdkToken.toJSON()),
3414
4163
  transferTx: JSON.stringify(transferTx.toJSON()),
3415
4164
  memo: request.memo
3416
4165
  });
4166
+ console.log(`[Payments] Direct token sent successfully`);
3417
4167
  this.log(`Token ${token.id} transferred, txHash: ${result.txHash}`);
3418
4168
  await this.removeToken(token.id, recipientNametag);
3419
4169
  }
@@ -3467,6 +4217,234 @@ var PaymentsModule = class {
3467
4217
  return TokenRegistry.getInstance().getIconUrl(coinId) ?? void 0;
3468
4218
  }
3469
4219
  // ===========================================================================
4220
+ // Public API - Instant Split (V5 Optimized)
4221
+ // ===========================================================================
4222
+ /**
4223
+ * Send tokens using INSTANT_SPLIT V5 optimized flow.
4224
+ *
4225
+ * This achieves ~2.3s critical path latency instead of ~42s by:
4226
+ * 1. Waiting only for burn proof (required)
4227
+ * 2. Creating transfer commitment from mint data (no mint proof needed)
4228
+ * 3. Sending bundle via Nostr immediately
4229
+ * 4. Processing mints in background
4230
+ *
4231
+ * @param request - Transfer request with recipient, amount, and coinId
4232
+ * @param options - Optional instant split configuration
4233
+ * @returns InstantSplitResult with timing info
4234
+ */
4235
+ async sendInstant(request, options) {
4236
+ this.ensureInitialized();
4237
+ const startTime = performance.now();
4238
+ try {
4239
+ const peerInfo = await this.deps.transport.resolve?.(request.recipient) ?? null;
4240
+ const recipientPubkey = this.resolveTransportPubkey(request.recipient, peerInfo);
4241
+ const recipientAddress = await this.resolveRecipientAddress(request.recipient, request.addressMode, peerInfo);
4242
+ const signingService = await this.createSigningService();
4243
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4244
+ if (!stClient) {
4245
+ throw new Error("State transition client not available");
4246
+ }
4247
+ const trustBase = this.deps.oracle.getTrustBase?.();
4248
+ if (!trustBase) {
4249
+ throw new Error("Trust base not available");
4250
+ }
4251
+ const calculator = new TokenSplitCalculator();
4252
+ const availableTokens = Array.from(this.tokens.values());
4253
+ const splitPlan = await calculator.calculateOptimalSplit(
4254
+ availableTokens,
4255
+ BigInt(request.amount),
4256
+ request.coinId
4257
+ );
4258
+ if (!splitPlan) {
4259
+ throw new Error("Insufficient balance");
4260
+ }
4261
+ if (!splitPlan.requiresSplit || !splitPlan.tokenToSplit) {
4262
+ this.log("No split required, falling back to standard send()");
4263
+ const result2 = await this.send(request);
4264
+ return {
4265
+ success: result2.status === "completed",
4266
+ criticalPathDurationMs: performance.now() - startTime,
4267
+ error: result2.error
4268
+ };
4269
+ }
4270
+ this.log(`InstantSplit: amount=${splitPlan.splitAmount}, remainder=${splitPlan.remainderAmount}`);
4271
+ const tokenToSplit = splitPlan.tokenToSplit.uiToken;
4272
+ tokenToSplit.status = "transferring";
4273
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4274
+ const devMode = options?.devMode ?? this.deps.oracle.isDevMode?.() ?? false;
4275
+ const executor = new InstantSplitExecutor({
4276
+ stateTransitionClient: stClient,
4277
+ trustBase,
4278
+ signingService,
4279
+ devMode
4280
+ });
4281
+ const result = await executor.executeSplitInstant(
4282
+ splitPlan.tokenToSplit.sdkToken,
4283
+ splitPlan.splitAmount,
4284
+ splitPlan.remainderAmount,
4285
+ splitPlan.coinId,
4286
+ recipientAddress,
4287
+ this.deps.transport,
4288
+ recipientPubkey,
4289
+ {
4290
+ ...options,
4291
+ onChangeTokenCreated: async (changeToken) => {
4292
+ const changeTokenData = changeToken.toJSON();
4293
+ const uiToken = {
4294
+ id: crypto.randomUUID(),
4295
+ coinId: request.coinId,
4296
+ symbol: this.getCoinSymbol(request.coinId),
4297
+ name: this.getCoinName(request.coinId),
4298
+ decimals: this.getCoinDecimals(request.coinId),
4299
+ iconUrl: this.getCoinIconUrl(request.coinId),
4300
+ amount: splitPlan.remainderAmount.toString(),
4301
+ status: "confirmed",
4302
+ createdAt: Date.now(),
4303
+ updatedAt: Date.now(),
4304
+ sdkData: JSON.stringify(changeTokenData)
4305
+ };
4306
+ await this.addToken(uiToken, true);
4307
+ this.log(`Change token saved via background: ${uiToken.id}`);
4308
+ },
4309
+ onStorageSync: async () => {
4310
+ await this.save();
4311
+ return true;
4312
+ }
4313
+ }
4314
+ );
4315
+ if (result.success) {
4316
+ const recipientNametag = request.recipient.startsWith("@") ? request.recipient.slice(1) : void 0;
4317
+ await this.removeToken(tokenToSplit.id, recipientNametag);
4318
+ await this.addToHistory({
4319
+ type: "SENT",
4320
+ amount: request.amount,
4321
+ coinId: request.coinId,
4322
+ symbol: this.getCoinSymbol(request.coinId),
4323
+ timestamp: Date.now(),
4324
+ recipientNametag
4325
+ });
4326
+ await this.save();
4327
+ } else {
4328
+ tokenToSplit.status = "confirmed";
4329
+ this.tokens.set(tokenToSplit.id, tokenToSplit);
4330
+ }
4331
+ return result;
4332
+ } catch (error) {
4333
+ const errorMessage = error instanceof Error ? error.message : String(error);
4334
+ return {
4335
+ success: false,
4336
+ criticalPathDurationMs: performance.now() - startTime,
4337
+ error: errorMessage
4338
+ };
4339
+ }
4340
+ }
4341
+ /**
4342
+ * Process a received INSTANT_SPLIT bundle.
4343
+ *
4344
+ * This should be called when receiving an instant split bundle via transport.
4345
+ * It handles the recipient-side processing:
4346
+ * 1. Validate burn transaction
4347
+ * 2. Submit and wait for mint proof
4348
+ * 3. Submit and wait for transfer proof
4349
+ * 4. Finalize and save the token
4350
+ *
4351
+ * @param bundle - The received InstantSplitBundle (V4 or V5)
4352
+ * @param senderPubkey - Sender's public key for verification
4353
+ * @returns Processing result with finalized token
4354
+ */
4355
+ async processInstantSplitBundle(bundle, senderPubkey) {
4356
+ this.ensureInitialized();
4357
+ try {
4358
+ const signingService = await this.createSigningService();
4359
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
4360
+ if (!stClient) {
4361
+ throw new Error("State transition client not available");
4362
+ }
4363
+ const trustBase = this.deps.oracle.getTrustBase?.();
4364
+ if (!trustBase) {
4365
+ throw new Error("Trust base not available");
4366
+ }
4367
+ const devMode = this.deps.oracle.isDevMode?.() ?? false;
4368
+ const processor = new InstantSplitProcessor({
4369
+ stateTransitionClient: stClient,
4370
+ trustBase,
4371
+ devMode
4372
+ });
4373
+ const result = await processor.processReceivedBundle(
4374
+ bundle,
4375
+ signingService,
4376
+ senderPubkey,
4377
+ {
4378
+ findNametagToken: async (proxyAddress) => {
4379
+ if (this.nametag?.token) {
4380
+ try {
4381
+ const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
4382
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
4383
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
4384
+ if (proxy.address === proxyAddress) {
4385
+ return nametagToken;
4386
+ }
4387
+ this.log(`Nametag PROXY address mismatch: ${proxy.address} !== ${proxyAddress}`);
4388
+ return null;
4389
+ } catch (err) {
4390
+ this.log("Failed to parse nametag token:", err);
4391
+ return null;
4392
+ }
4393
+ }
4394
+ return null;
4395
+ }
4396
+ }
4397
+ );
4398
+ if (result.success && result.token) {
4399
+ const tokenData = result.token.toJSON();
4400
+ const info = await parseTokenInfo(tokenData);
4401
+ const uiToken = {
4402
+ id: crypto.randomUUID(),
4403
+ coinId: info.coinId,
4404
+ symbol: info.symbol,
4405
+ name: info.name,
4406
+ decimals: info.decimals,
4407
+ iconUrl: info.iconUrl,
4408
+ amount: bundle.amount,
4409
+ status: "confirmed",
4410
+ createdAt: Date.now(),
4411
+ updatedAt: Date.now(),
4412
+ sdkData: JSON.stringify(tokenData)
4413
+ };
4414
+ await this.addToken(uiToken);
4415
+ await this.addToHistory({
4416
+ type: "RECEIVED",
4417
+ amount: bundle.amount,
4418
+ coinId: info.coinId,
4419
+ symbol: info.symbol,
4420
+ timestamp: Date.now(),
4421
+ senderPubkey
4422
+ });
4423
+ await this.save();
4424
+ this.deps.emitEvent("transfer:incoming", {
4425
+ id: bundle.splitGroupId,
4426
+ senderPubkey,
4427
+ tokens: [uiToken],
4428
+ receivedAt: Date.now()
4429
+ });
4430
+ }
4431
+ return result;
4432
+ } catch (error) {
4433
+ const errorMessage = error instanceof Error ? error.message : String(error);
4434
+ return {
4435
+ success: false,
4436
+ error: errorMessage,
4437
+ durationMs: 0
4438
+ };
4439
+ }
4440
+ }
4441
+ /**
4442
+ * Check if a payload is an instant split bundle
4443
+ */
4444
+ isInstantSplitBundle(payload) {
4445
+ return isInstantSplitBundle(payload);
4446
+ }
4447
+ // ===========================================================================
3470
4448
  // Public API - Payment Requests
3471
4449
  // ===========================================================================
3472
4450
  /**
@@ -3484,7 +4462,8 @@ var PaymentsModule = class {
3484
4462
  };
3485
4463
  }
3486
4464
  try {
3487
- const recipientPubkey = await this.resolveRecipient(recipientPubkeyOrNametag);
4465
+ const peerInfo = await this.deps.transport.resolve?.(recipientPubkeyOrNametag) ?? null;
4466
+ const recipientPubkey = this.resolveTransportPubkey(recipientPubkeyOrNametag, peerInfo);
3488
4467
  const payload = {
3489
4468
  amount: request.amount,
3490
4469
  coinId: request.coinId,
@@ -3788,47 +4767,46 @@ var PaymentsModule = class {
3788
4767
  // Public API - Balance & Tokens
3789
4768
  // ===========================================================================
3790
4769
  /**
3791
- * Get balance for coin type
4770
+ * Set or update price provider
3792
4771
  */
3793
- getBalance(coinId) {
3794
- const balances = /* @__PURE__ */ new Map();
3795
- for (const token of this.tokens.values()) {
3796
- if (token.status !== "confirmed") continue;
3797
- if (coinId && token.coinId !== coinId) continue;
3798
- const key = token.coinId;
3799
- const existing = balances.get(key);
3800
- if (existing) {
3801
- existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
3802
- existing.tokenCount++;
3803
- } else {
3804
- balances.set(key, {
3805
- coinId: token.coinId,
3806
- symbol: token.symbol,
3807
- name: token.name,
3808
- totalAmount: token.amount,
3809
- tokenCount: 1,
3810
- decimals: 8
3811
- });
4772
+ setPriceProvider(provider) {
4773
+ this.priceProvider = provider;
4774
+ }
4775
+ /**
4776
+ * Get total portfolio value in USD
4777
+ * Returns null if PriceProvider is not configured
4778
+ */
4779
+ async getBalance() {
4780
+ const assets = await this.getAssets();
4781
+ if (!this.priceProvider) {
4782
+ return null;
4783
+ }
4784
+ let total = 0;
4785
+ let hasAnyPrice = false;
4786
+ for (const asset of assets) {
4787
+ if (asset.fiatValueUsd != null) {
4788
+ total += asset.fiatValueUsd;
4789
+ hasAnyPrice = true;
3812
4790
  }
3813
4791
  }
3814
- return Array.from(balances.values());
4792
+ return hasAnyPrice ? total : null;
3815
4793
  }
3816
4794
  /**
3817
- * Get aggregated assets (tokens grouped by coinId)
4795
+ * Get aggregated assets (tokens grouped by coinId) with price data
3818
4796
  * Only includes confirmed tokens
3819
4797
  */
3820
- getAssets(coinId) {
3821
- const assets = /* @__PURE__ */ new Map();
4798
+ async getAssets(coinId) {
4799
+ const assetsMap = /* @__PURE__ */ new Map();
3822
4800
  for (const token of this.tokens.values()) {
3823
4801
  if (token.status !== "confirmed") continue;
3824
4802
  if (coinId && token.coinId !== coinId) continue;
3825
4803
  const key = token.coinId;
3826
- const existing = assets.get(key);
4804
+ const existing = assetsMap.get(key);
3827
4805
  if (existing) {
3828
4806
  existing.totalAmount = (BigInt(existing.totalAmount) + BigInt(token.amount)).toString();
3829
4807
  existing.tokenCount++;
3830
4808
  } else {
3831
- assets.set(key, {
4809
+ assetsMap.set(key, {
3832
4810
  coinId: token.coinId,
3833
4811
  symbol: token.symbol,
3834
4812
  name: token.name,
@@ -3839,7 +4817,66 @@ var PaymentsModule = class {
3839
4817
  });
3840
4818
  }
3841
4819
  }
3842
- return Array.from(assets.values());
4820
+ const rawAssets = Array.from(assetsMap.values());
4821
+ let priceMap = null;
4822
+ if (this.priceProvider && rawAssets.length > 0) {
4823
+ const registry = TokenRegistry.getInstance();
4824
+ const nameToCoins = /* @__PURE__ */ new Map();
4825
+ for (const asset of rawAssets) {
4826
+ const def = registry.getDefinition(asset.coinId);
4827
+ if (def?.name) {
4828
+ const existing = nameToCoins.get(def.name);
4829
+ if (existing) {
4830
+ existing.push(asset.coinId);
4831
+ } else {
4832
+ nameToCoins.set(def.name, [asset.coinId]);
4833
+ }
4834
+ }
4835
+ }
4836
+ if (nameToCoins.size > 0) {
4837
+ const tokenNames = Array.from(nameToCoins.keys());
4838
+ const prices = await this.priceProvider.getPrices(tokenNames);
4839
+ priceMap = /* @__PURE__ */ new Map();
4840
+ for (const [name, coinIds] of nameToCoins) {
4841
+ const price = prices.get(name);
4842
+ if (price) {
4843
+ for (const cid of coinIds) {
4844
+ priceMap.set(cid, {
4845
+ priceUsd: price.priceUsd,
4846
+ priceEur: price.priceEur,
4847
+ change24h: price.change24h
4848
+ });
4849
+ }
4850
+ }
4851
+ }
4852
+ }
4853
+ }
4854
+ return rawAssets.map((raw) => {
4855
+ const price = priceMap?.get(raw.coinId);
4856
+ let fiatValueUsd = null;
4857
+ let fiatValueEur = null;
4858
+ if (price) {
4859
+ const humanAmount = Number(raw.totalAmount) / Math.pow(10, raw.decimals);
4860
+ fiatValueUsd = humanAmount * price.priceUsd;
4861
+ if (price.priceEur != null) {
4862
+ fiatValueEur = humanAmount * price.priceEur;
4863
+ }
4864
+ }
4865
+ return {
4866
+ coinId: raw.coinId,
4867
+ symbol: raw.symbol,
4868
+ name: raw.name,
4869
+ decimals: raw.decimals,
4870
+ iconUrl: raw.iconUrl,
4871
+ totalAmount: raw.totalAmount,
4872
+ tokenCount: raw.tokenCount,
4873
+ priceUsd: price?.priceUsd ?? null,
4874
+ priceEur: price?.priceEur ?? null,
4875
+ change24h: price?.change24h ?? null,
4876
+ fiatValueUsd,
4877
+ fiatValueEur
4878
+ };
4879
+ });
3843
4880
  }
3844
4881
  /**
3845
4882
  * Get all tokens
@@ -3865,14 +4902,52 @@ var PaymentsModule = class {
3865
4902
  // ===========================================================================
3866
4903
  /**
3867
4904
  * Add a token
3868
- * @returns false if duplicate
4905
+ * Tokens are uniquely identified by (tokenId, stateHash) composite key.
4906
+ * Multiple historic states of the same token can coexist.
4907
+ * @returns false if exact duplicate (same tokenId AND same stateHash)
3869
4908
  */
3870
4909
  async addToken(token, skipHistory = false) {
3871
4910
  this.ensureInitialized();
3872
- for (const existing of this.tokens.values()) {
3873
- if (isSameToken(existing, token)) {
3874
- this.log(`Duplicate token detected: ${token.id}`);
3875
- return false;
4911
+ const incomingTokenId = extractTokenIdFromSdkData(token.sdkData);
4912
+ const incomingStateHash = extractStateHashFromSdkData(token.sdkData);
4913
+ const incomingStateKey = incomingTokenId && incomingStateHash ? createTokenStateKey(incomingTokenId, incomingStateHash) : null;
4914
+ if (incomingTokenId && incomingStateHash && this.isStateTombstoned(incomingTokenId, incomingStateHash)) {
4915
+ this.log(`Rejecting tombstoned token: ${incomingTokenId.slice(0, 8)}..._${incomingStateHash.slice(0, 8)}...`);
4916
+ return false;
4917
+ }
4918
+ if (incomingStateKey) {
4919
+ for (const [existingId, existing] of this.tokens) {
4920
+ if (isSameTokenState(existing, token)) {
4921
+ this.log(`Duplicate token state ignored: ${incomingTokenId?.slice(0, 8)}..._${incomingStateHash?.slice(0, 8)}...`);
4922
+ return false;
4923
+ }
4924
+ }
4925
+ }
4926
+ for (const [existingId, existing] of this.tokens) {
4927
+ if (hasSameGenesisTokenId(existing, token)) {
4928
+ const existingStateHash = extractStateHashFromSdkData(existing.sdkData);
4929
+ if (incomingStateHash && existingStateHash && incomingStateHash === existingStateHash) {
4930
+ continue;
4931
+ }
4932
+ if (existing.status === "spent" || existing.status === "invalid") {
4933
+ this.log(`Replacing spent/invalid token ${incomingTokenId?.slice(0, 8)}...`);
4934
+ this.tokens.delete(existingId);
4935
+ break;
4936
+ }
4937
+ if (incomingStateHash && existingStateHash && incomingStateHash !== existingStateHash) {
4938
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... state updated: ${existingStateHash.slice(0, 8)}... -> ${incomingStateHash.slice(0, 8)}...`);
4939
+ await this.archiveToken(existing);
4940
+ this.tokens.delete(existingId);
4941
+ break;
4942
+ }
4943
+ if (!incomingStateHash || !existingStateHash) {
4944
+ if (existingId !== token.id) {
4945
+ this.log(`Token ${incomingTokenId?.slice(0, 8)}... .id changed, replacing`);
4946
+ await this.archiveToken(existing);
4947
+ this.tokens.delete(existingId);
4948
+ break;
4949
+ }
4950
+ }
3876
4951
  }
3877
4952
  }
3878
4953
  this.tokens.set(token.id, token);
@@ -4027,8 +5102,10 @@ var PaymentsModule = class {
4027
5102
  );
4028
5103
  if (!alreadyTombstoned) {
4029
5104
  this.tombstones.push(tombstone);
4030
- this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}...`);
5105
+ this.log(`Created tombstone for ${tombstone.tokenId.slice(0, 8)}..._${tombstone.stateHash.slice(0, 8)}...`);
4031
5106
  }
5107
+ } else {
5108
+ this.log(`Warning: Could not create tombstone for token ${tokenId.slice(0, 8)}... (missing tokenId or stateHash)`);
4032
5109
  }
4033
5110
  this.tokens.delete(tokenId);
4034
5111
  if (!skipHistory && token.coinId && token.amount) {
@@ -4346,15 +5423,15 @@ var PaymentsModule = class {
4346
5423
  }
4347
5424
  try {
4348
5425
  const signingService = await this.createSigningService();
4349
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4350
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5426
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5427
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4351
5428
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4352
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4353
- const addressRef = await UnmaskedPredicateReference3.create(
5429
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5430
+ const addressRef = await UnmaskedPredicateReference4.create(
4354
5431
  tokenType,
4355
5432
  signingService.algorithm,
4356
5433
  signingService.publicKey,
4357
- import_HashAlgorithm3.HashAlgorithm.SHA256
5434
+ import_HashAlgorithm5.HashAlgorithm.SHA256
4358
5435
  );
4359
5436
  const ownerAddress = await addressRef.toAddress();
4360
5437
  const minter = new NametagMinter({
@@ -4521,40 +5598,22 @@ var PaymentsModule = class {
4521
5598
  * Detect if a string is an L3 address (not a nametag)
4522
5599
  * Returns true for: hex pubkeys (64+ chars), PROXY:, DIRECT: prefixed addresses
4523
5600
  */
4524
- isL3Address(value) {
4525
- if (value.startsWith("PROXY:") || value.startsWith("DIRECT:")) {
4526
- return true;
4527
- }
4528
- if (value.length >= 64 && /^[0-9a-fA-F]+$/.test(value)) {
4529
- return true;
4530
- }
4531
- return false;
4532
- }
4533
5601
  /**
4534
- * Resolve recipient to Nostr pubkey for messaging
4535
- * Supports: nametag (with or without @), hex pubkey
5602
+ * Resolve recipient to transport pubkey for messaging.
5603
+ * Uses pre-resolved PeerInfo if available, otherwise resolves via transport.
4536
5604
  */
4537
- async resolveRecipient(recipient) {
4538
- if (recipient.startsWith("@")) {
4539
- const nametag = recipient.slice(1);
4540
- const pubkey = await this.deps.transport.resolveNametag?.(nametag);
4541
- if (!pubkey) {
4542
- throw new Error(`Nametag not found: ${nametag}`);
4543
- }
4544
- return pubkey;
4545
- }
4546
- if (this.isL3Address(recipient)) {
4547
- return recipient;
5605
+ resolveTransportPubkey(recipient, peerInfo) {
5606
+ if (peerInfo?.transportPubkey) {
5607
+ return peerInfo.transportPubkey;
4548
5608
  }
4549
- if (this.deps?.transport.resolveNametag) {
4550
- const pubkey = await this.deps.transport.resolveNametag(recipient);
4551
- if (pubkey) {
4552
- this.log(`Resolved "${recipient}" as nametag to pubkey`);
4553
- return pubkey;
5609
+ if (recipient.length >= 64 && /^[0-9a-fA-F]+$/.test(recipient)) {
5610
+ if (recipient.length === 66 && (recipient.startsWith("02") || recipient.startsWith("03"))) {
5611
+ return recipient.slice(2);
4554
5612
  }
5613
+ return recipient;
4555
5614
  }
4556
5615
  throw new Error(
4557
- `Recipient "${recipient}" is not a valid nametag or address. Use @nametag for explicit nametag or a valid hex pubkey/PROXY:/DIRECT: address.`
5616
+ `Cannot resolve transport pubkey for "${recipient}". No binding event found. The recipient must publish their identity first.`
4558
5617
  );
4559
5618
  }
4560
5619
  /**
@@ -4562,9 +5621,9 @@ var PaymentsModule = class {
4562
5621
  */
4563
5622
  async createSdkCommitment(token, recipientAddress, signingService) {
4564
5623
  const tokenData = token.sdkData ? typeof token.sdkData === "string" ? JSON.parse(token.sdkData) : token.sdkData : token;
4565
- const sdkToken = await import_Token4.Token.fromJSON(tokenData);
5624
+ const sdkToken = await import_Token6.Token.fromJSON(tokenData);
4566
5625
  const salt = crypto.getRandomValues(new Uint8Array(32));
4567
- const commitment = await import_TransferCommitment2.TransferCommitment.create(
5626
+ const commitment = await import_TransferCommitment4.TransferCommitment.create(
4568
5627
  sdkToken,
4569
5628
  recipientAddress,
4570
5629
  salt,
@@ -4590,75 +5649,264 @@ var PaymentsModule = class {
4590
5649
  * Create DirectAddress from a public key using UnmaskedPredicateReference
4591
5650
  */
4592
5651
  async createDirectAddressFromPubkey(pubkeyHex) {
4593
- const { UnmaskedPredicateReference: UnmaskedPredicateReference3 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
4594
- const { TokenType: TokenType3 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5652
+ const { UnmaskedPredicateReference: UnmaskedPredicateReference4 } = await import("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5653
+ const { TokenType: TokenType5 } = await import("@unicitylabs/state-transition-sdk/lib/token/TokenType");
4595
5654
  const UNICITY_TOKEN_TYPE_HEX3 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
4596
- const tokenType = new TokenType3(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
5655
+ const tokenType = new TokenType5(Buffer.from(UNICITY_TOKEN_TYPE_HEX3, "hex"));
4597
5656
  const pubkeyBytes = new Uint8Array(
4598
5657
  pubkeyHex.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))
4599
5658
  );
4600
- const addressRef = await UnmaskedPredicateReference3.create(
5659
+ const addressRef = await UnmaskedPredicateReference4.create(
4601
5660
  tokenType,
4602
5661
  "secp256k1",
4603
5662
  pubkeyBytes,
4604
- import_HashAlgorithm3.HashAlgorithm.SHA256
5663
+ import_HashAlgorithm5.HashAlgorithm.SHA256
4605
5664
  );
4606
5665
  return addressRef.toAddress();
4607
5666
  }
4608
5667
  /**
4609
- * Resolve nametag to 33-byte compressed public key using resolveNametagInfo
4610
- * Returns null if nametag not found or publicKey not available
5668
+ * Resolve recipient to IAddress for L3 transfers.
5669
+ * Uses pre-resolved PeerInfo when available to avoid redundant network queries.
4611
5670
  */
4612
- async resolveNametagToPublicKey(nametag) {
4613
- if (!this.deps?.transport.resolveNametagInfo) {
4614
- this.log("resolveNametagInfo not available on transport");
4615
- return null;
5671
+ async resolveRecipientAddress(recipient, addressMode = "auto", peerInfo) {
5672
+ const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
5673
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
5674
+ if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
5675
+ return AddressFactory.createAddress(recipient);
5676
+ }
5677
+ if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
5678
+ this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
5679
+ return this.createDirectAddressFromPubkey(recipient);
4616
5680
  }
4617
- const info = await this.deps.transport.resolveNametagInfo(nametag);
5681
+ const info = peerInfo ?? await this.deps?.transport.resolve?.(recipient) ?? null;
4618
5682
  if (!info) {
4619
- this.log(`Nametag "${nametag}" not found`);
4620
- return null;
5683
+ throw new Error(
5684
+ `Recipient "${recipient}" not found. Use @nametag, a valid PROXY:/DIRECT: address, or a 33-byte hex pubkey.`
5685
+ );
4621
5686
  }
4622
- if (!info.chainPubkey) {
4623
- this.log(`Nametag "${nametag}" has no 33-byte chainPubkey (legacy event)`);
4624
- return null;
5687
+ const nametag = recipient.startsWith("@") ? recipient.slice(1) : info.nametag || recipient;
5688
+ if (addressMode === "proxy") {
5689
+ console.log(`[Payments] Using PROXY address for "${nametag}" (forced)`);
5690
+ return ProxyAddress.fromNameTag(nametag);
5691
+ }
5692
+ if (addressMode === "direct") {
5693
+ if (!info.directAddress) {
5694
+ throw new Error(`"${nametag}" has no DirectAddress stored. It may be a legacy registration.`);
5695
+ }
5696
+ console.log(`[Payments] Using DirectAddress for "${nametag}" (forced): ${info.directAddress.slice(0, 30)}...`);
5697
+ return AddressFactory.createAddress(info.directAddress);
4625
5698
  }
4626
- return info.chainPubkey;
5699
+ if (info.directAddress) {
5700
+ this.log(`Using DirectAddress for "${nametag}": ${info.directAddress.slice(0, 30)}...`);
5701
+ return AddressFactory.createAddress(info.directAddress);
5702
+ }
5703
+ this.log(`Using PROXY address for legacy nametag "${nametag}"`);
5704
+ return ProxyAddress.fromNameTag(nametag);
4627
5705
  }
4628
5706
  /**
4629
- * Resolve recipient to IAddress for L3 transfers
4630
- * Supports: nametag (with or without @), PROXY:, DIRECT:, hex pubkey
5707
+ * Handle NOSTR-FIRST commitment-only transfer (recipient side)
5708
+ * This is called when receiving a transfer with only commitmentData and no proof yet.
5709
+ * We create the token as 'submitted', submit commitment (idempotent), and poll for proof.
4631
5710
  */
4632
- async resolveRecipientAddress(recipient) {
4633
- const { AddressFactory } = await import("@unicitylabs/state-transition-sdk/lib/address/AddressFactory");
4634
- if (recipient.startsWith("@")) {
4635
- const nametag = recipient.slice(1);
4636
- const publicKey2 = await this.resolveNametagToPublicKey(nametag);
4637
- if (publicKey2) {
4638
- this.log(`Resolved @${nametag} to 33-byte publicKey for DirectAddress`);
4639
- return this.createDirectAddressFromPubkey(publicKey2);
5711
+ async handleCommitmentOnlyTransfer(transfer, payload) {
5712
+ try {
5713
+ const sourceTokenInput = typeof payload.sourceToken === "string" ? JSON.parse(payload.sourceToken) : payload.sourceToken;
5714
+ const commitmentInput = typeof payload.commitmentData === "string" ? JSON.parse(payload.commitmentData) : payload.commitmentData;
5715
+ if (!sourceTokenInput || !commitmentInput) {
5716
+ console.warn("[Payments] Invalid NOSTR-FIRST transfer format");
5717
+ return;
4640
5718
  }
4641
- throw new Error(`Nametag "${nametag}" not found or missing publicKey`);
4642
- }
4643
- if (recipient.startsWith("PROXY:") || recipient.startsWith("DIRECT:")) {
4644
- return AddressFactory.createAddress(recipient);
4645
- }
4646
- if (recipient.length === 66 && /^[0-9a-fA-F]+$/.test(recipient)) {
4647
- this.log(`Creating DirectAddress from 33-byte compressed pubkey`);
4648
- return this.createDirectAddressFromPubkey(recipient);
5719
+ const tokenInfo = await parseTokenInfo(sourceTokenInput);
5720
+ const token = {
5721
+ id: tokenInfo.tokenId ?? crypto.randomUUID(),
5722
+ coinId: tokenInfo.coinId,
5723
+ symbol: tokenInfo.symbol,
5724
+ name: tokenInfo.name,
5725
+ decimals: tokenInfo.decimals,
5726
+ iconUrl: tokenInfo.iconUrl,
5727
+ amount: tokenInfo.amount,
5728
+ status: "submitted",
5729
+ // NOSTR-FIRST: unconfirmed until proof
5730
+ createdAt: Date.now(),
5731
+ updatedAt: Date.now(),
5732
+ sdkData: typeof sourceTokenInput === "string" ? sourceTokenInput : JSON.stringify(sourceTokenInput)
5733
+ };
5734
+ const nostrTokenId = extractTokenIdFromSdkData(token.sdkData);
5735
+ const nostrStateHash = extractStateHashFromSdkData(token.sdkData);
5736
+ if (nostrTokenId && nostrStateHash && this.isStateTombstoned(nostrTokenId, nostrStateHash)) {
5737
+ this.log(`NOSTR-FIRST: Rejecting tombstoned token ${nostrTokenId.slice(0, 8)}..._${nostrStateHash.slice(0, 8)}...`);
5738
+ return;
5739
+ }
5740
+ this.tokens.set(token.id, token);
5741
+ await this.save();
5742
+ this.log(`NOSTR-FIRST: Token ${token.id.slice(0, 8)}... added as submitted (unconfirmed)`);
5743
+ const incomingTransfer = {
5744
+ id: transfer.id,
5745
+ senderPubkey: transfer.senderTransportPubkey,
5746
+ tokens: [token],
5747
+ memo: payload.memo,
5748
+ receivedAt: transfer.timestamp
5749
+ };
5750
+ this.deps.emitEvent("transfer:incoming", incomingTransfer);
5751
+ try {
5752
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
5753
+ const requestIdBytes = commitment.requestId;
5754
+ const requestIdHex = requestIdBytes instanceof Uint8Array ? Array.from(requestIdBytes).map((b) => b.toString(16).padStart(2, "0")).join("") : String(requestIdBytes);
5755
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
5756
+ if (stClient) {
5757
+ const response = await stClient.submitTransferCommitment(commitment);
5758
+ this.log(`NOSTR-FIRST recipient commitment submit: ${response.status}`);
5759
+ }
5760
+ this.addProofPollingJob({
5761
+ tokenId: token.id,
5762
+ requestIdHex,
5763
+ commitmentJson: JSON.stringify(commitmentInput),
5764
+ startedAt: Date.now(),
5765
+ attemptCount: 0,
5766
+ lastAttemptAt: 0,
5767
+ onProofReceived: async (tokenId) => {
5768
+ await this.finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, transfer.senderTransportPubkey);
5769
+ }
5770
+ });
5771
+ } catch (err) {
5772
+ console.error("[Payments] Failed to parse commitment for proof polling:", err);
5773
+ }
5774
+ } catch (error) {
5775
+ console.error("[Payments] Failed to process NOSTR-FIRST transfer:", error);
4649
5776
  }
4650
- const publicKey = await this.resolveNametagToPublicKey(recipient);
4651
- if (publicKey) {
4652
- this.log(`Resolved "${recipient}" as nametag to 33-byte publicKey for DirectAddress`);
4653
- return this.createDirectAddressFromPubkey(publicKey);
5777
+ }
5778
+ /**
5779
+ * Shared finalization logic for received transfers.
5780
+ * Handles both PROXY (with nametag token + address validation) and DIRECT schemes.
5781
+ */
5782
+ async finalizeTransferToken(sourceToken, transferTx, stClient, trustBase) {
5783
+ const recipientAddress = transferTx.data.recipient;
5784
+ const addressScheme = recipientAddress.scheme;
5785
+ const signingService = await this.createSigningService();
5786
+ const transferSalt = transferTx.data.salt;
5787
+ const recipientPredicate = await import_UnmaskedPredicate5.UnmaskedPredicate.create(
5788
+ sourceToken.id,
5789
+ sourceToken.type,
5790
+ signingService,
5791
+ import_HashAlgorithm5.HashAlgorithm.SHA256,
5792
+ transferSalt
5793
+ );
5794
+ const recipientState = new import_TokenState5.TokenState(recipientPredicate, null);
5795
+ let nametagTokens = [];
5796
+ if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
5797
+ const { ProxyAddress } = await import("@unicitylabs/state-transition-sdk/lib/address/ProxyAddress");
5798
+ if (!this.nametag?.token) {
5799
+ throw new Error("Cannot finalize PROXY transfer - no nametag token");
5800
+ }
5801
+ const nametagToken = await import_Token6.Token.fromJSON(this.nametag.token);
5802
+ const proxy = await ProxyAddress.fromTokenId(nametagToken.id);
5803
+ if (proxy.address !== recipientAddress.address) {
5804
+ throw new Error(
5805
+ `PROXY address mismatch: nametag resolves to ${proxy.address} but transfer targets ${recipientAddress.address}`
5806
+ );
5807
+ }
5808
+ nametagTokens = [nametagToken];
4654
5809
  }
4655
- throw new Error(
4656
- `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.`
5810
+ return stClient.finalizeTransaction(
5811
+ trustBase,
5812
+ sourceToken,
5813
+ recipientState,
5814
+ transferTx,
5815
+ nametagTokens
4657
5816
  );
4658
5817
  }
5818
+ /**
5819
+ * Finalize a received token after proof is available
5820
+ */
5821
+ async finalizeReceivedToken(tokenId, sourceTokenInput, commitmentInput, senderPubkey) {
5822
+ try {
5823
+ const token = this.tokens.get(tokenId);
5824
+ if (!token) {
5825
+ this.log(`Token ${tokenId} not found for finalization`);
5826
+ return;
5827
+ }
5828
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(commitmentInput);
5829
+ if (!this.deps.oracle.waitForProofSdk) {
5830
+ this.log("Cannot finalize - no waitForProofSdk");
5831
+ token.status = "confirmed";
5832
+ token.updatedAt = Date.now();
5833
+ await this.save();
5834
+ return;
5835
+ }
5836
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
5837
+ const transferTx = commitment.toTransaction(inclusionProof);
5838
+ const sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
5839
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
5840
+ const trustBase = this.deps.oracle.getTrustBase?.();
5841
+ if (!stClient || !trustBase) {
5842
+ this.log("Cannot finalize - missing state transition client or trust base");
5843
+ token.status = "confirmed";
5844
+ token.updatedAt = Date.now();
5845
+ await this.save();
5846
+ return;
5847
+ }
5848
+ const finalizedSdkToken = await this.finalizeTransferToken(
5849
+ sourceToken,
5850
+ transferTx,
5851
+ stClient,
5852
+ trustBase
5853
+ );
5854
+ const finalizedToken = {
5855
+ ...token,
5856
+ status: "confirmed",
5857
+ updatedAt: Date.now(),
5858
+ sdkData: JSON.stringify(finalizedSdkToken.toJSON())
5859
+ };
5860
+ this.tokens.set(tokenId, finalizedToken);
5861
+ await this.save();
5862
+ await this.saveTokenToFileStorage(finalizedToken);
5863
+ this.log(`NOSTR-FIRST: Token ${tokenId.slice(0, 8)}... finalized and confirmed`);
5864
+ this.deps.emitEvent("transfer:confirmed", {
5865
+ id: crypto.randomUUID(),
5866
+ status: "completed",
5867
+ tokens: [finalizedToken]
5868
+ });
5869
+ await this.addToHistory({
5870
+ type: "RECEIVED",
5871
+ amount: finalizedToken.amount,
5872
+ coinId: finalizedToken.coinId,
5873
+ symbol: finalizedToken.symbol,
5874
+ timestamp: Date.now(),
5875
+ senderPubkey
5876
+ });
5877
+ } catch (error) {
5878
+ console.error("[Payments] Failed to finalize received token:", error);
5879
+ const token = this.tokens.get(tokenId);
5880
+ if (token && token.status === "submitted") {
5881
+ token.status = "confirmed";
5882
+ token.updatedAt = Date.now();
5883
+ await this.save();
5884
+ }
5885
+ }
5886
+ }
4659
5887
  async handleIncomingTransfer(transfer) {
4660
5888
  try {
4661
5889
  const payload = transfer.payload;
5890
+ if (isInstantSplitBundle(payload)) {
5891
+ this.log("Processing INSTANT_SPLIT bundle...");
5892
+ try {
5893
+ if (!this.nametag) {
5894
+ await this.loadNametagFromFileStorage();
5895
+ }
5896
+ const result = await this.processInstantSplitBundle(
5897
+ payload,
5898
+ transfer.senderTransportPubkey
5899
+ );
5900
+ if (result.success) {
5901
+ this.log("INSTANT_SPLIT processed successfully");
5902
+ } else {
5903
+ console.warn("[Payments] INSTANT_SPLIT processing failed:", result.error);
5904
+ }
5905
+ } catch (err) {
5906
+ console.error("[Payments] INSTANT_SPLIT processing error:", err);
5907
+ }
5908
+ return;
5909
+ }
4662
5910
  let tokenData;
4663
5911
  let finalizedSdkToken = null;
4664
5912
  if (payload.sourceToken && payload.transferTx) {
@@ -4669,82 +5917,71 @@ var PaymentsModule = class {
4669
5917
  console.warn("[Payments] Invalid Sphere wallet transfer format");
4670
5918
  return;
4671
5919
  }
4672
- const sourceToken = await import_Token4.Token.fromJSON(sourceTokenInput);
4673
- const transferTx = await import_TransferTransaction.TransferTransaction.fromJSON(transferTxInput);
4674
- const recipientAddress = transferTx.data.recipient;
4675
- const addressScheme = recipientAddress.scheme;
4676
- if (addressScheme === import_AddressScheme.AddressScheme.PROXY) {
4677
- if (!this.nametag?.token) {
4678
- console.error("[Payments] Cannot finalize PROXY transfer - no nametag token. Token rejected.");
4679
- return;
4680
- }
4681
- {
5920
+ let sourceToken;
5921
+ let transferTx;
5922
+ try {
5923
+ sourceToken = await import_Token6.Token.fromJSON(sourceTokenInput);
5924
+ } catch (err) {
5925
+ console.error("[Payments] Failed to parse sourceToken:", err);
5926
+ return;
5927
+ }
5928
+ try {
5929
+ const hasInclusionProof = transferTxInput.inclusionProof !== void 0;
5930
+ const hasData = transferTxInput.data !== void 0;
5931
+ const hasTransactionData = transferTxInput.transactionData !== void 0;
5932
+ const hasAuthenticator = transferTxInput.authenticator !== void 0;
5933
+ if (hasData && hasInclusionProof) {
5934
+ transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
5935
+ } else if (hasTransactionData && hasAuthenticator) {
5936
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
5937
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
5938
+ if (!stClient) {
5939
+ console.error("[Payments] Cannot process commitment - no state transition client");
5940
+ return;
5941
+ }
5942
+ const response = await stClient.submitTransferCommitment(commitment);
5943
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
5944
+ console.error("[Payments] Transfer commitment submission failed:", response.status);
5945
+ return;
5946
+ }
5947
+ if (!this.deps.oracle.waitForProofSdk) {
5948
+ console.error("[Payments] Cannot wait for proof - missing oracle method");
5949
+ return;
5950
+ }
5951
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
5952
+ transferTx = commitment.toTransaction(inclusionProof);
5953
+ } else {
4682
5954
  try {
4683
- const nametagToken = await import_Token4.Token.fromJSON(this.nametag.token);
4684
- const signingService = await this.createSigningService();
4685
- const transferSalt = transferTx.data.salt;
4686
- const recipientPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
4687
- sourceToken.id,
4688
- sourceToken.type,
4689
- signingService,
4690
- import_HashAlgorithm3.HashAlgorithm.SHA256,
4691
- transferSalt
4692
- );
4693
- const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
5955
+ transferTx = await import_TransferTransaction2.TransferTransaction.fromJSON(transferTxInput);
5956
+ } catch {
5957
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(transferTxInput);
4694
5958
  const stClient = this.deps.oracle.getStateTransitionClient?.();
4695
- const trustBase = this.deps.oracle.getTrustBase?.();
4696
- if (!stClient || !trustBase) {
4697
- console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
4698
- return;
5959
+ if (!stClient || !this.deps.oracle.waitForProofSdk) {
5960
+ throw new Error("Cannot submit commitment - missing oracle methods");
4699
5961
  }
4700
- finalizedSdkToken = await stClient.finalizeTransaction(
4701
- trustBase,
4702
- sourceToken,
4703
- recipientState,
4704
- transferTx,
4705
- [nametagToken]
4706
- );
4707
- tokenData = finalizedSdkToken.toJSON();
4708
- this.log("Token finalized successfully");
4709
- } catch (finalizeError) {
4710
- console.error("[Payments] Finalization failed:", finalizeError);
4711
- return;
5962
+ await stClient.submitTransferCommitment(commitment);
5963
+ const inclusionProof = await this.deps.oracle.waitForProofSdk(commitment);
5964
+ transferTx = commitment.toTransaction(inclusionProof);
4712
5965
  }
4713
5966
  }
4714
- } else {
4715
- this.log("Finalizing DIRECT address transfer for state tracking...");
4716
- try {
4717
- const signingService = await this.createSigningService();
4718
- const transferSalt = transferTx.data.salt;
4719
- const recipientPredicate = await import_UnmaskedPredicate3.UnmaskedPredicate.create(
4720
- sourceToken.id,
4721
- sourceToken.type,
4722
- signingService,
4723
- import_HashAlgorithm3.HashAlgorithm.SHA256,
4724
- transferSalt
4725
- );
4726
- const recipientState = new import_TokenState3.TokenState(recipientPredicate, null);
4727
- const stClient = this.deps.oracle.getStateTransitionClient?.();
4728
- const trustBase = this.deps.oracle.getTrustBase?.();
4729
- if (!stClient || !trustBase) {
4730
- this.log("Cannot finalize DIRECT transfer - missing client, using source token");
4731
- tokenData = sourceTokenInput;
4732
- } else {
4733
- finalizedSdkToken = await stClient.finalizeTransaction(
4734
- trustBase,
4735
- sourceToken,
4736
- recipientState,
4737
- transferTx,
4738
- []
4739
- // No nametag tokens needed for DIRECT
4740
- );
4741
- tokenData = finalizedSdkToken.toJSON();
4742
- this.log("DIRECT transfer finalized successfully");
4743
- }
4744
- } catch (finalizeError) {
4745
- this.log("DIRECT finalization failed, using source token:", finalizeError);
4746
- tokenData = sourceTokenInput;
5967
+ } catch (err) {
5968
+ console.error("[Payments] Failed to parse transferTx:", err);
5969
+ return;
5970
+ }
5971
+ try {
5972
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
5973
+ const trustBase = this.deps.oracle.getTrustBase?.();
5974
+ if (!stClient || !trustBase) {
5975
+ console.error("[Payments] Cannot finalize - missing state transition client or trust base. Token rejected.");
5976
+ return;
4747
5977
  }
5978
+ finalizedSdkToken = await this.finalizeTransferToken(sourceToken, transferTx, stClient, trustBase);
5979
+ tokenData = finalizedSdkToken.toJSON();
5980
+ const addressScheme = transferTx.data.recipient.scheme;
5981
+ this.log(`${addressScheme === import_AddressScheme.AddressScheme.PROXY ? "PROXY" : "DIRECT"} finalization successful`);
5982
+ } catch (finalizeError) {
5983
+ console.error(`[Payments] Finalization FAILED - token rejected:`, finalizeError);
5984
+ return;
4748
5985
  }
4749
5986
  } else if (payload.token) {
4750
5987
  tokenData = payload.token;
@@ -4771,12 +6008,6 @@ var PaymentsModule = class {
4771
6008
  updatedAt: Date.now(),
4772
6009
  sdkData: typeof tokenData === "string" ? tokenData : JSON.stringify(tokenData)
4773
6010
  };
4774
- const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
4775
- const stateHash = extractStateHashFromSdkData(token.sdkData);
4776
- if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
4777
- this.log(`Rejected tombstoned token ${sdkTokenId.slice(0, 8)}...`);
4778
- return;
4779
- }
4780
6011
  await this.addToken(token);
4781
6012
  const incomingTransfer = {
4782
6013
  id: transfer.id,
@@ -4864,14 +6095,159 @@ var PaymentsModule = class {
4864
6095
  }
4865
6096
  loadFromStorageData(data) {
4866
6097
  const parsed = parseTxfStorageData(data);
6098
+ this.tombstones = parsed.tombstones;
4867
6099
  this.tokens.clear();
4868
6100
  for (const token of parsed.tokens) {
6101
+ const sdkTokenId = extractTokenIdFromSdkData(token.sdkData);
6102
+ const stateHash = extractStateHashFromSdkData(token.sdkData);
6103
+ if (sdkTokenId && stateHash && this.isStateTombstoned(sdkTokenId, stateHash)) {
6104
+ this.log(`Skipping tombstoned token ${sdkTokenId.slice(0, 8)}... during load (exact state match)`);
6105
+ continue;
6106
+ }
4869
6107
  this.tokens.set(token.id, token);
4870
6108
  }
4871
- this.tombstones = parsed.tombstones;
4872
6109
  this.archivedTokens = parsed.archivedTokens;
4873
6110
  this.forkedTokens = parsed.forkedTokens;
4874
- this.nametag = parsed.nametag;
6111
+ if (parsed.nametag !== null) {
6112
+ this.nametag = parsed.nametag;
6113
+ }
6114
+ }
6115
+ // ===========================================================================
6116
+ // Private: NOSTR-FIRST Proof Polling
6117
+ // ===========================================================================
6118
+ /**
6119
+ * Submit commitment to aggregator and start background proof polling
6120
+ * (NOSTR-FIRST pattern: fire-and-forget submission)
6121
+ */
6122
+ async submitAndPollForProof(tokenId, commitment, requestIdHex, onProofReceived) {
6123
+ try {
6124
+ const stClient = this.deps.oracle.getStateTransitionClient?.();
6125
+ if (!stClient) {
6126
+ this.log("Cannot submit commitment - no state transition client");
6127
+ return;
6128
+ }
6129
+ const response = await stClient.submitTransferCommitment(commitment);
6130
+ if (response.status !== "SUCCESS" && response.status !== "REQUEST_ID_EXISTS") {
6131
+ this.log(`Transfer commitment submission failed: ${response.status}`);
6132
+ const token = this.tokens.get(tokenId);
6133
+ if (token) {
6134
+ token.status = "invalid";
6135
+ token.updatedAt = Date.now();
6136
+ this.tokens.set(tokenId, token);
6137
+ await this.save();
6138
+ }
6139
+ return;
6140
+ }
6141
+ this.addProofPollingJob({
6142
+ tokenId,
6143
+ requestIdHex,
6144
+ commitmentJson: JSON.stringify(commitment.toJSON()),
6145
+ startedAt: Date.now(),
6146
+ attemptCount: 0,
6147
+ lastAttemptAt: 0,
6148
+ onProofReceived
6149
+ });
6150
+ } catch (error) {
6151
+ this.log("submitAndPollForProof error:", error);
6152
+ }
6153
+ }
6154
+ /**
6155
+ * Add a proof polling job to the queue
6156
+ */
6157
+ addProofPollingJob(job) {
6158
+ this.proofPollingJobs.set(job.tokenId, job);
6159
+ this.log(`Added proof polling job for token ${job.tokenId.slice(0, 8)}...`);
6160
+ this.startProofPolling();
6161
+ }
6162
+ /**
6163
+ * Start the proof polling interval if not already running
6164
+ */
6165
+ startProofPolling() {
6166
+ if (this.proofPollingInterval) return;
6167
+ if (this.proofPollingJobs.size === 0) return;
6168
+ this.log("Starting proof polling...");
6169
+ this.proofPollingInterval = setInterval(
6170
+ () => this.processProofPollingQueue(),
6171
+ _PaymentsModule.PROOF_POLLING_INTERVAL_MS
6172
+ );
6173
+ }
6174
+ /**
6175
+ * Stop the proof polling interval
6176
+ */
6177
+ stopProofPolling() {
6178
+ if (this.proofPollingInterval) {
6179
+ clearInterval(this.proofPollingInterval);
6180
+ this.proofPollingInterval = null;
6181
+ this.log("Stopped proof polling");
6182
+ }
6183
+ }
6184
+ /**
6185
+ * Process all pending proof polling jobs
6186
+ */
6187
+ async processProofPollingQueue() {
6188
+ if (this.proofPollingJobs.size === 0) {
6189
+ this.stopProofPolling();
6190
+ return;
6191
+ }
6192
+ const completedJobs = [];
6193
+ for (const [tokenId, job] of this.proofPollingJobs) {
6194
+ try {
6195
+ job.attemptCount++;
6196
+ job.lastAttemptAt = Date.now();
6197
+ if (job.attemptCount >= _PaymentsModule.PROOF_POLLING_MAX_ATTEMPTS) {
6198
+ this.log(`Proof polling timeout for token ${tokenId.slice(0, 8)}...`);
6199
+ const token2 = this.tokens.get(tokenId);
6200
+ if (token2 && token2.status === "submitted") {
6201
+ token2.status = "invalid";
6202
+ token2.updatedAt = Date.now();
6203
+ this.tokens.set(tokenId, token2);
6204
+ }
6205
+ completedJobs.push(tokenId);
6206
+ continue;
6207
+ }
6208
+ const commitment = await import_TransferCommitment4.TransferCommitment.fromJSON(JSON.parse(job.commitmentJson));
6209
+ let inclusionProof = null;
6210
+ try {
6211
+ const abortController = new AbortController();
6212
+ const timeoutId = setTimeout(() => abortController.abort(), 500);
6213
+ if (this.deps.oracle.waitForProofSdk) {
6214
+ inclusionProof = await Promise.race([
6215
+ this.deps.oracle.waitForProofSdk(commitment, abortController.signal),
6216
+ new Promise((resolve) => setTimeout(() => resolve(null), 500))
6217
+ ]);
6218
+ } else {
6219
+ const proof = await this.deps.oracle.getProof(job.requestIdHex);
6220
+ if (proof) {
6221
+ inclusionProof = proof;
6222
+ }
6223
+ }
6224
+ clearTimeout(timeoutId);
6225
+ } catch (err) {
6226
+ continue;
6227
+ }
6228
+ if (!inclusionProof) {
6229
+ continue;
6230
+ }
6231
+ const token = this.tokens.get(tokenId);
6232
+ if (token) {
6233
+ token.status = "spent";
6234
+ token.updatedAt = Date.now();
6235
+ this.tokens.set(tokenId, token);
6236
+ await this.save();
6237
+ this.log(`Proof received for token ${tokenId.slice(0, 8)}..., status: spent`);
6238
+ }
6239
+ job.onProofReceived?.(tokenId);
6240
+ completedJobs.push(tokenId);
6241
+ } catch (error) {
6242
+ this.log(`Proof polling attempt ${job.attemptCount} for ${tokenId.slice(0, 8)}...: ${error}`);
6243
+ }
6244
+ }
6245
+ for (const tokenId of completedJobs) {
6246
+ this.proofPollingJobs.delete(tokenId);
6247
+ }
6248
+ if (this.proofPollingJobs.size === 0) {
6249
+ this.stopProofPolling();
6250
+ }
4875
6251
  }
4876
6252
  // ===========================================================================
4877
6253
  // Private: Helpers
@@ -4886,6 +6262,14 @@ function createPaymentsModule(config) {
4886
6262
  return new PaymentsModule(config);
4887
6263
  }
4888
6264
 
6265
+ // modules/payments/TokenRecoveryService.ts
6266
+ var import_TokenId4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenId");
6267
+ var import_TokenState6 = require("@unicitylabs/state-transition-sdk/lib/token/TokenState");
6268
+ var import_TokenType3 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
6269
+ var import_CoinId5 = require("@unicitylabs/state-transition-sdk/lib/token/fungible/CoinId");
6270
+ var import_HashAlgorithm6 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
6271
+ var import_UnmaskedPredicate6 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicate");
6272
+
4889
6273
  // modules/communications/CommunicationsModule.ts
4890
6274
  var CommunicationsModule = class {
4891
6275
  config;
@@ -5939,20 +7323,20 @@ async function parseAndDecryptWalletDat(data, password, onProgress) {
5939
7323
 
5940
7324
  // core/Sphere.ts
5941
7325
  var import_SigningService2 = require("@unicitylabs/state-transition-sdk/lib/sign/SigningService");
5942
- var import_TokenType2 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
5943
- var import_HashAlgorithm4 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
5944
- var import_UnmaskedPredicateReference2 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
7326
+ var import_TokenType4 = require("@unicitylabs/state-transition-sdk/lib/token/TokenType");
7327
+ var import_HashAlgorithm7 = require("@unicitylabs/state-transition-sdk/lib/hash/HashAlgorithm");
7328
+ var import_UnmaskedPredicateReference3 = require("@unicitylabs/state-transition-sdk/lib/predicate/embedded/UnmaskedPredicateReference");
5945
7329
  var UNICITY_TOKEN_TYPE_HEX2 = "f8aa13834268d29355ff12183066f0cb902003629bbc5eb9ef0efbe397867509";
5946
7330
  async function deriveL3PredicateAddress(privateKey) {
5947
7331
  const secret = Buffer.from(privateKey, "hex");
5948
7332
  const signingService = await import_SigningService2.SigningService.createFromSecret(secret);
5949
7333
  const tokenTypeBytes = Buffer.from(UNICITY_TOKEN_TYPE_HEX2, "hex");
5950
- const tokenType = new import_TokenType2.TokenType(tokenTypeBytes);
5951
- const predicateRef = import_UnmaskedPredicateReference2.UnmaskedPredicateReference.create(
7334
+ const tokenType = new import_TokenType4.TokenType(tokenTypeBytes);
7335
+ const predicateRef = import_UnmaskedPredicateReference3.UnmaskedPredicateReference.create(
5952
7336
  tokenType,
5953
7337
  signingService.algorithm,
5954
7338
  signingService.publicKey,
5955
- import_HashAlgorithm4.HashAlgorithm.SHA256
7339
+ import_HashAlgorithm7.HashAlgorithm.SHA256
5956
7340
  );
5957
7341
  return (await (await predicateRef).toAddress()).toString();
5958
7342
  }
@@ -5968,7 +7352,11 @@ var Sphere = class _Sphere {
5968
7352
  _derivationMode = "bip32";
5969
7353
  _basePath = DEFAULT_BASE_PATH;
5970
7354
  _currentAddressIndex = 0;
5971
- /** Map of addressId -> (nametagIndex -> nametag). Supports multiple nametags per address (e.g., from Nostr recovery) */
7355
+ /** Registry of all tracked (activated) addresses, keyed by HD index */
7356
+ _trackedAddresses = /* @__PURE__ */ new Map();
7357
+ /** Reverse lookup: addressId -> HD index */
7358
+ _addressIdToIndex = /* @__PURE__ */ new Map();
7359
+ /** Nametag cache: addressId -> (nametagIndex -> nametag). Separate from tracked addresses. */
5972
7360
  _addressNametags = /* @__PURE__ */ new Map();
5973
7361
  /** Cached PROXY address (computed once when nametag is set) */
5974
7362
  _cachedProxyAddress = void 0;
@@ -5977,6 +7365,7 @@ var Sphere = class _Sphere {
5977
7365
  _tokenStorageProviders = /* @__PURE__ */ new Map();
5978
7366
  _transport;
5979
7367
  _oracle;
7368
+ _priceProvider;
5980
7369
  // Modules
5981
7370
  _payments;
5982
7371
  _communications;
@@ -5985,10 +7374,11 @@ var Sphere = class _Sphere {
5985
7374
  // ===========================================================================
5986
7375
  // Constructor (private)
5987
7376
  // ===========================================================================
5988
- constructor(storage, transport, oracle, tokenStorage, l1Config) {
7377
+ constructor(storage, transport, oracle, tokenStorage, l1Config, priceProvider) {
5989
7378
  this._storage = storage;
5990
7379
  this._transport = transport;
5991
7380
  this._oracle = oracle;
7381
+ this._priceProvider = priceProvider ?? null;
5992
7382
  if (tokenStorage) {
5993
7383
  this._tokenStorageProviders.set(tokenStorage.id, tokenStorage);
5994
7384
  }
@@ -6048,7 +7438,8 @@ var Sphere = class _Sphere {
6048
7438
  transport: options.transport,
6049
7439
  oracle: options.oracle,
6050
7440
  tokenStorage: options.tokenStorage,
6051
- l1: options.l1
7441
+ l1: options.l1,
7442
+ price: options.price
6052
7443
  });
6053
7444
  return { sphere: sphere2, created: false };
6054
7445
  }
@@ -6072,7 +7463,8 @@ var Sphere = class _Sphere {
6072
7463
  tokenStorage: options.tokenStorage,
6073
7464
  derivationPath: options.derivationPath,
6074
7465
  nametag: options.nametag,
6075
- l1: options.l1
7466
+ l1: options.l1,
7467
+ price: options.price
6076
7468
  });
6077
7469
  return { sphere, created: true, generatedMnemonic };
6078
7470
  }
@@ -6091,7 +7483,8 @@ var Sphere = class _Sphere {
6091
7483
  options.transport,
6092
7484
  options.oracle,
6093
7485
  options.tokenStorage,
6094
- options.l1
7486
+ options.l1,
7487
+ options.price
6095
7488
  );
6096
7489
  await sphere.storeMnemonic(options.mnemonic, options.derivationPath);
6097
7490
  await sphere.initializeIdentityFromMnemonic(options.mnemonic, options.derivationPath);
@@ -6100,10 +7493,12 @@ var Sphere = class _Sphere {
6100
7493
  await sphere.finalizeWalletCreation();
6101
7494
  sphere._initialized = true;
6102
7495
  _Sphere.instance = sphere;
7496
+ await sphere.ensureAddressTracked(0);
6103
7497
  if (options.nametag) {
6104
7498
  await sphere.registerNametag(options.nametag);
6105
7499
  } else {
6106
- await sphere.recoverNametagFromNostr();
7500
+ await sphere.syncIdentityWithTransport();
7501
+ await sphere.recoverNametagFromTransport();
6107
7502
  }
6108
7503
  return sphere;
6109
7504
  }
@@ -6119,14 +7514,28 @@ var Sphere = class _Sphere {
6119
7514
  options.transport,
6120
7515
  options.oracle,
6121
7516
  options.tokenStorage,
6122
- options.l1
7517
+ options.l1,
7518
+ options.price
6123
7519
  );
6124
7520
  await sphere.loadIdentityFromStorage();
6125
7521
  await sphere.initializeProviders();
6126
7522
  await sphere.initializeModules();
6127
- await sphere.syncNametagWithNostr();
7523
+ await sphere.syncIdentityWithTransport();
6128
7524
  sphere._initialized = true;
6129
7525
  _Sphere.instance = sphere;
7526
+ if (sphere._identity?.nametag && !sphere._payments.hasNametag()) {
7527
+ console.log(`[Sphere] Nametag @${sphere._identity.nametag} has no token, attempting to mint...`);
7528
+ try {
7529
+ const result = await sphere.mintNametag(sphere._identity.nametag);
7530
+ if (result.success) {
7531
+ console.log(`[Sphere] Nametag token minted successfully on load`);
7532
+ } else {
7533
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
7534
+ }
7535
+ } catch (err) {
7536
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
7537
+ }
7538
+ }
6130
7539
  return sphere;
6131
7540
  }
6132
7541
  /**
@@ -6142,7 +7551,8 @@ var Sphere = class _Sphere {
6142
7551
  options.transport,
6143
7552
  options.oracle,
6144
7553
  options.tokenStorage,
6145
- options.l1
7554
+ options.l1,
7555
+ options.price
6146
7556
  );
6147
7557
  if (options.mnemonic) {
6148
7558
  if (!_Sphere.validateMnemonic(options.mnemonic)) {
@@ -6167,11 +7577,12 @@ var Sphere = class _Sphere {
6167
7577
  await sphere.initializeProviders();
6168
7578
  await sphere.initializeModules();
6169
7579
  if (!options.nametag) {
6170
- await sphere.recoverNametagFromNostr();
7580
+ await sphere.recoverNametagFromTransport();
6171
7581
  }
6172
7582
  await sphere.finalizeWalletCreation();
6173
7583
  sphere._initialized = true;
6174
7584
  _Sphere.instance = sphere;
7585
+ await sphere.ensureAddressTracked(0);
6175
7586
  if (options.nametag) {
6176
7587
  await sphere.registerNametag(options.nametag);
6177
7588
  }
@@ -6207,6 +7618,7 @@ var Sphere = class _Sphere {
6207
7618
  await storage.remove(STORAGE_KEYS_GLOBAL.DERIVATION_MODE);
6208
7619
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_SOURCE);
6209
7620
  await storage.remove(STORAGE_KEYS_GLOBAL.WALLET_EXISTS);
7621
+ await storage.remove(STORAGE_KEYS_GLOBAL.TRACKED_ADDRESSES);
6210
7622
  await storage.remove(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
6211
7623
  await storage.remove(STORAGE_KEYS_ADDRESS.PENDING_TRANSFERS);
6212
7624
  await storage.remove(STORAGE_KEYS_ADDRESS.OUTBOX);
@@ -6331,6 +7743,13 @@ var Sphere = class _Sphere {
6331
7743
  hasTokenStorageProvider(providerId) {
6332
7744
  return this._tokenStorageProviders.has(providerId);
6333
7745
  }
7746
+ /**
7747
+ * Set or update the price provider after initialization
7748
+ */
7749
+ setPriceProvider(provider) {
7750
+ this._priceProvider = provider;
7751
+ this._payments.setPriceProvider(provider);
7752
+ }
6334
7753
  getTransport() {
6335
7754
  return this._transport;
6336
7755
  }
@@ -6818,10 +8237,9 @@ var Sphere = class _Sphere {
6818
8237
  * @returns Primary nametag (index 0) or undefined if not registered
6819
8238
  */
6820
8239
  getNametagForAddress(addressId) {
6821
- const id = addressId ?? this.getCurrentAddressId();
8240
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
6822
8241
  if (!id) return void 0;
6823
- const nametagsMap = this._addressNametags.get(id);
6824
- return nametagsMap?.get(0);
8242
+ return this._addressNametags.get(id)?.get(0);
6825
8243
  }
6826
8244
  /**
6827
8245
  * Get all nametags for a specific address
@@ -6830,29 +8248,89 @@ var Sphere = class _Sphere {
6830
8248
  * @returns Map of nametagIndex to nametag, or undefined if no nametags
6831
8249
  */
6832
8250
  getNametagsForAddress(addressId) {
6833
- const id = addressId ?? this.getCurrentAddressId();
8251
+ const id = addressId ?? this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
6834
8252
  if (!id) return void 0;
6835
- const nametagsMap = this._addressNametags.get(id);
6836
- return nametagsMap ? new Map(nametagsMap) : void 0;
8253
+ const nametags = this._addressNametags.get(id);
8254
+ return nametags && nametags.size > 0 ? new Map(nametags) : void 0;
6837
8255
  }
6838
8256
  /**
6839
8257
  * Get all registered address nametags
6840
- *
8258
+ * @deprecated Use getActiveAddresses() or getAllTrackedAddresses() instead
6841
8259
  * @returns Map of addressId to (nametagIndex -> nametag)
6842
8260
  */
6843
8261
  getAllAddressNametags() {
6844
8262
  const result = /* @__PURE__ */ new Map();
6845
- this._addressNametags.forEach((nametagsMap, addressId) => {
6846
- result.set(addressId, new Map(nametagsMap));
6847
- });
8263
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
8264
+ if (nametags.size > 0) {
8265
+ result.set(addressId, new Map(nametags));
8266
+ }
8267
+ }
6848
8268
  return result;
6849
8269
  }
6850
8270
  /**
6851
- * Get current address identifier (DIRECT://xxx format)
8271
+ * Get all active (non-hidden) tracked addresses.
8272
+ * Returns addresses that have been activated through create, switchToAddress,
8273
+ * registerNametag, or nametag recovery.
8274
+ *
8275
+ * @returns Array of TrackedAddress entries sorted by index, excluding hidden ones
8276
+ */
8277
+ getActiveAddresses() {
8278
+ this.ensureReady();
8279
+ const result = [];
8280
+ for (const entry of this._trackedAddresses.values()) {
8281
+ if (!entry.hidden) {
8282
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8283
+ result.push({ ...entry, nametag });
8284
+ }
8285
+ }
8286
+ return result.sort((a, b) => a.index - b.index);
8287
+ }
8288
+ /**
8289
+ * Get all tracked addresses, including hidden ones.
8290
+ *
8291
+ * @returns Array of all TrackedAddress entries sorted by index
8292
+ */
8293
+ getAllTrackedAddresses() {
8294
+ this.ensureReady();
8295
+ const result = [];
8296
+ for (const entry of this._trackedAddresses.values()) {
8297
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8298
+ result.push({ ...entry, nametag });
8299
+ }
8300
+ return result.sort((a, b) => a.index - b.index);
8301
+ }
8302
+ /**
8303
+ * Get tracked address info by index.
8304
+ *
8305
+ * @param index - Address index
8306
+ * @returns TrackedAddress or undefined if not tracked
8307
+ */
8308
+ getTrackedAddress(index) {
8309
+ this.ensureReady();
8310
+ const entry = this._trackedAddresses.get(index);
8311
+ if (!entry) return void 0;
8312
+ const nametag = this._addressNametags.get(entry.addressId)?.get(0);
8313
+ return { ...entry, nametag };
8314
+ }
8315
+ /**
8316
+ * Set visibility of a tracked address.
8317
+ * Hidden addresses are not returned by getActiveAddresses() but remain tracked.
8318
+ *
8319
+ * @param index - Address index to hide/unhide
8320
+ * @param hidden - true to hide, false to show
8321
+ * @throws Error if address index is not tracked
6852
8322
  */
6853
- getCurrentAddressId() {
6854
- if (!this._identity?.directAddress) return void 0;
6855
- return getAddressId(this._identity.directAddress);
8323
+ async setAddressHidden(index, hidden) {
8324
+ this.ensureReady();
8325
+ const entry = this._trackedAddresses.get(index);
8326
+ if (!entry) {
8327
+ throw new Error(`Address at index ${index} is not tracked. Switch to it first.`);
8328
+ }
8329
+ if (entry.hidden === hidden) return;
8330
+ entry.hidden = hidden;
8331
+ await this.persistTrackedAddresses();
8332
+ const eventType = hidden ? "address:hidden" : "address:unhidden";
8333
+ this.emitEvent(eventType, { index, addressId: entry.addressId });
6856
8334
  }
6857
8335
  /**
6858
8336
  * Switch to a different address by index
@@ -6873,7 +8351,7 @@ var Sphere = class _Sphere {
6873
8351
  * await sphere.switchToAddress(0);
6874
8352
  * ```
6875
8353
  */
6876
- async switchToAddress(index) {
8354
+ async switchToAddress(index, options) {
6877
8355
  this.ensureReady();
6878
8356
  if (!this._masterKey) {
6879
8357
  throw new Error("HD derivation requires master key with chain code. Cannot switch addresses.");
@@ -6881,12 +8359,28 @@ var Sphere = class _Sphere {
6881
8359
  if (index < 0) {
6882
8360
  throw new Error("Address index must be non-negative");
6883
8361
  }
8362
+ const newNametag = options?.nametag?.startsWith("@") ? options.nametag.slice(1) : options?.nametag;
8363
+ if (newNametag && !this.validateNametag(newNametag)) {
8364
+ throw new Error("Invalid nametag format. Use alphanumeric characters, 3-20 chars.");
8365
+ }
6884
8366
  const addressInfo = this.deriveAddress(index, false);
6885
8367
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
6886
8368
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
8369
+ await this.ensureAddressTracked(index);
6887
8370
  const addressId = getAddressId(predicateAddress);
6888
- const nametagsMap = this._addressNametags.get(addressId);
6889
- const nametag = nametagsMap?.get(0);
8371
+ if (newNametag) {
8372
+ const existing = await this._transport.resolveNametag?.(newNametag);
8373
+ if (existing) {
8374
+ throw new Error(`Nametag @${newNametag} is already taken`);
8375
+ }
8376
+ let nametags = this._addressNametags.get(addressId);
8377
+ if (!nametags) {
8378
+ nametags = /* @__PURE__ */ new Map();
8379
+ this._addressNametags.set(addressId, nametags);
8380
+ }
8381
+ nametags.set(0, newNametag);
8382
+ }
8383
+ const nametag = this._addressNametags.get(addressId)?.get(0);
6890
8384
  this._identity = {
6891
8385
  privateKey: addressInfo.privateKey,
6892
8386
  chainPubkey: addressInfo.publicKey,
@@ -6899,11 +8393,47 @@ var Sphere = class _Sphere {
6899
8393
  await this._updateCachedProxyAddress();
6900
8394
  await this._storage.set(STORAGE_KEYS_GLOBAL.CURRENT_ADDRESS_INDEX, index.toString());
6901
8395
  this._storage.setIdentity(this._identity);
6902
- this._transport.setIdentity(this._identity);
8396
+ await this._transport.setIdentity(this._identity);
6903
8397
  for (const provider of this._tokenStorageProviders.values()) {
6904
8398
  provider.setIdentity(this._identity);
8399
+ await provider.initialize();
6905
8400
  }
6906
8401
  await this.reinitializeModulesForNewAddress();
8402
+ if (this._identity.nametag) {
8403
+ await this.syncIdentityWithTransport();
8404
+ }
8405
+ if (newNametag) {
8406
+ await this.persistAddressNametags();
8407
+ if (!this._payments.hasNametag()) {
8408
+ console.log(`[Sphere] Minting nametag token for @${newNametag}...`);
8409
+ try {
8410
+ const result = await this.mintNametag(newNametag);
8411
+ if (result.success) {
8412
+ console.log(`[Sphere] Nametag token minted successfully`);
8413
+ } else {
8414
+ console.warn(`[Sphere] Could not mint nametag token: ${result.error}`);
8415
+ }
8416
+ } catch (err) {
8417
+ console.warn(`[Sphere] Nametag token mint failed:`, err);
8418
+ }
8419
+ }
8420
+ this.emitEvent("nametag:registered", {
8421
+ nametag: newNametag,
8422
+ addressIndex: index
8423
+ });
8424
+ } else if (this._identity.nametag && !this._payments.hasNametag()) {
8425
+ console.log(`[Sphere] Nametag @${this._identity.nametag} has no token after switch, minting...`);
8426
+ try {
8427
+ const result = await this.mintNametag(this._identity.nametag);
8428
+ if (result.success) {
8429
+ console.log(`[Sphere] Nametag token minted successfully after switch`);
8430
+ } else {
8431
+ console.warn(`[Sphere] Could not mint nametag token after switch: ${result.error}`);
8432
+ }
8433
+ } catch (err) {
8434
+ console.warn(`[Sphere] Nametag token mint failed after switch:`, err);
8435
+ }
8436
+ }
6907
8437
  this.emitEvent("identity:changed", {
6908
8438
  l1Address: this._identity.l1Address,
6909
8439
  directAddress: this._identity.directAddress,
@@ -6925,7 +8455,8 @@ var Sphere = class _Sphere {
6925
8455
  transport: this._transport,
6926
8456
  oracle: this._oracle,
6927
8457
  emitEvent,
6928
- chainCode: this._masterKey?.chainCode
8458
+ chainCode: this._masterKey?.chainCode,
8459
+ price: this._priceProvider ?? void 0
6929
8460
  });
6930
8461
  this._communications.initialize({
6931
8462
  identity: this._identity,
@@ -6958,6 +8489,14 @@ var Sphere = class _Sphere {
6958
8489
  */
6959
8490
  deriveAddress(index, isChange = false) {
6960
8491
  this.ensureReady();
8492
+ return this._deriveAddressInternal(index, isChange);
8493
+ }
8494
+ /**
8495
+ * Internal address derivation without ensureReady() check.
8496
+ * Used during initialization (loadTrackedAddresses, ensureAddressTracked)
8497
+ * when _initialized is still false.
8498
+ */
8499
+ _deriveAddressInternal(index, isChange = false) {
6961
8500
  if (!this._masterKey) {
6962
8501
  throw new Error("HD derivation requires master key with chain code");
6963
8502
  }
@@ -7096,6 +8635,22 @@ var Sphere = class _Sphere {
7096
8635
  getProxyAddress() {
7097
8636
  return this._cachedProxyAddress;
7098
8637
  }
8638
+ /**
8639
+ * Resolve any identifier to full peer information.
8640
+ * Accepts @nametag, bare nametag, DIRECT://, PROXY://, L1 address, or transport pubkey.
8641
+ *
8642
+ * @example
8643
+ * ```ts
8644
+ * const peer = await sphere.resolve('@alice');
8645
+ * const peer = await sphere.resolve('DIRECT://...');
8646
+ * const peer = await sphere.resolve('alpha1...');
8647
+ * const peer = await sphere.resolve('ab12cd...'); // 64-char hex transport pubkey
8648
+ * ```
8649
+ */
8650
+ async resolve(identifier) {
8651
+ this.ensureReady();
8652
+ return this._transport.resolve?.(identifier) ?? null;
8653
+ }
7099
8654
  /** Compute and cache the PROXY address from the current nametag */
7100
8655
  async _updateCachedProxyAddress() {
7101
8656
  const nametag = this._identity?.nametag;
@@ -7134,11 +8689,12 @@ var Sphere = class _Sphere {
7134
8689
  if (this._identity?.nametag) {
7135
8690
  throw new Error(`Nametag already registered for address ${this._currentAddressIndex}: @${this._identity.nametag}`);
7136
8691
  }
7137
- if (this._transport.registerNametag) {
7138
- const success = await this._transport.registerNametag(
7139
- cleanNametag,
8692
+ if (this._transport.publishIdentityBinding) {
8693
+ const success = await this._transport.publishIdentityBinding(
7140
8694
  this._identity.chainPubkey,
7141
- this._identity.directAddress || ""
8695
+ this._identity.l1Address,
8696
+ this._identity.directAddress || "",
8697
+ cleanNametag
7142
8698
  );
7143
8699
  if (!success) {
7144
8700
  throw new Error("Failed to register nametag. It may already be taken.");
@@ -7146,14 +8702,14 @@ var Sphere = class _Sphere {
7146
8702
  }
7147
8703
  this._identity.nametag = cleanNametag;
7148
8704
  await this._updateCachedProxyAddress();
7149
- const addressId = this.getCurrentAddressId();
7150
- if (addressId) {
7151
- let nametagsMap = this._addressNametags.get(addressId);
7152
- if (!nametagsMap) {
7153
- nametagsMap = /* @__PURE__ */ new Map();
7154
- this._addressNametags.set(addressId, nametagsMap);
8705
+ const currentAddressId = this._trackedAddresses.get(this._currentAddressIndex)?.addressId;
8706
+ if (currentAddressId) {
8707
+ let nametags = this._addressNametags.get(currentAddressId);
8708
+ if (!nametags) {
8709
+ nametags = /* @__PURE__ */ new Map();
8710
+ this._addressNametags.set(currentAddressId, nametags);
7155
8711
  }
7156
- nametagsMap.set(0, cleanNametag);
8712
+ nametags.set(0, cleanNametag);
7157
8713
  }
7158
8714
  await this.persistAddressNametags();
7159
8715
  if (!this._payments.hasNametag()) {
@@ -7172,19 +8728,19 @@ var Sphere = class _Sphere {
7172
8728
  console.log(`[Sphere] Nametag registered for address ${this._currentAddressIndex}:`, cleanNametag);
7173
8729
  }
7174
8730
  /**
7175
- * Persist address nametags to storage
7176
- * Format: { "DIRECT://abc...xyz": { "0": "alice", "1": "alice2" }, ... }
8731
+ * Persist tracked addresses to storage (only minimal fields via StorageProvider)
7177
8732
  */
7178
- async persistAddressNametags() {
7179
- const result = {};
7180
- this._addressNametags.forEach((nametagsMap, addressId) => {
7181
- const innerObj = {};
7182
- nametagsMap.forEach((nametag, index) => {
7183
- innerObj[index.toString()] = nametag;
8733
+ async persistTrackedAddresses() {
8734
+ const entries = [];
8735
+ for (const entry of this._trackedAddresses.values()) {
8736
+ entries.push({
8737
+ index: entry.index,
8738
+ hidden: entry.hidden,
8739
+ createdAt: entry.createdAt,
8740
+ updatedAt: entry.updatedAt
7184
8741
  });
7185
- result[addressId] = innerObj;
7186
- });
7187
- await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
8742
+ }
8743
+ await this._storage.saveTrackedAddresses(entries);
7188
8744
  }
7189
8745
  /**
7190
8746
  * Mint a nametag token on-chain (like Sphere wallet and lottery)
@@ -7218,63 +8774,184 @@ var Sphere = class _Sphere {
7218
8774
  return this._payments.isNametagAvailable(nametag);
7219
8775
  }
7220
8776
  /**
7221
- * Load address nametags from storage
7222
- * Supports new format: { "DIRECT://abc...xyz": { "0": "alice" } }
7223
- * And legacy format: { "0": "alice" } (migrates to new format on save)
8777
+ * Load tracked addresses from storage.
8778
+ * Falls back to migrating from old ADDRESS_NAMETAGS format.
7224
8779
  */
7225
- async loadAddressNametags() {
8780
+ async loadTrackedAddresses() {
8781
+ this._trackedAddresses.clear();
8782
+ this._addressIdToIndex.clear();
7226
8783
  try {
7227
- const saved = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
7228
- if (saved) {
7229
- const parsed = JSON.parse(saved);
7230
- this._addressNametags.clear();
7231
- for (const [key, value] of Object.entries(parsed)) {
7232
- if (typeof value === "object" && value !== null) {
7233
- const nametagsMap = /* @__PURE__ */ new Map();
7234
- for (const [indexStr, nametag] of Object.entries(value)) {
7235
- nametagsMap.set(parseInt(indexStr, 10), nametag);
7236
- }
7237
- this._addressNametags.set(key, nametagsMap);
7238
- } else if (typeof value === "string") {
7239
- }
8784
+ const entries = await this._storage.loadTrackedAddresses();
8785
+ if (entries.length > 0) {
8786
+ for (const stored of entries) {
8787
+ const addrInfo = this._deriveAddressInternal(stored.index, false);
8788
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
8789
+ const addressId = getAddressId(directAddress);
8790
+ const entry = {
8791
+ ...stored,
8792
+ addressId,
8793
+ l1Address: addrInfo.address,
8794
+ directAddress,
8795
+ chainPubkey: addrInfo.publicKey
8796
+ };
8797
+ this._trackedAddresses.set(entry.index, entry);
8798
+ this._addressIdToIndex.set(addressId, entry.index);
7240
8799
  }
8800
+ return;
8801
+ }
8802
+ const oldData = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
8803
+ if (oldData) {
8804
+ const parsed = JSON.parse(oldData);
8805
+ await this.migrateFromOldNametagFormat(parsed);
8806
+ await this.persistTrackedAddresses();
7241
8807
  }
7242
8808
  } catch {
7243
8809
  }
7244
8810
  }
7245
8811
  /**
7246
- * Sync nametag with Nostr on wallet load
7247
- * If local nametag exists but not registered on Nostr, re-register it
8812
+ * Migrate from old ADDRESS_NAMETAGS format to tracked addresses.
8813
+ * Scans HD indices 0..19 to match addressIds from the old format.
8814
+ * Populates both _trackedAddresses and _addressNametags.
7248
8815
  */
7249
- async syncNametagWithNostr() {
7250
- const nametag = this._identity?.nametag;
7251
- if (!nametag) {
7252
- return;
8816
+ async migrateFromOldNametagFormat(parsed) {
8817
+ const addressIdToNametags = /* @__PURE__ */ new Map();
8818
+ for (const [key, value] of Object.entries(parsed)) {
8819
+ if (typeof value === "object" && value !== null) {
8820
+ addressIdToNametags.set(key, value);
8821
+ }
8822
+ }
8823
+ if (addressIdToNametags.size === 0 || !this._masterKey) return;
8824
+ const SCAN_LIMIT = 20;
8825
+ for (let i = 0; i < SCAN_LIMIT && addressIdToNametags.size > 0; i++) {
8826
+ try {
8827
+ const addrInfo = this._deriveAddressInternal(i, false);
8828
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
8829
+ const addressId = getAddressId(directAddress);
8830
+ if (addressIdToNametags.has(addressId)) {
8831
+ const nametagsObj = addressIdToNametags.get(addressId);
8832
+ const nametagMap = /* @__PURE__ */ new Map();
8833
+ for (const [idx, tag] of Object.entries(nametagsObj)) {
8834
+ nametagMap.set(parseInt(idx, 10), tag);
8835
+ }
8836
+ if (nametagMap.size > 0) {
8837
+ this._addressNametags.set(addressId, nametagMap);
8838
+ }
8839
+ const now = Date.now();
8840
+ const entry = {
8841
+ index: i,
8842
+ addressId,
8843
+ l1Address: addrInfo.address,
8844
+ directAddress,
8845
+ chainPubkey: addrInfo.publicKey,
8846
+ nametag: nametagMap.get(0),
8847
+ hidden: false,
8848
+ createdAt: now,
8849
+ updatedAt: now
8850
+ };
8851
+ this._trackedAddresses.set(i, entry);
8852
+ this._addressIdToIndex.set(addressId, i);
8853
+ addressIdToNametags.delete(addressId);
8854
+ }
8855
+ } catch {
8856
+ }
8857
+ }
8858
+ await this.persistAddressNametags();
8859
+ }
8860
+ /**
8861
+ * Ensure an address is tracked in the registry.
8862
+ * If not yet tracked, derives full info and creates the entry.
8863
+ */
8864
+ async ensureAddressTracked(index) {
8865
+ const existing = this._trackedAddresses.get(index);
8866
+ if (existing) return existing;
8867
+ const addrInfo = this._deriveAddressInternal(index, false);
8868
+ const directAddress = await deriveL3PredicateAddress(addrInfo.privateKey);
8869
+ const addressId = getAddressId(directAddress);
8870
+ const now = Date.now();
8871
+ const nametag = this._addressNametags.get(addressId)?.get(0);
8872
+ const entry = {
8873
+ index,
8874
+ addressId,
8875
+ l1Address: addrInfo.address,
8876
+ directAddress,
8877
+ chainPubkey: addrInfo.publicKey,
8878
+ nametag,
8879
+ hidden: false,
8880
+ createdAt: now,
8881
+ updatedAt: now
8882
+ };
8883
+ this._trackedAddresses.set(index, entry);
8884
+ this._addressIdToIndex.set(addressId, index);
8885
+ await this.persistTrackedAddresses();
8886
+ this.emitEvent("address:activated", { address: { ...entry } });
8887
+ return entry;
8888
+ }
8889
+ /**
8890
+ * Persist nametag cache to storage.
8891
+ * Format: { addressId: { "0": "alice", "1": "alice2" } }
8892
+ */
8893
+ async persistAddressNametags() {
8894
+ const result = {};
8895
+ for (const [addressId, nametags] of this._addressNametags.entries()) {
8896
+ const obj = {};
8897
+ for (const [idx, tag] of nametags.entries()) {
8898
+ obj[idx.toString()] = tag;
8899
+ }
8900
+ result[addressId] = obj;
7253
8901
  }
7254
- if (!this._transport.resolveNametag || !this._transport.registerNametag) {
8902
+ await this._storage.set(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS, JSON.stringify(result));
8903
+ }
8904
+ /**
8905
+ * Load nametag cache from storage.
8906
+ */
8907
+ async loadAddressNametags() {
8908
+ this._addressNametags.clear();
8909
+ try {
8910
+ const data = await this._storage.get(STORAGE_KEYS_GLOBAL.ADDRESS_NAMETAGS);
8911
+ if (!data) return;
8912
+ const parsed = JSON.parse(data);
8913
+ for (const [addressId, nametags] of Object.entries(parsed)) {
8914
+ const map = /* @__PURE__ */ new Map();
8915
+ for (const [idx, tag] of Object.entries(nametags)) {
8916
+ map.set(parseInt(idx, 10), tag);
8917
+ }
8918
+ this._addressNametags.set(addressId, map);
8919
+ }
8920
+ } catch {
8921
+ }
8922
+ }
8923
+ /**
8924
+ * Publish identity binding via transport.
8925
+ * Always publishes base identity (chainPubkey, l1Address, directAddress).
8926
+ * If nametag is set, also publishes nametag hash, proxy address, encrypted nametag.
8927
+ */
8928
+ async syncIdentityWithTransport() {
8929
+ if (!this._transport.publishIdentityBinding) {
7255
8930
  return;
7256
8931
  }
7257
8932
  try {
7258
- const success = await this._transport.registerNametag(
7259
- nametag,
8933
+ const nametag = this._identity?.nametag;
8934
+ const success = await this._transport.publishIdentityBinding(
7260
8935
  this._identity.chainPubkey,
7261
- this._identity.directAddress || ""
8936
+ this._identity.l1Address,
8937
+ this._identity.directAddress || "",
8938
+ nametag || void 0
7262
8939
  );
7263
8940
  if (success) {
7264
- console.log(`[Sphere] Nametag @${nametag} synced with Nostr`);
7265
- } else {
8941
+ console.log(`[Sphere] Identity binding published${nametag ? ` with nametag @${nametag}` : ""}`);
8942
+ } else if (nametag) {
7266
8943
  console.warn(`[Sphere] Nametag @${nametag} is taken by another pubkey`);
7267
8944
  }
7268
8945
  } catch (error) {
7269
- console.warn(`[Sphere] Nametag sync failed:`, error);
8946
+ console.warn(`[Sphere] Identity binding sync failed:`, error);
7270
8947
  }
7271
8948
  }
7272
8949
  /**
7273
- * Recover nametag from Nostr after wallet import
8950
+ * Recover nametag from transport after wallet import.
7274
8951
  * Searches for encrypted nametag events authored by this wallet's pubkey
7275
- * and decrypts them to restore the nametag association
8952
+ * and decrypts them to restore the nametag association.
7276
8953
  */
7277
- async recoverNametagFromNostr() {
8954
+ async recoverNametagFromTransport() {
7278
8955
  if (this._identity?.nametag) {
7279
8956
  return;
7280
8957
  }
@@ -7288,22 +8965,21 @@ var Sphere = class _Sphere {
7288
8965
  this._identity.nametag = recoveredNametag;
7289
8966
  await this._updateCachedProxyAddress();
7290
8967
  }
7291
- const addressId = this.getCurrentAddressId();
7292
- if (addressId) {
7293
- let nametagsMap = this._addressNametags.get(addressId);
7294
- if (!nametagsMap) {
7295
- nametagsMap = /* @__PURE__ */ new Map();
7296
- this._addressNametags.set(addressId, nametagsMap);
7297
- }
7298
- const nextIndex = nametagsMap.size;
7299
- nametagsMap.set(nextIndex, recoveredNametag);
8968
+ const entry = await this.ensureAddressTracked(this._currentAddressIndex);
8969
+ let nametags = this._addressNametags.get(entry.addressId);
8970
+ if (!nametags) {
8971
+ nametags = /* @__PURE__ */ new Map();
8972
+ this._addressNametags.set(entry.addressId, nametags);
7300
8973
  }
8974
+ const nextIndex = nametags.size;
8975
+ nametags.set(nextIndex, recoveredNametag);
7301
8976
  await this.persistAddressNametags();
7302
- if (this._transport.registerNametag) {
7303
- await this._transport.registerNametag(
7304
- recoveredNametag,
8977
+ if (this._transport.publishIdentityBinding) {
8978
+ await this._transport.publishIdentityBinding(
7305
8979
  this._identity.chainPubkey,
7306
- this._identity.directAddress || ""
8980
+ this._identity.l1Address,
8981
+ this._identity.directAddress || "",
8982
+ recoveredNametag
7307
8983
  );
7308
8984
  }
7309
8985
  this.emitEvent("nametag:recovered", { nametag: recoveredNametag });
@@ -7331,6 +9007,9 @@ var Sphere = class _Sphere {
7331
9007
  await this._oracle.disconnect();
7332
9008
  this._initialized = false;
7333
9009
  this._identity = null;
9010
+ this._trackedAddresses.clear();
9011
+ this._addressIdToIndex.clear();
9012
+ this._addressNametags.clear();
7334
9013
  this.eventHandlers.clear();
7335
9014
  if (_Sphere.instance === this) {
7336
9015
  _Sphere.instance = null;
@@ -7428,14 +9107,14 @@ var Sphere = class _Sphere {
7428
9107
  if (this._identity) {
7429
9108
  this._storage.setIdentity(this._identity);
7430
9109
  }
9110
+ await this.loadTrackedAddresses();
7431
9111
  await this.loadAddressNametags();
9112
+ const trackedEntry = await this.ensureAddressTracked(this._currentAddressIndex);
9113
+ const nametag = this._addressNametags.get(trackedEntry.addressId)?.get(0);
7432
9114
  if (this._currentAddressIndex > 0 && this._masterKey) {
7433
- const addressInfo = this.deriveAddress(this._currentAddressIndex, false);
9115
+ const addressInfo = this._deriveAddressInternal(this._currentAddressIndex, false);
7434
9116
  const ipnsHash = sha256(addressInfo.publicKey, "hex").slice(0, 40);
7435
9117
  const predicateAddress = await deriveL3PredicateAddress(addressInfo.privateKey);
7436
- const addressId = getAddressId(predicateAddress);
7437
- const nametagsMap = this._addressNametags.get(addressId);
7438
- const nametag = nametagsMap?.get(0);
7439
9118
  this._identity = {
7440
9119
  privateKey: addressInfo.privateKey,
7441
9120
  chainPubkey: addressInfo.publicKey,
@@ -7446,13 +9125,8 @@ var Sphere = class _Sphere {
7446
9125
  };
7447
9126
  this._storage.setIdentity(this._identity);
7448
9127
  console.log(`[Sphere] Restored to address ${this._currentAddressIndex}:`, this._identity.l1Address);
7449
- } else if (this._identity) {
7450
- const addressId = this.getCurrentAddressId();
7451
- const nametagsMap = addressId ? this._addressNametags.get(addressId) : void 0;
7452
- const nametag = nametagsMap?.get(0);
7453
- if (nametag) {
7454
- this._identity.nametag = nametag;
7455
- }
9128
+ } else if (this._identity && nametag) {
9129
+ this._identity.nametag = nametag;
7456
9130
  }
7457
9131
  await this._updateCachedProxyAddress();
7458
9132
  }
@@ -7510,7 +9184,7 @@ var Sphere = class _Sphere {
7510
9184
  // ===========================================================================
7511
9185
  async initializeProviders() {
7512
9186
  this._storage.setIdentity(this._identity);
7513
- this._transport.setIdentity(this._identity);
9187
+ await this._transport.setIdentity(this._identity);
7514
9188
  for (const provider of this._tokenStorageProviders.values()) {
7515
9189
  provider.setIdentity(this._identity);
7516
9190
  }
@@ -7531,7 +9205,8 @@ var Sphere = class _Sphere {
7531
9205
  oracle: this._oracle,
7532
9206
  emitEvent,
7533
9207
  // Pass chain code for L1 HD derivation
7534
- chainCode: this._masterKey?.chainCode
9208
+ chainCode: this._masterKey?.chainCode,
9209
+ price: this._priceProvider ?? void 0
7535
9210
  });
7536
9211
  this._communications.initialize({
7537
9212
  identity: this._identity,