diff --git a/packages/svm/programs/settler/src/constants.rs b/packages/svm/programs/settler/src/constants.rs index 92d20de..47addf0 100644 --- a/packages/svm/programs/settler/src/constants.rs +++ b/packages/svm/programs/settler/src/constants.rs @@ -4,3 +4,7 @@ pub const DEPLOYER_KEY: &str = env!( ); pub const CHAIN_ID: u32 = 507424; + +pub const DISCRIMINATOR_LEN: usize = 8; + +pub const VEC_SIZE_LEN: usize = 4; diff --git a/packages/svm/programs/settler/src/errors.rs b/packages/svm/programs/settler/src/errors.rs index ea1e3a7..89221d4 100644 --- a/packages/svm/programs/settler/src/errors.rs +++ b/packages/svm/programs/settler/src/errors.rs @@ -116,6 +116,9 @@ pub enum SettlerError { #[msg("Incorrect recipient token account: mint or authority do not match expected")] IncorrectRecipientTokenAccount, + #[msg("Incorrect fee payer token account: mint or authority do not match expected")] + IncorrectFeePayerTokenAccount, + #[msg("Incorrect user token account: mint or authority do not match expected")] IncorrectUserTokenAccount, @@ -125,6 +128,9 @@ pub enum SettlerError { #[msg("Incorrect token program account provided")] IncorrectTokenProgram, + #[msg("Incorrect user delegate")] + IncorrectUserDelegate, + #[msg("Math Error")] MathError, } diff --git a/packages/svm/programs/settler/src/instructions/create_intent.rs b/packages/svm/programs/settler/src/instructions/create_intent.rs index ba004c4..950663e 100644 --- a/packages/svm/programs/settler/src/instructions/create_intent.rs +++ b/packages/svm/programs/settler/src/instructions/create_intent.rs @@ -8,12 +8,11 @@ use crate::{ }, errors::SettlerError, state::Intent, - types::{IntentEvent, OpType, TokenFee}, + types::{Operation, TokenFee}, }; #[derive(Accounts)] -// TODO: can we optimize this deser? we just need the three Vec for their length -#[instruction(intent_hash: [u8; 32], data: Vec, max_fees: Vec, events: Vec, min_validations: u16)] +#[instruction(intent_hash: [u8; 32], operations: Vec, max_fees: Vec, min_validations: u16)] pub struct CreateIntent<'info> { #[account(mut)] pub solver: Signer<'info>, @@ -30,10 +29,12 @@ pub struct CreateIntent<'info> { seeds = [b"intent", intent_hash.as_ref()], bump, payer = solver, - space = Intent::total_size(data.len(), max_fees.len(), &events, min_validations.max(controller_settings.min_validations))? + space = Intent::total_size( + max_fees.len(), + &operations, + min_validations.max(controller_settings.min_validations) + )? )] - // TODO: change to AccountLoader? - // TODO: init within the handler body to save compute? pub intent: Box>, #[account( @@ -56,12 +57,10 @@ pub struct CreateIntent<'info> { pub fn create_intent( ctx: Context, intent_hash: [u8; 32], - data: Vec, + operations: Vec, max_fees: Vec, - events: Vec, min_validations: u16, - op: OpType, - user: Pubkey, + fee_payer: Pubkey, nonce: [u8; 32], deadline: u64, is_final: bool, @@ -75,18 +74,16 @@ pub fn create_intent( let intent = &mut ctx.accounts.intent; let controller_min_validations = ctx.accounts.controller_settings.min_validations; - intent.op = op; - intent.user = user; + intent.fee_payer = fee_payer; intent.creator = ctx.accounts.solver.key(); intent.hash = intent_hash; intent.nonce = nonce; intent.deadline = deadline; intent.min_validations = min_validations.max(controller_min_validations); intent.is_final = is_final; - intent.data = data; intent.max_fees = max_fees; - intent.events = events; intent.validators = vec![]; + intent.operations = operations; intent.bump = ctx.bumps.intent; Ok(()) diff --git a/packages/svm/programs/settler/src/instructions/execute_proposal.rs b/packages/svm/programs/settler/src/instructions/execute_proposal.rs index 56ba288..8aab19d 100644 --- a/packages/svm/programs/settler/src/instructions/execute_proposal.rs +++ b/packages/svm/programs/settler/src/instructions/execute_proposal.rs @@ -4,7 +4,7 @@ use crate::{ controller::{self, accounts::EntityRegistry, types::EntityType}, errors::SettlerError, state::{FulfilledIntent, Intent, Proposal}, - types::IntentEvent, + types::OperationEvent, utils::{handle_intent_execution, pay_solver_fees}, }; @@ -55,12 +55,27 @@ pub struct ExecuteProposal<'info> { )] pub fulfilled_intent: Box>, - #[account(seeds = [b"delegate", intent.user.key().as_ref()], bump)] - pub delegate: SystemAccount<'info>, + #[account(seeds = [b"delegate", intent.fee_payer.key().as_ref()], bump)] + pub fee_payer_delegate: SystemAccount<'info>, pub system_program: Program<'info, System>, } +//////////////////////////////////////////////////////// +// REMAINING ACCOUNTS // +// // +// [token_program, token_2022_program] // +// // +// for operation in intent.operations: // +// [user_delegate(operation.user)] // +// // +// for transfer in operation.transfers: // +// [token_mint, recipient, recipient_ta, user_ta] // +// // +// for each fee in proposal.fees / intent.max_fees: // +// [fee_token_mint, solver_ta, fee_payer_ta] // +//////////////////////////////////////////////////////// + pub fn execute_proposal<'info>( ctx: Context<'_, '_, '_, 'info, ExecuteProposal<'info>>, ) -> Result<()> { @@ -85,33 +100,26 @@ pub fn execute_proposal<'info>( handle_intent_execution( intent, proposal, - &ctx.accounts.delegate.clone(), &mut remaining_accounts_iter, token_program, token_2022_program, - ctx.bumps.delegate, + ctx.program_id, )?; - intent.events.iter().for_each(|event| { - emit!(IntentEventEvent { - event: event.clone() - }) - }); - pay_solver_fees( &mut remaining_accounts_iter, intent, proposal, token_program, token_2022_program, - &ctx.accounts.delegate.clone(), - ctx.bumps.delegate, + &ctx.accounts.fee_payer_delegate.clone(), + ctx.bumps.fee_payer_delegate, )?; Ok(()) } #[event] -pub struct IntentEventEvent { - event: IntentEvent, +pub struct OperationEventEvent { + pub event: OperationEvent, } diff --git a/packages/svm/programs/settler/src/instructions/extend_intent.rs b/packages/svm/programs/settler/src/instructions/extend_intent.rs index ac56959..fdd7b78 100644 --- a/packages/svm/programs/settler/src/instructions/extend_intent.rs +++ b/packages/svm/programs/settler/src/instructions/extend_intent.rs @@ -3,11 +3,11 @@ use anchor_lang::prelude::*; use crate::{ errors::SettlerError, state::Intent, - types::{IntentEvent, TokenFee}, + types::{Operation, TokenFee}, }; #[derive(Accounts)] -#[instruction(more_data: Option>, more_max_fees: Option>, more_events: Option>)] +#[instruction(more_max_fees: Option>, more_operations: Option>)] pub struct ExtendIntent<'info> { #[account(mut)] pub creator: Signer<'info>, @@ -18,7 +18,11 @@ pub struct ExtendIntent<'info> { constraint = !intent.is_final @ SettlerError::IntentIsFinal, constraint = intent.deadline > Clock::get()?.unix_timestamp as u64 @ SettlerError::IntentIsExpired, realloc = - Intent::extended_size(intent.to_account_info().data_len(), &more_data, &more_max_fees, &more_events)?, + Intent::extended_size( + intent.to_account_info().data_len(), + &more_max_fees, + &more_operations + )?, realloc::payer = creator, realloc::zero = true )] @@ -29,23 +33,18 @@ pub struct ExtendIntent<'info> { pub fn extend_intent( ctx: Context, - more_data: Option>, more_max_fees: Option>, - more_events: Option>, + more_operations: Option>, finalize: bool, ) -> Result<()> { let intent = &mut ctx.accounts.intent; - if let Some(_more_data) = more_data { - intent.data.extend_from_slice(&_more_data); - } - if let Some(_more_max_fees) = more_max_fees { intent.max_fees.extend_from_slice(&_more_max_fees); } - if let Some(_more_events) = more_events { - intent.events.extend_from_slice(&_more_events); + if let Some(_more_operations) = more_operations { + intent.operations.extend_from_slice(&_more_operations); } if finalize { diff --git a/packages/svm/programs/settler/src/lib.rs b/packages/svm/programs/settler/src/lib.rs index c2229d5..b1b4c91 100644 --- a/packages/svm/programs/settler/src/lib.rs +++ b/packages/svm/programs/settler/src/lib.rs @@ -45,12 +45,10 @@ pub mod settler { pub fn create_intent( ctx: Context, intent_hash: [u8; 32], - data: Vec, + operations: Vec, max_fees: Vec, - events: Vec, min_validations: u16, - op: OpType, - user: Pubkey, + fee_payer: Pubkey, nonce: [u8; 32], deadline: u64, is_final: bool, @@ -58,12 +56,10 @@ pub mod settler { instructions::create_intent( ctx, intent_hash, - data, + operations, max_fees, - events, min_validations, - op, - user, + fee_payer, nonce, deadline, is_final, @@ -88,12 +84,11 @@ pub mod settler { pub fn extend_intent( ctx: Context, - more_data: Option>, more_max_fees: Option>, - more_events: Option>, + more_operations: Option>, finalize: bool, ) -> Result<()> { - instructions::extend_intent(ctx, more_data, more_max_fees, more_events, finalize) + instructions::extend_intent(ctx, more_max_fees, more_operations, finalize) } pub fn initialize(ctx: Context, domain: Eip712Domain) -> Result<()> { diff --git a/packages/svm/programs/settler/src/state/intent.rs b/packages/svm/programs/settler/src/state/intent.rs index e548c0b..0c37283 100644 --- a/packages/svm/programs/settler/src/state/intent.rs +++ b/packages/svm/programs/settler/src/state/intent.rs @@ -1,14 +1,14 @@ use anchor_lang::prelude::*; use crate::{ - types::{IntentEvent, OpType, TokenFee}, + constants::{DISCRIMINATOR_LEN, VEC_SIZE_LEN}, + types::{Operation, TokenFee}, utils::{add, mul, sub}, }; #[account] pub struct Intent { - pub op: OpType, - pub user: Pubkey, + pub fee_payer: Pubkey, pub creator: Pubkey, pub hash: [u8; 32], pub nonce: [u8; 32], @@ -16,17 +16,15 @@ pub struct Intent { pub min_validations: u16, pub is_final: bool, pub validators: Vec<[u8; 20]>, // TODO: how to store more efficiently? - pub data: Vec, pub max_fees: Vec, - pub events: Vec, + pub operations: Vec, pub bump: u8, } impl Intent { /// Doesn't take into account size of variable fields pub const BASE_LEN: usize = - 1 + // op - 32 + // user + 32 + // fee_payer 32 + // creator 32 + // hash 32 + // nonce @@ -39,59 +37,50 @@ impl Intent { pub const VALIDATOR_ADDRESS_SIZE: usize = 20; pub fn total_size( - data_len: usize, max_fees_len: usize, - events: &[IntentEvent], + operations: &[Operation], min_validations: u16, ) -> Result { - let size = add(8, Intent::BASE_LEN)?; - let size = add(size, Intent::data_size(data_len)?)?; - let size = add(size, Intent::max_fees_size(max_fees_len)?)?; - let size = add(size, Intent::events_size(events)?)?; + let size = add(DISCRIMINATOR_LEN, Intent::BASE_LEN)?; let size = add(size, Intent::validators_size(min_validations)?)?; + let size = add(size, Intent::max_fees_size(max_fees_len)?)?; + let size = add(size, Intent::operations_size(operations)?)?; Ok(size) } - pub fn data_size(len: usize) -> Result { - add(4, len) + pub fn validators_size(min_validations: u16) -> Result { + add( + VEC_SIZE_LEN, + mul(min_validations as usize, Self::VALIDATOR_ADDRESS_SIZE)?, + ) } pub fn max_fees_size(len: usize) -> Result { - add(4, mul(TokenFee::INIT_SPACE, len)?) + add(VEC_SIZE_LEN, mul(TokenFee::INIT_SPACE, len)?) } - pub fn events_size(events: &[IntentEvent]) -> Result { - let sum = events - .iter() - .try_fold(0usize, |acc, e| add(acc, e.size()))?; - add(4, sum) - } - - pub fn validators_size(min_validations: u16) -> Result { + pub fn operations_size(operations: &[Operation]) -> Result { add( - 4, - mul(min_validations as usize, Self::VALIDATOR_ADDRESS_SIZE)?, + VEC_SIZE_LEN, + operations + .iter() + .try_fold(0usize, |acc, op| add(acc, op.total_size()?))?, ) } pub fn extended_size( size: usize, - more_data: &Option>, more_max_fees: &Option>, - more_events: &Option>, + more_operations: &Option>, ) -> Result { let mut size = size; - if let Some(v) = more_data { - size = add(size, sub(Intent::data_size(v.len())?, 4)?)?; - } - if let Some(v) = more_max_fees { - size = add(size, sub(Intent::max_fees_size(v.len())?, 4)?)?; + size = add(size, sub(Intent::max_fees_size(v.len())?, VEC_SIZE_LEN)?)?; } - if let Some(v) = more_events { - size = add(size, sub(Intent::events_size(v)?, 4)?)?; + if let Some(v) = more_operations { + size = add(size, sub(Intent::operations_size(v)?, VEC_SIZE_LEN)?)?; } Ok(size) diff --git a/packages/svm/programs/settler/src/state/proposal.rs b/packages/svm/programs/settler/src/state/proposal.rs index 781dc18..0aabb6d 100644 --- a/packages/svm/programs/settler/src/state/proposal.rs +++ b/packages/svm/programs/settler/src/state/proposal.rs @@ -1,6 +1,9 @@ use anchor_lang::prelude::*; -use crate::utils::{add, mul, sub, Proposal as Eip712Proposal}; +use crate::{ + constants::{DISCRIMINATOR_LEN, VEC_SIZE_LEN}, + utils::{add, mul, sub, Proposal as Eip712Proposal}, +}; #[account] pub struct Proposal { @@ -26,7 +29,7 @@ impl Proposal { ; pub fn total_size(instructions: &[ProposalInstruction], fees_len: usize) -> Result { - let size = add(8, Proposal::BASE_LEN)?; + let size = add(DISCRIMINATOR_LEN, Proposal::BASE_LEN)?; let size = add(size, Proposal::instructions_size(instructions)?)?; let size = add(size, Proposal::fees_size(fees_len)?)?; Ok(size) @@ -36,17 +39,17 @@ impl Proposal { let sum = instructions .iter() .try_fold(0usize, |acc, ix| add(acc, ix.size()))?; - add(4, sum) + add(VEC_SIZE_LEN, sum) } pub fn fees_size(len: usize) -> Result { - add(4, mul(8, len)?) + add(VEC_SIZE_LEN, mul(8, len)?) } pub fn extended_size(size: usize, more_instructions: &[ProposalInstruction]) -> Result { sub( add(size, Proposal::instructions_size(more_instructions)?)?, - 4, + VEC_SIZE_LEN, ) } @@ -57,7 +60,7 @@ impl Proposal { intent: intent_hash.into(), solver: self.creator.to_string(), deadline: U256::from(self.deadline), - data: vec![].into(), + datas: vec![vec![].into()], fees: self.fees.iter().map(|&fee| U256::from(fee)).collect(), } } @@ -72,8 +75,8 @@ pub struct ProposalInstruction { impl ProposalInstruction { pub fn size(&self) -> usize { - let accounts_size = 4 + self.accounts.len() * (32 + 1 + 1); - let data_size = 4 + self.data.len(); + let accounts_size = VEC_SIZE_LEN + self.accounts.len() * (32 + 1 + 1); + let data_size = VEC_SIZE_LEN + self.data.len(); 32 + accounts_size + data_size } diff --git a/packages/svm/programs/settler/src/types/mod.rs b/packages/svm/programs/settler/src/types/mod.rs index 1b8b438..ac7dc81 100644 --- a/packages/svm/programs/settler/src/types/mod.rs +++ b/packages/svm/programs/settler/src/types/mod.rs @@ -1,11 +1,7 @@ pub mod eip712_domain; -pub mod intent_data; -pub mod intent_event; -pub mod op_type; +pub mod operations; pub mod token_fee; pub use eip712_domain::*; -pub use intent_data::*; -pub use intent_event::*; -pub use op_type::*; +pub use operations::*; pub use token_fee::*; diff --git a/packages/svm/programs/settler/src/types/intent_data/mod.rs b/packages/svm/programs/settler/src/types/operations/data/mod.rs similarity index 100% rename from packages/svm/programs/settler/src/types/intent_data/mod.rs rename to packages/svm/programs/settler/src/types/operations/data/mod.rs diff --git a/packages/svm/programs/settler/src/types/intent_data/transfer.rs b/packages/svm/programs/settler/src/types/operations/data/transfer.rs similarity index 100% rename from packages/svm/programs/settler/src/types/intent_data/transfer.rs rename to packages/svm/programs/settler/src/types/operations/data/transfer.rs diff --git a/packages/svm/programs/settler/src/types/operations/mod.rs b/packages/svm/programs/settler/src/types/operations/mod.rs new file mode 100644 index 0000000..266b7d6 --- /dev/null +++ b/packages/svm/programs/settler/src/types/operations/mod.rs @@ -0,0 +1,9 @@ +pub mod data; +pub mod op_type; +pub mod operation; +pub mod operation_event; + +pub use data::*; +pub use op_type::*; +pub use operation::*; +pub use operation_event::*; diff --git a/packages/svm/programs/settler/src/types/op_type.rs b/packages/svm/programs/settler/src/types/operations/op_type.rs similarity index 71% rename from packages/svm/programs/settler/src/types/op_type.rs rename to packages/svm/programs/settler/src/types/operations/op_type.rs index cad0715..db34384 100644 --- a/packages/svm/programs/settler/src/types/op_type.rs +++ b/packages/svm/programs/settler/src/types/operations/op_type.rs @@ -6,5 +6,7 @@ pub enum OpType { Swap = 0, Transfer = 1, EvmCall = 2, - SvmCall = 3, + CrossChainSwap = 3, + EvmDynamicCall = 4, + SvmCall = 5, } diff --git a/packages/svm/programs/settler/src/types/operations/operation.rs b/packages/svm/programs/settler/src/types/operations/operation.rs new file mode 100644 index 0000000..01be589 --- /dev/null +++ b/packages/svm/programs/settler/src/types/operations/operation.rs @@ -0,0 +1,36 @@ +use anchor_lang::prelude::*; + +use crate::{ + constants::VEC_SIZE_LEN, types::{OpType, OperationEvent}, utils::add +}; + +#[derive(Clone, AnchorDeserialize, AnchorSerialize)] +pub struct Operation { + pub op_type: OpType, + pub user: Pubkey, + pub data: Vec, + pub events: Vec, +} + +impl Operation { + pub const BASE_LEN: usize = 1 + 32; + + pub fn total_size(&self) -> Result { + let size = Operation::BASE_LEN; + let size = add(size, self.data_size()?)?; + let size = add(size, self.events_size()?)?; + Ok(size) + } + + pub fn data_size(&self) -> Result { + add(VEC_SIZE_LEN, self.data.len()) + } + + pub fn events_size(&self) -> Result { + let sum = self + .events + .iter() + .try_fold(0usize, |acc, e| add(acc, e.size()))?; + add(VEC_SIZE_LEN, sum) + } +} diff --git a/packages/svm/programs/settler/src/types/intent_event.rs b/packages/svm/programs/settler/src/types/operations/operation_event.rs similarity index 80% rename from packages/svm/programs/settler/src/types/intent_event.rs rename to packages/svm/programs/settler/src/types/operations/operation_event.rs index 0eb00c4..93d68d7 100644 --- a/packages/svm/programs/settler/src/types/intent_event.rs +++ b/packages/svm/programs/settler/src/types/operations/operation_event.rs @@ -1,12 +1,12 @@ use anchor_lang::prelude::*; #[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct IntentEvent { +pub struct OperationEvent { pub topic: [u8; 32], pub data: Vec, } -impl IntentEvent { +impl OperationEvent { pub fn size(&self) -> usize { 32 + 4 + self.data.len() } diff --git a/packages/svm/programs/settler/src/utils/execution/misc.rs b/packages/svm/programs/settler/src/utils/execution/misc.rs index c1e6fd9..7639141 100644 --- a/packages/svm/programs/settler/src/utils/execution/misc.rs +++ b/packages/svm/programs/settler/src/utils/execution/misc.rs @@ -9,24 +9,63 @@ use core::slice::Iter; use crate::{ errors::SettlerError, + instructions::OperationEventEvent, state::{Intent, Proposal}, - types::OpType, + types::{OpType, Operation}, utils::handle_transfer, }; pub fn handle_intent_execution<'info>( intent: &Intent, proposal: &Proposal, + remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, + token_program: &AccountInfo<'info>, + token_2022_program: &AccountInfo<'info>, + program_id: &Pubkey, +) -> Result<()> { + intent.operations.iter().try_for_each(|operation| { + let user_delegate = next_account_info(remaining_accounts_iter)?; + let (expected_delegate, delegate_bump) = + Pubkey::find_program_address(&[b"delegate", operation.user.as_ref()], program_id); + + require_keys_eq!( + user_delegate.key(), + expected_delegate, + SettlerError::IncorrectUserDelegate + ); + + handle_operation_execution( + operation, + proposal, + user_delegate, + remaining_accounts_iter, + token_program, + token_2022_program, + delegate_bump, + )?; + + operation.events.iter().for_each(|event| { + emit!(OperationEventEvent { + event: event.clone() + }) + }); + + Ok(()) + }) +} + +pub fn handle_operation_execution<'info>( + operation: &Operation, + proposal: &Proposal, delegate: &AccountInfo<'info>, remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, token_program: &AccountInfo<'info>, token_2022_program: &AccountInfo<'info>, delegate_bump: u8, ) -> Result<()> { - match intent.op { - OpType::Swap => err!(SettlerError::UnsupportedIntentOp), + match operation.op_type { OpType::Transfer => handle_transfer( - intent, + operation, proposal, delegate, remaining_accounts_iter, @@ -34,8 +73,7 @@ pub fn handle_intent_execution<'info>( token_2022_program, delegate_bump, ), - OpType::EvmCall => err!(SettlerError::UnsupportedIntentOp), - OpType::SvmCall => err!(SettlerError::UnsupportedIntentOp), + _ => err!(SettlerError::UnsupportedIntentOp), } } @@ -54,10 +92,10 @@ pub fn handle_intent_execution<'info>( /// /// #[account( /// mut, -/// token::owner = user, +/// token::owner = fee_payer, /// token::mint = fee_token, /// )] -/// pub user_ta: Account<'info, ITokenAccount>, +/// pub fee_payer_ta: Account<'info, ITokenAccount>, /// pub fn pay_solver_fees<'info>( remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, @@ -68,7 +106,7 @@ pub fn pay_solver_fees<'info>( delegate: &AccountInfo<'info>, delegate_bump: u8, ) -> Result<()> { - let delegate_seeds: &[&[u8]] = &[b"delegate", intent.user.as_ref(), &[delegate_bump]]; + let delegate_seeds: &[&[u8]] = &[b"delegate", intent.fee_payer.as_ref(), &[delegate_bump]]; let signer_seeds = [delegate_seeds]; for (fee, max_fee) in proposal.fees.iter().zip(&intent.max_fees) { @@ -102,13 +140,13 @@ pub fn pay_solver_fees<'info>( ); require_keys_eq!( user_ta.owner, - intent.user, - SettlerError::IncorrectUserTokenAccount + intent.fee_payer, + SettlerError::IncorrectFeePayerTokenAccount ); require_keys_eq!( user_ta.mint, max_fee.token, - SettlerError::IncorrectUserTokenAccount + SettlerError::IncorrectFeePayerTokenAccount ); require_keys_eq!( solver_ta.owner, diff --git a/packages/svm/programs/settler/src/utils/execution/transfer.rs b/packages/svm/programs/settler/src/utils/execution/transfer.rs index c3e1652..997c85e 100644 --- a/packages/svm/programs/settler/src/utils/execution/transfer.rs +++ b/packages/svm/programs/settler/src/utils/execution/transfer.rs @@ -7,11 +7,15 @@ use anchor_spl::{ use core::slice::Iter; use crate::{ - constants::CHAIN_ID, errors::SettlerError, state::{Intent, Proposal}, types::{SvmTransfer, SvmTransferIntentData}, utils::check_owner_is_token_program + constants::CHAIN_ID, + errors::SettlerError, + state::Proposal, + types::{Operation, SvmTransfer, SvmTransferIntentData}, + utils::check_owner_is_token_program, }; pub fn handle_transfer<'info>( - intent: &Intent, + operation: &Operation, proposal: &Proposal, delegate: &AccountInfo<'info>, remaining_accounts_iter: &mut Iter<'_, AccountInfo<'info>>, @@ -19,13 +23,13 @@ pub fn handle_transfer<'info>( token_2022_program: &AccountInfo<'info>, delegate_bump: u8, ) -> Result<()> { - let decoded_intent_data = SvmTransferIntentData::try_from_slice(&intent.data)?; + let decoded_intent_data = SvmTransferIntentData::try_from_slice(&operation.data)?; validate_transfer(proposal, &decoded_intent_data)?; - let delegate_seeds: &[&[u8]] = &[b"delegate", intent.user.as_ref(), &[delegate_bump]]; + let delegate_seeds: &[&[u8]] = &[b"delegate", operation.user.as_ref(), &[delegate_bump]]; execute_transfers( - intent.user, + operation.user, delegate, remaining_accounts_iter, &decoded_intent_data, @@ -211,7 +215,11 @@ fn check_token_accounts( } fn validate_transfer(proposal: &Proposal, intent_data: &SvmTransferIntentData) -> Result<()> { - require_eq!(intent_data.chain_id, CHAIN_ID, SettlerError::IncorrectChainId); + require_eq!( + intent_data.chain_id, + CHAIN_ID, + SettlerError::IncorrectChainId + ); require_eq!( proposal.instructions.len(), 0, diff --git a/packages/svm/programs/settler/src/utils/signatures/eip712/proposal.rs b/packages/svm/programs/settler/src/utils/signatures/eip712/proposal.rs index e045631..407cacb 100644 --- a/packages/svm/programs/settler/src/utils/signatures/eip712/proposal.rs +++ b/packages/svm/programs/settler/src/utils/signatures/eip712/proposal.rs @@ -7,7 +7,7 @@ solidity! { bytes32 intent; string solver; uint256 deadline; - bytes data; + bytes[] datas; uint256[] fees; } } diff --git a/packages/svm/tests/helpers/intents.ts b/packages/svm/tests/helpers/intents.ts index 2372d4a..bcf716f 100644 --- a/packages/svm/tests/helpers/intents.ts +++ b/packages/svm/tests/helpers/intents.ts @@ -21,6 +21,8 @@ export type IntentAccount = NonNullable['acc export type CreateIntentOptions = Partial & { isFinal?: boolean } +export type CreateOperationParams = CreateIntentParams['operations'][number] + /** * Generate a random 32-byte hex string for intent hash */ @@ -39,6 +41,13 @@ export function createIntentParams(client: LiteSVM, params: Partial = {}): CreateOperationParams { + return { + ...DEFAULT_OPERATION_PARAMS, + ...params, + } +} + /** * Create a test intent with configurable parameters */ @@ -128,24 +137,29 @@ export function mapIntentFeesToTokenFees(intent: IntentAccount): SvmTokenFee[] { })) } -const DEFAULT_CREATE_INTENT_PARAMS: Omit = { - op: OpType.Transfer, +const DEFAULT_OPERATION_PARAMS: CreateOperationParams = { + opType: OpType.Transfer, + data: DEFAULT_DATA_HEX, + events: [ + { + topic: DEFAULT_TOPIC_HEX, + data: DEFAULT_EVENT_DATA_HEX, + }, + ], user: randomPubkey(), +} + +const DEFAULT_CREATE_INTENT_PARAMS: Omit = { + feePayer: randomPubkey(), nonce: generateNonce(), minValidations: DEFAULT_MIN_VALIDATIONS, - data: DEFAULT_DATA_HEX, maxFees: [ { token: randomPubkey(), amount: DEFAULT_MAX_FEE.toString(), }, ], - events: [ - { - topic: DEFAULT_TOPIC_HEX, - data: DEFAULT_EVENT_DATA_HEX, - }, - ], + operations: [DEFAULT_OPERATION_PARAMS], } function getDefaultCreateIntentParams(client: LiteSVM): CreateIntentParams { diff --git a/packages/svm/tests/helpers/signatures.ts b/packages/svm/tests/helpers/signatures.ts index 53ba180..057e337 100644 --- a/packages/svm/tests/helpers/signatures.ts +++ b/packages/svm/tests/helpers/signatures.ts @@ -33,7 +33,7 @@ export async function createAxiaSignature( intent: Buffer.from(intentHash), solver: proposal.creator.toString(), deadline: proposal.deadline.toString(), - data: '0x', // TODO + datas: ['0x'], // TODO fees: proposal.fees.map((fee) => fee.toString()), } diff --git a/packages/svm/tests/settler.test.ts b/packages/svm/tests/settler.test.ts index ccac8a8..a66a2ac 100644 --- a/packages/svm/tests/settler.test.ts +++ b/packages/svm/tests/settler.test.ts @@ -20,11 +20,11 @@ import { SolanaEip712Domain, SvmController, SvmSettler, - TransferIntentData, - TransferIntentTransfer, + TransferOperationData, + TransferOperationTransfer, ValidatorSigner, } from '@mimicprotocol/sdk' -import { svmEncodeTransferIntent } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' +import { svmEncodeTransferOperation } from '@mimicprotocol/sdk/dist/shared/codec/chains/svm' import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' import { AccountMeta, @@ -54,6 +54,7 @@ import { createAxiaSignature, CreateIntentOptions, createIntentParams, + createOperationParams, CreateProposalOptions, createProposalParams, createSignerInstructionAccount, @@ -62,7 +63,6 @@ import { createValidatedIntent, createValidatorSignature, createWritableInstructionAccount, - DEFAULT_DATA_HEX, DEFAULT_MAX_FEE, EMPTY_DATA_HEX, ethAddressToByteArray, @@ -90,7 +90,7 @@ import { WARP_TIME_SHORT, } from './helpers' import { approveDelegate, createFundedAta, createMint, getAtaBalance, revokeDelegate } from './helpers/spl' -import { makeTxSignAndSend, warpSeconds } from './utils' +import { getLamports, makeTxSignAndSend, warpSeconds } from './utils' describe('Settler', () => { let client: LiteSVM @@ -258,128 +258,163 @@ describe('Settler', () => { context('when caller is an allowlisted solver', () => { context('when intent data is valid', () => { context('when intent does not exist', () => { - context('when creating a basic intent', () => { - const itWorksAsExpected = (minValidations: number) => { - const intentOptions: CreateIntentOptions = { - op: OpType.Transfer, - user: randomPubkey(), - nonce: generateNonce(), - deadline: '10000', - minValidations, - data: TEST_DATA_HEX_1, - maxFees: [ - { - token: randomPubkey(), - amount: '1000', - }, - ], - events: [ - { - topic: randomHex(32).slice(2), - data: randomHex(100).slice(2), - }, - ], - isFinal: true, + context('when intent has one operation', () => { + context('when creating a basic intent', () => { + const itWorksAsExpected = (minValidations: number) => { + const intentOptions: CreateIntentOptions = { + feePayer: randomPubkey(), + nonce: generateNonce(), + deadline: '10000', + minValidations, + maxFees: [ + { + token: randomPubkey(), + amount: '1000', + }, + ], + operations: [ + { + opType: OpType.Transfer, + user: randomPubkey(), + data: TEST_DATA_HEX_1, + events: [ + { + topic: randomHex(32).slice(2), + data: randomHex(100).slice(2), + }, + ], + }, + ], + isFinal: true, + } + + it('creates the intent with correct properties', async () => { + intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) + const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + + expect(intent.feePayer.toString()).to.be.eq(intentOptions.feePayer!.toString()) + expect(intent.creator.toString()).to.be.eq(solver.publicKey.toString()) + expect(bytesToHex(Buffer.from(intent.nonce))).to.be.eq(intentOptions.nonce) + expect(intent.deadline.toString()).to.be.eq(intentOptions.deadline) + expect(intent.minValidations).to.be.eq( + Math.max(controllerMinValidations, intentOptions.minValidations ?? 0) + ) + expect(intent.isFinal).to.be.true + expect(intent.maxFees.length).to.be.eq(1) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.validators.length).to.be.eq(0) + + expect(intent.operations[0].opType).to.deep.include({ transfer: {} }) + expect(intent.operations[0].events.length).to.be.eq(1) + expect(Buffer.from(intent.operations[0].data).toString('hex')).to.be.eq( + intentOptions.operations![0].data + ) + expect(Buffer.from(intent.operations[0].events[0].data).toString('hex')).to.be.eq( + intentOptions.operations![0].events![0].data + ) + }) } - it('creates the intent with correct properties', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + const controllerMinValidations = 3 - expect(intent.op).to.deep.include({ transfer: {} }) - expect(intent.user.toString()).to.be.eq(intentOptions.user!.toString()) - expect(intent.creator.toString()).to.be.eq(solver.publicKey.toString()) - expect(bytesToHex(Buffer.from(intent.nonce))).to.be.eq(intentOptions.nonce) - expect(intent.deadline.toString()).to.be.eq(intentOptions.deadline) - expect(intent.minValidations).to.be.eq( - Math.max(controllerMinValidations, intentOptions.minValidations ?? 0) - ) - expect(intent.isFinal).to.be.true - expect(Buffer.from(intent.data).toString('hex')).to.be.eq(intentOptions.data) - expect(intent.maxFees.length).to.be.eq(1) - expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) - expect(intent.events.length).to.be.eq(1) - expect(intent.validators.length).to.be.eq(0) - expect(Buffer.from(intent.events[0].data).toString('hex')).to.be.eq(intentOptions.events![0].data) + before('set Controller min validations to 3 for tests', async () => { + const ix = await controllerSdk.setMinValidationsIx(controllerMinValidations) + await makeTxSignAndSend(adminProvider, ix) }) - } - const controllerMinValidations = 3 + context("when intent minValidations are less than Controller's", () => { + itWorksAsExpected(controllerMinValidations - 1) + }) - before('set Controller min validations to 3 for tests', async () => { - const ix = await controllerSdk.setMinValidationsIx(controllerMinValidations) - await makeTxSignAndSend(adminProvider, ix) - }) + context("when intent minValidations are more than Controller's", () => { + itWorksAsExpected(controllerMinValidations + 1) + }) - context("when intent minValidations are less than Controller's", () => { - itWorksAsExpected(controllerMinValidations - 1) - }) + context("when intent minValidations are equal to Controller's", () => { + itWorksAsExpected(controllerMinValidations) + }) - context("when intent minValidations are more than Controller's", () => { - itWorksAsExpected(controllerMinValidations + 1) + after('restore Controller min validations to 1 for future tests', async () => { + const ix = await controllerSdk.setMinValidationsIx(1) + await makeTxSignAndSend(adminProvider, ix) + }) }) - context("when intent minValidations are equal to Controller's", () => { - itWorksAsExpected(controllerMinValidations) - }) + context('when creating an intent with an operation with empty data', () => { + intentHash = generateIntentHash() + const intentOptions: CreateIntentOptions = { + operations: [createOperationParams({ data: EMPTY_DATA_HEX })], + } - after('restore Controller min validations to 1 for future tests', async () => { - const ix = await controllerSdk.setMinValidationsIx(1) - await makeTxSignAndSend(adminProvider, ix) + it('creates the intent', async () => { + intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) + const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.operations[0].opType).to.deep.include({ transfer: {} }) + expect(Buffer.from(intent.operations[0].data).toString('hex')).to.be.eq(EMPTY_DATA_HEX) + expect(intent.isFinal).to.be.true + }) }) - }) - context('when creating an intent with empty data', () => { - intentHash = generateIntentHash() - const intentOptions: CreateIntentOptions = { - data: EMPTY_DATA_HEX, - } + context('when creating an intent with an operation with empty events', () => { + intentHash = generateIntentHash() + const intentOptions: CreateIntentOptions = { + operations: [createOperationParams({ events: [] })], + } - it('creates the intent', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.op).to.deep.include({ transfer: {} }) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq(EMPTY_DATA_HEX) - expect(intent.isFinal).to.be.true + it('creates the intent', async () => { + intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) + const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.operations[0].events.length).to.be.eq(0) + }) }) - }) - context('when creating an intent with empty events', () => { - intentHash = generateIntentHash() - const intentOptions: CreateIntentOptions = { - events: [], - } + context('when creating an intent with is_final true', () => { + intentHash = generateIntentHash() + const intentOptions: CreateIntentOptions = { + isFinal: true, + } - it('creates the intent', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.events.length).to.be.eq(0) + it('creates the intent', async () => { + intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) + const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.isFinal).to.be.true + }) }) - }) - context('when creating an intent with is_final true', () => { - intentHash = generateIntentHash() - const intentOptions: CreateIntentOptions = { - isFinal: true, - } + context('when creating an intent with is_final false', () => { + intentHash = generateIntentHash() + const intentOptions: CreateIntentOptions = { + isFinal: false, + } - it('creates the intent', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.isFinal).to.be.true + it('creates the intent', async () => { + intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) + const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) + expect(intent.isFinal).to.be.false + }) }) }) - context('when creating an intent with is_final false', () => { - intentHash = generateIntentHash() - const intentOptions: CreateIntentOptions = { - isFinal: false, - } + context('when intent has no operations', () => { + beforeEach(() => { + intentOptions = { operations: [], isFinal: true } + }) - it('creates the intent', async () => { + it('creates the intent with correct properties', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, intentOptions) const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.isFinal).to.be.false + + const expectedIntent = createIntentParams(client, intentOptions) + expect(intent.feePayer.toString()).to.be.eq(expectedIntent.feePayer!.toString()) + expect(intent.creator.toString()).to.be.eq(solver.publicKey.toString()) + expect(bytesToHex(Buffer.from(intent.nonce))).to.be.eq(expectedIntent.nonce) + expect(intent.deadline.toString()).to.be.eq(expectedIntent.deadline) + expect(intent.minValidations).to.be.eq(Math.max(1, expectedIntent.minValidations ?? 0)) + expect(intent.isFinal).to.be.true + expect(intent.maxFees.length).to.be.eq(1) + expect(intent.maxFees[0].amount.toNumber()).to.be.eq(1000) + expect(intent.validators.length).to.be.eq(0) + expect(intent.operations.length).to.be.eq(0) }) }) }) @@ -433,33 +468,35 @@ describe('Settler', () => { // Build ix with invalid hash const params = createIntentParams(client, intentOptions) - const { op, user, nonce, deadline, minValidations, data, maxFees, events } = params + const { feePayer, nonce, deadline, minValidations, operations, maxFees } = params const intentHashParam = Array.from(hexToBytes(intentHash)) const nonceArray = Array.from(hexToBytes(nonce)) - const dataArray = hexToBytes(data) const maxFeesBn = maxFees.map((tokenFee) => ({ token: translateAddress(tokenFee.token), amount: new BN(tokenFee.amount), })) - const eventsArray = events.map((eventHex) => ({ - topic: Array.from(Uint8Array.from(hexToBytes(eventHex.topic))), - data: hexToBytes(eventHex.data), - })) const intentKey = PublicKey.findProgramAddressSync( [Buffer.from('intent'), hexToBytes(intentHash)], settler.programId )[0] + const operationsAnchor = operations.map((operation) => ({ + opType: solverSdk.opTypeToAnchorEnum(operation.opType), + data: hexToBytes(operation.data), + user: translateAddress(operation.user), + events: operation.events.map((event) => ({ + topic: Array.from(Uint8Array.from(hexToBytes(event.topic))), + data: hexToBytes(event.data), + })), + })) ix = await settler.methods .createIntent( intentHashParam, - dataArray, + operationsAnchor, maxFeesBn, - eventsArray, minValidations, - solverSdk.opTypeToAnchorEnum(op), - translateAddress(user), + translateAddress(feePayer), nonceArray, new BN(deadline), false @@ -530,24 +567,6 @@ describe('Settler', () => { context('when intent is not finalized', () => { context('when not finalizing intent', () => { context('when extending once', () => { - context('when extending with more data', () => { - beforeEach('create intent and extend params', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) - extendParams = { moreDataHex: randomHex(6).slice(2) } - }) - - it('extends the intent with more data', async () => { - const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) - await makeTxSignAndSend(solverProvider, ix) - - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq( - `${DEFAULT_DATA_HEX}${extendParams.moreDataHex}` - ) - expect(intent.isFinal).to.be.false - }) - }) - context('when extending with more max_fees', () => { beforeEach('create intent and extend params', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) @@ -573,54 +592,41 @@ describe('Settler', () => { }) }) - context('when extending with more events', () => { + context('when extending with more operations', () => { beforeEach('create intent and extend params', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) extendParams = { - moreEventsHex: [ - { - topic: randomHex(32).slice(2), - data: TEST_DATA_HEX_2, - }, - ], + moreOperations: [createOperationParams()], } }) - it('extends the intent with more events', async () => { + it('extends the intent with more operations', async () => { const ix = await solverSdk.extendIntentIx(intentHash, extendParams, false) await makeTxSignAndSend(solverProvider, ix) const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(intent.events.length).to.be.eq(2) - expect(Buffer.from(intent.events[1].topic).toString('hex')).to.be.eq( - extendParams.moreEventsHex![0].topic + expect(intent.operations.length).to.be.eq(2) + expect(intent.operations[1].opType).to.deep.include({ transfer: {} }) + expect(Buffer.from(intent.operations[1].data).toString('hex')).to.be.eq( + extendParams.moreOperations![0].data ) - expect(Buffer.from(intent.events[1].data).toString('hex')).to.be.eq( - extendParams.moreEventsHex![0].data + expect(Buffer.from(intent.operations[1].events[0].data).toString('hex')).to.be.eq( + extendParams.moreOperations![0].events![0].data ) }) }) context('when extending with all optional fields', () => { beforeEach('create intent and extend params', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, { - isFinal: false, - data: TEST_DATA_HEX_1, - }) + intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) extendParams = { - moreDataHex: TEST_DATA_HEX_2, moreMaxFees: [ { token: randomPubkey(), amount: '3000', }, ], - moreEventsHex: [ - { - topic: randomHex(32).slice(2), - data: TEST_DATA_HEX_3, - }, - ], + moreOperations: [createOperationParams(), createOperationParams()], } }) @@ -629,30 +635,20 @@ describe('Settler', () => { await makeTxSignAndSend(solverProvider, ix) const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq(`${TEST_DATA_HEX_1}${TEST_DATA_HEX_2}`) expect(intent.maxFees.length).to.be.eq(2) expect(intent.maxFees[1].amount.toString()).to.be.eq(extendParams.moreMaxFees![0].amount) - expect(intent.events.length).to.be.eq(2) - expect(Buffer.from(intent.events[1].topic).toString('hex')).to.be.eq( - extendParams.moreEventsHex![0].topic - ) - expect(Buffer.from(intent.events[1].data).toString('hex')).to.be.eq(TEST_DATA_HEX_3) + expect(intent.operations.length).to.be.eq(3) }) }) }) context('when extending more than once', () => { context('when extending to large size', () => { - const EXTEND_DATA_LOOPS = 100 - const EXTEND_EVENTS_LOOPS = 22 + const EXTEND_OPERATION_LOOPS = 30 const EXTEND_MAX_FEES_LOOPS = 18 extendParams = { - moreDataHex: randomHex(50).slice(2), - moreEventsHex: [ - { topic: randomHex(32).slice(2), data: randomHex(400).slice(2) }, - { topic: randomHex(32).slice(2), data: randomHex(400).slice(2) }, - ], + moreOperations: [createOperationParams(), createOperationParams(), createOperationParams()], moreMaxFees: [ { token: randomPubkey(), amount: '1' }, { token: randomPubkey(), amount: `${1 + 1000}` }, @@ -663,8 +659,7 @@ describe('Settler', () => { before('create intent', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false, - data: '', - events: [], + operations: [], }) intentKey = sdk.getIntentKey(intentHash) }) @@ -684,10 +679,8 @@ describe('Settler', () => { }) } - itExtendsIntentWithoutFailing('data', EXTEND_DATA_LOOPS, { moreDataHex: extendParams.moreDataHex }) - - itExtendsIntentWithoutFailing('events', EXTEND_EVENTS_LOOPS, { - moreEventsHex: extendParams.moreEventsHex, + itExtendsIntentWithoutFailing('operations', EXTEND_OPERATION_LOOPS, { + moreOperations: extendParams.moreOperations, }) itExtendsIntentWithoutFailing('max fees', EXTEND_MAX_FEES_LOOPS, { @@ -697,43 +690,10 @@ describe('Settler', () => { it('extended the intent fields as expected', async () => { const intent = await settler.account.intent.fetch(intentKey) const intentAcc = client.getAccount(intentKey) - expect(intent.data.length).to.be.eq(5000) - expect(intent.maxFees.length).to.be.eq(55) - expect(intent.events.length).to.be.eq(44) - expect(intent.isFinal).to.be.false - expect(intentAcc?.data.length).to.be.eq(26569) - }) - }) - - context('when extending multiple times', () => { - let extendParams1: ExtendIntentParams - let extendParams2: ExtendIntentParams - - before('create intent', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, { - isFinal: false, - data: TEST_DATA_HEX_1, - }) - extendParams1 = { moreDataHex: randomHex(6).slice(2) } - extendParams2 = { moreDataHex: randomHex(6).slice(2) } - }) - - it('extends the intent once without failing', async () => { - const ix = await solverSdk.extendIntentIx(intentHash, extendParams1, false) - await makeTxSignAndSend(solverProvider, ix) - }) - - it('extends the intent again without failing', async () => { - const ix = await solverSdk.extendIntentIx(intentHash, extendParams2, false) - await makeTxSignAndSend(solverProvider, ix) - }) - - it('extended the intent as expected', async () => { - const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq( - `${TEST_DATA_HEX_1}${extendParams1.moreDataHex}${extendParams2.moreDataHex}` - ) + expect(intent.maxFees.length).to.be.eq(1 + EXTEND_MAX_FEES_LOOPS * 3) + expect(intent.operations.length).to.be.eq(EXTEND_OPERATION_LOOPS * 3) expect(intent.isFinal).to.be.false + expect(intentAcc?.data.length).to.be.eq(9850) }) }) }) @@ -757,11 +717,8 @@ describe('Settler', () => { context('when extending and finalizing in one call', () => { beforeEach('create intent and extend params', async () => { - intentHash = await createTestIntent(solverSdk, solverProvider, { - isFinal: false, - data: TEST_DATA_HEX_2, - }) - extendParams = { moreDataHex: randomHex(6).slice(2) } + intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) + extendParams = { moreOperations: [createOperationParams()] } }) it('extends and finalizes the intent in one call', async () => { @@ -769,9 +726,7 @@ describe('Settler', () => { await makeTxSignAndSend(solverProvider, ix) const intent = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - expect(Buffer.from(intent.data).toString('hex')).to.be.eq( - `${TEST_DATA_HEX_2}${extendParams.moreDataHex}` - ) + expect(intent.operations.length).to.be.eq(2) expect(intent.isFinal).to.be.true }) }) @@ -781,7 +736,7 @@ describe('Settler', () => { context('when intent is already finalized', () => { beforeEach('create finalized intent and extend params', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: true }) - extendParams = { moreDataHex: TEST_DATA_HEX_1 } + extendParams = { moreOperations: [createOperationParams()] } }) it('throws an error', async () => { @@ -796,7 +751,7 @@ describe('Settler', () => { context('when intent does not exist', () => { beforeEach('generate non-existent intent hash and extend params', () => { intentHash = generateIntentHash() - extendParams = { moreDataHex: randomHex(6).slice(2) } + extendParams = { moreOperations: [createOperationParams()] } }) it('throws an error', async () => { @@ -811,7 +766,7 @@ describe('Settler', () => { context('when caller is not intent creator', () => { beforeEach('create intent and extend params', async () => { intentHash = await createTestIntent(solverSdk, solverProvider, { isFinal: false }) - extendParams = { moreDataHex: randomHex(6).slice(2) } + extendParams = { moreOperations: [createOperationParams()] } }) it('throws an error', async () => { @@ -839,14 +794,14 @@ describe('Settler', () => { it('claims the stale intent', async () => { const intentBefore = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - const intentBalanceBefore = Number(adminProvider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 - const intentCreatorBalanceBefore = Number(adminProvider.client.getBalance(intentBefore.creator)) || 0 + const intentBalanceBefore = getLamports(client, sdk.getIntentKey(intentHash)) + const intentCreatorBalanceBefore = getLamports(client, intentBefore.creator) const ix = await solverSdk.claimStaleIntentIx(intentHash) await makeTxSignAndSend(solverProvider, ix) - const intentBalanceAfter = Number(adminProvider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 - const intentCreatorBalanceAfter = Number(adminProvider.client.getBalance(intentBefore.creator)) || 0 + const intentBalanceAfter = getLamports(client, sdk.getIntentKey(intentHash)) + const intentCreatorBalanceAfter = getLamports(client, intentBefore.creator) try { await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) @@ -879,14 +834,14 @@ describe('Settler', () => { it('claims the stale intent', async () => { const intentBefore = await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) - const intentBalanceBefore = Number(adminProvider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 - const intentCreatorBalanceBefore = Number(adminProvider.client.getBalance(intentBefore.creator)) || 0 + const intentBalanceBefore = getLamports(client, sdk.getIntentKey(intentHash)) + const intentCreatorBalanceBefore = getLamports(client, intentBefore.creator) const ix = await solverSdk.claimStaleIntentIx(intentHash) await makeTxSignAndSend(solverProvider, ix) - const intentBalanceAfter = Number(adminProvider.client.getBalance(sdk.getIntentKey(intentHash))) || 0 - const intentCreatorBalanceAfter = Number(adminProvider.client.getBalance(intentBefore.creator)) || 0 + const intentBalanceAfter = getLamports(client, sdk.getIntentKey(intentHash)) + const intentCreatorBalanceAfter = getLamports(client, intentBefore.creator) try { await settler.account.intent.fetch(sdk.getIntentKey(intentHash)) @@ -1570,14 +1525,14 @@ describe('Settler', () => { it('claims the stale proposal', async () => { const proposalBefore = await settler.account.proposal.fetch(proposalKey) - const proposalBalanceBefore = Number(adminProvider.client.getBalance(proposalKey)) || 0 - const proposalCreatorBalanceBefore = Number(adminProvider.client.getBalance(proposalBefore.creator)) || 0 + const proposalBalanceBefore = getLamports(client, proposalKey) + const proposalCreatorBalanceBefore = getLamports(client, proposalBefore.creator) const ix = await solverSdk.claimStaleProposalIx(intentHash) await makeTxSignAndSend(solverProvider, ix) - const proposalBalanceAfter = Number(adminProvider.client.getBalance(proposalKey)) || 0 - const proposalCreatorBalanceAfter = Number(adminProvider.client.getBalance(proposalBefore.creator)) || 0 + const proposalBalanceAfter = getLamports(client, proposalKey) + const proposalCreatorBalanceAfter = getLamports(client, proposalBefore.creator) try { await settler.account.proposal.fetch(proposalKey) @@ -2318,6 +2273,7 @@ describe('Settler', () => { let userAta: web3.PublicKey let recipientAta: web3.PublicKey + let solverAta: web3.PublicKey const validator = ethers.Wallet.createRandom() const axia = ethers.Wallet.createRandom() @@ -2345,10 +2301,10 @@ describe('Settler', () => { intentCreator: proposal.solver, intent: sdk.getIntentKey(intentHash), fulfilledIntent: sdk.getFulfilledIntentKey(intentHash), - delegate: sdk.getDelegateKey(intent.user), + feePayerDelegate: sdk.getDelegateKey(intent.feePayer), ...accountsPartial, }) - .remainingAccounts(remainingAccounts ?? sdk.getExecuteProposalRemainingAccountsTransfer(intent, proposal)) + .remainingAccounts(remainingAccounts ?? sdk.getExecuteProposalRemainingAccounts(intent, proposal)) .instruction() return ix } @@ -2361,30 +2317,35 @@ describe('Settler', () => { } const createTestIntent = (data: string): Intent => ({ - configSig: randomHex(32), - data, + triggerSig: randomHex(32), deadline: (Number(client.getClock().unixTimestamp) + 1000).toString(), - events: [{ topic: randomHex(32), data: randomHex(50) }], maxFees: [{ token: usdc.toString(), amount: '10000000' }], minValidations: 1, nonce: randomHex(32), - op: 1, settler: settler.programId.toString(), - user: user.publicKey.toString(), + feePayer: user.publicKey.toString(), + operations: [ + { + opType: OpType.Transfer, + user: user.publicKey.toString(), + data, + events: [{ topic: randomHex(32), data: randomHex(50) }], + }, + ], }) const createTestProposal = ( intent: Intent, - data = '0x', + datas = ['0x'], solver: PublicKey = solverSdk.getSignerKey() ): Proposal => ({ - data, + datas, deadline: intent.deadline, fees: intent.maxFees.map((mf) => mf.amount), solver: solver.toString(), }) - const totalTransferAmount = (transfers: TransferIntentTransfer[]) => + const totalTransferAmount = (transfers: TransferOperationTransfer[]) => transfers.reduce((prev, curr) => prev + Number(curr.amount), 0) const totalFees = (fees: string[]) => fees.reduce((prev, curr) => prev + Number(curr), 0) @@ -2440,6 +2401,18 @@ describe('Settler', () => { ix = await createIx(sdk, accountsPartial, remainingAccounts) } + const createTestTransfers = (n: number) => + Array.from({ length: n }, () => ({ + amount: '1000000000', + token: usdc.toString(), + recipient: recipient.toString(), + })) + + const createTestIntentData = (transfers?: TransferOperationTransfer[]): TransferOperationData => ({ + chainId: Chains.Solana, + transfers: transfers ?? [], + }) + before('set correct domain', async () => { client.expireBlockhash() const ix = await adminSdk.updateEip712DomainIx({ @@ -2463,168 +2436,200 @@ describe('Settler', () => { userAta = (await createFundedAta(adminProvider, admin, user.publicKey, usdc, 100_000_000_000)).ata recipientAta = (await createFundedAta(adminProvider, admin, recipient, usdc, 0)).ata - await createFundedAta(adminProvider, admin, solver.publicKey, usdc, 0) + solverAta = (await createFundedAta(adminProvider, admin, solver.publicKey, usdc, 0)).ata }) beforeEach('new intent hash for each test case', () => { intentHash = randomHex(32) }) - context('when intent is transfer', () => { - let transfers: TransferIntentTransfer[] - let testIntentData: TransferIntentData + context('when intent has one operation', () => { + context('when operation is transfer', () => { + let transfers: TransferOperationTransfer[] + let testIntentData: TransferOperationData - const createTestTransfers = (n: number) => - Array.from({ length: n }, () => ({ - amount: '1000000000', - token: usdc.toString(), - recipient: recipient.toString(), - })) + const editProposal = async (proposalKey: web3.PublicKey, editedProposal: Partial) => { + const proposalAccount = await settler.account.proposal.fetch(proposalKey) + const proposalInfo = client.getAccount(proposalKey)! - const createTestIntentData = (transfers?: TransferIntentTransfer[]): TransferIntentData => ({ - chainId: Chains.Solana, - transfers: transfers ?? [], - }) + const modifiedProposal = { + ...proposalAccount, + ...editedProposal, + } - const editProposal = async (proposalKey: web3.PublicKey, editedProposal: Partial) => { - const proposalAccount = await settler.account.proposal.fetch(proposalKey) - const proposalInfo = client.getAccount(proposalKey)! + const serializedProposal = await settler.coder.accounts.encode('proposal', modifiedProposal) - const modifiedProposal = { - ...proposalAccount, - ...editedProposal, + client.setAccount(proposalKey, { + ...proposalInfo, + data: serializedProposal, + }) } - const serializedProposal = await settler.coder.accounts.encode('proposal', modifiedProposal) + const createTestData = (n: number, overrideTransfers?: TransferOperationTransfer[]) => { + transfers = overrideTransfers ?? createTestTransfers(n) + testIntentData = createTestIntentData(transfers) + intent = createTestIntent(svmEncodeTransferOperation(testIntentData)) + proposal = createTestProposal(intent) + } - client.setAccount(proposalKey, { - ...proposalInfo, - data: serializedProposal, - }) - } + const itWorksAsExpected = (n: number) => { + beforeEach(() => { + createTestData(n) + }) - const createTestData = (n: number, overrideTransfers?: TransferIntentTransfer[]) => { - transfers = overrideTransfers ?? createTestTransfers(n) - testIntentData = createTestIntentData(transfers) - intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) - proposal = createTestProposal(intent) - } + context('when remaining accounts are correct', () => { + context('when transfer/s is/are valid', () => { + context('when protocol has approval', () => { + context('when user has sufficient funds', () => { + beforeEach('create data and approve delegate', async () => { + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + totalTransferAmount(transfers) + totalFees(proposal.fees) + ) - const itWorksAsExpected = (n: number) => { - beforeEach(() => { - createTestData(n) - }) + await prepareAndBuildIx(solverSdk) + }) - context('when remaining accounts are correct', () => { - context('when transfer/s is/are valid', () => { - context('when protocol has approval', () => { - context('when user has sufficient funds', () => { - beforeEach('create data and approve delegate', async () => { - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - totalTransferAmount(transfers) + totalFees(proposal.fees) - ) + it('executes transfer', async () => { + const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) + const intentKey = sdk.getIntentKey(intentHash) + const fulfilledIntentKey = sdk.getFulfilledIntentKey(intentHash) - await prepareAndBuildIx(solverSdk) - }) + const proposalBalanceBefore = getLamports(client, proposalKey) + const intentBalanceBefore = getLamports(client, intentKey) + const solverBalanceBefore = getLamports(client, translateAddress(proposal.solver)) - it('executes transfer', async () => { - const solverAta = getAssociatedTokenAddressSync(usdc, solver.publicKey) - const proposalKey = sdk.getProposalKey(intentHash, proposal.solver) - const intentKey = sdk.getIntentKey(intentHash) - const fulfilledIntentKey = sdk.getFulfilledIntentKey(intentHash) + const recipientBalanceBefore = getAtaBalance(client, recipientAta) + const userBalanceBefore = getAtaBalance(client, userAta) + const solverAtaBalanceBefore = getAtaBalance(client, solverAta) - const proposalBalanceBefore = Number(adminProvider.client.getBalance(proposalKey)) || 0 - const intentBalanceBefore = Number(adminProvider.client.getBalance(intentKey)) || 0 - const solverBalanceBefore = - Number(adminProvider.client.getBalance(translateAddress(proposal.solver))) || 0 + await makeTxSignAndSend(solverProvider, ix) - const recipientBalanceBefore = getAtaBalance(client, recipientAta) - const userBalanceBefore = getAtaBalance(client, userAta) - const solverAtaBalanceBefore = getAtaBalance(client, solverAta) + const recipientBalanceAfter = getAtaBalance(client, recipientAta) + const userBalanceAfter = getAtaBalance(client, userAta) + const solverAtaBalanceAfter = getAtaBalance(client, solverAta) - await makeTxSignAndSend(solverProvider, ix) + const proposalBalanceAfter = getLamports(client, proposalKey) + const intentBalanceAfter = getLamports(client, intentKey) + const solverBalanceAfter = getLamports(client, translateAddress(proposal.solver)) + const fulfilledIntentBalanceAfter = getLamports(client, fulfilledIntentKey) - const recipientBalanceAfter = getAtaBalance(client, recipientAta) - const userBalanceAfter = getAtaBalance(client, userAta) - const solverAtaBalanceAfter = getAtaBalance(client, solverAta) - - const proposalBalanceAfter = Number(adminProvider.client.getBalance(proposalKey)) || 0 - const intentBalanceAfter = Number(adminProvider.client.getBalance(intentKey)) || 0 - const solverBalanceAfter = - Number(adminProvider.client.getBalance(translateAddress(proposal.solver))) || 0 - const fulfilledIntentBalanceAfter = Number(adminProvider.client.getBalance(fulfilledIntentKey)) || 0 - - try { - await settler.account.intent.fetch(intentKey) - expect.fail('Intent account should be closed') - } catch (error: any) { - expect(error.message).to.include('Account does not exist') - } + try { + await settler.account.intent.fetch(intentKey) + expect.fail('Intent account should be closed') + } catch (error: any) { + expect(error.message).to.include('Account does not exist') + } - try { - await settler.account.proposal.fetch(proposalKey) - expect.fail('Proposal account should be closed') - } catch (error: any) { - expect(error.message).to.include('Account does not exist') - } + try { + await settler.account.proposal.fetch(proposalKey) + expect.fail('Proposal account should be closed') + } catch (error: any) { + expect(error.message).to.include('Account does not exist') + } - const transfersAmount = totalTransferAmount(transfers) - const feesAmount = totalFees(proposal.fees) - - expect(client.getAccount(fulfilledIntentKey)?.owner.toString()).to.be.eq(settler.programId.toString()) - expect(recipientBalanceAfter).to.be.eq(recipientBalanceBefore + transfersAmount) - expect(userBalanceAfter).to.be.eq(userBalanceBefore - transfersAmount - feesAmount) - expect(solverBalanceAfter).to.be.eq( - solverBalanceBefore + - intentBalanceBefore + - proposalBalanceBefore - - fulfilledIntentBalanceAfter - - 5000 - ) - expect(proposalBalanceAfter).to.be.eq(0) - expect(intentBalanceAfter).to.be.eq(0) - expect(solverAtaBalanceAfter).to.be.eq(solverAtaBalanceBefore + feesAmount) + const transfersAmount = totalTransferAmount(transfers) + const feesAmount = totalFees(proposal.fees) + + expect(client.getAccount(fulfilledIntentKey)?.owner.toString()).to.be.eq( + settler.programId.toString() + ) + expect(recipientBalanceAfter).to.be.eq(recipientBalanceBefore + transfersAmount) + expect(userBalanceAfter).to.be.eq(userBalanceBefore - transfersAmount - feesAmount) + expect(solverBalanceAfter).to.be.eq( + solverBalanceBefore + + intentBalanceBefore + + proposalBalanceBefore - + fulfilledIntentBalanceAfter - + 5000 + ) + expect(proposalBalanceAfter).to.be.eq(0) + expect(intentBalanceAfter).to.be.eq(0) + expect(solverAtaBalanceAfter).to.be.eq(solverAtaBalanceBefore + feesAmount) + }) }) - }) - context('when user does not have sufficient funds', () => { - context('when user does not have transfer token sufficient funds', () => { - beforeEach('create data and approve delegate', async () => { - transfers = [ - { - amount: '1000000000000', - token: usdc.toString(), - recipient: recipient.toString(), - }, - ] + context('when user does not have sufficient funds', () => { + context('when user does not have transfer token sufficient funds', () => { + beforeEach('create data and approve delegate', async () => { + transfers = [ + { + amount: '1000000000000', + token: usdc.toString(), + recipient: recipient.toString(), + }, + ] + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + Number(transfers[0].amount) + ) + + createTestData(n, transfers) + await prepareAndBuildIx(solverSdk) + }) - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - Number(transfers[0].amount) - ) + itThrowsAnError('insufficient funds') + }) + + context('when user does not have fee token/s sufficient funds', () => { + beforeEach('create data with new token for fees', async () => { + const usdt = createMint(client, admin, { decimals: 9, freezeAuthority: null }).mint + const usdtUserAta = (await createFundedAta(adminProvider, admin, user.publicKey, usdt, 0)).ata + await createFundedAta(adminProvider, admin, solver.publicKey, usdt, 0) + + createTestData(n) + intent = { ...intent, maxFees: [{ token: usdt.toString(), amount: '10000000' }] } + proposal = createTestProposal(intent) + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + totalTransferAmount(transfers) + ) + + await approveDelegate( + userProvider, + usdtUserAta, + solverSdk.getDelegateKey(user.publicKey), + user, + totalFees(proposal.fees) + ) + + await prepareAndBuildIx(solverSdk) + }) - createTestData(n, transfers) + itThrowsAnError('insufficient funds') + }) + }) + }) + + context('when protocol does not have approval', () => { + context('when protocol does not have transfer token approval', () => { + beforeEach('create data and remove delegate', async () => { + await revokeDelegate(userProvider, userAta, user) await prepareAndBuildIx(solverSdk) }) - itThrowsAnError('insufficient funds') + itThrowsAnError('owner does not match') }) - context('when user does not have fee token/s sufficient funds', () => { - beforeEach('create data with new token for fees', async () => { + context('when protocol does not have fee token/s approval', () => { + beforeEach('create data, new fee token mint, approve Delegate for transfer token only', async () => { const usdt = createMint(client, admin, { decimals: 9, freezeAuthority: null }).mint - const usdtUserAta = (await createFundedAta(adminProvider, admin, user.publicKey, usdt, 0)).ata + await createFundedAta(adminProvider, admin, user.publicKey, usdt, 100_000_000_000) await createFundedAta(adminProvider, admin, solver.publicKey, usdt, 0) createTestData(n) - intent = { ...intent, maxFees: [{ token: usdt.toString(), amount: '10000000' }] } + intent = { ...intent, maxFees: [{ token: usdt.toString(), amount: '100000' }] } proposal = createTestProposal(intent) await approveDelegate( @@ -2632,240 +2637,174 @@ describe('Settler', () => { userAta, solverSdk.getDelegateKey(user.publicKey), user, - totalTransferAmount(transfers) - ) - - await approveDelegate( - userProvider, - usdtUserAta, - solverSdk.getDelegateKey(user.publicKey), - user, - totalFees(proposal.fees) + totalTransferAmount(transfers) + totalFees(proposal.fees) ) await prepareAndBuildIx(solverSdk) }) - itThrowsAnError('insufficient funds') + itThrowsAnError('owner does not match') }) }) }) - context('when protocol does not have approval', () => { - context('when protocol does not have transfer token approval', () => { - beforeEach('create data and remove delegate', async () => { - await revokeDelegate(userProvider, userAta, user) - await prepareAndBuildIx(solverSdk) - }) - - itThrowsAnError('owner does not match') - }) - - context('when protocol does not have fee token/s approval', () => { - beforeEach('create data, new fee token mint, approve Delegate for transfer token only', async () => { - const usdt = createMint(client, admin, { decimals: 9, freezeAuthority: null }).mint - await createFundedAta(adminProvider, admin, user.publicKey, usdt, 100_000_000_000) - await createFundedAta(adminProvider, admin, solver.publicKey, usdt, 0) - - createTestData(n) - intent = { ...intent, maxFees: [{ token: usdt.toString(), amount: '100000' }] } + context('when proposal is not valid', () => { + context('when proposal intent is not for chain Solana', () => { + beforeEach('create data for Optimism', async () => { + transfers = createTestTransfers(n) + testIntentData = { ...createTestIntentData(transfers), chainId: Chains.Optimism } + intent = createTestIntent(svmEncodeTransferOperation(testIntentData)) proposal = createTestProposal(intent) - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - totalTransferAmount(transfers) + totalFees(proposal.fees) - ) - await prepareAndBuildIx(solverSdk) }) - itThrowsAnError('owner does not match') - }) - }) - }) - - context('when proposal is not valid', () => { - context('when proposal intent is not for chain Solana', () => { - beforeEach('create data for Optimism', async () => { - transfers = createTestTransfers(n) - testIntentData = { ...createTestIntentData(transfers), chainId: Chains.Optimism } - intent = createTestIntent(svmEncodeTransferIntent(testIntentData)) - proposal = createTestProposal(intent) - - await prepareAndBuildIx(solverSdk) + itThrowsAnError('Incorrect intent chain id') }) - itThrowsAnError('Incorrect intent chain id') - }) + context('when proposal has data/instructions', () => { + beforeEach('create Proposal and manually edit bytes to add data on-chain', async () => { + await prepareIntentAndProposal() + await editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { + instructions: [ + { + programId: randomPubkey(), + accounts: [], + data: Buffer.from('deadbeef', 'hex'), + }, + ], + }) - context('when proposal has data/instructions', () => { - beforeEach('create Proposal and manually edit bytes to add data on-chain', async () => { - await prepareIntentAndProposal() - await editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { - instructions: [ - { - programId: randomPubkey(), - accounts: [], - data: Buffer.from('deadbeef', 'hex'), - }, - ], + ix = await createIx(solverSdk) }) - ix = await createIx(solverSdk) + itThrowsAnError('Incorrect proposal data') }) - - itThrowsAnError('Incorrect proposal data') }) }) - }) - context('when remaining accounts are not correct', () => { - let accountIndex: number - let expectedOwner: web3.PublicKey - let expectedMint: web3.PublicKey - let wrongValue: web3.PublicKey + context('when remaining accounts are not correct', () => { + let accountIndex: number + let expectedOwner: web3.PublicKey + let expectedMint: web3.PublicKey + let wrongValue: web3.PublicKey - const itThrowsAnErrorForGivenAccount = (expectedError: string) => { - beforeEach(async () => { - remainingAccounts[accountIndex].pubkey = wrongValue - await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) - }) - - itThrowsAnError(expectedError) - } - - const itChecksTokenAccount = (tokenAccountName: string) => { - const incorrectTokenAccountError = `Incorrect ${tokenAccountName}: mint or authority do not match expected` - const invalidTokenAccountError = 'Account not owned by TokenKeg or Token2022 programs' - - context(`when ${tokenAccountName} is another token account (wrong owner)`, () => { + const itThrowsAnErrorForGivenAccount = (expectedError: string) => { beforeEach(async () => { - wrongValue = (await createFundedAta(adminProvider, admin, randomPubkey(), expectedMint, 0)).ata + remainingAccounts[accountIndex].pubkey = wrongValue + await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) }) - itThrowsAnErrorForGivenAccount(incorrectTokenAccountError) - }) + itThrowsAnError(expectedError) + } - context(`when ${tokenAccountName} is another token account (wrong mint)`, () => { - beforeEach(async () => { - wrongValue = ( - await createFundedAta(adminProvider, admin, expectedOwner, createMint(client, admin).mint, 0) - ).ata - }) + const itChecksTokenAccount = (tokenAccountName: string) => { + const incorrectTokenAccountError = `Incorrect ${tokenAccountName}: mint or authority do not match expected` + const invalidTokenAccountError = 'Account not owned by TokenKeg or Token2022 programs' - itThrowsAnErrorForGivenAccount(incorrectTokenAccountError) - }) + context(`when ${tokenAccountName} is another token account (wrong owner)`, () => { + beforeEach(async () => { + wrongValue = (await createFundedAta(adminProvider, admin, randomPubkey(), expectedMint, 0)).ata + }) - context(`when ${tokenAccountName} is another type of account`, () => { - beforeEach(async () => { - wrongValue = randomPubkey() + itThrowsAnErrorForGivenAccount(incorrectTokenAccountError) }) - itThrowsAnErrorForGivenAccount(invalidTokenAccountError) - }) - } - - const itChecksMintAccount = (tokenName: string) => { - const incorrectTokenError = `Incorrect ${tokenName} mint account` - const invalidTokenError = 'Account not owned by TokenKeg or Token2022 programs' + context(`when ${tokenAccountName} is another token account (wrong mint)`, () => { + beforeEach(async () => { + wrongValue = ( + await createFundedAta(adminProvider, admin, expectedOwner, createMint(client, admin).mint, 0) + ).ata + }) - context(`when ${tokenName} is another token`, () => { - beforeEach(() => { - wrongValue = createMint(client, admin).mint + itThrowsAnErrorForGivenAccount(incorrectTokenAccountError) }) - itThrowsAnErrorForGivenAccount(incorrectTokenError) - }) + context(`when ${tokenAccountName} is another type of account`, () => { + beforeEach(async () => { + wrongValue = randomPubkey() + }) - context(`when ${tokenName} is another type of account`, () => { - beforeEach(() => { - wrongValue = randomPubkey() + itThrowsAnErrorForGivenAccount(invalidTokenAccountError) }) + } - itThrowsAnErrorForGivenAccount(invalidTokenError) - }) - } - - beforeEach('set up base data and re-approve', async () => { - remainingAccounts = sdk.getExecuteProposalRemainingAccountsTransfer(intent, proposal) + const itChecksMintAccount = (tokenName: string) => { + const incorrectTokenError = `Incorrect ${tokenName} mint account` + const invalidTokenError = 'Account not owned by TokenKeg or Token2022 programs' - // Re-approve Delegate for test - await approveDelegate( - userProvider, - userAta, - solverSdk.getDelegateKey(user.publicKey), - user, - totalTransferAmount(transfers) + totalFees(proposal.fees) - ) - }) + context(`when ${tokenName} is another token`, () => { + beforeEach(() => { + wrongValue = createMint(client, admin).mint + }) - context('when remaining accounts number is correct', () => { - context('when token programs are passed correctly', () => { - context('when transfer accounts are incorrect', () => { - context('when transfer token is incorrect', () => { - beforeEach(() => { - accountIndex = 2 - }) + itThrowsAnErrorForGivenAccount(incorrectTokenError) + }) - itChecksMintAccount('transfer token') + context(`when ${tokenName} is another type of account`, () => { + beforeEach(() => { + wrongValue = randomPubkey() }) - context('when recipient is incorrect', () => { - beforeEach(() => { - accountIndex = 3 - wrongValue = randomPubkey() - }) + itThrowsAnErrorForGivenAccount(invalidTokenError) + }) + } - itThrowsAnErrorForGivenAccount('Incorrect transfer recipient account') - }) + beforeEach('set up base data and re-approve', async () => { + remainingAccounts = sdk.getExecuteProposalRemainingAccounts(intent, proposal) - context('when recipient token account is incorrect', () => { - beforeEach(() => { - accountIndex = 4 - expectedOwner = recipient - expectedMint = usdc - }) + // Re-approve Delegate for test + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + totalTransferAmount(transfers) + totalFees(proposal.fees) + ) + }) - itChecksTokenAccount('recipient token account') - }) + context('when remaining accounts number is correct', () => { + context('when token programs are passed correctly', () => { + context('when transfer accounts are incorrect', () => { + context('when user delegate is incorrect', () => { + beforeEach(() => { + accountIndex = 2 + wrongValue = randomPubkey() + }) - context('when user token account is incorrect', () => { - beforeEach(() => { - accountIndex = 5 - expectedOwner = user.publicKey - expectedMint = usdc + itThrowsAnErrorForGivenAccount('Incorrect user delegate') }) - itChecksTokenAccount('user token account') - }) - }) + context('when transfer token is incorrect', () => { + beforeEach(() => { + accountIndex = 3 + }) - context('when transfer accounts are correct', () => { - context('when fee accounts are incorrect', () => { - context('when fee token is incorrect', () => { + itChecksMintAccount('transfer token') + }) + + context('when recipient is incorrect', () => { beforeEach(() => { - accountIndex = remainingAccounts.length - 3 + accountIndex = 4 + wrongValue = randomPubkey() }) - itChecksMintAccount('fee token') + itThrowsAnErrorForGivenAccount('Incorrect transfer recipient account') }) - context('when solver token account is incorrect', () => { + context('when recipient token account is incorrect', () => { beforeEach(() => { - accountIndex = remainingAccounts.length - 2 - expectedOwner = solver.publicKey + accountIndex = 5 + expectedOwner = recipient expectedMint = usdc }) - itChecksTokenAccount('solver token account') + itChecksTokenAccount('recipient token account') }) context('when user token account is incorrect', () => { beforeEach(() => { - accountIndex = remainingAccounts.length - 1 + accountIndex = 6 expectedOwner = user.publicKey expectedMint = usdc }) @@ -2873,177 +2812,291 @@ describe('Settler', () => { itChecksTokenAccount('user token account') }) }) + + context('when transfer accounts are correct', () => { + context('when fee accounts are incorrect', () => { + context('when fee token is incorrect', () => { + beforeEach(() => { + accountIndex = remainingAccounts.length - 3 + }) + + itChecksMintAccount('fee token') + }) + + context('when solver token account is incorrect', () => { + beforeEach(() => { + accountIndex = remainingAccounts.length - 2 + expectedOwner = solver.publicKey + expectedMint = usdc + }) + + itChecksTokenAccount('solver token account') + }) + + context('when fee payer token account is incorrect', () => { + beforeEach(() => { + accountIndex = remainingAccounts.length - 1 + expectedOwner = translateAddress(intent.feePayer) + expectedMint = usdc + }) + + itChecksTokenAccount('fee payer token account') + }) + }) + }) }) - }) - context('when token programs are not passed correctly', () => { - context('when first program is wrong', () => { - beforeEach(() => { - accountIndex = 0 - wrongValue = randomPubkey() + context('when token programs are not passed correctly', () => { + context('when first program is wrong', () => { + beforeEach(() => { + accountIndex = 0 + wrongValue = randomPubkey() + }) + + itThrowsAnErrorForGivenAccount('Incorrect token program account') }) - itThrowsAnErrorForGivenAccount('Incorrect token program account') - }) + context('when second program is wrong', () => { + beforeEach(() => { + accountIndex = 1 + wrongValue = randomPubkey() + }) - context('when second program is wrong', () => { - beforeEach(() => { - accountIndex = 1 - wrongValue = randomPubkey() + itThrowsAnErrorForGivenAccount('Incorrect token program account') }) - itThrowsAnErrorForGivenAccount('Incorrect token program account') + context('when both programs are wrong', () => { + beforeEach(async () => { + remainingAccounts[0].pubkey = randomPubkey() + remainingAccounts[1].pubkey = randomPubkey() + await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) + }) + + itThrowsAnError('Incorrect token program account') + }) }) + }) - context('when both programs are wrong', () => { + context('when remaining accounts number is not correct', () => { + context('when there are less remaining accounts than expected', () => { beforeEach(async () => { - remainingAccounts[0].pubkey = randomPubkey() - remainingAccounts[1].pubkey = randomPubkey() + remainingAccounts.pop() await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) }) - itThrowsAnError('Incorrect token program account') - }) - }) - }) - - context('when remaining accounts number is not correct', () => { - context('when there are less remaining accounts than expected', () => { - beforeEach(async () => { - remainingAccounts.pop() - await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) + itThrowsAnError('ProgramError') }) - itThrowsAnError('ProgramError') - }) - - context('when there are more remaining accounts than expected', () => { - beforeEach(async () => { - remainingAccounts.push({ pubkey: randomPubkey(), isWritable: true, isSigner: false }) - await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) - }) + context('when there are more remaining accounts than expected', () => { + beforeEach(async () => { + remainingAccounts.push({ pubkey: randomPubkey(), isWritable: true, isSigner: false }) + await prepareAndBuildIx(solverSdk, undefined, remainingAccounts) + }) - it('works normally', async () => { - const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).not.to.include('FailedTransactionMetadata') + it('works normally', async () => { + const res = await makeTxSignAndSend(solverProvider, ix) + expect(res.toString()).not.to.include('FailedTransactionMetadata') + }) }) }) }) - }) - } + } - beforeEach(() => { - createTestData(1) - }) + beforeEach(() => { + createTestData(1) + }) - context('when caller is allowlisted solver', () => { - context('when intent exists', () => { - context('when intent is correct', () => { - context('when proposal exists', () => { - context('when proposal is correct', () => { - context('when intent has one transfer', () => { - itWorksAsExpected(1) - }) + context('when caller is allowlisted solver', () => { + context('when intent exists', () => { + context('when intent is correct', () => { + context('when proposal exists', () => { + context('when proposal is correct', () => { + context('when intent has one transfer', () => { + itWorksAsExpected(1) + }) - context('when intent has more than one transfer', () => { - itWorksAsExpected(2) + context('when intent has more than one transfer', () => { + itWorksAsExpected(2) + }) }) - }) - - context('when proposal is not correct', () => { - context('when proposal is for another intent', () => { - beforeEach(async () => { - const otherIntentHash = randomHex(32) - const otherIntent = createTestIntent(svmEncodeTransferIntent(testIntentData)) - const otherIntentKey = solverSdk.getIntentKey(otherIntentHash) - const otherFulfilledIntentKey = solverSdk.getFulfilledIntentKey(otherIntentHash) - await makeTxSignAndSend( - solverProvider, - await solverSdk.createIntentIx(otherIntentHash, otherIntent, true) - ) - await prepareAndBuildIx(solverSdk, { - intent: otherIntentKey, - fulfilledIntent: otherFulfilledIntentKey, + context('when proposal is not correct', () => { + context('when proposal is for another intent', () => { + beforeEach(async () => { + const otherIntentHash = randomHex(32) + const otherIntent = createTestIntent(svmEncodeTransferOperation(testIntentData)) + const otherIntentKey = solverSdk.getIntentKey(otherIntentHash) + const otherFulfilledIntentKey = solverSdk.getFulfilledIntentKey(otherIntentHash) + await makeTxSignAndSend( + solverProvider, + await solverSdk.createIntentIx(otherIntentHash, otherIntent, true) + ) + + await prepareAndBuildIx(solverSdk, { + intent: otherIntentKey, + fulfilledIntent: otherFulfilledIntentKey, + }) }) + + itThrowsAnError('Incorrect intent for proposal') }) - itThrowsAnError('Incorrect intent for proposal') - }) + context('when proposal is from another proposal creator', () => { + beforeEach(async () => { + await prepareAndBuildIx(solverSdk, { proposalCreator: randomPubkey() }) + }) - context('when proposal is from another proposal creator', () => { - beforeEach(async () => { - await prepareAndBuildIx(solverSdk, { proposalCreator: randomPubkey() }) + itThrowsAnError('Incorrect proposal creator') }) - itThrowsAnError('Incorrect proposal creator') - }) + context('when proposal is not signed', () => { + beforeEach(async () => { + await prepareIntentAndProposal() + await editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { isSigned: false }) + ix = await createIx(solverSdk) + }) - context('when proposal is not signed', () => { - beforeEach(async () => { - await prepareIntentAndProposal() - await editProposal(solverSdk.getProposalKey(intentHash, proposal.solver), { isSigned: false }) - ix = await createIx(solverSdk) + itThrowsAnError('Proposal is not signed') }) - itThrowsAnError('Proposal is not signed') - }) + context('when proposal is expired', () => { + beforeEach(async () => { + await prepareAndBuildIx(solverSdk) - context('when proposal is expired', () => { - beforeEach(async () => { - await prepareAndBuildIx(solverSdk) + const delta = Number(proposal.deadline) - Number(client.getClock().unixTimestamp) + warpSeconds(solverProvider, delta * 2) + }) - const delta = Number(proposal.deadline) - Number(client.getClock().unixTimestamp) - warpSeconds(solverProvider, delta * 2) + itThrowsAnError('Proposal has already expired') }) + }) + }) + }) - itThrowsAnError('Proposal has already expired') + context('when intent is not correct', () => { + context('when intent_creator is not correct', () => { + beforeEach(async () => { + await prepareAndBuildIx(solverSdk, { intentCreator: randomPubkey() }) }) + + itThrowsAnError('Incorrect intent creator') }) }) }) + }) - context('when intent is not correct', () => { - context('when intent_creator is not correct', () => { - beforeEach(async () => { - await prepareAndBuildIx(solverSdk, { intentCreator: randomPubkey() }) - }) + context('when caller is not allowlisted solver', () => { + beforeEach(async () => { + await prepareIntentAndProposal() + ix = await createIx(maliciousSdk) + }) - itThrowsAnError('Incorrect intent creator') - }) + it('throws an error', async () => { + const res = await makeTxSignAndSend(maliciousProvider, ix) + expect(res.toString()).to.include( + 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' + ) }) }) }) - context('when caller is not allowlisted solver', () => { + context('when operation is not transfer', () => { beforeEach(async () => { - await prepareIntentAndProposal() - ix = await createIx(maliciousSdk) + intent = createTestIntent('0xdeadbeef') + intent = { ...intent, operations: [{ ...intent.operations[0], opType: 2 }] } + proposal = createTestProposal(intent) + + await prepareAndBuildIx(solverSdk, undefined, [ + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: solverSdk.getDelegateKey(intent.operations[0].user), isSigner: false, isWritable: false }, + ]) }) it('throws an error', async () => { - const res = await makeTxSignAndSend(maliciousProvider, ix) - expect(res.toString()).to.include( - 'AnchorError caused by account: solver_registry. Error Code: AccountNotInitialized' - ) + const res = await makeTxSignAndSend(solverProvider, ix) + expect(res.toString()).to.include('Unsupported intent op') }) }) }) - context('when intent is not transfer', () => { - beforeEach(async () => { - intent = { ...createTestIntent('0xdeadbeef'), op: 2 } - proposal = createTestProposal(intent) + context('when intent does not have one operation', () => { + context('when intent has more than one operation', () => { + let transfer: TransferOperationTransfer[] + + beforeEach(async () => { + transfer = createTestTransfers(1) + intent = createTestIntent(svmEncodeTransferOperation(createTestIntentData(transfer))) + intent.operations = [intent.operations[0], intent.operations[0]] + proposal = createTestProposal(intent) + + await prepareAndBuildIx(solverSdk) + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + 2 * totalTransferAmount(transfer) + totalFees(proposal.fees) + ) + }) + + it('executes two operations', async () => { + const userBalanceBefore = getAtaBalance(client, userAta) + const recipientBalanceBefore = getAtaBalance(client, recipientAta) + const solverBalanceBefore = getAtaBalance(client, solverAta) - await prepareAndBuildIx(solverSdk, undefined, [ - { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, - { pubkey: TOKEN_2022_PROGRAM_ID, isSigner: false, isWritable: false }, - ]) + const res = await makeTxSignAndSend(solverProvider, ix) + + const userBalanceAfter = getAtaBalance(client, userAta) + const recipientBalanceAfter = getAtaBalance(client, recipientAta) + const solverBalanceAfter = getAtaBalance(client, solverAta) + + const totalTransfers = totalTransferAmount(transfer) * 2 + const proposalFee = totalFees(proposal.fees) + + // Logs events of two operations + expect(res.toString().split('Program data:').length).to.be.eq(3) + + expect(userBalanceAfter).to.be.eq(userBalanceBefore - totalTransfers - proposalFee) + expect(recipientBalanceAfter).to.be.eq(recipientBalanceBefore + totalTransfers) + expect(solverBalanceAfter).to.be.eq(solverBalanceBefore + proposalFee) + }) }) - it('throws an error', async () => { - const res = await makeTxSignAndSend(solverProvider, ix) - expect(res.toString()).to.include('Unsupported intent op') + context('when intent has no operations', () => { + beforeEach(async () => { + intent = createTestIntent('0x') + intent.operations = [] + proposal = createTestProposal(intent) + await prepareAndBuildIx(solverSdk) + + await approveDelegate( + userProvider, + userAta, + solverSdk.getDelegateKey(user.publicKey), + user, + totalFees(proposal.fees) + ) + }) + + it('executes noop', async () => { + const feePayerAta = getAssociatedTokenAddressSync(usdc, translateAddress(intent.feePayer)) + + const solverBalanceBefore = getAtaBalance(client, solverAta) + const feePayerBalanceBefore = getAtaBalance(client, feePayerAta) + + await makeTxSignAndSend(solverProvider, ix) + + const solverBalanceAfter = getAtaBalance(client, solverAta) + const feePayerBalanceAfter = getAtaBalance(client, feePayerAta) + const proposalFee = totalFees(proposal.fees) + + // Only accounts in context and other remaining accounts (no operations) + expect(ix.keys.length).to.be.eq(14) + expect(solverBalanceAfter).to.be.eq(solverBalanceBefore + proposalFee) + expect(feePayerBalanceAfter).to.be.eq(feePayerBalanceBefore - proposalFee) + }) }) }) }) diff --git a/packages/svm/tests/utils.ts b/packages/svm/tests/utils.ts index 75bdbad..c66d572 100644 --- a/packages/svm/tests/utils.ts +++ b/packages/svm/tests/utils.ts @@ -1,6 +1,6 @@ -import { web3 } from '@coral-xyz/anchor' +import { Address, translateAddress, web3 } from '@coral-xyz/anchor' import { LiteSVMProvider } from 'anchor-litesvm' -import { Clock, FailedTransactionMetadata, TransactionMetadata } from 'litesvm' +import { Clock, FailedTransactionMetadata, LiteSVM, TransactionMetadata } from 'litesvm' export async function signAndSendTx( provider: LiteSVMProvider, @@ -35,3 +35,7 @@ export function warpSeconds(provider: LiteSVMProvider, seconds: number): void { ) ) } + +export function getLamports(client: LiteSVM, key: Address): number { + return Number(client.getBalance(translateAddress(key))) || 0 +}