add ledger support (#277)

* add ledger support

* address feedbacks
This commit is contained in:
Daniel Chew 2022-09-15 01:26:39 +08:00 committed by GitHub
parent 1c6977ec96
commit f3e17ad307
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 1312 additions and 47 deletions

View File

@ -1,4 +1,4 @@
# Pyth Multisig CLI Program
# Pyth Governance Multisig CLI Program
This program allows you to create/execute a multisig transaction that includes an instruction from wormhole for cross-chain governance.
@ -10,28 +10,65 @@ npm install
## Usage
Note: Node.js v17.15.0 or higher is required as it [introduces support for fetch API](https://nodejs.org/tr/blog/release/v17.5.0/).
Note:
- Node.js v17.15.0 or higher is required as it [introduces support for fetch API](https://nodejs.org/tr/blog/release/v17.5.0/).
- When using with Ledger, please enable [blind signing](https://www.ledger.com/academy/enable-blind-signing-why-when-and-how-to-stay-safe) in the Solana app settings. TLDR: When you enable blind signing, you enable your device to approve a smart contract transaction, even though it hasnt been able to display full contract data to you. In other words, youre agreeing to trust, instead of verify, the transaction. You still have to manually approve each transactions.
- Information about ledger derivation can be found [here](https://github.com/LedgerHQ/ledger-live-common/blob/master/docs/derivation.md).
- RPC URLs can be found [here](https://book.wormhole.com/reference/rpcnodes.html).
### Create a multisig transaction
```
npm start -- create -c <CLUSTER> -v <VAULT_ADDRESS> -w <WALLET_SECRET_KEY_FILEPATH> -p <PAYLOAD>
npm start -- create -c <CLUSTER> -v <VAULT_ADDRESS> -l -lda <LEDGER_DERIVATION_ACCOUNT> -ldc <LEDGER_DERIVATION_CHANGE> -w <WALLET_SECRET_KEY_FILEPATH> -p <PAYLOAD>
```
Example:
To use ledger with default derivation account and change:
```
npm start -- create -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -p hello
```
To use ledger with custom derivation account and/or change:
```
npm start -- create -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -lda 0 -p hello
npm start -- create -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -lda 0 -ldc 1 -p hello
```
To use hot wallet :
```
npm start -- create -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -w keys/key.json -p hello
```
---
### Execute a multisig transaction
```
npm start -- execute -c <CLUSTER> -v <VAULT_ADDRESS> -w <WALLET_SECRET_KEY_FILEPATH> -m <MESSAGE_SECRET_KEY_FILEPATH> -t <TX_ID> -u <RPC_URL>
```
To use ledger with default derivation account and change:
```
npm start -- execute -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -m keys/message.json -t GSC8r7Qsi9pc698fckaQgzHufG6LqVq3vZijyu5KsXLh -u https://wormhole-v2-testnet-api.certus.one/
```
To use ledger with custom derivation account and/or change:
```
npm start -- execute -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -lda 0 -m keys/message.json -t GSC8r7Qsi9pc698fckaQgzHufG6LqVq3vZijyu5KsXLh -u https://wormhole-v2-testnet-api.certus.one/
npm start -- execute -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -l -lda 0 -ldc 1 -m keys/message.json -t GSC8r7Qsi9pc698fckaQgzHufG6LqVq3vZijyu5KsXLh -u https://wormhole-v2-testnet-api.certus.one/
```
Example:
```
npm start -- execute -c devnet -v HezRVdwZmKpdKbksxFytKnHTQVztiTmL3GHdNadMFYui -w keys/key.json -m keys/message.json -t GSC8r7Qsi9pc698fckaQgzHufG6LqVq3vZijyu5KsXLh -u https://wormhole-v2-testnet-api.certus.one/
```
https://github.com/LedgerHQ/ledger-live/wiki/LLC:derivation

File diff suppressed because it is too large Load Diff

View File

@ -39,6 +39,8 @@
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.6.2",
"@ledgerhq/hw-transport": "^6.27.2",
"@ledgerhq/hw-transport-node-hid": "^6.27.2",
"@project-serum/anchor": "^0.25.0",
"@solana/web3.js": "^1.53.0",
"@sqds/sdk": "^1.0.4",

View File

@ -10,14 +10,13 @@ import {
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
sendAndConfirmTransaction,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import Squads from "@sqds/sdk";
import bs58 from "bs58";
import { program } from "commander";
import * as fs from "fs";
import { LedgerNodeWallet } from "./wallet";
setDefaultWasm("node");
@ -31,6 +30,15 @@ program
.description("Create a new multisig transaction")
.option("-c, --cluster <network>", "solana cluster to use", "devnet")
.requiredOption("-v, --vault-address <address>", "multisig vault address")
.option("-l, --ledger", "use ledger")
.option(
"-lda, --ledger-derivation-account <number>",
"ledger derivation account to use"
)
.option(
"-ldc, --ledger-derivation-change <number>",
"ledger derivation change to use"
)
.option(
"-w, --wallet <filepath>",
"multisig wallet secret key filepath",
@ -41,6 +49,9 @@ program
createMultisigTx(
options.cluster,
new PublicKey(options.vaultAddress),
options.ledger,
options.ledgerDerivationAccount,
options.ledgerDerivationChange,
options.wallet,
options.payload
);
@ -51,6 +62,15 @@ program
.description("Execute a multisig transaction that is ready")
.option("-c, --cluster <network>", "solana cluster to use", "devnet")
.requiredOption("-v, --vault-address <address>", "multisig vault address")
.option("-l, --ledger", "use ledger")
.option(
"-lda, --ledger-derivation-account <number>",
"ledger derivation account to use"
)
.option(
"-ldc, --ledger-derivation-change <number>",
"ledger derivation change to use"
)
.option(
"-w, --wallet <filepath>",
"multisig wallet secret key filepath",
@ -67,6 +87,9 @@ program
executeMultisigTx(
options.cluster,
new PublicKey(options.vaultAddress),
options.ledger,
options.ledgerDerivationAccount,
options.ledgerDerivationChange,
options.wallet,
options.message,
new PublicKey(options.txPda),
@ -74,6 +97,8 @@ program
);
});
// TODO: add subcommand for creating governance messages in the right format
program.parse();
// custom solana cluster type
@ -129,15 +154,28 @@ async function getWormholeMessageIx(
async function createMultisigTx(
cluster: Cluster,
vault: PublicKey,
ledger: boolean,
ledgerDerivationAccount: number | undefined,
ledgerDerivationChange: number | undefined,
walletPath: string,
payload: string
) {
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
)
);
let wallet: LedgerNodeWallet | NodeWallet;
if (ledger) {
console.log("Please connect to ledger...");
wallet = await LedgerNodeWallet.createWallet(
ledgerDerivationAccount,
ledgerDerivationChange
);
console.log(`Ledger connected! ${wallet.publicKey.toBase58()}`);
} else {
wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
)
);
console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
}
const squads =
cluster === "devnet" ? Squads.devnet(wallet) : Squads.mainnet(wallet);
const msAccount = await squads.getMultisig(vault);
@ -148,6 +186,9 @@ async function createMultisigTx(
);
console.log(`Emitter Address: ${emitter.toBase58()}`);
console.log("Creating new transaction...");
if (ledger)
console.log("Please approve the transaction on your ledger device...");
const newTx = await squads.createTransaction(
msAccount.publicKey,
msAccount.authorityIndex
@ -170,12 +211,20 @@ async function createMultisigTx(
);
console.log("Wormhole instructions created.");
console.log("Creating transaction...");
console.log("Adding instruction 1/2 to transaction...");
if (ledger)
console.log("Please approve the transaction on your ledger device...");
// transfer sol to the message account
await squads.addInstruction(newTx.publicKey, wormholeIxs[0]);
console.log("Adding instruction 2/2 to transaction...");
if (ledger)
console.log("Please approve the transaction on your ledger device...");
// wormhole post message ix
await squads.addInstruction(newTx.publicKey, wormholeIxs[1]);
console.log("Activating transaction...");
if (ledger)
console.log("Please approve the transaction on your ledger device...");
await squads.activateTransaction(newTx.publicKey);
console.log("Transaction created.");
}
@ -183,17 +232,30 @@ async function createMultisigTx(
async function executeMultisigTx(
cluster: string,
vault: PublicKey,
ledger: boolean,
ledgerDerivationAccount: number | undefined,
ledgerDerivationChange: number | undefined,
walletPath: string,
messagePath: string,
txPDA: PublicKey,
rpcUrl: string
) {
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
)
);
console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
let wallet: LedgerNodeWallet | NodeWallet;
if (ledger) {
console.log("Please connect to ledger...");
wallet = await LedgerNodeWallet.createWallet(
ledgerDerivationAccount,
ledgerDerivationChange
);
console.log(`Ledger connected! ${wallet.publicKey.toBase58()}`);
} else {
wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(walletPath, "ascii")))
)
);
console.log(`Loaded wallet with address: ${wallet.publicKey.toBase58()}`);
}
const message = Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(messagePath, "ascii")))
@ -223,6 +285,7 @@ async function executeMultisigTx(
// airdrop 0.1 SOL to emitter if on devnet
if (cluster === "devnet") {
console.log("Airdropping 0.1 SOL to emitter...");
const airdropSignature = await squads.connection.requestAirdrop(
emitter,
0.1 * LAMPORTS_PER_SOL
@ -239,19 +302,22 @@ async function executeMultisigTx(
const { blockhash, lastValidBlockHeight } =
await squads.connection.getLatestBlockhash();
const tx = new Transaction({
const executeTx = new anchor.web3.Transaction({
blockhash,
lastValidBlockHeight,
feePayer: wallet.payer.publicKey,
feePayer: wallet.publicKey,
});
tx.add(executeIx);
await squads.wallet.signTransaction(tx);
const signature = await sendAndConfirmTransaction(
squads.connection,
tx,
[wallet.payer, message],
{ commitment: "confirmed" }
);
const provider = new anchor.AnchorProvider(squads.connection, wallet, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});
executeTx.add(executeIx);
console.log("Sending transaction...");
if (ledger)
console.log("Please approve the transaction on your ledger device...");
const signature = await provider.sendAndConfirm(executeTx, [message]);
console.log(
`Executed tx: https://explorer.solana.com/tx/${signature}${
cluster === "devnet" ? "?cluster=devnet" : ""

View File

@ -0,0 +1,84 @@
import type { default as Transport } from '@ledgerhq/hw-transport';
import { StatusCodes, TransportStatusError } from '@ledgerhq/hw-transport';
import type { Transaction } from '@solana/web3.js';
import { PublicKey } from '@solana/web3.js';
export function getDerivationPath(account?: number, change?: number): Buffer {
const length = account !== undefined ? (change === undefined ? 3 : 4) : 2;
const derivationPath = Buffer.alloc(1 + length * 4);
let offset = derivationPath.writeUInt8(length, 0);
offset = derivationPath.writeUInt32BE(harden(44), offset); // Using BIP44
offset = derivationPath.writeUInt32BE(harden(501), offset); // Solana's BIP44 path
if (account !== undefined) {
offset = derivationPath.writeUInt32BE(harden(account), offset);
if (change !== undefined) {
derivationPath.writeUInt32BE(harden(change), offset);
}
}
return derivationPath;
}
const BIP32_HARDENED_BIT = (1 << 31) >>> 0;
function harden(n: number): number {
return (n | BIP32_HARDENED_BIT) >>> 0;
}
const INS_GET_PUBKEY = 0x05;
const INS_SIGN_MESSAGE = 0x06;
const P1_NON_CONFIRM = 0x00;
const P1_CONFIRM = 0x01;
const P2_EXTEND = 0x01;
const P2_MORE = 0x02;
const MAX_PAYLOAD = 255;
const LEDGER_CLA = 0xe0;
/** @internal */
export async function getPublicKey(transport: Transport, derivationPath: Buffer): Promise<PublicKey> {
const bytes = await send(transport, INS_GET_PUBKEY, P1_NON_CONFIRM, derivationPath);
return new PublicKey(bytes);
}
/** @internal */
export async function signTransaction(
transport: Transport,
transaction: Transaction,
derivationPath: Buffer
): Promise<Buffer> {
const paths = Buffer.alloc(1);
paths.writeUInt8(1, 0);
const message = transaction.serializeMessage();
const data = Buffer.concat([paths, derivationPath, message]);
return await send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, data);
}
async function send(transport: Transport, instruction: number, p1: number, data: Buffer): Promise<Buffer> {
let p2 = 0;
let offset = 0;
if (data.length > MAX_PAYLOAD) {
while (data.length - offset > MAX_PAYLOAD) {
const buffer = data.subarray(offset, offset + MAX_PAYLOAD);
const response = await transport.send(LEDGER_CLA, instruction, p1, p2 | P2_MORE, buffer);
// @ts-ignore -- TransportStatusError is a constructor Function, not a Class
if (response.length !== 2) throw new TransportStatusError(StatusCodes.INCORRECT_DATA);
p2 |= P2_EXTEND;
offset += MAX_PAYLOAD;
}
}
const buffer = data.subarray(offset);
const response = await transport.send(LEDGER_CLA, instruction, p1, p2, buffer);
return response.subarray(0, response.length - 2);
}

View File

@ -0,0 +1,52 @@
// this file referenced solana's wallet adapter for ledger: https://github.com/solana-labs/wallet-adapter/blob/master/packages/wallets/ledger/src/adapter.ts
import type { default as Transport } from "@ledgerhq/hw-transport";
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
import { Wallet } from "@project-serum/anchor/dist/cjs/provider.js";
import type { PublicKey, Transaction } from "@solana/web3.js";
import { getDerivationPath, getPublicKey, signTransaction } from "./util";
export class LedgerNodeWallet implements Wallet {
private _derivationPath: Buffer;
private _transport: Transport;
publicKey: PublicKey;
constructor(
derivationPath: Buffer,
transport: Transport,
publicKey: PublicKey
) {
this._derivationPath = derivationPath;
this._transport = transport;
this.publicKey = publicKey;
}
static async createWallet(
derivationAccount?: number,
derivationChange?: number
): Promise<LedgerNodeWallet> {
const transport = await TransportNodeHid.create();
const derivationPath = getDerivationPath(
derivationAccount,
derivationChange
);
const publicKey = await getPublicKey(transport, derivationPath);
return new LedgerNodeWallet(derivationPath, transport, publicKey);
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
const transport = this._transport;
const publicKey = this.publicKey;
const signature = await signTransaction(
transport,
transaction,
this._derivationPath
);
transaction.addSignature(publicKey, signature);
return transaction;
}
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
return await Promise.all(txs.map((tx) => this.signTransaction(tx)));
}
}