diff --git a/README.md b/README.md index 451df2ea..031c0ec9 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ This directory is a [Cosmos Kit](https://cosmoskit.com) alternative to interact **Wallets supported**: +- [Bitget](https://web3.bitget.com/) (for Sei only) - [Station](https://docs.terra.money/learn/station/) - [Keplr](https://www.keplr.app/) - [Leap](https://www.leapwallet.io/) diff --git a/examples/solid-vite/src/App.tsx b/examples/solid-vite/src/App.tsx index 854ec9dc..df5687c2 100644 --- a/examples/solid-vite/src/App.tsx +++ b/examples/solid-vite/src/App.tsx @@ -3,6 +3,7 @@ import { createStore } from "solid-js/store"; import { MsgSend } from "cosmes/client"; import { + BitgetController, CompassController, ConnectedWallet, CosmostationController, @@ -34,6 +35,7 @@ const CHAINS: Record = { "pacific-1": "Sei", }; const WALLETS: Record = { + [WalletName.BITGET]: "Bitget", [WalletName.KEPLR]: "Keplr", [WalletName.COSMOSTATION]: "Cosmostation", [WalletName.STATION]: "Terra Station", @@ -47,6 +49,7 @@ const TYPES: Record = { [WalletType.WALLETCONNECT]: "Wallet Connect", }; const CONTROLLERS: Record = { + [WalletName.BITGET]: new BitgetController(WC_PROJECT_ID), [WalletName.STATION]: new StationController(), [WalletName.KEPLR]: new KeplrController(WC_PROJECT_ID), [WalletName.LEAP]: new LeapController(WC_PROJECT_ID), diff --git a/src/wallet/constants/WalletName.ts b/src/wallet/constants/WalletName.ts index 32079185..c1801d00 100644 --- a/src/wallet/constants/WalletName.ts +++ b/src/wallet/constants/WalletName.ts @@ -2,6 +2,7 @@ * The unique identifier of the wallet. */ export const WalletName = { + BITGET: "bitget", STATION: "station", KEPLR: "keplr", LEAP: "leap", diff --git a/src/wallet/index.ts b/src/wallet/index.ts index f891a640..c29e750e 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -13,6 +13,7 @@ export { type ChainInfo, type EventCallback, } from "./wallets/WalletController"; +export { BitgetController } from "./wallets/bitget/BitgetController"; export { CompassController } from "./wallets/compass/CompassController"; export { CosmostationController } from "./wallets/cosmostation/CosmostationController"; export { KeplrController } from "./wallets/keplr/KeplrController"; diff --git a/src/wallet/wallets/bitget/BitgetController.ts b/src/wallet/wallets/bitget/BitgetController.ts new file mode 100644 index 00000000..07432e1b --- /dev/null +++ b/src/wallet/wallets/bitget/BitgetController.ts @@ -0,0 +1,93 @@ +import { Secp256k1PubKey } from "cosmes/client"; +import { base64 } from "cosmes/codec"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { onWindowEvent } from "../../utils/window"; +import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2"; +import { ConnectedWallet } from "../ConnectedWallet"; +import { ChainInfo, WalletController } from "../WalletController"; +import { BitgetExtension } from "./BitgetExtension"; +import { BitgetWalletConnectV2 } from "./BitgetWalletConnectV2"; + +export class BitgetController extends WalletController { + private readonly wc: WalletConnectV2; + + constructor(wcProjectId: string) { + super(WalletName.BITGET); + this.wc = new WalletConnectV2(wcProjectId, { + name: "Bitget", + android: "https://bkcode.vip", + ios: "bitkeep://bkconnect", + }); + this.registerAccountChangeHandlers(); + } + + public async isInstalled(type: WalletType) { + return type === WalletType.EXTENSION ? "bitkeep" in window : true; + } + + protected async connectWalletConnect( + chains: ChainInfo[] + ) { + const wallets = new Map(); + await this.wc.connect(chains.map(({ chainId }) => chainId)); + for (let i = 0; i < chains.length; i++) { + const { chainId, rpc, gasPrice } = chains[i]; + const { pubkey, address } = await this.wc.getAccount(chainId); + const key = new Secp256k1PubKey({ + key: base64.decode(pubkey), + }); + wallets.set( + chainId, + new BitgetWalletConnectV2( + this.id, + this.wc, + chainId, + key, + address, + rpc, + gasPrice, + true // TODO: use sign mode direct when supported + ) + ); + } + return { wallets, wc: this.wc }; + } + + protected async connectExtension(chains: ChainInfo[]) { + const wallets = new Map(); + const ext = window.bitkeep && window.bitkeep.keplr; + if (!ext) { + throw new Error("Bitget extension is not installed"); + } + await ext.enable(chains.map(({ chainId }) => chainId)); + for (const { chainId, rpc, gasPrice } of Object.values(chains)) { + const { bech32Address, pubKey, isNanoLedger } = await ext.getKey(chainId); + const key = new Secp256k1PubKey({ + key: pubKey, + }); + wallets.set( + chainId, + new BitgetExtension( + this.id, + ext, + chainId, + key, + bech32Address, + rpc, + gasPrice, + isNanoLedger + ) + ); + } + return wallets; + } + + protected registerAccountChangeHandlers() { + onWindowEvent("keplr_keystorechange", () => + this.changeAccount(WalletType.EXTENSION) + ); + this.wc.onAccountChange(() => this.changeAccount(WalletType.WALLETCONNECT)); + } +} diff --git a/src/wallet/wallets/bitget/BitgetExtension.ts b/src/wallet/wallets/bitget/BitgetExtension.ts new file mode 100644 index 00000000..8b0f4bb1 --- /dev/null +++ b/src/wallet/wallets/bitget/BitgetExtension.ts @@ -0,0 +1,109 @@ +import { PlainMessage } from "@bufbuild/protobuf"; +import { + Secp256k1PubKey, + ToSignDocParams, + ToStdSignDocParams, + Tx, +} from "cosmes/client"; +import { base16 } from "cosmes/codec"; +import { + CosmosBaseV1beta1Coin as Coin, + CosmosTxV1beta1Fee as Fee, + CosmosTxV1beta1TxRaw as TxRaw, +} from "cosmes/protobufs"; +import type { BroadcastMode, Keplr } from "cosmes/registry"; + +import { WalletName } from "../../constants/WalletName"; +import { WalletType } from "../../constants/WalletType"; +import { + ConnectedWallet, + SignArbitraryResponse, + UnsignedTx, +} from "../ConnectedWallet"; + +export class BitgetExtension extends ConnectedWallet { + private readonly ext: Keplr; + private readonly useAmino: boolean; + + constructor( + walletName: WalletName, + ext: Keplr, + chainId: string, + pubKey: Secp256k1PubKey, + address: string, + rpc: string, + gasPrice: PlainMessage, + useAmino: boolean + ) { + super( + walletName, + WalletType.EXTENSION, + chainId, + pubKey, + address, + rpc, + gasPrice + ); + this.ext = ext; + this.ext.defaultOptions = { + sign: { + preferNoSetFee: true, + preferNoSetMemo: true, + }, + }; + this.useAmino = useAmino; + } + + public async signArbitrary(data: string): Promise { + const res = await this.ext.signArbitrary(this.chainId, this.address, data); + return { + data, + pubKey: res.pub_key.value, + signature: res.signature, + }; + } + + protected async signAndBroadcastTx( + { msgs, memo, timeoutHeight }: UnsignedTx, + fee: Fee, + accountNumber: bigint, + sequence: bigint + ): Promise { + const tx = new Tx({ + chainId: this.chainId, + pubKey: this.pubKey, + msgs: msgs, + }); + + const params: ToStdSignDocParams | ToSignDocParams = { + accountNumber, + sequence, + fee, + memo, + timeoutHeight, + }; + let txRaw: TxRaw; + if (this.useAmino) { + const { signed, signature } = await this.ext.signAmino( + this.chainId, + this.address, + tx.toStdSignDoc(params) + ); + txRaw = tx.toSignedAmino(signed, signature.signature); + } else { + const { signed, signature } = await this.ext.signDirect( + this.chainId, + this.address, + tx.toSignDoc(params) + ); + txRaw = tx.toSignedDirect(signed, signature.signature); + } + + const txHash = await this.ext.sendTx( + this.chainId, + txRaw.toBinary(), + "sync" as BroadcastMode + ); + return base16.encode(txHash); + } +} diff --git a/src/wallet/wallets/bitget/BitgetWalletConnectV2.ts b/src/wallet/wallets/bitget/BitgetWalletConnectV2.ts new file mode 100644 index 00000000..c3991fd1 --- /dev/null +++ b/src/wallet/wallets/bitget/BitgetWalletConnectV2.ts @@ -0,0 +1,94 @@ +import { PlainMessage } from "@bufbuild/protobuf"; +import { + RpcClient, + Secp256k1PubKey, + ToSignDocParams, + ToStdSignDocParams, + Tx, +} from "cosmes/client"; +import { + CosmosBaseV1beta1Coin as Coin, + CosmosTxV1beta1Fee as Fee, + CosmosTxV1beta1TxRaw as TxRaw, +} from "cosmes/protobufs"; +import { WalletName, WalletType } from "cosmes/wallet"; + +import { WalletConnectV2 } from "../../walletconnect/WalletConnectV2"; +import { + ConnectedWallet, + SignArbitraryResponse, + UnsignedTx, +} from "../ConnectedWallet"; + +export class BitgetWalletConnectV2 extends ConnectedWallet { + private readonly wc: WalletConnectV2; + private readonly useAmino: boolean; + + constructor( + walletName: WalletName, + wc: WalletConnectV2, + chainId: string, + pubKey: Secp256k1PubKey, + address: string, + rpc: string, + gasPrice: PlainMessage, + useAmino: boolean + ) { + super( + walletName, + WalletType.WALLETCONNECT, + chainId, + pubKey, + address, + rpc, + gasPrice + ); + this.wc = wc; + this.useAmino = useAmino; + } + + public async signArbitrary(_data: string): Promise { + // ! Not implemented by Bitget + throw new Error("Method not implemented."); + } + + public async signAndBroadcastTx( + { msgs, memo, timeoutHeight }: UnsignedTx, + fee: Fee, + accountNumber: bigint, + sequence: bigint + ): Promise { + const tx = new Tx({ + chainId: this.chainId, + pubKey: this.pubKey, + msgs: msgs, + }); + + const params: ToStdSignDocParams | ToSignDocParams = { + accountNumber, + sequence, + fee, + memo, + timeoutHeight, + }; + let txRaw: TxRaw; + if (this.useAmino) { + const { signed, signature } = await this.wc.signAmino( + this.chainId, + this.address, + tx.toStdSignDoc(params) + ); + txRaw = tx.toSignedAmino(signed, signature.signature); + } else { + const { signed, signature } = await this.wc.signDirect( + this.chainId, + this.address, + tx.toSignDoc(params) + ); + txRaw = tx.toSignedDirect(signed, signature.signature); + } + + // Since `sendTx` on WC isn't implemented yet, we have to broadcast manually + return RpcClient.broadcastTx(this.rpc, txRaw); + } +} diff --git a/src/wallet/wallets/bitget/types.ts b/src/wallet/wallets/bitget/types.ts new file mode 100644 index 00000000..815ebc2b --- /dev/null +++ b/src/wallet/wallets/bitget/types.ts @@ -0,0 +1,12 @@ +import { Keplr } from "cosmes/registry"; + +// Type is similar to Keplr +export type Bitget = Keplr; + +export type Window = { + bitkeep?: + | { + keplr: Bitget + } + | undefined; +}; diff --git a/src/wallet/wallets/window.d.ts b/src/wallet/wallets/window.d.ts index 90e6d54f..f6eaf622 100644 --- a/src/wallet/wallets/window.d.ts +++ b/src/wallet/wallets/window.d.ts @@ -1,5 +1,6 @@ import { Window as KeplrWindow } from "cosmes/registry"; +import { Window as BitgetWindow } from "./bitget/types"; import { Window as CompassWindow } from "./compass/types"; import { Window as CosmostationWindow } from "./cosmostation/types"; import { Window as LeapWindow } from "./leap/types"; @@ -9,7 +10,8 @@ import { Window as StationWindow } from "./station/types"; declare global { interface Window - extends KeplrWindow, + extends BitgetWindow, + KeplrWindow, CosmostationWindow, StationWindow, LeapWindow,