From 78d65e05ae6f6a1607f2e03b94e44a62e995c91e Mon Sep 17 00:00:00 2001 From: Bojan Angjelkoski Date: Sun, 25 Sep 2022 17:17:16 +0200 Subject: [PATCH] feat: cosmos wallet strategy --- .../src/Cosmos/CosmosWalletStrategy.ts | 90 +++++++++ packages/wallet-ts/src/Cosmos/index.ts | 1 + .../src/Cosmos/strategies/Cosmostation.ts | 190 ++++++++++++++++++ .../wallet-ts/src/Cosmos/strategies/Keplr.ts | 129 ++++++++++++ .../wallet-ts/src/Cosmos/strategies/Leap.ts | 131 ++++++++++++ .../wallet-ts/src/Cosmos/types/strategy.ts | 31 +++ packages/wallet-ts/src/index.ts | 1 + 7 files changed, 573 insertions(+) create mode 100644 packages/wallet-ts/src/Cosmos/CosmosWalletStrategy.ts create mode 100644 packages/wallet-ts/src/Cosmos/strategies/Cosmostation.ts create mode 100644 packages/wallet-ts/src/Cosmos/strategies/Keplr.ts create mode 100644 packages/wallet-ts/src/Cosmos/strategies/Leap.ts create mode 100644 packages/wallet-ts/src/Cosmos/types/strategy.ts diff --git a/packages/wallet-ts/src/Cosmos/CosmosWalletStrategy.ts b/packages/wallet-ts/src/Cosmos/CosmosWalletStrategy.ts new file mode 100644 index 000000000..ab64502a0 --- /dev/null +++ b/packages/wallet-ts/src/Cosmos/CosmosWalletStrategy.ts @@ -0,0 +1,90 @@ +import { AccountAddress } from '@injectivelabs/ts-types' +import { DirectSignResponse } from '@cosmjs/proto-signing' +import { Msgs } from '@injectivelabs/sdk-ts' +import { GeneralException } from '@injectivelabs/exceptions' +import { Wallet } from '../wallet-strategy/types' +import Keplr from './strategies/Keplr' +import Leap from './strategies/Leap' +import Cosmostation from './strategies/Cosmostation' +import { + ConcreteCosmosWalletStrategy, + CosmosWalletStrategyArguments, +} from './types/strategy' + +const createWallet = ({ + wallet, + args, +}: { + wallet: Wallet + args: CosmosWalletStrategyArguments +}): ConcreteCosmosWalletStrategy | undefined => { + switch (wallet) { + case Wallet.Keplr: + return new Keplr({ ...args }) + case Wallet.Leap: + return new Leap({ ...args }) + case Wallet.Cosmostation: + return new Cosmostation({ ...args }) + default: + throw new GeneralException( + new Error(`The ${wallet} concrete wallet strategy is not supported`), + ) + } +} + +const createWallets = ( + args: CosmosWalletStrategyArguments, +): Record => + Object.values(Wallet).reduce( + (strategies, wallet) => ({ + ...strategies, + [wallet]: createWallet({ wallet, args }), + }), + {} as Record, + ) + +export default class CosmosWalletStrategy { + public strategies: Record + + public wallet: Wallet + + constructor(args: CosmosWalletStrategyArguments) { + this.strategies = createWallets(args) + this.wallet = args.wallet || Wallet.Keplr + } + + public getWallet(): Wallet { + return this.wallet + } + + public setWallet(wallet: Wallet) { + this.wallet = wallet + } + + public getStrategy(): ConcreteCosmosWalletStrategy { + if (!this.strategies[this.wallet]) { + throw new GeneralException( + new Error(`Wallet ${this.wallet} is not enabled/available!`), + ) + } + + return this.strategies[this.wallet] as ConcreteCosmosWalletStrategy + } + + public getAddresses(): Promise { + return this.getStrategy().getAddresses() + } + + public async sendTransaction(tx: DirectSignResponse): Promise { + return this.getStrategy().sendTransaction(tx) + } + + public async signTransaction(data: { + address: string + memo: string + gas: string + message: Msgs | Msgs[] + }): Promise { + return this.getStrategy().signTransaction(data) + } +} diff --git a/packages/wallet-ts/src/Cosmos/index.ts b/packages/wallet-ts/src/Cosmos/index.ts index 784dab09f..b6b8ebae8 100644 --- a/packages/wallet-ts/src/Cosmos/index.ts +++ b/packages/wallet-ts/src/Cosmos/index.ts @@ -1 +1,2 @@ export * from './endpoints' +export { default as CosmosWalletStrategy } from './CosmosWalletStrategy' diff --git a/packages/wallet-ts/src/Cosmos/strategies/Cosmostation.ts b/packages/wallet-ts/src/Cosmos/strategies/Cosmostation.ts new file mode 100644 index 000000000..92f9baab1 --- /dev/null +++ b/packages/wallet-ts/src/Cosmos/strategies/Cosmostation.ts @@ -0,0 +1,190 @@ +/* eslint-disable class-methods-use-this */ +import { CosmosChainId } from '@injectivelabs/ts-types' +import { + UnspecifiedErrorCode, + CosmosWalletException, + TransactionException, + ErrorType, +} from '@injectivelabs/exceptions' +import { DEFAULT_STD_FEE } from '@injectivelabs/utils' +import { + createTxRawFromSigResponse, + createTransactionAndCosmosSignDocForAddressAndMsg, +} from '@injectivelabs/sdk-ts' +import type { Msgs } from '@injectivelabs/sdk-ts' +import { cosmos, InstallError, Cosmos } from '@cosmostation/extension-client' +import { DirectSignResponse, makeSignDoc } from '@cosmjs/proto-signing' +import { SEND_TRANSACTION_MODE } from '@cosmostation/extension-client/cosmos' +import { ConcreteCosmosWalletStrategy } from '../types/strategy' +import { WalletAction } from '../../wallet-strategy/types/enums' +import { getEndpointsFromChainId } from '../endpoints' + +const INJECTIVE_CHAIN_NAME = 'injective' + +const getChainNameFromChainId = (chainId: CosmosChainId) => { + const [chainName] = chainId.split('-') + + return chainName +} + +export default class Cosmostation implements ConcreteCosmosWalletStrategy { + public chainName: string + + public provider?: Cosmos + + public chainId: CosmosChainId + + constructor(args: { chainId: CosmosChainId }) { + this.chainId = args.chainId + this.chainName = getChainNameFromChainId(args.chainId) + } + + async isChainIdSupported(chainId?: CosmosChainId): Promise { + const actualChainId = chainId || this.chainId + const provider = await this.getProvider() + + const supportedChainIds = await provider.getSupportedChainIds() + + return !!supportedChainIds.official.find( + (chainId) => chainId === actualChainId, + ) + } + + async getAddresses(): Promise { + const { chainName } = this + const provider = await this.getProvider() + + try { + const accounts = await provider.requestAccount(chainName) + + return [accounts.address] + } catch (e: unknown) { + if ((e as any).code === 4001) { + throw new CosmosWalletException( + new Error('The user rejected the request'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetAccounts, + }, + ) + } + + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetAccounts, + }) + } + } + + async sendTransaction(signResponse: DirectSignResponse): Promise { + const { chainName } = this + const provider = await this.getProvider() + const txRaw = createTxRawFromSigResponse(signResponse) + + try { + const response = await provider.sendTransaction( + chainName, + txRaw.serializeBinary(), + SEND_TRANSACTION_MODE.ASYNC, + ) + + return response.tx_response.txhash + } catch (e: unknown) { + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.ChainError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + async signTransaction(transaction: { + memo: string + address: string + gas: string + message: Msgs | Msgs[] + }) { + const { chainName, chainId } = this + const provider = await this.getProvider() + const signer = await provider.getAccount(INJECTIVE_CHAIN_NAME) + const endpoints = getEndpointsFromChainId(chainId) + + try { + /** Prepare the Transaction * */ + const { bodyBytes, authInfoBytes, accountNumber } = + await createTransactionAndCosmosSignDocForAddressAndMsg({ + chainId, + address: transaction.address, + memo: transaction.memo, + message: transaction.message, + pubKey: Buffer.from(signer.publicKey).toString('base64'), + endpoint: endpoints.rest, + fee: { + ...DEFAULT_STD_FEE, + gas: transaction.gas || DEFAULT_STD_FEE.gas, + }, + }) + + /* Sign the transaction */ + const signDirectResponse = await provider.signDirect( + chainName, + { + chain_id: chainId, + body_bytes: bodyBytes, + auth_info_bytes: authInfoBytes, + account_number: accountNumber.toString(), + }, + { fee: false, memo: true }, + ) + + return { + signed: makeSignDoc( + signDirectResponse.signed_doc.body_bytes, + signDirectResponse.signed_doc.auth_info_bytes, + signDirectResponse.signed_doc.chain_id, + parseInt(signDirectResponse.signed_doc.account_number, 10), + ), + signature: { + signature: signDirectResponse.signature, + }, + } as DirectSignResponse + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + private async getProvider(): Promise { + if (this.provider) { + return this.provider + } + + try { + const provider = await cosmos() + + this.provider = provider + + return provider + } catch (e) { + if (e instanceof InstallError) { + throw new CosmosWalletException( + new Error('Please install the Cosmostation extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + }, + ) + } + + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + }) + } + } +} diff --git a/packages/wallet-ts/src/Cosmos/strategies/Keplr.ts b/packages/wallet-ts/src/Cosmos/strategies/Keplr.ts new file mode 100644 index 000000000..f823bfec7 --- /dev/null +++ b/packages/wallet-ts/src/Cosmos/strategies/Keplr.ts @@ -0,0 +1,129 @@ +/* eslint-disable class-methods-use-this */ +import { CosmosChainId } from '@injectivelabs/ts-types' +import { DEFAULT_STD_FEE } from '@injectivelabs/utils' +import { + createTransactionAndCosmosSignDocForAddressAndMsg, + createTxRawFromSigResponse, +} from '@injectivelabs/sdk-ts/dist/core/transaction' +import type { Msgs } from '@injectivelabs/sdk-ts' +import type { DirectSignResponse } from '@cosmjs/proto-signing' +import { + UnspecifiedErrorCode, + CosmosWalletException, + ErrorType, + TransactionException, +} from '@injectivelabs/exceptions' +import { KeplrWallet } from '../../keplr' +import { ConcreteCosmosWalletStrategy } from '../types/strategy' +import { WalletAction } from '../../wallet-strategy/types/enums' + +export default class Keplr implements ConcreteCosmosWalletStrategy { + public chainId: CosmosChainId + + private keplrWallet: KeplrWallet + + constructor(args: { chainId: CosmosChainId }) { + this.chainId = args.chainId || CosmosChainId.Injective + this.keplrWallet = new KeplrWallet(args.chainId) + } + + async isChainIdSupported(chainId?: CosmosChainId): Promise { + const keplrWallet = chainId + ? new KeplrWallet(chainId) + : this.getKeplrWallet() + + return keplrWallet.checkChainIdSupport() + } + + async getAddresses(): Promise { + const keplrWallet = this.getKeplrWallet() + + try { + if (!(await keplrWallet.checkChainIdSupport())) { + await keplrWallet.experimentalSuggestChain() + } + + const accounts = await keplrWallet.getAccounts() + + return accounts.map((account) => account.address) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetAccounts, + }) + } + } + + async sendTransaction(signResponse: DirectSignResponse): Promise { + const { keplrWallet } = this + const txRaw = createTxRawFromSigResponse(signResponse) + + try { + return await keplrWallet.broadcastTxBlock(txRaw) + } catch (e: unknown) { + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.ChainError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + async signTransaction(transaction: { + address: string + memo: string + gas: string + message: Msgs | Msgs[] + }) { + const { chainId } = this + const keplrWallet = this.getKeplrWallet() + + const endpoints = await keplrWallet.getChainEndpoints() + const key = await keplrWallet.getKey() + const signer = await keplrWallet.getOfflineSigner() + + try { + /** Prepare the Transaction * */ + const { cosmosSignDoc } = + await createTransactionAndCosmosSignDocForAddressAndMsg({ + chainId, + memo: transaction.memo, + address: transaction.address, + message: transaction.message, + pubKey: Buffer.from(key.pubKey).toString('base64'), + endpoint: endpoints.rest, + fee: { + ...DEFAULT_STD_FEE, + gas: transaction.gas || DEFAULT_STD_FEE.gas, + }, + }) + + /* Sign the transaction */ + return signer.signDirect(transaction.address, cosmosSignDoc) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + private getKeplrWallet(): KeplrWallet { + const { keplrWallet } = this + + if (!keplrWallet) { + throw new CosmosWalletException( + new Error('Please install the Keplr wallet extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + contextModule: WalletAction.SignTransaction, + }, + ) + } + + return keplrWallet + } +} diff --git a/packages/wallet-ts/src/Cosmos/strategies/Leap.ts b/packages/wallet-ts/src/Cosmos/strategies/Leap.ts new file mode 100644 index 000000000..859b4ae4c --- /dev/null +++ b/packages/wallet-ts/src/Cosmos/strategies/Leap.ts @@ -0,0 +1,131 @@ +/* eslint-disable class-methods-use-this */ +import { CosmosChainId } from '@injectivelabs/ts-types' +import { + UnspecifiedErrorCode, + CosmosWalletException, + TransactionException, + ErrorType, +} from '@injectivelabs/exceptions' +import { DEFAULT_STD_FEE } from '@injectivelabs/utils' +import { + createTxRawFromSigResponse, + createTransactionAndCosmosSignDocForAddressAndMsg, +} from '@injectivelabs/sdk-ts' +import type { Msgs } from '@injectivelabs/sdk-ts' +import type { DirectSignResponse } from '@cosmjs/proto-signing' +import { LeapWallet } from '../../leap' +import { WalletAction } from '../../wallet-strategy/types/enums' +import { ConcreteCosmosWalletStrategy } from '../types/strategy' + +export default class Leap implements ConcreteCosmosWalletStrategy { + public chainId: CosmosChainId + + private leapWallet: LeapWallet + + constructor(args: { chainId: CosmosChainId }) { + this.chainId = args.chainId || CosmosChainId.Injective + this.leapWallet = new LeapWallet(args.chainId) + } + + async isChainIdSupported(chainId?: CosmosChainId): Promise { + const leapWallet = chainId ? new LeapWallet(chainId) : this.getLeapWallet() + + return leapWallet.checkChainIdSupport() + } + + async getAddresses(): Promise { + const { chainId } = this + const leapWallet = this.getLeapWallet() + + try { + if (!(await leapWallet.checkChainIdSupport())) { + throw new CosmosWalletException( + new Error(`The ${chainId} is not supported on Leap.`), + { type: ErrorType.WalletError }, + ) + } + + const accounts = await leapWallet.getAccounts() + + return accounts.map((account) => account.address) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetAccounts, + }) + } + } + + async sendTransaction(signResponse: DirectSignResponse): Promise { + const { leapWallet } = this + const txRaw = createTxRawFromSigResponse(signResponse) + + try { + return await leapWallet.broadcastTxBlock(txRaw) + } catch (e: unknown) { + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.ChainError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + async signTransaction(transaction: { + address: string + memo: string + gas: string + message: Msgs | Msgs[] + }) { + const { chainId } = this + const leapWallet = this.getLeapWallet() + + const endpoints = await leapWallet.getChainEndpoints() + const key = await leapWallet.getKey() + const signer = await leapWallet.getOfflineSigner() + + try { + /** Prepare the Transaction * */ + const { cosmosSignDoc } = + await createTransactionAndCosmosSignDocForAddressAndMsg({ + chainId, + address: transaction.address, + memo: transaction.memo, + message: transaction.message, + pubKey: Buffer.from(key.pubKey).toString('base64'), + endpoint: endpoints.rest, + fee: { + ...DEFAULT_STD_FEE, + gas: transaction.gas || DEFAULT_STD_FEE.gas, + }, + }) + + /* Sign the transaction */ + return signer.signDirect(transaction.address, cosmosSignDoc) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendTransaction, + }) + } + } + + private getLeapWallet(): LeapWallet { + const { leapWallet } = this + + if (!leapWallet) { + throw new CosmosWalletException( + new Error('Please install the Leap wallet extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + contextModule: WalletAction.SignTransaction, + }, + ) + } + + return leapWallet + } +} diff --git a/packages/wallet-ts/src/Cosmos/types/strategy.ts b/packages/wallet-ts/src/Cosmos/types/strategy.ts new file mode 100644 index 000000000..14b03dd52 --- /dev/null +++ b/packages/wallet-ts/src/Cosmos/types/strategy.ts @@ -0,0 +1,31 @@ +import { DirectSignResponse } from '@cosmjs/proto-signing' +import { TxRaw } from '@injectivelabs/chain-api/cosmos/tx/v1beta1/tx_pb' +import { CosmosChainId } from '@injectivelabs/ts-types' +import { Msgs } from '@injectivelabs/sdk-ts' +import { Wallet } from '../../wallet-strategy/types/enums' + +export interface ConcreteCosmosWalletStrategy { + getAddresses(): Promise + + /** + * Sends Cosmos transaction. Returns a transaction hash + * @param transaction should implement TransactionConfig + * @param options + */ + sendTransaction(transaction: DirectSignResponse | TxRaw): Promise + + isChainIdSupported(chainId?: CosmosChainId): Promise + + signTransaction(data: { + address: string + memo: string + gas: string + message: Msgs | Msgs[] + }): Promise +} + +export interface CosmosWalletStrategyArguments { + chainId: CosmosChainId + endpoints?: { rpc: string; rest: string } + wallet?: Wallet +} diff --git a/packages/wallet-ts/src/index.ts b/packages/wallet-ts/src/index.ts index 885ab8130..c0f7590d5 100644 --- a/packages/wallet-ts/src/index.ts +++ b/packages/wallet-ts/src/index.ts @@ -1 +1,2 @@ export * from './wallet-strategy' +export * from './cosmos'