UNPKG

@arkade-os/sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

305 lines (304 loc) 10.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ServiceWorkerWallet = void 0; const response_1 = require("./response"); const base_1 = require("@scure/base"); const singleKey_1 = require("../../identity/singleKey"); const signingSession_1 = require("../../tree/signingSession"); const btc_signer_1 = require("@scure/btc-signer"); class UnexpectedResponseError extends Error { constructor(response) { super(`Unexpected response type. Got: ${JSON.stringify(response, null, 2)}`); this.name = "UnexpectedResponseError"; } } /** * Service Worker-based wallet implementation for browser environments. * * This wallet uses a service worker as a backend to handle wallet logic, * providing secure key storage and transaction signing in web applications. * The service worker runs in a separate thread and can persist data between * browser sessions. * * @example * ```typescript * // Create and initialize the service worker wallet * const serviceWorker = await setupServiceWorker("/service-worker.js"); * const wallet = new ServiceWorkerWallet(serviceWorker); * await wallet.init({ * privateKey: 'your_private_key_hex', * arkServerUrl: 'https://ark.example.com' * }); * * // Use like any other wallet * const address = await wallet.getAddress(); * const balance = await wallet.getBalance(); * ``` */ class ServiceWorkerWallet { constructor(serviceWorker) { this.serviceWorker = serviceWorker; } async getStatus() { const message = { type: "GET_STATUS", id: getRandomId(), }; const response = await this.sendMessage(message); if (response_1.Response.isWalletStatus(response)) { const { walletInitialized, xOnlyPublicKey } = response.status; if (walletInitialized) this.cachedXOnlyPublicKey = xOnlyPublicKey; return response.status; } throw new UnexpectedResponseError(response); } async init(config, failIfInitialized = false) { // Check if wallet is already initialized const statusMessage = { type: "GET_STATUS", id: getRandomId(), }; const response = await this.sendMessage(statusMessage); if (response_1.Response.isWalletStatus(response) && response.status.walletInitialized) { if (failIfInitialized) { throw new Error("Wallet already initialized"); } this.cachedXOnlyPublicKey = response.status.xOnlyPublicKey; return; } // If not initialized, proceed with initialization const message = { type: "INIT_WALLET", id: getRandomId(), privateKey: config.privateKey, arkServerUrl: config.arkServerUrl, arkServerPublicKey: config.arkServerPublicKey, }; await this.sendMessage(message); const privKeyBytes = base_1.hex.decode(config.privateKey); // cache the identity xOnlyPublicKey this.cachedXOnlyPublicKey = singleKey_1.SingleKey.fromPrivateKey(privKeyBytes).xOnlyPublicKey(); } async clear() { const message = { type: "CLEAR", id: getRandomId(), }; await this.sendMessage(message); // clear the cached xOnlyPublicKey this.cachedXOnlyPublicKey = undefined; } // send a message and wait for a response async sendMessage(message) { return new Promise((resolve, reject) => { const messageHandler = (event) => { const response = event.data; if (response.id === "") { reject(new Error("Invalid response id")); return; } if (response.id !== message.id) { return; } navigator.serviceWorker.removeEventListener("message", messageHandler); if (!response.success) { reject(new Error(response.message)); } else { resolve(response); } }; navigator.serviceWorker.addEventListener("message", messageHandler); this.serviceWorker.postMessage(message); }); } async getAddress() { const message = { type: "GET_ADDRESS", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isAddress(response)) { return response.address; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get address: ${error}`); } } async getBoardingAddress() { const message = { type: "GET_BOARDING_ADDRESS", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isBoardingAddress(response)) { return response.address; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get boarding address: ${error}`); } } async getBalance() { const message = { type: "GET_BALANCE", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isBalance(response)) { return response.balance; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get balance: ${error}`); } } async getVtxos(filter) { const message = { type: "GET_VTXOS", id: getRandomId(), filter, }; try { const response = await this.sendMessage(message); if (response_1.Response.isVtxos(response)) { return response.vtxos; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get vtxos: ${error}`); } } async getBoardingUtxos() { const message = { type: "GET_BOARDING_UTXOS", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isBoardingUtxos(response)) { return response.boardingUtxos; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get boarding UTXOs: ${error}`); } } async sendBitcoin(params) { const message = { type: "SEND_BITCOIN", params, id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isSendBitcoinSuccess(response)) { return response.txid; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to send bitcoin: ${error}`); } } async settle(params, callback) { const message = { type: "SETTLE", params, id: getRandomId(), }; try { return new Promise((resolve, reject) => { const messageHandler = (event) => { const response = event.data; if (!response.success) { navigator.serviceWorker.removeEventListener("message", messageHandler); reject(new Error(response.message)); return; } switch (response.type) { case "SETTLE_EVENT": if (callback) { callback(response.event); } break; case "SETTLE_SUCCESS": navigator.serviceWorker.removeEventListener("message", messageHandler); resolve(response.txid); break; default: break; } }; navigator.serviceWorker.addEventListener("message", messageHandler); this.serviceWorker.postMessage(message); }); } catch (error) { throw new Error(`Settlement failed: ${error}`); } } async getTransactionHistory() { const message = { type: "GET_TRANSACTION_HISTORY", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isTransactionHistory(response)) { return response.transactions; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get transaction history: ${error}`); } } xOnlyPublicKey() { if (!this.cachedXOnlyPublicKey) { throw new Error("Wallet not initialized"); } return this.cachedXOnlyPublicKey; } signerSession() { return signingSession_1.TreeSignerSession.random(); } async sign(tx, inputIndexes) { const message = { type: "SIGN", tx: base_1.base64.encode(tx.toPSBT()), inputIndexes, id: getRandomId(), }; try { const response = await this.sendMessage(message); if (response_1.Response.isSignSuccess(response)) { return btc_signer_1.Transaction.fromPSBT(base_1.base64.decode(response.tx), { allowUnknown: true, allowUnknownInputs: true, }); } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to sign: ${error}`); } } } exports.ServiceWorkerWallet = ServiceWorkerWallet; function getRandomId() { const randomValue = crypto.getRandomValues(new Uint8Array(16)); return base_1.hex.encode(randomValue); }