diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 8eff9494..fb71ccac 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -19,13 +19,15 @@ "@nabla-studio/chain-registry": "*", "@nabla-studio/wallet-registry": "*", "@keplr-wallet/types": "^0.12.38", - "@cosmostation/extension-client": "^0.1.15" + "@cosmostation/extension-client": "^0.1.15", + "base64-js": "^1.5.1" }, "peerDependencies": { "@cosmjs/amino": "^0.32.2", "@cosmjs/proto-signing": "^0.32.2", "cosmjs-types": "^0.9.0", - "long": "^5.2.3" + "long": "^5.2.3", + "@leapwallet/cosmos-snap-provider": "^0.1.25" }, "main": "./index.js", "module": "./index.js", diff --git a/packages/wallets/src/index.ts b/packages/wallets/src/index.ts index 28986885..10e64675 100644 --- a/packages/wallets/src/index.ts +++ b/packages/wallets/src/index.ts @@ -5,6 +5,7 @@ export * from './xdefi'; export * from './station'; export * from './okx'; export * from './shell'; +export * from './leap-metamask-snap'; export * from './wallet-connect'; export * from './keplr-mobile'; export * from './leap-mobile'; diff --git a/packages/wallets/src/leap-metamask-snap/extension.ts b/packages/wallets/src/leap-metamask-snap/extension.ts new file mode 100644 index 00000000..e0d125c8 --- /dev/null +++ b/packages/wallets/src/leap-metamask-snap/extension.ts @@ -0,0 +1,242 @@ +import { + ExtensionWallet, + type Key, + type SignOptions, + type SuggestChain, + assertIsDefined, + createClientNotExistError, + getClientFromExtension, +} from '@quirks/core'; +import type { Snap } from '@leapwallet/cosmos-snap-provider'; +import type { + OfflineAminoSigner, + StdSignDoc, + AminoSignResponse, + StdSignature, + AccountData, + Algo, +} from '@cosmjs/amino'; +import type { + OfflineDirectSigner, + DirectSignResponse, +} from '@cosmjs/proto-signing'; +import type { SignDoc } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import type { LeapMetamaskSnap } from './types'; +import Long from 'long'; +import { getChainInfo } from '../utils'; + +export class LeapMetamaskSnapWalletExtension extends ExtensionWallet { + snap?: Snap; + + override async init(): Promise { + assertIsDefined(this.options.windowKey); + + try { + this.client = await getClientFromExtension(this.options.windowKey); + + if (!this.client?.isMetamask && this.options.windowKey) { + throw createClientNotExistError('ethereum'); + } + + this.injected = true; + + if (this.client) { + this.addListeners(); + } + + return this.client; + } catch (err) { + this.injectionError = err as Error; + } + + return undefined; + } + + override async getAccount(chainId: string): Promise { + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const getKey = + cosmosSnapProvider.getKey ?? cosmosSnapProvider.default.getKey; + + const key = await getKey(chainId); + + return { + name: key.address, + address: new TextEncoder().encode(key.address), + bech32Address: key.address, + algo: key.algo, + pubKey: key.pubkey, + isKeystone: false, + isNanoLedger: false, + }; + } + + async getSignerAccount(chainId: string): Promise { + const key = await this.getAccount(chainId); + + return { + address: key.bech32Address, + algo: key.algo as Algo, + pubkey: key.pubKey!, + }; + } + + override async getAccounts(chainIds: string[]): Promise { + assertIsDefined(this.client); + + const keys = await Promise.allSettled( + chainIds.map((chainId) => this.getAccount(chainId)), + ); + + return keys + .map((key) => { + if (key.status === 'fulfilled') { + return key.value; + } + + return undefined; + }) + .filter((key) => key !== undefined) as Key[]; + } + + override async enable(): Promise { + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const getSnap = + cosmosSnapProvider.getSnap ?? cosmosSnapProvider.default.getSnap; + const connectSnap = + cosmosSnapProvider.connectSnap ?? cosmosSnapProvider.default.connectSnap; + const snap = await getSnap(); + + this.snap = snap; + + await connectSnap(this.snap?.id); + } + + override async disable(): Promise { + console.warn('disable method not implemented.'); + + return; + } + + override async getOfflineSigner( + chainId: string, + options?: SignOptions | undefined, + ): Promise { + assertIsDefined(this.client); + + return { + getAccounts: async () => [await this.getSignerAccount(chainId)], + signAmino: (signerAddress, signDoc) => + this.signAmino(chainId, signerAddress, signDoc, options), + signDirect: (signerAddress, signDoc) => + this.signDirect(chainId, signerAddress, signDoc), + }; + } + + override async getOfflineSignerOnlyAmino( + chainId: string, + options?: SignOptions | undefined, + ): Promise { + assertIsDefined(this.client); + + return { + getAccounts: async () => [await this.getSignerAccount(chainId)], + signAmino: (signerAddress, signDoc) => + this.signAmino(chainId, signerAddress, signDoc, options), + }; + } + + override async getOfflineSignerAuto( + chainId: string, + options?: SignOptions | undefined, + ): Promise { + assertIsDefined(this.client); + + return { + getAccounts: async () => [await this.getSignerAccount(chainId)], + signAmino: (signerAddress, signDoc) => + this.signAmino(chainId, signerAddress, signDoc, options), + signDirect: (signerAddress, signDoc) => + this.signDirect(chainId, signerAddress, signDoc), + }; + } + + override async signAmino( + chainId: string, + signer: string, + signDoc: StdSignDoc, + signOptions?: SignOptions | undefined, + ): Promise { + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const requestSignAmino = + cosmosSnapProvider.requestSignAmino ?? + cosmosSnapProvider.default.requestSignAmino; + + return requestSignAmino(chainId, signer, signDoc, signOptions); + } + + override async signDirect( + chainId: string, + signer: string, + signDoc: SignDoc, + ): Promise { + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const requestSignature = + cosmosSnapProvider.requestSignature ?? + cosmosSnapProvider.default.requestSignature; + + const signResponse = await requestSignature(chainId, signer, { + ...signDoc, + accountNumber: Long.fromString(signDoc.accountNumber.toString()), + }); + + return { + ...signResponse, + signed: { + ...signResponse.signed, + accountNumber: BigInt(signResponse.signed.accountNumber.toString()), + }, + }; + } + + override async signArbitrary( + chainId: string, + signer: string, + data: string | Uint8Array, + ): Promise { + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const signArbitrary = + cosmosSnapProvider.signArbitrary ?? + cosmosSnapProvider.default.signArbitrary; + + const base64js = await import('base64-js'); + const fromByteArray = + base64js.fromByteArray ?? base64js.default.fromByteArray; + + const payload = typeof data === 'string' ? data : fromByteArray(data); + + return signArbitrary(chainId, signer, payload); + } + + override verifyArbitrary(): Promise { + throw new Error('verifyArbitrary method not implemented.'); + } + + override async suggestTokens(): Promise { + console.warn('suggestTokens method not implemented.'); + return; + } + + override async suggestChains(suggestions: SuggestChain[]): Promise { + await this.enable(); + const cosmosSnapProvider = await import('@leapwallet/cosmos-snap-provider'); + const suggestChain = + cosmosSnapProvider.suggestChain ?? + cosmosSnapProvider.default.suggestChain; + + for (const suggestion of suggestions) { + const chainInfo = getChainInfo(suggestion.chain, suggestion.assetList); + + await suggestChain(chainInfo, {}); + } + } +} diff --git a/packages/wallets/src/leap-metamask-snap/index.ts b/packages/wallets/src/leap-metamask-snap/index.ts new file mode 100644 index 00000000..3f5c63f8 --- /dev/null +++ b/packages/wallets/src/leap-metamask-snap/index.ts @@ -0,0 +1,8 @@ +import { LeapMetamaskSnapWalletExtension } from './extension'; +import { leapMetamaskSnapOptions } from './registry'; + +const leapMetamaskSnapExtension = new LeapMetamaskSnapWalletExtension( + leapMetamaskSnapOptions, +); + +export { leapMetamaskSnapExtension }; diff --git a/packages/wallets/src/leap-metamask-snap/registry.ts b/packages/wallets/src/leap-metamask-snap/registry.ts new file mode 100644 index 00000000..9738aadd --- /dev/null +++ b/packages/wallets/src/leap-metamask-snap/registry.ts @@ -0,0 +1,7 @@ +import type { WalletOptions } from '@quirks/core'; +import { leapmetamasksnap } from '@nabla-studio/wallet-registry'; + +export const leapMetamaskSnapOptions: WalletOptions = { + ...leapmetamasksnap, + windowKey: 'ethereum', +}; diff --git a/packages/wallets/src/leap-metamask-snap/types.ts b/packages/wallets/src/leap-metamask-snap/types.ts new file mode 100644 index 00000000..40c40545 --- /dev/null +++ b/packages/wallets/src/leap-metamask-snap/types.ts @@ -0,0 +1,3 @@ +export interface LeapMetamaskSnap { + isMetamask: boolean; +} diff --git a/packages/wallets/vite.config.ts b/packages/wallets/vite.config.ts index c296258d..c2b93e24 100644 --- a/packages/wallets/vite.config.ts +++ b/packages/wallets/vite.config.ts @@ -49,6 +49,8 @@ export default defineConfig({ '@nabla-studio/chain-registry', '@nabla-studio/wallet-registry', '@cosmostation/extension-client', + '@leapwallet/cosmos-snap-provider', + 'base64-js', ], }, },