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