UNPKG

@arklabs/wallet-sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

453 lines (452 loc) 19.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Worker = void 0; /// <reference lib="webworker" /> const inMemoryKey_1 = require("../../identity/inMemoryKey"); const wallet_1 = require("../wallet"); const request_1 = require("./request"); const response_1 = require("./response"); const ark_1 = require("../../providers/ark"); const default_1 = require("../../script/default"); const idb_1 = require("./db/vtxo/idb"); const transactionHistory_1 = require("../../utils/transactionHistory"); // Worker is a class letting to interact with ServiceWorkerWallet from the client // it aims to be run in a service worker context class Worker { constructor(vtxoRepository = new idb_1.IndexedDBVtxoRepository(), messageCallback = () => { }) { this.vtxoRepository = vtxoRepository; this.messageCallback = messageCallback; } async start() { self.addEventListener("message", async (event) => { await this.handleMessage(event); }); } async clear() { if (this.vtxoSubscription) { this.vtxoSubscription.abort(); } await this.vtxoRepository.close(); this.wallet = undefined; this.arkProvider = undefined; this.vtxoSubscription = undefined; } async onWalletInitialized() { if (!this.wallet || !this.arkProvider || !this.wallet.offchainTapscript || !this.wallet.boardingTapscript) { return; } // subscribe to address updates const addressInfo = await this.wallet.getAddressInfo(); if (!addressInfo.offchain) { return; } await this.vtxoRepository.open(); // set the initial vtxos state const { spendableVtxos, spentVtxos } = await this.arkProvider.getVirtualCoins(addressInfo.offchain.address); const encodedOffchainTapscript = this.wallet.offchainTapscript.encode(); const forfeit = this.wallet.offchainTapscript.forfeit(); const vtxos = [...spendableVtxos, ...spentVtxos].map((vtxo) => ({ ...vtxo, tapLeafScript: forfeit, scripts: encodedOffchainTapscript, })); await this.vtxoRepository.addOrUpdate(vtxos); this.processVtxoSubscription(addressInfo.offchain); } async processVtxoSubscription({ address, scripts, }) { try { const addressScripts = [...scripts.exit, ...scripts.forfeit]; const vtxoScript = default_1.DefaultVtxo.Script.decode(addressScripts); const tapLeafScript = vtxoScript.findLeaf(scripts.forfeit[0]); const abortController = new AbortController(); const subscription = this.arkProvider.subscribeForAddress(address, abortController.signal); this.vtxoSubscription = abortController; for await (const update of subscription) { const vtxos = [...update.newVtxos, ...update.spentVtxos]; if (vtxos.length === 0) { continue; } const extendedVtxos = vtxos.map((vtxo) => ({ ...vtxo, tapLeafScript, scripts: addressScripts, })); await this.vtxoRepository.addOrUpdate(extendedVtxos); } } catch (error) { console.error("Error processing address updates:", error); } } async handleClear(event) { this.clear(); if (request_1.Request.isBase(event.data)) { event.source?.postMessage(response_1.Response.clearResponse(event.data.id, true)); } } async handleInitWallet(event) { const message = event.data; if (!request_1.Request.isInitWallet(message)) { console.error("Invalid INIT_WALLET message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid INIT_WALLET message format")); return; } try { this.arkProvider = new ark_1.RestArkProvider(message.arkServerUrl); this.wallet = await wallet_1.Wallet.create({ network: message.network, identity: inMemoryKey_1.InMemoryKey.fromHex(message.privateKey), arkServerUrl: message.arkServerUrl, arkServerPublicKey: message.arkServerPublicKey, }); event.source?.postMessage(response_1.Response.walletInitialized(message.id)); await this.onWalletInitialized(); } catch (error) { console.error("Error initializing wallet:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleSettle(event) { const message = event.data; if (!request_1.Request.isSettle(message)) { console.error("Invalid SETTLE message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid SETTLE message format")); return; } try { if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } const txid = await this.wallet.settle(message.params, (e) => { event.source?.postMessage(response_1.Response.settleEvent(message.id, e)); }); event.source?.postMessage(response_1.Response.settleSuccess(message.id, txid)); } catch (error) { console.error("Error settling:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleSendBitcoin(event) { const message = event.data; if (!request_1.Request.isSendBitcoin(message)) { console.error("Invalid SEND_BITCOIN message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid SEND_BITCOIN message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const txid = await this.wallet.sendBitcoin(message.params, message.zeroFee); event.source?.postMessage(response_1.Response.sendBitcoinSuccess(message.id, txid)); } catch (error) { console.error("Error sending bitcoin:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetAddress(event) { const message = event.data; if (!request_1.Request.isGetAddress(message)) { console.error("Invalid GET_ADDRESS message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_ADDRESS message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const addresses = await this.wallet.getAddress(); event.source?.postMessage(response_1.Response.addresses(message.id, addresses)); } catch (error) { console.error("Error getting address:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetAddressInfo(event) { const message = event.data; if (!request_1.Request.isGetAddressInfo(message)) { console.error("Invalid GET_ADDRESS_INFO message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_ADDRESS_INFO message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const addressInfo = await this.wallet.getAddressInfo(); event.source?.postMessage(response_1.Response.addressInfo(message.id, addressInfo)); } catch (error) { console.error("Error getting address info:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetBalance(event) { const message = event.data; if (!request_1.Request.isGetBalance(message)) { console.error("Invalid GET_BALANCE message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_BALANCE message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const coins = await this.wallet.getCoins(); const onchainConfirmed = coins .filter((coin) => coin.status.confirmed) .reduce((sum, coin) => sum + coin.value, 0); const onchainUnconfirmed = coins .filter((coin) => !coin.status.confirmed) .reduce((sum, coin) => sum + coin.value, 0); const onchainTotal = onchainConfirmed + onchainUnconfirmed; const spendableVtxos = await this.vtxoRepository.getSpendableVtxos(); const offchainSettledBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "settled" ? sum + vtxo.value : sum, 0); const offchainPendingBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "pending" ? sum + vtxo.value : sum, 0); const offchainSweptBalance = spendableVtxos.reduce((sum, vtxo) => vtxo.virtualStatus.state === "swept" ? sum + vtxo.value : sum, 0); const offchainTotal = offchainSettledBalance + offchainPendingBalance + offchainSweptBalance; event.source?.postMessage(response_1.Response.balance(message.id, { onchain: { confirmed: onchainConfirmed, unconfirmed: onchainUnconfirmed, total: onchainTotal, }, offchain: { swept: offchainSweptBalance, settled: offchainSettledBalance, pending: offchainPendingBalance, total: offchainTotal, }, total: onchainTotal + offchainTotal, })); } catch (error) { console.error("Error getting balance:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetCoins(event) { const message = event.data; if (!request_1.Request.isGetCoins(message)) { console.error("Invalid GET_COINS message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_COINS message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const coins = await this.wallet.getCoins(); event.source?.postMessage(response_1.Response.coins(message.id, coins)); } catch (error) { console.error("Error getting coins:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetVtxos(event) { const message = event.data; if (!request_1.Request.isGetVtxos(message)) { console.error("Invalid GET_VTXOS message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_VTXOS message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const vtxos = await this.vtxoRepository.getSpendableVtxos(); event.source?.postMessage(response_1.Response.vtxos(message.id, vtxos)); } catch (error) { console.error("Error getting vtxos:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetBoardingUtxos(event) { const message = event.data; if (!request_1.Request.isGetBoardingUtxos(message)) { console.error("Invalid GET_BOARDING_UTXOS message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_BOARDING_UTXOS message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const boardingUtxos = await this.wallet.getBoardingUtxos(); event.source?.postMessage(response_1.Response.boardingUtxos(message.id, boardingUtxos)); } catch (error) { console.error("Error getting boarding utxos:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetTransactionHistory(event) { const message = event.data; if (!request_1.Request.isGetTransactionHistory(message)) { console.error("Invalid GET_TRANSACTION_HISTORY message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_TRANSACTION_HISTORY message format")); return; } if (!this.wallet) { console.error("Wallet not initialized"); event.source?.postMessage(response_1.Response.error(message.id, "Wallet not initialized")); return; } try { const { boardingTxs, roundsToIgnore } = await this.wallet.getBoardingTxs(); const { spendable, spent } = await this.vtxoRepository.getAllVtxos(); // convert VTXOs to offchain transactions const offchainTxs = (0, transactionHistory_1.vtxosToTxs)(spendable, spent, roundsToIgnore); const txs = [...boardingTxs, ...offchainTxs]; // sort transactions by creation time in descending order (newest first) txs.sort( // place createdAt = 0 (unconfirmed txs) first, then descending (a, b) => { if (a.createdAt === 0) return -1; if (b.createdAt === 0) return 1; return b.createdAt - a.createdAt; }); event.source?.postMessage(response_1.Response.transactionHistory(message.id, txs)); } catch (error) { console.error("Error getting transaction history:", error); const errorMessage = error instanceof Error ? error.message : "Unknown error occurred"; event.source?.postMessage(response_1.Response.error(message.id, errorMessage)); } } async handleGetStatus(event) { const message = event.data; if (!request_1.Request.isGetStatus(message)) { console.error("Invalid GET_STATUS message format", message); event.source?.postMessage(response_1.Response.error(message.id, "Invalid GET_STATUS message format")); return; } event.source?.postMessage(response_1.Response.walletStatus(message.id, this.wallet !== undefined)); } async handleMessage(event) { this.messageCallback(event); const message = event.data; if (!request_1.Request.isBase(message)) { console.warn("Invalid message format", JSON.stringify(message)); // ignore invalid messages return; } switch (message.type) { case "INIT_WALLET": { await this.handleInitWallet(event); break; } case "SETTLE": { await this.handleSettle(event); break; } case "SEND_BITCOIN": { await this.handleSendBitcoin(event); break; } case "GET_ADDRESS": { await this.handleGetAddress(event); break; } case "GET_ADDRESS_INFO": { await this.handleGetAddressInfo(event); break; } case "GET_BALANCE": { await this.handleGetBalance(event); break; } case "GET_COINS": { await this.handleGetCoins(event); break; } case "GET_VTXOS": { await this.handleGetVtxos(event); break; } case "GET_BOARDING_UTXOS": { await this.handleGetBoardingUtxos(event); break; } case "GET_TRANSACTION_HISTORY": { await this.handleGetTransactionHistory(event); break; } case "GET_STATUS": { await this.handleGetStatus(event); break; } case "CLEAR": { await this.handleClear(event); break; } default: event.source?.postMessage(response_1.Response.error(message.id, "Unknown message type")); } } } exports.Worker = Worker;