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

feat: OneKey wallet #102

Merged
merged 4 commits into from
Dec 11, 2024
Merged
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
5 changes: 5 additions & 0 deletions .changeset/red-mice-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@babylonlabs-io/bbn-wallet-connect": patch
---

OneKey wallet
3 changes: 2 additions & 1 deletion src/core/wallets/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import icon from "./bitcoin.png";

import injectable from "./injectable";
import okx from "./okx";
import onekey from "./onekey";

import type { ChainMetadata, BTCConfig } from "@/core/types";

const metadata: ChainMetadata<"BTC", BTCProvider, BTCConfig> = {
chain: "BTC",
name: "Bitcoin",
icon,
wallets: [injectable, okx],
wallets: [injectable, okx, onekey],
};

export default metadata;
18 changes: 18 additions & 0 deletions src/core/wallets/btc/onekey/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Network, type BTCConfig, type WalletMetadata } from "@/core/types";

import type { BTCProvider } from "../BTCProvider";

import logo from "./logo.svg";
import { OneKeyProvider } from "./provider";

const metadata: WalletMetadata<BTCProvider, BTCConfig> = {
id: "onekey",
name: "OneKey",
icon: logo,
docs: "https://onekey.so/download",
wallet: "$onekey",
createProvider: (wallet, config) => new OneKeyProvider(wallet, config),
networks: [Network.MAINNET, Network.SIGNET],
};

export default metadata;
12 changes: 12 additions & 0 deletions src/core/wallets/btc/onekey/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
192 changes: 192 additions & 0 deletions src/core/wallets/btc/onekey/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import type { BTCConfig, Fees, InscriptionIdentifier, UTXO, WalletInfo } from "@/core/types";
import { Network } from "@/core/types";
import { validateAddress } from "@/core/utils/wallet";
import { BTCProvider } from "@/core/wallets/btc/BTCProvider";

const INTERNAL_NETWORK_NAMES = {
[Network.MAINNET]: "livenet",
[Network.TESTNET]: "testnet",
[Network.SIGNET]: "signet",
};

export class OneKeyProvider extends BTCProvider {
private provider: any;
private walletInfo: WalletInfo | undefined;

constructor(wallet: any, config: BTCConfig) {
super(config);

// check whether there is an OneKey extension
if (!wallet?.btcwallet) {
throw new Error("OneKey Wallet extension not found");
}

this.provider = wallet.btcwallet;
}

connectWallet = async (): Promise<void> => {
try {
await this.provider.connectWallet();
} catch (error) {
if ((error as Error)?.message?.includes("rejected")) {
throw new Error("Connection to OneKey Wallet was rejected");
} else {
throw new Error((error as Error)?.message);
}
}

const address = await this.provider.getAddress();
validateAddress(this.config.network, address);

const publicKeyHex = await this.provider.getPublicKeyHex();

if (publicKeyHex && address) {
this.walletInfo = {
publicKeyHex,
address,
};
} else {
throw new Error("Could not connect to OneKey Wallet");
}
};

getWalletProviderName = async (): Promise<string> => {
return "OneKey";
};

getAddress = async (): Promise<string> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

return this.walletInfo.address;
};

getPublicKeyHex = async (): Promise<string> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

return this.walletInfo.publicKeyHex;
};

signPsbt = async (psbtHex: string): Promise<string> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");
if (!psbtHex) throw new Error("psbt hex is required");

return this.provider.signPsbt(psbtHex);
};

signPsbts = async (psbtsHexes: string[]): Promise<string[]> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");
if (!psbtsHexes && !Array.isArray(psbtsHexes)) throw new Error("psbts hexes are required");

return this.provider.signPsbts(psbtsHexes);
};

getNetwork = async (): Promise<Network> => {
const internalNetwork = await this.provider.getNetwork();

for (const [key, value] of Object.entries(INTERNAL_NETWORK_NAMES)) {
// TODO remove as soon as OneKey implements
if (value === "testnet") {
// in case of testnet return signet
return Network.SIGNET;
} else if (value === internalNetwork) {
return key as Network;
}
}

throw new Error("Unsupported network");
};

signMessageBIP322 = async (message: string): Promise<string> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

return await this.provider.signMessageBIP322(message);
};

signMessage = async (message: string, type: "ecdsa" | "bip322-simple" = "ecdsa"): Promise<string> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

return await this.provider.signMessage(message, type);
};

on = (eventName: string, callBack: () => void) => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

// subscribe to account change event: `accountChanged` -> `accountsChanged`
if (eventName === "accountChanged") {
return this.provider.on("accountsChanged", callBack);
}
return this.provider.on(eventName, callBack);
};

off = (eventName: string, callBack: () => void) => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");

// unsubscribe to account change event
if (eventName === "accountChanged") {
return this.provider.off("accountsChanged", callBack);
}
return this.provider.off(eventName, callBack);
};

// Mempool calls
getBalance = async (): Promise<number> => {
return await this.mempool.getAddressBalance(await this.getAddress());
};

getNetworkFees = async (): Promise<Fees> => {
return await this.mempool.getNetworkFees();
};

pushTx = async (txHex: string): Promise<string> => {
return await this.mempool.pushTx(txHex);
};

getUtxos = async (address: string, amount: number): Promise<UTXO[]> => {
return await this.mempool.getFundingUTXOs(address, amount);
};

getBTCTipHeight = async (): Promise<number> => {
return await this.mempool.getTipHeight();
};

// Inscriptions are only available on OneKey Wallet BTC mainnet
getInscriptions = async (): Promise<InscriptionIdentifier[]> => {
if (!this.walletInfo) throw new Error("OneKey Wallet not connected");
if (this.config.network !== Network.MAINNET) {
throw new Error("Inscriptions are only available on OneKey Wallet BTC Mainnet");
}

// max num of iterations to prevent infinite loop
const MAX_ITERATIONS = 100;
// Fetch inscriptions in batches of 100
const limit = 100;
const inscriptionIdentifiers: InscriptionIdentifier[] = [];
let cursor = 0;
let iterations = 0;
try {
while (iterations < MAX_ITERATIONS) {
const { list } = await this.provider.getInscriptions(cursor, limit);
const identifiers = list.map((i: { output: string }) => {
const [txid, vout] = i.output.split(":");
return {
txid,
vout,
};
});
inscriptionIdentifiers.push(...identifiers);
if (list.length < limit) {
break;
}
cursor += limit;
iterations++;
if (iterations >= MAX_ITERATIONS) {
throw new Error("Exceeded maximum iterations when fetching inscriptions");
}
}
} catch {
throw new Error("Failed to get inscriptions from OneKey Wallet");
}

return inscriptionIdentifiers;
};
}