From cf8c569b6b29ce0066102bfbb40bdad00495cfd1 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 20:34:38 +0200 Subject: [PATCH 01/14] feat: make `KeyringAccount` generic again --- .vscode/settings.json | 5 +++ jest.config.js | 2 +- src/api.test.ts | 35 ---------------- src/api.ts | 92 ++++++++++++------------------------------- src/btc/types.ts | 14 +++++-- src/internal/types.ts | 20 +++------- 6 files changed, 47 insertions(+), 121 deletions(-) create mode 100644 .vscode/settings.json delete mode 100644 src/api.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..f80a6ee3a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "bech32", "p2wpkh", "sendmany" + ] +} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index bd7fddb65..23e0bb535 100644 --- a/jest.config.js +++ b/jest.config.js @@ -148,7 +148,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - // testEnvironment: "jest-environment-node", + testEnvironment: "jest-environment-node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, diff --git a/src/api.test.ts b/src/api.test.ts deleted file mode 100644 index b77424811..000000000 --- a/src/api.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { assert } from 'superstruct'; - -import { KeyringAccountStruct, KeyringAccountStructs } from './api'; // Import from `index.ts` to test the public API - -const supportedKeyringAccountTypes = Object.keys(KeyringAccountStructs) - .map((type: string) => `"${type}"`) - .join(','); - -describe('api', () => { - const baseAccount = { - id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', - address: '0x000', - options: {}, - methods: [], - }; - - describe('KeyringAccount', () => { - it.each([ - [undefined, 'undefined'], - [null, 'null'], - ['not:supported', '"not:supported"'], - ])( - 'throws an error if account type is: %s', - (type: any, typeAsStr: string) => { - const account = { - type, - ...baseAccount, - }; - expect(() => assert(account, KeyringAccountStruct)).toThrow( - `At path: type -- Expected one of \`${supportedKeyringAccountTypes}\`, but received: ${typeAsStr}`, - ); - }, - ); - }); -}); diff --git a/src/api.ts b/src/api.ts index 455473505..f526e443a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,53 +1,24 @@ import type { Json } from '@metamask/utils'; import { JsonStruct } from '@metamask/utils'; -import type { Infer, Struct } from 'superstruct'; -import { - enums, - array, - define, - validate, - literal, - record, - string, - union, - mask, -} from 'superstruct'; +import type { Infer } from 'superstruct'; +import { enums, array, literal, record, string, union } 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, - EthErc4337AccountStruct, - EthAccountType, -} from './eth'; +import { BtcAccountType } from './btc'; +import { EthAccountType } from './eth'; import { exactOptional, object } from './superstruct'; import { UuidStruct } from './utils'; /** - * Type of supported accounts. - */ -export type KeyringAccounts = StaticAssertAbstractAccount< - EthEoaAccount | EthErc4337Account | BtcP2wpkhAccount ->; - -/** - * Mapping between account types and their matching `superstruct` schema. + * Generic account struct. + * + * See {@link KeyringAccount}. */ -export const KeyringAccountStructs: Record< - string, - Struct | Struct | Struct -> = { - [`${EthAccountType.Eoa}`]: EthEoaAccountStruct, - [`${EthAccountType.Erc4337}`]: EthErc4337AccountStruct, - [`${BtcAccountType.P2wpkh}`]: BtcP2wpkhAccountStruct, -}; +export const KeyringAccountStruct = object({ + /** + * Account ID (UUIDv4). + */ + id: UuidStruct, -/** - * Base type for `KeyringAccount` as a `superstruct.object`. - */ -export const BaseKeyringAccountStruct = object({ /** * Account type. */ @@ -56,36 +27,25 @@ export const BaseKeyringAccountStruct = object({ `${EthAccountType.Erc4337}`, `${BtcAccountType.P2wpkh}`, ]), -}); -/** - * Account as a `superstruct.object`. - * - * See {@link KeyringAccount}. - */ -export const KeyringAccountStruct = define( - // We do use a custom `define` for this type to avoid having to use a `union` since error - // messages are a bit confusing. - // - // Doing manual validation allows us to use the "concrete" type of each supported acounts giving - // use a much nicer message from `superstruct`. - 'KeyringAccount', - (value: unknown) => { - // This will also raise if `value` does not match any of the supported account types! - const account = mask(value, BaseKeyringAccountStruct); + /** + * Account main address. + */ + address: string(), - // At this point, we know that `value.type` can be used as an index for `KeyringAccountStructs` - const [error] = validate( - value, - KeyringAccountStructs[account.type] as Struct, - ); + /** + * Account options. + */ + options: record(string(), JsonStruct), - return error ?? true; - }, -); + /** + * Account supported methods. + */ + methods: array(string()), +}); /** - * Account object. + * Generic account type. * * Represents an account with its properties and capabilities. */ diff --git a/src/btc/types.ts b/src/btc/types.ts index bac42af29..1f23afce2 100644 --- a/src/btc/types.ts +++ b/src/btc/types.ts @@ -1,8 +1,11 @@ import { bech32 } from 'bech32'; import type { Infer } from 'superstruct'; -import { object, string, array, enums, literal, refine } from 'superstruct'; +import { object, string, array, enums, refine, record } from 'superstruct'; -import { BaseAccount } from '../base-types'; +import { KeyringAccountStruct } from '../api'; +import { UuidStruct } from '../utils'; +import { address } from '@metamask/snaps-sdk'; +import { JsonStruct } from '@metamask/utils'; export const BtcP2wpkhAddressStruct = refine( string(), @@ -35,12 +38,15 @@ export enum BtcAccountType { } export const BtcP2wpkhAccountStruct = object({ - ...BaseAccount, + // id: UuidStruct, + // address: BtcP2wpkhAddressStruct, + // options: record(string(), JsonStruct), + ...KeyringAccountStruct.schema, /** * Account type. */ - type: literal(`${BtcAccountType.P2wpkh}`), + type: enums([`${BtcAccountType.P2wpkh}`]), /** * Account supported methods. diff --git a/src/internal/types.ts b/src/internal/types.ts index 9ee0ca0ef..dc104501c 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -1,7 +1,7 @@ import type { Infer, Struct } from 'superstruct'; -import { boolean, string, number, define, mask, validate } from 'superstruct'; +import { boolean, string, number, assign } from 'superstruct'; -import { BaseKeyringAccountStruct } from '../api'; +import { KeyringAccountStruct } from '../api'; import { BtcP2wpkhAccountStruct, BtcAccountType } from '../btc/types'; import { EthEoaAccountStruct, @@ -82,19 +82,9 @@ export type InternalAccountTypes = | InternalEthErc4337Account | InternalBtcP2wpkhAccount; -export const InternalAccountStruct = define( - 'InternalAccount', - (value: unknown) => { - const account = mask(value, BaseKeyringAccountStruct); - - // At this point, we know that `value.type` can be used as an index for `KeyringAccountStructs` - const [error] = validate( - value, - InternalAccountStructs[account.type] as Struct, - ); - - return error ?? true; - }, +export const InternalAccountStruct = assign( + KeyringAccountStruct, + InternalAccountMetadataStruct, ); /** From 1d311cf7e2fee21d2dcadd03cbecf86ec2ba4137 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 20:51:24 +0200 Subject: [PATCH 02/14] fix: fix circular dependency --- src/api.ts | 17 +++++++++++++++-- src/btc/types.ts | 14 ++------------ src/eth/types.test-d.ts | 3 ++- src/eth/types.ts | 9 +-------- src/eth/utils.test.ts | 3 +-- src/eth/utils.ts | 2 +- src/internal/events.test.ts | 2 +- src/internal/types.ts | 10 +++------- 8 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/api.ts b/src/api.ts index f526e443a..666c477dc 100644 --- a/src/api.ts +++ b/src/api.ts @@ -3,11 +3,24 @@ import { JsonStruct } from '@metamask/utils'; import type { Infer } from 'superstruct'; import { enums, array, literal, record, string, union } from 'superstruct'; -import { BtcAccountType } from './btc'; -import { EthAccountType } from './eth'; import { exactOptional, object } from './superstruct'; import { UuidStruct } from './utils'; +/** + * Supported Ethereum account types. + */ +export enum EthAccountType { + Eoa = 'eip155:eoa', + Erc4337 = 'eip155:erc4337', +} + +/** + * Supported Bitcoin account types. + */ +export enum BtcAccountType { + P2wpkh = 'bip122:p2wpkh', +} + /** * Generic account struct. * diff --git a/src/btc/types.ts b/src/btc/types.ts index 1f23afce2..8e25ba3a8 100644 --- a/src/btc/types.ts +++ b/src/btc/types.ts @@ -1,11 +1,8 @@ import { bech32 } from 'bech32'; import type { Infer } from 'superstruct'; -import { object, string, array, enums, refine, record } from 'superstruct'; +import { object, string, array, enums, refine } from 'superstruct'; -import { KeyringAccountStruct } from '../api'; -import { UuidStruct } from '../utils'; -import { address } from '@metamask/snaps-sdk'; -import { JsonStruct } from '@metamask/utils'; +import { KeyringAccountStruct, BtcAccountType } from '../api'; export const BtcP2wpkhAddressStruct = refine( string(), @@ -30,13 +27,6 @@ export enum BtcMethod { SendMany = 'btc_sendmany', } -/** - * Supported Bitcoin account types. - */ -export enum BtcAccountType { - P2wpkh = 'bip122:p2wpkh', -} - export const BtcP2wpkhAccountStruct = object({ // id: UuidStruct, // address: BtcP2wpkhAddressStruct, diff --git a/src/eth/types.test-d.ts b/src/eth/types.test-d.ts index ab0d631b1..e5db1616e 100644 --- a/src/eth/types.test-d.ts +++ b/src/eth/types.test-d.ts @@ -1,7 +1,8 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; +import { EthAccountType } from '../api'; import type { EthEoaAccount, EthErc4337Account } from './types'; -import { EthAccountType, EthErc4337Method, EthMethod } from './types'; +import { EthErc4337Method, EthMethod } from './types'; const id = '606a7759-b0fb-48e4-9874-bab62ff8e7eb'; const address = '0x000'; diff --git a/src/eth/types.ts b/src/eth/types.ts index 45c958459..fa1624d3b 100644 --- a/src/eth/types.ts +++ b/src/eth/types.ts @@ -1,6 +1,7 @@ import type { Infer } from 'superstruct'; import { object, array, enums, literal } from 'superstruct'; +import { EthAccountType } from '../api'; import { BaseAccount } from '../base-types'; import { definePattern } from '../superstruct'; @@ -39,14 +40,6 @@ export enum EthErc4337Method { SignUserOperation = 'eth_signUserOperation', } -/** - * Supported Ethereum account types. - */ -export enum EthAccountType { - Eoa = 'eip155:eoa', - Erc4337 = 'eip155:erc4337', -} - export const EthEoaAccountStruct = object({ ...BaseAccount, diff --git a/src/eth/utils.test.ts b/src/eth/utils.test.ts index 98cf97fc5..ea3e9dd79 100644 --- a/src/eth/utils.test.ts +++ b/src/eth/utils.test.ts @@ -1,5 +1,4 @@ -import { EthAccountType } from '.'; -import { BtcAccountType } from '../btc'; +import { BtcAccountType, EthAccountType } from '../api'; import { isEvmAccountType } from './utils'; describe('isEvmAccountType', () => { diff --git a/src/eth/utils.ts b/src/eth/utils.ts index 1c451069d..a5b5c728a 100644 --- a/src/eth/utils.ts +++ b/src/eth/utils.ts @@ -1,5 +1,5 @@ +import { EthAccountType } from '../api'; import type { InternalAccountType } from '../internal'; -import { EthAccountType } from './types'; /** * Checks if the given type is an EVM account type. diff --git a/src/internal/events.test.ts b/src/internal/events.test.ts index feae5bae6..dc33ae9ec 100644 --- a/src/internal/events.test.ts +++ b/src/internal/events.test.ts @@ -1,6 +1,6 @@ import { is } from 'superstruct'; -import { EthAccountType } from '../eth/types'; +import { EthAccountType } from '../api'; import { KeyringEvent } from '../events'; import { AccountCreatedEventStruct, diff --git a/src/internal/types.ts b/src/internal/types.ts index dc104501c..5ebfcdf95 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -1,13 +1,9 @@ import type { Infer, Struct } from 'superstruct'; import { boolean, string, number, assign } from 'superstruct'; -import { KeyringAccountStruct } from '../api'; -import { BtcP2wpkhAccountStruct, BtcAccountType } from '../btc/types'; -import { - EthEoaAccountStruct, - EthErc4337AccountStruct, - EthAccountType, -} from '../eth/types'; +import { BtcAccountType, EthAccountType, KeyringAccountStruct } from '../api'; +import { BtcP2wpkhAccountStruct } from '../btc/types'; +import { EthEoaAccountStruct, EthErc4337AccountStruct } from '../eth/types'; import { exactOptional, object } from '../superstruct'; export type InternalAccountType = EthAccountType | BtcAccountType; From 61a66b32acba8f11c2adb6ea24e9126e99863336 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:05:48 +0200 Subject: [PATCH 03/14] fix: reinstate ERC-4337 methods to the `EthMethod` enum --- src/base-types.ts | 55 --------------------- src/btc/types.ts | 33 +++++++------ src/eth/types.test-d.ts | 32 ++++++------ src/eth/types.ts | 107 +++++++++++++++++++++------------------- 4 files changed, 90 insertions(+), 137 deletions(-) delete mode 100644 src/base-types.ts diff --git a/src/base-types.ts b/src/base-types.ts deleted file mode 100644 index 02b0f17af..000000000 --- a/src/base-types.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { Json } from '@metamask/utils'; -import { JsonStruct } from '@metamask/utils'; -import { object, record, string } from 'superstruct'; - -import { UuidStruct } from './utils'; - -/** - * Base type for any account. This type must be composed and extended to add a `methods` - * and `type` fields. - * - * NOTE: This type isn't a `superstruct.object` as it is used to compose other objects. See - * {@link BaseAccountStruct}. - */ -export const BaseAccount = { - /** - * Account ID (UUIDv4). - */ - id: UuidStruct, - - /** - * Account address or next receive address (UTXO). - */ - address: string(), - - /** - * Keyring-dependent account options. - */ - options: record(string(), JsonStruct), -}; - -/** - * Base type for any account as a `superstruct.object`. - */ -export const BaseAccountStruct = object(BaseAccount); - -/** - * Abstract struct that is used to match every supported account type. Making sure their type - * definition do not diverge from each others. - * - * NOTE: This type is using "primitive types" such as `string` to not contrain any real account - * type. It's up to those types to use more restrictions on their type definition. - */ -export type AbstractAccount = { - id: string; - address: string; - options: Record; - type: string; - methods: string[]; -}; - -/** - * Type helper to make sure `Type` is "equal to" `AbstractAccount`, asserting that `Type` (an account - * type actually) never diverges from other account types. - */ -export type StaticAssertAbstractAccount = Type; diff --git a/src/btc/types.ts b/src/btc/types.ts index 8e25ba3a8..1907020c4 100644 --- a/src/btc/types.ts +++ b/src/btc/types.ts @@ -1,6 +1,6 @@ import { bech32 } from 'bech32'; import type { Infer } from 'superstruct'; -import { object, string, array, enums, refine } from 'superstruct'; +import { object, string, array, enums, refine, assign } from 'superstruct'; import { KeyringAccountStruct, BtcAccountType } from '../api'; @@ -27,21 +27,24 @@ export enum BtcMethod { SendMany = 'btc_sendmany', } -export const BtcP2wpkhAccountStruct = object({ - // id: UuidStruct, - // address: BtcP2wpkhAddressStruct, - // options: record(string(), JsonStruct), - ...KeyringAccountStruct.schema, +export const BtcP2wpkhAccountStruct = assign( + KeyringAccountStruct, + object({ + /** + * Account address. + */ + address: BtcP2wpkhAddressStruct, - /** - * Account type. - */ - type: enums([`${BtcAccountType.P2wpkh}`]), + /** + * Account type. + */ + type: enums([`${BtcAccountType.P2wpkh}`]), - /** - * Account supported methods. - */ - methods: array(enums([`${BtcMethod.SendMany}`])), -}); + /** + * Account supported methods. + */ + methods: array(enums([`${BtcMethod.SendMany}`])), + }), +); export type BtcP2wpkhAccount = Infer; diff --git a/src/eth/types.test-d.ts b/src/eth/types.test-d.ts index e5db1616e..8e32bdbcd 100644 --- a/src/eth/types.test-d.ts +++ b/src/eth/types.test-d.ts @@ -2,7 +2,7 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; import { EthAccountType } from '../api'; import type { EthEoaAccount, EthErc4337Account } from './types'; -import { EthErc4337Method, EthMethod } from './types'; +import { EthMethod } from './types'; const id = '606a7759-b0fb-48e4-9874-bab62ff8e7eb'; const address = '0x000'; @@ -39,9 +39,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ], }); @@ -52,9 +52,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ], }); @@ -79,9 +79,9 @@ expectAssignable({ `${EthMethod.SignTypedDataV1}`, `${EthMethod.SignTypedDataV3}`, `${EthMethod.SignTypedDataV4}`, - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ], }); @@ -92,9 +92,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ], }); @@ -105,8 +105,8 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, ], }); diff --git a/src/eth/types.ts b/src/eth/types.ts index fa1624d3b..48e3b73b3 100644 --- a/src/eth/types.ts +++ b/src/eth/types.ts @@ -1,8 +1,7 @@ import type { Infer } from 'superstruct'; -import { object, array, enums, literal } from 'superstruct'; +import { object, array, enums, literal, assign } from 'superstruct'; -import { EthAccountType } from '../api'; -import { BaseAccount } from '../base-types'; +import { EthAccountType, KeyringAccountStruct } from '../api'; import { definePattern } from '../superstruct'; export const EthBytesStruct = definePattern('EthBytes', /^0x[0-9a-f]*$/iu); @@ -28,66 +27,72 @@ export enum EthMethod { SignTypedDataV1 = 'eth_signTypedData_v1', SignTypedDataV3 = 'eth_signTypedData_v3', SignTypedDataV4 = 'eth_signTypedData_v4', -} - -/** - * Supported Ethereum methods for ERC-4337 (Account Abstraction) accounts. - */ -export enum EthErc4337Method { // ERC-4337 methods PrepareUserOperation = 'eth_prepareUserOperation', PatchUserOperation = 'eth_patchUserOperation', SignUserOperation = 'eth_signUserOperation', } -export const EthEoaAccountStruct = object({ - ...BaseAccount, +export const EthEoaAccountStruct = assign( + KeyringAccountStruct, + object({ + /** + * Account address. + */ + address: EthAddressStruct, - /** - * Account type. - */ - type: literal(`${EthAccountType.Eoa}`), + /** + * Account type. + */ + type: literal(`${EthAccountType.Eoa}`), - /** - * Account supported methods. - */ - methods: array( - enums([ - `${EthMethod.PersonalSign}`, - `${EthMethod.Sign}`, - `${EthMethod.SignTransaction}`, - `${EthMethod.SignTypedDataV1}`, - `${EthMethod.SignTypedDataV3}`, - `${EthMethod.SignTypedDataV4}`, - ]), - ), -}); + /** + * Account supported methods. + */ + methods: array( + enums([ + `${EthMethod.PersonalSign}`, + `${EthMethod.Sign}`, + `${EthMethod.SignTransaction}`, + `${EthMethod.SignTypedDataV1}`, + `${EthMethod.SignTypedDataV3}`, + `${EthMethod.SignTypedDataV4}`, + ]), + ), + }), +); export type EthEoaAccount = Infer; -export const EthErc4337AccountStruct = object({ - ...BaseAccount, +export const EthErc4337AccountStruct = assign( + KeyringAccountStruct, + object({ + /** + * Account address. + */ + address: EthAddressStruct, - /** - * Account type. - */ - type: literal(`${EthAccountType.Erc4337}`), + /** + * Account type. + */ + type: literal(`${EthAccountType.Erc4337}`), - /** - * Account supported methods. - */ - methods: array( - enums([ - `${EthMethod.PersonalSign}`, - `${EthMethod.Sign}`, - `${EthMethod.SignTypedDataV1}`, - `${EthMethod.SignTypedDataV3}`, - `${EthMethod.SignTypedDataV4}`, - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, - ]), - ), -}); + /** + * Account supported methods. + */ + methods: array( + enums([ + `${EthMethod.PersonalSign}`, + `${EthMethod.Sign}`, + `${EthMethod.SignTypedDataV1}`, + `${EthMethod.SignTypedDataV3}`, + `${EthMethod.SignTypedDataV4}`, + `${EthMethod.PrepareUserOperation}`, + `${EthMethod.PatchUserOperation}`, + `${EthMethod.SignUserOperation}`, + ]), + ), + }), +); export type EthErc4337Account = Infer; From 944e9ee6736bfce1611b75fbd7cc1b76830b4cdb Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:36:19 +0200 Subject: [PATCH 04/14] feat: add type tests to ETH- and BTC-specific account types --- src/btc/types.test-d.ts | 7 +++++++ src/eth/types.test-d.ts | 9 +++++++++ src/utils.test-d.ts | 12 ++++++++++++ src/utils.test.ts | 7 +++++++ src/utils.ts | 28 ++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+) create mode 100644 src/btc/types.test-d.ts create mode 100644 src/utils.test-d.ts create mode 100644 src/utils.test.ts diff --git a/src/btc/types.test-d.ts b/src/btc/types.test-d.ts new file mode 100644 index 000000000..f3be51f5a --- /dev/null +++ b/src/btc/types.test-d.ts @@ -0,0 +1,7 @@ +import type { KeyringAccount } from '../api'; +import type { Extends } from '../utils'; +import { expectTrue } from '../utils'; +import type { BtcP2wpkhAccount } from './types'; + +// `BtcP2wpkhAccount` extends `KeyringAccount` +expectTrue>(); diff --git a/src/eth/types.test-d.ts b/src/eth/types.test-d.ts index 8e32bdbcd..d413ffc2a 100644 --- a/src/eth/types.test-d.ts +++ b/src/eth/types.test-d.ts @@ -1,6 +1,9 @@ import { expectAssignable, expectNotAssignable } from 'tsd'; +import type { KeyringAccount } from '../api'; import { EthAccountType } from '../api'; +import type { Extends } from '../utils'; +import { expectTrue } from '../utils'; import type { EthEoaAccount, EthErc4337Account } from './types'; import { EthMethod } from './types'; @@ -110,3 +113,9 @@ expectNotAssignable({ `${EthMethod.SignUserOperation}`, ], }); + +// `EthEoaAccount` extends `KeyringAccount` +expectTrue>(); + +// `EthErc4337Account` extends `KeyringAccount` +expectTrue>(); diff --git a/src/utils.test-d.ts b/src/utils.test-d.ts new file mode 100644 index 000000000..894d8f322 --- /dev/null +++ b/src/utils.test-d.ts @@ -0,0 +1,12 @@ +import type { Extends } from './utils'; +import { expectTrue } from './utils'; + +expectTrue(); + +// @ts-expect-error [test] Type `false` doesn't extend `true`. +expectTrue(); + +expectTrue>(); + +// @ts-expect-error [test] The first type doesn't extend the second type. +expectTrue>(); diff --git a/src/utils.test.ts b/src/utils.test.ts new file mode 100644 index 000000000..b54237d1b --- /dev/null +++ b/src/utils.test.ts @@ -0,0 +1,7 @@ +import { expectTrue } from './utils'; + +describe('expectTrue', () => { + it('does nothing since expectTrue is an empty function', () => { + expect(() => expectTrue()).not.toThrow(); + }); +}); diff --git a/src/utils.ts b/src/utils.ts index 7e4393c2f..e4617b88c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -42,3 +42,31 @@ export function strictMask( assert(value, struct, message); return value; } + +/** + * Type that resolves to `true` if `Child` extends `Base`, otherwise `false`. + * + * @example + * ```ts + * type A = Extends<{a: string, b: string}, {a: string}>; // true + * type B = Extends<{a: string}, {a: string, b: string}>; // false + * ``` + */ +export type Extends = Child extends Base ? true : false; + +/** + * Assert that a type extends `true`. It can be used, for example, to assert + * that a given type extends another type. + * + * @example + * ```ts + * expectTrue>(); // Ok + * expectTrue>(); // Error + * ``` + * + * This function follows the naming pattern used on `tsd`. + */ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function expectTrue(): void { + // Intentionally empty +} From 7112eeee8a19bbbb321ccd1231af599f27d501fd Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:37:32 +0200 Subject: [PATCH 05/14] chore: revert change to `jest.config.js` --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index 23e0bb535..bd7fddb65 100644 --- a/jest.config.js +++ b/jest.config.js @@ -148,7 +148,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: "jest-environment-node", + // testEnvironment: "jest-environment-node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, From a9854507c5e12267e1e7070cd36af976723eeb5a Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:48:46 +0200 Subject: [PATCH 06/14] chore: add missing empty line at the end of the file --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f80a6ee3a..93dd70aa8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,4 +2,4 @@ "cSpell.words": [ "bech32", "p2wpkh", "sendmany" ] -} \ No newline at end of file +} From 155338b0f44d947bd8e4bc365f60ddb7c11f49f1 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:57:15 +0200 Subject: [PATCH 07/14] chore: apply linter --- .vscode/settings.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 93dd70aa8..2e4e839f8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,3 @@ { - "cSpell.words": [ - "bech32", "p2wpkh", "sendmany" - ] + "cSpell.words": ["bech32", "p2wpkh", "sendmany"] } From 4dd7a6b60dd31cdc7e56455c750e6ccc74cde337 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 21:57:54 +0200 Subject: [PATCH 08/14] chore: move fix `EthErc4337Method` -> `EthMethod` to a different PR --- src/eth/types.test-d.ts | 32 ++++++++++++++++---------------- src/eth/types.ts | 12 +++++++++--- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/eth/types.test-d.ts b/src/eth/types.test-d.ts index d413ffc2a..bec12c935 100644 --- a/src/eth/types.test-d.ts +++ b/src/eth/types.test-d.ts @@ -5,7 +5,7 @@ import { EthAccountType } from '../api'; import type { Extends } from '../utils'; import { expectTrue } from '../utils'; import type { EthEoaAccount, EthErc4337Account } from './types'; -import { EthMethod } from './types'; +import { EthMethod, EthErc4337Method } from './types'; const id = '606a7759-b0fb-48e4-9874-bab62ff8e7eb'; const address = '0x000'; @@ -42,9 +42,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ], }); @@ -55,9 +55,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ], }); @@ -82,9 +82,9 @@ expectAssignable({ `${EthMethod.SignTypedDataV1}`, `${EthMethod.SignTypedDataV3}`, `${EthMethod.SignTypedDataV4}`, - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ], }); @@ -95,9 +95,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ], }); @@ -108,9 +108,9 @@ expectNotAssignable({ address, options: {}, methods: [ - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ], }); diff --git a/src/eth/types.ts b/src/eth/types.ts index 48e3b73b3..e545f3942 100644 --- a/src/eth/types.ts +++ b/src/eth/types.ts @@ -27,6 +27,12 @@ export enum EthMethod { SignTypedDataV1 = 'eth_signTypedData_v1', SignTypedDataV3 = 'eth_signTypedData_v3', SignTypedDataV4 = 'eth_signTypedData_v4', +} + +/** + * Supported Ethereum methods for ERC-4337 (Account Abstraction) accounts. + */ +export enum EthErc4337Method { // ERC-4337 methods PrepareUserOperation = 'eth_prepareUserOperation', PatchUserOperation = 'eth_patchUserOperation', @@ -87,9 +93,9 @@ export const EthErc4337AccountStruct = assign( `${EthMethod.SignTypedDataV1}`, `${EthMethod.SignTypedDataV3}`, `${EthMethod.SignTypedDataV4}`, - `${EthMethod.PrepareUserOperation}`, - `${EthMethod.PatchUserOperation}`, - `${EthMethod.SignUserOperation}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, ]), ), }), From ddd7ac9cfd17fc85a0951259954fb81d3aeb2edc Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 22:04:43 +0200 Subject: [PATCH 09/14] test: put `api.test.ts` file back --- src/api.test.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/api.test.ts diff --git a/src/api.test.ts b/src/api.test.ts new file mode 100644 index 000000000..c3b3d000b --- /dev/null +++ b/src/api.test.ts @@ -0,0 +1,37 @@ +import { assert } from 'superstruct'; + +import { KeyringAccountStruct } from './api'; + +const supportedKeyringAccountTypes = Object.keys( + KeyringAccountStruct.schema.type.schema, +) + .map((type: string) => `"${type}"`) + .join(','); + +describe('api', () => { + const baseAccount = { + id: '606a7759-b0fb-48e4-9874-bab62ff8e7eb', + address: '0x000', + options: {}, + methods: [], + }; + + describe('KeyringAccount', () => { + it.each([ + [undefined, 'undefined'], + [null, 'null'], + ['not:supported', '"not:supported"'], + ])( + 'throws an error if account type is: %s', + (type: any, typeAsStr: string) => { + const account = { + type, + ...baseAccount, + }; + expect(() => assert(account, KeyringAccountStruct)).toThrow( + `At path: type -- Expected one of \`${supportedKeyringAccountTypes}\`, but received: ${typeAsStr}`, + ); + }, + ); + }); +}); From 39e0f8baa07836668de7766fb56a708321036d05 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Wed, 22 May 2024 22:31:14 +0200 Subject: [PATCH 10/14] chore: update comments --- src/api.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index 666c477dc..4e45fb0c4 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,6 +6,9 @@ import { enums, array, literal, record, string, union } from 'superstruct'; import { exactOptional, object } from './superstruct'; import { UuidStruct } from './utils'; +// ! The `*AccountType` enums defined below should be kept in this file to +// ! avoid circular dependencies when the API is used by other files. + /** * Supported Ethereum account types. */ @@ -22,7 +25,9 @@ export enum BtcAccountType { } /** - * Generic account struct. + * A struct which represents a Keyring account object. It is abstract enough to + * be used with any blockchain. Specific blockchain account types should extend + * this struct. * * See {@link KeyringAccount}. */ From d1558d52c7931dfda1b3038aa1a0482c74742c2d Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 23 May 2024 15:13:25 +0200 Subject: [PATCH 11/14] chore: destructure schema instead of using `assign` --- src/btc/types.ts | 40 +++++++------- src/eth/types.ts | 118 +++++++++++++++++++++--------------------- src/internal/types.ts | 12 ++--- 3 files changed, 84 insertions(+), 86 deletions(-) diff --git a/src/btc/types.ts b/src/btc/types.ts index 1907020c4..50bdbc303 100644 --- a/src/btc/types.ts +++ b/src/btc/types.ts @@ -1,8 +1,9 @@ import { bech32 } from 'bech32'; import type { Infer } from 'superstruct'; -import { object, string, array, enums, refine, assign } from 'superstruct'; +import { string, array, enums, refine } from 'superstruct'; import { KeyringAccountStruct, BtcAccountType } from '../api'; +import { object } from '../superstruct'; export const BtcP2wpkhAddressStruct = refine( string(), @@ -27,24 +28,23 @@ export enum BtcMethod { SendMany = 'btc_sendmany', } -export const BtcP2wpkhAccountStruct = assign( - KeyringAccountStruct, - object({ - /** - * Account address. - */ - address: BtcP2wpkhAddressStruct, - - /** - * Account type. - */ - type: enums([`${BtcAccountType.P2wpkh}`]), - - /** - * Account supported methods. - */ - methods: array(enums([`${BtcMethod.SendMany}`])), - }), -); +export const BtcP2wpkhAccountStruct = object({ + ...KeyringAccountStruct.schema, + + /** + * Account address. + */ + address: BtcP2wpkhAddressStruct, + + /** + * Account type. + */ + type: enums([`${BtcAccountType.P2wpkh}`]), + + /** + * Account supported methods. + */ + methods: array(enums([`${BtcMethod.SendMany}`])), +}); export type BtcP2wpkhAccount = Infer; diff --git a/src/eth/types.ts b/src/eth/types.ts index e545f3942..4dd2f6a27 100644 --- a/src/eth/types.ts +++ b/src/eth/types.ts @@ -1,8 +1,8 @@ import type { Infer } from 'superstruct'; -import { object, array, enums, literal, assign } from 'superstruct'; +import { array, enums, literal } from 'superstruct'; import { EthAccountType, KeyringAccountStruct } from '../api'; -import { definePattern } from '../superstruct'; +import { object, definePattern } from '../superstruct'; export const EthBytesStruct = definePattern('EthBytes', /^0x[0-9a-f]*$/iu); @@ -39,66 +39,64 @@ export enum EthErc4337Method { SignUserOperation = 'eth_signUserOperation', } -export const EthEoaAccountStruct = assign( - KeyringAccountStruct, - object({ - /** - * Account address. - */ - address: EthAddressStruct, - - /** - * Account type. - */ - type: literal(`${EthAccountType.Eoa}`), - - /** - * Account supported methods. - */ - methods: array( - enums([ - `${EthMethod.PersonalSign}`, - `${EthMethod.Sign}`, - `${EthMethod.SignTransaction}`, - `${EthMethod.SignTypedDataV1}`, - `${EthMethod.SignTypedDataV3}`, - `${EthMethod.SignTypedDataV4}`, - ]), - ), - }), -); +export const EthEoaAccountStruct = object({ + ...KeyringAccountStruct.schema, + + /** + * Account address. + */ + address: EthAddressStruct, + + /** + * Account type. + */ + type: literal(`${EthAccountType.Eoa}`), + + /** + * Account supported methods. + */ + methods: array( + enums([ + `${EthMethod.PersonalSign}`, + `${EthMethod.Sign}`, + `${EthMethod.SignTransaction}`, + `${EthMethod.SignTypedDataV1}`, + `${EthMethod.SignTypedDataV3}`, + `${EthMethod.SignTypedDataV4}`, + ]), + ), +}); export type EthEoaAccount = Infer; -export const EthErc4337AccountStruct = assign( - KeyringAccountStruct, - object({ - /** - * Account address. - */ - address: EthAddressStruct, - - /** - * Account type. - */ - type: literal(`${EthAccountType.Erc4337}`), - - /** - * Account supported methods. - */ - methods: array( - enums([ - `${EthMethod.PersonalSign}`, - `${EthMethod.Sign}`, - `${EthMethod.SignTypedDataV1}`, - `${EthMethod.SignTypedDataV3}`, - `${EthMethod.SignTypedDataV4}`, - `${EthErc4337Method.PrepareUserOperation}`, - `${EthErc4337Method.PatchUserOperation}`, - `${EthErc4337Method.SignUserOperation}`, - ]), - ), - }), -); +export const EthErc4337AccountStruct = object({ + ...KeyringAccountStruct.schema, + + /** + * Account address. + */ + address: EthAddressStruct, + + /** + * Account type. + */ + type: literal(`${EthAccountType.Erc4337}`), + + /** + * Account supported methods. + */ + methods: array( + enums([ + `${EthMethod.PersonalSign}`, + `${EthMethod.Sign}`, + `${EthMethod.SignTypedDataV1}`, + `${EthMethod.SignTypedDataV3}`, + `${EthMethod.SignTypedDataV4}`, + `${EthErc4337Method.PrepareUserOperation}`, + `${EthErc4337Method.PatchUserOperation}`, + `${EthErc4337Method.SignUserOperation}`, + ]), + ), +}); export type EthErc4337Account = Infer; diff --git a/src/internal/types.ts b/src/internal/types.ts index 5ebfcdf95..f79acf0b5 100644 --- a/src/internal/types.ts +++ b/src/internal/types.ts @@ -1,5 +1,5 @@ import type { Infer, Struct } from 'superstruct'; -import { boolean, string, number, assign } from 'superstruct'; +import { boolean, string, number } from 'superstruct'; import { BtcAccountType, EthAccountType, KeyringAccountStruct } from '../api'; import { BtcP2wpkhAccountStruct } from '../btc/types'; @@ -30,7 +30,7 @@ export const InternalAccountMetadataStruct = object({ * Creates an `InternalAccount` from an existing account `superstruct` object. * * @param accountStruct - An account `superstruct` object. - * @returns The `InternalAccount` assocaited to `accountStruct`. + * @returns The `InternalAccount` associated to `accountStruct`. */ function asInternalAccountStruct( accountStruct: Struct, @@ -78,10 +78,10 @@ export type InternalAccountTypes = | InternalEthErc4337Account | InternalBtcP2wpkhAccount; -export const InternalAccountStruct = assign( - KeyringAccountStruct, - InternalAccountMetadataStruct, -); +export const InternalAccountStruct = object({ + ...KeyringAccountStruct.schema, + ...InternalAccountMetadataStruct.schema, +}); /** * Internal account representation. From 3473a7a14443e463b3b8b887eedc224efcd6d331 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Thu, 23 May 2024 15:18:58 +0200 Subject: [PATCH 12/14] chore: update jsdoc --- src/api.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/api.ts b/src/api.ts index 4e45fb0c4..809115eba 100644 --- a/src/api.ts +++ b/src/api.ts @@ -63,9 +63,8 @@ export const KeyringAccountStruct = object({ }); /** - * Generic account type. - * - * Represents an account with its properties and capabilities. + * Keyring Account type represents an accounts and its properties from the + * point of view of the keyring. */ export type KeyringAccount = Infer; From 91426bd93c4637ef87a1508aaf2c760ef1d81a13 Mon Sep 17 00:00:00 2001 From: Daniel Rocha Date: Fri, 24 May 2024 09:31:52 +0200 Subject: [PATCH 13/14] chore: fix typo Co-authored-by: Charly Chevalier --- src/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api.ts b/src/api.ts index 809115eba..c29ba5eeb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -63,7 +63,7 @@ export const KeyringAccountStruct = object({ }); /** - * Keyring Account type represents an accounts and its properties from the + * Keyring Account type represents an account and its properties from the * point of view of the keyring. */ export type KeyringAccount = Infer; From 0d414bf863b1295b72061c4fdf5b605af22609e3 Mon Sep 17 00:00:00 2001 From: Daniel Rocha <68558152+danroc@users.noreply.github.com> Date: Fri, 24 May 2024 09:35:08 +0200 Subject: [PATCH 14/14] chore: revert `enum` to `literal` --- src/btc/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/btc/types.ts b/src/btc/types.ts index 50bdbc303..7fcf02ad0 100644 --- a/src/btc/types.ts +++ b/src/btc/types.ts @@ -1,6 +1,6 @@ import { bech32 } from 'bech32'; import type { Infer } from 'superstruct'; -import { string, array, enums, refine } from 'superstruct'; +import { string, array, enums, refine, literal } from 'superstruct'; import { KeyringAccountStruct, BtcAccountType } from '../api'; import { object } from '../superstruct'; @@ -39,7 +39,7 @@ export const BtcP2wpkhAccountStruct = object({ /** * Account type. */ - type: enums([`${BtcAccountType.P2wpkh}`]), + type: literal(`${BtcAccountType.P2wpkh}`), /** * Account supported methods.