UNPKG

@arkade-os/sdk

Version:

Bitcoin wallet SDK with Taproot and Ark integration

365 lines (364 loc) 13.1 kB
import { Response } from './response.js'; import { hex } from "@scure/base"; import { IndexedDBStorageAdapter } from '../../storage/indexedDB.js'; import { WalletRepositoryImpl } from '../../repositories/walletRepository.js'; import { ContractRepositoryImpl } from '../../repositories/contractRepository.js'; import { DEFAULT_DB_NAME, setupServiceWorker } from './utils.js'; const isPrivateKeyIdentity = (identity) => { return typeof identity.toHex === "function"; }; class UnexpectedResponseError extends Error { constructor(response) { super(`Unexpected response type. Got: ${JSON.stringify(response, null, 2)}`); this.name = "UnexpectedResponseError"; } } const createCommon = (options) => { // Default to IndexedDB for service worker context const storage = new IndexedDBStorageAdapter(options.dbName || DEFAULT_DB_NAME, options.dbVersion); // Create repositories return { walletRepo: new WalletRepositoryImpl(storage), contractRepo: new ContractRepositoryImpl(storage), }; }; export class ServiceWorkerReadonlyWallet { constructor(serviceWorker, identity, walletRepository, contractRepository) { this.serviceWorker = serviceWorker; this.identity = identity; this.walletRepository = walletRepository; this.contractRepository = contractRepository; } static async create(options) { const { walletRepo, contractRepo } = createCommon(options); // Create the wallet instance const wallet = new ServiceWorkerReadonlyWallet(options.serviceWorker, options.identity, walletRepo, contractRepo); const publicKey = await options.identity .compressedPublicKey() .then(hex.encode); // Initialize the service worker with the config const initMessage = { type: "INIT_WALLET", id: getRandomId(), key: { publicKey }, arkServerUrl: options.arkServerUrl, arkServerPublicKey: options.arkServerPublicKey, }; // Initialize the service worker await wallet.sendMessage(initMessage); return wallet; } /** * Simplified setup method that handles service worker registration, * identity creation, and wallet initialization automatically. * * @example * ```typescript * // One-liner setup - handles everything automatically! * const wallet = await ServiceWorkerReadonlyWallet.setup({ * serviceWorkerPath: '/service-worker.js', * arkServerUrl: 'https://mutinynet.arkade.sh' * }); * * // With custom readonly identity * const identity = ReadonlySingleKey.fromPublicKey('your_public_key_hex'); * const wallet = await ServiceWorkerReadonlyWallet.setup({ * serviceWorkerPath: '/service-worker.js', * arkServerUrl: 'https://mutinynet.arkade.sh', * identity * }); * ``` */ static async setup(options) { // Register and setup the service worker const serviceWorker = await setupServiceWorker(options.serviceWorkerPath); // Use the existing create method return ServiceWorkerReadonlyWallet.create({ ...options, serviceWorker, }); } // 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 clear() { const message = { type: "CLEAR", id: getRandomId(), }; // Clear page-side storage to maintain parity with SW try { const address = await this.getAddress(); await this.walletRepository.clearVtxos(address); } catch (_) { console.warn("Failed to clear vtxos from wallet repository"); } await this.sendMessage(message); } async getAddress() { const message = { type: "GET_ADDRESS", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (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.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.isBalance(response)) { return response.balance; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get balance: ${error}`); } } async getBoardingUtxos() { const message = { type: "GET_BOARDING_UTXOS", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (Response.isBoardingUtxos(response)) { return response.boardingUtxos; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get boarding UTXOs: ${error}`); } } async getStatus() { const message = { type: "GET_STATUS", id: getRandomId(), }; const response = await this.sendMessage(message); if (Response.isWalletStatus(response)) { return response.status; } throw new UnexpectedResponseError(response); } async getTransactionHistory() { const message = { type: "GET_TRANSACTION_HISTORY", id: getRandomId(), }; try { const response = await this.sendMessage(message); if (Response.isTransactionHistory(response)) { return response.transactions; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get transaction history: ${error}`); } } async getVtxos(filter) { const message = { type: "GET_VTXOS", id: getRandomId(), filter, }; try { const response = await this.sendMessage(message); if (Response.isVtxos(response)) { return response.vtxos; } throw new UnexpectedResponseError(response); } catch (error) { throw new Error(`Failed to get vtxos: ${error}`); } } async reload() { const message = { type: "RELOAD_WALLET", id: getRandomId(), }; const response = await this.sendMessage(message); if (Response.isWalletReloaded(response)) { return response.success; } throw new UnexpectedResponseError(response); } } export class ServiceWorkerWallet extends ServiceWorkerReadonlyWallet { constructor(serviceWorker, identity, walletRepository, contractRepository) { super(serviceWorker, identity, walletRepository, contractRepository); this.serviceWorker = serviceWorker; this.identity = identity; this.walletRepository = walletRepository; this.contractRepository = contractRepository; } static async create(options) { const { walletRepo, contractRepo } = createCommon(options); // Extract identity and check if it can expose private key const identity = isPrivateKeyIdentity(options.identity) ? options.identity : null; if (!identity) { throw new Error("ServiceWorkerWallet.create() requires a Identity that can expose a single private key"); } // Extract private key for service worker initialization const privateKey = identity.toHex(); // Create the wallet instance const wallet = new ServiceWorkerWallet(options.serviceWorker, identity, walletRepo, contractRepo); // Initialize the service worker with the config const initMessage = { type: "INIT_WALLET", id: getRandomId(), key: { privateKey }, arkServerUrl: options.arkServerUrl, arkServerPublicKey: options.arkServerPublicKey, }; // Initialize the service worker await wallet.sendMessage(initMessage); return wallet; } /** * Simplified setup method that handles service worker registration, * identity creation, and wallet initialization automatically. * * @example * ```typescript * // One-liner setup - handles everything automatically! * const wallet = await ServiceWorkerWallet.setup({ * serviceWorkerPath: '/service-worker.js', * arkServerUrl: 'https://mutinynet.arkade.sh' * }); * * // With custom identity * const identity = SingleKey.fromHex('your_private_key_hex'); * const wallet = await ServiceWorkerWallet.setup({ * serviceWorkerPath: '/service-worker.js', * arkServerUrl: 'https://mutinynet.arkade.sh', * identity * }); * ``` */ static async setup(options) { // Register and setup the service worker const serviceWorker = await setupServiceWorker(options.serviceWorkerPath); // Use the existing create method return ServiceWorkerWallet.create({ ...options, serviceWorker, }); } async sendBitcoin(params) { const message = { type: "SEND_BITCOIN", params, id: getRandomId(), }; try { const response = await this.sendMessage(message); if (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.id !== message.id) { return; } 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}`); } } } function getRandomId() { const randomValue = crypto.getRandomValues(new Uint8Array(16)); return hex.encode(randomValue); }