import { SparkWallet, getLatestDepositTxId, isValidSparkAddress, getNetworkFromSparkAddress, decodeSparkAddress, buildUnilateralExitChain, Network, } from '@buildonspark/spark-sdk'; import { LightningSendRequestStatus, SparkCoopExitRequestStatus, LightningReceiveRequestStatus, SparkLeavesSwapRequestStatus, SparkUserRequestStatus, ClaimStaticDepositStatus, } from '@buildonspark/spark-sdk/types'; import { getAllSparkTransactions } from './transactions'; import { SPARK_TO_SPARK_FEE } from '../../constants/math'; import { getCachedTokens, mergeTokensWithCache, migrateCachedTokens, saveCachedTokens, } from '../lrc20/cachedTokens'; import sha256Hash from '../hash'; import { getHandshakeComplete, OPERATION_TYPES, sendWebViewRequestGlobal, setForceReactNative, } from '../../../context-store/webViewContext'; import { getLocalStorageItem, setLocalStorageItem } from '../localStorage'; import { deriveSparkAddress, deriveSparkIdentityKey, } from '../gift/deriveGiftWallet'; import { DEFAULT_PAYMENT_EXPIRY_SEC, USDB_TOKEN_ID } from '../../constants'; import { FlashnetClient } from '@flashnet/sdk'; export let sparkWallet = {}; export let flashnetClients = {}; let initializingWallets = {}; // Hash cache to avoid recalculating hashes const mnemonicHashCache = new Map(); const getMnemonicHash = mnemonic => { if (!mnemonicHashCache.has(mnemonic)) { mnemonicHashCache.set(mnemonic, sha256Hash(mnemonic)); } return mnemonicHashCache.get(mnemonic); }; export const getWallet = async mnemonic => { const hash = getMnemonicHash(mnemonic); let wallet = sparkWallet[hash]; if (!wallet) { if (initializingWallets[hash]) { await initializingWallets[hash]; return sparkWallet[hash]; } console.log('Creating native wallet because none exists'); initializingWallets[hash] = initializeWallet(mnemonic); wallet = await initializingWallets[hash]; sparkWallet[hash] = wallet; delete initializingWallets[hash]; // cleanup after done } return wallet; }; export const getFlashnetClient = mnemonic => { const hash = getMnemonicHash(mnemonic); const client = flashnetClients[hash]; if (!client) { throw new Error('Flashnet client not initialized'); } return client; }; /** * Determines which runtime to use for Spark functions. * Uses getHandshakeComplete() as the single source of truth — it checks * both handshakeComplete and forceReactNativeUse internally. * @param {string} mnemonic - user mnemonic * @param {boolean} isInitialLoad - true only on first connection attempt * @param {boolean?} force - optional force to native runtime * @returns { 'native' | 'webview' } */ export const selectSparkRuntime = async ( mnemonic, isInitialLoad = false, force = undefined, createNativeWallet = true, ) => { // Force native runtime explicitly via the canonical latch if (isInitialLoad && force) { setForceReactNative(true, 'forced by caller'); } const handshakeDone = getHandshakeComplete(); if (handshakeDone) { return 'webview'; } if (createNativeWallet) { // Handshake not done → fallback to native const walletHash = getMnemonicHash(mnemonic); if (!sparkWallet[walletHash]) { await getWallet(mnemonic); } } return 'native'; }; // Clear cache when needed (call this on logout/cleanup) export const clearMnemonicCache = () => { mnemonicHashCache.clear(); Object.keys(sparkWallet).forEach(key => delete sparkWallet[key]); }; /** * Tears down a short-lived derived wallet (gift/pool/savings) after a flow * completes: removes its event listeners, closes SDK connections/streams, and * drops the reference so no stale session or foreign push events linger. * Only ever call this with a DERIVED mnemonic — never the main wallet. * @param {string} mnemonic - derived wallet mnemonic * @returns {Promise<{didWork: boolean, error?: string}>} */ export const disposeSparkWallet = async mnemonic => { try { // Don't create a native wallet just to dispose one that isn't there. const runtime = await selectSparkRuntime(mnemonic, false, undefined, false); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.disposeWallet, { mnemonic }, ); return response || { didWork: true }; } const hash = getMnemonicHash(mnemonic); const wallet = sparkWallet[hash]; if (!wallet) return { didWork: true }; try { wallet.removeAllListeners?.('transfer:claimed'); wallet.removeAllListeners?.('balance:update'); wallet.removeAllListeners?.('token-balance:update'); wallet.removeAllListeners?.('stream:connected'); wallet.removeAllListeners?.('stream:disconnected'); wallet.removeAllListeners?.('stream:reconnecting'); await wallet.cleanupConnections?.(); } finally { delete sparkWallet[hash]; delete flashnetClients[hash]; } return { didWork: true }; } catch (err) { console.log('Dispose spark wallet error', err); return { didWork: false, error: err.message }; } }; export const initializeSparkWallet = async ( mnemonic, isInitialLoad = true, options = {}, ) => { const { maxRetries = 8, retryDelay = 15000, // 15 seconds between retries enableRetry = true, } = options; const attemptInitialization = async (attemptNumber = 0) => { try { const runtime = await selectSparkRuntime(mnemonic, isInitialLoad); if (runtime === 'webview') { // Use WebView to initialize wallet const response = await sendWebViewRequestGlobal( OPERATION_TYPES.initWallet, { mnemonic, }, ); if (response?.isConnected) return response; else if ( response.error ?.toLowerCase() .includes('load failed [endpoint: authenticate') ) throw new Error('Internet error'); } const hash = getMnemonicHash(mnemonic); // Early return if already initialized if (sparkWallet[hash]) { return { isConnected: true }; } if (initializingWallets[hash]) { await initializingWallets[hash]; return { isConnected: true }; } initializingWallets[hash] = (async () => { try { const wallet = await initializeWallet(mnemonic); sparkWallet[hash] = wallet; return wallet; } catch (err) { delete initializingWallets[hash]; // cleanup after done delete sparkWallet[hash]; throw err; } })(); await initializingWallets[hash]; delete initializingWallets[hash]; setForceReactNative(true, 'native wallet initialized successfully'); return { isConnected: true }; } catch (err) { console.log( `Initialize spark wallet error (attempt ${attemptNumber + 1}/${ maxRetries + 1 }):`, err, ); const hash = getMnemonicHash(mnemonic); delete initializingWallets[hash]; delete sparkWallet[hash]; // If retry is disabled or max retries reached, return error if (!enableRetry || attemptNumber >= maxRetries) { return { isConnected: false, error: err.message }; } // Log retry attempt console.log( `Wallet failed to connect. Retrying in ${ retryDelay / 1000 } seconds... (${attemptNumber + 1}/${maxRetries} retries)`, ); // Wait before retry await new Promise(res => setTimeout(res, retryDelay)); // Recursive retry return attemptInitialization(attemptNumber + 1); } }; return attemptInitialization(0); }; const initializeWallet = async mnemonic => { const { wallet } = await SparkWallet.initialize({ mnemonicOrSeed: mnemonic, options: { network: 'MAINNET', optimizationOptions: { multiplicity: 2, }, }, }); console.log('did initialize wallet'); return wallet; }; export const initializeFlashnet = async mnemonic => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.initializeFlashnet, { mnemonic, }, ); return response.didWork; } else { const wallet = await getWallet(mnemonic); const flashnetAPI = new FlashnetClient(wallet, { autoAuthenticate: true, }); await flashnetAPI.initialize(); flashnetClients[sha256Hash(mnemonic)] = flashnetAPI; return true; } } catch (err) { console.log('Error initializing flashnet', err); return false; } }; export const setPrivacyEnabled = async (mnemonic, freshIdentityPubKey) => { try { const didSetPrivacySetting = (await getLocalStorageItem('didSetPrivacySettingNew').then(JSON.parse)) || {}; const currentWallet = didSetPrivacySetting[freshIdentityPubKey]; if (currentWallet) return; const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.setPrivacyEnabled, { mnemonic, }, ); const validatedResponse = validateWebViewResponse( response, 'unable to generate spark identity pubkey', ); if (validatedResponse.didWork) { didSetPrivacySetting[freshIdentityPubKey] = true; setLocalStorageItem( 'didSetPrivacySettingNew', JSON.stringify(didSetPrivacySetting), ); } return; } else { const wallet = await getWallet(mnemonic); const walletSetings = await wallet.getWalletSettings(); if (!walletSetings?.privateEnabled) { await wallet.setPrivacyEnabled(true); } didSetPrivacySetting[freshIdentityPubKey] = true; setLocalStorageItem( 'didSetPrivacySettingNew', JSON.stringify(didSetPrivacySetting), ); return true; } } catch (err) { console.log('Get spark balance error', err); } }; export const getSparkIdentityPubKey = async (mnemonic, sendWebViewRequest) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const derivedIdentityPubKey = await deriveSparkIdentityKey(mnemonic, 1); if (derivedIdentityPubKey.publicKeyHex) { return derivedIdentityPubKey.publicKeyHex; } const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getIdentityKey, { mnemonic, }, ); return validateWebViewResponse( response, 'unable to generate spark identity pubkey', ); } else { const wallet = await getWallet(mnemonic); return await wallet.getIdentityPublicKey(); } } catch (err) { console.log('Get spark balance error', err); } }; export const getSparkBalance = async mnemonic => { try { const runtime = await selectSparkRuntime(mnemonic); const hash = getMnemonicHash(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getBalance, { mnemonic, }, ); validateWebViewResponse(response, 'unable to get spark balance'); const balanceString = response.balance; const tokensObject = response.tokensObject; const balance = BigInt(balanceString); const convertedTokensObj = {}; for (const [tokensIdentifier, tokensData] of Object.entries( tokensObject, )) { console.log(tokensIdentifier, tokensData); convertedTokensObj[tokensIdentifier] = { ...tokensData, balance: BigInt(tokensData.balance), tokenMetadata: { ...tokensData.tokenMetadata, maxSupply: BigInt(tokensData.tokenMetadata.maxSupply), }, }; } const cachedTokens = await migrateCachedTokens(mnemonic); const allTokens = mergeTokensWithCache( convertedTokensObj, cachedTokens, mnemonic, ); await saveCachedTokens(allTokens); return { tokensObj: allTokens[hash], balance, didWork: true, }; } else { const wallet = await getWallet(mnemonic); const balance = await wallet.getBalance(); const cachedTokens = await migrateCachedTokens(mnemonic); let currentTokensObj = {}; for (const [tokensIdentifier, tokensData] of balance.tokenBalances) { currentTokensObj[tokensIdentifier] = { ...tokensData, balance: tokensData.availableToSendBalance, }; } const allTokens = mergeTokensWithCache( currentTokensObj, cachedTokens, mnemonic, ); await saveCachedTokens(allTokens); return { tokensObj: allTokens[hash], balance: balance.balance, didWork: true, }; } } catch (err) { console.log('Get spark balance error', err); return { didWork: false }; } }; export const getSparkLeaves = async (mnemonic, isBalanceCheck = true) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getSparkLeaves, { mnemonic, isBalanceCheck }, ); return validateWebViewResponse( response, 'Not able to query wallet leaves', ); } else { const wallet = await getWallet(mnemonic); return wallet.getLeaves(isBalanceCheck); } } catch (err) { console.log('get spark leaves error', err); } }; // Fetches ancestor TreeNodes for a SMALL BATCH of leaves, returning a per-leaf // map { [leafId]: node[] } so the caller can cache each leaf's chain // independently. Reuses one spark client and one cross-leaf ancestor cache for // the whole batch (ancestors shared between leaves are fetched once). // // A leaf whose chain builds successfully is present in the map — with an empty // array when it is a root-level leaf (genuinely no ancestors, still a success). // A leaf whose chain throws is OMITTED from the map, which is the caller's // fetch-failed signal so it stays pending and is retried later. // // Native runtime only. Returns {} on the WebView path, on any missing SDK // surface, or on failure (WebView installs stay leaves-only, unchanged). export const getSparkExitNodesForLeaves = async (mnemonic, leaves) => { const runtime = await selectSparkRuntime(mnemonic); try { if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getSparkLeafExitNodes, { mnemonic, leaves }, ); return validateWebViewResponse( response, 'Not abler to generate bitcoin l1 daddress', ); } else { if (!Array.isArray(leaves) || leaves.length === 0) return {}; if (typeof buildUnilateralExitChain !== 'function') return {}; const wallet = await getWallet(mnemonic); const coordinatorAddress = wallet?.config?.getCoordinatorAddress?.(); const createSparkClient = wallet?.connectionManager?.createSparkClient; if (!coordinatorAddress || typeof createSparkClient !== 'function') return {}; const sparkClient = await wallet.connectionManager.createSparkClient( coordinatorAddress, ); // nodeMap seeds buildUnilateralExitChain with locally-known nodes so shared // ancestors between leaves in this batch are only fetched from operators once. const nodeMap = new Map(leaves.map(leaf => [leaf.id, leaf])); const leafIds = new Set(leaves.map(leaf => leaf.id)); const result = {}; for (const leaf of leaves) { try { const chain = await buildUnilateralExitChain( leaf, nodeMap, sparkClient, Network.MAINNET, ); // Keep only true ancestors (drop the leaf itself and dedup within chain). const seen = new Set(); const ancestors = []; for (const node of chain) { if (!node?.id || leafIds.has(node.id) || seen.has(node.id)) continue; seen.add(node.id); ancestors.push(node); } result[leaf.id] = ancestors; } catch (err) { // Omit this leaf so the caller leaves it pending and retries later. console.log('build exit chain error for leaf', leaf?.id, err); } } return result; } } catch (err) { console.log('get spark exit nodes for leaves error', err); return {}; } }; export const getSparkStaticBitcoinL1Address = async mnemonic => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getL1Address, { mnemonic }, ); return validateWebViewResponse( response, 'Not abler to generate bitcoin l1 daddress', ); } else { const wallet = await getWallet(mnemonic); return await wallet.getStaticDepositAddress(); } } catch (err) { console.log('Get reusable Bitcoin mainchain address error', err); } }; export const queryAllStaticDepositAddresses = async mnemonic => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.queryStaticL1Address, { mnemonic }, ); return validateWebViewResponse( response, 'Not able to query all bitcoin l1 daddress', ); } else { const wallet = await getWallet(mnemonic); return wallet.queryStaticDepositAddresses(); } } catch (err) { console.log('refund reusable Bitcoin mainchain address error', err); } }; export const getSparkStaticBitcoinL1AddressQuote = async (txid, mnemonic) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getL1AddressQuote, { mnemonic, txid }, ); return validateWebViewResponse( response, 'Not able to get bitcoin l1 quote', ); } else { const wallet = await getWallet(mnemonic); const quote = await wallet.getClaimStaticDepositQuote(txid); return { didWork: true, quote }; } } catch (err) { console.log('Get reusable Bitcoin mainchain address quote error', err); return { didWork: false, error: err.message }; } }; export const refundSparkStaticBitcoinL1AddressQuote = async ({ depositTransactionId, destinationAddress, fee, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { return await getWallet(mnemonic).refundStaticDeposit({ depositTransactionId, destinationAddress, fee, }); } else { const wallet = await getWallet(mnemonic); return await wallet.refundStaticDeposit({ depositTransactionId, destinationAddress, fee, }); } } catch (err) { console.log('refund reusable Bitcoin mainchain address error', err); } }; export const claimnSparkStaticDepositAddress = async ({ creditAmountSats, outputIndex, sspSignature, transactionId, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.claimStaticDepositAddress, { mnemonic, creditAmountSats, sspSignature, transactionId, outputIndex, }, ); return validateWebViewResponse( response, 'Not able to clain bitcoin l1 deposit', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.claimStaticDeposit({ creditAmountSats, sspSignature, transactionId, outputIndex, }); return { didWork: true, response }; } } catch (err) { console.log('claim static deposit address error', err); return { didWork: false, error: err.message }; } }; export const getSparkAddress = async mnemonic => { try { const derivedIdentityPubKey = await deriveSparkIdentityKey(mnemonic, 1); const derivedSparkAddress = deriveSparkAddress( derivedIdentityPubKey.publicKey, ); if (derivedSparkAddress.address) { return { didWork: true, response: derivedSparkAddress.address }; } const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const derivedIdentityPubKey = await deriveSparkIdentityKey(mnemonic, 1); const derivedSparkAddress = deriveSparkAddress( derivedIdentityPubKey.publicKey, ); if (derivedSparkAddress.address) { return { didWork: true, response: derivedSparkAddress.address }; } const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getSparkAddress, { mnemonic, }, ); return validateWebViewResponse(response, 'Not able to get spark address'); } else { const wallet = await getWallet(mnemonic); const response = await wallet.getSparkAddress(); return { didWork: true, response }; } } catch (err) { console.log('Get spark address error', err); return { didWork: false, error: err.message }; } }; export const sendSparkPayment = async ({ receiverSparkAddress, amountSats, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.sendSparkPayment, { mnemonic, receiverSparkAddress, amountSats, }, ); return validateWebViewResponse( response, 'Not able to send spark payment', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.transfer({ receiverSparkAddress: receiverSparkAddress.toLowerCase(), amountSats, }); return { didWork: true, response }; } } catch (err) { console.log('Send spark payment error', err); return { didWork: false, error: err.message }; } }; export const sendSparkTokens = async ({ tokenIdentifier, tokenAmount, receiverSparkAddress, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.sendTokenPayment, { mnemonic, tokenIdentifier, tokenAmount, receiverSparkAddress, }, ); return validateWebViewResponse( response, 'Not able to send spark token payment', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.transferTokens({ tokenIdentifier, tokenAmount: BigInt(tokenAmount), receiverSparkAddress, }); return { didWork: true, response }; } } catch (err) { console.log('Send spark token error', err); return { didWork: false, error: err.message }; } }; export const getSparkLightningPaymentFeeEstimate = async ( invoice, amountSat, mnemonic, ) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getLightningFee, { mnemonic, amountSat, invoice, }, ); return validateWebViewResponse( response, 'Not able to get spark lightning fee estimate', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.getLightningSendFeeEstimate({ encodedInvoice: invoice.toLowerCase(), amountSats: amountSat, }); return { didWork: true, response }; } } catch (err) { console.log('Get lightning payment fee error', err); return { didWork: false, error: err.message }; } }; /** * Extracts the hex-encoded identity public key from a Spark address string. * Uses SDK static utilities — no wallet instance required. * @param {string} address - A bech32m Spark address * @returns {string} Hex-encoded secp256k1 compressed public key */ export const extractPubkeyFromSparkAddress = address => { if (!address || typeof address !== 'string') { throw new Error( 'extractPubkeyFromSparkAddress: address must be a non-empty string', ); } if (!isValidSparkAddress(address)) { throw new Error( `extractPubkeyFromSparkAddress: invalid Spark address: ${address}`, ); } const network = getNetworkFromSparkAddress(address); const decoded = decodeSparkAddress(address, network); if (!decoded?.identityPublicKey) { throw new Error( `extractPubkeyFromSparkAddress: could not decode pubkey from: ${address}`, ); } return decoded.identityPublicKey; }; /** * Creates a Spark sats invoice routed to the holder of the given Spark address. * Uses createSatsInvoice with receiverIdentityPubkey — no recipient private key needed. * Native path only (WebView createSatsInvoice handler ignores receiverIdentityPubkey). * @param {{ address: string, amountSats: number, mnemonic: string }} params * @returns {Promise<{ didWork: boolean, invoice?: string, error?: string }>} */ export const generateSparkInvoiceFromAddress = async ({ address, amountSats, mnemonic, }) => { try { if ( typeof amountSats !== 'number' || !Number.isInteger(amountSats) || amountSats <= 0 || !Number.isSafeInteger(amountSats) ) { throw new Error( `generateSparkInvoiceFromAddress: amountSats must be a positive safe integer, got: ${amountSats}`, ); } const receiverIdentityPubkey = extractPubkeyFromSparkAddress(address); const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.createSatsInvoice, { mnemonic, amountSats, receiverIdentityPubkey }, ); return validateWebViewResponse( response, 'Not able to create paylink invoice', ); } else { const wallet = await getWallet(mnemonic); const invoice = await wallet.createSatsInvoice({ amount: amountSats, receiverIdentityPubkey, }); console.log(invoice); return { didWork: true, invoice }; } } catch (err) { console.log('generateSparkInvoiceFromAddress error', err); return { didWork: false, error: err.message }; } }; export const fufillSparkInvoices = async ({ mnemonic, invoices = [] }) => { try { if (!Array.isArray(invoices) || invoices.length === 0) { return { successful: [], failed: [], totalPaid: 0, error: 'No recipients provided', }; } const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const serializedInvoices = invoices.map(({ invoice, amount }) => ({ invoice, amount: amount.toString(), // BigInt → string for JSON })); const response = await sendWebViewRequestGlobal( OPERATION_TYPES.fufillSparkInvoices, { mnemonic, invoices: serializedInvoices }, ); return validateWebViewResponse( response, 'Not able to create paylink invoice', ); } else { const wallet = await getWallet(mnemonic); const fulfillResult = await wallet.fulfillSparkInvoice(invoices); return { didWork: true, fulfillResult }; } } catch (err) { console.log('generateSparkInvoiceFromAddress error', err); return { didWork: false, error: err.message }; } }; export const batchSendTokens = async ({ mnemonic, invoices = [] }) => { try { if (!Array.isArray(invoices) || invoices.length === 0) { return { successful: [], failed: [], totalPaid: 0, error: 'No recipients provided', }; } const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const serializedInvoices = invoices.map( ({ tokenIdentifier, receiverSparkAddress, tokenAmount }) => ({ tokenIdentifier, receiverSparkAddress, tokenAmount: tokenAmount.toString(), // BigInt → string for JSON }), ); console.log(serializedInvoices, 'staralized invioces'); const response = await sendWebViewRequestGlobal( OPERATION_TYPES.batchTransferTokens, { mnemonic, invoices: serializedInvoices }, ); return validateWebViewResponse( response, 'Not able to create paylink invoice', ); } else { const wallet = await getWallet(mnemonic); const fulfillResult = await wallet.batchTransferTokens(invoices); return { didWork: true, invoice: fulfillResult }; } } catch (err) { console.log('generateSparkInvoiceFromAddress error', err); return { didWork: false, error: err.message }; } }; export const getSparkBitcoinPaymentRequest = async (paymentId, mnemonic) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getBitcoinPaymentRequest, { mnemonic, paymentId, }, ); return validateWebViewResponse( response, 'Not able to get spark bitcoin payment request', ); } else { const wallet = await getWallet(mnemonic); return await wallet.getCoopExitRequest(paymentId); } } catch (err) { console.log('Get bitcoin payment fee estimate error', err); } }; export const getSparkBitcoinPaymentFeeEstimate = async ({ amountSats, withdrawalAddress, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getBitcoinPaymentFee, { mnemonic, amountSats, withdrawalAddress, }, ); return validateWebViewResponse( response, 'Not able to get spark bitcoin payment fee', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.getWithdrawalFeeQuote({ amountSats, withdrawalAddress: withdrawalAddress, }); return { didWork: true, response }; } } catch (err) { console.log('Get bitcoin payment fee estimate error', err); return { didWork: false, error: err.message }; } }; export const getSparkPaymentFeeEstimate = async (amountSats, mnemonic) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getSparkPaymentFee, { mnemonic, amountSats, }, ); validateWebViewResponse( response, 'Not able to get spark bitcoin payment fee', ); const amount = response.feeEstimate.originalValue; return amount; } else { const wallet = await getWallet(mnemonic); const feeResponse = await wallet.getSwapFeeEstimate(amountSats); return feeResponse.feeEstimate.originalValue || SPARK_TO_SPARK_FEE; } } catch (err) { console.log('Get bitcoin payment fee estimate error', err); return SPARK_TO_SPARK_FEE; } }; export const receiveSparkLightningPayment = async ({ amountSats, memo, mnemonic, includeSparkAddress = true, expirySeconds = DEFAULT_PAYMENT_EXPIRY_SEC, // 12 hour invoice expiry receiverIdentityPubkey, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.receiveLightningPayment, { mnemonic, amountSats, memo, expirySeconds, includeSparkAddress, receiverIdentityPubkey, }, ); return validateWebViewResponse( response, 'Not able to get spark bitcoin lightning request', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.createLightningInvoice({ amountSats, memo, expirySeconds, includeSparkAddress, receiverIdentityPubkey, }); return { didWork: true, response }; } } catch (err) { console.log('Receive lightning payment error', err); return { didWork: false, error: err.message }; } }; export const claimSparkHodlLightningPayment = async ({ preimage, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.claimSparkHodlLightningPayment, { preimage, mnemonic, }, ); return validateWebViewResponse( response, 'Not able to get hold lightning invoice request', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.claimHTLC(preimage); return { didWork: true, response }; } } catch (err) { console.log('Receive HODL lightning payment error', err); return { didWork: false, error: err.message }; } }; export const querySparkHodlLightningPayments = async ({ paymentHashes = [], mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.querySparkHodlLightningPayments, { paymentHashes, mnemonic, }, ); return validateWebViewResponse( response, 'Not able to get hold lightning invoice request', ); } else { const wallet = await getWallet(mnemonic); const response = await await wallet.queryHTLC({ paymentHashes, limit: 50, offset: 0, }); const paidPreimages = response.preimageRequests.map(request => ({ status: request.status, createdTime: request.createdTime, paymentHash: Buffer.from(request.paymentHash).toString('hex'), transferId: request.transfer.id, satValue: request.transfer.totalValue, })); return { didWork: true, paidPreimages }; } } catch (err) { console.log('Receive HODL lightning payment error', err); return { didWork: false, error: err.message }; } }; export const receiveSparkHodlLightningPayment = async ({ amountSats, paymentHash, memo, expirySeconds, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.receiveSparkHodlLightningPayment, { amountSats, paymentHash, memo, expirySeconds, mnemonic, }, ); return validateWebViewResponse( response, 'Not able to get hold lightning invoice request', ); } else { // createLightningHodlInvoice is native-SDK only; always use native runtime const wallet = await getWallet(mnemonic); const response = await wallet.createLightningHodlInvoice({ amountSats, paymentHash, memo, expirySeconds, includeSparkAddress: false, }); return { didWork: true, response }; } } catch (err) { console.log('Receive HODL lightning payment error', err); return { didWork: false, error: err.message }; } }; export const getSparkLightningSendRequest = async (id, mnemonic) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getLightningSendRequest, { mnemonic, id, }, ); return validateWebViewResponse( response, 'Not able to get spark bitcoin lightning send request', ); } else { const wallet = await getWallet(mnemonic); return await wallet.getLightningSendRequest(id); } } catch (err) { console.log('Get spark lightning send request error', err); } }; export const getSparkLightningPaymentStatus = async ({ lightningInvoiceId, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getLightningPaymentStatus, { mnemonic, lightningInvoiceId, }, ); return validateWebViewResponse( response, 'Not able to get spark bitcoin lightning payment status', ); } else { const wallet = await getWallet(mnemonic); return await wallet.getLightningReceiveRequest(lightningInvoiceId); } } catch (err) { console.log('Get lightning payment status error', err); } }; export const sendSparkLightningPayment = async ({ invoice, maxFeeSats, amountSats, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.sendLightningPayment, { mnemonic, invoice, maxFeeSats, amountSat: amountSats, }, ); return validateWebViewResponse( response, 'Not able to send spark bitcoin lightning payment', ); } else { const wallet = await getWallet(mnemonic); const paymentResponse = await wallet.payLightningInvoice({ invoice: invoice.toLowerCase(), maxFeeSats: maxFeeSats, amountSatsToSend: amountSats, preferSpark: true, }); return { didWork: true, paymentResponse }; } } catch (err) { console.log('Send lightning payment error', err); return { didWork: false, error: err.message }; } }; export const getUtxosForDepositAddress = async ({ depositAddress, mnemonic, limit = 100, offset = 0, excludeClaimed = true, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getUtxosForDepositAddress, { depositAddress, mnemonic, limit, offset, excludeClaimed, }, ); return validateWebViewResponse( response, 'Not able to send spark bitcoin payment', ); } else { const wallet = await getWallet(mnemonic); const utxos = await wallet.getUtxosForDepositAddress( depositAddress, limit, offset, excludeClaimed, ); return { didWork: true, utxos }; } } catch (err) { console.log('Send Bitcoin payment error', err); return { didWork: false, error: err.message }; } }; export const sendSparkBitcoinPayment = async ({ onchainAddress, exitSpeed, amountSats, feeQuote, deductFeeFromWithdrawalAmount = false, mnemonic, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.sendBitcoinPayment, { mnemonic, onchainAddress, exitSpeed, feeQuote, amountSats, deductFeeFromWithdrawalAmount, }, ); return validateWebViewResponse( response, 'Not able to send spark bitcoin payment', ); } else { const wallet = await getWallet(mnemonic); const paymentFee = (feeQuote.l1BroadcastFeeFast?.originalValue || 0) + (feeQuote.userFeeFast?.originalValue || 0); const response = await wallet.withdraw({ onchainAddress: onchainAddress, amountSats, exitSpeed, feeQuoteId: feeQuote.id, feeAmountSats: paymentFee, deductFeeFromWithdrawalAmount, }); return { didWork: true, response }; } } catch (err) { console.log('Send Bitcoin payment error', err); return { didWork: false, error: err.message }; } }; export const getSparkTransactions = async ( transferCount = 100, offsetIndex, mnemonic, ) => { try { const runtime = await selectSparkRuntime(mnemonic); let response; if (runtime === 'webview') { const webViewResponse = await sendWebViewRequestGlobal( OPERATION_TYPES.getTransactions, { mnemonic, transferCount, offsetIndex, }, ); if (webViewResponse.offset === undefined) throw new Error('Failed to get transfers'); response = validateWebViewResponse( webViewResponse, 'Not able to send spark transactions', ); } else { const wallet = await getWallet(mnemonic); response = await wallet.getTransfers(transferCount, offsetIndex); } // success:true marks a genuine reply from Spark (an empty transfers array // here means the wallet truly has no more transactions). Callers use this to // tell a real "no transactions" from a failed fetch below, which are // otherwise indistinguishable at the .transfers level. return { ...response, transfers: response?.transfers || [], success: true }; } catch (err) { console.log('get spark transactions error', err); // success:false — the fetch itself failed (network/WebView error). Callers // MUST NOT treat this as "no transactions" or persist a restore-complete flag. return { transfers: [], success: false }; } }; export const getSparkTokenTransactions = async ({ ownerPublicKeys, issuerPublicKeys, tokenTransactionHashes, tokenIdentifiers, outputIds, mnemonic, lastSavedTransactionId, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getTokenTransactions, { mnemonic, ownerPublicKeys, issuerPublicKeys, tokenTransactionHashes, tokenIdentifiers, outputIds, lastSavedTransactionId, }, ); return validateWebViewResponse( response, 'Not able to send spark token transactions', ); } else { const wallet = await getWallet(mnemonic); const response = await wallet.queryTokenTransactions({ ownerPublicKeys, issuerPublicKeys, tokenTransactionHashes, tokenIdentifiers, outputIds, }); let filteredTransactions = response.tokenTransactionsWithStatus; if (lastSavedTransactionId) { const lastIndex = response.tokenTransactionsWithStatus.findIndex( tx => Buffer.from(Object.values(tx.tokenTransactionHash)).toString( 'hex', ) === lastSavedTransactionId, ); if (lastIndex !== -1) { filteredTransactions = response.tokenTransactionsWithStatus.slice( 0, lastIndex, ); } } return { tokenTransactionsWithStatus: filteredTransactions, offset: response.offset, }; } } catch (err) { console.log('get spark Tokens transactions error', err); return []; } }; export const createSatsInvoice = async ({ mnemonic, amountSats, memo, receiverIdentityPubkey, }) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.createSatsInvoice, { mnemonic, amountSats, memo, receiverIdentityPubkey }, ); return validateWebViewResponse( response, 'Not able to create paylink invoice', ); } else { const wallet = await getWallet(mnemonic); const invoice = await wallet.createSatsInvoice({ amount: amountSats, memo, receiverIdentityPubkey, }); console.log(invoice); return { didWork: true, invoice }; } } catch (err) { console.log('createSatsInvoice error', err); return { didWork: false, error: err.message }; } }; export const createTokensInvoice = async ( mnemonic, tokenIdentifier = USDB_TOKEN_ID, ) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.createTokensInvoice, { mnemonic, tokenIdentifier, }, ); return validateWebViewResponse( response, 'Not able to send spark transactions', ); } else { const wallet = await getWallet(mnemonic); const invoice = await wallet.createTokensInvoice({ tokenIdentifier, }); console.log('Token Invoice:', invoice); return { didWork: true, invoice }; } } catch (err) { console.log('get spark transactions error', err); return { didWork: false, error: err.message }; } }; export const getCachedSparkTransactions = async (limit, identifyPubKey) => { try { const txResponse = await getAllSparkTransactions({ limit, accountId: identifyPubKey, }); if (!txResponse) throw new Error('Unable to get cached spark transactins'); return txResponse; } catch (err) { console.log('get cached spark transaction error', err); } }; export const sparkPaymentType = tx => { try { const isLightningPayment = tx.type === 'PREIMAGE_SWAP'; const isBitcoinPayment = tx.type === 'COOPERATIVE_EXIT' || tx.type === 'UTXO_SWAP'; const isSparkPayment = tx.type === 'TRANSFER'; return isLightningPayment ? 'lightning' : isBitcoinPayment ? 'bitcoin' : 'spark'; } catch (err) { console.log('Error finding which payment method was used', err); } }; export const getSparkPaymentStatus = status => { return status === 'TRANSFER_STATUS_COMPLETED' || status === LightningSendRequestStatus.TRANSFER_COMPLETED || status === SparkCoopExitRequestStatus.SUCCEEDED || status === LightningReceiveRequestStatus.TRANSFER_COMPLETED || status === LightningSendRequestStatus.PREIMAGE_PROVIDED || status === SparkLeavesSwapRequestStatus.SUCCEEDED || status === SparkUserRequestStatus.SUCCEEDED || status === ClaimStaticDepositStatus.TRANSFER_COMPLETED || status === ClaimStaticDepositStatus.SPEND_TX_BROADCAST || status === LightningSendRequestStatus.LIGHTNING_PAYMENT_SUCCEEDED || status == LightningReceiveRequestStatus.LIGHTNING_PAYMENT_RECEIVED ? 'completed' : status === 'TRANSFER_STATUS_RETURNED' || status === 'TRANSFER_STATUS_EXPIRED' || status === 'TRANSFER_STATUS_SENDER_INITIATED' || status === LightningSendRequestStatus.USER_SWAP_RETURNED || status === LightningSendRequestStatus.LIGHTNING_PAYMENT_FAILED || status === LightningSendRequestStatus.TRANSFER_FAILED || status === LightningSendRequestStatus.USER_TRANSFER_VALIDATION_FAILED || status === LightningSendRequestStatus.PREIMAGE_PROVIDING_FAILED || status === LightningSendRequestStatus.USER_SWAP_RETURN_FAILED || status === SparkCoopExitRequestStatus.FAILED || status === SparkCoopExitRequestStatus.EXPIRED || status === LightningReceiveRequestStatus.TRANSFER_FAILED || status === LightningReceiveRequestStatus.PAYMENT_PREIMAGE_RECOVERING_FAILED || status === LightningReceiveRequestStatus.REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED || status === LightningReceiveRequestStatus.REFUND_SIGNING_FAILED || status === LightningReceiveRequestStatus.TRANSFER_CREATION_FAILED || status === SparkLeavesSwapRequestStatus.FAILED || status === SparkLeavesSwapRequestStatus.EXPIRED || status === SparkUserRequestStatus.FAILED || status === SparkUserRequestStatus.CANCELED || status === ClaimStaticDepositStatus.TRANSFER_CREATION_FAILED || status === ClaimStaticDepositStatus.REFUND_SIGNING_FAILED || status === ClaimStaticDepositStatus.UTXO_SWAPPING_FAILED || status === ClaimStaticDepositStatus.REFUND_SIGNING_COMMITMENTS_QUERYING_FAILED ? 'failed' : 'pending'; }; export const getSingleTxDetails = async (mnemonic, id) => { try { const runtime = await selectSparkRuntime(mnemonic); if (runtime === 'webview') { const response = await sendWebViewRequestGlobal( OPERATION_TYPES.getSingleTxDetails, { mnemonic, id, }, ); return validateWebViewResponse(response, 'No transaction found'); } else { const wallet = await getWallet(mnemonic); return await wallet.getTransfer(id); } } catch (err) { console.log('get single spark transaction error', err); return undefined; } }; /** * Validates WebView response and throws if error present */ export const validateWebViewResponse = (response, errorMessage) => { if (!response) { throw new Error(errorMessage || 'No response from WebView'); } if (response.error) { throw new Error(response.error); } if (response.hasOwnProperty('didWork') && !response.didWork) { throw new Error(response.error || errorMessage || 'Operation failed'); } return response; };