Skip to content
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
21 changes: 14 additions & 7 deletions governance/xc_admin/packages/crank_executor/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions governance/xc_admin/packages/xc_admin_cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
88 changes: 71 additions & 17 deletions governance/xc_admin/packages/xc_admin_cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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);
Expand Down Expand Up @@ -104,7 +133,7 @@ mutlisigCommand(
.accept()
.accounts({
currentAuthority: current,
newAuthority: mapKey(vaultAuthority),
newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
programAccount: programId,
programDataAccount,
bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
Expand All @@ -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);
Expand Down Expand Up @@ -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,
},
],
};

Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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();
163 changes: 163 additions & 0 deletions governance/xc_admin/packages/xc_admin_cli/src/ledger.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.