Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bitget wallet #44

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Expand Down
3 changes: 3 additions & 0 deletions examples/solid-vite/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createStore } from "solid-js/store";

import { MsgSend } from "cosmes/client";
import {
BitgetController,
CompassController,
ConnectedWallet,
CosmostationController,
Expand Down Expand Up @@ -34,6 +35,7 @@ const CHAINS: Record<string, string> = {
"pacific-1": "Sei",
};
const WALLETS: Record<WalletName, string> = {
[WalletName.BITGET]: "Bitget",
[WalletName.KEPLR]: "Keplr",
[WalletName.COSMOSTATION]: "Cosmostation",
[WalletName.STATION]: "Terra Station",
Expand All @@ -47,6 +49,7 @@ const TYPES: Record<WalletType, string> = {
[WalletType.WALLETCONNECT]: "Wallet Connect",
};
const CONTROLLERS: Record<string, WalletController> = {
[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),
Expand Down
1 change: 1 addition & 0 deletions src/wallet/constants/WalletName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* The unique identifier of the wallet.
*/
export const WalletName = {
BITGET: "bitget",
STATION: "station",
KEPLR: "keplr",
LEAP: "leap",
Expand Down
1 change: 1 addition & 0 deletions src/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
93 changes: 93 additions & 0 deletions src/wallet/wallets/bitget/BitgetController.ts
Original file line number Diff line number Diff line change
@@ -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<T extends string>(
chains: ChainInfo<T>[]
) {
const wallets = new Map<T, ConnectedWallet>();
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<T extends string>(chains: ChainInfo<T>[]) {
const wallets = new Map<T, ConnectedWallet>();
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));
}
}
109 changes: 109 additions & 0 deletions src/wallet/wallets/bitget/BitgetExtension.ts
Original file line number Diff line number Diff line change
@@ -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<Coin>,
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<SignArbitraryResponse> {
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<string> {
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);
}
}
94 changes: 94 additions & 0 deletions src/wallet/wallets/bitget/BitgetWalletConnectV2.ts
Original file line number Diff line number Diff line change
@@ -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<Coin>,
useAmino: boolean
) {
super(
walletName,
WalletType.WALLETCONNECT,
chainId,
pubKey,
address,
rpc,
gasPrice
);
this.wc = wc;
this.useAmino = useAmino;
}

public async signArbitrary(_data: string): Promise<SignArbitraryResponse> {
// ! Not implemented by Bitget
throw new Error("Method not implemented.");
}

public async signAndBroadcastTx(
{ msgs, memo, timeoutHeight }: UnsignedTx,
fee: Fee,
accountNumber: bigint,
sequence: bigint
): Promise<string> {
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);
}
}
12 changes: 12 additions & 0 deletions src/wallet/wallets/bitget/types.ts
Original file line number Diff line number Diff line change
@@ -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;
};
4 changes: 3 additions & 1 deletion src/wallet/wallets/window.d.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,7 +10,8 @@ import { Window as StationWindow } from "./station/types";

declare global {
interface Window
extends KeplrWindow,
extends BitgetWindow,
KeplrWindow,
CosmostationWindow,
StationWindow,
LeapWindow,
Expand Down