bitmask-core
Version:
Core functionality for the BitMask wallet
333 lines (297 loc) • 8.75 kB
text/typescript
// 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 };
}