[Xc-admin] ledger (#561)

* Add ledger support

* Checkpoint

* Checkpoint

* Package locK

* Console err
This commit is contained in:
guibescos 2023-02-03 12:07:15 -06:00 committed by GitHub
parent 56e5ed8c1d
commit fc08ec277e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 254 additions and 24 deletions

View File

@ -53,13 +53,16 @@ async function run() {
multisigProgramId: DEFAULT_MULTISIG_PROGRAM_ID,
});
const multisigParser = MultisigParser.fromCluster(CLUSTER as PythCluster);
const wormholeFee = (
await getWormholeBridgeData(
squad.connection,
multisigParser.wormholeBridgeAddress!,
COMMITMENT
)
).config.fee;
const wormholeFee = multisigParser.wormholeBridgeAddress
? (
await getWormholeBridgeData(
squad.connection,
multisigParser.wormholeBridgeAddress!,
COMMITMENT
)
).config.fee
: 0;
const proposals = await getProposals(squad, VAULT, undefined, "executeReady");
for (const proposal of proposals) {
@ -114,11 +117,15 @@ async function run() {
} catch (error) {
// Mark the transaction as cancelled if we failed to run it
if (error instanceof SendTransactionError) {
console.error(error);
await squad.cancelTransaction(proposal.publicKey);
console.log("Cancelled: ", proposal.publicKey.toBase58());
}
break;
}
}
} else {
console.log("Skipping: ", proposal.publicKey.toBase58());
}
}
}

View File

@ -19,6 +19,8 @@
},
"dependencies": {
"@coral-xyz/anchor": "^0.26.0",
"@ledgerhq/hw-transport": "^6.27.10",
"@ledgerhq/hw-transport-node-hid": "^6.27.10",
"@pythnetwork/client": "^2.9.0",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",

View File

@ -28,14 +28,43 @@ import {
WORMHOLE_ADDRESS,
} from "xc_admin_common";
import { pythOracleProgram } from "@pythnetwork/client";
import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
import { LedgerNodeWallet } from "./ledger";
export async function loadHotWalletOrLedger(
wallet: string,
lda: number,
ldc: number
): Promise<Wallet> {
if (wallet === "ledger") {
return await LedgerNodeWallet.createWallet(lda, ldc);
} else {
return new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(wallet, "ascii")))
)
);
}
}
const mutlisigCommand = (name: string, description: string) =>
program
.command(name)
.description(description)
.requiredOption("-c, --cluster <network>", "solana cluster to use")
.requiredOption("-w, --wallet <filepath>", "path to the operations key")
.requiredOption("-v, --vault <pubkey>", "multisig address");
.requiredOption(
"-w, --wallet <filepath>",
'path to the operations key or "ledger"'
)
.requiredOption("-v, --vault <pubkey>", "multisig address")
.option(
"-lda, --ledger-derivation-account <number>",
"ledger derivation account to use"
)
.option(
"-ldc, --ledger-derivation-change <number>",
"ledger derivation change to use"
);
program
.name("xc_admin_cli")
@ -56,10 +85,10 @@ mutlisigCommand(
)
.action(async (options: any) => {
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
)
const wallet = await loadHotWalletOrLedger(
options.wallet,
options.ledgerDerivationAccount,
options.ledgerDerivationChange
);
const cluster: PythCluster = options.cluster;
const programId: PublicKey = new PublicKey(options.programId);
@ -104,7 +133,7 @@ mutlisigCommand(
.accept()
.accounts({
currentAuthority: current,
newAuthority: mapKey(vaultAuthority),
newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
programAccount: programId,
programDataAccount,
bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
@ -128,10 +157,10 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
.requiredOption("-b, --buffer <pubkey>", "buffer account")
.action(async (options: any) => {
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
)
const wallet = await loadHotWalletOrLedger(
options.wallet,
options.ledgerDerivationAccount,
options.ledgerDerivationChange
);
const cluster: PythCluster = options.cluster;
const programId: PublicKey = new PublicKey(options.programId);
@ -166,7 +195,11 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
{ pubkey: wallet.publicKey, isSigner: false, isWritable: true },
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
{ pubkey: mapKey(vaultAuthority), isSigner: true, isWritable: false },
{
pubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
isSigner: true,
isWritable: false,
},
],
};
@ -186,10 +219,10 @@ mutlisigCommand(
.requiredOption("-p, --price <pubkey>", "Price account to modify")
.requiredOption("-e, --exponent <number>", "New exponent")
.action(async (options: any) => {
const wallet = new NodeWallet(
Keypair.fromSecretKey(
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
)
const wallet = await loadHotWalletOrLedger(
options.wallet,
options.ledgerDerivationAccount,
options.ledgerDerivationChange
);
const cluster: PythCluster = options.cluster;
const vault: PublicKey = new PublicKey(options.vault);
@ -222,7 +255,10 @@ program
.command("parse-transaction")
.description("Parse a transaction sitting in the multisig")
.requiredOption("-c, --cluster <network>", "solana cluster to use")
.requiredOption("-t, --transaction <pubkey>", "path to the operations key")
.requiredOption(
"-t, --transaction <pubkey>",
"address of the outstanding transaction"
)
.action(async (options: any) => {
const cluster = options.cluster;
const transaction: PublicKey = new PublicKey(options.transaction);
@ -245,4 +281,22 @@ program
console.log(JSON.stringify(parsed, null, 2));
});
mutlisigCommand("approve", "Approve a transaction sitting in the multisig")
.requiredOption(
"-t, --transaction <pubkey>",
"address of the outstanding transaction"
)
.action(async (options: any) => {
const wallet = await loadHotWalletOrLedger(
options.wallet,
options.ledgerDerivationAccount,
options.ledgerDerivationChange
);
const transaction: PublicKey = new PublicKey(options.transaction);
const cluster: PythCluster = options.cluster;
const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
await squad.approveTransaction(transaction);
});
program.parse();

View File

@ -0,0 +1,163 @@
import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
import Transport, {
StatusCodes,
TransportStatusError,
} from "@ledgerhq/hw-transport";
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
import { PublicKey, Transaction } from "@solana/web3.js";
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);
console.log(`Loaded ledger: ${publicKey.toBase58()}}`);
return new LedgerNodeWallet(derivationPath, transport, publicKey);
}
async signTransaction(transaction: Transaction): Promise<Transaction> {
console.log("Please approve the transaction on your ledger device...");
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)));
}
}
/** @internal */
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;
/** @internal */
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);
}
/** @internal */
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
);
if (response.length !== 2)
throw 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);
}

4
package-lock.json generated
View File

@ -1356,6 +1356,8 @@
"license": "ISC",
"dependencies": {
"@coral-xyz/anchor": "^0.26.0",
"@ledgerhq/hw-transport": "^6.27.10",
"@ledgerhq/hw-transport-node-hid": "^6.27.10",
"@pythnetwork/client": "^2.9.0",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",
@ -85114,6 +85116,8 @@
"version": "file:governance/xc_admin/packages/xc_admin_cli",
"requires": {
"@coral-xyz/anchor": "^0.26.0",
"@ledgerhq/hw-transport": "^6.27.10",
"@ledgerhq/hw-transport-node-hid": "^6.27.10",
"@pythnetwork/client": "^2.9.0",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",