Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: btc send flow e2e #28340

Merged
merged 28 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a3894dc
test(e2e): btc send test
montelaidev Oct 21, 2024
2362877
fix: import
montelaidev Oct 21, 2024
dd8b473
fix: lint
montelaidev Oct 21, 2024
522aac4
fix: add to timeout
montelaidev Oct 21, 2024
c2df37d
fix: input
montelaidev Oct 22, 2024
67de3ce
fix: set activity tab prior to send flow
montelaidev Oct 22, 2024
00c0497
Merge branch 'develop' into feat/btc-send-e2e
montelaidev Oct 22, 2024
97c1dea
test: add set max test
montelaidev Oct 22, 2024
da45fb9
Merge branch 'develop' into feat/btc-send-e2e
montelaidev Oct 22, 2024
6677c82
fix: lint
montelaidev Oct 22, 2024
8e20411
fix: fence
montelaidev Oct 22, 2024
f9db355
refactor: reorder
montelaidev Oct 23, 2024
d2b2331
Apply suggestions from code review
montelaidev Oct 23, 2024
2cfaf42
fix: remove used
montelaidev Oct 23, 2024
ed367c6
refactor: create helpers and remove unused.
montelaidev Oct 23, 2024
87d711d
Merge branch 'develop' into feat/btc-send-e2e
montelaidev Oct 23, 2024
78a98b5
fix: return transaction
montelaidev Oct 23, 2024
385e126
Apply suggestions from code review
montelaidev Nov 4, 2024
115b945
Merge branch 'develop' into feat/btc-send-e2e
montelaidev Nov 4, 2024
eb10a82
fix: lint
montelaidev Nov 5, 2024
eca13f1
refactor: remove delay and put start flow in send test
montelaidev Nov 6, 2024
f780952
Merge remote-tracking branch 'origin/develop' into feat/btc-send-e2e
montelaidev Nov 7, 2024
da39a9a
fix: add delays to fix firefox tests
montelaidev Nov 7, 2024
4045c22
fix: remove focused test
montelaidev Nov 7, 2024
31d17f2
fix: add delay to max
montelaidev Nov 7, 2024
d11daf4
Merge remote-tracking branch 'origin/develop' into feat/btc-send-e2e
montelaidev Nov 12, 2024
659193a
fix: update to use the same delay
montelaidev Nov 12, 2024
4c0ee13
Update test/e2e/flask/btc/btc-send.spec.ts
montelaidev Nov 12, 2024
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
13 changes: 13 additions & 0 deletions test/e2e/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,16 @@ export const DEFAULT_BTC_ACCOUNT = 'bc1qg6whd6pc0cguh6gpp3ewujm53hv32ta9hdp252';

/* Default (mocked) BTC balance used by the Bitcoin RPC provider */
export const DEFAULT_BTC_BALANCE = 1; // BTC

/* Default BTC fees rate */
export const DEFAULT_BTC_FEES_RATE = 0.00001; // BTC

/* Default BTC conversion rate to USD */
export const DEFAULT_BTC_CONVERSION_RATE = 62000; // USD

/* Default BTC transaction ID */
export const DEFAULT_BTC_TRANSACTION_ID =
'e4111a707317da67d49a71af4cbcf6c0546f900ca32c3842d2254e315d1fca18';

/* Number of sats in 1 BTC */
export const SATS_IN_1_BTC = 100000000; // sats
164 changes: 164 additions & 0 deletions test/e2e/flask/btc/btc-send.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { strict as assert } from 'assert';
import { Suite } from 'mocha';
import { Driver } from '../../webdriver/driver';
import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants';
import {
getTransactionRequest,
SendFlowPlaceHolders,
withBtcAccountSnap,
} from './common-btc';

export async function startSendFlow(driver: Driver, recipient?: string) {
// Wait a bit so the MultichainRatesController is able to fetch BTC -> USD rates.
await driver.delay(1000);

// Start the send flow.
const sendButton = await driver.waitForSelector({
text: 'Send',
tag: 'button',
css: '[data-testid="coin-overview-send"]',
});

// Firefox test is flaky without this delay. The send flow doesn't start properly.
if (driver.browser === 'firefox') {
await driver.delay(1000);
}

montelaidev marked this conversation as resolved.
Show resolved Hide resolved
await sendButton.click();

// See the review button is disabled by default.
await driver.waitForSelector({
text: 'Review',
tag: 'button',
css: '[disabled]',
});

if (recipient) {
// Set the recipient address (if any).
await driver.pasteIntoField(
`input[placeholder="${SendFlowPlaceHolders.RECIPIENT}"]`,
recipient,
);
}
}

describe('BTC Account - Send', function (this: Suite) {
it('can send complete the send flow', async function () {
await withBtcAccountSnap(
{ title: this.test?.fullTitle() },
async (driver, mockServer) => {
await startSendFlow(driver, DEFAULT_BTC_ACCOUNT);

// TODO: Remove delay here. There is a race condition if the amount and address are set too fast.
danroc marked this conversation as resolved.
Show resolved Hide resolved
await driver.delay(500);
montelaidev marked this conversation as resolved.
Show resolved Hide resolved

// Set the amount to send.
const mockAmountToSend = '0.5';
await driver.pasteIntoField(
`input[placeholder="${SendFlowPlaceHolders.AMOUNT}"]`,
mockAmountToSend,
);

// From here, the "summary panel" should have some information about the fees and total.
await driver.waitForSelector({
text: 'Total',
tag: 'p',
});

// The review button will become available.
const snapReviewButton = await driver.findClickableElement({
text: 'Review',
tag: 'button',
css: '.snap-ui-renderer__footer-button',
});
assert.equal(await snapReviewButton.isEnabled(), true);
await snapReviewButton.click();

// TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically.
// We already have unit tests for these calculations on the Snap.

// ------------------------------------------------------------------------------
// From here, we have moved to the confirmation screen (second part of the flow).

// We should be able to send the transaction right away.
const snapSendButton = await driver.waitForSelector({
text: 'Send',
tag: 'button',
css: '.snap-ui-renderer__footer-button',
});
assert.equal(await snapSendButton.isEnabled(), true);
await snapSendButton.click();

// Check that we are selecting the "Activity tab" right after the send.
await driver.waitForSelector({
tag: 'div',
text: 'Bitcoin activity is not supported',
});

const transaction = await getTransactionRequest(mockServer);
assert(transaction !== undefined);
},
);
});

it('can send the max amount', async function () {
await withBtcAccountSnap(
{ title: this.test?.fullTitle() },
async (driver, mockServer) => {
await startSendFlow(driver, DEFAULT_BTC_ACCOUNT);
// TODO: Remove delay here. There is a race condition if the amount and address are set too fast.
await driver.delay(1000);
montelaidev marked this conversation as resolved.
Show resolved Hide resolved

// Use the max spendable amount of that account.
await driver.clickElement({
text: 'Max',
tag: 'button',
});

// From here, the "summary panel" should have some information about the fees and total.
await driver.waitForSelector({
text: 'Total',
tag: 'p',
});

await driver.waitForSelector({
text: `${DEFAULT_BTC_BALANCE} BTC`,
tag: 'p',
});

// The review button will become available.
const snapReviewButton = await driver.findClickableElement({
text: 'Review',
tag: 'button',
css: '.snap-ui-renderer__footer-button',
});
assert.equal(await snapReviewButton.isEnabled(), true);
await snapReviewButton.click();

// TODO: There isn't any check for the fees and total amount. This requires calculating the vbytes used in a transaction dynamically.
// We already have unit tests for these calculations on the snap.

// ------------------------------------------------------------------------------
// From here, we have moved to the confirmation screen (second part of the flow).

// We should be able to send the transaction right away.
const snapSendButton = await driver.waitForSelector({
text: 'Send',
tag: 'button',
css: '.snap-ui-renderer__footer-button',
});
assert.equal(await snapSendButton.isEnabled(), true);
await snapSendButton.click();

// Check that we are selecting the "Activity tab" right after the send.
await driver.waitForSelector({
tag: 'div',
text: 'Bitcoin activity is not supported',
});

const transaction = await getTransactionRequest(mockServer);
assert(transaction !== undefined);
},
);
});
});
158 changes: 152 additions & 6 deletions test/e2e/flask/btc/common-btc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { Mockttp } from 'mockttp';
import FixtureBuilder from '../../fixture-builder';
import { withFixtures, unlockWallet } from '../../helpers';
import { DEFAULT_BTC_ACCOUNT, DEFAULT_BTC_BALANCE } from '../../constants';
import {
DEFAULT_BTC_ACCOUNT,
DEFAULT_BTC_BALANCE,
DEFAULT_BTC_FEES_RATE,
DEFAULT_BTC_TRANSACTION_ID,
DEFAULT_BTC_CONVERSION_RATE,
SATS_IN_1_BTC,
} from '../../constants';
import { MultichainNetworks } from '../../../../shared/constants/multichain/networks';
import { Driver } from '../../webdriver/driver';
import messages from '../../../../app/_locales/en/messages.json';

const QUICKNODE_URL_REGEX = /^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u;

export enum SendFlowPlaceHolders {
AMOUNT = 'Enter amount to send',
RECIPIENT = 'Enter receiving address',
LOADING = 'Preparing transaction',
}

export async function createBtcAccount(driver: Driver) {
await driver.clickElement('[data-testid="account-menu-icon"]');
await driver.clickElement(
Expand All @@ -27,12 +42,17 @@ export async function createBtcAccount(driver: Driver) {
);
}

export function btcToSats(btc: number): number {
// Watchout, we're not using BigNumber(s) here (but that's ok for test purposes)
return btc * SATS_IN_1_BTC;
}

export async function mockBtcBalanceQuote(
mockServer: Mockttp,
address: string = DEFAULT_BTC_ACCOUNT,
) {
return await mockServer
.forPost(/^https:\/\/.*\.btc.*\.quiknode\.pro(\/|$)/u)
.forPost(QUICKNODE_URL_REGEX)
.withJsonBodyIncluding({
method: 'bb_getaddress',
})
Expand All @@ -42,7 +62,7 @@ export async function mockBtcBalanceQuote(
json: {
result: {
address,
balance: (DEFAULT_BTC_BALANCE * 1e8).toString(), // Converts from BTC to sats
balance: btcToSats(DEFAULT_BTC_BALANCE).toString(), // Converts from BTC to sats
totalReceived: '0',
totalSent: '0',
unconfirmedBalance: '0',
Expand All @@ -54,6 +74,105 @@ export async function mockBtcBalanceQuote(
});
}

export async function mockBtcFeeCallQuote(mockServer: Mockttp) {
return await mockServer
.forPost(QUICKNODE_URL_REGEX)
.withJsonBodyIncluding({
method: 'estimatesmartfee',
})
.thenCallback(() => {
return {
statusCode: 200,
json: {
result: {
blocks: 1,
feerate: DEFAULT_BTC_FEES_RATE, // sats
},
},
};
});
}

export async function mockMempoolInfo(mockServer: Mockttp) {
return await mockServer
.forPost(QUICKNODE_URL_REGEX)
.withJsonBodyIncluding({
method: 'getmempoolinfo',
})
.thenCallback(() => {
return {
statusCode: 200,
json: {
result: {
loaded: true,
size: 165194,
bytes: 93042828,
usage: 550175264,
total_fee: 1.60127931,
maxmempool: 2048000000,
mempoolminfee: DEFAULT_BTC_FEES_RATE,
minrelaytxfee: DEFAULT_BTC_FEES_RATE,
incrementalrelayfee: 0.00001,
unbroadcastcount: 0,
fullrbf: true,
},
},
};
});
}

export async function mockGetUTXO(mockServer: Mockttp) {
return await mockServer
.forPost(QUICKNODE_URL_REGEX)
.withJsonBodyIncluding({
method: 'bb_getutxos',
})
.thenCallback(() => {
return {
statusCode: 200,
json: {
result: [
{
txid: DEFAULT_BTC_TRANSACTION_ID,
vout: 0,
value: btcToSats(DEFAULT_BTC_BALANCE).toString(),
height: 101100110,
confirmations: 6,
},
],
},
};
});
}

export async function mockSendTransaction(mockServer: Mockttp) {
return await mockServer
.forPost(QUICKNODE_URL_REGEX)
.withJsonBodyIncluding({
method: 'sendrawtransaction',
})
.thenCallback(() => {
return {
statusCode: 200,
json: {
result: DEFAULT_BTC_TRANSACTION_ID,
},
};
});
}

export async function mockRatesCall(mockServer: Mockttp) {
return await mockServer
.forGet('https://min-api.cryptocompare.com/data/pricemulti')
.withQuery({ fsyms: 'btc', tsyms: 'usd,USD' })
.thenCallback(() => {
return {
statusCode: 200,
json: { BTC: { USD: DEFAULT_BTC_CONVERSION_RATE } },
};
});
}

export async function mockRampsDynamicFeatureFlag(
mockServer: Mockttp,
subDomain: string,
Expand Down Expand Up @@ -87,7 +206,7 @@ export async function withBtcAccountSnap(
title,
bitcoinSupportEnabled,
}: { title?: string; bitcoinSupportEnabled?: boolean },
test: (driver: Driver) => Promise<void>,
test: (driver: Driver, mockServer: Mockttp) => Promise<void>,
) {
await withFixtures(
{
Expand All @@ -99,17 +218,44 @@ export async function withBtcAccountSnap(
title,
dapp: true,
testSpecificMock: async (mockServer: Mockttp) => [
await mockRatesCall(mockServer),
await mockBtcBalanceQuote(mockServer),
// See: PROD_RAMP_API_BASE_URL
await mockRampsDynamicFeatureFlag(mockServer, 'api'),
// See: UAT_RAMP_API_BASE_URL
await mockRampsDynamicFeatureFlag(mockServer, 'uat-api'),
await mockMempoolInfo(mockServer),
await mockBtcFeeCallQuote(mockServer),
await mockGetUTXO(mockServer),
await mockSendTransaction(mockServer),
],
},
async ({ driver }: { driver: Driver }) => {
async ({ driver, mockServer }: { driver: Driver; mockServer: Mockttp }) => {
await unlockWallet(driver);
await createBtcAccount(driver);
await test(driver);
await test(driver, mockServer);
},
);
}

export async function getQuickNodeSeenRequests(mockServer: Mockttp) {
const seenRequests = await Promise.all(
(
await mockServer.getMockedEndpoints()
).map((mockedEndpoint) => mockedEndpoint.getSeenRequests()),
);
return seenRequests
.flat()
.filter((request) => request.url.match(QUICKNODE_URL_REGEX));
}

export async function getTransactionRequest(mockServer: Mockttp) {
// Check that the transaction has been sent.
const transactionRequest = (await getQuickNodeSeenRequests(mockServer)).find(
async (request) => {
const body = (await request.body.getJson()) as { method: string };
return body.method === 'sendrawtransaction';
},
);
return transactionRequest;
}
Loading