Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/bitget-cosmos-near-starknet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@swapkit/wallet-extensions": minor
"@swapkit/wallets": minor
---

Bitget Wallet offers Cosmos and Starknet again, and connects NEAR.
18 changes: 17 additions & 1 deletion packages/wallet-extensions/src/bitget/__tests__/bitget.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// @ts-nocheck - Test file with intentional mocking of browser globals
import { afterEach, describe, expect, test } from "bun:test";
import { createHash } from "node:crypto";
import { actionCreators } from "@near-js/transactions";
import { base64 } from "@scure/base";
import { Keypair, SystemProgram, Transaction } from "@solana/web3.js";
import { Chain, SwapKitError } from "@swapkit/helpers";
import { encode } from "ripple-binary-codec";

import { getWalletMethods } from "../helpers";
import { getWalletMethods, toBitgetNearAction } from "../helpers";
import { bitgetWallet } from "../index";

const SUI_ADDRESS = `0x${"ab".repeat(32)}`;
Expand Down Expand Up @@ -138,6 +139,21 @@ describe("Bitget Solana signer", () => {
});
});

describe("Bitget Near actions", () => {
test("turns transfers and contract calls into the JSON form Bitget expects", () => {
const call = actionCreators.functionCall("ft_transfer", { amount: "5", receiver_id: "bob.near" }, 30n, 1n);

expect(toBitgetNearAction(actionCreators.transfer(1000n))).toEqual({
params: { deposit: "1000" },
type: "Transfer",
});
expect(toBitgetNearAction(call)).toEqual({
params: { args: { amount: "5", receiver_id: "bob.near" }, deposit: "1", gas: "30", methodName: "ft_transfer" },
type: "FunctionCall",
});
});
});

describe("Bitget Tron signer", () => {
function mockTron(sign) {
globalThis.window = {
Expand Down
116 changes: 114 additions & 2 deletions packages/wallet-extensions/src/bitget/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import { base64, hex } from "@scure/base";
import { Chain, type EVMChain, EVMChains, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers";
import {
Chain,
type EVMChain,
EVMChains,
GAIAConfig,
prepareNetworkSwitch,
SwapKitError,
WalletOption,
} from "@swapkit/helpers";
import type { NearSigner } from "@swapkit/toolboxes/near";
import type { RippleTransaction } from "@swapkit/toolboxes/ripple";
import type { SolanaProvider } from "@swapkit/toolboxes/solana";
import type { SuiChainSigner } from "@swapkit/toolboxes/sui";
Expand All @@ -8,7 +17,7 @@ import { Transaction } from "@swapkit/utxo-signer";
import type { Eip1193Provider } from "ethers";
import { getTonConnectWallet } from "../helpers/tonConnect";
import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType";
import type { BitgetProviders, BitgetSolanaTransaction } from "./types";
import type { BitgetNearAction, BitgetNearResponse, BitgetProviders, BitgetSolanaTransaction } from "./types";

const SUI_MAINNET = "sui:mainnet";
const TRON_USER_REJECTED = 4001;
Expand Down Expand Up @@ -80,6 +89,21 @@ async function getBitcoinWalletMethods() {
return { ...toolbox, address };
}

async function getCosmosWalletMethods() {
const wallet = getProvider("keplr", Chain.Cosmos);

await wallet.enable(GAIAConfig.chainId);
const offlineSigner = await wallet.getOfflineSignerAuto(GAIAConfig.chainId);
const accounts = await offlineSigner.getAccounts();
if (!accounts?.[0]) throw noAccountsError(Chain.Cosmos);

const { getCosmosToolbox } = await import("@swapkit/toolboxes/cosmos");
const [{ address }] = accounts;
const signer = Object.assign(offlineSigner, { getAddress: () => Promise.resolve(address) });

return { ...getCosmosToolbox(Chain.Cosmos, { signer }), address };
}

async function getSolanaWalletMethods() {
const provider = getProvider("solana", Chain.Solana);
const connection = await provider.connect();
Expand Down Expand Up @@ -221,6 +245,88 @@ async function getSuiWalletMethods() {
return { ...getSuiToolbox({ signer }), address: account.address };
}

async function getStarknetWalletMethods() {
const provider = window.starknet_bitkeep;
if (!provider) throw notFoundError(Chain.Starknet);

await provider.enable();
const { account } = provider;
if (!account) throw noAccountsError(Chain.Starknet);

const { getStarknetToolbox } = await import("@swapkit/toolboxes/starknet");

return { ...getStarknetToolbox({ signer: account, walletDeploysAccount: true }), address: account.address };
}

type NearAction = {
functionCall?: { methodName: string; args: Uint8Array; gas: bigint; deposit: bigint };
transfer?: { deposit: bigint };
};

// Bitget takes NEAR actions in the wallet-selector JSON form instead of the borsh classes
export function toBitgetNearAction({ functionCall, transfer }: NearAction): BitgetNearAction {
if (transfer) return { params: { deposit: transfer.deposit.toString() }, type: "Transfer" };

if (functionCall) {
return {
params: {
args: JSON.parse(new TextDecoder().decode(functionCall.args) || "{}"),
deposit: functionCall.deposit.toString(),
gas: functionCall.gas.toString(),
methodName: functionCall.methodName,
},
type: "FunctionCall",
};
}

throw new SwapKitError("wallet_near_method_not_supported", { method: "action", wallet: WalletOption.BITGET });
}

function readNearResponse({ error, response }: BitgetNearResponse) {
if (error) throw transactionFailedError(Chain.Near, error);
if (!(Array.isArray(response) && response.length)) throw transactionFailedError(Chain.Near, response);

return response;
}

async function getNearWalletMethods() {
const provider = getProvider("near", Chain.Near);

if (!(provider.isSignedIn() && provider.getAccountId())) {
const { accessKey, error } = await provider.requestSignIn({ contractId: "", methodNames: [] });
if (!accessKey || error) throw connectionRejectedError(Chain.Near);
}

const address = provider.getAccountId();
if (!address) throw noAccountsError(Chain.Near);

const { getNearToolbox } = await import("@swapkit/toolboxes/near");

const notSupported = (method: string) =>
Promise.reject(new SwapKitError("wallet_near_method_not_supported", { method, wallet: WalletOption.BITGET }));

// Bitget only signs and sends NEAR transactions, so the toolbox falls back to signAndSendTransaction
const signer = {
getAddress: () => Promise.resolve(address),
async getPublicKey() {
const { PublicKey } = await import("@near-js/crypto");
return PublicKey.fromString((await provider.getPublicKey()).toString());
},
async signAndSendTransaction({ actions, receiverId }: { actions: NearAction[]; receiverId?: string }) {
if (!receiverId) throw transactionFailedError(Chain.Near, "Missing receiver");

return readNearResponse(
await provider.signAndSendTransaction({ actions: actions.map(toBitgetNearAction), receiverId }),
);
},
signDelegateAction: () => notSupported("signDelegateAction"),
signNep413Message: () => notSupported("signNep413Message"),
signTransaction: () => notSupported("signTransaction"),
};

return { ...getNearToolbox({ signer: signer as unknown as NearSigner }), address };
}

const XRP_WALLET_FIELDS = ["Fee", "LastLedgerSequence", "Sequence", "SigningPubKey"];
const XRP_FULLY_CANONICAL_SIG = 0x80000000;

Expand Down Expand Up @@ -288,10 +394,16 @@ export function getWalletMethods(chain: Chain): Promise<WalletMethodsWithAddress
return getAptosWalletMethods();
case Chain.Bitcoin:
return getBitcoinWalletMethods();
case Chain.Cosmos:
return getCosmosWalletMethods();
case Chain.Near:
return getNearWalletMethods();
case Chain.Ripple:
return getRippleWalletMethods();
case Chain.Solana:
return getSolanaWalletMethods();
case Chain.Starknet:
return getStarknetWalletMethods();
case Chain.Sui:
return getSuiWalletMethods();
case Chain.Ton:
Expand Down
16 changes: 15 additions & 1 deletion packages/wallet-extensions/src/bitget/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,28 @@ export const bitgetWallet: ExtensionWallet<"connectBitget"> = createWallet({
].map((chain) => [chain, { direct: true, typedData: true }]),
),
[Chain.Bitcoin]: { direct: true },
[Chain.Cosmos]: { direct: true },
[Chain.Near]: { direct: true },
[Chain.Ripple]: { direct: true },
[Chain.Solana]: { direct: true },
[Chain.Starknet]: { direct: true },
[Chain.Sui]: { direct: true },
[Chain.Ton]: { direct: true },
[Chain.Tron]: { direct: true },
// [Chain.Aptos]: blocked on toolbox — getAptosToolbox needs to accept AptosExtensionProvider
},
supportedChains: [...EVMChains, Chain.Bitcoin, Chain.Ripple, Chain.Solana, Chain.Sui, Chain.Ton, Chain.Tron],
supportedChains: [
...EVMChains,
Chain.Bitcoin,
Chain.Cosmos,
Chain.Near,
Chain.Ripple,
Chain.Solana,
Chain.Starknet,
Chain.Sui,
Chain.Ton,
Chain.Tron,
],
walletType: WalletOption.BITGET,
});

Expand Down
44 changes: 44 additions & 0 deletions packages/wallet-extensions/src/bitget/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { AminoSignResponse, StdSignature, StdSignDoc } from "@cosmjs/amino";
import type { PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";
import type { EthereumWindowProvider } from "@swapkit/helpers";
import type { OfflineAminoSigner, OfflineDirectSigner } from "@swapkit/toolboxes/cosmos";

import type { StarknetToolboxParams } from "@swapkit/toolboxes/starknet";

type UnisatToSignInputs = { index: number; sighashTypes?: number[]; disableTweakSigner?: boolean } & (
| { address: string }
Expand All @@ -16,6 +20,20 @@ export type BitgetBtcProvider = {
) => Promise<string>;
};

export type BitgetKeplrProvider = {
enable: (chainId: string | string[]) => Promise<void>;
signAmino: (chainId: string, signer: string, signDoc: StdSignDoc, signOptions: any) => Promise<AminoSignResponse>;
signArbitrary: (chainId: string, signer: string, data: string | Uint8Array) => Promise<StdSignature>;
verifyArbitrary: (
chainId: string,
signer: string,
data: string | Uint8Array,
signature: StdSignature,
) => Promise<boolean>;
getOfflineSignerOnlyAmino: (chainId: string) => OfflineAminoSigner;
getOfflineSignerAuto: (chainId: string) => Promise<OfflineAminoSigner | OfflineDirectSigner>;
};

export type BitgetSolanaTransaction = Transaction | VersionedTransaction;

// Bitget answers either like Phantom or in the Wallet Standard shape, depending on the wallet version
Expand Down Expand Up @@ -52,6 +70,30 @@ export type BitgetSuiProvider = {
}) => Promise<{ bytes: string; signature: string }>;
};

export type BitgetNearAction = { type: string; params?: Record<string, unknown> };

type BitgetNearOutcome = { transaction?: { hash?: string }; transaction_outcome?: { id?: string } };

export type BitgetNearResponse = {
error?: string | { type?: string; message?: string };
response?: BitgetNearOutcome[] | { error: { message: string } };
};

export type BitgetNearProvider = {
getAccountId: () => string;
getPublicKey: () => Promise<{ toString: () => string } | string>;
isSignedIn: () => boolean;
requestSignIn: (params: {
contractId: string;
methodNames?: string[];
}) => Promise<{ accessKey?: unknown; error?: string | { type?: string } }>;
signAndSendTransaction: (params: { receiverId: string; actions: BitgetNearAction[] }) => Promise<BitgetNearResponse>;
};

type StarknetAccount = NonNullable<Extract<StarknetToolboxParams, { signer?: unknown }>["signer"]>;

export type BitgetStarknetProvider = { account?: StarknetAccount; enable: () => Promise<string[]> };

export type BitgetXrpProvider = {
connect: () => Promise<unknown>;
signTransaction: (transaction: Record<string, unknown>) => Promise<unknown>;
Expand All @@ -60,6 +102,8 @@ export type BitgetXrpProvider = {
export type BitgetProviders = BitgetTronProvider & {
aptos: unknown;
ethereum: EthereumWindowProvider;
keplr: BitgetKeplrProvider;
near: BitgetNearProvider;
solana: BitgetSolanaProvider;
suiWallet: BitgetSuiProvider;
unisat: BitgetBtcProvider;
Expand Down
3 changes: 2 additions & 1 deletion packages/wallet-extensions/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type { SolanaProvider } from "@swapkit/toolboxes/solana";
import type { StarknetToolboxParams } from "@swapkit/toolboxes/starknet";
import type { SubstrateInjectedExtension } from "@swapkit/toolboxes/substrate";
import type { Eip1193Provider } from "ethers";
import type { BitgetProviders } from "./bitget/types";
import type { BitgetProviders, BitgetStarknetProvider } from "./bitget/types";
import type { NearBrowserWalletProvider } from "./helpers/near";
import type { TronLinkWindow } from "./tronlink";

Expand Down Expand Up @@ -88,6 +88,7 @@ declare global {
};

bitkeep?: Partial<BitgetProviders>;
starknet_bitkeep?: BitgetStarknetProvider;
bitgetTonWallet?: { tonconnect: TonConnectBridge };

okxwallet?:
Expand Down
Loading