UNPKG

bitmask-core

Version:

Core functionality for the BitMask wallet

333 lines (297 loc) 8.75 kB
// Methods meant to work with BDK defined within the web::bitcoin module from bitmask-core: // https://github.com/diba-io/bitmask-core/blob/development/src/web.rs import * as BMC from "./bitmask_core"; import { getEnv, getNetwork, Network } from "./constants"; export const hashPassword = (password: string) => BMC.hash_password(password); export const decryptWallet = async ( hash: string, encryptedDescriptors: string, seedPassword = "" ): Promise<Vault> => JSON.parse( await BMC.decrypt_wallet(hash, encryptedDescriptors, seedPassword) ); export const upgradeWallet = async ( hash: string, encryptedDescriptors: string, seedPassword = "" ): Promise<string> => JSON.parse( await BMC.upgrade_wallet(hash, encryptedDescriptors, seedPassword) ); export const syncWallets = async (): Promise<void> => BMC.sync_wallets(); export const newWallet = async ( hash: string, seedPassword: string ): Promise<string> => JSON.parse(await BMC.new_wallet(hash, seedPassword)); export const encryptWallet = async ( mnemonic: string, hash: string, seedPassword: string ): Promise<string> => JSON.parse(await BMC.encrypt_wallet(mnemonic, hash, seedPassword)); export const getWalletData = async ( descriptor: string, changeDescriptor?: string ): Promise<WalletData> => JSON.parse(await BMC.get_wallet_data(descriptor, changeDescriptor)); export const getNewAddress = async ( descriptor: string, changeDescriptor?: string ): Promise<string> => JSON.parse(await BMC.get_new_address(descriptor, changeDescriptor)); export const sendSats = async ( descriptor: string, changeDescriptor: string, address: string, amount: bigint, broadcast: boolean, feeRate: number ): Promise<TransactionData> => JSON.parse( await BMC.send_sats( descriptor, changeDescriptor, address, amount, broadcast, feeRate ) ); export const fundVault = async ( descriptor: string, changeDescriptor: string, rgbAddress: string, broadcast: boolean, feeRate?: number ): Promise<FundVaultDetails> => JSON.parse( await BMC.fund_vault( descriptor, changeDescriptor, rgbAddress, broadcast, feeRate ) ); export const getAssetsVault = async ( rgbDescriptorXpub: string ): Promise<FundVaultDetails> => JSON.parse(await BMC.get_assets_vault(rgbDescriptorXpub)); export const drainWallet = async ( destination: string, descriptor: string, changeDescriptor?: string, feeRate?: number ): Promise<TransactionData> => JSON.parse( await BMC.drain_wallet(destination, descriptor, changeDescriptor, feeRate) ); export const bumpFee = async ( txid: string, feeRate: number, broadcast: boolean, descriptor: string, changeDescriptor?: string ): Promise<TransactionData> => JSON.parse( await BMC.bump_fee(txid, feeRate, descriptor, changeDescriptor, broadcast) ); // Core type interfaces based on structs defined within the bitmask-core Rust crate: // https://github.com/diba-io/bitmask-core/blob/development/src/structs.rs export interface PrivateWalletData { xprvkh: string; btcDescriptorXprv: string; btcChangeDescriptorXprv: string; rgbDescriptorXprv: string; nostrPrv: string; nostrNsec: string; } export interface PublicWalletData { xpubkh: string; btcDescriptorXpub: string; btcChangeDescriptorXpub: string; rgbDescriptorXpub: string; nostrPub: string; nostrNpub: string; } export interface Vault { mnemonic: string; private: PrivateWalletData; public: PublicWalletData; } export interface Transaction extends WalletTransaction { amount: number; asset?: string; assetType: string; fee: number; message?: string; note?: string; } export interface Activity extends Transaction { id: string; date: number; action: string; status: string; lightning?: boolean; sender?: { name: string; address: string; }; recipient?: { name: string; address: string; invoice: string; }; } export interface TransactionDetails extends Transaction { sender: { name: string; address: string; }; recipient: { name: string; address: string; invoice: string; }; } export interface TransactionData { details: TransactionDataDetails; vsize: number; feeRate: number; } export interface TransactionDataDetails { transaction?: Transaction; txid: string; received: number; sent: number; fee: number; confirmationTime?: ConfirmationTime; confirmed?: boolean; } export interface ConfirmationTime { height: number; timestamp: number; } export interface WalletTransaction { txid: string; received: number; sent: number; fee: number; confirmed: boolean; confirmationTime: ConfirmationTime; vsize: number; feeRate: number; } export interface WalletBalance { immature: number; trustedPending: number; untrustedPending: number; confirmed: number; } export interface WalletData { wallet?: string; name: string; address: string; balance: WalletBalance; transactions: WalletTransaction[]; utxos: string[]; } export interface FundVaultDetails { rgbOutput?: string; isFunded: boolean; fundTxid?: string; fundFee?: number; } /** * Fetch current fee estimates (sat/vB) from the configured Esplora endpoint. * Esplora returns a JSON object keyed by confirmation target (e.g., "1","2","3","6",...) * with floating-point sat/vB values. * Example: * { "1": 9.2, "2": 7.8, "3": 6.5, "6": 3.1, "144": 1.01 } */ export type FeeEstimates = Record<string, number>; export async function fetchFeeEstimates(): Promise<FeeEstimates> { const net = await getNetwork(); // Map exact network to explicit env key as requested // bitcoin -> BITCOIN_EXPLORER_API_MAINNET // testnet -> BITCOIN_EXPLORER_API_TESTNET // signet -> BITCOIN_EXPLORER_API_SIGNET // regtest -> BITCOIN_EXPLORER_API_REGTEST let envKey: string; switch (net) { case "bitcoin": envKey = "BITCOIN_EXPLORER_API_MAINNET"; break; case "testnet": envKey = "BITCOIN_EXPLORER_API_TESTNET"; break; case "signet": envKey = "BITCOIN_EXPLORER_API_SIGNET"; break; case "regtest": envKey = "BITCOIN_EXPLORER_API_REGTEST"; break; default: // Fallback to testnet if unknown string is returned envKey = "BITCOIN_EXPLORER_API_TESTNET"; break; } const base = await getEnv(envKey); if (!base) { throw new Error(`fee-estimates: missing env ${envKey} for network ${net}`); } const baseUrl = (base || "").replace(/\/$/, ""); const url = `${baseUrl}/fee-estimates`; const resp = await fetch(url, { method: "GET" }); if (!resp.ok) { const text = await resp.text().catch(() => ""); throw new Error(`fee-estimates fetch failed ${resp.status}: ${text}`); } return (await resp.json()) as FeeEstimates; } /** * Compute absolute fee in sats from a sat/vB rate and an estimated vsize. * Always ceil so we don't underpay policy minimums. */ export function feeFromRate(vbytes: number, satPerVb: number): number { return Math.ceil(vbytes * satPerVb); } /** * Rough vbytes guesser for swap/bid PSBTs when you don't yet know exact inputs/outputs. * Prefer passing a conservative bound to avoid underpaying relays: * - Default assumes ~2 inputs, ~3 outputs, taproot heavy mix, returns ~400 vB. * - Scale linearly for more inputs/outputs. * * If you know inputs/outputs counts beforehand, pass them to tighten the estimate. */ export function guessSwapVbytes( inputs = 2, outputs = 3, taprootHeavy = true ): number { // Heuristic base (conservative): taproot-heavy ~180 vB per input, ~34 vB per output, +10 overhead // Non-taproot may be slightly bigger; this errs on the safe side. const perIn = taprootHeavy ? 180 : 200; const perOut = 34; const overhead = 10; // Ensure a floor to avoid extremely small estimates in edge cases const est = inputs * perIn + outputs * perOut + overhead; return Math.max(est, 300); } /** * Convenience: get a safe absolute fee quote for a swap given a target confirmation bucket. * - bucket can be "1","2","3","6","144", etc., matching Esplora keys. * - vbytesEstimate can be computed via guessSwapVbytes() or a tighter UI estimate. * - Adds a small 10% safety margin on the feerate to reduce min-relay rejections. */ export async function quoteSwapFeeSats( bucket: string, vbytesEstimate: number ): Promise<{ rate: number; fee: number }> { const estimates = await fetchFeeEstimates(); const satPerVb = estimates[bucket] ?? estimates["6"] ?? 1.0; const paddedRate = Math.max(1.0, satPerVb * 1.1); // 10% headroom const fee = feeFromRate(vbytesEstimate, paddedRate); return { rate: paddedRate, fee }; }