diff --git a/.github/workflows/ci-pre-commit.yml b/.github/workflows/ci-pre-commit.yml index a360dc368..118c105c3 100644 --- a/.github/workflows/ci-pre-commit.yml +++ b/.github/workflows/ci-pre-commit.yml @@ -46,6 +46,8 @@ jobs: - name: Install forge dependencies 5 working-directory: contracts/evm run: forge install nomad-xyz/ExcessivelySafeCall@be417ab0c26233578b8d8f3a37b87bd1fcb4e286 --no-git --no-commit + - name: Install Anchor CLI + run: npm install -g @coral-xyz/anchor-cli@v0.30.1 - uses: pre-commit/action@v3.0.0 if: ${{ github.event_name == 'pull_request' }} with: diff --git a/Dockerfile b/Dockerfile index 8885b570f..e8a13a43b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,28 +7,11 @@ COPY contracts/evm contracts/evm WORKDIR /src/contracts/evm RUN npm install -# Build solana anchor -FROM solanalabs/solana:v1.18.18 AS solana_build -RUN apt-get update \ - && apt-get install -y \ - apt-utils \ - curl \ - gcc \ - && rm -rf /var/lib/apt/lists/* -RUN curl https://sh.rustup.rs -sSf > /tmp/rustup-init.sh \ - && chmod +x /tmp/rustup-init.sh \ - && sh /tmp/rustup-init.sh -y \ - && rm -rf /tmp/rustup-init.sh -ENV PATH="/root/.cargo/bin:${PATH}" -RUN ["/bin/bash", "-c", "source $HOME/.cargo/env"] +FROM rust:${RUST_VERSION} AS build + +# Latest version supporting anchor RUN rustup default nightly-2024-02-04 RUN cargo install --git https://github.com/coral-xyz/anchor --tag v0.30.1 anchor-cli --locked -WORKDIR /src -COPY contracts/svm contracts/svm -WORKDIR /src/contracts/svm -RUN anchor build - -FROM rust:${RUST_VERSION} AS build # Set default toolchain RUN rustup default nightly-2024-04-10 @@ -51,9 +34,6 @@ RUN forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v4.9.6 --no-gi RUN forge install Uniswap/permit2@0x000000000022D473030F116dDEE9F6B43aC78BA3 --no-git --no-commit RUN forge install nomad-xyz/ExcessivelySafeCall@be417ab0c26233578b8d8f3a37b87bd1fcb4e286 --no-git --no-commit -# Add solana dependencies -COPY --from=solana_build /src/contracts/svm/target/ /src/contracts/svm/target/ - # Build auction-server WORKDIR /src COPY auction-server auction-server diff --git a/auction-server/Cargo.toml b/auction-server/Cargo.toml index ece82c8ed..4f54cdc93 100644 --- a/auction-server/Cargo.toml +++ b/auction-server/Cargo.toml @@ -4,6 +4,9 @@ version = "0.10.0" edition = "2021" license-file = "license.txt" +[build-dependencies] +anchor-lang-idl = { version = "0.1.1", features = ["convert"] } + [dependencies] tokio = { version = "1.28", features = ["macros", "sync", "rt-multi-thread", "signal"] } tokio-stream = "0.1.14" @@ -43,7 +46,6 @@ serde_path_to_error = "0.1.16" solana-sdk = "2.0.7" bincode = "1.3.3" serde_with = "3.9.0" -anchor-lang-idl = { version = "0.1.1", features = ["convert"] } anchor-lang = "0.30.1" express-relay = { path = "../contracts/svm/programs/express_relay" } diff --git a/auction-server/build.rs b/auction-server/build.rs index 047eb9e9f..1407dafc4 100644 --- a/auction-server/build.rs +++ b/auction-server/build.rs @@ -1,13 +1,23 @@ -use std::process::Command; +use { + anchor_lang_idl::{ + convert::convert_idl, + types::{ + Idl, + IdlInstructionAccountItem, + }, + }, + std::{ + fs, + process::Command, + }, +}; -fn main() { +fn build_evm_contracts() { let contract_setup = r#" cd ../contracts/evm forge build --via-ir "#; println!("cargo:rerun-if-changed=../contracts/evm"); - println!("cargo:rerun-if-changed=migrations"); - // Build the contracts and generate the ABIs. This is required for abigen! macro expansions to work. let output = Command::new("sh") .args(["-c", contract_setup]) @@ -25,3 +35,85 @@ fn main() { ); } } + + +const SUBMIT_BID_INSTRUCTION_SVM: &str = "submit_bid"; +const PERMISSION_ACCOUNT_SVM: &str = "permission"; +const ROUTER_ACCOUNT_SVM: &str = "router"; +const IDL_LOCATION: &str = "../contracts/svm/target/idl/express_relay.json"; + +fn extract_account_position(idl: Idl, instruction_name: &str, account_name: &str) -> usize { + let instruction = idl + .instructions + .iter() + .find(|i| i.name == instruction_name) + .unwrap_or_else(|| panic!("Instruction {} not found in IDL", instruction_name)); + instruction + .accounts + .iter() + .position(|a| match a { + IdlInstructionAccountItem::Single(a) => a.name == account_name, + IdlInstructionAccountItem::Composite(a) => a.name == account_name, + }) + .unwrap_or_else(|| { + panic!( + "Account {} not found in instruction {}", + account_name, instruction_name + ) + }) +} + +fn verify_and_extract_idl_data() { + let idl_json = fs::read(IDL_LOCATION).expect("Failed to read IDL JSON"); + let express_relay_idl = + convert_idl(idl_json.as_slice()).expect("Failed to convert IDL to Rust"); + println!( + "cargo:rustc-env=SUBMIT_BID_PERMISSION_ACCOUNT_POSITION={}", + extract_account_position( + express_relay_idl.clone(), + SUBMIT_BID_INSTRUCTION_SVM, + PERMISSION_ACCOUNT_SVM, + ) + ); + println!( + "cargo:rustc-env=SUBMIT_BID_ROUTER_ACCOUNT_POSITION={}", + extract_account_position( + express_relay_idl.clone(), + SUBMIT_BID_INSTRUCTION_SVM, + ROUTER_ACCOUNT_SVM, + ) + ); +} + +fn build_svm_contracts() { + let contract_setup_svm = r#" + cd ../contracts/svm/programs/express_relay + mkdir -p ../../target/idl + anchor idl build > ../../target/idl/express_relay.json + "#; + println!("cargo:rerun-if-changed=../contracts/svm"); + // Build the svm contract and generate the IDLs. + let output = Command::new("sh") + .args(["-c", contract_setup_svm]) + .output() + .expect("Failed to run build svm contracts command"); + if !output.status.success() { + panic!( + "Failed to build svm contracts: {}", + String::from_utf8_lossy(&output.stderr) + ); + } else { + println!( + "Built all svm contracts {}", + String::from_utf8_lossy(&output.stdout) + ); + } +} + +fn main() { + println!("cargo:rerun-if-changed=migrations"); + + build_evm_contracts(); + build_svm_contracts(); + verify_and_extract_idl_data(); +} diff --git a/auction-server/src/auction.rs b/auction-server/src/auction.rs index 007082b26..520c99309 100644 --- a/auction-server/src/auction.rs +++ b/auction-server/src/auction.rs @@ -19,13 +19,20 @@ use { BidStatus, ChainStoreEvm, ChainStoreSvm, + ExpressRelaySvm, + PermissionKey, SimulatedBid, Store, }, traced_client::TracedClient, }, - ::express_relay as express_relay_svm, - anchor_lang::Discriminator, + ::express_relay::{ + self as express_relay_svm, + }, + anchor_lang::{ + AnchorDeserialize, + Discriminator, + }, anyhow::{ anyhow, Result, @@ -82,7 +89,11 @@ use { Deserializer, Serialize, }, - solana_sdk::transaction::VersionedTransaction, + solana_sdk::{ + instruction::CompiledInstruction, + pubkey::Pubkey, + transaction::VersionedTransaction, + }, sqlx::types::time::OffsetDateTime, std::{ result, @@ -880,8 +891,8 @@ pub async fn run_tracker_loop(store: Arc, chain_id: String) -> Result<()> pub fn verify_submit_bid_instruction_svm( chain_store: &ChainStoreSvm, transaction: VersionedTransaction, -) -> Result<(), RestError> { - if transaction +) -> Result { + let submit_bid_instructions: Vec = transaction .message .instructions() .iter() @@ -895,15 +906,69 @@ pub fn verify_submit_bid_instruction_svm( .data .starts_with(&express_relay_svm::instruction::SubmitBid::discriminator()) }) - .count() - != 1 - { - return Err(RestError::BadParameters( + .cloned() + .collect(); + + match submit_bid_instructions.len() { + 1 => Ok(submit_bid_instructions[0].clone()), + _ => Err(RestError::BadParameters( "Bid has to include exactly one submit_bid instruction to Express Relay program" .to_string(), - )); + )), } - Ok(()) +} + +fn extract_account_svm( + accounts: &[Pubkey], + instruction: CompiledInstruction, + position: usize, +) -> Result { + let account_position = instruction.accounts.get(position).ok_or_else(|| { + tracing::error!( + "Account position not found in instruction: {:?} - {}", + instruction, + position, + ); + RestError::BadParameters("Account not found in submit_bid instruction".to_string()) + })?; + + let account_position: usize = (*account_position).into(); + let account = accounts.get(account_position).ok_or_else(|| { + tracing::error!( + "Account not found in transaction accounts: {:?} - {}", + accounts, + account_position, + ); + RestError::BadParameters("Account not found in transaction accounts".to_string()) + })?; + + Ok(*account) +} + +fn extract_bid_data_svm( + express_relay_svm: ExpressRelaySvm, + accounts: &[Pubkey], + instruction: CompiledInstruction, +) -> Result<(u64, PermissionKey), RestError> { + let discriminator = express_relay_svm::instruction::SubmitBid::discriminator(); + let submit_bid_data = express_relay_svm::SubmitBidArgs::try_from_slice( + &instruction.data.as_slice()[discriminator.len()..], + ) + .map_err(|e| RestError::BadParameters(format!("Invalid submit_bid instruction data: {}", e)))?; + + let permission_account = extract_account_svm( + accounts, + instruction.clone(), + express_relay_svm.permission_account_position, + )?; + let router_account = extract_account_svm( + accounts, + instruction.clone(), + express_relay_svm.router_account_position, + )?; + + let concat = [permission_account.to_bytes(), router_account.to_bytes()].concat(); + Ok((submit_bid_data.bid_amount, concat.into())) } #[tracing::instrument(skip_all)] @@ -918,7 +983,13 @@ pub async fn handle_bid_svm( .get(&bid.chain_id) .ok_or(RestError::InvalidChainId)?; - verify_submit_bid_instruction_svm(chain_store, bid.transaction.clone())?; + let submit_bid_instruction = + verify_submit_bid_instruction_svm(chain_store, bid.transaction.clone())?; + let (_bid_amount, _permission_key) = extract_bid_data_svm( + store.express_relay_svm.clone(), + bid.transaction.message.static_account_keys(), + submit_bid_instruction, + )?; // TODO implement this Err(RestError::NotImplemented) diff --git a/auction-server/src/server.rs b/auction-server/src/server.rs index e9f4b69e9..ccdbe0c8c 100644 --- a/auction-server/src/server.rs +++ b/auction-server/src/server.rs @@ -27,15 +27,12 @@ use { state::{ ChainStoreEvm, ChainStoreSvm, + ExpressRelaySvm, OpportunityStore, Store, }, traced_client::TracedClient, }, - anchor_lang_idl::{ - convert::convert_idl, - types::Idl, - }, anyhow::anyhow, axum_prometheus::{ metrics_exporter_prometheus::{ @@ -68,7 +65,6 @@ use { }, std::{ collections::HashMap, - fs, sync::{ atomic::{ AtomicBool, @@ -232,12 +228,6 @@ async fn setup_chain_store( .collect() } -pub fn load_express_relay_idl() -> anyhow::Result { - let idl = fs::read("../contracts/svm/target/idl/express_relay.json")?; - convert_idl(idl.as_slice()) - .map_err(|err| anyhow!("Failed to convert express relay idl: {:?}", err)) -} - const NOTIFICATIONS_CHAN_LEN: usize = 1000; pub async fn start_server(run_options: RunOptions) -> anyhow::Result<()> { tokio::spawn(async move { @@ -267,6 +257,14 @@ pub async fn start_server(run_options: RunOptions) -> anyhow::Result<()> { }; let chains_svm = setup_chain_store_svm(config_map); + let express_relay_svm = ExpressRelaySvm { + permission_account_position: env!("SUBMIT_BID_PERMISSION_ACCOUNT_POSITION") + .parse::() + .expect("Failed to parse permission account position"), + router_account_position: env!("SUBMIT_BID_ROUTER_ACCOUNT_POSITION") + .parse::() + .expect("Failed to parse router account position"), + }; let (broadcast_sender, broadcast_receiver) = tokio::sync::broadcast::channel(NOTIFICATIONS_CHAN_LEN); @@ -312,7 +310,7 @@ pub async fn start_server(run_options: RunOptions) -> anyhow::Result<()> { secret_key: run_options.secret_key.clone(), access_tokens: RwLock::new(access_tokens), metrics_recorder: setup_metrics_recorder()?, - express_relay_idl: load_express_relay_idl()?, + express_relay_svm, }); tokio::join!( diff --git a/auction-server/src/state.rs b/auction-server/src/state.rs index 2c89332ea..962c08614 100644 --- a/auction-server/src/state.rs +++ b/auction-server/src/state.rs @@ -17,7 +17,6 @@ use { models, traced_client::TracedClient, }, - anchor_lang_idl::types::Idl, axum::Json, axum_prometheus::metrics_exporter_prometheus::PrometheusHandle, base64::{ @@ -296,6 +295,12 @@ pub struct BidStatusWithId { pub bid_status: BidStatus, } +#[derive(Clone)] +pub struct ExpressRelaySvm { + pub permission_account_position: usize, + pub router_account_position: usize, +} + pub struct Store { pub chains: HashMap, pub chains_svm: HashMap, @@ -311,7 +316,7 @@ pub struct Store { pub secret_key: String, pub access_tokens: RwLock>, pub metrics_recorder: PrometheusHandle, - pub express_relay_idl: Idl, + pub express_relay_svm: ExpressRelaySvm, } impl SimulatedBid {