diff --git a/examples/misc/tests/misc.js b/examples/misc/tests/misc.js index ead0ed537e..219d2204bb 100644 --- a/examples/misc/tests/misc.js +++ b/examples/misc/tests/misc.js @@ -386,6 +386,28 @@ describe("misc", () => { assert.ok(account.data === 3); }); + it("Can init a random account prefunded", async () => { + const data = anchor.web3.Keypair.generate(); + await program.rpc.testInit({ + accounts: { + data: data.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + }, + signers: [data], + instructions: [ + anchor.web3.SystemProgram.transfer({ + fromPubkey: program.provider.wallet.publicKey, + toPubkey: data.publicKey, + lamports: 4039280, + }), + ], + }); + + const account = await program.account.dataI8.fetch(data.publicKey); + assert.ok(account.data === 3); + }); + it("Can init a random zero copy account", async () => { const data = anchor.web3.Keypair.generate(); await program.rpc.testInitZeroCopy({ @@ -428,6 +450,38 @@ describe("misc", () => { ); }); + it("Can create a random mint account prefunded", async () => { + mint = anchor.web3.Keypair.generate(); + await program.rpc.testInitMint({ + accounts: { + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [mint], + instructions: [ + anchor.web3.SystemProgram.transfer({ + fromPubkey: program.provider.wallet.publicKey, + toPubkey: mint.publicKey, + lamports: 4039280, + }), + ], + }); + const client = new Token( + program.provider.connection, + mint.publicKey, + TOKEN_PROGRAM_ID, + program.provider.wallet.payer + ); + const mintAccount = await client.getMintInfo(); + assert.ok(mintAccount.decimals === 6); + assert.ok( + mintAccount.mintAuthority.equals(program.provider.wallet.publicKey) + ); + }); + it("Can create a random token account", async () => { const token = anchor.web3.Keypair.generate(); await program.rpc.testInitToken({ @@ -454,4 +508,72 @@ describe("misc", () => { assert.ok(account.owner.equals(program.provider.wallet.publicKey)); assert.ok(account.mint.equals(mint.publicKey)); }); + + it("Can create a random token with prefunding", async () => { + const token = anchor.web3.Keypair.generate(); + await program.rpc.testInitToken({ + accounts: { + token: token.publicKey, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [token], + instructions: [ + anchor.web3.SystemProgram.transfer({ + fromPubkey: program.provider.wallet.publicKey, + toPubkey: token.publicKey, + lamports: 4039280, + }), + ], + }); + const client = new Token( + program.provider.connection, + mint.publicKey, + TOKEN_PROGRAM_ID, + program.provider.wallet.payer + ); + const account = await client.getAccountInfo(token.publicKey); + assert.ok(account.state === 1); + assert.ok(account.amount.toNumber() === 0); + assert.ok(account.isInitialized); + assert.ok(account.owner.equals(program.provider.wallet.publicKey)); + assert.ok(account.mint.equals(mint.publicKey)); + }); + + it("Can create a random token with prefunding under the rent exemption", async () => { + const token = anchor.web3.Keypair.generate(); + await program.rpc.testInitToken({ + accounts: { + token: token.publicKey, + mint: mint.publicKey, + payer: program.provider.wallet.publicKey, + systemProgram: anchor.web3.SystemProgram.programId, + tokenProgram: TOKEN_PROGRAM_ID, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [token], + instructions: [ + anchor.web3.SystemProgram.transfer({ + fromPubkey: program.provider.wallet.publicKey, + toPubkey: token.publicKey, + lamports: 1, + }), + ], + }); + const client = new Token( + program.provider.connection, + mint.publicKey, + TOKEN_PROGRAM_ID, + program.provider.wallet.payer + ); + const account = await client.getAccountInfo(token.publicKey); + assert.ok(account.state === 1); + assert.ok(account.amount.toNumber() === 0); + assert.ok(account.isInitialized); + assert.ok(account.owner.equals(program.provider.wallet.publicKey)); + assert.ok(account.mint.equals(mint.publicKey)); + }); }); diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index 9b4289eb97..b8ff696ca9 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -446,88 +446,66 @@ pub fn generate_pda( }; match kind { - InitKind::Token { owner, mint } => quote! { - let #field: #combined_account_ty = { - #payer - - // Fund the account for rent exemption. - let required_lamports = __anchor_rent - .minimum_balance(anchor_spl::token::TokenAccount::LEN) - .max(1) - .saturating_sub(#field.to_account_info().lamports()); - - // Create the token account with right amount of lamports and space, and the correct owner. - anchor_lang::solana_program::program::invoke_signed( - &anchor_lang::solana_program::system_instruction::create_account( - payer.to_account_info().key, - #field.to_account_info().key, - required_lamports, - anchor_spl::token::TokenAccount::LEN as u64, - token_program.to_account_info().key, - ), - &[ - payer.to_account_info(), - #field.to_account_info(), - system_program.to_account_info().clone(), - ], - &[#seeds_with_nonce], - )?; + InitKind::Token { owner, mint } => { + let create_account = generate_create_account( + field, + quote! {anchor_spl::token::TokenAccount::LEN}, + quote! {token_program.to_account_info().key}, + seeds_with_nonce, + ); + quote! { + let #field: #combined_account_ty = { + // Define payer variable. + #payer - // Initialize the token account. - let cpi_program = token_program.to_account_info(); - let accounts = anchor_spl::token::InitializeAccount { - account: #field.to_account_info(), - mint: #mint.to_account_info(), - authority: #owner.to_account_info(), - rent: rent.to_account_info(), + // Create the account with the system program. + #create_account + + // Initialize the token account. + let cpi_program = token_program.to_account_info(); + let accounts = anchor_spl::token::InitializeAccount { + account: #field.to_account_info(), + mint: #mint.to_account_info(), + authority: #owner.to_account_info(), + rent: rent.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, accounts); + anchor_spl::token::initialize_account(cpi_ctx)?; + anchor_lang::CpiAccount::try_from_unchecked( + &#field.to_account_info(), + )? }; - let cpi_ctx = CpiContext::new(cpi_program, accounts); - anchor_spl::token::initialize_account(cpi_ctx)?; - anchor_lang::CpiAccount::try_from_unchecked( - &#field.to_account_info(), - )? - }; - }, - InitKind::Mint { owner, decimals } => quote! { - let #field: #combined_account_ty = { - #payer - - // Fund the account for rent exemption. - let required_lamports = rent - .minimum_balance(anchor_spl::token::Mint::LEN) - .max(1) - .saturating_sub(#field.to_account_info().lamports()); - - // Create the token account with right amount of lamports and space, and the correct owner. - anchor_lang::solana_program::program::invoke_signed( - &anchor_lang::solana_program::system_instruction::create_account( - payer.to_account_info().key, - #field.to_account_info().key, - required_lamports, - anchor_spl::token::Mint::LEN as u64, - token_program.to_account_info().key, - ), - &[ - payer.to_account_info(), - #field.to_account_info(), - system_program.to_account_info().clone(), - ], - &[#seeds_with_nonce], - )?; + } + } + InitKind::Mint { owner, decimals } => { + let create_account = generate_create_account( + field, + quote! {anchor_spl::token::Mint::LEN}, + quote! {token_program.to_account_info().key}, + seeds_with_nonce, + ); + quote! { + let #field: #combined_account_ty = { + // Define payer variable. + #payer - // Initialize the mint account. - let cpi_program = token_program.to_account_info(); - let accounts = anchor_spl::token::InitializeMint { - mint: #field.to_account_info(), - rent: rent.to_account_info(), + // Create the account with the system program. + #create_account + + // Initialize the mint account. + let cpi_program = token_program.to_account_info(); + let accounts = anchor_spl::token::InitializeMint { + mint: #field.to_account_info(), + rent: rent.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, accounts); + anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.to_account_info().key, None)?; + anchor_lang::CpiAccount::try_from_unchecked( + &#field.to_account_info(), + )? }; - let cpi_ctx = CpiContext::new(cpi_program, accounts); - anchor_spl::token::initialize_mint(cpi_ctx, #decimals, &#owner.to_account_info().key, None)?; - anchor_lang::CpiAccount::try_from_unchecked( - &#field.to_account_info(), - )? - }; - }, + } + } InitKind::Program { owner } => { let space = match space { // If no explicit space param was given, serialize the type to bytes @@ -560,34 +538,13 @@ pub fn generate_pda( &#o }, }; + let create_account = + generate_create_account(field, quote! {space}, owner, seeds_with_nonce); quote! { let #field = { #space #payer - - let lamports = __anchor_rent.minimum_balance(space); - let ix = anchor_lang::solana_program::system_instruction::create_account( - payer.to_account_info().key, - #field.to_account_info().key, - lamports, - space as u64, - #owner, - ); - - anchor_lang::solana_program::program::invoke_signed( - &ix, - &[ - - #field.to_account_info(), - payer.to_account_info(), - system_program.to_account_info(), - ], - &[#seeds_with_nonce], - ).map_err(|e| { - anchor_lang::solana_program::msg!("Unable to create associated account"); - e - })?; - + #create_account let mut pa: #combined_account_ty = #try_from; pa }; @@ -596,6 +553,89 @@ pub fn generate_pda( } } +// Generated code to create an account with with system program with the +// given `space` amount of data, owned by `owner`. +// +// `seeds_with_nonce` should be given for creating PDAs. Otherwise it's an +// empty stream. +pub fn generate_create_account( + field: &Ident, + space: proc_macro2::TokenStream, + owner: proc_macro2::TokenStream, + seeds_with_nonce: proc_macro2::TokenStream, +) -> proc_macro2::TokenStream { + quote! { + // If the account being initialized already has lamports, then + // return them all back to the payer so that the account has + // zero lamports when the system program's create instruction + // is eventually called. + let __current_lamports = #field.to_account_info().lamports(); + if __current_lamports == 0 { + // Create the token account with right amount of lamports and space, and the correct owner. + let lamports = __anchor_rent.minimum_balance(#space); + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::create_account( + payer.to_account_info().key, + #field.to_account_info().key, + lamports, + #space as u64, + #owner, + ), + &[ + payer.to_account_info(), + #field.to_account_info(), + system_program.to_account_info().clone(), + ], + &[#seeds_with_nonce], + )?; + } else { + // Fund the account for rent exemption. + let required_lamports = __anchor_rent + .minimum_balance(#space) + .max(1) + .saturating_sub(__current_lamports); + if required_lamports > 0 { + anchor_lang::solana_program::program::invoke( + &anchor_lang::solana_program::system_instruction::transfer( + payer.to_account_info().key, + #field.to_account_info().key, + required_lamports, + ), + &[ + payer.to_account_info(), + #field.to_account_info(), + system_program.to_account_info().clone(), + ], + )?; + } + // Allocate space. + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::allocate( + #field.to_account_info().key, + #space as u64, + ), + &[ + #field.to_account_info(), + system_program.clone(), + ], + &[#seeds_with_nonce], + )?; + // Assign to the spl token program. + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::assign( + #field.to_account_info().key, + #owner, + ), + &[ + #field.to_account_info(), + system_program.to_account_info(), + ], + &[#seeds_with_nonce], + )?; + } + } +} + pub fn generate_constraint_executable( f: &Field, _c: &ConstraintExecutable, diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index b148e9fd82..d7e45e14e9 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -436,6 +436,8 @@ pub struct ConstraintSpace { #[allow(clippy::large_enum_variant)] pub enum InitKind { Program { owner: Option }, + // Owner for token and mint represents the authority. Not to be confused + // with the owner of the AccountInfo. Token { owner: Expr, mint: Expr }, Mint { owner: Expr, decimals: Expr }, }