Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

Commit

Permalink
feat: add bip122:p2wpkh account support (#294)
Browse files Browse the repository at this point in the history
* feat: add bip122:p2wpkh account support

* refactor: add and use asInternalAccountStruct for internal accounts
  • Loading branch information
ccharly authored Apr 25, 2024
1 parent 42752f0 commit 042caf7
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 16 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@metamask/snaps-sdk": "^4.0.0",
"@metamask/utils": "^8.3.0",
"@types/uuid": "^9.0.1",
"bech32": "^2.0.0",
"superstruct": "^1.0.3",
"uuid": "^9.0.0"
},
Expand Down
13 changes: 10 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from 'superstruct';

import type { StaticAssertAbstractAccount } from './base-types';
import type { BtcP2wpkhAccount } from './btc';
import { BtcP2wpkhAccountStruct, BtcAccountType } from './btc';
import type { EthEoaAccount, EthErc4337Account } from './eth';
import {
EthEoaAccountStruct,
Expand All @@ -27,18 +29,19 @@ import { UuidStruct } from './utils';
* Type of supported accounts.
*/
export type KeyringAccounts = StaticAssertAbstractAccount<
EthEoaAccount | EthErc4337Account
EthEoaAccount | EthErc4337Account | BtcP2wpkhAccount
>;

/**
* Mapping between account types and their matching `superstruct` schema.
*/
export const KeyringAccountStructs: Record<
string,
Struct<EthEoaAccount> | Struct<EthErc4337Account>
Struct<EthEoaAccount> | Struct<EthErc4337Account> | Struct<BtcP2wpkhAccount>
> = {
[`${EthAccountType.Eoa}`]: EthEoaAccountStruct,
[`${EthAccountType.Erc4337}`]: EthErc4337AccountStruct,
[`${BtcAccountType.P2wpkh}`]: BtcP2wpkhAccountStruct,
};

/**
Expand All @@ -48,7 +51,11 @@ export const BaseKeyringAccountStruct = object({
/**
* Account type.
*/
type: enums([`${EthAccountType.Eoa}`, `${EthAccountType.Erc4337}`]),
type: enums([
`${EthAccountType.Eoa}`,
`${EthAccountType.Erc4337}`,
`${BtcAccountType.P2wpkh}`,
]),
});

/**
Expand Down
1 change: 1 addition & 0 deletions src/btc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types';
41 changes: 41 additions & 0 deletions src/btc/types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { BtcP2wpkhAddressStruct } from './types';

describe('types', () => {
describe('BtcP2wpkhAddressStruct', () => {
const errorPrefix = 'Could not decode P2WPKH address';

it.each([
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
'tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx',
])('is valid address; %s', (address) => {
expect(() => BtcP2wpkhAddressStruct.assert(address)).not.toThrow();
});

it.each([
// Too short
'',
'bc1q',
// Must have at least 6 characters after separator '1'
'bc1q000',
])('throws an error if address is too short: %s', (address) => {
expect(() => BtcP2wpkhAddressStruct.assert(address)).toThrow(
`${errorPrefix}: ${address} too short`,
);
});

it('throws an error if address is too long', () => {
const address =
'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4w508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4w508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
expect(() => BtcP2wpkhAddressStruct.assert(address)).toThrow(
`${errorPrefix}: Exceeds length limit`,
);
});

it('throws an error if there no seperator', () => {
const address = 'bc0qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4';
expect(() => BtcP2wpkhAddressStruct.assert(address)).toThrow(
`${errorPrefix}: No separator character for ${address}`,
);
});
});
});
51 changes: 51 additions & 0 deletions src/btc/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { bech32 } from 'bech32';
import type { Infer } from 'superstruct';
import { object, string, array, enums, literal, refine } from 'superstruct';

import { BaseAccount } from '../base-types';

export const BtcP2wpkhAddressStruct = refine(
string(),
'BtcP2wpkhAddressStruct',
(address: string) => {
try {
bech32.decode(address);
} catch (error) {
return new Error(
`Could not decode P2WPKH address: ${(error as Error).message}`,
);
}
return true;
},
);

/**
* Supported Bitcoin methods.
*/
export enum BtcMethod {
// General transaction methods
SendTransaction = 'btc_sendTransaction',
}

/**
* Supported Bitcoin account types.
*/
export enum BtcAccountType {
P2wpkh = 'bip122:p2wpkh',
}

export const BtcP2wpkhAccountStruct = object({
...BaseAccount,

/**
* Account type.
*/
type: literal(`${BtcAccountType.P2wpkh}`),

/**
* Account supported methods.
*/
methods: array(enums([`${BtcMethod.SendTransaction}`])),
});

export type BtcP2wpkhAccount = Infer<typeof BtcP2wpkhAccountStruct>;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './api';
export * from './btc';
export * from './contexts';
export * from './eth';
export * from './events';
Expand Down
12 changes: 9 additions & 3 deletions src/internal/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,19 @@ import { assert } from 'superstruct';
import { InternalAccountStruct } from '.';

describe('InternalAccount', () => {
it('should have the correct structure', () => {
it.each([
{ type: 'eip155:eoa', address: '0x000' },
{
type: 'bip122:p2wpkh',
address: 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4',
},
])('should have the correct structure: %s', ({ type, address }) => {
const account = {
id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb',
address: '0x000',
address,
options: {},
methods: [],
type: 'eip155:eoa',
type,
metadata: {
keyring: {
type: 'Test Keyring',
Expand Down
44 changes: 34 additions & 10 deletions src/internal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Infer, Struct } from 'superstruct';
import { boolean, string, number, define, mask, validate } from 'superstruct';

import { BaseKeyringAccountStruct } from '../api';
import { BtcP2wpkhAccountStruct, BtcAccountType } from '../btc/types';
import {
EthEoaAccountStruct,
EthErc4337AccountStruct,
Expand All @@ -27,28 +28,51 @@ export const InternalAccountMetadataStruct = object({
}),
});

export const InternalEthEoaAccountStruct = object({
...EthEoaAccountStruct.schema,
...InternalAccountMetadataStruct.schema,
});
/**
* Creates an `InternalAccount` from an existing account `superstruct` object.
*
* @param accountStruct - An account `superstruct` object.
* @returns The `InternalAccount` assocaited to `accountStruct`.
*/
function asInternalAccountStruct<Account, AccountSchema>(
accountStruct: Struct<Account, AccountSchema>,
) {
return object({
...accountStruct.schema,
...InternalAccountMetadataStruct.schema,
});
}

export type InternalEthEoaAccount = Infer<typeof InternalEthEoaAccountStruct>;
export const InternalEthEoaAccountStruct =
asInternalAccountStruct(EthEoaAccountStruct);

export const InternalEthErc4337AccountStruct = object({
...EthErc4337AccountStruct.schema,
...InternalAccountMetadataStruct.schema,
});
export const InternalEthErc4337AccountStruct = asInternalAccountStruct(
EthErc4337AccountStruct,
);

export const InternalBtcP2wpkhAccountStruct = asInternalAccountStruct(
BtcP2wpkhAccountStruct,
);

export type InternalEthEoaAccount = Infer<typeof InternalEthEoaAccountStruct>;

export type InternalEthErc4337Account = Infer<
typeof InternalEthErc4337AccountStruct
>;

export type InternalBtcP2wpkhAccount = Infer<
typeof InternalBtcP2wpkhAccountStruct
>;

export const InternalAccountStructs: Record<
string,
Struct<InternalEthEoaAccount> | Struct<InternalEthErc4337Account>
| Struct<InternalEthEoaAccount>
| Struct<InternalEthErc4337Account>
| Struct<InternalBtcP2wpkhAccount>
> = {
[`${EthAccountType.Eoa}`]: InternalEthEoaAccountStruct,
[`${EthAccountType.Erc4337}`]: InternalEthErc4337AccountStruct,
[`${BtcAccountType.P2wpkh}`]: InternalBtcP2wpkhAccountStruct,
};

export const InternalAccountStruct = define(
Expand Down
8 changes: 8 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ __metadata:
"@types/uuid": ^9.0.1
"@typescript-eslint/eslint-plugin": ^5.43.0
"@typescript-eslint/parser": ^5.43.0
bech32: ^2.0.0
depcheck: ^1.4.3
eslint: ^8.27.0
eslint-config-prettier: ^8.5.0
Expand Down Expand Up @@ -2206,6 +2207,13 @@ __metadata:
languageName: node
linkType: hard

"bech32@npm:^2.0.0":
version: 2.0.0
resolution: "bech32@npm:2.0.0"
checksum: fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e
languageName: node
linkType: hard

"big-integer@npm:^1.6.44":
version: 1.6.51
resolution: "big-integer@npm:1.6.51"
Expand Down

0 comments on commit 042caf7

Please sign in to comment.