From 3ab0700fe9b8e0d546bdb4a28abff9254edf467d Mon Sep 17 00:00:00 2001 From: Ry Racherbaumer Date: Mon, 22 Jan 2024 18:43:26 -0600 Subject: [PATCH] refactor: update Keystore types --- src/Client.ts | 10 +- src/Message.ts | 6 +- src/authn/KeystoreAuthenticator.ts | 13 +- src/conversations/JobRunner.ts | 6 +- src/keystore/InMemoryKeystore.ts | 5 +- src/keystore/SnapKeystore.ts | 38 ++-- src/keystore/index.ts | 1 - src/keystore/interfaces.ts | 1 + .../providers/KeyGeneratorKeystoreProvider.ts | 4 +- .../providers/NetworkKeystoreProvider.ts | 4 +- src/keystore/providers/SnapProvider.ts | 8 +- .../providers/StaticKeystoreProvider.ts | 4 +- src/keystore/providers/interfaces.ts | 8 +- src/keystore/rpcDefinitions.ts | 181 ++++++++++++++++-- src/keystore/snapHelpers.ts | 68 ++++--- src/types/metamask.ts | 3 - test/conversations/JobRunner.test.ts | 4 +- test/keystore/encryption.test.ts | 5 +- 18 files changed, 272 insertions(+), 97 deletions(-) diff --git a/src/Client.ts b/src/Client.ts index cb32c09b1..98f3e54fe 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -28,7 +28,6 @@ import { KeystoreAuthenticator } from './authn' import { Flatten } from './utils/typedefs' import BackupClient, { BackupType } from './message-backup/BackupClient' import { createBackupClient } from './message-backup/BackupClientFactory' -import { Keystore } from './keystore' import { KeyGeneratorKeystoreProvider, KeystoreProvider, @@ -47,6 +46,7 @@ import { packageName, version } from './snapInfo.json' import { ExtractDecodedType } from './types/client' import type { WalletClient } from 'viem' import { Contacts } from './Contacts' +import { KeystoreInterfaces } from './keystore/rpcDefinitions' const { Compression } = proto // eslint-disable @typescript-eslint/explicit-module-boundary-types @@ -147,7 +147,7 @@ export type KeyStoreOptions = { * The client will attempt to use each one in sequence until one successfully * returns a Keystore instance */ - keystoreProviders: KeystoreProvider[] + keystoreProviders: KeystoreProvider[] /** * Enable the Keystore to persist conversations in the provided storage interface */ @@ -252,7 +252,7 @@ export function defaultOptions(opts?: Partial): ClientOptions { // eslint-disable-next-line @typescript-eslint/no-explicit-any export default class Client { address: string - keystore: Keystore + keystore: KeystoreInterfaces apiClient: ApiClient contacts: Contacts publicKeyBundle: PublicKeyBundle @@ -271,7 +271,7 @@ export default class Client { publicKeyBundle: PublicKeyBundle, apiClient: ApiClient, backupClient: BackupClient, - keystore: Keystore + keystore: KeystoreInterfaces ) { this.knownPublicKeyBundles = new Map< string, @@ -857,7 +857,7 @@ async function bootstrapKeystore( opts: ClientOptions, apiClient: ApiClient, wallet: Signer | null -): Promise { +) { for (const provider of opts.keystoreProviders) { try { return await provider.newKeystore(opts, apiClient, wallet ?? undefined) diff --git a/src/Message.ts b/src/Message.ts index f98a41e88..d32df3dc8 100644 --- a/src/Message.ts +++ b/src/Message.ts @@ -12,8 +12,8 @@ import { bytesToHex } from './crypto/utils' import { sha256 } from './crypto/encryption' import { ContentTypeId } from './MessageContent' import { dateToNs, nsToDate } from './utils' -import { Keystore } from './keystore' import { buildDecryptV1Request, getResultOrThrow } from './utils/keystore' +import { KeystoreInterfaces } from './keystore/rpcDefinitions' const headerBytesAndCiphertext = ( msg: proto.Message @@ -105,7 +105,7 @@ export class MessageV1 extends MessageBase implements proto.MessageV1 { } async decrypt( - keystore: Keystore, + keystore: KeystoreInterfaces, myPublicKeyBundle: PublicKeyBundle ): Promise { const responses = ( @@ -151,7 +151,7 @@ export class MessageV1 extends MessageBase implements proto.MessageV1 { } static async encode( - keystore: Keystore, + keystore: KeystoreInterfaces, payload: Uint8Array, sender: PublicKeyBundle, recipient: PublicKeyBundle, diff --git a/src/authn/KeystoreAuthenticator.ts b/src/authn/KeystoreAuthenticator.ts index 14c76d3f3..0b505a57e 100644 --- a/src/authn/KeystoreAuthenticator.ts +++ b/src/authn/KeystoreAuthenticator.ts @@ -1,7 +1,10 @@ import { authn } from '@xmtp/proto' -import { Keystore } from '../keystore' import { dateToNs } from '../utils' import Token from './Token' +import { + KeystoreInterface, + KeystoreInterfaces, +} from '../keystore/rpcDefinitions' const wrapToken = (token: authn.Token): Token => { if (token instanceof Token) { @@ -10,10 +13,12 @@ const wrapToken = (token: authn.Token): Token => { return new Token(token) } -export default class KeystoreAuthenticator { - private keystore: Keystore +export default class KeystoreAuthenticator< + T extends KeystoreInterfaces = KeystoreInterface, +> { + private keystore: T - constructor(keystore: Keystore) { + constructor(keystore: T) { this.keystore = keystore } diff --git a/src/conversations/JobRunner.ts b/src/conversations/JobRunner.ts index 2a58eb76f..3f86d166f 100644 --- a/src/conversations/JobRunner.ts +++ b/src/conversations/JobRunner.ts @@ -1,8 +1,8 @@ import { keystore } from '@xmtp/proto' import { Mutex } from 'async-mutex' -import { Keystore } from '../keystore' import Long from 'long' import { dateToNs, nsToDate } from '../utils' +import { KeystoreInterfaces } from '../keystore/rpcDefinitions' const CLOCK_SKEW_OFFSET_MS = 10000 @@ -13,10 +13,10 @@ type UpdateJob = (lastRun: Date | undefined) => Promise export default class JobRunner { readonly jobType: JobType readonly mutex: Mutex - readonly keystore: Keystore + readonly keystore: KeystoreInterfaces disableOffset: boolean = false - constructor(jobType: JobType, keystore: Keystore) { + constructor(jobType: JobType, keystore: KeystoreInterfaces) { this.jobType = jobType this.mutex = new Mutex() this.keystore = keystore diff --git a/src/keystore/InMemoryKeystore.ts b/src/keystore/InMemoryKeystore.ts index 39ee8f687..71b8531c9 100644 --- a/src/keystore/InMemoryKeystore.ts +++ b/src/keystore/InMemoryKeystore.ts @@ -5,7 +5,7 @@ import { } from './../crypto/PrivateKeyBundle' import { InvitationV1, SealedInvitation } from './../Invitation' import { PrivateKey, PublicKeyBundle } from '../crypto' -import { Keystore, TopicData } from './interfaces' +import { TopicData } from './interfaces' import { decryptV1, encryptV1, encryptV2, decryptV2 } from './encryption' import { KeystoreError } from './errors' import { @@ -34,6 +34,7 @@ import { userPreferencesEncrypt, generateUserPreferencesTopic, } from '../crypto/selfEncryption' +import { KeystoreInterface } from '..' const { ErrorCode } = keystore @@ -57,7 +58,7 @@ async function deriveKey( ) } -export default class InMemoryKeystore implements Keystore { +export default class InMemoryKeystore implements KeystoreInterface { private v1Keys: PrivateKeyBundleV1 private v2Keys: PrivateKeyBundleV2 // Do I need this? private v1Store: V1Store diff --git a/src/keystore/SnapKeystore.ts b/src/keystore/SnapKeystore.ts index 84e86911f..26564ab81 100644 --- a/src/keystore/SnapKeystore.ts +++ b/src/keystore/SnapKeystore.ts @@ -1,38 +1,32 @@ -import { Keystore } from './interfaces' import { SnapMeta, snapRPC } from './snapHelpers' import type { XmtpEnv } from '../Client' -import { apiDefs } from './rpcDefinitions' - -async function getResponse( - method: T, - req: Uint8Array | null, - meta: SnapMeta, - snapId: string -): Promise<(typeof apiDefs)[T]['res']> { - return snapRPC(method, apiDefs[method], req, meta, snapId) -} +import { + SnapKeystoreApiEntries, + SnapKeystoreApiRequestValues, + SnapKeystoreInterface, + snapApiDefs, +} from './rpcDefinitions' export function SnapKeystore( walletAddress: string, env: XmtpEnv, snapId: string -): Keystore { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const generatedMethods: any = {} +) { + const generatedMethods: Partial = {} const snapMeta: SnapMeta = { walletAddress, env, } - for (const [method, apiDef] of Object.entries(apiDefs)) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - generatedMethods[method] = async (req: any) => { - if (!apiDef.req) { - return getResponse(method as keyof Keystore, null, snapMeta, snapId) + for (const [method, rpc] of Object.entries( + snapApiDefs + ) as SnapKeystoreApiEntries) { + generatedMethods[method] = async (req?: SnapKeystoreApiRequestValues) => { + if (!rpc.req) { + return snapRPC(method, rpc, undefined, snapMeta, snapId) } - - return getResponse(method as keyof Keystore, req, snapMeta, snapId) + return snapRPC(method, rpc, req, snapMeta, snapId) } } @@ -42,5 +36,5 @@ export function SnapKeystore( async getAccountAddress() { return walletAddress }, - } + } as SnapKeystoreInterface } diff --git a/src/keystore/index.ts b/src/keystore/index.ts index 9072433a0..d5e3787c7 100644 --- a/src/keystore/index.ts +++ b/src/keystore/index.ts @@ -1,6 +1,5 @@ export { default as InMemoryKeystore } from './InMemoryKeystore' export { SnapKeystore } from './SnapKeystore' -export { apiDefs as keystoreApiDefs } from './rpcDefinitions' export { V1Store, V2Store } from './conversationStores' export * from './encryption' export * from './errors' diff --git a/src/keystore/interfaces.ts b/src/keystore/interfaces.ts index 15382f7b2..7ea373309 100644 --- a/src/keystore/interfaces.ts +++ b/src/keystore/interfaces.ts @@ -4,6 +4,7 @@ import { WithoutUndefined } from '../utils/typedefs' /** * A Keystore is responsible for holding the user's XMTP private keys and using them to encrypt/decrypt/sign messages. * Keystores are instantiated using a `KeystoreProvider` + * @deprecated Use `KeystoreInterface` instead */ export interface Keystore { /** diff --git a/src/keystore/providers/KeyGeneratorKeystoreProvider.ts b/src/keystore/providers/KeyGeneratorKeystoreProvider.ts index 3ae3775f3..0fbf4d88d 100644 --- a/src/keystore/providers/KeyGeneratorKeystoreProvider.ts +++ b/src/keystore/providers/KeyGeneratorKeystoreProvider.ts @@ -6,8 +6,8 @@ import { KeystoreProviderUnavailableError } from './errors' import { buildPersistenceFromOptions } from './helpers' import NetworkKeyManager from './NetworkKeyManager' import type { Signer } from '../../types/Signer' -import type { Keystore } from '../interfaces' import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' +import { KeystoreInterface } from '../rpcDefinitions' /** * KeyGeneratorKeystoreProvider will create a new XMTP `PrivateKeyBundle` and persist it to the network @@ -19,7 +19,7 @@ export default class KeyGeneratorKeystoreProvider implements KeystoreProvider { opts: KeystoreProviderOptions, apiClient: ApiClient, wallet?: Signer - ): Promise { + ): Promise { if (!wallet) { throw new KeystoreProviderUnavailableError( 'Wallet required to generate new keys' diff --git a/src/keystore/providers/NetworkKeystoreProvider.ts b/src/keystore/providers/NetworkKeystoreProvider.ts index 798dff7b0..96e06df27 100644 --- a/src/keystore/providers/NetworkKeystoreProvider.ts +++ b/src/keystore/providers/NetworkKeystoreProvider.ts @@ -4,9 +4,9 @@ import { KeystoreProvider, KeystoreProviderOptions } from './interfaces' import NetworkKeyLoader from './NetworkKeyManager' import { KeystoreProviderUnavailableError } from './errors' import TopicPersistence from '../persistence/TopicPersistence' -import { Keystore } from '../interfaces' import InMemoryKeystore from '../InMemoryKeystore' import { buildPersistenceFromOptions } from './helpers' +import { KeystoreInterface } from '../rpcDefinitions' /** * NetworkKeystoreProvider will look on the XMTP network for an `EncryptedPrivateKeyBundle` @@ -18,7 +18,7 @@ export default class NetworkKeystoreProvider implements KeystoreProvider { opts: KeystoreProviderOptions, apiClient: ApiClient, wallet?: Signer - ): Promise { + ): Promise { if (!wallet) { throw new KeystoreProviderUnavailableError('No wallet provided') } diff --git a/src/keystore/providers/SnapProvider.ts b/src/keystore/providers/SnapProvider.ts index a22787a14..54610bf26 100644 --- a/src/keystore/providers/SnapProvider.ts +++ b/src/keystore/providers/SnapProvider.ts @@ -1,5 +1,4 @@ import { KeystoreProviderUnavailableError } from './errors' -import { Keystore } from '../interfaces' import { KeystoreProvider, KeystoreProviderOptions } from './interfaces' import { SnapKeystore } from '../SnapKeystore' import { @@ -17,6 +16,7 @@ import { PrivateKeyBundleV1, decodePrivateKeyBundle } from '../../crypto' import KeyGeneratorKeystoreProvider from './KeyGeneratorKeystoreProvider' import type { XmtpEnv } from '../../Client' import { semverGreaterThan } from '../../utils/semver' +import { SnapKeystoreInterface } from '../rpcDefinitions' const { GetKeystoreStatusResponse_KeystoreStatus: KeystoreStatus } = keystore export const SNAP_LOCAL_ORIGIN = 'local:http://localhost:8080' @@ -27,7 +27,9 @@ export const SNAP_LOCAL_ORIGIN = 'local:http://localhost:8080' * 2. Check if the user has already setup the Snap with the appropriate keys * 3. If not, will get keys from the network or create new keys and store them in the Snap */ -export default class SnapKeystoreProvider implements KeystoreProvider { +export default class SnapKeystoreProvider + implements KeystoreProvider +{ snapId: string snapVersion?: string @@ -40,7 +42,7 @@ export default class SnapKeystoreProvider implements KeystoreProvider { opts: KeystoreProviderOptions, apiClient: ApiClient, wallet?: Signer - ): Promise { + ) { if (!wallet) { throw new KeystoreProviderUnavailableError('No wallet provided') } diff --git a/src/keystore/providers/StaticKeystoreProvider.ts b/src/keystore/providers/StaticKeystoreProvider.ts index 1a02bb0c8..f4ade4596 100644 --- a/src/keystore/providers/StaticKeystoreProvider.ts +++ b/src/keystore/providers/StaticKeystoreProvider.ts @@ -1,5 +1,4 @@ import { KeystoreProviderUnavailableError } from './errors' -import { Keystore } from '../interfaces' import type { KeystoreProvider, KeystoreProviderOptions } from './interfaces' import InMemoryKeystore from '../InMemoryKeystore' import { @@ -7,6 +6,7 @@ import { PrivateKeyBundleV2, } from '../../crypto/PrivateKeyBundle' import { buildPersistenceFromOptions } from './helpers' +import { KeystoreInterface } from '../rpcDefinitions' /** * StaticKeystoreProvider will look for a `privateKeyOverride` in the provided options, @@ -16,7 +16,7 @@ import { buildPersistenceFromOptions } from './helpers' * the client to continue iterating through the `KeystoreProviders` list. */ export default class StaticKeystoreProvider implements KeystoreProvider { - async newKeystore(opts: KeystoreProviderOptions): Promise { + async newKeystore(opts: KeystoreProviderOptions): Promise { const { privateKeyOverride } = opts if (!privateKeyOverride) { throw new KeystoreProviderUnavailableError( diff --git a/src/keystore/providers/interfaces.ts b/src/keystore/providers/interfaces.ts index 6c5102685..113fd9c46 100644 --- a/src/keystore/providers/interfaces.ts +++ b/src/keystore/providers/interfaces.ts @@ -1,8 +1,8 @@ import type { XmtpEnv, PreEventCallbackOptions } from '../../Client' import type { Signer } from '../../types/Signer' -import type { Keystore } from '../interfaces' import type { ApiClient } from '../../ApiClient' import { Persistence } from '../persistence' +import { KeystoreInterface, KeystoreInterfaces } from '../rpcDefinitions' export type KeystoreProviderOptions = { env: XmtpEnv @@ -16,10 +16,12 @@ export type KeystoreProviderOptions = { * A Keystore Provider is responsible for either creating a Keystore instance or throwing a KeystoreUnavailableError * It is typically used once on application startup to bootstrap the Keystore and load/decrypt the user's private keys */ -export interface KeystoreProvider { +export interface KeystoreProvider< + T extends KeystoreInterfaces = KeystoreInterface, +> { newKeystore( opts: KeystoreProviderOptions, apiClient: ApiClient, wallet?: Signer - ): Promise + ): Promise } diff --git a/src/keystore/rpcDefinitions.ts b/src/keystore/rpcDefinitions.ts index 3d04d5208..6c823d890 100644 --- a/src/keystore/rpcDefinitions.ts +++ b/src/keystore/rpcDefinitions.ts @@ -1,88 +1,245 @@ -import { keystore, authn, publicKey, signature } from '@xmtp/proto' +import { keystore, authn, publicKey, privateKey, signature } from '@xmtp/proto' import { Reader, Writer } from 'protobufjs/minimal' +import { Flatten } from '../utils/typedefs' -type Codec = { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type KeystoreRPCCodec = { decode(input: Reader | Uint8Array, length?: number): T encode(message: T, writer?: Writer): Writer } -export type RPC = { - req: Codec | null - res: Codec +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type KeystoreRPC = { + req: KeystoreRPCCodec | null + res: KeystoreRPCCodec } +type Entries = { + [K in keyof T]: [K, T[K]] +}[keyof T][] + +type Values = { + [K in keyof T]: T[K] +}[keyof T] + type ApiDefs = { + [key: string]: KeystoreRPC +} + +type ApiInterface = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: (...args: any[]) => any +} + +type OtherKeyStoreMethods = { + /** + * Get the account address of the wallet used to create the Keystore + */ + getAccountAddress(): Promise +} + +type ExtractInterface = Flatten< + { + [K in keyof T]: T[K] extends KeystoreRPC + ? T[K]['req'] extends null + ? () => Promise + : (req: Req) => Promise + : never + } & OtherKeyStoreMethods +> + +type ExtractInterfaceRequestEncoders = { + [K in keyof T]: T[K]['req'] extends KeystoreRPCCodec + ? T[K]['req']['encode'] + : never +} + +type ExtractInterfaceResponseDecoders = { + [K in keyof T]: T[K]['res'] extends KeystoreRPCCodec + ? T[K]['res']['decode'] + : never +} + +type ExtractInterfaceRequestValues = { // eslint-disable-next-line @typescript-eslint/no-explicit-any - [k: string]: RPC + [K in keyof T]: T[K] extends (...args: any[]) => any + ? Parameters[0] + : never } -export const apiDefs: ApiDefs = { +export const apiDefs = { + /** + * Decrypt a batch of V1 messages + */ decryptV1: { req: keystore.DecryptV1Request, res: keystore.DecryptResponse, }, + /** + * Decrypt a batch of V2 messages + */ + decryptV2: { + req: keystore.DecryptV2Request, + res: keystore.DecryptResponse, + }, + /** + * Encrypt a batch of V1 messages + */ encryptV1: { req: keystore.EncryptV1Request, res: keystore.EncryptResponse, }, + /** + * Encrypt a batch of V2 messages + */ encryptV2: { req: keystore.EncryptV2Request, res: keystore.EncryptResponse, }, - decryptV2: { - req: keystore.DecryptV2Request, - res: keystore.DecryptResponse, - }, + /** + * Take a batch of invite messages and store the `TopicKeys` for later use in + * decrypting messages + */ saveInvites: { req: keystore.SaveInvitesRequest, res: keystore.SaveInvitesResponse, }, + /** + * Create a sealed/encrypted invite and store the Topic keys in the Keystore + * for later use. The returned invite payload must be sent to the network for + * the other party to be able to communicate. + */ createInvite: { req: keystore.CreateInviteRequest, res: keystore.CreateInviteResponse, }, + /** + * Create an XMTP auth token to be used as a header on XMTP API requests + */ createAuthToken: { req: keystore.CreateAuthTokenRequest, res: authn.Token, }, + /** + * Sign the provided digest with either the `IdentityKey` or a specified + * `PreKey` + */ signDigest: { req: keystore.SignDigestRequest, res: signature.Signature, }, + /** + * Get the `PublicKeyBundle` associated with the Keystore's private keys + */ getPublicKeyBundle: { req: null, res: publicKey.PublicKeyBundle, }, + /** + * Export the private keys. May throw an error if the keystore implementation + * does not allow this operation + */ + getPrivateKeyBundle: { + req: null, + res: privateKey.PrivateKeyBundleV1, + }, + /** + * Save V1 Conversations + */ saveV1Conversations: { req: keystore.SaveV1ConversationsRequest, res: keystore.SaveV1ConversationsResponse, }, + /** + * Get a list of V1 conversations + */ getV1Conversations: { req: null, res: keystore.GetConversationsResponse, }, + /** + * Get a list of V2 conversations + */ getV2Conversations: { req: null, res: keystore.GetConversationsResponse, }, + /** + * Get a refresh job from the persistence + */ getRefreshJob: { req: keystore.GetRefreshJobRequest, res: keystore.GetRefreshJobResponse, }, + /** + * Sets the time of a refresh job + */ setRefreshJob: { req: keystore.SetRefeshJobRequest, res: keystore.SetRefreshJobResponse, }, + /** + * Encrypt a batch of messages to yourself + */ selfEncrypt: { req: keystore.SelfEncryptRequest, res: keystore.SelfEncryptResponse, }, + /** + * Decrypt a batch of messages to yourself + */ selfDecrypt: { req: keystore.SelfDecryptRequest, res: keystore.DecryptResponse, }, + /** + * Get the private preferences topic identifier + */ getPrivatePreferencesTopicIdentifier: { req: null, res: keystore.GetPrivatePreferencesTopicIdentifierResponse, }, -} as const +} + +export type KeystoreApiDefs = typeof apiDefs +export type KeystoreApiMethods = keyof KeystoreApiDefs +export type KeystoreInterface = ExtractInterface +export type KeystoreApiEntries = Entries +export type KeystoreApiRequestEncoders = + ExtractInterfaceRequestEncoders +export type KeystoreApiResponseDecoders = + ExtractInterfaceResponseDecoders +export type KeystoreInterfaceRequestValues = + ExtractInterfaceRequestValues +export type KeystoreApiRequestValues = Values + +export const snapApiDefs = { + ...apiDefs, + getKeystoreStatus: { + req: keystore.GetKeystoreStatusRequest, + res: keystore.GetKeystoreStatusResponse, + }, + initKeystore: { + req: keystore.InitKeystoreRequest, + res: keystore.InitKeystoreResponse, + }, +} + +export type SnapKeystoreApiDefs = typeof snapApiDefs +export type SnapKeystoreApiMethods = keyof SnapKeystoreApiDefs +export type SnapKeystoreInterface = ExtractInterface +export type SnapKeystoreApiEntries = Entries +export type SnapKeystoreApiRequestEncoders = + ExtractInterfaceRequestEncoders +export type SnapKeystoreApiResponseDecoders = + ExtractInterfaceResponseDecoders +export type SnapKeystoreInterfaceRequestValues = + ExtractInterfaceRequestValues +export type SnapKeystoreApiRequestValues = + Values + +/** + * A Keystore is responsible for holding the user's XMTP private keys and using them to encrypt/decrypt/sign messages. + * Keystores are instantiated using a `KeystoreProvider` + */ +export type KeystoreInterfaces = KeystoreInterface | SnapKeystoreInterface diff --git a/src/keystore/snapHelpers.ts b/src/keystore/snapHelpers.ts index 56098bfa4..292d0ee6e 100644 --- a/src/keystore/snapHelpers.ts +++ b/src/keystore/snapHelpers.ts @@ -1,5 +1,11 @@ -import { keystore as keystoreProto } from '@xmtp/proto' -import type { RPC } from './rpcDefinitions' +import { keystore } from '@xmtp/proto' +import type { + SnapKeystoreApiDefs, + SnapKeystoreApiMethods, + SnapKeystoreInterfaceRequestValues, + SnapKeystoreApiRequestEncoders, + SnapKeystoreApiResponseDecoders, +} from './rpcDefinitions' import { b64Decode, b64Encode } from '../utils/bytes' import { KeystoreError } from './errors' import { PrivateKeyBundleV1 } from '../crypto' @@ -12,7 +18,7 @@ const { InitKeystoreResponse, GetKeystoreStatusRequest, GetKeystoreStatusResponse, -} = keystoreProto +} = keystore export type SnapMeta = { walletAddress: string @@ -24,16 +30,21 @@ type SnapParams = { req?: string } -export async function snapRPC( - method: string, - codecs: RPC, - req: Req, +type SnapResponse = { + res: string | string[] +} + +export async function snapRPC( + method: T, + rpc: SnapKeystoreApiDefs[T], + req: SnapKeystoreInterfaceRequestValues[T], meta: SnapMeta, snapId: string -): Promise { +) { let reqParam = null - if (codecs.req) { - const reqBytes = codecs.req.encode(req).finish() + if (rpc.req) { + const encoder = rpc.req.encode as SnapKeystoreApiRequestEncoders[T] + const reqBytes = encoder(req).finish() reqParam = b64Encode(reqBytes, 0, reqBytes.length) } @@ -42,20 +53,21 @@ export async function snapRPC( throw new Error('Unexpected array response') } - return codecs.res.decode(b64Decode(responseString)) + const decoder = rpc.res.decode as SnapKeystoreApiResponseDecoders[T] + return decoder(b64Decode(responseString)) } export async function snapRequest( - method: string, + method: SnapKeystoreApiMethods, req: string | null, meta: SnapMeta, snapId: string -): Promise { +) { const params: SnapParams = { meta } if (typeof req === 'string') { params.req = req } - const response = await getEthereum().request({ + const response = await getEthereum()?.request({ method: 'wallet_invokeSnap', params: { snapId, @@ -66,12 +78,11 @@ export async function snapRequest( }, }) - if (!response || typeof response !== 'object') { + if (!response || !response.res) { throw new Error('No response value') } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (response as any).res as unknown as string + return response.res } export type Snap = { @@ -143,10 +154,10 @@ export async function hasMetamaskWithSnaps() { return false } -export async function getSnaps(): Promise { - return (await getEthereum()?.request({ +export async function getSnaps() { + return await getEthereum()?.request({ method: 'wallet_getSnaps', - })) as unknown as GetSnapsResponse + }) } export async function getSnap( @@ -156,11 +167,16 @@ export async function getSnap( try { const snaps = await getSnaps() - return Object.values(snaps).find( - (snap) => - snap.id === snapId && - (!version || isSameMajorVersion(snap.version, version)) - ) + if (snaps) { + return Object.values(snaps).find( + (snap) => + snap && + snap.id === snapId && + (!version || isSameMajorVersion(snap.version, version)) + ) + } + + return undefined } catch (e) { console.warn('Failed to obtain installed snap', e) return undefined @@ -183,6 +199,7 @@ const getWalletStatusCodec = { req: GetKeystoreStatusRequest, res: GetKeystoreStatusResponse, } + export async function getWalletStatus(meta: SnapMeta, snapId: string) { const response = await snapRPC( 'getKeystoreStatus', @@ -210,6 +227,7 @@ const initKeystoreCodec = { req: InitKeystoreRequest, res: InitKeystoreResponse, } + export async function initSnap( bundle: PrivateKeyBundleV1, env: XmtpEnv, diff --git a/src/types/metamask.ts b/src/types/metamask.ts index ee1a7a72f..7bedde4b8 100644 --- a/src/types/metamask.ts +++ b/src/types/metamask.ts @@ -1,7 +1,4 @@ -/* eslint-disable*/ - import { MetaMaskInpageProvider } from '@metamask/providers' -import type { providers } from 'ethers' type EthereumType = MetaMaskInpageProvider & { setProvider?: (provider: MetaMaskInpageProvider) => void diff --git a/test/conversations/JobRunner.test.ts b/test/conversations/JobRunner.test.ts index 9bd539c26..20deef2a4 100644 --- a/test/conversations/JobRunner.test.ts +++ b/test/conversations/JobRunner.test.ts @@ -1,7 +1,7 @@ import { InMemoryKeystore, InMemoryPersistence, - Keystore, + KeystoreInterface, PrivateKeyBundleV1, nsToDate, } from '../../src' @@ -10,7 +10,7 @@ import JobRunner from '../../src/conversations/JobRunner' import { newWallet, sleep } from '../helpers' describe('JobRunner', () => { - let keystore: Keystore + let keystore: KeystoreInterface beforeEach(async () => { const bundle = await PrivateKeyBundleV1.generate(newWallet()) diff --git a/test/keystore/encryption.test.ts b/test/keystore/encryption.test.ts index b3246b086..9b0681042 100644 --- a/test/keystore/encryption.test.ts +++ b/test/keystore/encryption.test.ts @@ -1,4 +1,3 @@ -import { Keystore } from '../../src/keystore' import { Ciphertext } from '../../src/crypto' import { PrivateKeyBundleV1 } from './../../src/crypto/PrivateKeyBundle' import { decryptV1, encryptV1 } from '../../src/keystore/encryption' @@ -7,12 +6,12 @@ import { Wallet } from 'ethers' import { equalBytes } from '../../src/crypto/utils' import { newWallet } from '../helpers' import { InMemoryKeystore } from '../../src/keystore' -import { InMemoryPersistence } from '../../src' +import { InMemoryPersistence, KeystoreInterface } from '../../src' describe('encryption primitives', () => { let aliceKeys: PrivateKeyBundleV1 let aliceWallet: Wallet - let aliceKeystore: Keystore + let aliceKeystore: KeystoreInterface let bobKeys: PrivateKeyBundleV1 let bobWallet: Wallet