diff --git a/solana/app/StakeConnection.ts b/solana/app/StakeConnection.ts index badd6da26..2d7597a6c 100644 --- a/solana/app/StakeConnection.ts +++ b/solana/app/StakeConnection.ts @@ -1,6 +1,5 @@ import { AnchorProvider, - Idl, IdlAccounts, Program, utils, @@ -8,6 +7,7 @@ import { } from "@coral-xyz/anchor"; import { Connection, + ComputeBudgetProgram, Keypair, PublicKey, SYSVAR_CLOCK_PUBKEY, @@ -23,7 +23,6 @@ import { } from "@solana/spl-token"; import BN from "bn.js"; import { Staking } from "../target/types/staking"; -import IDL from "../target/idl/staking.json"; import { WHTokenBalance } from "./whTokenBalance"; import { contracts } from "@wormhole-foundation/sdk-base"; import { @@ -40,6 +39,12 @@ import { import { signaturesToSolanaArray } from "@wormhole-foundation/wormhole-query-sdk"; import { deriveGuardianSetKey } from "./helpers/guardianSet"; import crypto from "crypto"; +import { + STAKING_ADDRESS as mainnetStakingAddress, +} from "./deploy/mainnet/constants"; +import { + STAKING_ADDRESS as devnetStakingAddress, +} from "./deploy/devnet/constants"; let wasm = importedWasm; export { wasm }; @@ -102,10 +107,12 @@ export class StakeConnection { priorityFeeConfig?: PriorityFeeConfig, ): Promise { const provider = new AnchorProvider(connection, wallet, {}); - const program = new Program( - IDL as Idl, - provider, - ) as unknown as Program; + const stakingAddress = connection.rpcEndpoint.includes("mainnet") + ? mainnetStakingAddress + : devnetStakingAddress; + const idl = (await Program.fetchIdl(stakingAddress, provider))!; + const program = new Program(idl, provider) as unknown as Program; + // Sometimes in the browser, the import returns a promise. // Don't fully understand, but this workaround is not terrible if (wasm.hasOwnProperty("default")) { @@ -747,7 +754,8 @@ export class StakeConnection { guardianSignatures: PublicKey, guardianSetIndex: number, unoptimized?: boolean, - ): Promise { + simulateOnly?: boolean, + ): Promise> { const { proposalAccount } = await this.fetchProposalAccount(proposalId); const networkType = this.provider.connection.rpcEndpoint.includes("mainnet") ? "Mainnet" @@ -767,13 +775,28 @@ export class StakeConnection { guardianSet: deriveGuardianSetKey(coreBridge, guardianSetIndex), }); - if (unoptimized) { + if (simulateOnly) { + const { blockhash } = await this.provider.connection.getLatestBlockhash("finalized"); + const computeBudgetInstruction = ComputeBudgetProgram.setComputeUnitLimit({ + units: 400000, + }); + + const simulationResult = await methodsBuilder + .preInstructions([computeBudgetInstruction]) + .transaction() + .then((tx) => { + tx.recentBlockhash = blockhash; + return methodsBuilder.simulate({ + commitment: "finalized", + }); + }); + + return simulationResult; + } else if (unoptimized) { await methodsBuilder.rpc().then(this.confirm); } else { const instructions: TransactionInstruction[] = []; - instructions.push(await methodsBuilder.instruction()); - await this.sendAndConfirmAsVersionedTransaction(instructions); } } @@ -852,6 +875,16 @@ export interface BalanceSummary { balance: WHTokenBalance; } +export type SimulateResponse = { + events: readonly Event[]; + raw: readonly string[]; +}; + +export type Event = { + name: string; + data: Defined; +}; + export class StakeAccount { address: PublicKey; checkpointAccount: CheckpointAccount; diff --git a/solana/app/deploy/mainnet/constants.ts b/solana/app/deploy/mainnet/constants.ts index ea09623c5..dd7dba711 100644 --- a/solana/app/deploy/mainnet/constants.ts +++ b/solana/app/deploy/mainnet/constants.ts @@ -7,21 +7,21 @@ export const RPC_NODE = "https://api.mainnet-beta.solana.com"; /// Wormhole Token (W) (mainnet solana address) export const WORMHOLE_TOKEN = new PublicKey( - "85VBFQZC9TZkfaptBWjvUw7YbZjy52A6mjtPGjstQAmQ", + "3bgLkkFyNRejfYXZMsbAodnskvaF9jXaScvK1KtTv8W4", ); export const STAKING_ADDRESS = new PublicKey( - "AFuHPdrQGsW8rNQ4oEFF35sm5fg36gwrxyqjkjKvi6ap", + "H3mSXnmYN3fChvRU6rhkLf9nGkytpEenSGm5DrgjFgHK", ); export const CORE_BRIDGE_PID = new PublicKey( contracts.coreBridge.get("Mainnet", "Solana")!, // worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth ); -console.log(CORE_BRIDGE_PID); +// console.log("CORE_BRIDGE_PID:", CORE_BRIDGE_PID); /// Wormhole Hub Proposal Metadata Contract (mainnet ethereum address) export const HUB_PROPOSAL_METADATA_ADDRESS = - "0x1a3e5624769c3dc9106347a239523e4a08d85c38"; // TODO: needs to be updated after a deployment to ethereum mainnet + "0xe1485b53e6E94aD4B82b19E48DA1911d2E19bFaE"; // TODO: needs to be updated after a deployment to ethereum mainnet export const hubProposalMetadataUint8Array = new Uint8Array( HUB_PROPOSAL_METADATA_ADDRESS.slice(2) .toLowerCase() @@ -37,7 +37,7 @@ export const CHECKPOINTS_ACCOUNT_LIMIT = 654998; /// Wormhole hubSolanaMessageDispatcher Contract (mainnet ethereum address) export const HUB_SOLANA_MESSAGE_DISPATCHER_ADDRESS = - "0xaeb78fb7ddedbbcab908e91e94f1fb04a23fbce5"; // TODO: needs to be updated after a deployment to ethereum mainnet + "0xadB8de6dfB41a1Fce6635460E77bEaDc73148BE4"; // TODO: needs to be updated after a deployment to ethereum mainnet const hubSolanaMessageDispatcherHex20 = HUB_SOLANA_MESSAGE_DISPATCHER_ADDRESS.slice(2); const hubSolanaMessageDispatcherHex32 = diff --git a/solana/app/deploy/mainnet/tests/15_addProposal.ts b/solana/app/deploy/mainnet/tests/15_addProposal.ts new file mode 100644 index 000000000..1012d7e8e --- /dev/null +++ b/solana/app/deploy/mainnet/tests/15_addProposal.ts @@ -0,0 +1,87 @@ +// Usage: npx ts-node app/deploy/mainnet/tests/15_addProposal.ts + +import { AnchorProvider, Wallet } from "@coral-xyz/anchor"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { StakeConnection } from "../../../StakeConnection"; +import { + CORE_BRIDGE_PID, + DEPLOYER_AUTHORITY_KEYPAIR, + RPC_NODE, +} from "../constants"; +import { getWormholeBridgeData } from "../../../helpers/wormholeBridgeConfig"; +import input from "@inquirer/input"; + +async function main() { + try { + const connection = new Connection(RPC_NODE); + + const provider = new AnchorProvider( + connection, + new Wallet(DEPLOYER_AUTHORITY_KEYPAIR), + {}, + ); + + const stakeConnection = await StakeConnection.createStakeConnection( + connection, + provider.wallet as Wallet, + ); + + const info = await getWormholeBridgeData(connection, CORE_BRIDGE_PID); + let guardianSetIndex = info.guardianSetIndex; + + const mainnetEthProposalResponse = { + bytes: + "01000091c88387e43b5949ff6c07d4251571f9328baf9accb55ef61480117a540a9d4a6b845f8c0410b0244f8e9c0c8378642f1ad175ad77e2ca1c326941fff9246d60000000006401000000000100020300000057000000093078313531643936650000000966696e616c697a656401e1485b53e6e94ad4b82b19e48da1911d2e19bfae00000024eb9b9838fcdbb9499c27488ca8300323b4f6b3b08802093c3d822d931ef0149f797116df0100020300000075000000000151d96e08eaca24e456367d064e1fdaa2fed41e383bc581dfa6010d6c4443199ab117100006315aa3ab63c00100000040fcdbb9499c27488ca8300323b4f6b3b08802093c3d822d931ef0149f797116df0000000000000000000000000000000000000000000000000000000067e5527d", + signatures: [ + "a9e756c766edaae672144d8799dbb927f6bc6c34f4769e9a1010cfea0818ce074ba9fd65713914e3f4dd66f45181032129c76b0af825213187be3c53ef62f0330100", + "82c78d62baa11304c1fa0c345e819d68798f9e183b55a4fc506b64a72bb0681e41b243d87d53fd9766723c1dc02a951730f1b4b178ceb11412d0891afd63bf4c0002", + "5819250b9ac5eb8a2896c99ea3a91af418643fab0b25e4547021e5ad673412fb6226e820ae6454bad9c0a15da406a32b0341d8b73fa426453df204f2f9f4ded20103", + "218c79a4deaf8a111d4573b55e33e77c40bc3f995d79d9edfc271d6a1bf1a1111aee8332800d45e8ad629429bbde3f6026f31bb8baa72c23adbc8a55cbaf44700106", + "40f0738532d9c3fd5921bbd2b058768f79ecbc4efd9dc3f14cbabb04e378cf1023792ea66ab44e92205f12c6552c25a2377460ca3fa1d90396de8e69bb99e5080007", + "72f0a5715c5c069baeba3e9586abfe661812671f9a12c73af375b3de92c128834657ded1a9159c9ce9ea6bb712cc98f4b0d5d3d45173ecac68b70f2a31aa3efa0109", + "eea28b273c3bc13f2e0ccabfea2e9760d92b45d87217281babce73976097a04106da9d153abd7d344b3a99b77065c095cfa85bc6ae95518d105ba73b5c06faf3000b", + "2655cdba9ef7707c6f9204d0ca02f779c8686e559d94cd4145eab84f9cb2df702daaced2bb74b1dd242b59a6b670da8c127c855c1b6b502e215e4e08467580bf010c", + "deed51fa86f355c5b6916873aa6b5d2b8bf798feec720da5e16b5ec05049b511278cb1f952cd9d01a3b3585ba7fe4a57c94c2f387137fa985424ae406d368467000d", + "53df6deebeda09aa91dacd9e32aa10663a03fb081c51d0a19887fddb30f147290ed1a74e3185009d961b235828792efa7c1a44fb2b39342837a4e321187e6673010e", + "7b88e420e5337bbb04cfd39b1c7f1c076837e32e859e8dc841fde3a71c9fffcc30400ec2896202ed37b7ca9ba12e2b03d6d35ad45f8501a77234bacf4f6698280010", + "186079ac19355cc723ae9276eef7c6940b45ed6993757f06a21c0c640ed45a8139ff2b7480e1a290bc8ffa1ced663af74b9fc56ac60dc7ff316bb6ec2bf6faf10111", + "f9412d3c8847878a62379b9e65ac598541e3b5892582ced8c1584815e4bae29702b609ee84a9ed294c9c6f75f544a6ccd4a488b5e496f7e0c5354b85232874910112" + ], + }; + + const proposalId = await input({ message: "Enter the proposal id:" }); + const proposalIdHex = BigInt(proposalId).toString(16).padStart(64, "0"); + // console.log("proposalIdHex:", proposalIdHex); + const proposalIdArray = Buffer.from(proposalIdHex, "hex"); + +// const guardianSignaturesPda = await stakeConnection.postSignatures( +// mainnetEthProposalResponse.signatures, +// ); + const guardianSignaturesPda = new PublicKey( + "WfU95bkYavZdQHi4dMJ17K7HtfzRqzduwHKAnLzkaiA", + ); + + const simulationResult = await stakeConnection.addProposal( + proposalIdArray, + Buffer.from(mainnetEthProposalResponse.bytes, "hex"), + guardianSignaturesPda, + guardianSetIndex, + false, // unoptimized + true, // simulateOnly + ); + + console.log("Simulation result:", simulationResult); + +// await stakeConnection.addProposal( +// proposalIdArray, +// Buffer.from(mainnetEthProposalResponse.bytes, "hex"), +// guardianSignaturesPda, +// guardianSetIndex, +// true +// ); + } catch (err) { + console.error("Error:", err); + } +} + +main(); diff --git a/solana/app/e2e/02_addProposal_mainnet.ts b/solana/app/e2e/02_addProposal_mainnet.ts new file mode 100644 index 000000000..69e10fe4b --- /dev/null +++ b/solana/app/e2e/02_addProposal_mainnet.ts @@ -0,0 +1,234 @@ +// Usage: npx ts-node app/e2e/02_addProposal_mainnet.ts + +import { + EthCallData, + EthCallWithFinalityQueryRequest, + PerChainQueryRequest, + QueryRequest, + QueryResponse, +} from "@wormhole-foundation/wormhole-query-sdk"; +import axios, { AxiosError } from "axios"; +import { ethers } from "ethers"; +import input from "@inquirer/input"; +import "dotenv/config"; +import { + HUB_CHAIN_ID, + HUB_PROPOSAL_METADATA_ADDRESS, + CORE_BRIDGE_PID, + DEPLOYER_AUTHORITY_KEYPAIR, + RPC_NODE, +} from "../deploy/mainnet/constants"; + +import { AnchorProvider, Wallet } from "@coral-xyz/anchor"; +import { Connection } from "@solana/web3.js"; +import { StakeConnection } from "../StakeConnection"; +import { getWormholeBridgeData } from "../helpers/wormholeBridgeConfig"; +import * as fs from "fs"; + +const WORMHOLE_API_KEY = process.env.WORMHOLE_API_KEY as string; +const rpcUrl = process.env[`RPC_${HUB_CHAIN_ID}`]; +if (!rpcUrl) { + throw new Error(`RPC endpoint not found for chain ${HUB_CHAIN_ID}`); +} + +function encodeSignature(signature: string): string { + return ethers.id(signature).substring(0, 10); +} + +function encodeCalldata(signature: string, parameters: string): string { + return signature + parameters.substring(2); +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +const MAX_RETRIES = 5; +const RETRY_DELAY = 10000; // 10 seconds +async function getLatestFinalizedBlock(rpcUrl: string): Promise { + let attempt = 0; + + while (attempt < MAX_RETRIES) { + try { + const response = await axios.post(rpcUrl, { + method: "eth_getBlockByNumber", + params: ["finalized", false], + id: 1, + jsonrpc: "2.0", + }); + + return response.data?.result?.number; + } catch (error) { + if ( + error instanceof AxiosError && + (error.code === "EHOSTUNREACH" || + error.code === "ENETUNREACH" || + error.code === "ERR_BAD_REQUEST") + ) { + attempt++; + console.log( + `Attempt ${attempt} failed. Retrying in ${RETRY_DELAY / 1000} seconds...`, + ); + await sleep(RETRY_DELAY); + } else { + console.error("Error during request:", error); + break; + } + } + } + + throw new Error("Failed to get the latest finalized block."); +} + +async function getWormholeQuery( + chain: number, + block: number, + calldata: EthCallData, +) { + const request = new QueryRequest( + 0, // nonce + [ + new PerChainQueryRequest( + chain, // Ethereum Wormhole Chain ID + new EthCallWithFinalityQueryRequest(block, "finalized", [calldata]), + ), + ], + ).serialize(); +// console.log("bytes:", Buffer.from(request).toString("hex")) + return ( + await axios.post( + "https://query.wormhole.com/v1/query", + { bytes: Buffer.from(request).toString("hex") }, + { headers: { "X-API-Key": WORMHOLE_API_KEY } }, + ) + ).data; +} + +async function getProposalMetadata( + proposalId: string, +): Promise<{ rawResponse: any }> { + const encodedSignature = encodeSignature("getProposalMetadata(uint256)"); + const encodedParameters = new ethers.AbiCoder().encode( + ["uint256"], + [proposalId], + ); + + const calldata: EthCallData = { + to: HUB_PROPOSAL_METADATA_ADDRESS, + data: encodeCalldata(encodedSignature, encodedParameters), + }; + + const latestFinalizedBlock = await getLatestFinalizedBlock(rpcUrl!); + console.log( + "latestFinalizedBlock:", + parseInt(latestFinalizedBlock.toString(), 16), + ); + const rawResponse = await getWormholeQuery( + HUB_CHAIN_ID, + latestFinalizedBlock, + calldata, + ); + console.log("rawResponse: ", rawResponse); + const queryResponse = QueryResponse.from( + Buffer.from(rawResponse.bytes, "hex"), + ); + // console.log("queryResponse:", queryResponse); + const solRequest = queryResponse.request.requests[0].query; + const solResponse = queryResponse.responses[0].response; + // console.log("solRequest:", solRequest); + // console.log("solResponse:", solResponse); + + return { rawResponse }; +} + +async function addProposal() { + try { + const connection = new Connection(RPC_NODE); + + const provider = new AnchorProvider( + connection, + new Wallet(DEPLOYER_AUTHORITY_KEYPAIR), + {}, + ); + + const stakeConnection = await StakeConnection.createStakeConnection( + connection, + provider.wallet as Wallet, + ); + + const proposalId = await input({ message: "Enter the proposal id:" }); + let proposalIdHex = BigInt(proposalId).toString(16).padStart(64, "0"); + // console.log("proposalIdHex:", proposalIdHex); + + const fileName = `./app/e2e/log/${proposalId.toString()}.json`; + if (!fs.existsSync(fileName)) + throw new Error(`Proposal file not found: ${fileName}`); + const savedProposalData = JSON.parse(fs.readFileSync(fileName, "utf8")); + console.log("savedProposalData:", savedProposalData); + + while (true) { + const latestFinalizedBlockInt = parseInt( + (await getLatestFinalizedBlock(rpcUrl!)).toString(), + 16, + ); + console.log("Latest finalized block:", latestFinalizedBlockInt); + + if (latestFinalizedBlockInt >= savedProposalData.receiptBlockNumber) { + console.log("The proposal block is finalized."); + break; + } else { + await sleep(5000); + } + } + + const proposalIdArray = Buffer.from(proposalIdHex, "hex"); + const { proposalAccount } = + await stakeConnection.fetchProposalAccount(proposalIdArray); + const proposalAccountData = + await stakeConnection.program.account.proposalData.fetchNullable( + proposalAccount, + ); + console.log("proposalAccountData:", proposalAccountData); + + if (proposalAccountData == null) { + const { rawResponse } = await getProposalMetadata(proposalId); + + const mainnetEthProposalResponse = { + bytes: rawResponse.bytes, + signatures: rawResponse.signatures, + }; + console.log("mainnetEthProposalResponse:",mainnetEthProposalResponse) + + const guardianSignaturesPda = await stakeConnection.postSignatures( + mainnetEthProposalResponse.signatures, + ); + + const info = await getWormholeBridgeData(connection, CORE_BRIDGE_PID); + const guardianSetIndex = info.guardianSetIndex; + + const simulationResult = await stakeConnection.addProposal( + proposalIdArray, + Buffer.from(mainnetEthProposalResponse.bytes, "hex"), + guardianSignaturesPda, + guardianSetIndex, + false, // unoptimized + true, // simulateOnly + ); + + console.log("Simulation result:", simulationResult); + + // await stakeConnection.addProposal( + // proposalIdArray, + // Buffer.from(mainnetEthProposalResponse.bytes, "hex"), + // guardianSignaturesPda, + // guardianSetIndex, + // ); + } else { + console.log("The proposal has already been created on the solana"); + } + } catch (err) { + console.error("Error:", err); + } +} + +addProposal();