intent_transfer/intrachain/processor/
send_tokens.rs

1use crate::{
2    config::state::fee_config::{FeeConfig, FEE_CONFIG_SEED},
3    error::IntentTransferError,
4    fees::{PaidInstruction, VerifyAndCollectAccounts},
5    intrachain::message::Message,
6    nonce::{self, Nonce},
7    verify::{verify_and_update_nonce, verify_signer_matches_source, verify_symbol_or_mint},
8    INTENT_TRANSFER_SEED,
9};
10use anchor_lang::{prelude::*, solana_program::sysvar::instructions};
11use anchor_spl::{
12    associated_token::AssociatedToken,
13    token::{
14        spl_token::try_ui_amount_into_amount, transfer_checked, Mint, Token, TokenAccount,
15        TransferChecked,
16    },
17};
18use chain_id::ChainId;
19use solana_intents::Intent;
20
21#[derive(Accounts)]
22pub struct SendTokens<'info> {
23    #[account(seeds = [chain_id::SEED], seeds::program = chain_id::ID, bump)]
24    pub chain_id: Account<'info, ChainId>,
25
26    /// CHECK: we check the address of this account
27    #[account(address = instructions::ID)]
28    pub sysvar_instructions: UncheckedAccount<'info>,
29
30    /// CHECK: this is just a signer for token program CPIs
31    #[account(seeds = [INTENT_TRANSFER_SEED], bump)]
32    pub intent_transfer_setter: UncheckedAccount<'info>,
33
34    #[account(mut, token::mint = mint)]
35    pub source: Account<'info, TokenAccount>,
36
37    #[account(init_if_needed, payer = sponsor, associated_token::mint = mint, associated_token::authority = destination_owner)]
38    pub destination: Account<'info, TokenAccount>,
39
40    pub mint: Account<'info, Mint>,
41
42    pub metadata: Option<UncheckedAccount<'info>>,
43
44    #[account(
45        init_if_needed,
46        payer = sponsor,
47        space = Nonce::DISCRIMINATOR.len() + Nonce::INIT_SPACE,
48        seeds = [nonce::INTENT_TRANSFER_NONCE_SEED, source.owner.key().as_ref()],
49        bump
50    )]
51    pub nonce: Account<'info, Nonce>,
52
53    #[account(mut)]
54    pub sponsor: Signer<'info>,
55
56    /// CHECK: This account is checked against the signed message
57    pub destination_owner: AccountInfo<'info>,
58
59    #[account(mut, token::mint = fee_mint, token::authority = source.owner )]
60    pub fee_source: Account<'info, TokenAccount>,
61
62    #[account(init_if_needed, payer = sponsor, associated_token::mint = fee_mint, associated_token::authority = sponsor)]
63    pub fee_destination: Account<'info, TokenAccount>,
64
65    pub fee_mint: Account<'info, Mint>,
66
67    pub fee_metadata: Option<UncheckedAccount<'info>>,
68
69    #[account(seeds = [FEE_CONFIG_SEED, fee_mint.key().as_ref()], bump)]
70    pub fee_config: Account<'info, FeeConfig>,
71
72    pub system_program: Program<'info, System>,
73    pub token_program: Program<'info, Token>,
74    pub associated_token_program: Program<'info, AssociatedToken>,
75}
76
77impl<'info> PaidInstruction<'info> for SendTokens<'info> {
78    fn fee_amount(&self) -> u64 {
79        self.fee_config.intrachain_transfer_fee
80    }
81
82    fn verify_and_collect_accounts<'a>(&'a self) -> VerifyAndCollectAccounts<'a, 'info> {
83        let Self {
84            fee_source,
85            fee_destination,
86            fee_mint,
87            fee_metadata,
88            intent_transfer_setter,
89            token_program,
90            ..
91        } = self;
92        VerifyAndCollectAccounts {
93            fee_source,
94            fee_destination,
95            fee_mint,
96            fee_metadata,
97            intent_transfer_setter,
98            token_program,
99        }
100    }
101}
102
103impl<'info> SendTokens<'info> {
104    pub fn verify_and_send(&mut self, signer_seeds: &[&[&[u8]]]) -> Result<()> {
105        let Self {
106            chain_id,
107            destination,
108            intent_transfer_setter,
109            metadata,
110            mint,
111            source,
112            sysvar_instructions,
113            token_program,
114            nonce,
115            destination_owner,
116            ..
117        } = self;
118
119        let Intent {
120            message:
121                Message {
122                    amount,
123                    chain_id: expected_chain_id,
124                    recipient,
125                    symbol_or_mint,
126                    nonce: new_nonce,
127                    version: _,
128                    fee_amount,
129                    fee_symbol_or_mint,
130                },
131            signer,
132        } = Intent::load(sysvar_instructions.as_ref())
133            .map_err(Into::<IntentTransferError>::into)?;
134
135        if chain_id.chain_id != expected_chain_id {
136            return err!(IntentTransferError::ChainIdMismatch);
137        }
138
139        verify_symbol_or_mint(&symbol_or_mint, metadata, mint)?;
140        verify_signer_matches_source(signer, source.owner)?;
141
142        require_keys_eq!(
143            recipient,
144            destination_owner.key(),
145            IntentTransferError::RecipientMismatch
146        );
147
148        verify_and_update_nonce(nonce, new_nonce)?;
149
150        transfer_checked(
151            CpiContext::new_with_signer(
152                token_program.to_account_info(),
153                TransferChecked {
154                    authority: intent_transfer_setter.to_account_info(),
155                    from: source.to_account_info(),
156                    mint: mint.to_account_info(),
157                    to: destination.to_account_info(),
158                },
159                signer_seeds,
160            ),
161            try_ui_amount_into_amount(amount, mint.decimals)?,
162            mint.decimals,
163        )?;
164
165        self.verify_and_collect_fee(fee_amount, fee_symbol_or_mint, signer_seeds)
166    }
167}