diff --git a/packages/wallet-ts/src/strategies/cosmos-wallet-strategy/strategies/Welldone.ts b/packages/wallet-ts/src/strategies/cosmos-wallet-strategy/strategies/Welldone.ts new file mode 100644 index 000000000..508d15e6a --- /dev/null +++ b/packages/wallet-ts/src/strategies/cosmos-wallet-strategy/strategies/Welldone.ts @@ -0,0 +1,133 @@ +/* eslint-disable class-methods-use-this */ +import { CosmosChainId } from '@injectivelabs/ts-types' +import { + TxRaw, + TxResponse, + createTxRawFromSigResponse, + createCosmosSignDocFromSignDoc, + createSignDocFromTransaction, +} from '@injectivelabs/sdk-ts' +import type { DirectSignResponse } from '@cosmjs/proto-signing' +import { + ErrorType, + TransactionException, + UnspecifiedErrorCode, + CosmosWalletException, +} from '@injectivelabs/exceptions' +import { AminoSignResponse, StdSignDoc } from '@cosmjs/launchpad' +import { ConcreteCosmosWalletStrategy } from '../../types/strategy' +import { WalletAction, WalletDeviceType } from '../../../types/enums' +import { WelldoneWallet } from '../../../../src/utils/wallets' + +export default class Welldone implements ConcreteCosmosWalletStrategy { + public chainId: CosmosChainId + + private welldoneWallet: WelldoneWallet + + constructor(args: { chainId: CosmosChainId }) { + this.chainId = args.chainId || CosmosChainId.Injective + this.welldoneWallet = new WelldoneWallet(args.chainId) + } + + enable(): Promise { + const welldoneWallet = this.getWelldoneWallet(); + return welldoneWallet.enable(); + } + + async getWalletDeviceType(): Promise { + return Promise.resolve(WalletDeviceType.Browser) + } + + async isChainIdSupported(chainId?: CosmosChainId): Promise { + const welldoneWallet = chainId + ? new WelldoneWallet(chainId) + : this.getWelldoneWallet() + + return welldoneWallet.checkChainIdSupport() + } + + async getAddresses(): Promise { + const welldoneWallet = this.getWelldoneWallet() + + try { + const accounts = await welldoneWallet.getAccounts() + + return [accounts.address] + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + context: WalletAction.GetAccounts, + }) + } + } + + async sendTransaction( + transaction: DirectSignResponse | TxRaw, + ): Promise { + const { welldoneWallet } = this + const txRaw = createTxRawFromSigResponse(transaction) + + try { + return await welldoneWallet.waitTxBroadcasted( + await welldoneWallet.broadcastTx(txRaw), + ) + } catch (e: unknown) { + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + async signTransaction(transaction: { + txRaw: TxRaw + chainId: string + accountNumber: number + address: string + }): Promise { + const welldoneWallet = this.getWelldoneWallet() + const signDoc = createSignDocFromTransaction(transaction) + + try { + const result = await welldoneWallet.signTransaction( + createCosmosSignDocFromSignDoc(signDoc), + ) + + return result + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + async signAminoTransaction(_transaction: { + address: string + stdSignDoc: StdSignDoc + }): Promise { + throw new CosmosWalletException( + new Error('signAminoTransaction not supported on WELLDONE wallet'), + ) + } + + async getPubKey(): Promise { + const welldoneWallet = this.getWelldoneWallet() + const key = await welldoneWallet.getKey() + + return Buffer.from(key.replace('0x', ''), 'hex').toString('base64') + } + + private getWelldoneWallet(): WelldoneWallet { + const { welldoneWallet } = this + + if (!welldoneWallet) { + throw new CosmosWalletException( + new Error('Please install the WELLDONE wallet extension'), + ) + } + + return welldoneWallet + } +} diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts b/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts index 0143b6641..60583e303 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts @@ -22,6 +22,7 @@ import Torus from './strategies/Torus' import Phantom from './strategies/Phantom' import Okx from './strategies/Okx' import Cosmostation from './strategies/Cosmostation' +import Welldone from './strategies/Welldone' import LedgerCosmos from './strategies/LedgerCosmos' import { Wallet, WalletDeviceType } from '../../types/enums' import { isEthWallet, isCosmosWallet } from './utils' @@ -102,6 +103,8 @@ const createStrategy = ({ return new LedgerCosmos({ ...args }) case Wallet.Leap: return new Leap({ ...args }) + case Wallet.Welldone: + return new Welldone({ ...args }) case Wallet.Ninji: return new Ninji({ ...args }) default: diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/Welldone.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/Welldone.ts new file mode 100644 index 000000000..4bc3bb83d --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/Welldone.ts @@ -0,0 +1,209 @@ +/* eslint-disable class-methods-use-this */ +import { + ChainId, + CosmosChainId, + AccountAddress, + EthereumChainId, +} from '@injectivelabs/ts-types' +import { + TxRaw, + TxResponse, + createTxRawFromSigResponse, + createCosmosSignDocFromSignDoc, + createSignDocFromTransaction, +} from '@injectivelabs/sdk-ts' +import type { DirectSignResponse } from '@cosmjs/proto-signing' +import { + ErrorType, + TransactionException, + UnspecifiedErrorCode, + CosmosWalletException, +} from '@injectivelabs/exceptions' +import { ConcreteWalletStrategy } from '../../types' +import BaseConcreteStrategy from './Base' +import { WalletAction, WalletDeviceType } from '../../../types/enums' +import { WelldoneWallet } from '../../../utils/wallets/welldone' + +export default class Welldone + extends BaseConcreteStrategy + implements ConcreteWalletStrategy +{ + private welldoneWallet: WelldoneWallet + + constructor(args: { chainId: ChainId }) { + super(args) + this.chainId = args.chainId || CosmosChainId.Injective + this.welldoneWallet = new WelldoneWallet(args.chainId) + } + signAminoCosmosTransaction(_transaction: { signDoc: any; accountNumber: number; chainId: string; address: string }): Promise { + throw new CosmosWalletException( + new Error('This wallet does not support signing using amino'), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }, + ) + } + + async signArbitrary(_signer: string, _data: string | Uint8Array): Promise { + throw new Error('Method not implemented.') + } + + async enable(): Promise { + const welldoneWallet = this.getWelldoneWallet(); + return welldoneWallet.enable(); + } + + async getWalletDeviceType(): Promise { + return Promise.resolve(WalletDeviceType.Browser) + } + + async getAddresses(): Promise { + const welldoneWallet = this.getWelldoneWallet() + + try { + const accounts = await welldoneWallet.getAccounts() + + return [accounts.address] + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + context: WalletAction.GetAccounts, + }) + } + } + + async confirm(address: AccountAddress): Promise { + return Promise.resolve( + `0x${Buffer.from( + `Confirmation for ${address} at time: ${Date.now()}`, + ).toString('hex')}`, + ) + } + + // eslint-disable-next-line class-methods-use-this + async sendEthereumTransaction( + _transaction: unknown, + _options: { address: AccountAddress; ethereumChainId: EthereumChainId }, + ): Promise { + throw new CosmosWalletException( + new Error( + 'sendEthereumTransaction is not supported. WELLDONE only supports sending cosmos transactions', + ), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendEthereumTransaction, + }, + ) + } + + async sendTransaction( + transaction: DirectSignResponse | TxRaw, + _options: { address: AccountAddress; chainId: ChainId }, + ): Promise { + const { welldoneWallet } = this + const txRaw = createTxRawFromSigResponse(transaction) + + try { + return await welldoneWallet.waitTxBroadcasted( + await welldoneWallet.broadcastTx(txRaw), + ) + } catch (e: unknown) { + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + /** @deprecated */ + async signTransaction( + transaction: { txRaw: TxRaw; accountNumber: number; chainId: string }, + injectiveAddress: AccountAddress, + ) { + return this.signCosmosTransaction({ + ...transaction, + address: injectiveAddress, + }) + } + + async signCosmosTransaction(transaction: { + txRaw: TxRaw + accountNumber: number + chainId: string + address: string + }): Promise { + const welldoneWallet = this.getWelldoneWallet() + const signDoc = createSignDocFromTransaction(transaction) + + try { + const result = await welldoneWallet.signTransaction( + createCosmosSignDocFromSignDoc(signDoc), + ) + + return result + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + async signEip712TypedData( + _transaction: string, + _address: AccountAddress, + ): Promise { + throw new CosmosWalletException( + new Error( + 'WELLDONE wallet does not support signing Ethereum transactions', + ), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }, + ) + } + + async getEthereumChainId(): Promise { + throw new CosmosWalletException( + new Error('getEthereumChainId is not supported on WELLDONE Wallet'), + { + code: UnspecifiedErrorCode, + context: WalletAction.GetChainId, + }, + ) + } + + async getEthereumTransactionReceipt(_txHash: string): Promise { + throw new CosmosWalletException( + new Error( + 'getEthereumTransactionReceipt is not supported on WELLDONE Wallet', + ), + { + code: UnspecifiedErrorCode, + context: WalletAction.GetEthereumTransactionReceipt, + }, + ) + } + + async getPubKey(): Promise { + const welldoneWallet = this.getWelldoneWallet() + const key = await welldoneWallet.getKey() + + return Buffer.from(key.replace('0x', ''), 'hex').toString('base64') + } + + private getWelldoneWallet(): WelldoneWallet { + const { welldoneWallet } = this + + if (!welldoneWallet) { + throw new CosmosWalletException( + new Error('Please install the WELLDONE wallet extension'), + ) + } + + return welldoneWallet + } +} diff --git a/packages/wallet-ts/src/types/enums.ts b/packages/wallet-ts/src/types/enums.ts index fcde1ca75..963e48fcc 100644 --- a/packages/wallet-ts/src/types/enums.ts +++ b/packages/wallet-ts/src/types/enums.ts @@ -16,6 +16,7 @@ export enum Wallet { LedgerLegacy = 'ledger-legacy', WalletConnect = 'wallet-connect', CosmostationEth = 'cosmostation-eth', + Welldone = 'welldone', } export enum WalletDeviceType { diff --git a/packages/wallet-ts/src/utils/wallets/cosmos/utils.ts b/packages/wallet-ts/src/utils/wallets/cosmos/utils.ts index 3bbd45e87..732ed1178 100644 --- a/packages/wallet-ts/src/utils/wallets/cosmos/utils.ts +++ b/packages/wallet-ts/src/utils/wallets/cosmos/utils.ts @@ -2,6 +2,7 @@ import { DEFAULT_TIMESTAMP_TIMEOUT_MS } from '@injectivelabs/utils' import { Cosmos } from '@cosmostation/extension-client' import type { Keplr } from '@keplr-wallet/types' import { Wallet } from '../../../types/enums' +import { WalletProvider } from '../welldone/welldone' /** * Returns a timeout timestamp in milliseconds so its compatible @@ -26,7 +27,7 @@ export const makeTimeoutTimestampInNs = ( ) => makeTimeoutTimestamp(timeoutInMs) * 1e6 export const isCosmosBrowserWallet = (wallet: Wallet): boolean => - [Wallet.Leap, Wallet.Ninji, Wallet.Keplr, Wallet.Cosmostation].includes( + [Wallet.Leap, Wallet.Ninji, Wallet.Keplr, Wallet.Cosmostation, Wallet.Welldone].includes( wallet, ) @@ -39,6 +40,7 @@ export const isCosmosWalletInstalled = (wallet: Wallet) => { keplr?: Keplr ninji?: Keplr cosmostation?: Cosmos + dapp?: WalletProvider } switch (wallet) { @@ -50,6 +52,8 @@ export const isCosmosWalletInstalled = (wallet: Wallet) => { return $window.cosmostation !== undefined case Wallet.Leap: return $window.leap !== undefined + case Wallet.Welldone: + return $window.dapp !== undefined default: return false } diff --git a/packages/wallet-ts/src/utils/wallets/index.ts b/packages/wallet-ts/src/utils/wallets/index.ts index 7838f9d7d..a7b9b208d 100644 --- a/packages/wallet-ts/src/utils/wallets/index.ts +++ b/packages/wallet-ts/src/utils/wallets/index.ts @@ -3,6 +3,7 @@ export * from './cosmostation' export * from './leap' export * from './phantom' export * from './metamask' +export * from './welldone' export * from './okx' export * from './ninji' export * from './trust-wallet' diff --git a/packages/wallet-ts/src/utils/wallets/welldone/WelldoneWallet.ts b/packages/wallet-ts/src/utils/wallets/welldone/WelldoneWallet.ts new file mode 100644 index 000000000..aa5e24403 --- /dev/null +++ b/packages/wallet-ts/src/utils/wallets/welldone/WelldoneWallet.ts @@ -0,0 +1,209 @@ +/* eslint-disable class-methods-use-this */ +import { + ChainId, + CosmosChainId, + TestnetCosmosChainId, +} from '@injectivelabs/ts-types' +import { + TxResponse, + TxRestApi, + CosmosTxV1Beta1Tx, + getPublicKey, +} from '@injectivelabs/sdk-ts' +import { DirectSignResponse, makeSignBytes } from '@cosmjs/proto-signing' +import { + ErrorType, + UnspecifiedErrorCode, + CosmosWalletException, + WalletErrorActionModule, +} from '@injectivelabs/exceptions' +import { getEndpointsFromChainId } from '../cosmos/endpoints' +import { WalletProvider } from './welldone' + +const $window = (typeof window !== 'undefined' ? window : {}) as Window & { + dapp?: WalletProvider +} + +export class WelldoneWallet { + private chainId: CosmosChainId | TestnetCosmosChainId | ChainId + + private chainName: string + + constructor(chainId: CosmosChainId | TestnetCosmosChainId | ChainId) { + this.chainId = chainId + this.chainName = chainId.split('-')[0] + } + + async enable(): Promise { + const accounts = await this.getAccounts(); + return accounts && !!accounts.address; + } + + async getAccounts(): Promise<{ address: string; pubKey: string }> { + const welldone = await this.getWelldoneWallet() + + try { + const accounts = await welldone.request(this.chainName, { + method: 'dapp:accounts', + }) + + if (Object.keys(accounts).length === 0) { + throw new Error( + `Please make the ${this.chainName} account in the WELLDONE wallet`, + ) + } + + return accounts[this.chainName] + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + contextModule: WalletErrorActionModule.GetAccounts, + }) + } + } + + public async broadcastTx(txRaw: CosmosTxV1Beta1Tx.TxRaw): Promise { + const welldone = await this.getWelldoneWallet() + + try { + const { hash } = await welldone.request(this.chainName, { + method: 'dapp:sendSignedTransaction', + params: [ + `0x${Buffer.from( + CosmosTxV1Beta1Tx.TxRaw.encode(txRaw).finish(), + ).toString('hex')}`, + ], + }) + + return hash + } catch (e) { + throw new CosmosWalletException(new Error((e as any).message), { + context: 'WELLDONE', + contextModule: 'bradcast-tx', + }) + } + } + + public async waitTxBroadcasted(txHash: string): Promise { + const endpoints = await this.getChainEndpoints() + + return new TxRestApi(endpoints.rest).fetchTxPoll(txHash) + } + + public async signTransaction( + signDoc: Omit & { + accountNumber: Long + }, + ): Promise { + const welldone = await this.getWelldoneWallet() + + try { + const signBytes = makeSignBytes(signDoc) + const response = await welldone.request(this.chainName, { + method: 'dapp:signTransaction', + params: [`0x${Buffer.from(signBytes).toString('hex')}`], + }) + const pubKey = getPublicKey({ + chainId: this.chainId, + key: Buffer.from(response[0].publicKey.replace('0x', ''), 'hex').toString('base64'), + }) + + return { + signed: signDoc, + signature: { + pub_key: pubKey as any, + signature: Buffer.from( + response[0].signature.replace('0x', ''), + 'hex', + ).toString('base64'), + }, + } + } catch (e) { + throw new CosmosWalletException(new Error((e as any).message), { + context: 'WELLDONE', + contextModule: 'sign-tx', + }) + } + } + + public async getKey() { + const account = await this.getAccounts() + + return account.pubKey + } + + public async getChainEndpoints(): Promise<{ rpc: string; rest: string }> { + const { chainId } = this + + try { + return getEndpointsFromChainId(chainId) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + context: 'WELLDONE', + contextModule: 'get-chain-endpoints', + }) + } + } + + // WELLDONE Wallet only supports injective, cosmos, juno netoworks. + public async checkChainIdSupport() { + const { chainId } = this + + return ['injective', 'cosmoshub', 'juno'].some((cosmosChainId) => + chainId.includes(cosmosChainId), + ) + } + + private async checkNetwork() { + const welldone = this.getWelldone() + + try { + const { node_info } = await welldone.request(this.chainName, { + method: 'status', + }) + + return node_info.network === this.chainId + } catch (e) { + throw new CosmosWalletException(new Error((e as any).message), { + context: 'WELLDONE', + contextModule: 'check-network', + }) + } + } + + public async getWelldoneWallet() { + const { chainId } = this + const welldone = this.getWelldone() + + if (!(await this.checkNetwork())) { + throw new Error(`Change the WELLDONE Wallet Network to ${chainId}`) + } + + return welldone + } + + private getWelldone() { + if (!$window) { + throw new CosmosWalletException( + new Error('Please install WELLDONE extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + context: 'WELLDONE', + }, + ) + } + + if (!$window.dapp) { + throw new CosmosWalletException( + new Error('Please install WELLDONE extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + context: 'WELLDONE', + }, + ) + } + + return $window.dapp! + } +} diff --git a/packages/wallet-ts/src/utils/wallets/welldone/index.ts b/packages/wallet-ts/src/utils/wallets/welldone/index.ts new file mode 100644 index 000000000..00d928573 --- /dev/null +++ b/packages/wallet-ts/src/utils/wallets/welldone/index.ts @@ -0,0 +1 @@ +export * from './WelldoneWallet' diff --git a/packages/wallet-ts/src/utils/wallets/welldone/welldone.d.ts b/packages/wallet-ts/src/utils/wallets/welldone/welldone.d.ts new file mode 100644 index 000000000..4de733f97 --- /dev/null +++ b/packages/wallet-ts/src/utils/wallets/welldone/welldone.d.ts @@ -0,0 +1,17 @@ +interface RequestParams { + jsonrpc?: '2.0' + id?: number + method: string + params?: object +} + +export interface WalletProvider { + request: (chainId: string, args: RequestParams) => Promise + networks: any +} + +declare global { + interface Window { + dapp: WalletProvider + } +}