From 3ab972f6c7761ae713fd8dd9010af9bac2fa27f9 Mon Sep 17 00:00:00 2001 From: Joshua Averett Date: Mon, 22 Dec 2025 21:38:33 -0800 Subject: [PATCH 1/6] Added solana for governance client and VAA verification. --- src/solana/tsconfig.json | 5 +- ts-pkgs/deploy/guardian/governance_client.ts | 341 ++++++++++++++----- ts-pkgs/deploy/package.json | 2 + ts-pkgs/deploy/tsconfig.json | 6 +- ts-pkgs/deploy/vaatool/verifyVaa.ts | 196 +++++++++-- 5 files changed, 436 insertions(+), 114 deletions(-) diff --git a/src/solana/tsconfig.json b/src/solana/tsconfig.json index d449ad8..68eb719 100644 --- a/src/solana/tsconfig.json +++ b/src/solana/tsconfig.json @@ -1,9 +1,10 @@ { "extends": "../../ts-pkgs/config/tsconfig.base.json", "compilerOptions": { - "outDir": "./ts-build" + "outDir": "./ts-build", + "resolveJsonModule": true }, - "include": ["tests", "target/types", "scripts"], + "include": ["tests", "target/types", "target/idl", "scripts"], "references": [ { "path": "../../ts-pkgs/tss-definitions" } ] diff --git a/ts-pkgs/deploy/guardian/governance_client.ts b/ts-pkgs/deploy/guardian/governance_client.ts index 7d5b614..578dd03 100644 --- a/ts-pkgs/deploy/guardian/governance_client.ts +++ b/ts-pkgs/deploy/guardian/governance_client.ts @@ -2,12 +2,20 @@ import fs from "fs"; import { createWalletClient, defineChain, http, isHex, encodePacked, Hex } from "viem"; import { privateKeyToAccount } from "viem/accounts"; import { waitForTransactionReceipt } from "viem/actions"; -import yargs from "yargs"; +import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, Transaction } from "@solana/web3.js"; +import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor"; +import yargs, { type Argv } from "yargs"; import { hideBin } from 'yargs/helpers'; import { parseGuardianKey, errorMsg, errorStack } from '@xlabs-xyz/peer-lib'; +import type { VerificationV2 } from "../../../src/solana/target/types/verification_v2.js"; +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const idl = require("../../../src/solana/target/idl/verification_v2.json"); + // Default contract address for WormholeVerifier -const DEFAULT_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"; // TODO: Update with actual deployed address +const DEFAULT_EVM_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"; // TODO: Update with actual deployed address +const DEFAULT_SOLANA_PROGRAM_ID = "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6"; const UPDATE_SET_SHARD_ID = 0; const UPDATE_APPEND_SCHNORR_KEY = 1; @@ -23,11 +31,16 @@ const UPDATE_ABI = [ }, ] as const; -type Args = { +type BaseArgs = { + chain: "evm" | "solana"; contractAddress: string; rpcUrl: string; - chainId: number; signer: string; +} + +type EvmArgs = BaseArgs & { + chain: "evm"; + chainId: number; limit: number; } & ({ command: "set_shard_id"; @@ -39,6 +52,17 @@ type Args = { command: "pull_multisigs"; }) +type SolanaArgs = BaseArgs & { + chain: "solana"; + command: "append_schnorr"; + postedVaa: string; + signatureSet: string; + newKeyIndex: number; + oldKeyIndex?: number; +} + +type Args = EvmArgs | SolanaArgs; + // TODO: Use binary-layout for these function encodeSetShardId(guardianMessage: Buffer): Hex { // set_shard_id: opcode (1 byte) + guardian message data @@ -64,7 +88,7 @@ function encodePullMultisigKeyData(limit: number): `0x${string}` { ); } -function encodeUpdate(args: Args, dataBytes: Buffer): `0x${string}` { +function encodeUpdate(args: EvmArgs, dataBytes: Buffer): `0x${string}` { if (args.command === "append_schnorr") { const pullData = encodePullMultisigKeyData(args.limit); const appendData = encodeAppendSchnorrKey(dataBytes); @@ -80,86 +104,23 @@ function encodeUpdate(args: Args, dataBytes: Buffer): `0x${string}` { } } -async function main() { - const parser = yargs(hideBin(process.argv)) - .command('set_shard_id ', 'Set the shard ID of the guardian', - (yargs) => yargs.positional('guardian-message', { - description: 'Path to file containing base64-encoded signed guardian message', - type: 'string', - } - )) - .command('append_schnorr ', 'Append a Schnorr key to the VerificationV2 contract', - (yargs) => yargs.positional('vaa', { - description: 'Base64 encoded string of a governance VAA', - type: 'string', - } - )) - .command('pull_multisigs', 'Pull multisig sets from the core contract') - .demandCommand(1, 'A command is required') - .strictCommands() - //TODO: add support for ledger signer - .option('signer', { - description: 'Path to gpg armor guardian private key file', - demandOption: true, - type: 'string', - alias: 's', - }) - .option('contract-address', { - description: 'Address of the WormholeVerifier contract', - type: 'string', - default: DEFAULT_CONTRACT_ADDRESS, - alias: 'c', - }) - .option('rpc-url', { - description: 'RPC endpoint URL for the target chain', - type: 'string', - default: 'https://eth.llamarpc.com', - alias: 'r', - }) - .option('chain-id', { - description: 'EIP-155 Chain ID', - type: 'number', - default: 1, - alias: 'i', - }) - .option('limit', { - description: 'Maximum number of multisig sets to pull.', - defaultDescription: '0 (Pull all necessary multisig sets)', - type: 'number', - alias: 'l', - default: 0, - }) - .strictOptions() - .help() - .alias('help', 'h'); +// Derive the schnorr key PDA from key index +function deriveSchnorrKeyPda(programId: PublicKey, schnorrKeyIndex: number): PublicKey { + const schnorrKeyIndexBuf = Buffer.alloc(4); + schnorrKeyIndexBuf.writeUint32LE(schnorrKeyIndex); + const seeds = [Buffer.from("schnorrkey"), schnorrKeyIndexBuf]; + const [pda] = PublicKey.findProgramAddressSync(seeds, programId); + return pda; +} - const parsedArgs = await parser.parse(); - const args = { ...parsedArgs, command: parsedArgs._[0] } as Args; - - let dataBytes = Buffer.alloc(0); - - if (args.command === "set_shard_id" ) { - // Load guardian message from base64 encoded file - // TODO: Take a TLS certificate as input instead - try { - const messageBase64 = fs.readFileSync(args.guardianMessage, 'utf-8').trim(); - dataBytes = Buffer.from(messageBase64, 'base64'); - } catch (error) { - console.error(`Failed to load data from ${args.guardianMessage}: ${errorMsg(error)}`); - process.exit(1); - } - console.log(`Loaded ${dataBytes.length} bytes of data from ${args.guardianMessage}`); - } else if (args.command === "append_schnorr") { - // Load VAA from base64 encoded string - try { - dataBytes = Buffer.from(args.vaa, 'base64'); - } catch (error) { - console.error(`Failed to load VAA: ${errorMsg(error)}`); - process.exit(1); - } - console.log(`Loaded ${dataBytes.length} bytes of VAA`); - } +// Derive the latest key PDA +function deriveLatestKeyPda(programId: PublicKey): PublicKey { + const seeds = [Buffer.from("latestkey")]; + const [pda] = PublicKey.findProgramAddressSync(seeds, programId); + return pda; +} +async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise { let signerKey: Hex; try { const signerFile = fs.readFileSync(args.signer, 'utf-8'); @@ -236,6 +197,220 @@ async function main() { } } +async function executeSolanaTransaction(args: SolanaArgs): Promise { + // Load keypair from JSON file + let keypair: Keypair; + try { + const keypairData = JSON.parse(fs.readFileSync(args.signer, 'utf-8')); + keypair = Keypair.fromSecretKey(new Uint8Array(keypairData)); + } catch (error) { + console.error(`Failed to load Solana keypair from ${args.signer}: ${errorMsg(error)}`); + process.exit(1); + } + + console.log(`Using signer: ${keypair.publicKey.toBase58()}`); + + const connection = new Connection(args.rpcUrl, "confirmed"); + const programId = new PublicKey(args.contractAddress || DEFAULT_SOLANA_PROGRAM_ID); + + const wallet = new Wallet(keypair); + const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); + const program = new Program(idl as VerificationV2, provider); + + const postedVaa = new PublicKey(args.postedVaa); + const signatureSet = new PublicKey(args.signatureSet); + const latestKeyPda = deriveLatestKeyPda(programId); + const newSchnorrKeyPda = deriveSchnorrKeyPda(programId, args.newKeyIndex); + const oldSchnorrKeyPda = args.oldKeyIndex !== undefined + ? deriveSchnorrKeyPda(programId, args.oldKeyIndex) + : null; + + console.log(`Target program: ${programId.toBase58()}`); + console.log(`Posted VAA: ${postedVaa.toBase58()}`); + console.log(`Signature Set: ${signatureSet.toBase58()}`); + console.log(`Latest Key PDA: ${latestKeyPda.toBase58()}`); + console.log(`New Schnorr Key PDA: ${newSchnorrKeyPda.toBase58()}`); + if (oldSchnorrKeyPda) { + console.log(`Old Schnorr Key PDA: ${oldSchnorrKeyPda.toBase58()}`); + } + console.log(`RPC URL: ${args.rpcUrl}`); + + try { + console.log('\nBuilding transaction...'); + const ix = await program.methods.appendSchnorrKey() + .accountsPartial({ + payer: keypair.publicKey, + vaa: postedVaa, + signatureSet: signatureSet, + latestSchnorrKey: latestKeyPda, + newSchnorrKey: newSchnorrKeyPda, + oldSchnorrKey: oldSchnorrKeyPda, + }) + .instruction(); + + const tx = new Transaction().add(ix); + + console.log('Sending transaction...'); + const signature = await sendAndConfirmTransaction(connection, tx, [keypair], { + commitment: "confirmed", + }); + + console.log(`Transaction confirmed: ${signature}`); + } catch (error) { + console.error(`Transaction failed: ${errorStack(error)}`); + process.exit(1); + } +} + +async function main() { + const parser = yargs(hideBin(process.argv)) + .command('set_shard_id ', 'Set the shard ID of the guardian (EVM only)', + (yargs: Argv) => yargs.positional('guardian-message', { + description: 'Path to file containing base64-encoded signed guardian message', + type: 'string', + } + )) + .command('append_schnorr ', 'Append a Schnorr key to the VerificationV2 contract', + (yargs: Argv) => yargs + .positional('vaa', { + description: 'Base64 encoded governance VAA (EVM) or ignored (Solana)', + type: 'string', + }) + .option('posted-vaa', { + description: '[Solana only] Address of the posted VAA account', + type: 'string', + }) + .option('signature-set', { + description: '[Solana only] Address of the signature set account', + type: 'string', + }) + .option('new-key-index', { + description: '[Solana only] Index of the new schnorr key to create', + type: 'number', + }) + .option('old-key-index', { + description: '[Solana only] Index of the old schnorr key (omit for init)', + type: 'number', + }) + ) + .command('pull_multisigs', 'Pull multisig sets from the core contract (EVM only)') + .demandCommand(1, 'A command is required') + .strictCommands() + .option('chain', { + description: 'Target chain type', + choices: ['evm', 'solana'] as const, + default: 'evm' as const, + alias: 't', + }) + //TODO: add support for ledger signer + .option('signer', { + description: 'Path to signer key file (GPG armor guardian key for EVM, JSON keypair for Solana)', + demandOption: true, + type: 'string', + alias: 's', + }) + .option('contract-address', { + description: 'Address of the WormholeVerifier contract/program', + type: 'string', + default: DEFAULT_EVM_CONTRACT_ADDRESS, + alias: 'c', + }) + .option('rpc-url', { + description: 'RPC endpoint URL for the target chain', + type: 'string', + default: 'https://eth.llamarpc.com', + alias: 'r', + }) + .option('chain-id', { + description: '[EVM only] EIP-155 Chain ID', + type: 'number', + default: 1, + alias: 'i', + }) + .option('limit', { + description: '[EVM only] Maximum number of multisig sets to pull.', + defaultDescription: '0 (Pull all necessary multisig sets)', + type: 'number', + alias: 'l', + default: 0, + }) + .strictOptions() + .help() + .alias('help', 'h'); + + const parsedArgs = await parser.parse(); + const command = parsedArgs._[0] as string; + + if (parsedArgs.chain === "solana") { + // Validate Solana-specific requirements + if (command !== "append_schnorr") { + console.error(`Command '${command}' is not supported on Solana. Only 'append_schnorr' is available.`); + process.exit(1); + } + + if (!parsedArgs.postedVaa || !parsedArgs.signatureSet || parsedArgs.newKeyIndex === undefined) { + console.error("Solana append_schnorr requires --posted-vaa, --signature-set, and --new-key-index"); + process.exit(1); + } + + const args: SolanaArgs = { + chain: "solana", + command: "append_schnorr", + contractAddress: parsedArgs.contractAddress === DEFAULT_EVM_CONTRACT_ADDRESS + ? DEFAULT_SOLANA_PROGRAM_ID + : parsedArgs.contractAddress, + rpcUrl: parsedArgs.rpcUrl === 'https://eth.llamarpc.com' + ? 'https://api.mainnet-beta.solana.com' + : parsedArgs.rpcUrl, + signer: parsedArgs.signer, + postedVaa: parsedArgs.postedVaa, + signatureSet: parsedArgs.signatureSet, + newKeyIndex: parsedArgs.newKeyIndex, + oldKeyIndex: parsedArgs.oldKeyIndex, + }; + + await executeSolanaTransaction(args); + } else { + // EVM flow + let dataBytes = Buffer.alloc(0); + + if (command === "set_shard_id") { + const guardianMessage = parsedArgs.guardianMessage as string; + try { + const messageBase64 = fs.readFileSync(guardianMessage, 'utf-8').trim(); + dataBytes = Buffer.from(messageBase64, 'base64'); + } catch (error) { + console.error(`Failed to load data from ${guardianMessage}: ${errorMsg(error)}`); + process.exit(1); + } + console.log(`Loaded ${dataBytes.length} bytes of data from ${guardianMessage}`); + } else if (command === "append_schnorr") { + const vaa = parsedArgs.vaa as string; + try { + dataBytes = Buffer.from(vaa, 'base64'); + } catch (error) { + console.error(`Failed to load VAA: ${errorMsg(error)}`); + process.exit(1); + } + console.log(`Loaded ${dataBytes.length} bytes of VAA`); + } + + const args: EvmArgs = { + chain: "evm", + command: command as EvmArgs["command"], + contractAddress: parsedArgs.contractAddress, + rpcUrl: parsedArgs.rpcUrl, + chainId: parsedArgs.chainId, + signer: parsedArgs.signer, + limit: parsedArgs.limit, + ...(command === "set_shard_id" ? { guardianMessage: parsedArgs.guardianMessage as string } : {}), + ...(command === "append_schnorr" ? { vaa: parsedArgs.vaa as string } : {}), + } as EvmArgs; + + await executeEvmTransaction(args, dataBytes); + } +} + main().catch((error: unknown) => { console.error(`[ERROR] Unhandled error: ${errorStack(error)}`); process.exit(1); diff --git a/ts-pkgs/deploy/package.json b/ts-pkgs/deploy/package.json index b55b49f..c427f2f 100644 --- a/ts-pkgs/deploy/package.json +++ b/ts-pkgs/deploy/package.json @@ -10,6 +10,8 @@ "license": "Apache-2.0", "description": "", "dependencies": { + "@coral-xyz/anchor": "^0.31.1", + "@solana/web3.js": "^1.98.4", "@wormhole-foundation/sdk": "^4.5.0", "@xlabs-xyz/peer-lib": "workspace:*", "viem": "^2.41.2", diff --git a/ts-pkgs/deploy/tsconfig.json b/ts-pkgs/deploy/tsconfig.json index 20877eb..7fefae0 100644 --- a/ts-pkgs/deploy/tsconfig.json +++ b/ts-pkgs/deploy/tsconfig.json @@ -1,10 +1,12 @@ { "extends": "../config/tsconfig.base.json", "compilerOptions": { - "outDir": "./ts-build" + "outDir": "./ts-build", + "resolveJsonModule": true }, "include": ["evm", "guardian", "vaatool"], "references": [ - { "path": "../peer-lib" } + { "path": "../peer-lib" }, + { "path": "../../src/solana" } ] } diff --git a/ts-pkgs/deploy/vaatool/verifyVaa.ts b/ts-pkgs/deploy/vaatool/verifyVaa.ts index 9fdfa70..da8ce3a 100644 --- a/ts-pkgs/deploy/vaatool/verifyVaa.ts +++ b/ts-pkgs/deploy/vaatool/verifyVaa.ts @@ -1,8 +1,20 @@ import { createPublicClient, http, Address, Hex, PublicClient } from "viem"; -import { Chain, isChainId, toChain } from "@wormhole-foundation/sdk"; +import { Chain, isChainId, toChain } from "@wormhole-foundation/sdk"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { Program, AnchorProvider } from "@coral-xyz/anchor"; +import yargs from "yargs"; +import { hideBin } from 'yargs/helpers'; + +import type { VerificationV2 } from "../../../src/solana/target/types/verification_v2.js"; +import { createRequire } from "module"; +const require = createRequire(import.meta.url); +const idl = require("../../../src/solana/target/idl/verification_v2.json"); const VERIFICATION_FAILED_ERROR_SIGNATURE = "0x32629d58"; +// Default Solana program ID from IDL +const DEFAULT_SOLANA_PROGRAM_ID = "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6"; + const WORMHOLE_VERIFIER_ABI = [ { inputs: [{ internalType: "bytes", name: "data", type: "bytes" }], @@ -21,7 +33,7 @@ const WORMHOLE_VERIFIER_ABI = [ export type VerificationResult = { verified: true; emitterChainId: number; - emitterAddress: Address; + emitterAddress: string; sequence: bigint; payloadOffset: number; } | { @@ -29,7 +41,8 @@ export type VerificationResult = { error: string; } -async function verifyVaa( +// EVM verification +async function verifyVaaEvm( client: PublicClient, verifierAddress: Address, vaa: Hex, @@ -49,7 +62,7 @@ async function verifyVaa( payloadOffset: result[3], }; } catch (error) { - if (!(error instanceof Error && "raw" in (error.cause as { raw: Hex }))) { + if (!(error instanceof Error && error.cause && typeof error.cause === 'object' && "raw" in error.cause)) { return { verified: false, error: error instanceof Error ? error.message : "Unknown error" }; } const hexData = (error.cause as { raw: Hex }).raw; @@ -61,6 +74,84 @@ async function verifyVaa( } } +// Derive the schnorr key PDA from key index +function deriveSchnorrKeyPda(programId: PublicKey, schnorrKeyIndex: number): PublicKey { + const schnorrKeyIndexBuf = Buffer.alloc(4); + schnorrKeyIndexBuf.writeUint32LE(schnorrKeyIndex); + const seeds = [Buffer.from("schnorrkey"), schnorrKeyIndexBuf]; + const [pda] = PublicKey.findProgramAddressSync(seeds, programId); + return pda; +} + +// Extract schnorr key index from VAA header (first 4 bytes after version byte for Schnorr VAAs) +function getSchnorrKeyIndexFromVaa(vaaBytes: Buffer): number { + // Version is first byte, schnorr key index is next 4 bytes (little endian) + return vaaBytes.readUInt32LE(1); +} + +// Solana verification via simulate +async function verifyVaaSolana( + connection: Connection, + programId: PublicKey, + vaaBytes: Buffer, +): Promise { + try { + // Create a read-only provider (no wallet needed for simulation) + const provider = new AnchorProvider( + connection, + { + publicKey: PublicKey.default, + signTransaction: async () => { throw new Error("Read-only"); }, + signAllTransactions: async () => { throw new Error("Read-only"); }, + }, + { commitment: "confirmed" } + ); + + const program = new Program(idl as VerificationV2, provider); + + const schnorrKeyIndex = getSchnorrKeyIndexFromVaa(vaaBytes); + const schnorrKeyPda = deriveSchnorrKeyPda(programId, schnorrKeyIndex); + + // Use simulate to verify without submitting a transaction + const ix = await program.methods + .verifyVaa(vaaBytes) + .accounts({ + keyAccount: schnorrKeyPda, + }) + .instruction(); + + const { blockhash } = await connection.getLatestBlockhash(); + const message = new (await import("@solana/web3.js")).TransactionMessage({ + payerKey: PublicKey.default, + recentBlockhash: blockhash, + instructions: [ix], + }).compileToV0Message(); + + const tx = new (await import("@solana/web3.js")).VersionedTransaction(message); + + const result = await connection.simulateTransaction(tx, { + sigVerify: false, + }); + + if (result.value.err) { + const logs = result.value.logs?.join("\n") || "No logs"; + return { verified: false, error: `Simulation failed: ${JSON.stringify(result.value.err)}\nLogs:\n${logs}` }; + } + + // For Solana, we don't get the parsed VAA data back from verify_vaa (only verify_vaa_and_decode returns it) + // Return a simplified success result + return { + verified: true, + emitterChainId: 0, // Not available from verify_vaa + emitterAddress: "0x" + "0".repeat(64), + sequence: 0n, + payloadOffset: 0, + }; + } catch (error) { + return { verified: false, error: error instanceof Error ? error.message : "Unknown error" }; + } +} + function toMaybeUnknownChain(chainId: number): Chain | "Unknown" { if (isChainId(chainId)) { return toChain(chainId); @@ -68,47 +159,98 @@ function toMaybeUnknownChain(chainId: number): Chain | "Unknown" { return "Unknown"; } -function getVaaType(vaa: Hex): "Multisig" | "Schnorr" | undefined { - if (vaa.startsWith("0x01")) { +function getVaaType(vaaBytes: Buffer): "Multisig" | "Schnorr" | undefined { + if (vaaBytes[0] === 0x01) { return "Multisig"; } - if (vaa.startsWith("0x02")) { + if (vaaBytes[0] === 0x02) { return "Schnorr"; } return undefined; } +type Args = { + chain: "evm" | "solana"; + rpcUrl: string; + verifierAddress: string; + vaa: string; +} + async function main() { - // TODO: use yargs to get explicit option parsing instead - const rpcUrl = process.argv[2]; - const verifierAddress = process.argv[3] as Address; - const vaaBase64 = process.argv[4]; - if (!vaaBase64) { - console.error("Usage: tsx verifyV2Vaa.ts "); - process.exit(1); - } - const client = createPublicClient({ transport: http(rpcUrl),}); - const vaaHex = ("0x" + Buffer.from(vaaBase64, "base64").toString("hex")) as Hex; - const vaaType = getVaaType(vaaHex); + const parser = yargs(hideBin(process.argv)) + .option('chain', { + description: 'Target chain type', + choices: ['evm', 'solana'] as const, + default: 'evm' as const, + alias: 't', + }) + .option('rpc-url', { + description: 'RPC endpoint URL', + type: 'string', + demandOption: true, + alias: 'r', + }) + .option('verifier-address', { + description: 'Verifier contract/program address', + type: 'string', + demandOption: true, + alias: 'a', + }) + .option('vaa', { + description: 'Base64 encoded VAA to verify', + type: 'string', + demandOption: true, + alias: 'v', + }) + .strictOptions() + .help() + .alias('help', 'h'); + + const args = await parser.parse() as Args; + const vaaBytes = Buffer.from(args.vaa, "base64"); + const vaaType = getVaaType(vaaBytes); + if (vaaType === undefined) { - console.error(`Invalid VAA type`); + console.error(`Invalid VAA type (first byte: 0x${vaaBytes[0].toString(16).padStart(2, '0')})`); process.exit(1); } - console.log(`Verifying ${vaaType} VAA...`); - const result = await verifyVaa(client, verifierAddress, vaaHex); + + console.log(`Verifying ${vaaType} VAA on ${args.chain.toUpperCase()}...`); + + let result: VerificationResult; + + if (args.chain === "evm") { + const vaaHex = ("0x" + vaaBytes.toString("hex")) as Hex; + const client = createPublicClient({ transport: http(args.rpcUrl) }); + result = await verifyVaaEvm(client, args.verifierAddress as Address, vaaHex); + } else { + if (vaaType !== "Schnorr") { + console.error("Solana verification currently only supports Schnorr VAAs"); + process.exit(1); + } + const connection = new Connection(args.rpcUrl, "confirmed"); + const programId = new PublicKey(args.verifierAddress || DEFAULT_SOLANA_PROGRAM_ID); + result = await verifyVaaSolana(connection, programId, vaaBytes); + } + if (!result.verified) { console.error("VAA verification failed:"); console.error(result.error); process.exit(1); } - const emitterChain = toMaybeUnknownChain(result.emitterChainId); + console.log("VAA verified successfully"); console.log("================================================"); - console.log(`Emitter Chain: ${emitterChain} (${result.emitterChainId})`); - console.log("Emitter Address:", result.emitterAddress); - console.log("Sequence:", result.sequence.toString()); - console.log("Payload Offset:", result.payloadOffset); + if (args.chain === "evm") { + const emitterChain = toMaybeUnknownChain(result.emitterChainId); + console.log(`Emitter Chain: ${emitterChain} (${result.emitterChainId})`); + console.log("Emitter Address:", result.emitterAddress); + console.log("Sequence:", result.sequence.toString()); + console.log("Payload Offset:", result.payloadOffset); + } else { + console.log("(Solana verify_vaa does not return parsed VAA data)"); + } console.log("================================================"); } -await main().catch((error: unknown) => { console.error(error) }); +await main().catch((error: unknown) => { console.error(error); process.exit(1); }); From 43decdda99fb42064f2628b271e9a0e805193f50 Mon Sep 17 00:00:00 2001 From: Joshua Averett Date: Wed, 7 Jan 2026 22:44:08 -0800 Subject: [PATCH 2/6] Add ledger support, transition to ethers, add testing for governance client --- .gitignore | 2 +- src/solana/tsconfig.json | 3 +- .../governance_client.integration.test.ts | 457 ++++++++++++++++++ .../deploy/guardian/governance_client.test.ts | 378 +++++++++++++++ ts-pkgs/deploy/guardian/governance_client.ts | 433 +++++++++++++---- ts-pkgs/deploy/guardian/verification_v2.json | 452 +++++++++++++++++ ts-pkgs/deploy/package.json | 13 +- ts-pkgs/deploy/scripts/copy-idl.js | 19 + ts-pkgs/deploy/tsconfig.json | 5 +- ts-pkgs/deploy/vitest.config.ts | 13 + yarn.lock | 426 +++++++++++++++- 11 files changed, 2092 insertions(+), 109 deletions(-) create mode 100644 ts-pkgs/deploy/guardian/governance_client.integration.test.ts create mode 100644 ts-pkgs/deploy/guardian/governance_client.test.ts create mode 100644 ts-pkgs/deploy/guardian/verification_v2.json create mode 100644 ts-pkgs/deploy/scripts/copy-idl.js create mode 100644 ts-pkgs/deploy/vitest.config.ts diff --git a/.gitignore b/.gitignore index 92781f9..3093189 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ guardian_peers.json !.yarn/plugins !.yarn/releases !.yarn/sdks -!.yarn/versions \ No newline at end of file +!.yarn/versionsverification_v2.json diff --git a/src/solana/tsconfig.json b/src/solana/tsconfig.json index 68eb719..205cdd8 100644 --- a/src/solana/tsconfig.json +++ b/src/solana/tsconfig.json @@ -4,7 +4,8 @@ "outDir": "./ts-build", "resolveJsonModule": true }, - "include": ["tests", "target/types", "target/idl", "scripts"], + "include": ["target/types", "target/idl", "scripts", "tests/testing-wormhole-core.ts", "tests/testing_helpers.ts"], + "exclude": ["tests/**/*.test.ts"], "references": [ { "path": "../../ts-pkgs/tss-definitions" } ] diff --git a/ts-pkgs/deploy/guardian/governance_client.integration.test.ts b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts new file mode 100644 index 0000000..ffb9ce5 --- /dev/null +++ b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts @@ -0,0 +1,457 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ethers } from 'ethers'; +import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js'; +import { Program, AnchorProvider, Wallet } from '@coral-xyz/anchor'; +import fs from 'fs'; +import { + encodeSetShardId, + encodeAppendSchnorrKey, + encodePullMultisigKeyData, + encodeUpdate +} from './governance_client.js'; +import { parseGuardianKey } from '@xlabs-xyz/peer-lib'; + +// Mock dependencies +vi.mock('fs'); +vi.mock('@xlabs-xyz/peer-lib', () => ({ + parseGuardianKey: vi.fn(), + errorMsg: (e: unknown) => String(e), + errorStack: (e: unknown) => String(e), +})); + +describe('EVM Contract Integration', () => { + const mockPrivateKey = '0x' + '1'.repeat(64); + const mockKeyBytes = Buffer.from('1'.repeat(32), 'hex'); + const mockGuardianKeyFile = '-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY-----\n...\n-----END WORMHOLE GUARDIAN PRIVATE KEY-----'; + const testContractAddress = '0x1234567890123456789012345678901234567890'; + const testRpcUrl = 'https://test.rpc'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('Contract Interface Verification', () => { + it('should encode data compatible with update(bytes) function signature', () => { + const guardianMessage = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const encoded = encodeSetShardId(guardianMessage); + + // Verify it's a valid hex string that can be passed to bytes parameter + expect(encoded.startsWith('0x')).toBe(true); + expect(() => ethers.getBytes(encoded)).not.toThrow(); + + // Verify the data structure: opcode + message + const decoded = ethers.getBytes(encoded); + expect(decoded[0]).toBe(0); // UPDATE_SET_SHARD_ID opcode + expect(decoded.slice(1)).toEqual(new Uint8Array(guardianMessage)); + }); + + it('should encode append_schnorr with pull_multisigs correctly', () => { + const vaa = Buffer.from([0x10, 0x20, 0x30]); + const limit = 5; + + const pullData = encodePullMultisigKeyData(limit); + const appendData = encodeAppendSchnorrKey(vaa); + const combined = ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); + + // Verify structure: pull_multisigs (5 bytes) + append_schnorr (variable) + expect(combined.startsWith('0x')).toBe(true); + + const decoded = ethers.getBytes(combined); + // First 5 bytes should be pull_multisigs: opcode (1) + limit (4) + expect(decoded[0]).toBe(2); // UPDATE_PULL_MULTISIG_KEY_DATA + // Next should be append_schnorr: opcode (1) + length (2) + data + const appendStart = 5; + expect(decoded[appendStart]).toBe(1); // UPDATE_APPEND_SCHNORR_KEY + }); + + it('should create contract instance with correct ABI', () => { + const provider = new ethers.JsonRpcProvider(testRpcUrl); + const wallet = new ethers.Wallet(mockPrivateKey, provider); + + const UPDATE_ABI = [ + { + name: 'update', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'data', type: 'bytes' }], + outputs: [], + }, + ] as const; + + const contract = new ethers.Contract(testContractAddress, UPDATE_ABI, wallet); + + // Verify contract has the update function + expect(contract.update).toBeDefined(); + expect(typeof contract.update).toBe('function'); + + // Verify the function signature matches + const iface = new ethers.Interface(UPDATE_ABI); + const updateFunc = iface.getFunction('update'); + expect(updateFunc).toBeDefined(); + // Verify function signature (format includes 'function' prefix, so check the signature hash) + expect(updateFunc?.format('sighash')).toBe('update(bytes)'); + }); + + it('should encode data that matches expected contract input format', () => { + const testCases = [ + { + command: 'set_shard_id' as const, + data: Buffer.from([0x01, 0x02, 0x03]), + expectedOpcode: 0, + }, + { + command: 'pull_multisigs' as const, + data: Buffer.alloc(0), + expectedOpcode: 2, + limit: 10, + }, + ]; + + for (const testCase of testCases) { + const args: any = { + chain: 'evm' as const, + contractAddress: testContractAddress, + rpcUrl: testRpcUrl, + signer: 'test.key', + chainId: 1, + limit: testCase.limit || 0, + command: testCase.command, + ...(testCase.command === 'set_shard_id' ? { guardianMessage: 'test.msg' } : {}), + }; + + const encoded = encodeUpdate(args, testCase.data); + + // Verify it's valid bytes + const bytes = ethers.getBytes(encoded); + expect(bytes[0]).toBe(testCase.expectedOpcode); + + // Verify it can be used as contract parameter + const iface = new ethers.Interface([{ + name: 'update', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'data', type: 'bytes' }], + outputs: [], + }]); + + // Should be able to encode for contract call + const encodedCall = iface.encodeFunctionData('update', [encoded]); + expect(encodedCall).toBeTruthy(); + expect(encodedCall.startsWith('0x')).toBe(true); + } + }); + }); + + describe('Full Transaction Flow (Mocked)', () => { + it('should prepare complete transaction with correct encoding', async () => { + vi.mocked(fs.readFileSync).mockReturnValue(mockGuardianKeyFile); + vi.mocked(parseGuardianKey).mockReturnValue(mockKeyBytes); + + const provider = new ethers.JsonRpcProvider(testRpcUrl); + const wallet = new ethers.Wallet(mockPrivateKey, provider); + + const UPDATE_ABI = [ + { + name: 'update', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'data', type: 'bytes' }], + outputs: [], + }, + ] as const; + + const contract = new ethers.Contract(testContractAddress, UPDATE_ABI, wallet); + + const guardianMessage = Buffer.from([0x01, 0x02, 0x03]); + const updateData = encodeSetShardId(guardianMessage); + + // Mock the contract call + const mockTx = { + hash: '0x' + 'a'.repeat(64), + wait: vi.fn().mockResolvedValue({ + status: 1, + blockNumber: 12345, + gasUsed: ethers.parseUnits('100000', 'wei'), + }), + }; + + vi.spyOn(contract, 'update').mockResolvedValue(mockTx); + + // Execute the transaction + const tx = await contract.update(updateData); + const receipt = await tx.wait(); + + expect(contract.update).toHaveBeenCalledWith(updateData); + expect(receipt.status).toBe(1); + expect(receipt.blockNumber).toBe(12345); + }); + + it('should handle transaction errors correctly', async () => { + const provider = new ethers.JsonRpcProvider(testRpcUrl); + const wallet = new ethers.Wallet(mockPrivateKey, provider); + + const UPDATE_ABI = [ + { + name: 'update', + type: 'function', + stateMutability: 'nonpayable', + inputs: [{ name: 'data', type: 'bytes' }], + outputs: [], + }, + ] as const; + + const contract = new ethers.Contract(testContractAddress, UPDATE_ABI, wallet); + + const updateData = encodePullMultisigKeyData(5); + + // Mock transaction failure + const mockTx = { + hash: '0x' + 'b'.repeat(64), + wait: vi.fn().mockResolvedValue({ + status: 0, // Failed + blockNumber: 12346, + gasUsed: ethers.parseUnits('50000', 'wei'), + }), + }; + + vi.spyOn(contract, 'update').mockResolvedValue(mockTx); + + const tx = await contract.update(updateData); + const receipt = await tx.wait(); + + expect(receipt.status).toBe(0); + }); + }); +}); + +describe('Solana Program Integration', () => { + const testProgramId = new PublicKey('GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6'); + const testRpcUrl = 'http://localhost:8899'; + + describe('Program Interface Verification', () => { + it('should derive PDAs correctly for program', () => { + const schnorrKeyIndex = 5; + const schnorrKeyIndexBuf = Buffer.alloc(4); + schnorrKeyIndexBuf.writeUint32LE(schnorrKeyIndex); + const seeds = [Buffer.from('schnorrkey'), schnorrKeyIndexBuf]; + const [pda] = PublicKey.findProgramAddressSync(seeds, testProgramId); + + expect(pda).toBeInstanceOf(PublicKey); + expect(pda.toBase58().length).toBeGreaterThan(0); + + // Verify PDA is deterministic + const [pda2] = PublicKey.findProgramAddressSync(seeds, testProgramId); + expect(pda.toBase58()).toBe(pda2.toBase58()); + }); + + it('should derive latest key PDA correctly', () => { + const seeds = [Buffer.from('latestkey')]; + const [pda] = PublicKey.findProgramAddressSync(seeds, testProgramId); + + expect(pda).toBeInstanceOf(PublicKey); + expect(pda.toBase58().length).toBeGreaterThan(0); + }); + + it('should create connection and provider correctly', () => { + const connection = new Connection(testRpcUrl, 'confirmed'); + const keypair = Keypair.generate(); + const wallet = new Wallet(keypair); + const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' }); + + expect(provider.connection).toBe(connection); + expect(provider.wallet).toBe(wallet); + }); + }); + + describe('Instruction Building (Mocked)', () => { + it('should build appendSchnorrKey instruction with correct accounts', async () => { + const connection = new Connection(testRpcUrl, 'confirmed'); + const keypair = Keypair.generate(); + const wallet = new Wallet(keypair); + const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' }); + + // Mock IDL - we'll use a minimal structure + const mockIdl = { + version: '0.1.0', + name: 'verification_v2', + metadata: { + address: testProgramId.toBase58(), + }, + instructions: [ + { + name: 'appendSchnorrKey', + accounts: [ + { name: 'payer', isSigner: true, isWritable: true }, + { name: 'vaa', isSigner: false, isWritable: false }, + { name: 'signatureSet', isSigner: false, isWritable: false }, + { name: 'latestSchnorrKey', isSigner: false, isWritable: false }, + { name: 'newSchnorrKey', isSigner: false, isWritable: true }, + { name: 'oldSchnorrKey', isSigner: false, isWritable: false, optional: true }, + ], + args: [], + }, + ], + }; + + // Skip Program creation test - it requires full IDL structure + // Instead, test that we can build the accounts structure correctly + const accounts = { + payer: keypair.publicKey, + vaa: PublicKey.default, + signatureSet: PublicKey.default, + latestSchnorrKey: PublicKey.findProgramAddressSync( + [Buffer.from('latestkey')], + testProgramId + )[0], + newSchnorrKey: (() => { + const indexBuf = Buffer.alloc(4); + indexBuf.writeUint32LE(0, 0); + return PublicKey.findProgramAddressSync( + [Buffer.from('schnorrkey'), indexBuf], + testProgramId + )[0]; + })(), + oldSchnorrKey: null, + }; + + // Verify accounts structure is correct + expect(accounts.payer).toBeInstanceOf(PublicKey); + expect(accounts.vaa).toBeInstanceOf(PublicKey); + expect(accounts.signatureSet).toBeInstanceOf(PublicKey); + expect(accounts.latestSchnorrKey).toBeInstanceOf(PublicKey); + expect(accounts.newSchnorrKey).toBeInstanceOf(PublicKey); + + // Mock the program method call + const program = { methods: { appendSchnorrKey: vi.fn() } } as any; + + // Verify accounts structure matches what would be passed to the program + expect(accounts).toMatchObject({ + payer: expect.any(PublicKey), + vaa: expect.any(PublicKey), + signatureSet: expect.any(PublicKey), + latestSchnorrKey: expect.any(PublicKey), + newSchnorrKey: expect.any(PublicKey), + oldSchnorrKey: null, + }); + }); + + it('should build transaction with correct structure', () => { + const keypair = Keypair.generate(); + const connection = new Connection(testRpcUrl, 'confirmed'); + + const mockInstruction = { + keys: [], + programId: testProgramId, + data: Buffer.from([0x01, 0x02, 0x03]), + }; + + const tx = new Transaction().add(mockInstruction as any); + tx.feePayer = keypair.publicKey; + + expect(tx.instructions.length).toBe(1); + expect(tx.feePayer?.equals(keypair.publicKey)).toBe(true); + }); + }); + + describe('PDA Derivation Consistency', () => { + it('should derive consistent PDAs for same inputs', () => { + const testCases = [ + { index: 0, seed: 'schnorrkey' }, + { index: 1, seed: 'schnorrkey' }, + { index: 100, seed: 'schnorrkey' }, + { seed: 'latestkey' }, + ]; + + for (const testCase of testCases) { + const seeds = testCase.index !== undefined + ? [Buffer.from(testCase.seed), (() => { + const buf = Buffer.alloc(4); + buf.writeUint32LE(testCase.index, 0); + return buf; + })()] + : [Buffer.from(testCase.seed)]; + + const [pda1] = PublicKey.findProgramAddressSync(seeds, testProgramId); + const [pda2] = PublicKey.findProgramAddressSync(seeds, testProgramId); + + expect(pda1.toBase58()).toBe(pda2.toBase58()); + } + }); + + it('should derive different PDAs for different indices', () => { + const index1 = 0; + const index2 = 1; + + const buf1 = Buffer.alloc(4); + buf1.writeUint32LE(index1, 0); + const buf2 = Buffer.alloc(4); + buf2.writeUint32LE(index2, 0); + const seeds1 = [Buffer.from('schnorrkey'), buf1]; + const seeds2 = [Buffer.from('schnorrkey'), buf2]; + + const [pda1] = PublicKey.findProgramAddressSync(seeds1, testProgramId); + const [pda2] = PublicKey.findProgramAddressSync(seeds2, testProgramId); + + expect(pda1.toBase58()).not.toBe(pda2.toBase58()); + }); + }); +}); + +describe('End-to-End Encoding Verification', () => { + it('should produce encoding that matches contract expectations', () => { + // Test set_shard_id + const guardianMessage = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]); + const encoded = encodeSetShardId(guardianMessage); + + // Verify structure matches contract expectation + const bytes = ethers.getBytes(encoded); + expect(bytes.length).toBe(1 + guardianMessage.length); // opcode + data + expect(bytes[0]).toBe(0); // UPDATE_SET_SHARD_ID + + // Verify it can be decoded as bytes in contract + const iface = new ethers.Interface([{ + name: 'update', + type: 'function', + inputs: [{ name: 'data', type: 'bytes' }], + outputs: [], + }]); + + const callData = iface.encodeFunctionData('update', [encoded]); + expect(callData).toBeTruthy(); + }); + + it('should handle append_schnorr with various VAA sizes', () => { + const sizes = [0, 1, 10, 100, 1000, 5000]; + + for (const size of sizes) { + const vaa = Buffer.alloc(size, 0x42); + const encoded = encodeAppendSchnorrKey(vaa); + + const bytes = ethers.getBytes(encoded); + // Structure: opcode (1) + length (2) + data (size) + expect(bytes.length).toBe(1 + 2 + size); + expect(bytes[0]).toBe(1); // UPDATE_APPEND_SCHNORR_KEY + + // Verify length encoding (big-endian uint16) + const length = (bytes[1] << 8) | bytes[2]; + expect(length).toBe(size); + } + }); + + it('should encode pull_multisigs with various limits', () => { + const limits = [0, 1, 10, 100, 1000, 0xFFFFFFFF]; + + for (const limit of limits) { + const encoded = encodePullMultisigKeyData(limit); + const bytes = ethers.getBytes(encoded); + + // Structure: opcode (1) + limit (4) + expect(bytes.length).toBe(5); + expect(bytes[0]).toBe(2); // UPDATE_PULL_MULTISIG_KEY_DATA + + // Verify limit encoding (big-endian uint32) + const decodedLimit = Number((BigInt(bytes[1]) << 24n) | (BigInt(bytes[2]) << 16n) | (BigInt(bytes[3]) << 8n) | BigInt(bytes[4])); + expect(decodedLimit).toBe(limit); + } + }); +}); diff --git a/ts-pkgs/deploy/guardian/governance_client.test.ts b/ts-pkgs/deploy/guardian/governance_client.test.ts new file mode 100644 index 0000000..ccd309c --- /dev/null +++ b/ts-pkgs/deploy/guardian/governance_client.test.ts @@ -0,0 +1,378 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { ethers } from 'ethers'; +import fs from 'fs'; +import { + encodeSetShardId, + encodeAppendSchnorrKey, + encodePullMultisigKeyData, + encodeUpdate +} from './governance_client.js'; +import { parseGuardianKey } from '@xlabs-xyz/peer-lib'; +import { Connection, Keypair, PublicKey } from '@solana/web3.js'; + +// Mock dependencies +vi.mock('fs'); +vi.mock('@xlabs-xyz/peer-lib', () => ({ + parseGuardianKey: vi.fn(), + errorMsg: (e: unknown) => String(e), + errorStack: (e: unknown) => String(e), +})); + +describe('Encoding Functions', () => { + describe('encodeSetShardId', () => { + it('should encode set_shard_id command correctly', () => { + const guardianMessage = Buffer.from([0x01, 0x02, 0x03, 0x04]); + const result = encodeSetShardId(guardianMessage); + + // Should start with opcode 0x00 + expect(result).toBeTruthy(); + expect(result.length).toBeGreaterThan(0); + + // Verify it's a valid hex string + expect(result.startsWith('0x')).toBe(true); + + // Decode and verify structure: opcode (1 byte) + data + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(0); // UPDATE_SET_SHARD_ID + expect(decoded.slice(1)).toEqual(new Uint8Array(guardianMessage)); + }); + + it('should handle empty guardian message', () => { + const guardianMessage = Buffer.alloc(0); + const result = encodeSetShardId(guardianMessage); + + expect(result).toBeTruthy(); + expect(result.startsWith('0x')).toBe(true); + + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(0); // UPDATE_SET_SHARD_ID + expect(decoded.length).toBe(1); // Only opcode + }); + }); + + describe('encodeAppendSchnorrKey', () => { + it('should encode append_schnorr_key command correctly', () => { + const vaa = Buffer.from([0x10, 0x20, 0x30, 0x40, 0x50]); + const result = encodeAppendSchnorrKey(vaa); + + expect(result).toBeTruthy(); + expect(result.startsWith('0x')).toBe(true); + + // Decode and verify: opcode (1 byte) + length (2 bytes, big-endian) + data + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(1); // UPDATE_APPEND_SCHNORR_KEY + // Length is encoded as uint16 big-endian: 5 = 0x0005 + expect(decoded[1]).toBe(0); // Length high byte + expect(decoded[2]).toBe(5); // Length low byte + expect(decoded.slice(3)).toEqual(new Uint8Array(vaa)); + }); + + it('should handle large VAA data', () => { + const vaa = Buffer.alloc(1000, 0x42); + const result = encodeAppendSchnorrKey(vaa); + + expect(result).toBeTruthy(); + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(1); // UPDATE_APPEND_SCHNORR_KEY + // Length should be 1000 = 0x03E8 (big-endian) + expect(decoded[1]).toBe(0x03); // High byte + expect(decoded[2]).toBe(0xE8); // Low byte + }); + }); + + describe('encodePullMultisigKeyData', () => { + it('should encode pull_multisigs command correctly', () => { + const limit = 42; + const result = encodePullMultisigKeyData(limit); + + expect(result).toBeTruthy(); + expect(result.startsWith('0x')).toBe(true); + + // Decode and verify: opcode (1 byte) + limit (4 bytes, big-endian) + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(2); // UPDATE_PULL_MULTISIG_KEY_DATA + // Limit should be 42 = 0x0000002A (big-endian) + expect(decoded[1]).toBe(0x00); + expect(decoded[2]).toBe(0x00); + expect(decoded[3]).toBe(0x00); + expect(decoded[4]).toBe(0x2A); + }); + + it('should handle zero limit', () => { + const limit = 0; + const result = encodePullMultisigKeyData(limit); + + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(2); // UPDATE_PULL_MULTISIG_KEY_DATA + expect(decoded.slice(1, 5)).toEqual(new Uint8Array([0, 0, 0, 0])); + }); + + it('should handle large limit values', () => { + const limit = 0xFFFFFFFF; // Max uint32 + const result = encodePullMultisigKeyData(limit); + + const decoded = ethers.getBytes(result); + expect(decoded[0]).toBe(2); + expect(decoded.slice(1, 5)).toEqual(new Uint8Array([0xFF, 0xFF, 0xFF, 0xFF])); + }); + }); + + describe('encodeUpdate', () => { + it('should encode set_shard_id command', () => { + const args = { + chain: 'evm' as const, + contractAddress: '0x1234567890123456789012345678901234567890', + rpcUrl: 'https://test.rpc', + signer: 'test.key', + chainId: 1, + limit: 0, + command: 'set_shard_id' as const, + guardianMessage: 'test.msg', + }; + const dataBytes = Buffer.from([0x01, 0x02, 0x03]); + + const result = encodeUpdate(args, dataBytes); + + expect(result).toBeTruthy(); + expect(result.startsWith('0x')).toBe(true); + + // Should match encodeSetShardId output + const expected = encodeSetShardId(dataBytes); + expect(result).toBe(expected); + }); + + it('should encode append_schnorr command with pull_multisigs', () => { + const args = { + chain: 'evm' as const, + contractAddress: '0x1234567890123456789012345678901234567890', + rpcUrl: 'https://test.rpc', + signer: 'test.key', + chainId: 1, + limit: 10, + command: 'append_schnorr' as const, + vaa: 'test.vaa', + }; + const dataBytes = Buffer.from([0x10, 0x20, 0x30]); + + const result = encodeUpdate(args, dataBytes); + + expect(result).toBeTruthy(); + expect(result.startsWith('0x')).toBe(true); + + // Should be concatenation of pull_multisigs + append_schnorr + const pullData = encodePullMultisigKeyData(args.limit); + const appendData = encodeAppendSchnorrKey(dataBytes); + const expected = ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); + expect(result).toBe(expected); + }); + + it('should encode pull_multisigs command', () => { + const args = { + chain: 'evm' as const, + contractAddress: '0x1234567890123456789012345678901234567890', + rpcUrl: 'https://test.rpc', + signer: 'test.key', + chainId: 1, + limit: 5, + command: 'pull_multisigs' as const, + }; + const dataBytes = Buffer.alloc(0); + + const result = encodeUpdate(args, dataBytes); + + expect(result).toBeTruthy(); + + // Should match encodePullMultisigKeyData output + const expected = encodePullMultisigKeyData(args.limit); + expect(result).toBe(expected); + }); + }); +}); + +describe('File-based Signing', () => { + const mockPrivateKey = '0x' + '1'.repeat(64); // 32 bytes + const mockKeyBytes = Buffer.from('1'.repeat(32), 'hex'); + const mockGuardianKeyFile = '-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY-----\n...\n-----END WORMHOLE GUARDIAN PRIVATE KEY-----'; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('EVM Key Parsing', () => { + it('should parse guardian key file correctly', () => { + vi.mocked(fs.readFileSync).mockReturnValue(mockGuardianKeyFile); + vi.mocked(parseGuardianKey).mockReturnValue(mockKeyBytes); + + const fileContent = fs.readFileSync('test.key', 'utf-8'); + const keyBytes = parseGuardianKey(fileContent); + const signerKey = `0x${Buffer.from(keyBytes).toString('hex')}`; + + expect(fs.readFileSync).toHaveBeenCalledWith('test.key', 'utf-8'); + expect(parseGuardianKey).toHaveBeenCalledWith(mockGuardianKeyFile); + expect(signerKey).toBe(`0x${mockKeyBytes.toString('hex')}`); + }); + + it('should create valid ethers wallet from parsed key', () => { + const wallet = new ethers.Wallet(mockPrivateKey); + + expect(wallet.address).toBeTruthy(); + expect(wallet.address.length).toBe(42); // 0x + 40 hex chars + expect(wallet.address.startsWith('0x')).toBe(true); + }); + + it('should validate contract addresses', () => { + const validAddress = '0x1234567890123456789012345678901234567890'; + const invalidAddress = '0xinvalid'; + + expect(ethers.isAddress(validAddress)).toBe(true); + expect(ethers.isAddress(invalidAddress)).toBe(false); + }); + }); + + describe('EVM Transaction Execution (Mocked)', () => { + it('should create provider and wallet correctly', () => { + const rpcUrl = 'https://test.rpc'; + const provider = new ethers.JsonRpcProvider(rpcUrl); + const wallet = new ethers.Wallet(mockPrivateKey, provider); + + expect(wallet.address).toBeTruthy(); + expect(wallet.provider).toBe(provider); + }); + + it('should encode update data correctly for contract call', () => { + const args = { + chain: 'evm' as const, + contractAddress: '0x1234567890123456789012345678901234567890', + rpcUrl: 'https://test.rpc', + signer: 'test.key', + chainId: 1, + limit: 0, + command: 'set_shard_id' as const, + guardianMessage: 'test.msg', + }; + const dataBytes = Buffer.from([0x01, 0x02, 0x03]); + + const updateData = encodeUpdate(args, dataBytes); + + expect(updateData).toBeTruthy(); + expect(updateData.startsWith('0x')).toBe(true); + }); + }); + + describe('Solana PDA Derivation', () => { + it('should parse GPG armor key file and create keypair from 32-byte seed', () => { + // Generate a valid keypair for testing + const validKeypair = Keypair.generate(); + const seed = validKeypair.secretKey.slice(0, 32); // First 32 bytes are the seed + + // Create a mock GPG armor file (simplified - just the key bytes encoded) + // In reality, parseGuardianKey would parse the full GPG armor format + const mockGpgArmor = `-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY----- + +${Buffer.from(seed).toString('base64')} +-----END WORMHOLE GUARDIAN PRIVATE KEY-----`; + + vi.mocked(fs.readFileSync).mockReturnValue(mockGpgArmor); + vi.mocked(parseGuardianKey).mockReturnValue(seed); + + const signerFile = fs.readFileSync('test.key', 'utf-8'); + const keyBytes = parseGuardianKey(signerFile); + const keypair = Keypair.fromSeed(keyBytes); + + expect(fs.readFileSync).toHaveBeenCalledWith('test.key', 'utf-8'); + expect(keypair.publicKey).toBeInstanceOf(PublicKey); + expect(keypair.secretKey.length).toBe(64); + // The public key should match when derived from the same seed + expect(keypair.publicKey.toBase58()).toBe(validKeypair.publicKey.toBase58()); + }); + + it('should parse GPG armor key file and create keypair from 64-byte keypair', () => { + // Generate a valid keypair for testing + const validKeypair = Keypair.generate(); + const fullKeypair = validKeypair.secretKey; // 64 bytes + + // Create a mock GPG armor file + const mockGpgArmor = `-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY----- + +${Buffer.from(fullKeypair).toString('base64')} +-----END WORMHOLE GUARDIAN PRIVATE KEY-----`; + + vi.mocked(fs.readFileSync).mockReturnValue(mockGpgArmor); + vi.mocked(parseGuardianKey).mockReturnValue(fullKeypair); + + const signerFile = fs.readFileSync('test.key', 'utf-8'); + const keyBytes = parseGuardianKey(signerFile); + const keypair = Keypair.fromSecretKey(keyBytes); + + expect(fs.readFileSync).toHaveBeenCalledWith('test.key', 'utf-8'); + expect(keypair.publicKey).toBeInstanceOf(PublicKey); + expect(keypair.secretKey.length).toBe(64); + expect(keypair.publicKey.toBase58()).toBe(validKeypair.publicKey.toBase58()); + }); + + it('should derive PDAs correctly', () => { + const programId = new PublicKey('GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6'); + + // Test schnorr key PDA derivation + const schnorrKeyIndex = 5; + const schnorrKeyIndexBuf = Buffer.alloc(4); + schnorrKeyIndexBuf.writeUint32LE(schnorrKeyIndex); + const seeds = [Buffer.from('schnorrkey'), schnorrKeyIndexBuf]; + const [pda] = PublicKey.findProgramAddressSync(seeds, programId); + + expect(pda).toBeInstanceOf(PublicKey); + expect(pda.toBase58().length).toBeGreaterThan(0); + + // Test latest key PDA derivation + const latestSeeds = [Buffer.from('latestkey')]; + const [latestPda] = PublicKey.findProgramAddressSync(latestSeeds, programId); + + expect(latestPda).toBeInstanceOf(PublicKey); + expect(latestPda.toBase58().length).toBeGreaterThan(0); + }); + }); +}); + +describe('Integration: Encoding and Validation', () => { + it('should produce consistent encoding results', () => { + const testData = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]); + + // Encode multiple times - should be consistent + const result1 = encodeSetShardId(testData); + const result2 = encodeSetShardId(testData); + + expect(result1).toBe(result2); + }); + + it('should handle different data sizes correctly', () => { + const sizes = [0, 1, 10, 100, 1000]; + + for (const size of sizes) { + const data = Buffer.alloc(size, 0x42); + const result = encodeAppendSchnorrKey(data); + + expect(result).toBeTruthy(); + const decoded = ethers.getBytes(result); + // Length is encoded as uint16 big-endian + const length = (decoded[1] << 8) | decoded[2]; + expect(length).toBe(size); + } + }); + + it('should produce valid hex strings for all encoding functions', () => { + const testData = Buffer.from([0x01, 0x02, 0x03]); + + const results = [ + encodeSetShardId(testData), + encodeAppendSchnorrKey(testData), + encodePullMultisigKeyData(10), + ]; + + for (const result of results) { + expect(result.startsWith('0x')).toBe(true); + expect(result.length % 2).toBe(0); // Even length (hex pairs) + // Should be valid hex + expect(() => ethers.getBytes(result)).not.toThrow(); + } + }); +}); diff --git a/ts-pkgs/deploy/guardian/governance_client.ts b/ts-pkgs/deploy/guardian/governance_client.ts index 578dd03..9ad207e 100644 --- a/ts-pkgs/deploy/guardian/governance_client.ts +++ b/ts-pkgs/deploy/guardian/governance_client.ts @@ -1,21 +1,22 @@ import fs from "fs"; -import { createWalletClient, defineChain, http, isHex, encodePacked, Hex } from "viem"; -import { privateKeyToAccount } from "viem/accounts"; -import { waitForTransactionReceipt } from "viem/actions"; -import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, Transaction } from "@solana/web3.js"; +import { ethers } from "ethers"; +import type { Signer as EthersSigner } from "ethers"; +import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, Transaction, type Signer } from "@solana/web3.js"; import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor"; import yargs, { type Argv } from "yargs"; import { hideBin } from 'yargs/helpers'; import { parseGuardianKey, errorMsg, errorStack } from '@xlabs-xyz/peer-lib'; +import * as TransportNodeHid from "@ledgerhq/hw-transport-node-hid"; +import * as SolanaApp from "@ledgerhq/hw-app-solana"; import type { VerificationV2 } from "../../../src/solana/target/types/verification_v2.js"; -import { createRequire } from "module"; -const require = createRequire(import.meta.url); -const idl = require("../../../src/solana/target/idl/verification_v2.json"); +// Import IDL - copied to build output during build step +import idlJson from "./verification_v2.json" with { type: "json" }; +const idl = idlJson as VerificationV2; // Default contract address for WormholeVerifier const DEFAULT_EVM_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"; // TODO: Update with actual deployed address -const DEFAULT_SOLANA_PROGRAM_ID = "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6"; +const DEFAULT_SOLANA_PROGRAM_ID = "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6"; // TODO: Get it from the IDL file const UPDATE_SET_SHARD_ID = 0; const UPDATE_APPEND_SCHNORR_KEY = 1; @@ -42,6 +43,7 @@ type EvmArgs = BaseArgs & { chain: "evm"; chainId: number; limit: number; + ledger?: boolean; } & ({ command: "set_shard_id"; guardianMessage: string; @@ -59,42 +61,43 @@ type SolanaArgs = BaseArgs & { signatureSet: string; newKeyIndex: number; oldKeyIndex?: number; + ledger?: boolean; } type Args = EvmArgs | SolanaArgs; // TODO: Use binary-layout for these -function encodeSetShardId(guardianMessage: Buffer): Hex { +export function encodeSetShardId(guardianMessage: Buffer): string { // set_shard_id: opcode (1 byte) + guardian message data - return encodePacked( + return ethers.solidityPacked( ['uint8', 'bytes'], [UPDATE_SET_SHARD_ID, `0x${guardianMessage.toString('hex')}`] ); } -function encodeAppendSchnorrKey(vaa: Buffer): Hex { +export function encodeAppendSchnorrKey(vaa: Buffer): string { // append_schnorr_KEY: opcode (1 byte) + vaa length (2 bytes) + vaa data - return encodePacked( + return ethers.solidityPacked( ['uint8', 'uint16', 'bytes'], [UPDATE_APPEND_SCHNORR_KEY, vaa.length, `0x${vaa.toString('hex')}`] ); } -function encodePullMultisigKeyData(limit: number): `0x${string}` { +export function encodePullMultisigKeyData(limit: number): string { // PULL_MULTISIG_KEY_DATA: opcode (1 byte) + limit (4 bytes) - return encodePacked( + return ethers.solidityPacked( ['uint8', 'uint32'], [UPDATE_PULL_MULTISIG_KEY_DATA, limit] ); } -function encodeUpdate(args: EvmArgs, dataBytes: Buffer): `0x${string}` { +export function encodeUpdate(args: EvmArgs, dataBytes: Buffer): string { if (args.command === "append_schnorr") { const pullData = encodePullMultisigKeyData(args.limit); const appendData = encodeAppendSchnorrKey(dataBytes); console.log(`Prepared pull_multisigs with limit ${args.limit}`); console.log(`Prepared append_schnorr with ${dataBytes.length} bytes of data`); - return encodePacked(['bytes', 'bytes'], [pullData, appendData]); + return ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); } else if (args.command === "set_shard_id") { console.log(`Prepared set_shard_id with ${dataBytes.length} bytes of data`); return encodeSetShardId(dataBytes); @@ -120,46 +123,238 @@ function deriveLatestKeyPda(programId: PublicKey): PublicKey { return pda; } -async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise { - let signerKey: Hex; - try { - const signerFile = fs.readFileSync(args.signer, 'utf-8'); - const keyBytes = parseGuardianKey(signerFile); - signerKey = `0x${Buffer.from(keyBytes).toString('hex')}`; - } catch (error) { - console.error(`Failed to parse signer file: ${errorMsg(error)}`); - process.exit(1); +// ============================================================================ +// Signer Classes +// ============================================================================ + +// EVM file-based signer +class EvmSigner { + private wallet: ethers.Wallet; + + constructor(signerPath: string, provider: ethers.Provider) { + try { + const signerFile = fs.readFileSync(signerPath, 'utf-8'); + const keyBytes = parseGuardianKey(signerFile); + const signerKey = `0x${Buffer.from(keyBytes).toString('hex')}`; + this.wallet = new ethers.Wallet(signerKey, provider); + console.log(`Using file-based EVM signer: ${this.wallet.address}`); + } catch (error) { + console.error(`Failed to parse EVM signer file: ${errorMsg(error)}`); + throw error; + } + } + + getAddress(): string { + return this.wallet.address; + } + + getSigner(): ethers.Wallet { + return this.wallet; + } +} + +// EVM Ledger signer +class EvmLedgerSigner { + private ledgerSigner: EthersSigner; + + private constructor(ledgerSigner: EthersSigner) { + this.ledgerSigner = ledgerSigner; + } + + static async create(provider: ethers.Provider): Promise { + try { + // Dynamic import using Function constructor to prevent vite from analyzing it + const importLedger = new Function('specifier', 'return import(specifier)'); + const ledgerModule = await importLedger("@xlabs-xyz/ledger-ethers-signer"); + const LedgerSigner = ledgerModule.LedgerSigner || (ledgerModule as any).default?.LedgerSigner || (ledgerModule as any).default; + const ledgerSigner = new LedgerSigner(provider, "hid"); + const address = await ledgerSigner.getAddress(); + console.log(`Using Ledger EVM signer: ${address}`); + return new EvmLedgerSigner(ledgerSigner); + } catch (error) { + console.error(`Failed to initialize Ledger: ${errorMsg(error)}`); + console.error('Make sure your Ledger device is connected and the Ethereum app is open.'); + throw error; + } + } + + async getAddress(): Promise { + return await this.ledgerSigner.getAddress(); + } + + getSigner(): EthersSigner { + return this.ledgerSigner; + } +} + +// Solana file-based signer +class SolanaSigner implements Signer { + publicKey: PublicKey; + secretKey: Uint8Array; + private keypair: Keypair; + + constructor(signerPath: string) { + try { + const signerFile = fs.readFileSync(signerPath, 'utf-8'); + const keyBytes = parseGuardianKey(signerFile); + + // Solana Keypair.fromSecretKey expects 64 bytes (32-byte seed + 32-byte public key) + // If we have 32 bytes, we need to derive the Ed25519 keypair + // If we have 64 bytes, we can use it directly + if (keyBytes.length === 32) { + this.keypair = Keypair.fromSeed(keyBytes); + } else if (keyBytes.length === 64) { + this.keypair = Keypair.fromSecretKey(keyBytes); + } else { + throw new Error(`Invalid key length: expected 32 or 64 bytes, got ${keyBytes.length}`); + } + + this.publicKey = this.keypair.publicKey; + this.secretKey = this.keypair.secretKey; + console.log(`Using file-based Solana signer: ${this.publicKey.toBase58()}`); + } catch (error) { + console.error(`Failed to parse Solana signer file: ${errorMsg(error)}`); + throw error; + } + } + + getKeypair(): Keypair { + return this.keypair; } + + async signTransaction(tx: Transaction): Promise { + tx.partialSign(this.keypair); + return tx; + } + + async signAllTransactions(txs: Transaction[]): Promise { + return txs.map(tx => { + tx.partialSign(this.keypair); + return tx; + }); + } +} + +// Solana Ledger signer +class SolanaLedgerSigner implements Signer { + publicKey: PublicKey; + secretKey: Uint8Array; // Required by Signer interface, but not used for Ledger + private solanaApp: SolanaApp.default; + private derivationPath: number[]; + + private constructor(solanaApp: SolanaApp.default, publicKey: PublicKey, derivationPath: number[] = [44, 501, 0, 0]) { + this.solanaApp = solanaApp; + this.publicKey = publicKey; + this.derivationPath = derivationPath; + this.secretKey = new Uint8Array(64); // Dummy secret key - not used for Ledger + } + + static async create(): Promise { + try { + // Initialize Ledger transport + // Note: Ledger packages use complex ESM exports that TypeScript struggles with + // @ts-ignore - Runtime behavior is correct despite type errors + const Transport = TransportNodeHid.default || TransportNodeHid; + // @ts-ignore + const transport = await Transport.create(); + // @ts-ignore + const Solana = SolanaApp.default || SolanaApp; + // @ts-ignore + const solanaApp = new Solana(transport); + + // Get public key from Ledger (using default derivation path) + // Derivation path format: "44'/501'/0'/0'" for Solana + const derivationPath = [44, 501, 0, 0]; // Standard Solana derivation path + const derivationPathStr = derivationPath.map((n, i) => i < 2 ? `${n}'` : n.toString()).join('/'); + const { publicKey } = await solanaApp.getPublicKey(derivationPathStr); + + const ledgerPublicKey = new PublicKey(publicKey); + console.log(`Using Ledger Solana signer: ${ledgerPublicKey.toBase58()}`); + + return new SolanaLedgerSigner(solanaApp, ledgerPublicKey, derivationPath); + } catch (error) { + console.error(`Failed to initialize Ledger: ${errorMsg(error)}`); + console.error('Make sure your Ledger device is connected and the Solana app is open.'); + throw error; + } + } + + async signTransaction(tx: Transaction): Promise { + // Serialize the transaction + const message = tx.serializeMessage(); + + // Convert derivation path to string format (e.g., "44'/501'/0'/0'") + const derivationPathStr = this.derivationPath.map((n, i) => i < 2 ? `${n}'` : n.toString()).join('/'); + + // Sign with Ledger + const result = await this.solanaApp.signTransaction(derivationPathStr, message); + + // Ledger returns { signature: Buffer }, extract the signature + const signature = result.signature || result; + const sigBuffer = Buffer.isBuffer(signature) ? signature : Buffer.from(signature); + tx.addSignature(this.publicKey, sigBuffer); + + return tx; + } + + async signAllTransactions(txs: Transaction[]): Promise { + // Sign each transaction sequentially + const signedTxs: Transaction[] = []; + for (const tx of txs) { + signedTxs.push(await this.signTransaction(tx)); + } + return signedTxs; + } +} + +// ============================================================================ +// Unified Signer Factory +// ============================================================================ - const account = privateKeyToAccount(signerKey); - console.log(`Using signer address: ${account.address}`); +type SignerConfig = { + chain: 'evm' | 'solana'; + ledger: boolean; + signerPath?: string; // Required if ledger=false + rpcUrl: string; +}; + +async function createSigner(config: SignerConfig): Promise { + if (config.chain === 'evm') { + const provider = new ethers.JsonRpcProvider(config.rpcUrl); + if (config.ledger) { + return await EvmLedgerSigner.create(provider); + } else { + if (!config.signerPath) { + throw new Error('signerPath is required for file-based signing'); + } + return new EvmSigner(config.signerPath, provider); + } + } else { + // Solana + if (config.ledger) { + return await SolanaLedgerSigner.create(); + } else { + if (!config.signerPath) { + throw new Error('signerPath is required for file-based signing'); + } + return new SolanaSigner(config.signerPath); + } + } +} +async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise { // Validate contract address - if (!isHex(args.contractAddress)) { + if (!ethers.isAddress(args.contractAddress)) { console.error("Contract address must be a valid hex address"); process.exit(1); } - // Setup chain and wallet client - const viemChain = defineChain({ - id: args.chainId, - name: `Chain ${args.chainId}`, - nativeCurrency: { - decimals: 18, - name: 'ETH', - symbol: 'ETH', - }, - rpcUrls: { - default: { - http: [args.rpcUrl], - }, - }, - }); - - const walletClient = createWalletClient({ - chain: viemChain, - transport: http(args.rpcUrl), - account, + // Create signer using unified factory + const signer = await createSigner({ + chain: 'evm', + ledger: args.ledger || false, + signerPath: args.signer, + rpcUrl: args.rpcUrl, }); const updateData = encodeUpdate(args, dataBytes); @@ -170,23 +365,19 @@ async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise< try { console.log('\nSending transaction...'); - const txHash = await walletClient.writeContract({ - address: args.contractAddress, - abi: UPDATE_ABI, - functionName: 'update', - args: [updateData], - }); + // Get the underlying ethers signer - both EvmSigner and EvmLedgerSigner have getSigner() + const ethersSigner = (signer as EvmSigner | EvmLedgerSigner).getSigner(); + const contract = new ethers.Contract(args.contractAddress, UPDATE_ABI, ethersSigner); + const tx = await contract.update(updateData); - console.log(`Transaction sent: ${txHash}`); + console.log(`Transaction sent: ${tx.hash}`); console.log('Waiting for confirmation...'); - const receipt = await waitForTransactionReceipt(walletClient, { - hash: txHash, - }); + const receipt = await tx.wait(); - if (receipt.status === 'success') { + if (receipt.status === 1) { console.log(`Transaction confirmed in block ${receipt.blockNumber}`); - console.log(`Gas used: ${receipt.gasUsed}`); + console.log(`Gas used: ${receipt.gasUsed.toString()}`); } else { console.error('Transaction failed'); process.exit(1); @@ -198,24 +389,31 @@ async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise< } async function executeSolanaTransaction(args: SolanaArgs): Promise { - // Load keypair from JSON file - let keypair: Keypair; - try { - const keypairData = JSON.parse(fs.readFileSync(args.signer, 'utf-8')); - keypair = Keypair.fromSecretKey(new Uint8Array(keypairData)); - } catch (error) { - console.error(`Failed to load Solana keypair from ${args.signer}: ${errorMsg(error)}`); - process.exit(1); - } - - console.log(`Using signer: ${keypair.publicKey.toBase58()}`); + // Create signer using unified factory + const signer = await createSigner({ + chain: 'solana', + ledger: args.ledger || false, + signerPath: args.signer, + rpcUrl: args.rpcUrl, + }); + // Type assertion since we know this is a Solana signer + const solanaSigner = signer as SolanaSigner | SolanaLedgerSigner; + const signerPublicKey = solanaSigner.publicKey; const connection = new Connection(args.rpcUrl, "confirmed"); const programId = new PublicKey(args.contractAddress || DEFAULT_SOLANA_PROGRAM_ID); - const wallet = new Wallet(keypair); + // Create wallet/provider - all signer classes implement the Signer interface + const wallet = solanaSigner instanceof SolanaSigner + ? new Wallet(solanaSigner.getKeypair()) + : { + publicKey: solanaSigner.publicKey, + signTransaction: (tx: Transaction) => solanaSigner.signTransaction(tx), + signAllTransactions: (txs: Transaction[]) => solanaSigner.signAllTransactions(txs), + } as Wallet; + const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); - const program = new Program(idl as VerificationV2, provider); + const program = new Program(idl, provider); const postedVaa = new PublicKey(args.postedVaa); const signatureSet = new PublicKey(args.signatureSet); @@ -239,7 +437,7 @@ async function executeSolanaTransaction(args: SolanaArgs): Promise { console.log('\nBuilding transaction...'); const ix = await program.methods.appendSchnorrKey() .accountsPartial({ - payer: keypair.publicKey, + payer: signerPublicKey, vaa: postedVaa, signatureSet: signatureSet, latestSchnorrKey: latestKeyPda, @@ -248,14 +446,27 @@ async function executeSolanaTransaction(args: SolanaArgs): Promise { }) .instruction(); + // Get recent blockhash and set fee payer before signing + const { blockhash } = await connection.getLatestBlockhash(); const tx = new Transaction().add(ix); + tx.feePayer = signerPublicKey; + tx.recentBlockhash = blockhash; console.log('Sending transaction...'); - const signature = await sendAndConfirmTransaction(connection, tx, [keypair], { - commitment: "confirmed", - }); - - console.log(`Transaction confirmed: ${signature}`); + + // Sign and send transaction based on signer type + if (solanaSigner instanceof SolanaSigner) { + const signature = await sendAndConfirmTransaction(connection, tx, [solanaSigner.getKeypair()], { + commitment: "confirmed", + }); + console.log(`Transaction confirmed: ${signature}`); + } else { + // For Ledger, we need to sign the transaction first + const signedTx = await solanaSigner.signTransaction(tx); + const signature = await connection.sendRawTransaction(signedTx.serialize()); + await connection.confirmTransaction(signature, "confirmed"); + console.log(`Transaction confirmed: ${signature}`); + } } catch (error) { console.error(`Transaction failed: ${errorStack(error)}`); process.exit(1); @@ -302,13 +513,17 @@ async function main() { default: 'evm' as const, alias: 't', }) - //TODO: add support for ledger signer - .option('signer', { - description: 'Path to signer key file (GPG armor guardian key for EVM, JSON keypair for Solana)', - demandOption: true, - type: 'string', - alias: 's', - }) + .option('ledger', { + description: 'Use Ledger hardware wallet for signing', + type: 'boolean', + default: false, + }) + .option('signer', { + description: 'Path to signer key file (GPG armor guardian key for both EVM and Solana). Optional when --ledger is used.', + demandOption: false, + type: 'string', + alias: 's', + }) .option('contract-address', { description: 'Address of the WormholeVerifier contract/program', type: 'string', @@ -348,11 +563,22 @@ async function main() { process.exit(1); } - if (!parsedArgs.postedVaa || !parsedArgs.signatureSet || parsedArgs.newKeyIndex === undefined) { + const postedVaa = parsedArgs.postedVaa as string | undefined; + const signatureSet = parsedArgs.signatureSet as string | undefined; + const newKeyIndex = parsedArgs.newKeyIndex as number | undefined; + const oldKeyIndex = parsedArgs.oldKeyIndex as number | undefined; + + if (!postedVaa || !signatureSet || newKeyIndex === undefined) { console.error("Solana append_schnorr requires --posted-vaa, --signature-set, and --new-key-index"); process.exit(1); } + // Validate signer requirement + if (!parsedArgs.ledger && !parsedArgs.signer) { + console.error("Either --signer or --ledger must be provided"); + process.exit(1); + } + const args: SolanaArgs = { chain: "solana", command: "append_schnorr", @@ -362,11 +588,12 @@ async function main() { rpcUrl: parsedArgs.rpcUrl === 'https://eth.llamarpc.com' ? 'https://api.mainnet-beta.solana.com' : parsedArgs.rpcUrl, - signer: parsedArgs.signer, - postedVaa: parsedArgs.postedVaa, - signatureSet: parsedArgs.signatureSet, - newKeyIndex: parsedArgs.newKeyIndex, - oldKeyIndex: parsedArgs.oldKeyIndex, + signer: parsedArgs.signer || '', // Required by type but not used when ledger is true + ledger: parsedArgs.ledger || false, + postedVaa: postedVaa!, // Safe after check above + signatureSet: signatureSet!, // Safe after check above + newKeyIndex: newKeyIndex!, // Safe after check above + oldKeyIndex, }; await executeSolanaTransaction(args); @@ -395,13 +622,20 @@ async function main() { console.log(`Loaded ${dataBytes.length} bytes of VAA`); } + // Validate signer requirement + if (!parsedArgs.ledger && !parsedArgs.signer) { + console.error("Either --signer or --ledger must be provided"); + process.exit(1); + } + const args: EvmArgs = { chain: "evm", command: command as EvmArgs["command"], contractAddress: parsedArgs.contractAddress, rpcUrl: parsedArgs.rpcUrl, chainId: parsedArgs.chainId, - signer: parsedArgs.signer, + signer: parsedArgs.signer || '', // Required by type but not used when ledger is true + ledger: parsedArgs.ledger || false, limit: parsedArgs.limit, ...(command === "set_shard_id" ? { guardianMessage: parsedArgs.guardianMessage as string } : {}), ...(command === "append_schnorr" ? { vaa: parsedArgs.vaa as string } : {}), @@ -411,7 +645,12 @@ async function main() { } } -main().catch((error: unknown) => { - console.error(`[ERROR] Unhandled error: ${errorStack(error)}`); - process.exit(1); -}); +// Only run main if this file is executed directly (not imported for tests) +// Check if we're running as a script (not being imported) +if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/')) || + process.argv[1]?.includes('governance_client')) { + main().catch((error: unknown) => { + console.error(`[ERROR] Unhandled error: ${errorStack(error)}`); + process.exit(1); + }); +} diff --git a/ts-pkgs/deploy/guardian/verification_v2.json b/ts-pkgs/deploy/guardian/verification_v2.json new file mode 100644 index 0000000..6c31a5e --- /dev/null +++ b/ts-pkgs/deploy/guardian/verification_v2.json @@ -0,0 +1,452 @@ +{ + "address": "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6", + "metadata": { + "name": "verification_v2", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Wormhole threshold signature verification program" + }, + "instructions": [ + { + "name": "append_ecdsa_key", + "discriminator": [ + 74, + 74, + 208, + 15, + 133, + 104, + 213, + 132 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vaa" + }, + { + "name": "signature_set" + }, + { + "name": "latest_ecdsa_key", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 97, + 116, + 101, + 115, + 116, + 101, + 99, + 100, + 115, + 97, + 107, + 101, + 121 + ] + } + ] + } + }, + { + "name": "new_ecdsa_key", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 101, + 99, + 100, + 115, + 97, + 107, + 101, + 121 + ] + }, + { + "kind": "account", + "path": "vaa" + } + ] + } + }, + { + "name": "old_ecdsa_key", + "writable": true, + "optional": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "append_schnorr_key", + "discriminator": [ + 8, + 6, + 50, + 98, + 26, + 48, + 99, + 30 + ], + "accounts": [ + { + "name": "payer", + "writable": true, + "signer": true + }, + { + "name": "vaa" + }, + { + "name": "signature_set" + }, + { + "name": "latest_schnorr_key", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 108, + 97, + 116, + 101, + 115, + 116, + 115, + 99, + 104, + 110, + 111, + 114, + 114, + 107, + 101, + 121 + ] + } + ] + } + }, + { + "name": "new_schnorr_key", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 115, + 99, + 104, + 110, + 111, + 114, + 114, + 107, + 101, + 121 + ] + }, + { + "kind": "account", + "path": "vaa" + } + ] + } + }, + { + "name": "old_schnorr_key", + "writable": true, + "optional": true + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "verify_vaa", + "discriminator": [ + 147, + 254, + 88, + 41, + 24, + 223, + 219, + 29 + ], + "accounts": [ + { + "name": "key_account" + } + ], + "args": [ + { + "name": "raw_vaa", + "type": "bytes" + } + ] + }, + { + "name": "verify_vaa_and_decode", + "discriminator": [ + 234, + 128, + 204, + 252, + 150, + 171, + 153, + 75 + ], + "accounts": [ + { + "name": "key_account" + } + ], + "args": [ + { + "name": "raw_vaa", + "type": "bytes" + } + ], + "returns": "bytes" + }, + { + "name": "verify_vaa_header_with_digest", + "discriminator": [ + 228, + 60, + 144, + 171, + 140, + 217, + 77, + 189 + ], + "accounts": [ + { + "name": "key_account" + } + ], + "args": [ + { + "name": "raw_vaa_header", + "type": "bytes" + }, + { + "name": "digest", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + ], + "accounts": [ + { + "name": "ECDSAKeyAccount", + "discriminator": [ + 224, + 208, + 218, + 94, + 194, + 145, + 77, + 151 + ] + }, + { + "name": "LatestECDSAKeyAccount", + "discriminator": [ + 73, + 118, + 207, + 182, + 110, + 212, + 135, + 226 + ] + }, + { + "name": "LatestSchnorrKeyAccount", + "discriminator": [ + 198, + 126, + 27, + 188, + 86, + 108, + 157, + 207 + ] + }, + { + "name": "SchnorrKeyAccount", + "discriminator": [ + 239, + 35, + 12, + 8, + 168, + 74, + 77, + 153 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidSignature", + "msg": "Signature does not satisfy preconditions" + }, + { + "code": 6001, + "name": "SignatureVerificationFailed", + "msg": "Signature verification failed" + }, + { + "code": 6002, + "name": "RecoveryFailed", + "msg": "Public key recovery failed" + } + ], + "types": [ + { + "name": "ECDSAKey", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": { + "array": [ + "u8", + 20 + ] + } + } + ] + } + }, + { + "name": "ECDSAKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u32" + }, + { + "name": "ecdsa_key", + "type": { + "defined": { + "name": "ECDSAKey" + } + } + }, + { + "name": "expiration_timestamp", + "type": "u64" + } + ] + } + }, + { + "name": "LatestECDSAKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "account", + "type": "pubkey" + } + ] + } + }, + { + "name": "LatestSchnorrKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "account", + "type": "pubkey" + } + ] + } + }, + { + "name": "SchnorrKey", + "type": { + "kind": "struct", + "fields": [ + { + "name": "key", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "SchnorrKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u32" + }, + { + "name": "schnorr_key", + "type": { + "defined": { + "name": "SchnorrKey" + } + } + }, + { + "name": "expiration_timestamp", + "type": "u64" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/ts-pkgs/deploy/package.json b/ts-pkgs/deploy/package.json index c427f2f..b033cf7 100644 --- a/ts-pkgs/deploy/package.json +++ b/ts-pkgs/deploy/package.json @@ -4,16 +4,24 @@ "version": "1.0.0", "type": "module", "scripts": { - "build": "tsc --build" + "build": "tsc --build", + "build:solana": "cd ../../src/solana && anchor build", + "copy:idl": "node scripts/copy-idl.js", + "prebuild": "yarn build:solana && yarn copy:idl", + "test": "vitest --run" }, "author": "", "license": "Apache-2.0", "description": "", "dependencies": { "@coral-xyz/anchor": "^0.31.1", + "@ledgerhq/hw-app-solana": "^7.0.0", + "@ledgerhq/hw-transport-node-hid": "^6.28.0", "@solana/web3.js": "^1.98.4", "@wormhole-foundation/sdk": "^4.5.0", + "@xlabs-xyz/ledger-ethers-signer": "github:XLabs/ledger-ethers-signer", "@xlabs-xyz/peer-lib": "workspace:*", + "ethers": "^6.16.0", "viem": "^2.41.2", "yargs": "^18.0.0" }, @@ -21,6 +29,7 @@ "@types/node": "^24.10.3", "@types/yargs": "^17.0.35", "tsx": "^4.21.0", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.0.15" } } diff --git a/ts-pkgs/deploy/scripts/copy-idl.js b/ts-pkgs/deploy/scripts/copy-idl.js new file mode 100644 index 0000000..71de7cf --- /dev/null +++ b/ts-pkgs/deploy/scripts/copy-idl.js @@ -0,0 +1,19 @@ +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const src = path.join(__dirname, '../../../src/solana/target/idl/verification_v2.json'); +const dest = path.join(__dirname, '../guardian/verification_v2.json'); + +if (fs.existsSync(src)) { + // Ensure destination directory exists + fs.mkdirSync(path.dirname(dest), { recursive: true }); + // Copy the file + fs.copyFileSync(src, dest); + console.log(`Copied IDL from ${src} to ${dest}`); +} else { + console.warn(`Warning: IDL file not found at ${src}. Solana build may be required.`); +} diff --git a/ts-pkgs/deploy/tsconfig.json b/ts-pkgs/deploy/tsconfig.json index 7fefae0..85faed5 100644 --- a/ts-pkgs/deploy/tsconfig.json +++ b/ts-pkgs/deploy/tsconfig.json @@ -2,9 +2,10 @@ "extends": "../config/tsconfig.base.json", "compilerOptions": { "outDir": "./ts-build", - "resolveJsonModule": true + "resolveJsonModule": true, + "allowImportingTsExtensions": false }, - "include": ["evm", "guardian", "vaatool"], + "include": ["evm", "guardian", "vaatool", "**/*.test.ts", "guardian/verification_v2.json"], "references": [ { "path": "../peer-lib" }, { "path": "../../src/solana" } diff --git a/ts-pkgs/deploy/vitest.config.ts b/ts-pkgs/deploy/vitest.config.ts new file mode 100644 index 0000000..eebef2c --- /dev/null +++ b/ts-pkgs/deploy/vitest.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + testTimeout: 10000, + }, + ssr: { + noExternal: [], + external: ['@xlabs-xyz/ledger-ethers-signer'], + }, +}); diff --git a/yarn.lock b/yarn.lock index 3a76943..49368e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1267,6 +1267,84 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/devices@npm:8.8.0": + version: 8.8.0 + resolution: "@ledgerhq/devices@npm:8.8.0" + dependencies: + "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/logs": "npm:^6.13.0" + rxjs: "npm:7.8.2" + semver: "npm:^7.3.5" + checksum: 10c0/41169c8b040e98adf7699cf0141e1a5c1004dd58b076938b69aa10662ff7618d37f16ea4c26ed76e12bdf5e085527d06e043d4ebd67e50b19f0f06d92383685c + languageName: node + linkType: hard + +"@ledgerhq/errors@npm:^6.28.0": + version: 6.28.0 + resolution: "@ledgerhq/errors@npm:6.28.0" + checksum: 10c0/4e68d0b472c7e56b0fb8c5455629a8a39a67258823121fce87ecd0a9e0ff6e6e6e60c08fde826420a815939aa927d8bbfd62f5bf109478f4bbe6a0d0b11e1980 + languageName: node + linkType: hard + +"@ledgerhq/hw-app-solana@npm:^7.0.0": + version: 7.6.2 + resolution: "@ledgerhq/hw-app-solana@npm:7.6.2" + dependencies: + "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/hw-transport": "npm:6.31.15" + bip32-path: "npm:^0.4.2" + checksum: 10c0/588cbf5bed4d230b032739718902c9611b86f06b2a9f4780d8277e7d0fdb55d48cd68fef4c4dd9c1ab676252801d238f755e1f95c2d86fc3f5d3a544b2284c19 + languageName: node + linkType: hard + +"@ledgerhq/hw-transport-node-hid-noevents@npm:^6.30.16": + version: 6.30.16 + resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.30.16" + dependencies: + "@ledgerhq/devices": "npm:8.8.0" + "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/hw-transport": "npm:6.31.15" + "@ledgerhq/logs": "npm:^6.13.0" + node-hid: "npm:2.1.2" + checksum: 10c0/29579ef964996a2a0f7649706476d1053758ed503164816a59ebda21f0ce34ce49a3d1668c829d05317fe3b05cc5181f4a210fb175c25d86e0753097b2fa6644 + languageName: node + linkType: hard + +"@ledgerhq/hw-transport-node-hid@npm:^6.28.0": + version: 6.29.16 + resolution: "@ledgerhq/hw-transport-node-hid@npm:6.29.16" + dependencies: + "@ledgerhq/devices": "npm:8.8.0" + "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/hw-transport": "npm:6.31.15" + "@ledgerhq/hw-transport-node-hid-noevents": "npm:^6.30.16" + "@ledgerhq/logs": "npm:^6.13.0" + lodash: "npm:^4.17.21" + node-hid: "npm:2.1.2" + usb: "npm:2.9.0" + checksum: 10c0/239d4dfd17e81083643e58605fd890611e1c73f1e3ce4f6c74dc8bb0715644109995a7da7661c9a03e739cf1bdbf368952b80b712748ac6b6fcf6f0c284a3a0d + languageName: node + linkType: hard + +"@ledgerhq/hw-transport@npm:6.31.15": + version: 6.31.15 + resolution: "@ledgerhq/hw-transport@npm:6.31.15" + dependencies: + "@ledgerhq/devices": "npm:8.8.0" + "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/logs": "npm:^6.13.0" + events: "npm:^3.3.0" + checksum: 10c0/8911fd5cc7e2b8453d57d7f30321cb3284c1df48b4fa6540df5a617a52199b61e586ddd238a50d757e58bc695a311fe880b3060579b435fd362d9ef5d6583369 + languageName: node + linkType: hard + +"@ledgerhq/logs@npm:^6.13.0": + version: 6.13.0 + resolution: "@ledgerhq/logs@npm:6.13.0" + checksum: 10c0/8b40a7af64a9526b441394e84f05afc42552b4d3d1e45c33dd262d669c77e2351e06bd354929ffdf6cfe0e0404b0042b8b5da610016232a0eb516f1daa73f39f + languageName: node + linkType: hard + "@mysten/bcs@npm:1.9.2": version: 1.9.2 resolution: "@mysten/bcs@npm:1.9.2" @@ -2083,6 +2161,13 @@ __metadata: languageName: node linkType: hard +"@types/w3c-web-usb@npm:^1.0.6": + version: 1.0.13 + resolution: "@types/w3c-web-usb@npm:1.0.13" + checksum: 10c0/f9d59a3cea5149639bbdc28caf01d6234a9f982307cd7fa4635ad86c3a4a476184103f9ac77ed8ff39d188dac37f4d1028d0b65bcfa4cad5d35a5efa2c34168a + languageName: node + linkType: hard + "@types/ws@npm:^7.4.4": version: 7.4.7 resolution: "@types/ws@npm:7.4.7" @@ -2769,6 +2854,13 @@ __metadata: languageName: node linkType: hard +"@xlabs-xyz/ledger-ethers-signer@github:XLabs/ledger-ethers-signer": + version: 0.0.0 + resolution: "@xlabs-xyz/ledger-ethers-signer@https://github.com/XLabs/ledger-ethers-signer.git#commit=4d11de2ab8dd87064a5290555cdd93f32612210f" + checksum: 10c0/332a28195faf9f2c7a79149a0a9b1d576cbdc5ddbed9e13ac4c6e9906f036dd075f05cbad57249483d44efe91128bbf908e6952df478ee0606e47818fdfcef8e + languageName: node + linkType: hard + "@xlabs-xyz/peer-client@workspace:*, @xlabs-xyz/peer-client@workspace:ts-pkgs/peer-client": version: 0.0.0-use.local resolution: "@xlabs-xyz/peer-client@workspace:ts-pkgs/peer-client" @@ -3105,7 +3197,7 @@ __metadata: languageName: node linkType: hard -"bindings@npm:^1.3.0": +"bindings@npm:^1.3.0, bindings@npm:^1.5.0": version: 1.5.0 resolution: "bindings@npm:1.5.0" dependencies: @@ -3114,6 +3206,13 @@ __metadata: languageName: node linkType: hard +"bip32-path@npm:^0.4.2": + version: 0.4.2 + resolution: "bip32-path@npm:0.4.2" + checksum: 10c0/7d4123a5393fc5b70a20d0997c2b5a77feb789b49bdc3485ff7db02f916acf0f8c5eccf659f3d5dff5e0b34f22f5681babba86eb9ad7fa1287daf31d69982d75 + languageName: node + linkType: hard + "bip39@npm:^3.1.0": version: 3.1.0 resolution: "bip39@npm:3.1.0" @@ -3123,6 +3222,17 @@ __metadata: languageName: node linkType: hard +"bl@npm:^4.0.3": + version: 4.1.0 + resolution: "bl@npm:4.1.0" + dependencies: + buffer: "npm:^5.5.0" + inherits: "npm:^2.0.4" + readable-stream: "npm:^3.4.0" + checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f + languageName: node + linkType: hard + "bn.js@npm:^4.11.9": version: 4.12.2 resolution: "bn.js@npm:4.12.2" @@ -3231,6 +3341,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^5.5.0": + version: 5.7.1 + resolution: "buffer@npm:5.7.1" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.1.13" + checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e + languageName: node + linkType: hard + "bufferutil@npm:^4.0.1": version: 4.0.9 resolution: "bufferutil@npm:4.0.9" @@ -3344,6 +3464,13 @@ __metadata: languageName: node linkType: hard +"chownr@npm:^1.1.1": + version: 1.1.4 + resolution: "chownr@npm:1.1.4" + checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db + languageName: node + linkType: hard + "chownr@npm:^3.0.0": version: 3.0.0 resolution: "chownr@npm:3.0.0" @@ -3538,6 +3665,22 @@ __metadata: languageName: node linkType: hard +"decompress-response@npm:^6.0.0": + version: 6.0.0 + resolution: "decompress-response@npm:6.0.0" + dependencies: + mimic-response: "npm:^3.1.0" + checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e + languageName: node + linkType: hard + +"deep-extend@npm:^0.6.0": + version: 0.6.0 + resolution: "deep-extend@npm:0.6.0" + checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566 + languageName: node + linkType: hard + "deep-is@npm:^0.1.3": version: 0.1.4 resolution: "deep-is@npm:0.1.4" @@ -3592,17 +3735,31 @@ __metadata: version: 0.0.0-use.local resolution: "deploy@workspace:ts-pkgs/deploy" dependencies: + "@coral-xyz/anchor": "npm:^0.31.1" + "@ledgerhq/hw-app-solana": "npm:^7.0.0" + "@ledgerhq/hw-transport-node-hid": "npm:^6.28.0" + "@solana/web3.js": "npm:^1.98.4" "@types/node": "npm:^24.10.3" "@types/yargs": "npm:^17.0.35" "@wormhole-foundation/sdk": "npm:^4.5.0" + "@xlabs-xyz/ledger-ethers-signer": "github:XLabs/ledger-ethers-signer" "@xlabs-xyz/peer-lib": "workspace:*" + ethers: "npm:^6.16.0" tsx: "npm:^4.21.0" typescript: "npm:^5.9.3" viem: "npm:^2.41.2" + vitest: "npm:^4.0.15" yargs: "npm:^18.0.0" languageName: unknown linkType: soft +"detect-libc@npm:^2.0.0": + version: 2.1.2 + resolution: "detect-libc@npm:2.1.2" + checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 + languageName: node + linkType: hard + "dezalgo@npm:^1.0.4": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" @@ -3707,6 +3864,15 @@ __metadata: languageName: node linkType: hard +"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1": + version: 1.4.5 + resolution: "end-of-stream@npm:1.4.5" + dependencies: + once: "npm:^1.4.0" + checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8 + languageName: node + linkType: hard + "env-paths@npm:^2.2.0": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -4139,6 +4305,20 @@ __metadata: languageName: node linkType: hard +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + +"expand-template@npm:^2.0.3": + version: 2.0.3 + resolution: "expand-template@npm:2.0.3" + checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51 + languageName: node + linkType: hard + "expect-type@npm:^1.2.2": version: 1.2.2 resolution: "expect-type@npm:1.2.2" @@ -4367,6 +4547,13 @@ __metadata: languageName: node linkType: hard +"fs-constants@npm:^1.0.0": + version: 1.0.0 + resolution: "fs-constants@npm:1.0.0" + checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8 + languageName: node + linkType: hard + "fs-minipass@npm:^3.0.0": version: 3.0.3 resolution: "fs-minipass@npm:3.0.3" @@ -4470,6 +4657,13 @@ __metadata: languageName: node linkType: hard +"github-from-package@npm:0.0.0": + version: 0.0.0 + resolution: "github-from-package@npm:0.0.0" + checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12 + languageName: node + linkType: hard + "glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" @@ -4754,7 +4948,7 @@ __metadata: languageName: node linkType: hard -"ieee754@npm:^1.2.1": +"ieee754@npm:^1.1.13, ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb @@ -4809,6 +5003,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:~1.3.0": + version: 1.3.8 + resolution: "ini@npm:1.3.8" + checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a + languageName: node + linkType: hard + "interpret@npm:^1.0.0": version: 1.4.0 resolution: "interpret@npm:1.4.0" @@ -5105,6 +5306,13 @@ __metadata: languageName: node linkType: hard +"lodash@npm:^4.17.21": + version: 4.17.21 + resolution: "lodash@npm:4.17.21" + checksum: 10c0/d8cbea072bb08655bb4c989da418994b073a608dffa608b09ac04b43a791b12aeae7cd7ad919aa4c925f33b48490b5cfe6c1f71d827956071dae2e7bb3a6b74c + languageName: node + linkType: hard + "log-symbols@npm:^4.1.0": version: 4.1.0 resolution: "log-symbols@npm:4.1.0" @@ -5267,6 +5475,13 @@ __metadata: languageName: node linkType: hard +"mimic-response@npm:^3.1.0": + version: 3.1.0 + resolution: "mimic-response@npm:3.1.0" + checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362 + languageName: node + linkType: hard + "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -5308,7 +5523,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.3": +"minimist@npm:^1.2.0, minimist@npm:^1.2.3": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -5391,6 +5606,13 @@ __metadata: languageName: node linkType: hard +"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3": + version: 0.5.3 + resolution: "mkdirp-classic@npm:0.5.3" + checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168 + languageName: node + linkType: hard + "mocha@npm:^11.7.5": version: 11.7.5 resolution: "mocha@npm:11.7.5" @@ -5439,6 +5661,13 @@ __metadata: languageName: node linkType: hard +"napi-build-utils@npm:^2.0.0": + version: 2.0.0 + resolution: "napi-build-utils@npm:2.0.0" + checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -5463,6 +5692,24 @@ __metadata: languageName: node linkType: hard +"node-abi@npm:^3.3.0": + version: 3.85.0 + resolution: "node-abi@npm:3.85.0" + dependencies: + semver: "npm:^7.3.5" + checksum: 10c0/d51b5718b6ebfcb23858e5429b74798c05fe3ab436d8afd8480b4809706bc53d6af3a60714ecc85e8c943f4e06e6378ca1935725c7611f3d1febdd3fc3bb5fe3 + languageName: node + linkType: hard + +"node-addon-api@npm:^3.0.2": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/41f21c9d12318875a2c429befd06070ce367065a3ef02952cfd4ea17ef69fa14012732f510b82b226e99c254da8d671847ea018cad785f839a5366e02dd56302 + languageName: node + linkType: hard + "node-addon-api@npm:^5.0.0": version: 5.1.0 resolution: "node-addon-api@npm:5.1.0" @@ -5472,6 +5719,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^6.0.0": + version: 6.1.0 + resolution: "node-addon-api@npm:6.1.0" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/d2699c4ad15740fd31482a3b6fca789af7723ab9d393adc6ac45250faaee72edad8f0b10b2b9d087df0de93f1bdc16d97afdd179b26b9ebc9ed68b569faa4bac + languageName: node + linkType: hard + "node-fetch@npm:^2.7.0": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" @@ -5486,7 +5742,7 @@ __metadata: languageName: node linkType: hard -"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0": +"node-gyp-build@npm:^4.2.0, node-gyp-build@npm:^4.3.0, node-gyp-build@npm:^4.5.0": version: 4.8.4 resolution: "node-gyp-build@npm:4.8.4" bin: @@ -5517,6 +5773,20 @@ __metadata: languageName: node linkType: hard +"node-hid@npm:2.1.2": + version: 2.1.2 + resolution: "node-hid@npm:2.1.2" + dependencies: + bindings: "npm:^1.5.0" + node-addon-api: "npm:^3.0.2" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + bin: + hid-showdevices: src/show-devices.js + checksum: 10c0/7f1a6b91e0a98e1bd90abf0e27cdbaaf61408f046cb92e5b05c6bae295d1d3c5ee8ed3f9a9d98265c627eeea5344291f883ca27a7c64c1ca89a39c057c30d041 + languageName: node + linkType: hard + "nopt@npm:^9.0.0": version: 9.0.0 resolution: "nopt@npm:9.0.0" @@ -5565,7 +5835,7 @@ __metadata: languageName: node linkType: hard -"once@npm:^1.3.0, once@npm:^1.4.0": +"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0": version: 1.4.0 resolution: "once@npm:1.4.0" dependencies: @@ -5770,6 +6040,28 @@ __metadata: languageName: node linkType: hard +"prebuild-install@npm:^7.1.1": + version: 7.1.3 + resolution: "prebuild-install@npm:7.1.3" + dependencies: + detect-libc: "npm:^2.0.0" + expand-template: "npm:^2.0.3" + github-from-package: "npm:0.0.0" + minimist: "npm:^1.2.3" + mkdirp-classic: "npm:^0.5.3" + napi-build-utils: "npm:^2.0.0" + node-abi: "npm:^3.3.0" + pump: "npm:^3.0.0" + rc: "npm:^1.2.7" + simple-get: "npm:^4.0.0" + tar-fs: "npm:^2.0.0" + tunnel-agent: "npm:^0.6.0" + bin: + prebuild-install: bin.js + checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -5866,6 +6158,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^3.0.0": + version: 3.0.3 + resolution: "pump@npm:3.0.3" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9 + languageName: node + linkType: hard + "punycode@npm:^2.1.0": version: 2.3.1 resolution: "punycode@npm:2.3.1" @@ -5910,6 +6212,20 @@ __metadata: languageName: node linkType: hard +"rc@npm:^1.2.7": + version: 1.2.8 + resolution: "rc@npm:1.2.8" + dependencies: + deep-extend: "npm:^0.6.0" + ini: "npm:~1.3.0" + minimist: "npm:^1.2.0" + strip-json-comments: "npm:~2.0.1" + bin: + rc: ./cli.js + checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15 + languageName: node + linkType: hard + "react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -5917,6 +6233,17 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": + version: 3.6.2 + resolution: "readable-stream@npm:3.6.2" + dependencies: + inherits: "npm:^2.0.3" + string_decoder: "npm:^1.1.1" + util-deprecate: "npm:^1.0.1" + checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7 + languageName: node + linkType: hard + "readdirp@npm:^4.0.1": version: 4.1.2 resolution: "readdirp@npm:4.1.2" @@ -6164,7 +6491,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0": +"safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -6349,6 +6676,24 @@ __metadata: languageName: node linkType: hard +"simple-concat@npm:^1.0.0": + version: 1.0.1 + resolution: "simple-concat@npm:1.0.1" + checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776 + languageName: node + linkType: hard + +"simple-get@npm:^4.0.0": + version: 4.0.1 + resolution: "simple-get@npm:4.0.1" + dependencies: + decompress-response: "npm:^6.0.0" + once: "npm:^1.3.1" + simple-concat: "npm:^1.0.0" + checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -6512,6 +6857,15 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:^1.1.1": + version: 1.3.0 + resolution: "string_decoder@npm:1.3.0" + dependencies: + safe-buffer: "npm:~5.2.0" + checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d + languageName: node + linkType: hard + "strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": version: 6.0.1 resolution: "strip-ansi@npm:6.0.1" @@ -6537,6 +6891,13 @@ __metadata: languageName: node linkType: hard +"strip-json-comments@npm:~2.0.1": + version: 2.0.1 + resolution: "strip-json-comments@npm:2.0.1" + checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43 + languageName: node + linkType: hard + "superagent@npm:^10.2.3": version: 10.2.3 resolution: "superagent@npm:10.2.3" @@ -6617,6 +6978,31 @@ __metadata: languageName: node linkType: hard +"tar-fs@npm:^2.0.0": + version: 2.1.4 + resolution: "tar-fs@npm:2.1.4" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.1.4" + checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c + languageName: node + linkType: hard + +"tar-stream@npm:^2.1.4": + version: 2.2.0 + resolution: "tar-stream@npm:2.2.0" + dependencies: + bl: "npm:^4.0.3" + end-of-stream: "npm:^1.4.1" + fs-constants: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.1.1" + checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692 + languageName: node + linkType: hard + "tar@npm:^7.5.2": version: 7.5.2 resolution: "tar@npm:7.5.2" @@ -6737,6 +7123,15 @@ __metadata: languageName: node linkType: hard +"tunnel-agent@npm:^0.6.0": + version: 0.6.0 + resolution: "tunnel-agent@npm:0.6.0" + dependencies: + safe-buffer: "npm:^5.0.1" + checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a + languageName: node + linkType: hard + "tweetnacl@npm:^1.0.3": version: 1.0.3 resolution: "tweetnacl@npm:1.0.3" @@ -6854,6 +7249,18 @@ __metadata: languageName: node linkType: hard +"usb@npm:2.9.0": + version: 2.9.0 + resolution: "usb@npm:2.9.0" + dependencies: + "@types/w3c-web-usb": "npm:^1.0.6" + node-addon-api: "npm:^6.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.5.0" + checksum: 10c0/0b19f094e6c85aa77b74f69c3cbea9ced1426de30234ad513f8ecb777a00ab6bfd3fb04323bf24ca499633f41871438304930ccf9f44b958460f0112e2c446eb + languageName: node + linkType: hard + "utf-8-validate@npm:^5.0.2": version: 5.0.10 resolution: "utf-8-validate@npm:5.0.10" @@ -6864,6 +7271,13 @@ __metadata: languageName: node linkType: hard +"util-deprecate@npm:^1.0.1": + version: 1.0.2 + resolution: "util-deprecate@npm:1.0.2" + checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942 + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" From 51b7b280b4b0827f7b79f2aab03751ea2a5ddf68 Mon Sep 17 00:00:00 2001 From: Joshua Averett Date: Thu, 15 Jan 2026 00:31:47 -0800 Subject: [PATCH 3/6] Update testPort.ts to handle peer_config.json file --- ts-pkgs/deploy/guardian/governance_client.ts | 1 - ts-pkgs/deploy/guardian/testPort.ts | 112 +++++++++++++++++-- 2 files changed, 101 insertions(+), 12 deletions(-) diff --git a/ts-pkgs/deploy/guardian/governance_client.ts b/ts-pkgs/deploy/guardian/governance_client.ts index 9ad207e..00c9dc4 100644 --- a/ts-pkgs/deploy/guardian/governance_client.ts +++ b/ts-pkgs/deploy/guardian/governance_client.ts @@ -646,7 +646,6 @@ async function main() { } // Only run main if this file is executed directly (not imported for tests) -// Check if we're running as a script (not being imported) if (import.meta.url.endsWith(process.argv[1]?.replace(/\\/g, '/')) || process.argv[1]?.includes('governance_client')) { main().catch((error: unknown) => { diff --git a/ts-pkgs/deploy/guardian/testPort.ts b/ts-pkgs/deploy/guardian/testPort.ts index 683a379..babbe69 100644 --- a/ts-pkgs/deploy/guardian/testPort.ts +++ b/ts-pkgs/deploy/guardian/testPort.ts @@ -1,4 +1,6 @@ import net from 'net'; +import fs from 'fs'; +import path from 'path'; type PortState = 'open' | 'closed' | 'filtered' | 'error'; @@ -14,6 +16,20 @@ interface CheckResult extends Peer { rttNs?: bigint; } +// Type for peer_config.json +interface PeerConfigPeer { + Hostname: string; + Port: number; + TlsX509?: string; +} + +interface PeerConfig { + Peers: PeerConfigPeer[]; + Self?: PeerConfigPeer; + NumParticipants?: number; + WantedThreshold?: number; +} + /** Check single host:port using a full TCP connect */ function checkTcp( host: string, @@ -100,24 +116,98 @@ async function scanList( return results; } +/** Load peer configuration from peer_config.json */ +function loadPeerConfig(configPath: string): Peer[] { + try { + const configData = fs.readFileSync(configPath, 'utf-8'); + const config = JSON.parse(configData) as PeerConfig; + + // Validate basic structure + if (!config.Peers || !Array.isArray(config.Peers)) { + throw new Error('Invalid config: missing or invalid "Peers" array'); + } + + const peers: Peer[] = config.Peers.map((p: PeerConfigPeer) => ({ + host: p.Hostname, + port: p.Port, + })); + + // Optionally include Self peer if present + if (config.Self) { + peers.push({ + host: config.Self.Hostname, + port: config.Self.Port, + }); + } + + return peers; + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to load peer_config.json: ${error.message}`); + } + throw error; + } +} + (async () => { - // yarn run tsx testPort.ts google.com:80 example.org:22 localhost:9999 - // TODO: use yargs, there should be a command to pull the peers from - // the peer description server. - // We also need to add options for concurrency and timeout. const raw = process.argv.slice(2); - if (raw.length === 0) { - console.log('Usage: testPort.ts host:port [host:port] ...'); + let targets: Peer[]; + + // Check if --config flag is used or if no arguments provided + if (raw.length === 0 || (raw.length > 0 && raw[0] === '--config')) { + const configPath = raw.length > 1 && raw[0] === '--config' + ? path.resolve(raw[1]) + : path.resolve('peer_config.json'); + + if (!fs.existsSync(configPath)) { + console.error(`Error: ${configPath} not found`); + console.log('\nUsage:'); + console.log(' testPort.ts # reads from ./peer_config.json'); + console.log(' testPort.ts --config # reads from specified config file'); + console.log(' testPort.ts host:port [host:port] ... # test specific hosts'); + process.exit(1); + } + + try { + targets = loadPeerConfig(configPath); + console.log(`Loaded ${targets.length} peers from ${configPath}`); + } catch (error) { + console.error((error as Error | undefined)?.message ?? error); + process.exit(1); + } + } else { + // Parse command-line arguments as host:port pairs + targets = raw.map((s) => { + const [h, p] = s.split(':'); + return { host: h, port: Number(p) }; + }); + } + + if (targets.length === 0) { + console.error('Error: No peers to test'); process.exit(1); } - const targets = raw.map((s) => { - const [h, p] = s.split(':'); - return { host: h, port: Number(p) }; - }); + + console.log(`Testing ${targets.length} peer(s)...\n`); const out = await scanList(targets); + + // Print results + const openPeers = out.filter(r => r.state === 'open'); + const failedPeers = out.filter(r => r.state !== 'open'); + for (const r of out) { + const statusSymbol = r.state === 'open' ? '✓' : '✗'; + const rttInfo = r.rttNs !== undefined ? ` (${(Number(r.rttNs) / 1_000_000).toFixed(2)}ms)` : ''; + const errorInfo = r.error !== undefined ? ` - ${r.error}` : ''; // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions - console.log(`${r.host}:${r.port} -> ${r.state}${r.rttNs !== undefined ? ` (${r.rttNs}ns)` : ''}${r.error !== undefined ? ` (${r.error})` : ''}`); + console.log(`${statusSymbol} ${r.host}:${r.port} -> ${r.state}${rttInfo}${errorInfo}`); + } + + console.log(`\nSummary: ${openPeers.length}/${out.length} peers reachable`); + + if (failedPeers.length > 0) { + console.error(`\n⚠ Warning: ${failedPeers.length} peer(s) failed connection test`); + process.exit(1); } })().catch((error: unknown) => { console.error((error as Error | undefined)?.stack ?? error); From 2073d312830a31965ede36bcbcc3e148ac2c7d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Claudio=20Nale?= Date: Wed, 11 Feb 2026 22:04:22 -0300 Subject: [PATCH 4/6] deploy: simplifies governance client Also marks a few TODOs. --- .../governance_client.integration.test.ts | 34 +- .../deploy/guardian/governance_client.test.ts | 50 +- ts-pkgs/deploy/guardian/governance_client.ts | 725 ++++++------------ .../verification_v2.ts} | 414 ++++++---- ts-pkgs/deploy/package.json | 7 +- ts-pkgs/deploy/tsconfig.json | 14 +- ts-pkgs/deploy/vaatool/verifyVaa.ts | 7 +- yarn.lock | 631 +++++++++++++-- 8 files changed, 1111 insertions(+), 771 deletions(-) rename ts-pkgs/deploy/{guardian/verification_v2.json => idl/verification_v2.ts} (60%) diff --git a/ts-pkgs/deploy/guardian/governance_client.integration.test.ts b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts index ffb9ce5..14c9fcb 100644 --- a/ts-pkgs/deploy/guardian/governance_client.integration.test.ts +++ b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ethers } from 'ethers'; import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js'; -import { Program, AnchorProvider, Wallet } from '@coral-xyz/anchor'; import fs from 'fs'; import { encodeSetShardId, @@ -45,7 +44,7 @@ describe('EVM Contract Integration', () => { expect(decoded.slice(1)).toEqual(new Uint8Array(guardianMessage)); }); - it('should encode append_schnorr with pull_multisigs correctly', () => { + it('should encode append-schnorr with pull-multisigs correctly', () => { const vaa = Buffer.from([0x10, 0x20, 0x30]); const limit = 5; @@ -53,13 +52,13 @@ describe('EVM Contract Integration', () => { const appendData = encodeAppendSchnorrKey(vaa); const combined = ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); - // Verify structure: pull_multisigs (5 bytes) + append_schnorr (variable) + // Verify structure: pull-multisigs (5 bytes) + append-schnorr (variable) expect(combined.startsWith('0x')).toBe(true); const decoded = ethers.getBytes(combined); - // First 5 bytes should be pull_multisigs: opcode (1) + limit (4) + // First 5 bytes should be pull-multisigs: opcode (1) + limit (4) expect(decoded[0]).toBe(2); // UPDATE_PULL_MULTISIG_KEY_DATA - // Next should be append_schnorr: opcode (1) + length (2) + data + // Next should be append-schnorr: opcode (1) + length (2) + data const appendStart = 5; expect(decoded[appendStart]).toBe(1); // UPDATE_APPEND_SCHNORR_KEY }); @@ -95,12 +94,12 @@ describe('EVM Contract Integration', () => { it('should encode data that matches expected contract input format', () => { const testCases = [ { - command: 'set_shard_id' as const, + command: 'set-shard-id' as const, data: Buffer.from([0x01, 0x02, 0x03]), expectedOpcode: 0, }, { - command: 'pull_multisigs' as const, + command: 'pull-multisigs' as const, data: Buffer.alloc(0), expectedOpcode: 2, limit: 10, @@ -116,7 +115,7 @@ describe('EVM Contract Integration', () => { chainId: 1, limit: testCase.limit || 0, command: testCase.command, - ...(testCase.command === 'set_shard_id' ? { guardianMessage: 'test.msg' } : {}), + ...(testCase.command === 'set-shard-id' ? { guardianMessage: 'test.msg' } : {}), }; const encoded = encodeUpdate(args, testCase.data); @@ -251,24 +250,11 @@ describe('Solana Program Integration', () => { expect(pda).toBeInstanceOf(PublicKey); expect(pda.toBase58().length).toBeGreaterThan(0); }); - - it('should create connection and provider correctly', () => { - const connection = new Connection(testRpcUrl, 'confirmed'); - const keypair = Keypair.generate(); - const wallet = new Wallet(keypair); - const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' }); - - expect(provider.connection).toBe(connection); - expect(provider.wallet).toBe(wallet); - }); }); describe('Instruction Building (Mocked)', () => { it('should build appendSchnorrKey instruction with correct accounts', async () => { - const connection = new Connection(testRpcUrl, 'confirmed'); const keypair = Keypair.generate(); - const wallet = new Wallet(keypair); - const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' }); // Mock IDL - we'll use a minimal structure const mockIdl = { @@ -399,7 +385,7 @@ describe('Solana Program Integration', () => { describe('End-to-End Encoding Verification', () => { it('should produce encoding that matches contract expectations', () => { - // Test set_shard_id + // Test set-shard-id const guardianMessage = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05]); const encoded = encodeSetShardId(guardianMessage); @@ -420,7 +406,7 @@ describe('End-to-End Encoding Verification', () => { expect(callData).toBeTruthy(); }); - it('should handle append_schnorr with various VAA sizes', () => { + it('should handle append-schnorr with various VAA sizes', () => { const sizes = [0, 1, 10, 100, 1000, 5000]; for (const size of sizes) { @@ -438,7 +424,7 @@ describe('End-to-End Encoding Verification', () => { } }); - it('should encode pull_multisigs with various limits', () => { + it('should encode pull-multisigs with various limits', () => { const limits = [0, 1, 10, 100, 1000, 0xFFFFFFFF]; for (const limit of limits) { diff --git a/ts-pkgs/deploy/guardian/governance_client.test.ts b/ts-pkgs/deploy/guardian/governance_client.test.ts index ccd309c..7b68d8c 100644 --- a/ts-pkgs/deploy/guardian/governance_client.test.ts +++ b/ts-pkgs/deploy/guardian/governance_client.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ethers } from 'ethers'; import fs from 'fs'; import { @@ -8,7 +8,7 @@ import { encodeUpdate } from './governance_client.js'; import { parseGuardianKey } from '@xlabs-xyz/peer-lib'; -import { Connection, Keypair, PublicKey } from '@solana/web3.js'; +import { Keypair, PublicKey } from '@solana/web3.js'; // Mock dependencies vi.mock('fs'); @@ -20,7 +20,7 @@ vi.mock('@xlabs-xyz/peer-lib', () => ({ describe('Encoding Functions', () => { describe('encodeSetShardId', () => { - it('should encode set_shard_id command correctly', () => { + it('should encode set-shard-id command correctly', () => { const guardianMessage = Buffer.from([0x01, 0x02, 0x03, 0x04]); const result = encodeSetShardId(guardianMessage); @@ -51,7 +51,7 @@ describe('Encoding Functions', () => { }); describe('encodeAppendSchnorrKey', () => { - it('should encode append_schnorr_key command correctly', () => { + it('should encode append-schnorr_key command correctly', () => { const vaa = Buffer.from([0x10, 0x20, 0x30, 0x40, 0x50]); const result = encodeAppendSchnorrKey(vaa); @@ -81,7 +81,7 @@ describe('Encoding Functions', () => { }); describe('encodePullMultisigKeyData', () => { - it('should encode pull_multisigs command correctly', () => { + it('should encode pull-multisigs command correctly', () => { const limit = 42; const result = encodePullMultisigKeyData(limit); @@ -118,15 +118,14 @@ describe('Encoding Functions', () => { }); describe('encodeUpdate', () => { - it('should encode set_shard_id command', () => { + it('should encode set-shard-id command', () => { const args = { chain: 'evm' as const, contractAddress: '0x1234567890123456789012345678901234567890', rpcUrl: 'https://test.rpc', - signer: 'test.key', - chainId: 1, - limit: 0, - command: 'set_shard_id' as const, + signer: {type: "keyfile", path:"test.key"} as const, + pullLimit: 0, + command: 'set-shard-id' as const, guardianMessage: 'test.msg', }; const dataBytes = Buffer.from([0x01, 0x02, 0x03]); @@ -141,15 +140,14 @@ describe('Encoding Functions', () => { expect(result).toBe(expected); }); - it('should encode append_schnorr command with pull_multisigs', () => { + it('should encode append-schnorr command with pull-multisigs', () => { const args = { chain: 'evm' as const, contractAddress: '0x1234567890123456789012345678901234567890', rpcUrl: 'https://test.rpc', - signer: 'test.key', - chainId: 1, - limit: 10, - command: 'append_schnorr' as const, + signer: {type: "keyfile", path:"test.key"} as const, + pullLimit: 10, + command: 'append-schnorr' as const, vaa: 'test.vaa', }; const dataBytes = Buffer.from([0x10, 0x20, 0x30]); @@ -159,22 +157,21 @@ describe('Encoding Functions', () => { expect(result).toBeTruthy(); expect(result.startsWith('0x')).toBe(true); - // Should be concatenation of pull_multisigs + append_schnorr - const pullData = encodePullMultisigKeyData(args.limit); + // Should be concatenation of pull-multisigs + append-schnorr + const pullData = encodePullMultisigKeyData(args.pullLimit); const appendData = encodeAppendSchnorrKey(dataBytes); const expected = ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); expect(result).toBe(expected); }); - it('should encode pull_multisigs command', () => { + it('should encode pull-multisigs command', () => { const args = { chain: 'evm' as const, contractAddress: '0x1234567890123456789012345678901234567890', rpcUrl: 'https://test.rpc', - signer: 'test.key', - chainId: 1, - limit: 5, - command: 'pull_multisigs' as const, + signer: {type: "keyfile", path:"test.key"} as const, + pullLimit: 5, + command: 'pull-multisigs' as const, }; const dataBytes = Buffer.alloc(0); @@ -183,7 +180,7 @@ describe('Encoding Functions', () => { expect(result).toBeTruthy(); // Should match encodePullMultisigKeyData output - const expected = encodePullMultisigKeyData(args.limit); + const expected = encodePullMultisigKeyData(args.pullLimit); expect(result).toBe(expected); }); }); @@ -244,10 +241,9 @@ describe('File-based Signing', () => { chain: 'evm' as const, contractAddress: '0x1234567890123456789012345678901234567890', rpcUrl: 'https://test.rpc', - signer: 'test.key', - chainId: 1, - limit: 0, - command: 'set_shard_id' as const, + signer: {type: "keyfile", path:"test.key"} as const, + pullLimit: 0, + command: 'set-shard-id' as const, guardianMessage: 'test.msg', }; const dataBytes = Buffer.from([0x01, 0x02, 0x03]); diff --git a/ts-pkgs/deploy/guardian/governance_client.ts b/ts-pkgs/deploy/guardian/governance_client.ts index 00c9dc4..5056aba 100644 --- a/ts-pkgs/deploy/guardian/governance_client.ts +++ b/ts-pkgs/deploy/guardian/governance_client.ts @@ -1,18 +1,12 @@ import fs from "fs"; import { ethers } from "ethers"; -import type { Signer as EthersSigner } from "ethers"; -import { Connection, Keypair, PublicKey, sendAndConfirmTransaction, Transaction, type Signer } from "@solana/web3.js"; -import { Program, AnchorProvider, Wallet } from "@coral-xyz/anchor"; -import yargs, { type Argv } from "yargs"; +import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { Program } from "@coral-xyz/anchor"; +import yargs from "yargs"; import { hideBin } from 'yargs/helpers'; -import { parseGuardianKey, errorMsg, errorStack } from '@xlabs-xyz/peer-lib'; -import * as TransportNodeHid from "@ledgerhq/hw-transport-node-hid"; -import * as SolanaApp from "@ledgerhq/hw-app-solana"; +import { parseGuardianKey, errorStack } from '@xlabs-xyz/peer-lib'; -import type { VerificationV2 } from "../../../src/solana/target/types/verification_v2.js"; -// Import IDL - copied to build output during build step -import idlJson from "./verification_v2.json" with { type: "json" }; -const idl = idlJson as VerificationV2; +import {idl, VerificationV2} from "../idl/verification_v2.js"; // Default contract address for WormholeVerifier const DEFAULT_EVM_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000"; // TODO: Update with actual deployed address @@ -33,39 +27,38 @@ const UPDATE_ABI = [ ] as const; type BaseArgs = { - chain: "evm" | "solana"; contractAddress: string; rpcUrl: string; - signer: string; + signer: + | { + type: "keyfile"; + path: string; + } + | { + type: "ledger"; + derivationPath: string; + }; } -type EvmArgs = BaseArgs & { - chain: "evm"; - chainId: number; - limit: number; - ledger?: boolean; -} & ({ - command: "set_shard_id"; +type EvmArgs = BaseArgs & ({ + command: "set-shard-id"; guardianMessage: string; } | { - command: "append_schnorr"; + command: "append-schnorr"; + pullLimit: number; vaa: string; } | { - command: "pull_multisigs"; + command: "pull-multisigs"; + pullLimit: number; }) -type SolanaArgs = BaseArgs & { - chain: "solana"; - command: "append_schnorr"; +type SvmArgs = BaseArgs & { postedVaa: string; signatureSet: string; newKeyIndex: number; oldKeyIndex?: number; - ledger?: boolean; } -type Args = EvmArgs | SolanaArgs; - // TODO: Use binary-layout for these export function encodeSetShardId(guardianMessage: Buffer): string { // set_shard_id: opcode (1 byte) + guardian message data @@ -92,21 +85,22 @@ export function encodePullMultisigKeyData(limit: number): string { } export function encodeUpdate(args: EvmArgs, dataBytes: Buffer): string { - if (args.command === "append_schnorr") { - const pullData = encodePullMultisigKeyData(args.limit); + if (args.command === "append-schnorr") { + const pullData = encodePullMultisigKeyData(args.pullLimit); const appendData = encodeAppendSchnorrKey(dataBytes); - console.log(`Prepared pull_multisigs with limit ${args.limit}`); - console.log(`Prepared append_schnorr with ${dataBytes.length} bytes of data`); + console.log(`Prepared pull-multisigs with limit ${args.pullLimit}`); + console.log(`Prepared append-schnorr with ${dataBytes.length} bytes of data`); return ethers.solidityPacked(['bytes', 'bytes'], [pullData, appendData]); - } else if (args.command === "set_shard_id") { - console.log(`Prepared set_shard_id with ${dataBytes.length} bytes of data`); + } else if (args.command === "set-shard-id") { + console.log(`Prepared set-shard-id with ${dataBytes.length} bytes of data`); return encodeSetShardId(dataBytes); } else { - console.log(`Prepared pull_multisigs with limit ${args.limit}`); - return encodePullMultisigKeyData(args.limit); + console.log(`Prepared pull-multisigs with limit ${args.pullLimit}`); + return encodePullMultisigKeyData(args.pullLimit); } } +// TODO: move these two derivation functions to tss-definitions package // Derive the schnorr key PDA from key index function deriveSchnorrKeyPda(programId: PublicKey, schnorrKeyIndex: number): PublicKey { const schnorrKeyIndexBuf = Buffer.alloc(4); @@ -123,298 +117,76 @@ function deriveLatestKeyPda(programId: PublicKey): PublicKey { return pda; } -// ============================================================================ -// Signer Classes -// ============================================================================ - -// EVM file-based signer -class EvmSigner { - private wallet: ethers.Wallet; - - constructor(signerPath: string, provider: ethers.Provider) { - try { - const signerFile = fs.readFileSync(signerPath, 'utf-8'); - const keyBytes = parseGuardianKey(signerFile); - const signerKey = `0x${Buffer.from(keyBytes).toString('hex')}`; - this.wallet = new ethers.Wallet(signerKey, provider); - console.log(`Using file-based EVM signer: ${this.wallet.address}`); - } catch (error) { - console.error(`Failed to parse EVM signer file: ${errorMsg(error)}`); - throw error; - } - } - - getAddress(): string { - return this.wallet.address; - } - - getSigner(): ethers.Wallet { - return this.wallet; - } -} +async function createEvmSigner(args: EvmArgs) { + const provider = new ethers.JsonRpcProvider(args.rpcUrl, undefined, {staticNetwork: true}); -// EVM Ledger signer -class EvmLedgerSigner { - private ledgerSigner: EthersSigner; - - private constructor(ledgerSigner: EthersSigner) { - this.ledgerSigner = ledgerSigner; - } - - static async create(provider: ethers.Provider): Promise { - try { - // Dynamic import using Function constructor to prevent vite from analyzing it - const importLedger = new Function('specifier', 'return import(specifier)'); - const ledgerModule = await importLedger("@xlabs-xyz/ledger-ethers-signer"); - const LedgerSigner = ledgerModule.LedgerSigner || (ledgerModule as any).default?.LedgerSigner || (ledgerModule as any).default; - const ledgerSigner = new LedgerSigner(provider, "hid"); - const address = await ledgerSigner.getAddress(); - console.log(`Using Ledger EVM signer: ${address}`); - return new EvmLedgerSigner(ledgerSigner); - } catch (error) { - console.error(`Failed to initialize Ledger: ${errorMsg(error)}`); - console.error('Make sure your Ledger device is connected and the Ethereum app is open.'); - throw error; - } - } - - async getAddress(): Promise { - return await this.ledgerSigner.getAddress(); - } - - getSigner(): EthersSigner { - return this.ledgerSigner; - } -} - -// Solana file-based signer -class SolanaSigner implements Signer { - publicKey: PublicKey; - secretKey: Uint8Array; - private keypair: Keypair; - - constructor(signerPath: string) { - try { - const signerFile = fs.readFileSync(signerPath, 'utf-8'); - const keyBytes = parseGuardianKey(signerFile); - - // Solana Keypair.fromSecretKey expects 64 bytes (32-byte seed + 32-byte public key) - // If we have 32 bytes, we need to derive the Ed25519 keypair - // If we have 64 bytes, we can use it directly - if (keyBytes.length === 32) { - this.keypair = Keypair.fromSeed(keyBytes); - } else if (keyBytes.length === 64) { - this.keypair = Keypair.fromSecretKey(keyBytes); - } else { - throw new Error(`Invalid key length: expected 32 or 64 bytes, got ${keyBytes.length}`); - } - - this.publicKey = this.keypair.publicKey; - this.secretKey = this.keypair.secretKey; - console.log(`Using file-based Solana signer: ${this.publicKey.toBase58()}`); - } catch (error) { - console.error(`Failed to parse Solana signer file: ${errorMsg(error)}`); - throw error; - } - } - - getKeypair(): Keypair { - return this.keypair; - } - - async signTransaction(tx: Transaction): Promise { - tx.partialSign(this.keypair); - return tx; - } - - async signAllTransactions(txs: Transaction[]): Promise { - return txs.map(tx => { - tx.partialSign(this.keypair); - return tx; - }); - } -} - -// Solana Ledger signer -class SolanaLedgerSigner implements Signer { - publicKey: PublicKey; - secretKey: Uint8Array; // Required by Signer interface, but not used for Ledger - private solanaApp: SolanaApp.default; - private derivationPath: number[]; - - private constructor(solanaApp: SolanaApp.default, publicKey: PublicKey, derivationPath: number[] = [44, 501, 0, 0]) { - this.solanaApp = solanaApp; - this.publicKey = publicKey; - this.derivationPath = derivationPath; - this.secretKey = new Uint8Array(64); // Dummy secret key - not used for Ledger - } - - static async create(): Promise { - try { - // Initialize Ledger transport - // Note: Ledger packages use complex ESM exports that TypeScript struggles with - // @ts-ignore - Runtime behavior is correct despite type errors - const Transport = TransportNodeHid.default || TransportNodeHid; - // @ts-ignore - const transport = await Transport.create(); - // @ts-ignore - const Solana = SolanaApp.default || SolanaApp; - // @ts-ignore - const solanaApp = new Solana(transport); - - // Get public key from Ledger (using default derivation path) - // Derivation path format: "44'/501'/0'/0'" for Solana - const derivationPath = [44, 501, 0, 0]; // Standard Solana derivation path - const derivationPathStr = derivationPath.map((n, i) => i < 2 ? `${n}'` : n.toString()).join('/'); - const { publicKey } = await solanaApp.getPublicKey(derivationPathStr); - - const ledgerPublicKey = new PublicKey(publicKey); - console.log(`Using Ledger Solana signer: ${ledgerPublicKey.toBase58()}`); - - return new SolanaLedgerSigner(solanaApp, ledgerPublicKey, derivationPath); - } catch (error) { - console.error(`Failed to initialize Ledger: ${errorMsg(error)}`); - console.error('Make sure your Ledger device is connected and the Solana app is open.'); - throw error; - } - } - - async signTransaction(tx: Transaction): Promise { - // Serialize the transaction - const message = tx.serializeMessage(); - - // Convert derivation path to string format (e.g., "44'/501'/0'/0'") - const derivationPathStr = this.derivationPath.map((n, i) => i < 2 ? `${n}'` : n.toString()).join('/'); - - // Sign with Ledger - const result = await this.solanaApp.signTransaction(derivationPathStr, message); - - // Ledger returns { signature: Buffer }, extract the signature - const signature = result.signature || result; - const sigBuffer = Buffer.isBuffer(signature) ? signature : Buffer.from(signature); - tx.addSignature(this.publicKey, sigBuffer); - - return tx; - } - - async signAllTransactions(txs: Transaction[]): Promise { - // Sign each transaction sequentially - const signedTxs: Transaction[] = []; - for (const tx of txs) { - signedTxs.push(await this.signTransaction(tx)); - } - return signedTxs; - } -} - -// ============================================================================ -// Unified Signer Factory -// ============================================================================ - -type SignerConfig = { - chain: 'evm' | 'solana'; - ledger: boolean; - signerPath?: string; // Required if ledger=false - rpcUrl: string; -}; - -async function createSigner(config: SignerConfig): Promise { - if (config.chain === 'evm') { - const provider = new ethers.JsonRpcProvider(config.rpcUrl); - if (config.ledger) { - return await EvmLedgerSigner.create(provider); - } else { - if (!config.signerPath) { - throw new Error('signerPath is required for file-based signing'); - } - return new EvmSigner(config.signerPath, provider); - } + if (args.signer.type === "ledger") { + const {LedgerSigner} = await import("@xlabs-xyz/ledger-signer-ethers-v6"); + // remove cast as any when ethers is made peer dependency of ledger signer + return LedgerSigner.create(provider as any, args.signer.derivationPath); } else { - // Solana - if (config.ledger) { - return await SolanaLedgerSigner.create(); - } else { - if (!config.signerPath) { - throw new Error('signerPath is required for file-based signing'); - } - return new SolanaSigner(config.signerPath); - } + const keyfile = fs.readFileSync(args.signer.path, {encoding: "utf8"}); + return new ethers.Wallet(JSON.parse(keyfile), provider); } } async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise { - // Validate contract address - if (!ethers.isAddress(args.contractAddress)) { - console.error("Contract address must be a valid hex address"); - process.exit(1); - } - // Create signer using unified factory - const signer = await createSigner({ - chain: 'evm', - ledger: args.ledger || false, - signerPath: args.signer, - rpcUrl: args.rpcUrl, - }); + const signer = await createEvmSigner(args); const updateData = encodeUpdate(args, dataBytes); console.log(`Target contract: ${args.contractAddress}`); - console.log(`Chain ID: ${args.chainId}`); console.log(`RPC URL: ${args.rpcUrl}`); - try { - console.log('\nSending transaction...'); - // Get the underlying ethers signer - both EvmSigner and EvmLedgerSigner have getSigner() - const ethersSigner = (signer as EvmSigner | EvmLedgerSigner).getSigner(); - const contract = new ethers.Contract(args.contractAddress, UPDATE_ABI, ethersSigner); - const tx = await contract.update(updateData); + console.log('\nSending transaction...'); + const contract = new ethers.Contract(args.contractAddress, UPDATE_ABI, signer as any); + const tx = await contract.update(updateData); - console.log(`Transaction sent: ${tx.hash}`); - console.log('Waiting for confirmation...'); + console.log(`Transaction sent: ${tx.hash}`); + console.log('Waiting for confirmation...'); - const receipt = await tx.wait(); + const receipt = await tx.wait(); - if (receipt.status === 1) { - console.log(`Transaction confirmed in block ${receipt.blockNumber}`); - console.log(`Gas used: ${receipt.gasUsed.toString()}`); - } else { - console.error('Transaction failed'); - process.exit(1); - } - } catch (error) { - console.error(`Transaction failed: ${errorStack(error)}`); + if (receipt.status !== 1) { + console.error('Transaction confirmed, but execution failed'); process.exit(1); } + + console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + console.log(`Gas used: ${receipt.gasUsed.toString()}`); } -async function executeSolanaTransaction(args: SolanaArgs): Promise { - // Create signer using unified factory - const signer = await createSigner({ - chain: 'solana', - ledger: args.ledger || false, - signerPath: args.signer, - rpcUrl: args.rpcUrl, - }); +async function createSvmSigner(args: SvmArgs) { + let publicKey: PublicKey; + let signTransaction; + if (args.signer.type === "ledger") { + const {SolanaLedgerSigner} = await import("@xlabs-xyz/ledger-signer-solana"); + // remove cast as any when ethers is made peer dependency of ledger signer + const signer = await SolanaLedgerSigner.create(args.signer.derivationPath); + publicKey = new PublicKey(await signer.getAddress()); + signTransaction = async (tx: Transaction) => { + const signature = await signer.signTransaction(tx.compileMessage().serialize()); + tx.addSignature(publicKey, signature); + } + } else { + const keyfile = fs.readFileSync(args.signer.path, {encoding: "utf8"}); + const signer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(keyfile))); + publicKey = signer.publicKey; + signTransaction = async (tx: Transaction) => { + tx.partialSign(signer); + } + } + return {publicKey, signTransaction}; +} - // Type assertion since we know this is a Solana signer - const solanaSigner = signer as SolanaSigner | SolanaLedgerSigner; - const signerPublicKey = solanaSigner.publicKey; +async function executeSolanaTransaction(args: SvmArgs): Promise { + const {publicKey, signTransaction} = await createSvmSigner(args); const connection = new Connection(args.rpcUrl, "confirmed"); - const programId = new PublicKey(args.contractAddress || DEFAULT_SOLANA_PROGRAM_ID); - - // Create wallet/provider - all signer classes implement the Signer interface - const wallet = solanaSigner instanceof SolanaSigner - ? new Wallet(solanaSigner.getKeypair()) - : { - publicKey: solanaSigner.publicKey, - signTransaction: (tx: Transaction) => solanaSigner.signTransaction(tx), - signAllTransactions: (txs: Transaction[]) => solanaSigner.signAllTransactions(txs), - } as Wallet; - - const provider = new AnchorProvider(connection, wallet, { commitment: "confirmed" }); - const program = new Program(idl, provider); + const programId = new PublicKey(args.contractAddress); + const program = new Program(idl, {connection, publicKey}); + // TODO: post VAA here instead const postedVaa = new PublicKey(args.postedVaa); const signatureSet = new PublicKey(args.signatureSet); const latestKeyPda = deriveLatestKeyPda(programId); @@ -433,216 +205,171 @@ async function executeSolanaTransaction(args: SolanaArgs): Promise { } console.log(`RPC URL: ${args.rpcUrl}`); - try { - console.log('\nBuilding transaction...'); - const ix = await program.methods.appendSchnorrKey() - .accountsPartial({ - payer: signerPublicKey, - vaa: postedVaa, - signatureSet: signatureSet, - latestSchnorrKey: latestKeyPda, - newSchnorrKey: newSchnorrKeyPda, - oldSchnorrKey: oldSchnorrKeyPda, - }) - .instruction(); - - // Get recent blockhash and set fee payer before signing - const { blockhash } = await connection.getLatestBlockhash(); - const tx = new Transaction().add(ix); - tx.feePayer = signerPublicKey; - tx.recentBlockhash = blockhash; - - console.log('Sending transaction...'); - - // Sign and send transaction based on signer type - if (solanaSigner instanceof SolanaSigner) { - const signature = await sendAndConfirmTransaction(connection, tx, [solanaSigner.getKeypair()], { - commitment: "confirmed", - }); - console.log(`Transaction confirmed: ${signature}`); - } else { - // For Ledger, we need to sign the transaction first - const signedTx = await solanaSigner.signTransaction(tx); - const signature = await connection.sendRawTransaction(signedTx.serialize()); - await connection.confirmTransaction(signature, "confirmed"); - console.log(`Transaction confirmed: ${signature}`); - } - } catch (error) { - console.error(`Transaction failed: ${errorStack(error)}`); - process.exit(1); - } + console.log('\nBuilding transaction...'); + const ix = await program.methods.appendSchnorrKey() + .accountsPartial({ + payer: publicKey, + vaa: postedVaa, + signatureSet: signatureSet, + latestKey: latestKeyPda, + newSchnorrKey: newSchnorrKeyPda, + oldSchnorrKey: oldSchnorrKeyPda, + }) + .instruction(); + + // Get recent blockhash and set fee payer before signing + const tx = new Transaction().add(ix); + tx.feePayer = publicKey; + + console.log('Sending transaction...'); + const blockhash = await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash.blockhash; + + await signTransaction(tx); + const signature = await connection.sendRawTransaction(tx.serialize()); + const receipt = await connection.confirmTransaction({signature, ...blockhash}, "confirmed",); + console.log(`Transaction confirmed: ${signature}`); + if (receipt.value.err !== null) throw new Error(`Transaction failed: ${receipt.value.err.toString()}`); } -async function main() { +function main() { const parser = yargs(hideBin(process.argv)) - .command('set_shard_id ', 'Set the shard ID of the guardian (EVM only)', - (yargs: Argv) => yargs.positional('guardian-message', { - description: 'Path to file containing base64-encoded signed guardian message', - type: 'string', - } - )) - .command('append_schnorr ', 'Append a Schnorr key to the VerificationV2 contract', - (yargs: Argv) => yargs - .positional('vaa', { - description: 'Base64 encoded governance VAA (EVM) or ignored (Solana)', - type: 'string', - }) - .option('posted-vaa', { - description: '[Solana only] Address of the posted VAA account', - type: 'string', - }) - .option('signature-set', { - description: '[Solana only] Address of the signature set account', - type: 'string', - }) - .option('new-key-index', { - description: '[Solana only] Index of the new schnorr key to create', - type: 'number', - }) - .option('old-key-index', { - description: '[Solana only] Index of the old schnorr key (omit for init)', - type: 'number', - }) - ) - .command('pull_multisigs', 'Pull multisig sets from the core contract (EVM only)') - .demandCommand(1, 'A command is required') - .strictCommands() - .option('chain', { - description: 'Target chain type', - choices: ['evm', 'solana'] as const, - default: 'evm' as const, - alias: 't', + .option('ledger', { + description: 'Use Ledger hardware wallet for signing. ', + type: 'string', + demandOption: false, + conflicts: ["signer"], + }) + .option('key', { + description: 'Path to key file.', + type: 'string', + demandOption: false, + conflicts: ["ledger"], }) - .option('ledger', { - description: 'Use Ledger hardware wallet for signing', - type: 'boolean', - default: false, - }) - .option('signer', { - description: 'Path to signer key file (GPG armor guardian key for both EVM and Solana). Optional when --ledger is used.', - demandOption: false, - type: 'string', - alias: 's', - }) .option('contract-address', { description: 'Address of the WormholeVerifier contract/program', type: 'string', - default: DEFAULT_EVM_CONTRACT_ADDRESS, - alias: 'c', }) .option('rpc-url', { description: 'RPC endpoint URL for the target chain', type: 'string', - default: 'https://eth.llamarpc.com', - alias: 'r', - }) - .option('chain-id', { - description: '[EVM only] EIP-155 Chain ID', - type: 'number', - default: 1, - alias: 'i', - }) - .option('limit', { - description: '[EVM only] Maximum number of multisig sets to pull.', - defaultDescription: '0 (Pull all necessary multisig sets)', - type: 'number', - alias: 'l', - default: 0, }) + .command('svm', 'SVM commands', + (yargs) => yargs + .default("contract-address", DEFAULT_SOLANA_PROGRAM_ID) + .default("rpc-url", 'https://api.mainnet-beta.solana.com') + .command('append-schnorr ', 'Append a Schnorr key to the VerificationV2 contract', + (yargs) => yargs + // TODO: add option for priority fee + // TODO: make as many of these options implicit as possible + // start from a base64 VAA instead + .option('posted-vaa', { + description: 'Address of the posted VAA account', + demandOption: true, + type: 'string', + }) + .option('signature-set', { + description: 'Address of the signature set account', + demandOption: true, + type: 'string', + }) + .option('new-key-index', { + description: 'Index of the new schnorr key to create', + demandOption: true, + type: 'number', + }) + .option('old-key-index', { + description: 'Index of the old schnorr key (omit for init)', + type: 'number', + }), + (args) => { + let signer; + if (args.ledger !== undefined) { + signer = { type: "ledger", derivationPath: args.ledger } as const; + } else { + signer = { type: "keyfile", path: args.key! } as const; + } + + executeSolanaTransaction({...args, signer}) + } + ) + .demandCommand(1, 'A command is required') + .strictCommands() + ) + .command('evm', 'EVM commands', + (yargs) => yargs + .coerce("contract-address", (arg) => ethers.getAddress(arg)) + .default("contract-address", DEFAULT_EVM_CONTRACT_ADDRESS) + .default("rpc-url", 'https://ethereum-rpc.publicnode.com') + .option('pull-limit', { + description: 'Maximum number of multisig sets to pull.', + defaultDescription: '0: Pull all necessary multisig sets', + type: 'number', + default: 0, + }) + .command('set-shard-id ', 'Set the shard ID of the guardian (EVM only)', + (yargs) => yargs.positional('guardian-message', { + description: 'Path to file containing base64-encoded signed guardian message', + demandOption: true, + type: 'string', + }) + // TODO: add 1) sign shard id command 2) sign and set shard id command + // TODO: add support for AWS KMS + // .option('signer', { + // description: 'Path to guardian signer key file.', + // demandOption: false, + // type: 'string', + // }) + , + (args) => { + let signer; + if (args.ledger !== undefined) { + signer = { type: "ledger", derivationPath: args.ledger } as const; + } else { + signer = { type: "keyfile", path: args.key! } as const; + } + const guardianMessage = Buffer.from(args.guardianMessage, 'base64'); + return executeEvmTransaction({...args, signer, command: "set-shard-id"}, guardianMessage); + } + ) + .command('append-schnorr ', 'Append a Schnorr key to the VerificationV2 contract', + (yargs) => yargs + .positional('vaa', { + description: 'Base64 encoded governance VAA', + demandOption: true, + type: 'string', + }), + (args) => { + let signer; + if (args.ledger !== undefined) { + signer = { type: "ledger", derivationPath: args.ledger } as const; + } else { + signer = { type: "keyfile", path: args.key! } as const; + } + + return executeEvmTransaction({...args, signer, command: "append-schnorr"}, Buffer.from(args.vaa, 'base64')); + } + ) + .command('pull-multisigs', 'Pull multisig sets from the core contract (EVM only)', undefined, (args) => { + let signer; + if (args.ledger !== undefined) { + signer = { type: "ledger", derivationPath: args.ledger } as const; + } else { + signer = { type: "keyfile", path: args.key! } as const; + } + + return executeEvmTransaction({...args, signer, command: "pull-multisigs"}, Buffer.alloc(0)); + }) + .demandCommand(1, 'A command is required') + .strictCommands() + ) + .demandCommand(1, 'A command is required') + .strictCommands() .strictOptions() .help() .alias('help', 'h'); - const parsedArgs = await parser.parse(); - const command = parsedArgs._[0] as string; - - if (parsedArgs.chain === "solana") { - // Validate Solana-specific requirements - if (command !== "append_schnorr") { - console.error(`Command '${command}' is not supported on Solana. Only 'append_schnorr' is available.`); - process.exit(1); - } - - const postedVaa = parsedArgs.postedVaa as string | undefined; - const signatureSet = parsedArgs.signatureSet as string | undefined; - const newKeyIndex = parsedArgs.newKeyIndex as number | undefined; - const oldKeyIndex = parsedArgs.oldKeyIndex as number | undefined; - - if (!postedVaa || !signatureSet || newKeyIndex === undefined) { - console.error("Solana append_schnorr requires --posted-vaa, --signature-set, and --new-key-index"); - process.exit(1); - } - - // Validate signer requirement - if (!parsedArgs.ledger && !parsedArgs.signer) { - console.error("Either --signer or --ledger must be provided"); - process.exit(1); - } - - const args: SolanaArgs = { - chain: "solana", - command: "append_schnorr", - contractAddress: parsedArgs.contractAddress === DEFAULT_EVM_CONTRACT_ADDRESS - ? DEFAULT_SOLANA_PROGRAM_ID - : parsedArgs.contractAddress, - rpcUrl: parsedArgs.rpcUrl === 'https://eth.llamarpc.com' - ? 'https://api.mainnet-beta.solana.com' - : parsedArgs.rpcUrl, - signer: parsedArgs.signer || '', // Required by type but not used when ledger is true - ledger: parsedArgs.ledger || false, - postedVaa: postedVaa!, // Safe after check above - signatureSet: signatureSet!, // Safe after check above - newKeyIndex: newKeyIndex!, // Safe after check above - oldKeyIndex, - }; - - await executeSolanaTransaction(args); - } else { - // EVM flow - let dataBytes = Buffer.alloc(0); - - if (command === "set_shard_id") { - const guardianMessage = parsedArgs.guardianMessage as string; - try { - const messageBase64 = fs.readFileSync(guardianMessage, 'utf-8').trim(); - dataBytes = Buffer.from(messageBase64, 'base64'); - } catch (error) { - console.error(`Failed to load data from ${guardianMessage}: ${errorMsg(error)}`); - process.exit(1); - } - console.log(`Loaded ${dataBytes.length} bytes of data from ${guardianMessage}`); - } else if (command === "append_schnorr") { - const vaa = parsedArgs.vaa as string; - try { - dataBytes = Buffer.from(vaa, 'base64'); - } catch (error) { - console.error(`Failed to load VAA: ${errorMsg(error)}`); - process.exit(1); - } - console.log(`Loaded ${dataBytes.length} bytes of VAA`); - } - - // Validate signer requirement - if (!parsedArgs.ledger && !parsedArgs.signer) { - console.error("Either --signer or --ledger must be provided"); - process.exit(1); - } - - const args: EvmArgs = { - chain: "evm", - command: command as EvmArgs["command"], - contractAddress: parsedArgs.contractAddress, - rpcUrl: parsedArgs.rpcUrl, - chainId: parsedArgs.chainId, - signer: parsedArgs.signer || '', // Required by type but not used when ledger is true - ledger: parsedArgs.ledger || false, - limit: parsedArgs.limit, - ...(command === "set_shard_id" ? { guardianMessage: parsedArgs.guardianMessage as string } : {}), - ...(command === "append_schnorr" ? { vaa: parsedArgs.vaa as string } : {}), - } as EvmArgs; - - await executeEvmTransaction(args, dataBytes); - } + return parser.parseAsync(); } // Only run main if this file is executed directly (not imported for tests) diff --git a/ts-pkgs/deploy/guardian/verification_v2.json b/ts-pkgs/deploy/idl/verification_v2.ts similarity index 60% rename from ts-pkgs/deploy/guardian/verification_v2.json rename to ts-pkgs/deploy/idl/verification_v2.ts index 6c31a5e..8f20f31 100644 --- a/ts-pkgs/deploy/guardian/verification_v2.json +++ b/ts-pkgs/deploy/idl/verification_v2.ts @@ -1,23 +1,29 @@ -{ +/** + * Program IDL in camelCase format in order to be used in JS/TS. + * + * Note that this is only a type helper and is not the actual IDL. The original + * IDL can be found at `target/idl/verification_v2.json`. + */ +export type VerificationV2 = { "address": "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6", "metadata": { - "name": "verification_v2", + "name": "verificationV2", "version": "0.1.0", "spec": "0.1.0", "description": "Wormhole threshold signature verification program" }, "instructions": [ { - "name": "append_ecdsa_key", + "name": "appendSchnorrKey", "discriminator": [ - 74, - 74, - 208, - 15, - 133, - 104, - 213, - 132 + 8, + 6, + 50, + 98, + 26, + 48, + 99, + 30 ], "accounts": [ { @@ -29,10 +35,10 @@ "name": "vaa" }, { - "name": "signature_set" + "name": "signatureSet" }, { - "name": "latest_ecdsa_key", + "name": "latestKey", "writable": true, "pda": { "seeds": [ @@ -45,11 +51,6 @@ 101, 115, 116, - 101, - 99, - 100, - 115, - 97, 107, 101, 121 @@ -59,18 +60,20 @@ } }, { - "name": "new_ecdsa_key", + "name": "newSchnorrKey", "writable": true, "pda": { "seeds": [ { "kind": "const", "value": [ - 101, - 99, - 100, 115, - 97, + 99, + 104, + 110, + 111, + 114, + 114, 107, 101, 121 @@ -84,19 +87,213 @@ } }, { - "name": "old_ecdsa_key", + "name": "oldSchnorrKey", "writable": true, "optional": true }, { - "name": "system_program", + "name": "systemProgram", "address": "11111111111111111111111111111111" } ], "args": [] }, { - "name": "append_schnorr_key", + "name": "verifyVaa", + "discriminator": [ + 147, + 254, + 88, + 41, + 24, + 223, + 219, + 29 + ], + "accounts": [ + { + "name": "schnorrKey" + } + ], + "args": [ + { + "name": "rawVaa", + "type": "bytes" + } + ] + }, + { + "name": "verifyVaaAndDecode", + "discriminator": [ + 234, + 128, + 204, + 252, + 150, + 171, + 153, + 75 + ], + "accounts": [ + { + "name": "schnorrKey" + } + ], + "args": [ + { + "name": "rawVaa", + "type": "bytes" + } + ], + "returns": "bytes" + }, + { + "name": "verifyVaaHeaderWithDigest", + "discriminator": [ + 228, + 60, + 144, + 171, + 140, + 217, + 77, + 189 + ], + "accounts": [ + { + "name": "schnorrKey" + } + ], + "args": [ + { + "name": "rawVaaHeader", + "type": { + "array": [ + "u8", + 57 + ] + } + }, + { + "name": "digest", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + ], + "accounts": [ + { + "name": "latestKeyAccount", + "discriminator": [ + 26, + 81, + 106, + 22, + 26, + 185, + 50, + 132 + ] + }, + { + "name": "schnorrKeyAccount", + "discriminator": [ + 239, + 35, + 12, + 8, + 168, + 74, + 77, + 153 + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "invalidSignature", + "msg": "Signature does not satisfy preconditions" + }, + { + "code": 6001, + "name": "signatureVerificationFailed" + } + ], + "types": [ + { + "name": "latestKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "account", + "type": "pubkey" + } + ] + } + }, + { + "name": "schnorrKey", + "type": { + "kind": "struct", + "fields": [ + { + "name": "key", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "schnorrKeyAccount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "index", + "type": "u32" + }, + { + "name": "schnorrKey", + "type": { + "defined": { + "name": "schnorrKey" + } + } + }, + { + "name": "expirationTimestamp", + "type": "u64" + } + ] + } + } + ] +}; + + +export const idl: VerificationV2 = { + "address": "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6", + "metadata": { + "name": "verificationV2", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Wormhole threshold signature verification program" + }, + "instructions": [ + { + "name": "appendSchnorrKey", "discriminator": [ 8, 6, @@ -117,10 +314,10 @@ "name": "vaa" }, { - "name": "signature_set" + "name": "signatureSet" }, { - "name": "latest_schnorr_key", + "name": "latestKey", "writable": true, "pda": { "seeds": [ @@ -133,13 +330,6 @@ 101, 115, 116, - 115, - 99, - 104, - 110, - 111, - 114, - 114, 107, 101, 121 @@ -149,7 +339,7 @@ } }, { - "name": "new_schnorr_key", + "name": "newSchnorrKey", "writable": true, "pda": { "seeds": [ @@ -176,19 +366,19 @@ } }, { - "name": "old_schnorr_key", + "name": "oldSchnorrKey", "writable": true, "optional": true }, { - "name": "system_program", + "name": "systemProgram", "address": "11111111111111111111111111111111" } ], "args": [] }, { - "name": "verify_vaa", + "name": "verifyVaa", "discriminator": [ 147, 254, @@ -201,18 +391,18 @@ ], "accounts": [ { - "name": "key_account" + "name": "schnorrKey" } ], "args": [ { - "name": "raw_vaa", + "name": "rawVaa", "type": "bytes" } ] }, { - "name": "verify_vaa_and_decode", + "name": "verifyVaaAndDecode", "discriminator": [ 234, 128, @@ -225,19 +415,19 @@ ], "accounts": [ { - "name": "key_account" + "name": "schnorrKey" } ], "args": [ { - "name": "raw_vaa", + "name": "rawVaa", "type": "bytes" } ], "returns": "bytes" }, { - "name": "verify_vaa_header_with_digest", + "name": "verifyVaaHeaderWithDigest", "discriminator": [ 228, 60, @@ -250,13 +440,18 @@ ], "accounts": [ { - "name": "key_account" + "name": "schnorrKey" } ], "args": [ { - "name": "raw_vaa_header", - "type": "bytes" + "name": "rawVaaHeader", + "type": { + "array": [ + "u8", + 57 + ] + } }, { "name": "digest", @@ -272,46 +467,20 @@ ], "accounts": [ { - "name": "ECDSAKeyAccount", + "name": "latestKeyAccount", "discriminator": [ - 224, - 208, - 218, - 94, - 194, - 145, - 77, - 151 - ] - }, - { - "name": "LatestECDSAKeyAccount", - "discriminator": [ - 73, - 118, - 207, - 182, - 110, - 212, - 135, - 226 - ] - }, - { - "name": "LatestSchnorrKeyAccount", - "discriminator": [ - 198, - 126, - 27, - 188, - 86, - 108, - 157, - 207 + 26, + 81, + 106, + 22, + 26, + 185, + 50, + 132 ] }, { - "name": "SchnorrKeyAccount", + "name": "schnorrKeyAccount", "discriminator": [ 239, 35, @@ -327,76 +496,17 @@ "errors": [ { "code": 6000, - "name": "InvalidSignature", + "name": "invalidSignature", "msg": "Signature does not satisfy preconditions" }, { "code": 6001, - "name": "SignatureVerificationFailed", - "msg": "Signature verification failed" - }, - { - "code": 6002, - "name": "RecoveryFailed", - "msg": "Public key recovery failed" + "name": "signatureVerificationFailed" } ], "types": [ { - "name": "ECDSAKey", - "type": { - "kind": "struct", - "fields": [ - { - "name": "address", - "type": { - "array": [ - "u8", - 20 - ] - } - } - ] - } - }, - { - "name": "ECDSAKeyAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "index", - "type": "u32" - }, - { - "name": "ecdsa_key", - "type": { - "defined": { - "name": "ECDSAKey" - } - } - }, - { - "name": "expiration_timestamp", - "type": "u64" - } - ] - } - }, - { - "name": "LatestECDSAKeyAccount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "account", - "type": "pubkey" - } - ] - } - }, - { - "name": "LatestSchnorrKeyAccount", + "name": "latestKeyAccount", "type": { "kind": "struct", "fields": [ @@ -408,7 +518,7 @@ } }, { - "name": "SchnorrKey", + "name": "schnorrKey", "type": { "kind": "struct", "fields": [ @@ -425,7 +535,7 @@ } }, { - "name": "SchnorrKeyAccount", + "name": "schnorrKeyAccount", "type": { "kind": "struct", "fields": [ @@ -434,19 +544,19 @@ "type": "u32" }, { - "name": "schnorr_key", + "name": "schnorrKey", "type": { "defined": { - "name": "SchnorrKey" + "name": "schnorrKey" } } }, { - "name": "expiration_timestamp", + "name": "expirationTimestamp", "type": "u64" } ] } } ] -} \ No newline at end of file +}; \ No newline at end of file diff --git a/ts-pkgs/deploy/package.json b/ts-pkgs/deploy/package.json index b033cf7..01ced89 100644 --- a/ts-pkgs/deploy/package.json +++ b/ts-pkgs/deploy/package.json @@ -5,9 +5,7 @@ "type": "module", "scripts": { "build": "tsc --build", - "build:solana": "cd ../../src/solana && anchor build", "copy:idl": "node scripts/copy-idl.js", - "prebuild": "yarn build:solana && yarn copy:idl", "test": "vitest --run" }, "author": "", @@ -15,11 +13,10 @@ "description": "", "dependencies": { "@coral-xyz/anchor": "^0.31.1", - "@ledgerhq/hw-app-solana": "^7.0.0", - "@ledgerhq/hw-transport-node-hid": "^6.28.0", "@solana/web3.js": "^1.98.4", "@wormhole-foundation/sdk": "^4.5.0", - "@xlabs-xyz/ledger-ethers-signer": "github:XLabs/ledger-ethers-signer", + "@xlabs-xyz/ledger-signer-ethers-v6": "^0.0.2", + "@xlabs-xyz/ledger-signer-solana": "^0.0.2", "@xlabs-xyz/peer-lib": "workspace:*", "ethers": "^6.16.0", "viem": "^2.41.2", diff --git a/ts-pkgs/deploy/tsconfig.json b/ts-pkgs/deploy/tsconfig.json index 85faed5..e6b63d5 100644 --- a/ts-pkgs/deploy/tsconfig.json +++ b/ts-pkgs/deploy/tsconfig.json @@ -2,12 +2,16 @@ "extends": "../config/tsconfig.base.json", "compilerOptions": { "outDir": "./ts-build", - "resolveJsonModule": true, - "allowImportingTsExtensions": false + "resolveJsonModule": true }, - "include": ["evm", "guardian", "vaatool", "**/*.test.ts", "guardian/verification_v2.json"], + "include": [ + "evm", + "guardian", + "vaatool", + "idl", + "**/*.test.ts", + ], "references": [ - { "path": "../peer-lib" }, - { "path": "../../src/solana" } + { "path": "../peer-lib" } ] } diff --git a/ts-pkgs/deploy/vaatool/verifyVaa.ts b/ts-pkgs/deploy/vaatool/verifyVaa.ts index 000bd68..6b80871 100644 --- a/ts-pkgs/deploy/vaatool/verifyVaa.ts +++ b/ts-pkgs/deploy/vaatool/verifyVaa.ts @@ -5,10 +5,7 @@ import { Program, AnchorProvider } from "@coral-xyz/anchor"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; -import type { VerificationV2 } from "../../../src/solana/target/types/verification_v2.js"; -import { createRequire } from "module"; -const require = createRequire(import.meta.url); -const idl = require("../../../src/solana/target/idl/verification_v2.json"); +import { idl, VerificationV2 } from "../idl/verification_v2.js"; const VERIFICATION_FAILED_ERROR_SIGNATURE = "0x32629d58"; @@ -127,7 +124,7 @@ async function verifyVaaSolana( const ix = await program.methods .verifyVaa(vaaBytes) .accounts({ - keyAccount: schnorrKeyPda, + schnorrKey: schnorrKeyPda, }) .instruction(); diff --git a/yarn.lock b/yarn.lock index 49368e0..b05bbbe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -978,6 +978,219 @@ __metadata: languageName: node linkType: hard +"@ethersproject/abi@npm:^5.7.0": + version: 5.8.0 + resolution: "@ethersproject/abi@npm:5.8.0" + dependencies: + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/constants": "npm:^5.8.0" + "@ethersproject/hash": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + checksum: 10c0/6b759247a2f43ecc1548647d0447d08de1e946dfc7e71bfb014fa2f749c1b76b742a1d37394660ebab02ff8565674b3593fdfa011e16a5adcfc87ca4d85af39c + languageName: node + linkType: hard + +"@ethersproject/abstract-provider@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/abstract-provider@npm:5.8.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/networks": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/transactions": "npm:^5.8.0" + "@ethersproject/web": "npm:^5.8.0" + checksum: 10c0/9c183da1d037b272ff2b03002c3d801088d0534f88985f4983efc5f3ebd59b05f04bc05db97792fe29ddf87eeba3c73416e5699615f183126f85f877ea6c8637 + languageName: node + linkType: hard + +"@ethersproject/abstract-signer@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/abstract-signer@npm:5.8.0" + dependencies: + "@ethersproject/abstract-provider": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + checksum: 10c0/143f32d7cb0bc7064e45674d4a9dffdb90d6171425d20e8de9dc95765be960534bae7246ead400e6f52346624b66569d9585d790eedd34b0b6b7f481ec331cc2 + languageName: node + linkType: hard + +"@ethersproject/address@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/address@npm:5.8.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/rlp": "npm:^5.8.0" + checksum: 10c0/8bac8a4b567c75c1abc00eeca08c200de1a2d5cf76d595dc04fa4d7bff9ffa5530b2cdfc5e8656cfa8f6fa046de54be47620a092fb429830a8ddde410b9d50bc + languageName: node + linkType: hard + +"@ethersproject/base64@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/base64@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + checksum: 10c0/60ae6d1e2367d70f4090b717852efe62075442ae59aeac9bb1054fe8306a2de8ef0b0561e7fb4666ecb1f8efa1655d683dd240675c3a25d6fa867245525a63ca + languageName: node + linkType: hard + +"@ethersproject/bignumber@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/bignumber@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + bn.js: "npm:^5.2.1" + checksum: 10c0/8e87fa96999d59d0ab4c814c79e3a8354d2ba914dfa78cf9ee688f53110473cec0df0db2aaf9d447e84ab2dbbfca39979abac4f2dac69fef4d080f4cc3e29613 + languageName: node + linkType: hard + +"@ethersproject/bytes@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/bytes@npm:5.8.0" + dependencies: + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10c0/47ef798f3ab43b95dc74097b2c92365c919308ecabc3e34d9f8bf7f886fa4b99837ba5cf4dc8921baaaafe6899982f96b0e723b3fc49132c061f83d1ca3fed8b + languageName: node + linkType: hard + +"@ethersproject/constants@npm:^5.7.0, @ethersproject/constants@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/constants@npm:5.8.0" + dependencies: + "@ethersproject/bignumber": "npm:^5.8.0" + checksum: 10c0/374b3c2c6da24f8fef62e2316eae96faa462826c0774ef588cd7313ae7ddac8eb1bb85a28dad80123148be2ba0821c217c14ecfc18e2e683c72adc734b6248c9 + languageName: node + linkType: hard + +"@ethersproject/hash@npm:^5.7.0, @ethersproject/hash@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/hash@npm:5.8.0" + dependencies: + "@ethersproject/abstract-signer": "npm:^5.8.0" + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/base64": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + checksum: 10c0/72a287d4d70fae716827587339ffb449b8c23ef8728db6f8a661f359f7cb1e5ffba5b693c55e09d4e7162bf56af4a0e98a334784e0706d98102d1a5786241537 + languageName: node + linkType: hard + +"@ethersproject/keccak256@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/keccak256@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + js-sha3: "npm:0.8.0" + checksum: 10c0/cd93ac6a5baf842313cde7de5e6e2c41feeea800db9e82955f96e7f3462d2ac6a6a29282b1c9e93b84ce7c91eec02347043c249fd037d6051214275bfc7fe99f + languageName: node + linkType: hard + +"@ethersproject/logger@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/logger@npm:5.8.0" + checksum: 10c0/7f39f33e8f254ee681d4778bb71ce3c5de248e1547666f85c43bfbc1c18996c49a31f969f056b66d23012f2420f2d39173107284bc41eb98d0482ace1d06403e + languageName: node + linkType: hard + +"@ethersproject/networks@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/networks@npm:5.8.0" + dependencies: + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10c0/3f23bcc4c3843cc9b7e4b9f34df0a1f230b24dc87d51cdad84552302159a84d7899cd80c8a3d2cf8007b09ac373a5b10407007adde23d4c4881a4d6ee6bc4b9c + languageName: node + linkType: hard + +"@ethersproject/properties@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/properties@npm:5.8.0" + dependencies: + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10c0/20256d7eed65478a38dabdea4c3980c6591b7b75f8c45089722b032ceb0e1cd3dd6dd60c436cfe259337e6909c28d99528c172d06fc74bbd61be8eb9e68be2e6 + languageName: node + linkType: hard + +"@ethersproject/rlp@npm:^5.7.0, @ethersproject/rlp@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/rlp@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10c0/db742ec9c1566d6441242cc2c2ae34c1e5304d48e1fe62bc4e53b1791f219df211e330d2de331e0e4f74482664e205c2e4220e76138bd71f1ec07884e7f5221b + languageName: node + linkType: hard + +"@ethersproject/signing-key@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/signing-key@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + bn.js: "npm:^5.2.1" + elliptic: "npm:6.6.1" + hash.js: "npm:1.1.7" + checksum: 10c0/a7ff6cd344b0609737a496b6d5b902cf5528ed5a7ce2c0db5e7b69dc491d1810d1d0cd51dddf9dc74dd562ab4961d76e982f1750359b834c53c202e85e4c8502 + languageName: node + linkType: hard + +"@ethersproject/strings@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/strings@npm:5.8.0" + dependencies: + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/constants": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + checksum: 10c0/6db39503c4be130110612b6d593a381c62657e41eebf4f553247ebe394fda32cdf74ff645daee7b7860d209fd02c7e909a95b1f39a2f001c662669b9dfe81d00 + languageName: node + linkType: hard + +"@ethersproject/transactions@npm:^5.7.0, @ethersproject/transactions@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/transactions@npm:5.8.0" + dependencies: + "@ethersproject/address": "npm:^5.8.0" + "@ethersproject/bignumber": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/constants": "npm:^5.8.0" + "@ethersproject/keccak256": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/rlp": "npm:^5.8.0" + "@ethersproject/signing-key": "npm:^5.8.0" + checksum: 10c0/dd32f090df5945313aafa8430ce76834479750d6655cb786c3b65ec841c94596b14d3c8c59ee93eed7b4f32f27d321a9b8b43bc6bb51f7e1c6694f82639ffe68 + languageName: node + linkType: hard + +"@ethersproject/web@npm:^5.8.0": + version: 5.8.0 + resolution: "@ethersproject/web@npm:5.8.0" + dependencies: + "@ethersproject/base64": "npm:^5.8.0" + "@ethersproject/bytes": "npm:^5.8.0" + "@ethersproject/logger": "npm:^5.8.0" + "@ethersproject/properties": "npm:^5.8.0" + "@ethersproject/strings": "npm:^5.8.0" + checksum: 10c0/e3cd547225638db6e94fcd890001c778d77adb0d4f11a7f8c447e961041678f3fbfaffe77a962c7aa3f6597504232442e7015f2335b1788508a108708a30308a + languageName: node + linkType: hard + "@gql.tada/cli-utils@npm:1.7.2": version: 1.7.2 resolution: "@gql.tada/cli-utils@npm:1.7.2" @@ -1267,74 +1480,190 @@ __metadata: languageName: node linkType: hard -"@ledgerhq/devices@npm:8.8.0": - version: 8.8.0 - resolution: "@ledgerhq/devices@npm:8.8.0" +"@ledgerhq/client-ids@npm:0.5.0": + version: 0.5.0 + resolution: "@ledgerhq/client-ids@npm:0.5.0" dependencies: - "@ledgerhq/errors": "npm:^6.28.0" - "@ledgerhq/logs": "npm:^6.13.0" + "@ledgerhq/live-env": "npm:^2.26.0" + "@reduxjs/toolkit": "npm:2.11.2" + uuid: "npm:^9.0.0" + checksum: 10c0/1523d7dd29fc76e139fb12c973465602acc9be922e9b54c849801c5e046890e0ea8bb29dfef7b6ded5e6df211e9455828f0fe2eb99f9929e38f08fd912a2a38b + languageName: node + linkType: hard + +"@ledgerhq/cryptoassets-evm-signatures@npm:^13.7.1": + version: 13.7.1 + resolution: "@ledgerhq/cryptoassets-evm-signatures@npm:13.7.1" + dependencies: + "@ledgerhq/live-env": "npm:^2.21.0" + axios: "npm:1.12.2" + checksum: 10c0/fa883a51c86461244bb5353956a5fb3c21137a8b4c6d22196a82d451d0f3bbca33255990daf32f6400c4164770a88945f451817085fb59d7b7d604c068954f11 + languageName: node + linkType: hard + +"@ledgerhq/devices@npm:8.10.0": + version: 8.10.0 + resolution: "@ledgerhq/devices@npm:8.10.0" + dependencies: + "@ledgerhq/errors": "npm:^6.29.0" + "@ledgerhq/logs": "npm:^6.14.0" rxjs: "npm:7.8.2" + semver: "npm:7.7.3" + checksum: 10c0/d2b8087efe0f76004f5939014649d892172ed3b958ce7eff1652217be2b61af2b91276abee04c4a63d4ef2bf624e90268f8984395bd38a0556182a0f1f5343b5 + languageName: node + linkType: hard + +"@ledgerhq/devices@npm:8.7.0": + version: 8.7.0 + resolution: "@ledgerhq/devices@npm:8.7.0" + dependencies: + "@ledgerhq/errors": "npm:^6.27.0" + "@ledgerhq/logs": "npm:^6.13.0" + rxjs: "npm:^7.8.1" semver: "npm:^7.3.5" - checksum: 10c0/41169c8b040e98adf7699cf0141e1a5c1004dd58b076938b69aa10662ff7618d37f16ea4c26ed76e12bdf5e085527d06e043d4ebd67e50b19f0f06d92383685c + checksum: 10c0/6f8e2120c214cb37261758a6d2efc7299ba9d53924b7b522befa724a618023d55f38973d58854213c5c394498323c1676ae459c393b7eb9ab05154b0f6109e8e + languageName: node + linkType: hard + +"@ledgerhq/domain-service@npm:^1.4.1": + version: 1.6.2 + resolution: "@ledgerhq/domain-service@npm:1.6.2" + dependencies: + "@ledgerhq/errors": "npm:^6.29.0" + "@ledgerhq/logs": "npm:^6.14.0" + "@ledgerhq/types-live": "npm:^6.95.0" + axios: "npm:1.13.2" + eip55: "npm:^2.1.1" + react: "npm:18.3.1" + react-dom: "npm:18.3.1" + checksum: 10c0/9f06bb41707b212adf939e6b017e6a24e0e1c5b490cc4b0a8ea590adc24d198c4fe5d2331d71e281f6921bba82168d2226a0571dcb95329654e0b1a9162806f4 languageName: node linkType: hard -"@ledgerhq/errors@npm:^6.28.0": - version: 6.28.0 - resolution: "@ledgerhq/errors@npm:6.28.0" - checksum: 10c0/4e68d0b472c7e56b0fb8c5455629a8a39a67258823121fce87ecd0a9e0ff6e6e6e60c08fde826420a815939aa927d8bbfd62f5bf109478f4bbe6a0d0b11e1980 +"@ledgerhq/errors@npm:^6.27.0, @ledgerhq/errors@npm:^6.29.0": + version: 6.29.0 + resolution: "@ledgerhq/errors@npm:6.29.0" + checksum: 10c0/fa6601979a60e90ff1b504ee446f42fe81bfb6f110a65191fabd72fe74b383880716634ebef729d9de844982ef5c9c799140b428bcc84319f5307c0ef29ce3c7 languageName: node linkType: hard -"@ledgerhq/hw-app-solana@npm:^7.0.0": - version: 7.6.2 - resolution: "@ledgerhq/hw-app-solana@npm:7.6.2" +"@ledgerhq/evm-tools@npm:^1.8.1": + version: 1.10.1 + resolution: "@ledgerhq/evm-tools@npm:1.10.1" + dependencies: + "@ethersproject/constants": "npm:^5.7.0" + "@ethersproject/hash": "npm:^5.7.0" + "@ledgerhq/live-env": "npm:^2.26.0" + axios: "npm:1.13.2" + crypto-js: "npm:4.2.0" + checksum: 10c0/54221c83cba679c425c5631c156eb4eabc22f6be0be8bd540187442d40c7b6d1a1e1d17b6950bde7a3dae8af901dbfe56800db3806cad230b48238aed1dfc444 + languageName: node + linkType: hard + +"@ledgerhq/hw-app-eth@npm:6.47.1": + version: 6.47.1 + resolution: "@ledgerhq/hw-app-eth@npm:6.47.1" + dependencies: + "@ethersproject/abi": "npm:^5.7.0" + "@ethersproject/rlp": "npm:^5.7.0" + "@ethersproject/transactions": "npm:^5.7.0" + "@ledgerhq/cryptoassets-evm-signatures": "npm:^13.7.1" + "@ledgerhq/domain-service": "npm:^1.4.1" + "@ledgerhq/errors": "npm:^6.27.0" + "@ledgerhq/evm-tools": "npm:^1.8.1" + "@ledgerhq/hw-transport": "npm:6.31.13" + "@ledgerhq/hw-transport-mocker": "npm:^6.29.13" + "@ledgerhq/logs": "npm:^6.13.0" + "@ledgerhq/types-live": "npm:^6.89.0" + axios: "npm:1.12.2" + bignumber.js: "npm:^9.1.2" + semver: "npm:^7.3.5" + checksum: 10c0/067cd7c0b0076b4c7c72805d069ec323828fa154160e7e365858ac3833aadca28104d83e3b98aa751b2fbb940852987a2a23e2d8ea523b7224b311cbf86c381a + languageName: node + linkType: hard + +"@ledgerhq/hw-app-solana@npm:7.6.0": + version: 7.6.0 + resolution: "@ledgerhq/hw-app-solana@npm:7.6.0" dependencies: - "@ledgerhq/errors": "npm:^6.28.0" - "@ledgerhq/hw-transport": "npm:6.31.15" + "@ledgerhq/errors": "npm:^6.27.0" + "@ledgerhq/hw-transport": "npm:6.31.13" bip32-path: "npm:^0.4.2" - checksum: 10c0/588cbf5bed4d230b032739718902c9611b86f06b2a9f4780d8277e7d0fdb55d48cd68fef4c4dd9c1ab676252801d238f755e1f95c2d86fc3f5d3a544b2284c19 + checksum: 10c0/2de56fdc0ab6bbfd313d8d700fa00237e2fdb7813fb7845edb4613e6761fc5b5234d0c07d4dab0289e27b9b121db36ee712bdcebd8dbf70c4778cdd3e22e28ac languageName: node linkType: hard -"@ledgerhq/hw-transport-node-hid-noevents@npm:^6.30.16": - version: 6.30.16 - resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.30.16" +"@ledgerhq/hw-transport-mocker@npm:^6.29.13": + version: 6.31.0 + resolution: "@ledgerhq/hw-transport-mocker@npm:6.31.0" dependencies: - "@ledgerhq/devices": "npm:8.8.0" - "@ledgerhq/errors": "npm:^6.28.0" - "@ledgerhq/hw-transport": "npm:6.31.15" - "@ledgerhq/logs": "npm:^6.13.0" + "@ledgerhq/hw-transport": "npm:6.32.0" + "@ledgerhq/logs": "npm:^6.14.0" + rxjs: "npm:7.8.2" + checksum: 10c0/72d5d701858b1a632d5cbeb1bd4547695bd6ec02e47fddacb03c1881308392d58d52c2c0d5b11a3312cebac8cfc0613cf73363e0d79b5346a14a5e81d2d12700 + languageName: node + linkType: hard + +"@ledgerhq/hw-transport-node-hid-noevents@npm:^6.30.14": + version: 6.31.0 + resolution: "@ledgerhq/hw-transport-node-hid-noevents@npm:6.31.0" + dependencies: + "@ledgerhq/devices": "npm:8.10.0" + "@ledgerhq/errors": "npm:^6.29.0" + "@ledgerhq/hw-transport": "npm:6.32.0" + "@ledgerhq/logs": "npm:^6.14.0" node-hid: "npm:2.1.2" - checksum: 10c0/29579ef964996a2a0f7649706476d1053758ed503164816a59ebda21f0ce34ce49a3d1668c829d05317fe3b05cc5181f4a210fb175c25d86e0753097b2fa6644 + checksum: 10c0/f1b9bd57317c6b1f70e7e9fff70726d71429fb640e65d2a13e759f33681fe006f1aea54299c5197c9b05e727ce808226700e506e59640441dc43d6f12ee04bb3 languageName: node linkType: hard -"@ledgerhq/hw-transport-node-hid@npm:^6.28.0": - version: 6.29.16 - resolution: "@ledgerhq/hw-transport-node-hid@npm:6.29.16" +"@ledgerhq/hw-transport-node-hid@npm:6.29.14": + version: 6.29.14 + resolution: "@ledgerhq/hw-transport-node-hid@npm:6.29.14" dependencies: - "@ledgerhq/devices": "npm:8.8.0" - "@ledgerhq/errors": "npm:^6.28.0" - "@ledgerhq/hw-transport": "npm:6.31.15" - "@ledgerhq/hw-transport-node-hid-noevents": "npm:^6.30.16" + "@ledgerhq/devices": "npm:8.7.0" + "@ledgerhq/errors": "npm:^6.27.0" + "@ledgerhq/hw-transport": "npm:6.31.13" + "@ledgerhq/hw-transport-node-hid-noevents": "npm:^6.30.14" "@ledgerhq/logs": "npm:^6.13.0" lodash: "npm:^4.17.21" node-hid: "npm:2.1.2" usb: "npm:2.9.0" - checksum: 10c0/239d4dfd17e81083643e58605fd890611e1c73f1e3ce4f6c74dc8bb0715644109995a7da7661c9a03e739cf1bdbf368952b80b712748ac6b6fcf6f0c284a3a0d + checksum: 10c0/7db1a3a4e9a081152c2b630bd2958188a3217599b43e78a9f8e872877b127f4ce31ab391ca45db6ec93a4dab15e997c25d9149ea89ce4c184fc127e4de78d66d languageName: node linkType: hard -"@ledgerhq/hw-transport@npm:6.31.15": - version: 6.31.15 - resolution: "@ledgerhq/hw-transport@npm:6.31.15" +"@ledgerhq/hw-transport@npm:6.31.13": + version: 6.31.13 + resolution: "@ledgerhq/hw-transport@npm:6.31.13" dependencies: - "@ledgerhq/devices": "npm:8.8.0" - "@ledgerhq/errors": "npm:^6.28.0" + "@ledgerhq/devices": "npm:8.7.0" + "@ledgerhq/errors": "npm:^6.27.0" "@ledgerhq/logs": "npm:^6.13.0" events: "npm:^3.3.0" - checksum: 10c0/8911fd5cc7e2b8453d57d7f30321cb3284c1df48b4fa6540df5a617a52199b61e586ddd238a50d757e58bc695a311fe880b3060579b435fd362d9ef5d6583369 + checksum: 10c0/8f7f76a5bbb0d6ccb38d5c86d5f38dd422fb60f18f357b0ca58f9953b6a7d064c66516a9fbbd968b670c6de2a0f43b7ca9fc05990013e0a685ccb9492bcd29b7 + languageName: node + linkType: hard + +"@ledgerhq/hw-transport@npm:6.32.0": + version: 6.32.0 + resolution: "@ledgerhq/hw-transport@npm:6.32.0" + dependencies: + "@ledgerhq/devices": "npm:8.10.0" + "@ledgerhq/errors": "npm:^6.29.0" + "@ledgerhq/logs": "npm:^6.14.0" + events: "npm:^3.3.0" + checksum: 10c0/3479d1516cc283e3b3f13c33f476c273ff07921a53c5ae099e12a340ca2382e2a9641861131ec7c84b63e9e4ebf4cc49616b6d219ac83ab94987d697df8474aa + languageName: node + linkType: hard + +"@ledgerhq/live-env@npm:^2.21.0, @ledgerhq/live-env@npm:^2.26.0": + version: 2.26.0 + resolution: "@ledgerhq/live-env@npm:2.26.0" + dependencies: + rxjs: "npm:7.8.2" + utility-types: "npm:^3.10.0" + checksum: 10c0/752c6667479a1433fd384b644e90468f45a012ba0820f06e679c3d175d56671c439764cb80ec232b2bb8d7b2a4daebf891e502414f3b2e0cbbf17d0c3873a5ad languageName: node linkType: hard @@ -1345,6 +1674,24 @@ __metadata: languageName: node linkType: hard +"@ledgerhq/logs@npm:^6.14.0": + version: 6.14.0 + resolution: "@ledgerhq/logs@npm:6.14.0" + checksum: 10c0/fd6b7849cc86a9023a550dee6e24a9ea5496bc097dcf5fac955b0caf20bfdfe16fe1c5cb1c2f65625e05a8e77de61c8a84bc7e1807d080f58c650976e56d0c38 + languageName: node + linkType: hard + +"@ledgerhq/types-live@npm:^6.89.0, @ledgerhq/types-live@npm:^6.95.0": + version: 6.95.0 + resolution: "@ledgerhq/types-live@npm:6.95.0" + dependencies: + "@ledgerhq/client-ids": "npm:0.5.0" + bignumber.js: "npm:^9.1.2" + rxjs: "npm:7.8.2" + checksum: 10c0/6787b043e25182c1ecd304aca9d97ec7292fdecc8bffbfc7efc445acc894dc508fa850aa9984bb56dc6ec373655a671439c32aceca715735339f4862974be4c6 + languageName: node + linkType: hard + "@mysten/bcs@npm:1.9.2": version: 1.9.2 resolution: "@mysten/bcs@npm:1.9.2" @@ -1609,6 +1956,28 @@ __metadata: languageName: node linkType: hard +"@reduxjs/toolkit@npm:2.11.2": + version: 2.11.2 + resolution: "@reduxjs/toolkit@npm:2.11.2" + dependencies: + "@standard-schema/spec": "npm:^1.0.0" + "@standard-schema/utils": "npm:^0.3.0" + immer: "npm:^11.0.0" + redux: "npm:^5.0.1" + redux-thunk: "npm:^3.1.0" + reselect: "npm:^5.1.0" + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + checksum: 10c0/4d388b96dc4b12a577af23607c252b3647c1b3b5136dbb0212e1dbbef9bb309e93d3ba6a95795ee165e87e4286453025cd67a98b5b3bb6d244b93ea487dd1ac0 + languageName: node + linkType: hard + "@rollup/rollup-android-arm-eabi@npm:4.53.3": version: 4.53.3 resolution: "@rollup/rollup-android-arm-eabi@npm:4.53.3" @@ -1930,6 +2299,13 @@ __metadata: languageName: node linkType: hard +"@standard-schema/utils@npm:^0.3.0": + version: 0.3.0 + resolution: "@standard-schema/utils@npm:0.3.0" + checksum: 10c0/6eb74cd13e52d5fc74054df51e37d947ef53f3ab9e02c085665dcca3c38c60ece8d735cebbdf18fbb13c775fbcb9becb3f53109b0e092a63f0f7389ce0993fd0 + languageName: node + linkType: hard + "@swc/helpers@npm:^0.5.11": version: 0.5.17 resolution: "@swc/helpers@npm:0.5.17" @@ -2854,10 +3230,26 @@ __metadata: languageName: node linkType: hard -"@xlabs-xyz/ledger-ethers-signer@github:XLabs/ledger-ethers-signer": - version: 0.0.0 - resolution: "@xlabs-xyz/ledger-ethers-signer@https://github.com/XLabs/ledger-ethers-signer.git#commit=4d11de2ab8dd87064a5290555cdd93f32612210f" - checksum: 10c0/332a28195faf9f2c7a79149a0a9b1d576cbdc5ddbed9e13ac4c6e9906f036dd075f05cbad57249483d44efe91128bbf908e6952df478ee0606e47818fdfcef8e +"@xlabs-xyz/ledger-signer-ethers-v6@npm:^0.0.2": + version: 0.0.2 + resolution: "@xlabs-xyz/ledger-signer-ethers-v6@npm:0.0.2" + dependencies: + "@ledgerhq/hw-app-eth": "npm:6.47.1" + "@ledgerhq/hw-transport": "npm:6.31.13" + "@ledgerhq/hw-transport-node-hid": "npm:6.29.14" + ethers: "npm:6.15.0" + checksum: 10c0/94e753a7e743c8242fd35dc9971369ecf7c5481189d469e4cba5ab03585555150ec80e393cb971aa6201da738b79a3336c43a50c448ca379b79272976338966f + languageName: node + linkType: hard + +"@xlabs-xyz/ledger-signer-solana@npm:^0.0.2": + version: 0.0.2 + resolution: "@xlabs-xyz/ledger-signer-solana@npm:0.0.2" + dependencies: + "@ledgerhq/hw-app-solana": "npm:7.6.0" + "@ledgerhq/hw-transport": "npm:6.31.13" + "@ledgerhq/hw-transport-node-hid": "npm:6.29.14" + checksum: 10c0/2a998c1f7595a8392ecc5ba0380fabd806c4b1ed4b73e25e7c00493743d6059764d78a8680fd309a0af02b8a4fedfd80661347dacdb279f3a6b64312bc9a3c26 languageName: node linkType: hard @@ -3125,7 +3517,18 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.8.1": +"axios@npm:1.12.2": + version: 1.12.2 + resolution: "axios@npm:1.12.2" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.4" + proxy-from-env: "npm:^1.1.0" + checksum: 10c0/80b063e318cf05cd33a4d991cea0162f3573481946f9129efb7766f38fde4c061c34f41a93a9f9521f02b7c9565ccbc197c099b0186543ac84a24580017adfed + languageName: node + linkType: hard + +"axios@npm:1.13.2, axios@npm:^1.4.0, axios@npm:^1.6.0, axios@npm:^1.8.1": version: 1.13.2 resolution: "axios@npm:1.13.2" dependencies: @@ -3639,7 +4042,7 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.2.0": +"crypto-js@npm:4.2.0, crypto-js@npm:^4.2.0": version: 4.2.0 resolution: "crypto-js@npm:4.2.0" checksum: 10c0/8fbdf9d56f47aea0794ab87b0eb9833baf80b01a7c5c1b0edc7faf25f662fb69ab18dc2199e2afcac54670ff0cd9607a9045a3f7a80336cccd18d77a55b9fdf0 @@ -3736,13 +4139,12 @@ __metadata: resolution: "deploy@workspace:ts-pkgs/deploy" dependencies: "@coral-xyz/anchor": "npm:^0.31.1" - "@ledgerhq/hw-app-solana": "npm:^7.0.0" - "@ledgerhq/hw-transport-node-hid": "npm:^6.28.0" "@solana/web3.js": "npm:^1.98.4" "@types/node": "npm:^24.10.3" "@types/yargs": "npm:^17.0.35" "@wormhole-foundation/sdk": "npm:^4.5.0" - "@xlabs-xyz/ledger-ethers-signer": "github:XLabs/ledger-ethers-signer" + "@xlabs-xyz/ledger-signer-ethers-v6": "npm:^0.0.2" + "@xlabs-xyz/ledger-signer-solana": "npm:^0.0.2" "@xlabs-xyz/peer-lib": "workspace:*" ethers: "npm:^6.16.0" tsx: "npm:^4.21.0" @@ -3812,7 +4214,16 @@ __metadata: languageName: node linkType: hard -"elliptic@npm:^6.5.4, elliptic@npm:^6.5.7, elliptic@npm:^6.6.1": +"eip55@npm:^2.1.1": + version: 2.1.1 + resolution: "eip55@npm:2.1.1" + dependencies: + keccak: "npm:^3.0.3" + checksum: 10c0/a04787e484737f38c894b064b02257e2138382ec5eda5e8eb74bc16220bc30ee2dafc909ee1e18669374e37e76cfbc50c0664a723f34b36ef7490043e682c698 + languageName: node + linkType: hard + +"elliptic@npm:6.6.1, elliptic@npm:^6.5.4, elliptic@npm:^6.5.7, elliptic@npm:^6.6.1": version: 6.6.1 resolution: "elliptic@npm:6.6.1" dependencies: @@ -4276,6 +4687,21 @@ __metadata: languageName: node linkType: hard +"ethers@npm:6.15.0": + version: 6.15.0 + resolution: "ethers@npm:6.15.0" + dependencies: + "@adraffy/ens-normalize": "npm:1.10.1" + "@noble/curves": "npm:1.2.0" + "@noble/hashes": "npm:1.3.2" + "@types/node": "npm:22.7.5" + aes-js: "npm:4.0.0-beta.5" + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: 10c0/0a4581b662fe46a889a524d3aba43dc6f0ac59b3ae08dce678ee4b5799aab4906109ab24684c9644deedfc9d6e79b59faccecbeda9b6b7ceb085724d596a49e9 + languageName: node + linkType: hard + "ethers@npm:^6.13.5, ethers@npm:^6.16.0, ethers@npm:^6.5.1": version: 6.16.0 resolution: "ethers@npm:6.16.0" @@ -4819,7 +5245,7 @@ __metadata: languageName: node linkType: hard -"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": +"hash.js@npm:1.1.7, hash.js@npm:^1.0.0, hash.js@npm:^1.0.3": version: 1.1.7 resolution: "hash.js@npm:1.1.7" dependencies: @@ -4969,6 +5395,13 @@ __metadata: languageName: node linkType: hard +"immer@npm:^11.0.0": + version: 11.1.4 + resolution: "immer@npm:11.1.4" + checksum: 10c0/77beca6b0a4e361b30414189d63c64beb7df40ac1d8cbf524308fcbabe755e9aa49db3c6e95a6e412911416fec621f2c8470be5ec79feedc6abd6e4f7f18a9ce + languageName: node + linkType: hard + "import-fresh@npm:^3.2.1": version: 3.3.1 resolution: "import-fresh@npm:3.3.1" @@ -5172,7 +5605,7 @@ __metadata: languageName: node linkType: hard -"js-sha3@npm:^0.8.0": +"js-sha3@npm:0.8.0, js-sha3@npm:^0.8.0": version: 0.8.0 resolution: "js-sha3@npm:0.8.0" checksum: 10c0/43a21dc7967c871bd2c46cb1c2ae97441a97169f324e509f382d43330d8f75cf2c96dba7c806ab08a425765a9c847efdd4bffbac2d99c3a4f3de6c0218f40533 @@ -5248,6 +5681,18 @@ __metadata: languageName: node linkType: hard +"keccak@npm:^3.0.3": + version: 3.0.4 + resolution: "keccak@npm:3.0.4" + dependencies: + node-addon-api: "npm:^2.0.0" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.2.0" + readable-stream: "npm:^3.6.0" + checksum: 10c0/153525c1c1f770beadb8f8897dec2f1d2dcbee11d063fe5f61957a5b236bfd3d2a111ae2727e443aa6a848df5edb98b9ef237c78d56df49087b0ca8a232ca9cd + languageName: node + linkType: hard + "keyv@npm:^4.5.4": version: 4.5.4 resolution: "keyv@npm:4.5.4" @@ -5337,7 +5782,7 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.4.0": +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": version: 1.4.0 resolution: "loose-envify@npm:1.4.0" dependencies: @@ -5701,6 +6146,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^2.0.0": + version: 2.0.2 + resolution: "node-addon-api@npm:2.0.2" + dependencies: + node-gyp: "npm:latest" + checksum: 10c0/ade6c097ba829fa4aee1ca340117bb7f8f29fdae7b777e343a9d5cbd548481d1f0894b7b907d23ce615c70d932e8f96154caed95c3fa935cfe8cf87546510f64 + languageName: node + linkType: hard + "node-addon-api@npm:^3.0.2": version: 3.2.1 resolution: "node-addon-api@npm:3.2.1" @@ -6226,6 +6680,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:18.3.1": + version: 18.3.1 + resolution: "react-dom@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + scheduler: "npm:^0.23.2" + peerDependencies: + react: ^18.3.1 + checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85 + languageName: node + linkType: hard + "react-is@npm:^16.13.1, react-is@npm:^16.7.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" @@ -6233,7 +6699,16 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0": +"react@npm:18.3.1": + version: 18.3.1 + resolution: "react@npm:18.3.1" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3 + languageName: node + linkType: hard + +"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -6267,6 +6742,22 @@ __metadata: languageName: node linkType: hard +"redux-thunk@npm:^3.1.0": + version: 3.1.0 + resolution: "redux-thunk@npm:3.1.0" + peerDependencies: + redux: ^5.0.0 + checksum: 10c0/21557f6a30e1b2e3e470933247e51749be7f1d5a9620069a3125778675ce4d178d84bdee3e2a0903427a5c429e3aeec6d4df57897faf93eb83455bc1ef7b66fd + languageName: node + linkType: hard + +"redux@npm:^5.0.1": + version: 5.0.1 + resolution: "redux@npm:5.0.1" + checksum: 10c0/b10c28357194f38e7d53b760ed5e64faa317cc63de1fb95bc5d9e127fab956392344368c357b8e7a9bedb0c35b111e7efa522210cfdc3b3c75e5074718e9069c + languageName: node + linkType: hard + "rehackt@npm:^0.1.0": version: 0.1.0 resolution: "rehackt@npm:0.1.0" @@ -6289,6 +6780,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^5.1.0": + version: 5.1.1 + resolution: "reselect@npm:5.1.1" + checksum: 10c0/219c30da122980f61853db3aebd173524a2accd4b3baec770e3d51941426c87648a125ca08d8c57daa6b8b086f2fdd2703cb035dd6231db98cdbe1176a71f489 + languageName: node + linkType: hard + "resolve-from@npm:^4.0.0": version: 4.0.0 resolution: "resolve-from@npm:4.0.0" @@ -6482,7 +6980,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:^7.4.0": +"rxjs@npm:7.8.2, rxjs@npm:^7.4.0, rxjs@npm:^7.8.1": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -6505,6 +7003,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.2": + version: 0.23.2 + resolution: "scheduler@npm:0.23.2" + dependencies: + loose-envify: "npm:^1.1.0" + checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78 + languageName: node + linkType: hard + "secp256k1@npm:^4.0.3": version: 4.0.4 resolution: "secp256k1@npm:4.0.4" @@ -6517,7 +7024,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.5, semver@npm:^7.6.0": +"semver@npm:7.7.3, semver@npm:^7.3.5, semver@npm:^7.6.0": version: 7.7.3 resolution: "semver@npm:7.7.3" bin: @@ -7278,6 +7785,13 @@ __metadata: languageName: node linkType: hard +"utility-types@npm:^3.10.0": + version: 3.11.0 + resolution: "utility-types@npm:3.11.0" + checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19 + languageName: node + linkType: hard + "uuid@npm:^8.3.2": version: 8.3.2 resolution: "uuid@npm:8.3.2" @@ -7287,6 +7801,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^9.0.0": + version: 9.0.1 + resolution: "uuid@npm:9.0.1" + bin: + uuid: dist/bin/uuid + checksum: 10c0/1607dd32ac7fc22f2d8f77051e6a64845c9bce5cd3dd8aa0070c074ec73e666a1f63c7b4e0f4bf2bc8b9d59dc85a15e17807446d9d2b17c8485fbc2147b27f9b + languageName: node + linkType: hard + "valibot@npm:^1.2.0": version: 1.2.0 resolution: "valibot@npm:1.2.0" From 3da2e8fa80f9a1a80ccc8e7d166d17cdc9a4d645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Claudio=20Nale?= Date: Wed, 11 Feb 2026 22:14:24 -0300 Subject: [PATCH 5/6] deploy: several adjustments to verify vaa and test port scripts --- ts-pkgs/deploy/guardian/testPort.ts | 1 + ts-pkgs/deploy/vaatool/verifyVaa.ts | 28 +++++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/ts-pkgs/deploy/guardian/testPort.ts b/ts-pkgs/deploy/guardian/testPort.ts index babbe69..475dfe3 100644 --- a/ts-pkgs/deploy/guardian/testPort.ts +++ b/ts-pkgs/deploy/guardian/testPort.ts @@ -123,6 +123,7 @@ function loadPeerConfig(configPath: string): Peer[] { const config = JSON.parse(configData) as PeerConfig; // Validate basic structure + // TODO: can we reuse schemas? if (!config.Peers || !Array.isArray(config.Peers)) { throw new Error('Invalid config: missing or invalid "Peers" array'); } diff --git a/ts-pkgs/deploy/vaatool/verifyVaa.ts b/ts-pkgs/deploy/vaatool/verifyVaa.ts index 6b80871..5df3c8c 100644 --- a/ts-pkgs/deploy/vaatool/verifyVaa.ts +++ b/ts-pkgs/deploy/vaatool/verifyVaa.ts @@ -1,14 +1,16 @@ import { createPublicClient, http, Address, Hex, PublicClient } from "viem"; import { Chain, isChainId, toChain } from "@wormhole-foundation/sdk"; -import { Connection, PublicKey } from "@solana/web3.js"; +import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { Program, AnchorProvider } from "@coral-xyz/anchor"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { idl, VerificationV2 } from "../idl/verification_v2.js"; +import { inspect } from "util"; const VERIFICATION_FAILED_ERROR_SIGNATURE = "0x32629d58"; +// TODO: update this to a better default when the contracts are deployed. // Default Solana program ID from IDL const DEFAULT_SOLANA_PROGRAM_ID = "GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6"; @@ -97,8 +99,8 @@ function getSchnorrKeyIndexFromVaa(vaaBytes: Buffer): number { return vaaBytes.readUInt32LE(1); } -// Solana verification via simulate -async function verifyVaaSolana( +// SVM verification via simulate +async function verifyVaaSvm( connection: Connection, programId: PublicKey, vaaBytes: Buffer, @@ -129,13 +131,13 @@ async function verifyVaaSolana( .instruction(); const { blockhash } = await connection.getLatestBlockhash(); - const message = new (await import("@solana/web3.js")).TransactionMessage({ + const message = new TransactionMessage({ payerKey: PublicKey.default, recentBlockhash: blockhash, instructions: [ix], }).compileToV0Message(); - const tx = new (await import("@solana/web3.js")).VersionedTransaction(message); + const tx = new VersionedTransaction(message); const result = await connection.simulateTransaction(tx, { sigVerify: false, @@ -143,10 +145,10 @@ async function verifyVaaSolana( if (result.value.err) { const logs = result.value.logs?.join("\n") || "No logs"; - return { verified: false, error: `Simulation failed: ${JSON.stringify(result.value.err)}\nLogs:\n${logs}` }; + return { verified: false, error: `Simulation failed: ${inspect(result.value.err)}\nLogs:\n${logs}` }; } - // For Solana, we don't get the parsed VAA data back from verify_vaa (only verify_vaa_and_decode returns it) + // For SVM, we don't get the parsed VAA data back from verify_vaa (only verify_vaa_and_decode returns it) // Return a simplified success result return { verified: true, @@ -178,7 +180,7 @@ function getVaaType(vaaBytes: Buffer): "Multisig" | "Schnorr" | undefined { } type Args = { - chain: "evm" | "solana"; + chain: "evm" | "svm"; rpcUrl: string; verifierAddress: string; vaa: string; @@ -186,12 +188,12 @@ type Args = { async function main() { const { chain, rpc, verifier, vaa } = await yargs(hideBin(process.argv)) - .usage("Usage: $0 --chain --rpc --verifier
--vaa ") + .usage("Usage: $0 --chain --rpc --verifier
--vaa ") .option("chain", { alias: "c", type: "string", description: "Target chain type", - choices: ["evm", "solana"] as const, + choices: ["evm", "svm"] as const, default: "evm" as const, }) .option("rpc", { @@ -233,12 +235,12 @@ async function main() { result = await verifyVaaEvm(client, verifier as Address, vaaHex); } else { if (vaaType !== "Schnorr") { - console.error("Solana verification currently only supports Schnorr VAAs"); + console.error("SVM verification currently only supports Schnorr VAAs"); process.exit(1); } const connection = new Connection(rpc, "confirmed"); const programId = new PublicKey(verifier || DEFAULT_SOLANA_PROGRAM_ID); - result = await verifyVaaSolana(connection, programId, vaaBytes); + result = await verifyVaaSvm(connection, programId, vaaBytes); } if (!result.verified) { @@ -256,7 +258,7 @@ async function main() { console.log("Sequence:", result.sequence.toString()); console.log("Payload Offset:", result.payloadOffset); } else { - console.log("(Solana verify_vaa does not return parsed VAA data)"); + console.log("(SVM verify_vaa does not return parsed VAA data)"); } console.log("================================================"); } From e02657746a6ddee19bf5e5f6e9daad2fbf039126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Claudio=20Nale?= Date: Wed, 11 Feb 2026 22:54:34 -0300 Subject: [PATCH 6/6] deploy: several linter fixes --- .../governance_client.integration.test.ts | 81 ++++++++++--------- ts-pkgs/deploy/guardian/governance_client.ts | 25 ++++-- ts-pkgs/deploy/guardian/testPort.ts | 5 +- ts-pkgs/deploy/vaatool/verifyVaa.ts | 26 ++---- 4 files changed, 74 insertions(+), 63 deletions(-) diff --git a/ts-pkgs/deploy/guardian/governance_client.integration.test.ts b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts index 14c9fcb..529a507 100644 --- a/ts-pkgs/deploy/guardian/governance_client.integration.test.ts +++ b/ts-pkgs/deploy/guardian/governance_client.integration.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ethers } from 'ethers'; -import { Connection, Keypair, PublicKey, Transaction } from '@solana/web3.js'; +import { Keypair, PublicKey, Transaction } from '@solana/web3.js'; import fs from 'fs'; import { encodeSetShardId, @@ -102,19 +102,19 @@ describe('EVM Contract Integration', () => { command: 'pull-multisigs' as const, data: Buffer.alloc(0), expectedOpcode: 2, - limit: 10, + pullLimit: 10, }, ]; for (const testCase of testCases) { - const args: any = { + const args = { chain: 'evm' as const, contractAddress: testContractAddress, rpcUrl: testRpcUrl, - signer: 'test.key', - chainId: 1, - limit: testCase.limit || 0, - command: testCase.command, + signer: {type: "keyfile", path:"test.key"} as const, + pullLimit: testCase.pullLimit ?? 0, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any + command: testCase.command as any, ...(testCase.command === 'set-shard-id' ? { guardianMessage: 'test.msg' } : {}), }; @@ -177,11 +177,15 @@ describe('EVM Contract Integration', () => { vi.spyOn(contract, 'update').mockResolvedValue(mockTx); // Execute the transaction + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const tx = await contract.update(updateData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const receipt = await tx.wait(); expect(contract.update).toHaveBeenCalledWith(updateData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(receipt.status).toBe(1); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(receipt.blockNumber).toBe(12345); }); @@ -215,9 +219,12 @@ describe('EVM Contract Integration', () => { vi.spyOn(contract, 'update').mockResolvedValue(mockTx); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const tx = await contract.update(updateData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const receipt = await tx.wait(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(receipt.status).toBe(0); }); }); @@ -225,7 +232,6 @@ describe('EVM Contract Integration', () => { describe('Solana Program Integration', () => { const testProgramId = new PublicKey('GbFfTqMqKDgAMRH8VmDmoLTdvDd1853TnkkEwpydv3J6'); - const testRpcUrl = 'http://localhost:8899'; describe('Program Interface Verification', () => { it('should derive PDAs correctly for program', () => { @@ -253,31 +259,31 @@ describe('Solana Program Integration', () => { }); describe('Instruction Building (Mocked)', () => { - it('should build appendSchnorrKey instruction with correct accounts', async () => { + it('should build appendSchnorrKey instruction with correct accounts', () => { const keypair = Keypair.generate(); // Mock IDL - we'll use a minimal structure - const mockIdl = { - version: '0.1.0', - name: 'verification_v2', - metadata: { - address: testProgramId.toBase58(), - }, - instructions: [ - { - name: 'appendSchnorrKey', - accounts: [ - { name: 'payer', isSigner: true, isWritable: true }, - { name: 'vaa', isSigner: false, isWritable: false }, - { name: 'signatureSet', isSigner: false, isWritable: false }, - { name: 'latestSchnorrKey', isSigner: false, isWritable: false }, - { name: 'newSchnorrKey', isSigner: false, isWritable: true }, - { name: 'oldSchnorrKey', isSigner: false, isWritable: false, optional: true }, - ], - args: [], - }, - ], - }; + // const mockIdl = { + // version: '0.1.0', + // name: 'verification_v2', + // metadata: { + // address: testProgramId.toBase58(), + // }, + // instructions: [ + // { + // name: 'appendSchnorrKey', + // accounts: [ + // { name: 'payer', isSigner: true, isWritable: true }, + // { name: 'vaa', isSigner: false, isWritable: false }, + // { name: 'signatureSet', isSigner: false, isWritable: false }, + // { name: 'latestSchnorrKey', isSigner: false, isWritable: false }, + // { name: 'newSchnorrKey', isSigner: false, isWritable: true }, + // { name: 'oldSchnorrKey', isSigner: false, isWritable: false, optional: true }, + // ], + // args: [], + // }, + // ], + // }; // Skip Program creation test - it requires full IDL structure // Instead, test that we can build the accounts structure correctly @@ -306,16 +312,18 @@ describe('Solana Program Integration', () => { expect(accounts.signatureSet).toBeInstanceOf(PublicKey); expect(accounts.latestSchnorrKey).toBeInstanceOf(PublicKey); expect(accounts.newSchnorrKey).toBeInstanceOf(PublicKey); - - // Mock the program method call - const program = { methods: { appendSchnorrKey: vi.fn() } } as any; - + // Verify accounts structure matches what would be passed to the program expect(accounts).toMatchObject({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment payer: expect.any(PublicKey), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment vaa: expect.any(PublicKey), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment signatureSet: expect.any(PublicKey), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment latestSchnorrKey: expect.any(PublicKey), + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment newSchnorrKey: expect.any(PublicKey), oldSchnorrKey: null, }); @@ -323,7 +331,6 @@ describe('Solana Program Integration', () => { it('should build transaction with correct structure', () => { const keypair = Keypair.generate(); - const connection = new Connection(testRpcUrl, 'confirmed'); const mockInstruction = { keys: [], @@ -331,11 +338,11 @@ describe('Solana Program Integration', () => { data: Buffer.from([0x01, 0x02, 0x03]), }; - const tx = new Transaction().add(mockInstruction as any); + const tx = new Transaction().add(mockInstruction); tx.feePayer = keypair.publicKey; expect(tx.instructions.length).toBe(1); - expect(tx.feePayer?.equals(keypair.publicKey)).toBe(true); + expect(tx.feePayer.equals(keypair.publicKey)).toBe(true); }); }); diff --git a/ts-pkgs/deploy/guardian/governance_client.ts b/ts-pkgs/deploy/guardian/governance_client.ts index 5056aba..033b293 100644 --- a/ts-pkgs/deploy/guardian/governance_client.ts +++ b/ts-pkgs/deploy/guardian/governance_client.ts @@ -4,7 +4,7 @@ import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { Program } from "@coral-xyz/anchor"; import yargs from "yargs"; import { hideBin } from 'yargs/helpers'; -import { parseGuardianKey, errorStack } from '@xlabs-xyz/peer-lib'; +import { errorStack } from '@xlabs-xyz/peer-lib'; import {idl, VerificationV2} from "../idl/verification_v2.js"; @@ -123,9 +123,11 @@ async function createEvmSigner(args: EvmArgs) { if (args.signer.type === "ledger") { const {LedgerSigner} = await import("@xlabs-xyz/ledger-signer-ethers-v6"); // remove cast as any when ethers is made peer dependency of ledger signer + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any return LedgerSigner.create(provider as any, args.signer.derivationPath); } else { const keyfile = fs.readFileSync(args.signer.path, {encoding: "utf8"}); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument return new ethers.Wallet(JSON.parse(keyfile), provider); } } @@ -140,26 +142,33 @@ async function executeEvmTransaction(args: EvmArgs, dataBytes: Buffer): Promise< console.log(`RPC URL: ${args.rpcUrl}`); console.log('\nSending transaction...'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any const contract = new ethers.Contract(args.contractAddress, UPDATE_ABI, signer as any); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const tx = await contract.update(updateData); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access console.log(`Transaction sent: ${tx.hash}`); console.log('Waiting for confirmation...'); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access const receipt = await tx.wait(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access if (receipt.status !== 1) { console.error('Transaction confirmed, but execution failed'); process.exit(1); } + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access console.log(`Transaction confirmed in block ${receipt.blockNumber}`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call console.log(`Gas used: ${receipt.gasUsed.toString()}`); } async function createSvmSigner(args: SvmArgs) { let publicKey: PublicKey; - let signTransaction; + let signTransaction: (tx: Transaction) => Promise | void; if (args.signer.type === "ledger") { const {SolanaLedgerSigner} = await import("@xlabs-xyz/ledger-signer-solana"); // remove cast as any when ethers is made peer dependency of ledger signer @@ -171,9 +180,10 @@ async function createSvmSigner(args: SvmArgs) { } } else { const keyfile = fs.readFileSync(args.signer.path, {encoding: "utf8"}); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const signer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(keyfile))); publicKey = signer.publicKey; - signTransaction = async (tx: Transaction) => { + signTransaction = (tx: Transaction) => { tx.partialSign(signer); } } @@ -229,7 +239,7 @@ async function executeSolanaTransaction(args: SvmArgs): Promise { const signature = await connection.sendRawTransaction(tx.serialize()); const receipt = await connection.confirmTransaction({signature, ...blockhash}, "confirmed",); console.log(`Transaction confirmed: ${signature}`); - if (receipt.value.err !== null) throw new Error(`Transaction failed: ${receipt.value.err.toString()}`); + if (receipt.value.err !== null) throw new Error(`Transaction failed: ${errorStack(receipt.value.err)}`); } function main() { @@ -287,10 +297,11 @@ function main() { if (args.ledger !== undefined) { signer = { type: "ledger", derivationPath: args.ledger } as const; } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signer = { type: "keyfile", path: args.key! } as const; } - executeSolanaTransaction({...args, signer}) + return executeSolanaTransaction({...args, signer}); } ) .demandCommand(1, 'A command is required') @@ -298,6 +309,7 @@ function main() { ) .command('evm', 'EVM commands', (yargs) => yargs + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument .coerce("contract-address", (arg) => ethers.getAddress(arg)) .default("contract-address", DEFAULT_EVM_CONTRACT_ADDRESS) .default("rpc-url", 'https://ethereum-rpc.publicnode.com') @@ -326,6 +338,7 @@ function main() { if (args.ledger !== undefined) { signer = { type: "ledger", derivationPath: args.ledger } as const; } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signer = { type: "keyfile", path: args.key! } as const; } const guardianMessage = Buffer.from(args.guardianMessage, 'base64'); @@ -344,6 +357,7 @@ function main() { if (args.ledger !== undefined) { signer = { type: "ledger", derivationPath: args.ledger } as const; } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signer = { type: "keyfile", path: args.key! } as const; } @@ -355,6 +369,7 @@ function main() { if (args.ledger !== undefined) { signer = { type: "ledger", derivationPath: args.ledger } as const; } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion signer = { type: "keyfile", path: args.key! } as const; } diff --git a/ts-pkgs/deploy/guardian/testPort.ts b/ts-pkgs/deploy/guardian/testPort.ts index 475dfe3..8c2afb7 100644 --- a/ts-pkgs/deploy/guardian/testPort.ts +++ b/ts-pkgs/deploy/guardian/testPort.ts @@ -1,6 +1,7 @@ import net from 'net'; import fs from 'fs'; import path from 'path'; +import { errorStack } from '@xlabs-xyz/peer-lib'; type PortState = 'open' | 'closed' | 'filtered' | 'error'; @@ -124,6 +125,7 @@ function loadPeerConfig(configPath: string): Peer[] { // Validate basic structure // TODO: can we reuse schemas? + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition, @typescript-eslint/strict-boolean-expressions if (!config.Peers || !Array.isArray(config.Peers)) { throw new Error('Invalid config: missing or invalid "Peers" array'); } @@ -199,8 +201,7 @@ function loadPeerConfig(configPath: string): Peer[] { for (const r of out) { const statusSymbol = r.state === 'open' ? '✓' : '✗'; const rttInfo = r.rttNs !== undefined ? ` (${(Number(r.rttNs) / 1_000_000).toFixed(2)}ms)` : ''; - const errorInfo = r.error !== undefined ? ` - ${r.error}` : ''; - // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions + const errorInfo = r.error !== undefined ? ` - ${errorStack(r.error)}` : ''; console.log(`${statusSymbol} ${r.host}:${r.port} -> ${r.state}${rttInfo}${errorInfo}`); } diff --git a/ts-pkgs/deploy/vaatool/verifyVaa.ts b/ts-pkgs/deploy/vaatool/verifyVaa.ts index 5df3c8c..26a16e2 100644 --- a/ts-pkgs/deploy/vaatool/verifyVaa.ts +++ b/ts-pkgs/deploy/vaatool/verifyVaa.ts @@ -1,7 +1,7 @@ import { createPublicClient, http, Address, Hex, PublicClient } from "viem"; import { Chain, isChainId, toChain } from "@wormhole-foundation/sdk"; import { Connection, PublicKey, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; -import { Program, AnchorProvider } from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; import yargs from "yargs"; import { hideBin } from "yargs/helpers"; @@ -107,17 +107,12 @@ async function verifyVaaSvm( ): Promise { try { // Create a read-only provider (no wallet needed for simulation) - const provider = new AnchorProvider( + const provider = { connection, - { - publicKey: PublicKey.default, - signTransaction: async () => { throw new Error("Read-only"); }, - signAllTransactions: async () => { throw new Error("Read-only"); }, - }, - { commitment: "confirmed" } - ); + publicKey: PublicKey.default, + }; - const program = new Program(idl as VerificationV2, provider); + const program = new Program(idl, provider); const schnorrKeyIndex = getSchnorrKeyIndexFromVaa(vaaBytes); const schnorrKeyPda = deriveSchnorrKeyPda(programId, schnorrKeyIndex); @@ -143,8 +138,8 @@ async function verifyVaaSvm( sigVerify: false, }); - if (result.value.err) { - const logs = result.value.logs?.join("\n") || "No logs"; + if (result.value.err !== null) { + const logs = result.value.logs?.join("\n") ?? "No logs"; return { verified: false, error: `Simulation failed: ${inspect(result.value.err)}\nLogs:\n${logs}` }; } @@ -179,13 +174,6 @@ function getVaaType(vaaBytes: Buffer): "Multisig" | "Schnorr" | undefined { return undefined; } -type Args = { - chain: "evm" | "svm"; - rpcUrl: string; - verifierAddress: string; - vaa: string; -} - async function main() { const { chain, rpc, verifier, vaa } = await yargs(hideBin(process.argv)) .usage("Usage: $0 --chain --rpc --verifier
--vaa ")