Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions packages/bitcoin/src/providers/maestro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,6 @@ export class MaestroProvider implements IBitcoinProvider {
const { data, status } = await this._axiosInstance.post(
"/esplora/tx",
txHex,
{
headers: {
"Content-Type": "application/json",
},
},
);

if (status === 200) return data.txid || data;
Expand All @@ -189,15 +184,26 @@ export class MaestroProvider implements IBitcoinProvider {
/**
* Get fee estimates for Bitcoin transactions.
* @param blocks - The number of blocks to estimate fees for (default: 6).
* @returns FeeEstimateResponse containing the estimated fee rate.
* @returns The estimated fee rate in satoshis per vByte.
*/
async fetchFeeEstimates(blocks: number = 6): Promise<number> {
try {
const { data, status } = await this._axiosInstance.get(
`/rpc/transaction/estimatefee/${blocks}`,
);

if (status === 200) return data.data.feerate;
if (status === 200) {
const feeRateInBtc = data.data.feerate;
if (feeRateInBtc === 0) {
if (this._network === "testnet") {
return 1; // 1 sat/vByte fallback for testnet (low activity expected)
} else {
throw new Error("Fee estimation unavailable for mainnet");
}
}

return feeRateInBtc * 100_000_000;
}
throw parseHttpError(data);
} catch (error) {
throw parseHttpError(error);
Expand Down
17 changes: 13 additions & 4 deletions packages/bitcoin/src/wallets/embedded/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,14 @@ export class EmbeddedWallet {
}

if (options.key.type === "mnemonic") {
// Use BIP84 standard: m/84'/coin_type'/0'/0/0
// coin_type: 0 for mainnet, 1 for testnet (including testnet4 and regtest)
const coinType = this._network === bitcoin.networks.bitcoin ? 0 : 1;
const defaultPath = `m/84'/${coinType}'/0'/0/0`;

this._wallet = _derive(
options.key.words,
options.path ?? "m/84'/0'/0'/0/0",
options.path ?? defaultPath,
this._network,
);
this._isReadOnly = false;
Expand Down Expand Up @@ -109,6 +114,7 @@ export class EmbeddedWallet {
}

const addressInfo = resolveAddress(this._wallet.publicKey, this._network);

return [
{
address: addressInfo.address,
Expand Down Expand Up @@ -498,7 +504,7 @@ export class EmbeddedWallet {
recipients: any[],
walletAddress: string,
): Promise<bitcoin.Psbt> {
let feeRate = 10; // Default fallback
let feeRate = 2; // Default fallback
if (this._provider) {
try {
feeRate = await this._provider.fetchFeeEstimates(6);
Expand All @@ -507,7 +513,7 @@ export class EmbeddedWallet {
}
}

// Use simple largest-first coin selection
// Use largest-first coin selection
const targetAmount = recipients.reduce((sum, r) => sum + r.amount, 0);
const { selectedUtxos, change } = this._selectUtxosLargestFirst(
utxos,
Expand All @@ -524,7 +530,10 @@ export class EmbeddedWallet {
psbt.addInput({
hash: utxo.txid,
index: utxo.vout,
witnessUtxo: { script: p2wpkh.output!, value: utxo.value },
witnessUtxo: {
script: p2wpkh.output!,
value: utxo.value,
},
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,10 @@ describe("Blockstream Integration Tests", () => {
expect(typeof fee12).toBe("number");
expect(typeof fee24).toBe("number");

// Generally, longer confirmation times have lower fees
expect(fee6).toBeGreaterThanOrEqual(fee24);
// All fees should be positive numbers
expect(fee6).toBeGreaterThan(0);
expect(fee12).toBeGreaterThan(0);
expect(fee24).toBeGreaterThan(0);
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/bitcoin/test/wallets/embedded-core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const MockedMaestroProvider = MaestroProvider as jest.MockedClass<typeof Maestro
describe("EmbeddedWallet - Core Functionality", () => {
let mockProvider: jest.Mocked<MaestroProvider>;
const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"];
const testAddressTestnet = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0";
const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0";
const testAddressTestnet = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl";
const readOnlyAddress = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl";

beforeEach(() => {
jest.clearAllMocks();
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcoin/test/wallets/embedded-derivation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const MockedMaestroProvider = MaestroProvider as jest.MockedClass<typeof Maestro
describe("EmbeddedWallet - Address Derivation", () => {
let mockProvider: jest.Mocked<MaestroProvider>;
const testMnemonic = ["abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "abandon", "about"];
const testAddressTestnet = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0";
const testAddressTestnet = "tb1q6rz28mcfaxtmd6v789l9rrlrusdprr9pqcpvkl";
const testAddressMainnet = "bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu";
const readOnlyAddress = "tb1qcr8te4kr609gcawutmrza0j4xv80jy8zmfp6l0";

Expand Down