jpcaulfi 3 years ago
parent
commit
b9691061e4

+ 27 - 0
basics/cross-program-invocation/README.md

@@ -0,0 +1,27 @@
+# Cross Program Invocation (CPI)
+
+A cross-program invocation *literally* means invoking a program from another program (hence the term "cross-program"). There's really nothing special about it besides that. You're leveraging other programs from within your program to conduct business on Solana accounts.   
+
+Whether or not you should send instructions to a program using a cross-program invocation or a client RPC call is a design choice that's completely up to the developer.   
+
+There are many design considerations when making this decision, but the most common one to acknowledge is a **dependent operation** embedded in your program.   
+
+Consider the below sequence of operations of an example **token mint** program:
+1. Create & initialize the mint.
+2. Create a metadata account for that mint.
+3. Create & initialize a user's token account for that mint.
+4. Mint some tokens to the user's token account.
+
+In the above steps, we can't create a metadata account without first creating a mint! In fact, we have to do all of these operations in order.   
+
+Let's say we decided it was essential to have our mint (operation 1) and our "mint to user" (operation 4) tasks on-chain. We would have no choice but to also include the other two operations, since we can't do operation #1, pause the program while we do #2 & #3 from the client, and then resume the program for #4.
+
+### Let's switch the power on and off using a CPI!   
+
+<img src="istockphoto-1303616086-612x612.jpeg" alt="lever" width="128" align="center"/>
+
+In this example, we're just going to simulate a simple CPI - using one program's method from another program.   
+
+Inside our `hand` program's `pull_lever` function, there's a cross-program invocation to our `lever` program's `switch_power` method.   
+
+Simply put, **our hand program will pull the lever on the lever program to switch the power on and off**.

+ 15 - 0
basics/cross-program-invocation/anchor/Anchor.toml

@@ -0,0 +1,15 @@
+[features]
+seeds = false
+[programs.localnet]
+hand = "ABoYG2GWbzLgnnGhK2pUGNupzKoYe7UGk2idrAXbstAS"
+lever = "EnjN3cm7xYqYHNUZbQfhJYj5S5RBrSU9tc5aHwQ6LqvT"
+
+[registry]
+url = "https://anchor.projectserum.com"
+
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 13 - 0
basics/cross-program-invocation/anchor/Cargo.toml

@@ -0,0 +1,13 @@
+[workspace]
+members = [
+    "programs/*"
+]
+
+[profile.release]
+overflow-checks = true
+lto = "fat"
+codegen-units = 1
+[profile.release.build-override]
+opt-level = 3
+incremental = false
+codegen-units = 1

+ 14 - 0
basics/cross-program-invocation/anchor/package.json

@@ -0,0 +1,14 @@
+{
+    "dependencies": {
+        "@project-serum/anchor": "^0.24.2"
+    },
+    "devDependencies": {
+        "@types/bn.js": "^5.1.0",
+        "@types/chai": "^4.3.0",
+        "@types/mocha": "^9.0.0",
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "ts-mocha": "^10.0.0",
+        "typescript": "^4.3.5"
+    }
+}

+ 20 - 0
basics/cross-program-invocation/anchor/programs/hand/Cargo.toml

@@ -0,0 +1,20 @@
+[package]
+name = "hand"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "hand"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = "0.24.2"
+lever = { path = "../lever", features = ["cpi"] }

+ 2 - 0
basics/cross-program-invocation/anchor/programs/hand/Xargo.toml

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

+ 29 - 0
basics/cross-program-invocation/anchor/programs/hand/src/lib.rs

@@ -0,0 +1,29 @@
+use anchor_lang::prelude::*;
+use lever::cpi::accounts::SetPowerStatus;
+use lever::program::Lever;
+use lever::{self, PowerStatus};
+
+
+declare_id!("ABoYG2GWbzLgnnGhK2pUGNupzKoYe7UGk2idrAXbstAS");
+
+
+#[program]
+mod hand {
+    use super::*;
+    pub fn pull_lever(ctx: Context<PullLever>, name: String) -> anchor_lang::Result<()> {
+        let cpi_program = ctx.accounts.lever_program.to_account_info();
+        let cpi_accounts = SetPowerStatus {
+            power: ctx.accounts.power.to_account_info(),
+        };
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        lever::cpi::switch_power(cpi_ctx, name)
+    }
+}
+
+
+#[derive(Accounts)]
+pub struct PullLever<'info> {
+    #[account(mut)]
+    pub power: Account<'info, PowerStatus>,
+    pub lever_program: Program<'info, Lever>,
+}

+ 19 - 0
basics/cross-program-invocation/anchor/programs/lever/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "lever"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "lever"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = "0.24.2"

+ 2 - 0
basics/cross-program-invocation/anchor/programs/lever/Xargo.toml

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

+ 49 - 0
basics/cross-program-invocation/anchor/programs/lever/src/lib.rs

@@ -0,0 +1,49 @@
+use anchor_lang::prelude::*;
+
+
+declare_id!("EnjN3cm7xYqYHNUZbQfhJYj5S5RBrSU9tc5aHwQ6LqvT");
+
+
+#[program]
+pub mod lever {
+    use super::*;
+    pub fn initialize(_ctx: Context<InitializeLever>) -> Result<()> {
+        Ok(())
+    }
+
+    pub fn switch_power(ctx: Context<SetPowerStatus>, name: String) -> Result<()> {
+        
+        let power = &mut ctx.accounts.power;
+        power.is_on = !power.is_on;
+
+        msg!("{} is pulling the power switch!", &name);
+
+        match power.is_on {
+            true => msg!("The power is now on."),
+            false => msg!("The power is now off!"),
+        };
+
+        Ok(())
+    }
+}
+
+
+#[derive(Accounts)]
+pub struct InitializeLever<'info> {
+    #[account(init, payer = user, space = 8 + 8)]
+    pub power: Account<'info, PowerStatus>,
+    #[account(mut)]
+    pub user: Signer<'info>,
+    pub system_program: Program<'info, System>,
+}
+
+#[derive(Accounts)]
+pub struct SetPowerStatus<'info> {
+    #[account(mut)]
+    pub power: Account<'info, PowerStatus>,
+}
+
+#[account]
+pub struct PowerStatus {
+    pub is_on: bool,
+}

+ 48 - 0
basics/cross-program-invocation/anchor/tests/test.ts

@@ -0,0 +1,48 @@
+import * as anchor from "@project-serum/anchor";
+import { Hand } from "../target/types/hand";
+import { Lever } from "../target/types/lever";
+
+describe("hello-solana", () => {
+  
+  const provider = anchor.AnchorProvider.env();
+  anchor.setProvider(provider);
+  const hand = anchor.workspace.Hand as anchor.Program<Hand>;
+  const lever = anchor.workspace.Lever as anchor.Program<Lever>;
+
+  const powerAccount = anchor.web3.Keypair.generate();
+
+  it("Initialize the lever!", async () => {
+    
+    await lever.methods.initialize()
+    .accounts({
+      power: powerAccount.publicKey,
+      user: provider.wallet.publicKey,
+      systemProgram: anchor.web3.SystemProgram.programId,
+    })
+    .signers([powerAccount])
+    .rpc();
+
+  });
+
+  it("Pull the lever!", async () => {
+
+    await hand.methods.pullLever("Chris")
+    .accounts({
+      power: powerAccount.publicKey,
+      leverProgram: lever.programId,
+    })
+    .rpc();
+
+  });
+
+  it("Pull it again!", async () => {
+
+    await hand.methods.pullLever("Ashley")
+    .accounts({
+      power: powerAccount.publicKey,
+      leverProgram: lever.programId,
+    })
+    .rpc();
+
+  });
+});

+ 10 - 0
basics/cross-program-invocation/anchor/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

BIN
basics/cross-program-invocation/istockphoto-1303616086-612x612.jpeg


+ 8 - 0
basics/cross-program-invocation/native/cicd.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+# This script is for quick building & deploying of the program.
+# It also serves as a reference for the commands used for building & deploying Solana programs.
+# Run this bad boy with "bash cicd.sh" or "./cicd.sh"
+
+cargo build-bpf --manifest-path=./program/Cargo.toml --bpf-out-dir=./program/target/so
+solana program deploy ./program/target/so/program.so

+ 18 - 0
basics/cross-program-invocation/native/package.json

@@ -0,0 +1,18 @@
+{
+  "scripts": {
+    "test": "yarn run ts-mocha -p ./tsconfig.json -t 1000000 ./tests/test.ts"
+  },
+  "dependencies": {
+    "@solana/web3.js": "^1.47.3",
+    "fs": "^0.0.1-security"
+  },
+  "devDependencies": {
+    "@types/bn.js": "^5.1.0",
+    "@types/chai": "^4.3.1",
+    "@types/mocha": "^9.1.1",
+    "chai": "^4.3.4",
+    "mocha": "^9.0.3",
+    "ts-mocha": "^10.0.0",
+    "typescript": "^4.3.5"
+  }
+}

+ 10 - 0
basics/cross-program-invocation/native/program/Cargo.toml

@@ -0,0 +1,10 @@
+[package]
+name = "program"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+solana-program = "1.10.12"
+
+[lib]
+crate-type = ["cdylib", "lib"]

+ 29 - 0
basics/cross-program-invocation/native/program/src/lib.rs

@@ -0,0 +1,29 @@
+use solana_program::{
+    account_info::AccountInfo, 
+    entrypoint, 
+    entrypoint::ProgramResult, 
+    msg, 
+    pubkey::Pubkey,
+};
+
+
+// Tells Solana that the entrypoint to this program
+//  is the "process_instruction" function.
+//
+entrypoint!(process_instruction);
+
+
+// Our entrypoint's parameters have to match the
+//  anatomy of a transaction instruction (see README).
+//
+fn process_instruction(
+    program_id: &Pubkey,
+    accounts: &[AccountInfo],
+    instruction_data: &[u8],
+) -> ProgramResult {
+
+    
+    msg!("Hello, Solana!");
+
+    Ok(())
+}

+ 45 - 0
basics/cross-program-invocation/native/tests/test.ts

@@ -0,0 +1,45 @@
+import {
+    Connection,
+    Keypair,
+    sendAndConfirmTransaction,
+    Transaction,
+    TransactionInstruction,
+} from '@solana/web3.js';
+
+function createKeypairFromFile(path: string): Keypair {
+    return Keypair.fromSecretKey(
+        Buffer.from(JSON.parse(require('fs').readFileSync(path, "utf-8")))
+    )
+};
+
+
+describe("hello-solana", () => {
+
+    // Loading these from local files for development
+    //
+    const connection = new Connection(`http://localhost:8899`, 'confirmed');
+    const payer = createKeypairFromFile(require('os').homedir() + '/.config/solana/id.json');
+    const program = createKeypairFromFile('./program/target/so/program-keypair.json');
+  
+    it("Say hello!", async () => {
+
+        // We set up our instruction first.
+        //
+        let ix = new TransactionInstruction({
+            keys: [
+                {pubkey: payer.publicKey, isSigner: true, isWritable: true}
+            ],
+            programId: program.publicKey,
+            data: Buffer.alloc(0), // No data
+        });
+
+        // Now we send the transaction over RPC
+        //
+        await sendAndConfirmTransaction(
+            connection, 
+            new Transaction().add(ix), // Add our instruction (you can add more than one)
+            [payer]
+        );
+    });
+  });
+  

+ 10 - 0
basics/cross-program-invocation/native/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}