Skip to content

Commit e8e3b91

Browse files
authored
feat: add confirmSend for unified send flow (#533)
1 parent 077353c commit e8e3b91

File tree

11 files changed

+1538
-148
lines changed

11 files changed

+1538
-148
lines changed

packages/snap/integration-test/blockchain-utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable import-x/no-nodejs-modules */
22
import { execSync } from 'child_process';
33
import process from 'process';
4+
45
/* eslint-enable import-x/no-nodejs-modules */
56

67
/**
@@ -136,4 +137,41 @@ export class BlockchainTestUtils {
136137

137138
await this.#waitForEsploraHeight(targetHeight);
138139
}
140+
141+
/**
142+
* Get the balance of a Bitcoin address in satoshis
143+
*
144+
* @param address - The Bitcoin address to query
145+
* @returns The balance in satoshis
146+
*/
147+
async getBalance(address: string): Promise<bigint> {
148+
try {
149+
const response = await fetch(
150+
`${this.#esploraBaseUrl}/address/${address}`,
151+
);
152+
if (!response.ok) {
153+
throw new Error(`Failed to get address info: ${response.statusText}`);
154+
}
155+
156+
const addressInfo = await response.json();
157+
const funded = BigInt(addressInfo.chain_stats.funded_txo_sum ?? 0);
158+
const spent = BigInt(addressInfo.chain_stats.spent_txo_sum ?? 0);
159+
return funded - spent;
160+
} catch (error) {
161+
throw new Error(
162+
`Failed to get balance for address ${address}: ${String(error)}`,
163+
);
164+
}
165+
}
166+
167+
/**
168+
* Get the balance of a Bitcoin address in BTC
169+
*
170+
* @param address - The Bitcoin address to query
171+
* @returns The balance in BTC as a number
172+
*/
173+
async getBalanceInBTC(address: string): Promise<number> {
174+
const balanceSats = await this.getBalance(address);
175+
return Number(balanceSats) / 100_000_000;
176+
}
139177
}

packages/snap/integration-test/client-request.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,176 @@ describe('OnClientRequestHandler', () => {
245245
errors: [{ code: 'Invalid' }],
246246
});
247247
});
248+
249+
it('missing accountId for onAddressInput', async () => {
250+
const response = await snap.onClientRequest({
251+
method: 'onAddressInput',
252+
params: {
253+
value: 'tb1qrn9d5qewjqq5syc4nrjprkfq8gge0cjdaznwcn',
254+
},
255+
});
256+
257+
expect(response).toRespondWithError({
258+
code: -32000,
259+
message:
260+
'Invalid format: At path: accountId -- Expected a string, but received: undefined',
261+
stack: expect.anything(),
262+
});
263+
});
264+
265+
describe('confirmSend', () => {
266+
it('creates a transaction without broadcasting', async () => {
267+
const response = await snap.onClientRequest({
268+
method: 'confirmSend',
269+
params: {
270+
fromAccountId: account.id,
271+
toAddress: TEST_ADDRESS_REGTEST,
272+
assetId: Caip19Asset.Regtest,
273+
amount: '0.001', // 0.001 BTC
274+
},
275+
});
276+
277+
expect(response).toRespondWith({
278+
type: 'send',
279+
id: expect.any(String),
280+
account: account.id,
281+
chain: BtcScope.Regtest,
282+
status: 'unconfirmed',
283+
timestamp: expect.any(Number),
284+
events: [
285+
{
286+
status: 'unconfirmed',
287+
timestamp: expect.any(Number),
288+
},
289+
],
290+
to: [
291+
{
292+
address: TEST_ADDRESS_REGTEST,
293+
asset: {
294+
amount: '0.001', // BTC amount
295+
fungible: true,
296+
unit: CurrencyUnit.Regtest,
297+
type: Caip19Asset.Regtest,
298+
},
299+
},
300+
],
301+
from: [],
302+
fees: [
303+
{
304+
type: FeeType.Priority,
305+
asset: {
306+
amount: expect.any(String),
307+
fungible: true,
308+
unit: CurrencyUnit.Regtest,
309+
type: Caip19Asset.Regtest,
310+
},
311+
},
312+
],
313+
});
314+
});
315+
316+
it('fails with invalid account ID', async () => {
317+
const response = await snap.onClientRequest({
318+
method: 'confirmSend',
319+
params: {
320+
fromAccountId: 'not-a-uuid',
321+
toAddress: TEST_ADDRESS_REGTEST,
322+
assetId: Caip19Asset.Regtest,
323+
amount: '0.001',
324+
},
325+
});
326+
327+
expect(response).toRespondWithError({
328+
code: -32000,
329+
message: expect.stringContaining('Expected a string matching'),
330+
stack: expect.anything(),
331+
});
332+
});
333+
334+
it('fails with invalid address', async () => {
335+
const response = await snap.onClientRequest({
336+
method: 'confirmSend',
337+
params: {
338+
fromAccountId: account.id,
339+
toAddress: 'invalid-address',
340+
assetId: Caip19Asset.Regtest,
341+
amount: '0.001',
342+
},
343+
});
344+
345+
expect(response).toRespondWith({
346+
errors: [{ code: 'Invalid' }],
347+
valid: false,
348+
});
349+
});
350+
351+
it('fails with invalid amount', async () => {
352+
const response = await snap.onClientRequest({
353+
method: 'confirmSend',
354+
params: {
355+
fromAccountId: account.id,
356+
toAddress: TEST_ADDRESS_REGTEST,
357+
assetId: Caip19Asset.Regtest,
358+
amount: '-0.001', // negative amount
359+
},
360+
});
361+
362+
expect(response).toRespondWith({
363+
errors: [{ code: 'Invalid' }],
364+
valid: false,
365+
});
366+
});
367+
368+
it('fails with insufficient funds', async () => {
369+
const response = await snap.onClientRequest({
370+
method: 'confirmSend',
371+
params: {
372+
fromAccountId: account.id,
373+
toAddress: TEST_ADDRESS_REGTEST,
374+
assetId: Caip19Asset.Regtest,
375+
amount: '1000', // 1000 BTC - more than available
376+
},
377+
});
378+
379+
expect(response).toRespondWith({
380+
errors: [{ code: 'InsufficientBalance' }],
381+
valid: false,
382+
});
383+
});
384+
385+
it('fails with insufficient funds to pay fees', async () => {
386+
const balanceBtc = await blockchain.getBalanceInBTC(account.address);
387+
388+
const response = await snap.onClientRequest({
389+
method: 'confirmSend',
390+
params: {
391+
fromAccountId: account.id,
392+
toAddress: TEST_ADDRESS_REGTEST,
393+
assetId: Caip19Asset.Regtest,
394+
amount: balanceBtc.toString(),
395+
},
396+
});
397+
398+
expect(response).toRespondWith({
399+
errors: [{ code: 'InsufficientBalanceToCoverFee' }],
400+
valid: false,
401+
});
402+
});
403+
404+
it('fails with missing parameters', async () => {
405+
const response = await snap.onClientRequest({
406+
method: 'confirmSend',
407+
params: {
408+
fromAccountId: account.id,
409+
// missing toAddress, assetId, amount
410+
} as any,
411+
});
412+
413+
expect(response).toRespondWithError({
414+
code: -32000,
415+
message: expect.stringContaining('At path:'),
416+
stack: expect.anything(),
417+
});
418+
});
419+
});
248420
});

0 commit comments

Comments
 (0)