Skip to content

Commit fc08ec2

Browse files
authored
[Xc-admin] ledger (#561)
* Add ledger support * Checkpoint * Checkpoint * Package locK * Console err
1 parent 56e5ed8 commit fc08ec2

File tree

5 files changed

+254
-24
lines changed

5 files changed

+254
-24
lines changed

governance/xc_admin/packages/crank_executor/src/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,16 @@ async function run() {
5353
multisigProgramId: DEFAULT_MULTISIG_PROGRAM_ID,
5454
});
5555
const multisigParser = MultisigParser.fromCluster(CLUSTER as PythCluster);
56-
const wormholeFee = (
57-
await getWormholeBridgeData(
58-
squad.connection,
59-
multisigParser.wormholeBridgeAddress!,
60-
COMMITMENT
61-
)
62-
).config.fee;
56+
57+
const wormholeFee = multisigParser.wormholeBridgeAddress
58+
? (
59+
await getWormholeBridgeData(
60+
squad.connection,
61+
multisigParser.wormholeBridgeAddress!,
62+
COMMITMENT
63+
)
64+
).config.fee
65+
: 0;
6366

6467
const proposals = await getProposals(squad, VAULT, undefined, "executeReady");
6568
for (const proposal of proposals) {
@@ -114,11 +117,15 @@ async function run() {
114117
} catch (error) {
115118
// Mark the transaction as cancelled if we failed to run it
116119
if (error instanceof SendTransactionError) {
120+
console.error(error);
117121
await squad.cancelTransaction(proposal.publicKey);
122+
console.log("Cancelled: ", proposal.publicKey.toBase58());
118123
}
119124
break;
120125
}
121126
}
127+
} else {
128+
console.log("Skipping: ", proposal.publicKey.toBase58());
122129
}
123130
}
124131
}

governance/xc_admin/packages/xc_admin_cli/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
},
2020
"dependencies": {
2121
"@coral-xyz/anchor": "^0.26.0",
22+
"@ledgerhq/hw-transport": "^6.27.10",
23+
"@ledgerhq/hw-transport-node-hid": "^6.27.10",
2224
"@pythnetwork/client": "^2.9.0",
2325
"@solana/web3.js": "^1.73.0",
2426
"@sqds/mesh": "^1.0.6",

governance/xc_admin/packages/xc_admin_cli/src/index.ts

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,43 @@ import {
2828
WORMHOLE_ADDRESS,
2929
} from "xc_admin_common";
3030
import { pythOracleProgram } from "@pythnetwork/client";
31+
import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
32+
import { LedgerNodeWallet } from "./ledger";
33+
34+
export async function loadHotWalletOrLedger(
35+
wallet: string,
36+
lda: number,
37+
ldc: number
38+
): Promise<Wallet> {
39+
if (wallet === "ledger") {
40+
return await LedgerNodeWallet.createWallet(lda, ldc);
41+
} else {
42+
return new NodeWallet(
43+
Keypair.fromSecretKey(
44+
Uint8Array.from(JSON.parse(fs.readFileSync(wallet, "ascii")))
45+
)
46+
);
47+
}
48+
}
3149

3250
const mutlisigCommand = (name: string, description: string) =>
3351
program
3452
.command(name)
3553
.description(description)
3654
.requiredOption("-c, --cluster <network>", "solana cluster to use")
37-
.requiredOption("-w, --wallet <filepath>", "path to the operations key")
38-
.requiredOption("-v, --vault <pubkey>", "multisig address");
55+
.requiredOption(
56+
"-w, --wallet <filepath>",
57+
'path to the operations key or "ledger"'
58+
)
59+
.requiredOption("-v, --vault <pubkey>", "multisig address")
60+
.option(
61+
"-lda, --ledger-derivation-account <number>",
62+
"ledger derivation account to use"
63+
)
64+
.option(
65+
"-ldc, --ledger-derivation-change <number>",
66+
"ledger derivation change to use"
67+
);
3968

4069
program
4170
.name("xc_admin_cli")
@@ -56,10 +85,10 @@ mutlisigCommand(
5685
)
5786

5887
.action(async (options: any) => {
59-
const wallet = new NodeWallet(
60-
Keypair.fromSecretKey(
61-
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
62-
)
88+
const wallet = await loadHotWalletOrLedger(
89+
options.wallet,
90+
options.ledgerDerivationAccount,
91+
options.ledgerDerivationChange
6392
);
6493
const cluster: PythCluster = options.cluster;
6594
const programId: PublicKey = new PublicKey(options.programId);
@@ -104,7 +133,7 @@ mutlisigCommand(
104133
.accept()
105134
.accounts({
106135
currentAuthority: current,
107-
newAuthority: mapKey(vaultAuthority),
136+
newAuthority: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
108137
programAccount: programId,
109138
programDataAccount,
110139
bpfUpgradableLoader: BPF_UPGRADABLE_LOADER,
@@ -128,10 +157,10 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
128157
.requiredOption("-b, --buffer <pubkey>", "buffer account")
129158

130159
.action(async (options: any) => {
131-
const wallet = new NodeWallet(
132-
Keypair.fromSecretKey(
133-
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
134-
)
160+
const wallet = await loadHotWalletOrLedger(
161+
options.wallet,
162+
options.ledgerDerivationAccount,
163+
options.ledgerDerivationChange
135164
);
136165
const cluster: PythCluster = options.cluster;
137166
const programId: PublicKey = new PublicKey(options.programId);
@@ -166,7 +195,11 @@ mutlisigCommand("upgrade-program", "Upgrade a program from a buffer")
166195
{ pubkey: wallet.publicKey, isSigner: false, isWritable: true },
167196
{ pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false },
168197
{ pubkey: SYSVAR_CLOCK_PUBKEY, isSigner: false, isWritable: false },
169-
{ pubkey: mapKey(vaultAuthority), isSigner: true, isWritable: false },
198+
{
199+
pubkey: isRemote ? mapKey(vaultAuthority) : vaultAuthority,
200+
isSigner: true,
201+
isWritable: false,
202+
},
170203
],
171204
};
172205

@@ -186,10 +219,10 @@ mutlisigCommand(
186219
.requiredOption("-p, --price <pubkey>", "Price account to modify")
187220
.requiredOption("-e, --exponent <number>", "New exponent")
188221
.action(async (options: any) => {
189-
const wallet = new NodeWallet(
190-
Keypair.fromSecretKey(
191-
Uint8Array.from(JSON.parse(fs.readFileSync(options.wallet, "ascii")))
192-
)
222+
const wallet = await loadHotWalletOrLedger(
223+
options.wallet,
224+
options.ledgerDerivationAccount,
225+
options.ledgerDerivationChange
193226
);
194227
const cluster: PythCluster = options.cluster;
195228
const vault: PublicKey = new PublicKey(options.vault);
@@ -222,7 +255,10 @@ program
222255
.command("parse-transaction")
223256
.description("Parse a transaction sitting in the multisig")
224257
.requiredOption("-c, --cluster <network>", "solana cluster to use")
225-
.requiredOption("-t, --transaction <pubkey>", "path to the operations key")
258+
.requiredOption(
259+
"-t, --transaction <pubkey>",
260+
"address of the outstanding transaction"
261+
)
226262
.action(async (options: any) => {
227263
const cluster = options.cluster;
228264
const transaction: PublicKey = new PublicKey(options.transaction);
@@ -245,4 +281,22 @@ program
245281
console.log(JSON.stringify(parsed, null, 2));
246282
});
247283

284+
mutlisigCommand("approve", "Approve a transaction sitting in the multisig")
285+
.requiredOption(
286+
"-t, --transaction <pubkey>",
287+
"address of the outstanding transaction"
288+
)
289+
.action(async (options: any) => {
290+
const wallet = await loadHotWalletOrLedger(
291+
options.wallet,
292+
options.ledgerDerivationAccount,
293+
options.ledgerDerivationChange
294+
);
295+
const transaction: PublicKey = new PublicKey(options.transaction);
296+
const cluster: PythCluster = options.cluster;
297+
298+
const squad = SquadsMesh.endpoint(getPythClusterApiUrl(cluster), wallet);
299+
await squad.approveTransaction(transaction);
300+
});
301+
248302
program.parse();
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { Wallet } from "@coral-xyz/anchor/dist/cjs/provider";
2+
import Transport, {
3+
StatusCodes,
4+
TransportStatusError,
5+
} from "@ledgerhq/hw-transport";
6+
import TransportNodeHid from "@ledgerhq/hw-transport-node-hid";
7+
import { PublicKey, Transaction } from "@solana/web3.js";
8+
9+
export class LedgerNodeWallet implements Wallet {
10+
private _derivationPath: Buffer;
11+
private _transport: Transport;
12+
publicKey: PublicKey;
13+
14+
constructor(
15+
derivationPath: Buffer,
16+
transport: Transport,
17+
publicKey: PublicKey
18+
) {
19+
this._derivationPath = derivationPath;
20+
this._transport = transport;
21+
this.publicKey = publicKey;
22+
}
23+
24+
static async createWallet(
25+
derivationAccount?: number,
26+
derivationChange?: number
27+
): Promise<LedgerNodeWallet> {
28+
const transport = await TransportNodeHid.create();
29+
const derivationPath = getDerivationPath(
30+
derivationAccount,
31+
derivationChange
32+
);
33+
const publicKey = await getPublicKey(transport, derivationPath);
34+
console.log(`Loaded ledger: ${publicKey.toBase58()}}`);
35+
return new LedgerNodeWallet(derivationPath, transport, publicKey);
36+
}
37+
38+
async signTransaction(transaction: Transaction): Promise<Transaction> {
39+
console.log("Please approve the transaction on your ledger device...");
40+
const transport = this._transport;
41+
const publicKey = this.publicKey;
42+
43+
const signature = await signTransaction(
44+
transport,
45+
transaction,
46+
this._derivationPath
47+
);
48+
transaction.addSignature(publicKey, signature);
49+
return transaction;
50+
}
51+
52+
async signAllTransactions(txs: Transaction[]): Promise<Transaction[]> {
53+
return await Promise.all(txs.map((tx) => this.signTransaction(tx)));
54+
}
55+
}
56+
57+
/** @internal */
58+
function getDerivationPath(account?: number, change?: number): Buffer {
59+
const length = account !== undefined ? (change === undefined ? 3 : 4) : 2;
60+
const derivationPath = Buffer.alloc(1 + length * 4);
61+
62+
let offset = derivationPath.writeUInt8(length, 0);
63+
offset = derivationPath.writeUInt32BE(harden(44), offset); // Using BIP44
64+
offset = derivationPath.writeUInt32BE(harden(501), offset); // Solana's BIP44 path
65+
66+
if (account !== undefined) {
67+
offset = derivationPath.writeUInt32BE(harden(account), offset);
68+
if (change !== undefined) {
69+
derivationPath.writeUInt32BE(harden(change), offset);
70+
}
71+
}
72+
73+
return derivationPath;
74+
}
75+
76+
const BIP32_HARDENED_BIT = (1 << 31) >>> 0;
77+
78+
/** @internal */
79+
function harden(n: number): number {
80+
return (n | BIP32_HARDENED_BIT) >>> 0;
81+
}
82+
83+
const INS_GET_PUBKEY = 0x05;
84+
const INS_SIGN_MESSAGE = 0x06;
85+
86+
const P1_NON_CONFIRM = 0x00;
87+
const P1_CONFIRM = 0x01;
88+
89+
const P2_EXTEND = 0x01;
90+
const P2_MORE = 0x02;
91+
92+
const MAX_PAYLOAD = 255;
93+
94+
const LEDGER_CLA = 0xe0;
95+
96+
/** @internal */
97+
export async function getPublicKey(
98+
transport: Transport,
99+
derivationPath: Buffer
100+
): Promise<PublicKey> {
101+
const bytes = await send(
102+
transport,
103+
INS_GET_PUBKEY,
104+
P1_NON_CONFIRM,
105+
derivationPath
106+
);
107+
return new PublicKey(bytes);
108+
}
109+
110+
/** @internal */
111+
export async function signTransaction(
112+
transport: Transport,
113+
transaction: Transaction,
114+
derivationPath: Buffer
115+
): Promise<Buffer> {
116+
const paths = Buffer.alloc(1);
117+
paths.writeUInt8(1, 0);
118+
119+
const message = transaction.serializeMessage();
120+
const data = Buffer.concat([paths, derivationPath, message]);
121+
122+
return await send(transport, INS_SIGN_MESSAGE, P1_CONFIRM, data);
123+
}
124+
125+
/** @internal */
126+
async function send(
127+
transport: Transport,
128+
instruction: number,
129+
p1: number,
130+
data: Buffer
131+
): Promise<Buffer> {
132+
let p2 = 0;
133+
let offset = 0;
134+
135+
if (data.length > MAX_PAYLOAD) {
136+
while (data.length - offset > MAX_PAYLOAD) {
137+
const buffer = data.subarray(offset, offset + MAX_PAYLOAD);
138+
const response = await transport.send(
139+
LEDGER_CLA,
140+
instruction,
141+
p1,
142+
p2 | P2_MORE,
143+
buffer
144+
);
145+
if (response.length !== 2)
146+
throw TransportStatusError(StatusCodes.INCORRECT_DATA);
147+
148+
p2 |= P2_EXTEND;
149+
offset += MAX_PAYLOAD;
150+
}
151+
}
152+
153+
const buffer = data.subarray(offset);
154+
const response = await transport.send(
155+
LEDGER_CLA,
156+
instruction,
157+
p1,
158+
p2,
159+
buffer
160+
);
161+
162+
return response.subarray(0, response.length - 2);
163+
}

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)