Skip to content
Open
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
4 changes: 4 additions & 0 deletions packages/bitcoin-wallet-snap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Split the chain `stopGap` configuration into `{ discovery: 5, scan: 20 }` so account discovery keeps the cheap probe while full account scans use the BIP44 gap limit ([#224](https://github.com/MetaMask/internal-snaps/pull/224))

### Fixed

- Ensure certain errors are stringified correctly ([#179](https://github.com/MetaMask/internal-snaps/pull/179))
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcoin-wallet-snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/MetaMask/internal-snaps.git"
},
"source": {
"shasum": "DAuPn0nXZ23flLpbBbGrnOrZW6wYqw0MofHBQ9vjD5U=",
"shasum": "G3MeXJ/FUoBzk6fXEO0VoNvvkQn+9BwqGHeOTUEYRHk=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcoin-wallet-snap/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const Config: SnapConfig = {
encrypt: false,
chain: {
parallelRequests: 5,
stopGap: 5,
stopGap: { discovery: 5, scan: 20 },
maxRetries: 3,
url: {
bitcoin: fromEnv('ESPLORA_BITCOIN', 'https://blockstream.info/api'),
Expand Down
4 changes: 3 additions & 1 deletion packages/bitcoin-wallet-snap/src/entities/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ export type BlockchainClient = {
* Note that this operation modifies the account in place.
*
* @param account - the account to full scan.
* @param mode - 'discovery' uses the short discovery stop gap for probing
* candidate accounts; the default 'scan' uses the full BIP44-sized gap.
*/
fullScan(account: BitcoinAccount): Promise<void>;
fullScan(account: BitcoinAccount, mode?: 'discovery' | 'scan'): Promise<void>;

/**
* Perform a sync operation on the account.
Expand Down
2 changes: 1 addition & 1 deletion packages/bitcoin-wallet-snap/src/entities/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export type SnapConfig = {

export type ChainConfig = {
parallelRequests: number;
stopGap: number;
stopGap: { discovery: number; scan: number };
maxRetries: number;
url: {
[network in Network]: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { FullScanRequest } from '@metamask/bitcoindevkit';
import { EsploraClient } from '@metamask/bitcoindevkit';
import { mock } from 'jest-mock-extended';

import type { BitcoinAccount, ChainConfig } from '../entities';
import { EsploraClientAdapter } from './EsploraClientAdapter';

jest.mock('@metamask/bitcoindevkit', () => ({
EsploraClient: jest.fn(),
}));

const setupTest = (): {
adapter: EsploraClientAdapter;
mockEsploraClient: ReturnType<typeof mock<EsploraClient>>;
account: BitcoinAccount;
mockRequest: FullScanRequest;
} => {
const mockEsploraClient = mock<EsploraClient>();
jest.mocked(EsploraClient).mockReturnValue(mockEsploraClient);

const config = mock<ChainConfig>({
parallelRequests: 5,
maxRetries: 3,
stopGap: { discovery: 5, scan: 20 },
url: {
bitcoin: 'https://bitcoin.example',
testnet: 'https://testnet.example',
testnet4: 'https://testnet4.example',
signet: 'https://signet.example',
regtest: 'https://regtest.example',
},
});

const adapter = new EsploraClientAdapter(config);
const mockRequest = mock<FullScanRequest>();
const account = mock<BitcoinAccount>({ network: 'bitcoin' });
account.startFullScan.mockReturnValue(mockRequest);

return { adapter, mockEsploraClient, account, mockRequest };
};

describe('EsploraClientAdapter', () => {
describe('fullScan', () => {
it('uses the scan stop gap by default', async () => {
const { adapter, mockEsploraClient, account, mockRequest } = setupTest();

await adapter.fullScan(account);

expect(mockEsploraClient.full_scan).toHaveBeenCalledWith(
mockRequest,
20,
5,
);
});

it("uses the discovery stop gap in 'discovery' mode", async () => {
const { adapter, mockEsploraClient, account, mockRequest } = setupTest();

await adapter.fullScan(account, 'discovery');

expect(mockEsploraClient.full_scan).toHaveBeenCalledWith(
mockRequest,
5,
5,
);
});
});
});
11 changes: 9 additions & 2 deletions packages/bitcoin-wallet-snap/src/infra/EsploraClientAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ export class EsploraClientAdapter implements BlockchainClient {
this.#config = config;
}

async fullScan(account: BitcoinAccount): Promise<void> {
async fullScan(
account: BitcoinAccount,
mode: 'discovery' | 'scan' = 'scan',
): Promise<void> {
try {
const stopGap =
mode === 'discovery'
? this.#config.stopGap.discovery
: this.#config.stopGap.scan;
const request = account.startFullScan();
const update = await this.#clients[account.network].full_scan(
request,
this.#config.stopGap,
stopGap,
this.#config.parallelRequests,
);
account.applyUpdate(update);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,10 @@ describe('AccountUseCases', () => {
discoverParams.network,
tAddressType,
);
expect(mockChain.fullScan).toHaveBeenCalledWith(mockAccount);
expect(mockChain.fullScan).toHaveBeenCalledWith(
mockAccount,
'discovery',
);
},
);

Expand Down Expand Up @@ -508,7 +511,10 @@ describe('AccountUseCases', () => {
tNetwork,
discoverParams.addressType,
);
expect(mockChain.fullScan).toHaveBeenCalledWith(mockAccount);
expect(mockChain.fullScan).toHaveBeenCalledWith(
mockAccount,
'discovery',
);
},
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ export class AccountUseCases {
// We need to do a full scan here to know if the account
// has any previous activity since later on we filter out
// accounts with no tx history
await this.#chain.fullScan(newAccount);
await this.#chain.fullScan(newAccount, 'discovery');

this.#logger.info(
'Bitcoin account discovered successfully. Request: %o',
Expand Down