Skip to content

Commit

Permalink
feat: Keystone (#113)
Browse files Browse the repository at this point in the history
* keystone wallet

* hardware

rm clog

* feat: add label

---------

Co-authored-by: David Totraev <[email protected]>
  • Loading branch information
gbarkhatov and totraev authored Dec 13, 2024
1 parent 0c6ed8b commit 621e270
Show file tree
Hide file tree
Showing 18 changed files with 2,006 additions and 90 deletions.
5 changes: 5 additions & 0 deletions .changeset/good-geese-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@babylonlabs-io/bbn-wallet-connect": patch
---

Keystone wallet
1,513 changes: 1,430 additions & 83 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@
"release": "npm run build && changeset publish"
},
"dependencies": {
"@bitcoin-js/tiny-secp256k1-asmjs": "^2.2.3",
"@cosmjs/stargate": "^0.32.4",
"@keplr-wallet/types": "^0.12.156",
"@keystonehq/animated-qr": "^0.8.6",
"@keystonehq/keystone-sdk": "^0.4.1",
"@keystonehq/sdk": "^0.21.3",
"buffer": "^6.0.3",
"nanoevents": "^9.1.0"
},
Expand Down
2 changes: 2 additions & 0 deletions src/components/Chains/index.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const Default: Story = {
docs: "",
provider: null,
account: null,
label: "Installed",
},
],
},
Expand All @@ -41,6 +42,7 @@ export const Default: Story = {
docs: "",
provider: null,
account: null,
label: "Installed",
},
},
className: "b-h-[600px]",
Expand Down
6 changes: 5 additions & 1 deletion src/components/Wallets/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Text } from "@babylonlabs-io/bbn-core-ui";
import type { Meta, StoryObj } from "@storybook/react";

import { WalletButton } from "@/components/WalletButton";
import { IWallet } from "@/core/types";

import { Wallets } from "./index";

Expand All @@ -14,7 +15,7 @@ export default meta;

type Story = StoryObj<typeof meta>;

const wallets = [
const wallets: IWallet[] = [
{
id: "injectable",
name: "Binance (Browser)",
Expand All @@ -23,6 +24,7 @@ const wallets = [
docs: "",
provider: null,
account: null,
label: "Injected",
},
{
id: "okx",
Expand All @@ -32,6 +34,7 @@ const wallets = [
docs: "",
provider: null,
account: null,
label: "Installed",
},
{
id: "keystone",
Expand All @@ -41,6 +44,7 @@ const wallets = [
docs: "",
provider: null,
account: null,
label: "Hardware wallet",
},
];

Expand Down
4 changes: 2 additions & 2 deletions src/components/Wallets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const Wallets = memo(({ chain, className, append, onClose, onBack, onSele
<WalletButton
name={injectableWallet.name}
logo={injectableWallet.icon}
label="Injected"
label={injectableWallet.label}
onClick={() => onSelectWallet?.(chain, injectableWallet)}
/>
)}
Expand All @@ -47,7 +47,7 @@ export const Wallets = memo(({ chain, className, append, onClose, onBack, onSele
key={wallet.id}
name={wallet.name}
logo={wallet.icon}
label={wallet.installed ? "Installed" : ""}
label={wallet.label}
onClick={() => onSelectWallet?.(chain, wallet)}
/>
))}
Expand Down
9 changes: 8 additions & 1 deletion src/core/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface WalletOptions<P extends IProvider> {
networks: Network[];
origin: any;
provider: P | null;
label?: string;
}

export class Wallet<P extends IProvider> implements IWallet {
Expand All @@ -18,22 +19,28 @@ export class Wallet<P extends IProvider> implements IWallet {
readonly docs: string;
readonly networks: Network[];
readonly provider: P | null = null;
private readonly _label?: string;
account: Account | null = null;

constructor({ id, origin, name, icon, docs, networks, provider }: WalletOptions<P>) {
constructor({ id, origin, name, icon, docs, networks, provider, label }: WalletOptions<P>) {
this.id = id;
this.origin = origin;
this.name = name;
this.icon = icon;
this.docs = docs;
this.networks = networks;
this.provider = provider;
this._label = label;
}

get installed() {
return Boolean(this.provider);
}

get label() {
return this._label ?? (this.installed ? "Installed" : "");
}

async connect() {
if (!this.provider) {
throw Error("Provider not found");
Expand Down
2 changes: 2 additions & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const createWallet = async <P extends IProvider, C>(metadata: WalletMetad
docs = "",
networks = [],
createProvider,
label,
} = metadata;

const options: WalletOptions<P> = {
Expand All @@ -23,6 +24,7 @@ export const createWallet = async <P extends IProvider, C>(metadata: WalletMetad
provider: null,
docs,
networks,
label,
};

if (walletGetter) {
Expand Down
2 changes: 2 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export interface IWallet<P extends IProvider = IProvider> {
installed: boolean;
provider: P | null;
account: Account | null;
label: string;
}

export interface IChain<K extends string = string, P extends IProvider = IProvider> {
Expand All @@ -96,6 +97,7 @@ export interface Account {
export interface WalletMetadata<P extends IProvider, C> {
id: string;
wallet?: string | ((context: any, config: C) => any);
label?: string;
name: string | ((wallet: any, config: C) => Promise<string>);
icon: string | ((wallet: any, config: C) => Promise<string>);
docs: string;
Expand Down
133 changes: 133 additions & 0 deletions src/core/utils/bip322.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import * as bitcoin from "bitcoinjs-lib";

/**
* https://github.com/ACken2/bip322-js/blob/main/src/BIP322.ts
* use the existing the bip322-js file with some modification, use the file directly instead of install the whole package,
* because the package will introduce too many dependencies.
* Class that handles BIP-322 related operations.
*/
class BIP322 {
// BIP322 message tag
static TAG = Buffer.from("BIP0322-signed-message");

/**
* Compute the message hash as specified in the BIP-322.
* The standard is specified in BIP-340 as:
* The function hashtag(x) where tag is a UTF-8 encoded tag name and x is a byte array returns the 32-byte hash SHA256(SHA256(tag) || SHA256(tag) || x).
* @param message Message to be hashed
* @returns Hashed message
*/
public static hashMessage(message: string): Buffer {
// Compute the message hash - SHA256(SHA256(tag) || SHA256(tag) || message)
const { sha256 } = bitcoin.crypto;
const tagHash = sha256(this.TAG);
const result = sha256(Buffer.concat([tagHash, tagHash, Buffer.from(message)]));
return result;
}

/**
* Build a to_spend transaction using simple signature in accordance to the BIP-322.
* @param message Message to be signed using BIP-322
* @param scriptPublicKey The script public key for the signing wallet
* @returns Bitcoin transaction that correspond to the to_spend transaction
*/
public static buildToSpendTx(message: string, scriptPublicKey: Buffer): bitcoin.Transaction {
// Create PSBT object for constructing the transaction
const psbt = new bitcoin.Psbt();
// Set default value for nVersion and nLockTime
psbt.setVersion(0); // nVersion = 0
psbt.setLocktime(0); // nLockTime = 0
// Compute the message hash - SHA256(SHA256(tag) || SHA256(tag) || message)
const messageHash = this.hashMessage(message);
// Construct the scriptSig - OP_0 PUSH32[ message_hash ]
const OP_0_CODE = 0x00; // OP_0
const PUSH32_CODE = 0x20; // PUSH32
const scriptSigPartOne = new Uint8Array([OP_0_CODE, PUSH32_CODE]); // OP_0 PUSH32
const scriptSig = new Uint8Array(scriptSigPartOne.length + messageHash.length);
scriptSig.set(scriptSigPartOne);
scriptSig.set(messageHash, scriptSigPartOne.length);
// Set the input
psbt.addInput({
hash: "0".repeat(64), // vin[0].prevout.hash = 0000...000
index: 0xffffffff, // vin[0].prevout.n = 0xFFFFFFFF
sequence: 0, // vin[0].nSequence = 0
finalScriptSig: Buffer.from(scriptSig), // vin[0].scriptSig = OP_0 PUSH32[ message_hash ]
witnessScript: Buffer.from([]), // vin[0].scriptWitness = []
});
// Set the output
psbt.addOutput({
value: 0, // vout[0].nValue = 0
script: scriptPublicKey, // vout[0].scriptPubKey = message_challenge
});
// Return transaction
return psbt.extractTransaction();
}

/**
* Build a to_sign transaction using simple signature in accordance to the BIP-322.
* @param toSpendTxId Transaction ID of the to_spend transaction as constructed by buildToSpendTx
* @param witnessScript The script public key for the signing wallet, or the redeemScript for P2SH-P2WPKH address
* @param isRedeemScript Set to true if the provided witnessScript is a redeemScript for P2SH-P2WPKH address, default to false
* @param tapInternalKey Used to set the taproot internal public key of a taproot signing address when provided, default to undefined
* @returns Ready-to-be-signed bitcoinjs.Psbt transaction
*/
public static buildToSignTx(
toSpendTxId: string,
witnessScript: Buffer,
isRedeemScript: boolean = false,
tapInternalKey?: Buffer,
): bitcoin.Psbt {
// Create PSBT object for constructing the transaction
const psbt = new bitcoin.Psbt();
// Set default value for nVersion and nLockTime
psbt.setVersion(0); // nVersion = 0
psbt.setLocktime(0); // nLockTime = 0
// Set the input
psbt.addInput({
hash: toSpendTxId, // vin[0].prevout.hash = to_spend.txid
index: 0, // vin[0].prevout.n = 0
sequence: 0, // vin[0].nSequence = 0
witnessUtxo: {
script: witnessScript,
value: 0,
},
});
// Set redeemScript as witnessScript if isRedeemScript
if (isRedeemScript) {
psbt.updateInput(0, {
redeemScript: witnessScript,
});
}
// Set tapInternalKey if provided
if (tapInternalKey) {
psbt.updateInput(0, {
tapInternalKey: tapInternalKey,
});
}
// Set the output
const OP_RETURN_CODE = 0x6a; // OP_RETURN
psbt.addOutput({
value: 0, // vout[0].nValue = 0
script: Buffer.from([OP_RETURN_CODE]), // vout[0].scriptPubKey = OP_RETURN
});
return psbt;
}

/**
* Encode witness stack in a signed BIP-322 PSBT into its base-64 encoded format.
* @param signedPsbt Signed PSBT
* @returns Base-64 encoded witness data
*/
public static encodeWitness(signedPsbt: bitcoin.Psbt): string {
// Obtain the signed witness data
const witness = signedPsbt.data.inputs[0].finalScriptWitness;
// Check if the witness data is present
if (!witness) {
throw new Error("Cannot encode empty witness stack.");
}
// Return the base-64 encoded witness stack
return witness.toString("base64");
}
}

export default BIP322;
14 changes: 14 additions & 0 deletions src/core/utils/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { networks } from "bitcoinjs-lib";

import { Network } from "@/core/types";

export function validateAddress(network: Network, address: string): void {
Expand All @@ -9,3 +11,15 @@ export function validateAddress(network: Network, address: string): void {
throw new Error(`Unsupported network: ${network}. Please provide a valid network.`);
}
}

export const toNetwork = (network: Network): networks.Network => {
switch (network) {
case Network.MAINNET:
return networks.bitcoin;
case Network.TESTNET:
case Network.SIGNET:
return networks.testnet;
default:
throw new Error("Unsupported network");
}
};
3 changes: 2 additions & 1 deletion src/core/wallets/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import icon from "./bitcoin.png";
import bitget from "./bitget";
import cactus from "./cactus";
import injectable from "./injectable";
import keystone from "./keystone";
import okx from "./okx";
import onekey from "./onekey";

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

export default metadata;
1 change: 1 addition & 0 deletions src/core/wallets/btc/injectable/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const metadata: WalletMetadata<BTCProvider, BTCConfig> = {
wallet: "btcwallet",
createProvider: (wallet) => wallet,
networks: [Network.MAINNET, Network.SIGNET],
label: "Injectable",
};

export default metadata;
18 changes: 18 additions & 0 deletions src/core/wallets/btc/keystone/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 { KeystoneProvider } from "./provider";

const metadata: WalletMetadata<BTCProvider, BTCConfig> = {
id: "keystone",
name: "Keystone",
icon: logo,
docs: "https://www.keyst.one/btc-only",
createProvider: (wallet, config) => new KeystoneProvider(wallet, config),
networks: [Network.MAINNET, Network.SIGNET],
label: "Hardware wallet",
};

export default metadata;
4 changes: 4 additions & 0 deletions src/core/wallets/btc/keystone/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 621e270

Please sign in to comment.