acta-maker-sdk 0.5.3

Rust SDK for Acta options market makers — WebSocket client, order signing, message types
Documentation
use super::*;
use solana_sdk::{
    hash::Hash, instruction::AccountMeta, signature::Keypair, transaction::Transaction,
};

#[test]
fn transaction_signing_returns_error_for_missing_signer() {
    let fee_payer = Keypair::new();
    let missing_signer = Pubkey::new_unique();
    let instruction = Instruction {
        program_id: Pubkey::new_unique(),
        accounts: vec![AccountMeta::new_readonly(missing_signer, true)],
        data: Vec::new(),
    };
    let mut transaction = Transaction::new_with_payer(&[instruction], Some(&fee_payer.pubkey()));
    let signers: [&(dyn Signer + Sync); 1] = [&fee_payer];

    assert!(matches!(
        sign_transaction(&mut transaction, &signers, Hash::new_unique()),
        Err(ChainError::Signing(_))
    ));
}

#[test]
fn transaction_signing_verifies_transaction() {
    let fee_payer = Keypair::new();
    let mut transaction = Transaction::new_with_payer(&[], Some(&fee_payer.pubkey()));
    let signers: [&(dyn Signer + Sync); 1] = [&fee_payer];

    sign_transaction(&mut transaction, &signers, Hash::new_unique()).unwrap();

    assert!(transaction.verify().is_ok());
}

#[tokio::test]
async fn native_funding_batches_only_the_required_accounts() {
    use crate::chain::wsol::{NATIVE_MINT, TOKEN_PROGRAM_ID};
    use serde_json::{Value, json};
    use solana_rpc_client::{
        api::{client_error, request::RpcRequest},
        rpc_client::RpcClientConfig,
        rpc_sender::{RpcSender, RpcTransportStats},
    };
    use std::{
        collections::VecDeque,
        sync::{Arc, Mutex},
    };

    type Replies = Arc<Mutex<VecDeque<(RpcRequest, Value, Value)>>>;
    struct Sender(Replies);
    #[async_trait::async_trait]
    impl RpcSender for Sender {
        async fn send(&self, request: RpcRequest, params: Value) -> client_error::Result<Value> {
            let (expected, expected_params, response) =
                self.0.lock().unwrap().pop_front().expect("unexpected RPC");
            assert_eq!(request, expected);
            assert_eq!(params, expected_params);
            Ok(response)
        }
        fn get_transport_stats(&self) -> RpcTransportStats {
            RpcTransportStats::default()
        }
        fn url(&self) -> String {
            "mock://funding".into()
        }
    }
    fn account(owner: Pubkey, data: Vec<u8>) -> Value {
        json!({"owner": owner.to_string(), "lamports": 10_000_000, "executable": false,
            "rentEpoch": 0, "data": [bs58::encode(data).into_string(), "base58"]})
    }
    fn response(value: Value) -> Value {
        json!({"context": {"slot": 10}, "value": value})
    }

    let program = Pubkey::new_unique();
    let maker = Pubkey::new_unique();
    let position = Pubkey::new_unique();
    let market = Pubkey::new_unique();
    let underlying = Pubkey::new_unique();
    let mut pos_data = vec![0; 424];
    pos_data[0..2].copy_from_slice(&[1, 2]);
    pos_data[4] = 1;
    pos_data[40..72].copy_from_slice(maker.as_ref());
    pos_data[72..104].copy_from_slice(market.as_ref());
    pos_data[104..112].copy_from_slice(&1_000_000_000u64.to_le_bytes());
    pos_data[112..120].copy_from_slice(&100u64.to_le_bytes());
    let mut market_data = vec![0; 416];
    market_data[0..5].copy_from_slice(&[4, 1, 0, 9, 9]);
    market_data[32..64].copy_from_slice(underlying.as_ref());
    market_data[64..96].copy_from_slice(NATIVE_MINT.as_ref());
    market_data[96..128].copy_from_slice(TOKEN_PROGRAM_ID.as_ref());
    market_data[128..160].copy_from_slice(TOKEN_PROGRAM_ID.as_ref());
    let funding_ata =
        crate::chain::ix::derive_associated_token_address(&maker, &NATIVE_MINT, &TOKEN_PROGRAM_ID);
    let position_ata = crate::chain::ix::derive_associated_token_address(
        &position,
        &NATIVE_MINT,
        &TOKEN_PROGRAM_ID,
    );
    let config = json!({"encoding": "base64+zstd", "commitment": "confirmed", "dataSlice": null, "minContextSlot": null});
    for (create_atas, self_paid, existing) in [
        (true, true, None),
        (true, true, Some(100)),
        (true, false, None),
        (false, true, None),
        (false, true, Some(30)),
    ] {
        let payer = if self_paid {
            maker
        } else {
            Pubkey::new_unique()
        };
        let mut replies = VecDeque::from([
            (
                RpcRequest::GetAccountInfo,
                json!([position.to_string(), config]),
                response(account(program, pos_data.clone())),
            ),
            (
                RpcRequest::GetAccountInfo,
                json!([market.to_string(), config]),
                response(account(program, market_data.clone())),
            ),
        ]);
        let source = existing.map_or(Value::Null, |amount: u64| {
            let mut data = vec![0; 165];
            data[..32].copy_from_slice(NATIVE_MINT.as_ref());
            data[32..64].copy_from_slice(maker.as_ref());
            data[64..72].copy_from_slice(&amount.to_le_bytes());
            account(TOKEN_PROGRAM_ID, data)
        });
        if create_atas && self_paid {
            replies.push_back((
                RpcRequest::GetMultipleAccounts,
                json!([[position_ata.to_string(), funding_ata.to_string()], config]),
                response(json!([null, source])),
            ));
            replies.push_back((
                RpcRequest::GetMinimumBalanceForRentExemption,
                json!([165]),
                json!(20),
            ));
        } else {
            replies.push_back((
                RpcRequest::GetAccountInfo,
                json!([funding_ata.to_string(), config]),
                response(source),
            ));
        }
        if existing.is_none() && create_atas && self_paid {
            replies.push_back((
                RpcRequest::GetMinimumBalanceForRentExemption,
                json!([165]),
                json!(20),
            ));
        }
        let missing = existing.is_none() && !create_atas;
        let wrapping = !missing && existing.unwrap_or(0) < 100;
        if wrapping {
            replies.push_back((
                RpcRequest::GetBalance,
                json!([maker.to_string(), {"commitment": "confirmed"}]),
                response(json!(10_000)),
            ));
        }
        let replies = Arc::new(Mutex::new(replies));
        let commitment = CommitmentConfig::confirmed();
        let client = ChainClient {
            rpc: RpcClient::new_sender(
                Sender(Arc::clone(&replies)),
                RpcClientConfig::with_commitment(commitment),
            ),
            program_id: program,
            commitment,
        };
        let result = client
            .prepare_fund_position(
                &FundPositionArgs {
                    maker_owner: maker,
                    position_pda: position,
                    create_atas,
                    native_sol: NativeSolFunding::WrapIfNeeded {
                        reserve_lamports: 10,
                    },
                },
                payer,
            )
            .await;
        assert!(
            replies.lock().unwrap().is_empty(),
            "unconsumed RPC expectations"
        );
        if missing {
            assert!(
                matches!(result, Err(ChainError::MissingFundingAccount(key)) if key == funding_ata)
            );
            continue;
        }
        let plan = result.unwrap();
        let mut expected = build_fund_position_ixs(
            &program,
            &FundPositionIxArgs {
                maker_owner: maker,
                position_pda: position,
                market_pda: market,
                position_type: 0,
                underlying_mint: underlying,
                quote_mint: NATIVE_MINT,
                underlying_token_program_id: TOKEN_PROGRAM_ID,
                quote_token_program_id: TOKEN_PROGRAM_ID,
                create_atas,
            },
            payer,
        )
        .unwrap();
        if wrapping {
            let funding = expected.pop().unwrap();
            let wrap = 100 - existing.unwrap_or(0);
            let tail = &plan.instructions[expected.len()..];
            assert_eq!(&plan.instructions[..expected.len()], &expected);
            assert_eq!(tail.len(), 3);
            assert_eq!(tail[0].accounts[1].pubkey, funding_ata);
            assert_eq!(
                tail[0].data,
                [2u32.to_le_bytes().as_slice(), wrap.to_le_bytes().as_slice()].concat()
            );
            assert_eq!(tail[1].program_id, TOKEN_PROGRAM_ID);
            assert_eq!(tail[1].accounts[0].pubkey, funding_ata);
            assert_eq!(tail[1].data, [17]);
            assert_eq!(tail[2], funding);
            let needed = wrap + 10 + if create_atas && self_paid { 40 } else { 0 };
            let budget = plan.native_budget.unwrap();
            assert!(budget.validate(needed + 5, 5).is_ok());
            assert!(matches!(
                budget.validate(needed + 4, 5),
                Err(ChainError::InsufficientNativeSol { .. })
            ));
        } else {
            assert_eq!(plan.instructions, expected);
            assert!(plan.native_budget.is_none());
        }
    }
}

#[test]
fn lookup_table_parses_active_tables_only() {
    let key = Pubkey::new_unique();
    let address = Pubkey::new_unique();
    let mut data = vec![0u8; LOOKUP_TABLE_META_LEN];
    data[..4].copy_from_slice(&1u32.to_le_bytes());
    data[4..12].copy_from_slice(&u64::MAX.to_le_bytes());
    data.extend_from_slice(address.as_ref());
    let mut account = Account {
        lamports: 1,
        data,
        owner: ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
        executable: false,
        rent_epoch: 0,
    };
    let table = parse_lookup_table(key, &account).unwrap();
    assert_eq!((table.key, table.addresses), (key, vec![address]));

    account.data[4..12].copy_from_slice(&7u64.to_le_bytes());
    assert!(matches!(
        parse_lookup_table(key, &account),
        Err(ChainError::LookupTableDeactivated(found)) if found == key
    ));
    account.data[4..12].copy_from_slice(&u64::MAX.to_le_bytes());
    account.data.push(0);
    assert!(matches!(
        parse_lookup_table(key, &account),
        Err(ChainError::InvalidAccountData)
    ));
    account.owner = Pubkey::new_unique();
    assert!(matches!(
        parse_lookup_table(key, &account),
        Err(ChainError::AccountOwnerMismatch { .. })
    ));
}