Browse Source

lang: Add `Lamports` trait (#2552)

acheron 2 years ago
parent
commit
e55cd3e646

+ 2 - 0
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)).

+ 47 - 2
lang/src/lib.rs

@@ -137,6 +137,51 @@ where
     }
 }
 
+/// Lamports related utility methods for accounts.
+pub trait Lamports<'info>: AsRef<AccountInfo<'info>> {
+    /// 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<AccountInfo<'info>>> 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};

+ 3 - 2
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"]

+ 15 - 0
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" }

+ 2 - 0
tests/misc/programs/lamports/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 75 - 0
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<TestLamportsTrait>, 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 {}

+ 2 - 0
tests/misc/tests/lamports/Test.toml

@@ -0,0 +1,2 @@
+[scripts]
+test = "yarn run ts-mocha -t 1000000 ./tests/lamports/*.ts"

+ 23 - 0
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<Lamports>;
+
+  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();
+  });
+});

+ 9 - 10
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
-    },
-}
+  "compilerOptions": {
+    "types": ["mocha", "node"],
+    "lib": ["ES6"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true,
+    "skipLibCheck": true
+  }
+}

+ 0 - 3
tests/zero-copy/rust-toolchain.toml

@@ -1,3 +0,0 @@
-# TODO: Remove when `cargo-test-sbf` works with stable Rust
-[toolchain]
-channel = "1.66.1-x86_64-unknown-linux-gnu"