diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fedb3f49..5abe5166a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The minor version will be incremented upon a breaking change and the patch versi ### Features +- lang: Add `get_lamports`, `add_lamports` and `sub_lamports` methods for all account types ([#2552](https://github.com/coral-xyz/anchor/pull/2552)). + ### Fixes - ts: Packages no longer depend on `assert` ([#2535](https://github.com/coral-xyz/anchor/pull/2535)). diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 1fb1a8091..afab0007b 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -137,6 +137,51 @@ where } } +/// Lamports related utility methods for accounts. +pub trait Lamports<'info>: AsRef> { + /// Get the lamports of the account. + fn get_lamports(&self) -> u64 { + self.as_ref().lamports() + } + + /// Add lamports to the account. + /// + /// This method is useful for transfering lamports from a PDA. + /// + /// # Requirements + /// + /// 1. The account must be marked `mut`. + /// 2. The total lamports **before** the transaction must equal to total lamports **after** + /// the transaction. + /// 3. `lamports` field of the account info should not currently be borrowed. + /// + /// See [`Lamports::sub_lamports`] for subtracting lamports. + fn add_lamports(&self, amount: u64) -> Result<&Self> { + **self.as_ref().try_borrow_mut_lamports()? += amount; + Ok(self) + } + + /// Subtract lamports from the account. + /// + /// This method is useful for transfering lamports from a PDA. + /// + /// # Requirements + /// + /// 1. The account must be owned by the executing program. + /// 2. The account must be marked `mut`. + /// 3. The total lamports **before** the transaction must equal to total lamports **after** + /// the transaction. + /// 4. `lamports` field of the account info should not currently be borrowed. + /// + /// See [`Lamports::add_lamports`] for adding lamports. + fn sub_lamports(&self, amount: u64) -> Result<&Self> { + **self.as_ref().try_borrow_mut_lamports()? -= amount; + Ok(self) + } +} + +impl<'info, T: AsRef>> Lamports<'info> for T {} + /// A data structure that can be serialized and stored into account storage, /// i.e. an /// [`AccountInfo`](../solana_program/account_info/struct.AccountInfo.html#structfield.data)'s @@ -300,8 +345,8 @@ pub mod prelude { require, require_eq, require_gt, require_gte, require_keys_eq, require_keys_neq, require_neq, solana_program::bpf_loader_upgradeable::UpgradeableLoaderState, source, system_program::System, zero_copy, AccountDeserialize, AccountSerialize, Accounts, - AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, Owner, - ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, + AccountsClose, AccountsExit, AnchorDeserialize, AnchorSerialize, Id, InitSpace, Key, + Lamports, Owner, ProgramData, Result, Space, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; #[cfg(feature = "event-cpi")] pub use super::{emit_cpi, event_cpi}; diff --git a/tests/misc/Anchor.toml b/tests/misc/Anchor.toml index 5c8022e11..61d78711a 100644 --- a/tests/misc/Anchor.toml +++ b/tests/misc/Anchor.toml @@ -3,10 +3,11 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [programs.localnet] -misc = "3TEqcc8xhrhdspwbvoamUJe2borm4Nr72JxL66k6rgrh" -misc_optional = "FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG" idl_doc = "BqmKjZGVa8fqyWuojJzG16zaKSV1GjAisZToNuvEaz6m" init_if_needed = "BZoppwWi6jMnydnUBEJzotgEXHwLr3b3NramJgZtWeF2" +lamports = "Lamports11111111111111111111111111111111111" +misc = "3TEqcc8xhrhdspwbvoamUJe2borm4Nr72JxL66k6rgrh" +misc_optional = "FNqz6pqLAwvMSds2FYjR4nKV3moVpPNtvkfGFrqLKrgG" [workspace] exclude = ["programs/shared"] diff --git a/tests/misc/programs/lamports/Cargo.toml b/tests/misc/programs/lamports/Cargo.toml new file mode 100644 index 000000000..ea99d59e5 --- /dev/null +++ b/tests/misc/programs/lamports/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "lamports" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[features] +no-entrypoint = [] +cpi = ["no-entrypoint"] + +[dependencies] +anchor-lang = { path = "../../../../lang" } diff --git a/tests/misc/programs/lamports/Xargo.toml b/tests/misc/programs/lamports/Xargo.toml new file mode 100644 index 000000000..1744f098a --- /dev/null +++ b/tests/misc/programs/lamports/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/tests/misc/programs/lamports/src/lib.rs b/tests/misc/programs/lamports/src/lib.rs new file mode 100644 index 000000000..84ac072d2 --- /dev/null +++ b/tests/misc/programs/lamports/src/lib.rs @@ -0,0 +1,75 @@ +use anchor_lang::prelude::*; + +declare_id!("Lamports11111111111111111111111111111111111"); + +#[program] +pub mod lamports { + use super::*; + + pub fn test_lamports_trait(ctx: Context, amount: u64) -> Result<()> { + let pda = &ctx.accounts.pda; + let signer = &ctx.accounts.signer; + + // Transfer **to** PDA + { + // Get the balance of the PDA **before** the transfer to PDA + let pda_balance_before = pda.get_lamports(); + + // Transfer to the PDA + anchor_lang::system_program::transfer( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: signer.to_account_info(), + to: pda.to_account_info(), + }, + ), + amount, + )?; + + // Get the balance of the PDA **after** the transfer to PDA + let pda_balance_after = pda.get_lamports(); + + // Validate balance + require_eq!(pda_balance_after, pda_balance_before + amount); + } + + // Transfer **from** PDA + { + // Get the balance of the PDA **before** the transfer from PDA + let pda_balance_before = pda.get_lamports(); + + // Transfer from the PDA + pda.sub_lamports(amount)?; + signer.add_lamports(amount)?; + + // Get the balance of the PDA **after** the transfer from PDA + let pda_balance_after = pda.get_lamports(); + + // Validate balance + require_eq!(pda_balance_after, pda_balance_before - amount); + } + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct TestLamportsTrait<'info> { + #[account(mut)] + pub signer: Signer<'info>, + + #[account( + init, + payer = signer, + space = 8, + seeds = [b"lamports"], + bump + )] + pub pda: Account<'info, LamportsPda>, + + pub system_program: Program<'info, System>, +} + +#[account] +pub struct LamportsPda {} diff --git a/tests/misc/tests/lamports/Test.toml b/tests/misc/tests/lamports/Test.toml new file mode 100644 index 000000000..a95a50b7e --- /dev/null +++ b/tests/misc/tests/lamports/Test.toml @@ -0,0 +1,2 @@ +[scripts] +test = "yarn run ts-mocha -t 1000000 ./tests/lamports/*.ts" diff --git a/tests/misc/tests/lamports/lamports.ts b/tests/misc/tests/lamports/lamports.ts new file mode 100644 index 000000000..678ed5c79 --- /dev/null +++ b/tests/misc/tests/lamports/lamports.ts @@ -0,0 +1,23 @@ +import * as anchor from "@coral-xyz/anchor"; + +import { Lamports, IDL } from "../../target/types/lamports"; + +describe(IDL.name, () => { + // Configure the client to use the local cluster + anchor.setProvider(anchor.AnchorProvider.env()); + + const program = anchor.workspace.Lamports as anchor.Program; + + it("Can use the Lamports trait", async () => { + const signer = program.provider.publicKey!; + const [pda] = anchor.web3.PublicKey.findProgramAddressSync( + [Buffer.from("lamports")], + program.programId + ); + + await program.methods + .testLamportsTrait(new anchor.BN(anchor.web3.LAMPORTS_PER_SOL)) + .accounts({ signer, pda }) + .rpc(); + }); +}); diff --git a/tests/misc/tsconfig.json b/tests/misc/tsconfig.json index d501e0599..95b932ed5 100644 --- a/tests/misc/tsconfig.json +++ b/tests/misc/tsconfig.json @@ -1,11 +1,10 @@ { - "compilerOptions": { - "types": ["mocha", "chai"], - "typeRoots": ["./node_modules/@types"], - "lib": ["es2015"], - "module": "commonjs", - "target": "es6", - "esModuleInterop": true, - "skipLibCheck": true - }, -} \ No newline at end of file + "compilerOptions": { + "types": ["mocha", "node"], + "lib": ["ES6"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/tests/zero-copy/rust-toolchain.toml b/tests/zero-copy/rust-toolchain.toml deleted file mode 100644 index 7bd425332..000000000 --- a/tests/zero-copy/rust-toolchain.toml +++ /dev/null @@ -1,3 +0,0 @@ -# TODO: Remove when `cargo-test-sbf` works with stable Rust -[toolchain] -channel = "1.66.1-x86_64-unknown-linux-gnu"