Parcourir la source

add cNFT vault example by Solandy

a simple Anchor program to demonstrate how to send cNFTs from a PDA
Solandy il y a 2 ans
Parent
commit
22654a893d

+ 7 - 0
tokens/cnft-vault/.gitignore

@@ -0,0 +1,7 @@
+
+.anchor
+.DS_Store
+target
+**/*.rs.bk
+node_modules
+test-ledger

+ 15 - 0
tokens/cnft-vault/Anchor.toml

@@ -0,0 +1,15 @@
+[features]
+seeds = false
+skip-lint = false
+[programs.devnet]
+cnft_vault = "CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk"
+
+[registry]
+url = "https://api.apr.dev"
+
+[provider]
+cluster = "Devnet"
+wallet = "../AndYPfCmbSSHpe2yukLXDT9N29twa7kJDk3yrRMQW7SN.json"
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

+ 13 - 0
tokens/cnft-vault/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

+ 22 - 0
tokens/cnft-vault/README.md

@@ -0,0 +1,22 @@
+# Solana Program cNFT Transfer example
+
+This repo contains example code of how you can work with Metaplex compressed NFTs inside of Solana Anchor programs.
+
+## Components
+
+The Anchor program can be found in the *programs* folder and *app* some typescript node scripts to interact with the program from client side.
+
+## Deployment
+
+The program is deployed on devnet at `CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk`. 
+You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml.
+
+## Limitations
+
+This is just an example implementation. It is missing all logic wheter a transfer should be performed or not (everyone can withdraw any cNFT in the vault). 
+Furthermore it is not optimized for using lowest possible compute. It is intended as a proof of concept and reference implemention only. 
+
+## Further resources
+
+A video about the creation of this code which also contains further explanations has been publised on Solandy's YouTube channel:
+https://youtu.be/qzr-q_E7H0M

+ 48 - 0
tokens/cnft-vault/app/readAPI.ts

@@ -0,0 +1,48 @@
+// I recommend using a WrappedConnection for production
+// as it supports more readAPI functionality
+// this is just a subset of functions for quick availabiity
+
+import axios from "axios";
+
+// TODO change to your API key (this is mine on free-tier)
+const HELIUS_RPC = "https://rpc-devnet.helius.xyz/?api-key=30536abf-e8e7-444f-a255-18e9a0c27e8b";
+
+export async function getAsset(assetId: any, rpcUrl = HELIUS_RPC): Promise<any> {
+  try {
+    const axiosInstance = axios.create({
+      baseURL: rpcUrl,
+    });
+    const response = await axiosInstance.post(rpcUrl, {
+      jsonrpc: "2.0",
+      method: "getAsset",
+      id: "rpd-op-123",
+      params: {
+        id: assetId
+      },
+    });
+    return response.data.result;
+  } catch (error) {
+    console.error(error);
+  }
+}
+
+
+export async function getAssetProof(assetId: any, rpcUrl = HELIUS_RPC): Promise<any> {
+  try {
+
+    const axiosInstance = axios.create({
+      baseURL: rpcUrl,
+    });
+    const response = await axiosInstance.post(rpcUrl, {
+      jsonrpc: "2.0",
+      method: "getAssetProof",
+      id: "rpd-op-123",
+      params: {
+        id: assetId
+      },
+    });
+    return response.data.result;
+  } catch (error) {
+    console.error(error);
+  }
+}

+ 31 - 0
tokens/cnft-vault/app/utils.ts

@@ -0,0 +1,31 @@
+import { Connection, Keypair, PublicKey, Signer, TransactionInstruction, TransactionMessage, VersionedTransaction, AccountMeta } from "@solana/web3.js";
+
+import * as bs58 from "bs58";
+
+export function loadWalletKey(keypairFile: string): Keypair {
+  const fs = require("fs")
+  return Keypair.fromSecretKey(
+    new Uint8Array(JSON.parse(fs.readFileSync(keypairFile).toString())),
+  );
+}
+
+export function decode(stuff: string) {
+  return bufferToArray(bs58.decode(stuff))
+}
+function bufferToArray(buffer: Buffer): number[] {
+  const nums: number[] = [];
+  for (let i = 0; i < buffer.length; i++) {
+    nums.push(buffer[i]);
+  }
+  return nums;
+}
+export const mapProof = (assetProof: { proof: string[] }): AccountMeta[] => {
+  if (!assetProof.proof || assetProof.proof.length === 0) {
+    throw new Error("Proof is empty");
+  }
+  return assetProof.proof.map((node) => ({
+    pubkey: new PublicKey(node),
+    isSigner: false,
+    isWritable: false,
+  }));
+};

+ 61 - 0
tokens/cnft-vault/app/withdraw.ts

@@ -0,0 +1,61 @@
+import * as anchor from "@project-serum/anchor";
+import { CnftVault } from "../target/types/cnft_vault";
+import { loadWalletKey, decode, mapProof } from "./utils";
+import { IDL } from "../target/types/cnft_vault"
+import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
+import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID } from "@solana/spl-account-compression";
+import { getAsset, getAssetProof } from "./readAPI";
+
+
+const connection = new anchor.web3.Connection("https://api.devnet.solana.com");
+const keypair = loadWalletKey("../AndYPfCmbSSHpe2yukLXDT9N29twa7kJDk3yrRMQW7SN.json");
+const wallet = new anchor.Wallet(keypair);
+const provider = new anchor.AnchorProvider(connection, wallet, {});
+const programID = new anchor.web3.PublicKey("CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk")
+const program = new anchor.Program<CnftVault>(IDL, programID, provider);
+async function main() {
+  const [vaultPDA, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
+    [Buffer.from("cNFT-vault", "utf8")],
+    programID,
+  );
+
+  const tree = new anchor.web3.PublicKey("trezdkTFPKyj4gE9LAJYPpxn8AYVCvM7Mc4JkTb9X5B")
+
+  const receiver = new anchor.web3.PublicKey("Andys9wuoMdUeRiZLgRS5aJwYNFv4Ut6qQi8PNDTAPEM")
+
+  const [treeAuthority, _bump2] = anchor.web3.PublicKey.findProgramAddressSync(
+    [tree.toBuffer()],
+    BUBBLEGUM_PROGRAM_ID,
+  );
+
+  const assetId = "DGWU3mHenDerCvjkeDsKYEbsvXbWvqdo1bVoXy3dkeTd";
+  const asset = await getAsset(assetId);
+  // console.log(res)
+
+  const proof = await getAssetProof(assetId);
+  const proofPathAsAccounts = mapProof(proof);
+
+  const root = decode(proof.root);
+  const dataHash = decode(asset.compression.data_hash);
+  const creatorHash = decode(asset.compression.creator_hash);
+  const nonce = new anchor.BN(asset.compression.leaf_id);
+  const index = asset.compression.leaf_id;
+
+  const tx = await program.methods.withdrawCnft(root, dataHash, creatorHash, nonce, index)
+    .accounts({
+      leafOwner: vaultPDA,
+      leafDelegate: vaultPDA,
+      merkleTree: tree,
+      newLeafOwner: receiver,
+      treeAuthority: treeAuthority,
+      bubblegumProgram: BUBBLEGUM_PROGRAM_ID,
+      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
+      logWrapper: SPL_NOOP_PROGRAM_ID,
+      systemProgram: anchor.web3.SystemProgram.programId
+    })
+    .remainingAccounts(proofPathAsAccounts)
+    .rpc();
+  console.log(tx);
+};
+
+main();

+ 87 - 0
tokens/cnft-vault/app/withdrawTwo.ts

@@ -0,0 +1,87 @@
+import * as anchor from "@project-serum/anchor";
+import { CnftVault } from "../target/types/cnft_vault";
+import { loadWalletKey, decode, mapProof } from "./utils";
+import { IDL } from "../target/types/cnft_vault"
+import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
+import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID } from "@solana/spl-account-compression";
+import { getAsset, getAssetProof } from "./readAPI";
+import { AccountMeta } from "@solana/web3.js";
+
+
+const connection = new anchor.web3.Connection("https://api.devnet.solana.com");
+const keypair = loadWalletKey("../AndYPfCmbSSHpe2yukLXDT9N29twa7kJDk3yrRMQW7SN.json");
+const wallet = new anchor.Wallet(keypair);
+const provider = new anchor.AnchorProvider(connection, wallet, {});
+const programID = new anchor.web3.PublicKey("CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk")
+const program = new anchor.Program<CnftVault>(IDL, programID, provider);
+async function main() {
+
+  // TODO change all of these to your values
+  const assetId1 = "DGWU3mHenDerCvjkeDsKYEbsvXbWvqdo1bVoXy3dkeTd";
+  const assetId2 = "14JojSTdBZvP7f77rCxB3oQK78skTVD6DiXrXUL4objg";//"D2CoMLCRfsfv1EAiNbaBHfoU1Sqf1964KXLGxEfyUwWo";
+
+  const tree1 = new anchor.web3.PublicKey("trezdkTFPKyj4gE9LAJYPpxn8AYVCvM7Mc4JkTb9X5B")
+  const tree2 = new anchor.web3.PublicKey("Feywkti8LLBLfxhSGmYgzUBqpq89qehfB1SMTYV1zCu")
+
+  const receiver1 = new anchor.web3.PublicKey("Andys9wuoMdUeRiZLgRS5aJwYNFv4Ut6qQi8PNDTAPEM")
+  const receiver2 = new anchor.web3.PublicKey("Andys9wuoMdUeRiZLgRS5aJwYNFv4Ut6qQi8PNDTAPEM")
+  // ---
+
+  const [vaultPDA, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
+    [Buffer.from("cNFT-vault", "utf8")],
+    programID,
+  );
+
+  const [treeAuthority1, _bump2] = anchor.web3.PublicKey.findProgramAddressSync(
+    [tree1.toBuffer()],
+    BUBBLEGUM_PROGRAM_ID,
+  );
+  const [treeAuthority2, _bump3] = anchor.web3.PublicKey.findProgramAddressSync(
+    [tree2.toBuffer()],
+    BUBBLEGUM_PROGRAM_ID,
+  );
+
+  const asset1 = await getAsset(assetId1);
+  const asset2 = await getAsset(assetId2);
+
+  const proof1 = await getAssetProof(assetId1);
+  const proofPathAsAccounts1 = mapProof(proof1);
+  const proof2 = await getAssetProof(assetId2);
+  const proofPathAsAccounts2 = mapProof(proof2);
+
+  const ixData1 = getInstructionData(asset1, proof1);
+  const ixData2 = getInstructionData(asset2, proof2);
+
+  const remainingAccounts: AccountMeta[] = [...proofPathAsAccounts1, ...proofPathAsAccounts2];
+
+  const tx = await program.methods.withdrawTwoCnfts(...ixData1, ...ixData2)
+    .accounts({
+      leafOwner: vaultPDA,
+      merkleTree1: tree1,
+      newLeafOwner1: receiver1,
+      treeAuthority1: treeAuthority1,
+      merkleTree2: tree2,
+      newLeafOwner2: receiver2,
+      treeAuthority2: treeAuthority2,
+      bubblegumProgram: BUBBLEGUM_PROGRAM_ID,
+      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
+      logWrapper: SPL_NOOP_PROGRAM_ID,
+      systemProgram: anchor.web3.SystemProgram.programId
+    })
+    .remainingAccounts(remainingAccounts)
+    .rpc();
+  console.log(tx);
+};
+
+function getInstructionData(asset: any, proof: any):
+  [number[], number[], number[], anchor.BN, number, number] {
+  const root = decode(proof.root);
+  const dataHash = decode(asset.compression.data_hash);
+  const creatorHash = decode(asset.compression.creator_hash);
+  const nonce = new anchor.BN(asset.compression.leaf_id);
+  const index = asset.compression.leaf_id;
+  const proofLength = proof.proof.length;
+  return [root, dataHash, creatorHash, nonce, index, proofLength];
+}
+
+main();

+ 169 - 0
tokens/cnft-vault/app/withdrawWithLookup.ts

@@ -0,0 +1,169 @@
+import * as anchor from "@project-serum/anchor";
+import { CnftVault } from "../target/types/cnft_vault";
+import { loadWalletKey, decode, mapProof } from "./utils";
+import { IDL } from "../target/types/cnft_vault"
+import { PROGRAM_ID as BUBBLEGUM_PROGRAM_ID } from "@metaplex-foundation/mpl-bubblegum";
+import { SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID } from "@solana/spl-account-compression";
+import { getAsset, getAssetProof } from "./readAPI";
+import { AccountMeta, AddressLookupTableProgram, PublicKey, SystemProgram, Transaction, TransactionMessage, VersionedTransaction, sendAndConfirmTransaction } from "@solana/web3.js";
+
+
+const connection = new anchor.web3.Connection("https://api.devnet.solana.com");
+const keypair = loadWalletKey("../AndYPfCmbSSHpe2yukLXDT9N29twa7kJDk3yrRMQW7SN.json");
+const wallet = new anchor.Wallet(keypair);
+const provider = new anchor.AnchorProvider(connection, wallet, {});
+const programID = new anchor.web3.PublicKey("CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk")
+const program = new anchor.Program<CnftVault>(IDL, programID, provider);
+async function main() {
+
+  // TODO change all of these to your values
+  const assetId1 = "DGWU3mHenDerCvjkeDsKYEbsvXbWvqdo1bVoXy3dkeTd";
+  const assetId2 = "14JojSTdBZvP7f77rCxB3oQK78skTVD6DiXrXUL4objg";//"D2CoMLCRfsfv1EAiNbaBHfoU1Sqf1964KXLGxEfyUwWo";
+
+  const tree1 = new anchor.web3.PublicKey("trezdkTFPKyj4gE9LAJYPpxn8AYVCvM7Mc4JkTb9X5B")
+  const tree2 = new anchor.web3.PublicKey("trezdkTFPKyj4gE9LAJYPpxn8AYVCvM7Mc4JkTb9X5B")
+
+  const receiver1 = new anchor.web3.PublicKey("Andys9wuoMdUeRiZLgRS5aJwYNFv4Ut6qQi8PNDTAPEM")
+  const receiver2 = new anchor.web3.PublicKey("Andys9wuoMdUeRiZLgRS5aJwYNFv4Ut6qQi8PNDTAPEM")
+  // ---
+
+  const lookupTable = await createLookupTable();
+
+  const [vaultPDA, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
+    [Buffer.from("cNFT-vault", "utf8")],
+    programID,
+  );
+
+  const [treeAuthority1, _bump2] = anchor.web3.PublicKey.findProgramAddressSync(
+    [tree1.toBuffer()],
+    BUBBLEGUM_PROGRAM_ID,
+  );
+  const [treeAuthority2, _bump3] = anchor.web3.PublicKey.findProgramAddressSync(
+    [tree2.toBuffer()],
+    BUBBLEGUM_PROGRAM_ID,
+  );
+
+  const asset1 = await getAsset(assetId1);
+  const asset2 = await getAsset(assetId2);
+
+  const proof1 = await getAssetProof(assetId1);
+  const proofPathAsAccounts1 = mapProof(proof1);
+  const proof2 = await getAssetProof(assetId2);
+  const proofPathAsAccounts2 = mapProof(proof2);
+
+  const ixData1 = getInstructionData(asset1, proof1);
+  const ixData2 = getInstructionData(asset2, proof2);
+
+  const remainingAccounts: AccountMeta[] = [...proofPathAsAccounts1, ...proofPathAsAccounts2];
+
+  const ix = await program.methods.withdrawTwoCnfts(...ixData1, ...ixData2)
+    .accounts({
+      leafOwner: vaultPDA,
+      merkleTree1: tree1,
+      newLeafOwner1: receiver1,
+      treeAuthority1: treeAuthority1,
+      merkleTree2: tree2,
+      newLeafOwner2: receiver2,
+      treeAuthority2: treeAuthority2,
+      bubblegumProgram: BUBBLEGUM_PROGRAM_ID,
+      compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
+      logWrapper: SPL_NOOP_PROGRAM_ID,
+      systemProgram: anchor.web3.SystemProgram.programId
+    })
+    .remainingAccounts(remainingAccounts)
+    .instruction();
+
+
+  await extendLookupTable(lookupTable, proofPathAsAccounts1.map(acc => acc.pubkey));
+  await extendLookupTable(lookupTable, proofPathAsAccounts2.map(acc => acc.pubkey));
+
+  const lookupTableAccount = await connection
+    .getAddressLookupTable(lookupTable)
+    .then((res) => res.value);
+
+  if (!lookupTableAccount) {
+    console.log("could not fetch ATL!");
+    return;
+  }
+
+  await new Promise(_ => setTimeout(_, 30000));
+
+  const messageV0 = new TransactionMessage({
+    payerKey: keypair.publicKey,
+    recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
+    instructions: [ix],
+  }).compileToV0Message([lookupTableAccount]);
+
+  const transactionV0 = new VersionedTransaction(messageV0);
+  transactionV0.sign([keypair]);
+
+  const txid = await connection.sendTransaction(transactionV0);
+  console.log(txid);
+};
+
+function getInstructionData(asset: any, proof: any):
+  [number[], number[], number[], anchor.BN, number, number] {
+  const root = decode(proof.root);
+  const dataHash = decode(asset.compression.data_hash);
+  const creatorHash = decode(asset.compression.creator_hash);
+  const nonce = new anchor.BN(asset.compression.leaf_id);
+  const index = asset.compression.leaf_id;
+  const proofLength = proof.proof.length;
+  return [root, dataHash, creatorHash, nonce, index, proofLength];
+}
+
+main();
+
+async function extendLookupTable(lookupTableAddress: PublicKey, proofHashes: PublicKey[]) {
+
+  const extendInstruction = AddressLookupTableProgram.extendLookupTable({
+    payer: keypair.publicKey,
+    authority: keypair.publicKey,
+    lookupTable: lookupTableAddress,
+    addresses: [
+      ...proofHashes
+    ],
+  });
+
+  const tx = new Transaction();
+  tx.add(extendInstruction);
+
+  const sx = await sendAndConfirmTransaction(connection, tx, [keypair], { commitment: "finalized" });
+  console.log(sx);
+  console.log("ALT extended!");
+}
+
+async function createLookupTable(): Promise<PublicKey> {
+  const slot = await connection.getSlot();
+
+  const [lookupTableInst, lookupTableAddress] =
+    AddressLookupTableProgram.createLookupTable({
+      authority: keypair.publicKey,
+      payer: keypair.publicKey,
+      recentSlot: slot,
+    });
+  console.log(lookupTableAddress.toBase58());
+
+  const extendInstruction = AddressLookupTableProgram.extendLookupTable({
+    payer: keypair.publicKey,
+    authority: keypair.publicKey,
+    lookupTable: lookupTableAddress,
+    addresses: [
+      programID,
+      SystemProgram.programId,
+      BUBBLEGUM_PROGRAM_ID,
+      SPL_ACCOUNT_COMPRESSION_PROGRAM_ID,
+      SPL_NOOP_PROGRAM_ID,
+      // you could add more addresses here, like merkle trees, leaf owners etc.
+    ],
+  });
+
+  const tx = new Transaction();
+  tx.add(lookupTableInst).add(extendInstruction);
+
+  const sx = await sendAndConfirmTransaction(connection, tx, [keypair], { commitment: "finalized" });
+  console.log(sx);
+  console.log("ALT created");
+
+  return lookupTableAddress;
+}

+ 19 - 0
tokens/cnft-vault/package.json

@@ -0,0 +1,19 @@
+{
+    "scripts": {
+        "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
+        "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
+    },
+    "dependencies": {
+        "@project-serum/anchor": "^0.26.0"
+    },
+    "devDependencies": {
+        "chai": "^4.3.4",
+        "mocha": "^9.0.3",
+        "ts-mocha": "^10.0.0",
+        "@types/bn.js": "^5.1.0",
+        "@types/chai": "^4.3.0",
+        "@types/mocha": "^9.0.0",
+        "typescript": "^4.3.5",
+        "prettier": "^2.6.2"
+    }
+}

+ 22 - 0
tokens/cnft-vault/programs/cnft-vault/Cargo.toml

@@ -0,0 +1,22 @@
+[package]
+name = "cnft-vault"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "cnft_vault"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = "0.26.0"
+solana-program = "*"
+spl-account-compression = { version="0.1.8", features = ["cpi"] }
+mpl-bubblegum = { version = "0.7.0", features = ["no-entrypoint", "cpi"] } 

+ 2 - 0
tokens/cnft-vault/programs/cnft-vault/Xargo.toml

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

+ 295 - 0
tokens/cnft-vault/programs/cnft-vault/src/lib.rs

@@ -0,0 +1,295 @@
+use anchor_lang::prelude::*;
+use solana_program::{pubkey::Pubkey};
+use spl_account_compression::{
+    program::SplAccountCompression, Noop,
+};
+use mpl_bubblegum::{state::TreeConfig};
+
+declare_id!("CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk");
+
+#[derive(Clone)]
+pub struct MplBubblegum;
+
+impl anchor_lang::Id for MplBubblegum {
+    fn id() -> Pubkey {
+        mpl_bubblegum::id()
+    }
+}
+
+#[program]
+pub mod cnft_vault {
+
+    use super::*;
+
+    pub fn withdraw_cnft<'info>(ctx: Context<'_, '_, '_, 'info, Withdraw<'info>>,
+        root: [u8; 32],
+        data_hash: [u8; 32],
+        creator_hash: [u8; 32],
+        nonce: u64,
+        index: u32,) -> Result<()> {
+        msg!("attempting to send nft {} from tree {}", index, ctx.accounts.merkle_tree.key());
+
+        // CPI to bubblegum
+        // //attempt 1
+        // mpl_bubblegum::cpi::transfer(
+        //     CpiContext::new_with_signer(
+        //         ctx.accounts.bubblegum_program.to_account_info(), 
+        //         mpl_bubblegum::cpi::accounts::Transfer{
+        //             tree_authority: ctx.accounts.tree_authority.to_account_info(),
+        //             leaf_owner: ctx.accounts.leaf_owner.to_account_info(),
+        //             leaf_delegate: ctx.accounts.leaf_delegate.to_account_info(),
+        //             new_leaf_owner: ctx.accounts.new_leaf_owner.to_account_info(),
+        //             merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
+        //             log_wrapper: ctx.accounts.log_wrapper.to_account_info(),
+        //             compression_program: ctx.accounts.compression_program.to_account_info(),
+        //             system_program: ctx.accounts.system_program.to_account_info(),
+        //         }, &[&[b"cNFT-vault", &[*ctx.bumps.get("vault").unwrap()]]]),
+        //         root, data_hash, creator_hash, nonce, index)
+        
+        //attempt 2
+        let mut accounts:  Vec<solana_program::instruction::AccountMeta> = vec![
+            AccountMeta::new_readonly(ctx.accounts.tree_authority.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
+            AccountMeta::new_readonly(ctx.accounts.leaf_delegate.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.new_leaf_owner.key(), false),
+            AccountMeta::new(ctx.accounts.merkle_tree.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
+        ];
+
+        // first 8 bytes of SHA256("global:transfer")   
+        let transfer_discriminator: [u8;8] = [163, 52, 200, 231, 140, 3, 69, 186];//hex::decode("a334c8e78c0345ba").expect("hex decode fail"); 
+        //msg!("{:?}", transfer_discriminator);
+        
+        let mut data: Vec<u8> = vec![];
+        data.extend(transfer_discriminator);
+        data.extend(root);
+        data.extend(data_hash);
+        data.extend(creator_hash);
+        data.extend(nonce.to_le_bytes());
+        data.extend(index.to_le_bytes());
+
+        let mut account_infos: Vec<AccountInfo> = vec![
+            ctx.accounts.tree_authority.to_account_info(),
+            ctx.accounts.leaf_owner.to_account_info(),
+            ctx.accounts.leaf_delegate.to_account_info(),
+            ctx.accounts.new_leaf_owner.to_account_info(),
+            ctx.accounts.merkle_tree.to_account_info(),
+            ctx.accounts.log_wrapper.to_account_info(),
+            ctx.accounts.compression_program.to_account_info(),
+            ctx.accounts.system_program.to_account_info(),
+        ];
+        
+        // add "accounts" (hashes) that make up the merkle proof
+        for acc in ctx.remaining_accounts.iter() {
+            accounts.push(AccountMeta::new_readonly(acc.key(), false));
+            account_infos.push(acc.to_account_info());
+        }
+
+        msg!("manual cpi call");
+        solana_program::program::invoke_signed(
+        & solana_program::instruction::Instruction {
+            program_id: ctx.accounts.bubblegum_program.key(),
+            accounts: accounts,
+            data: data,
+        },
+        &account_infos[..],
+        &[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])
+        .map_err(Into::into)
+
+        
+    }
+
+    
+    pub fn withdraw_two_cnfts<'info>(ctx: Context<'_, '_, '_, 'info, WithdrawTwo<'info>>,
+        root1: [u8; 32],
+        data_hash1: [u8; 32],
+        creator_hash1: [u8; 32],
+        nonce1: u64,
+        index1: u32,
+        proof_1_length: u8,
+        root2: [u8; 32],
+        data_hash2: [u8; 32],
+        creator_hash2: [u8; 32],
+        nonce2: u64,
+        index2: u32,
+        _proof_2_length: u8 // we don't actually need this (proof_2_length = remaining_accounts_len - proof_1_length)
+    ) -> Result<()> {
+        let merkle_tree1 = ctx.accounts.merkle_tree1.key();
+        let merkle_tree2 = ctx.accounts.merkle_tree2.key();
+        msg!("attempting to send nfts from trees {} and {}", merkle_tree1, merkle_tree2);
+        
+        // TODO check if nft transfers are even valid (correct NFT, correct authority)
+        // in this example anyone can withdraw any NFT from the vault
+
+        let mut accounts1:  Vec<solana_program::instruction::AccountMeta> = vec![
+            AccountMeta::new_readonly(ctx.accounts.tree_authority1.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
+            AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.new_leaf_owner1.key(), false),
+            AccountMeta::new(ctx.accounts.merkle_tree1.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
+        ];
+        
+        let mut accounts2:  Vec<solana_program::instruction::AccountMeta> = vec![
+            AccountMeta::new_readonly(ctx.accounts.tree_authority2.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
+            AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.new_leaf_owner2.key(), false),
+            AccountMeta::new(ctx.accounts.merkle_tree2.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
+            AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
+        ];
+
+        let transfer_discriminator: [u8;8] = [163, 52, 200, 231, 140, 3, 69, 186];
+
+        let mut data1: Vec<u8> = vec![];
+        data1.extend(&transfer_discriminator);
+        data1.extend(root1);
+        data1.extend(data_hash1);
+        data1.extend(creator_hash1);
+        data1.extend(nonce1.to_le_bytes());
+        data1.extend(index1.to_le_bytes());
+        let mut data2: Vec<u8> = vec![];
+        data2.extend(&transfer_discriminator);
+        data2.extend(root2);
+        data2.extend(data_hash2);
+        data2.extend(creator_hash2);
+        data2.extend(nonce2.to_le_bytes());
+        data2.extend(index2.to_le_bytes());
+
+        let mut account_infos1: Vec<AccountInfo> = vec![
+            ctx.accounts.tree_authority1.to_account_info(),
+            ctx.accounts.leaf_owner.to_account_info(),
+            ctx.accounts.leaf_owner.to_account_info(),
+            ctx.accounts.new_leaf_owner1.to_account_info(),
+            ctx.accounts.merkle_tree1.to_account_info(),
+            ctx.accounts.log_wrapper.to_account_info(),
+            ctx.accounts.compression_program.to_account_info(),
+            ctx.accounts.system_program.to_account_info(),
+        ];
+        let mut account_infos2: Vec<AccountInfo> = vec![
+            ctx.accounts.tree_authority2.to_account_info(),
+            ctx.accounts.leaf_owner.to_account_info(),
+            ctx.accounts.leaf_owner.to_account_info(),
+            ctx.accounts.new_leaf_owner2.to_account_info(),
+            ctx.accounts.merkle_tree2.to_account_info(),
+            ctx.accounts.log_wrapper.to_account_info(),
+            ctx.accounts.compression_program.to_account_info(),
+            ctx.accounts.system_program.to_account_info(),
+        ];
+        
+        // add "accounts" (hashes) that make up the merkle proof
+        let mut i = 0u8;
+        for acc in ctx.remaining_accounts.iter() {
+            if i < proof_1_length {
+                accounts1.push(AccountMeta::new_readonly(acc.key(), false));
+                account_infos1.push(acc.to_account_info());
+            } else {
+                accounts2.push(AccountMeta::new_readonly(acc.key(), false));
+                account_infos2.push(acc.to_account_info());
+            }
+            i+=1;
+        }
+
+        msg!("withdrawing cNFT#1");
+        solana_program::program::invoke_signed(
+        & solana_program::instruction::Instruction {
+            program_id: ctx.accounts.bubblegum_program.key(),
+            accounts: accounts1,
+            data: data1,
+        },
+        &account_infos1[..],
+        &[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])?;
+        
+        msg!("withdrawing cNFT#2");
+        solana_program::program::invoke_signed(
+        & solana_program::instruction::Instruction {
+            program_id: ctx.accounts.bubblegum_program.key(),
+            accounts: accounts2,
+            data: data2,
+        },
+        &account_infos2[..],
+        &[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])?;
+
+        msg!("successfully sent cNFTs");
+        Ok(())
+
+        
+    }
+
+}
+
+#[derive(Accounts)]
+pub struct Withdraw<'info> {
+    #[account(
+        seeds = [merkle_tree.key().as_ref()],
+        bump, 
+        seeds::program = bubblegum_program.key()
+    )]
+    /// CHECK: This account is neither written to nor read from.
+    pub tree_authority: Account<'info, TreeConfig>,
+    
+    #[account(
+        seeds = [b"cNFT-vault"],
+        bump,
+    )]
+    /// CHECK: This account doesnt even exist (it is just the pda to sign)
+    pub leaf_owner: UncheckedAccount<'info>, // sender (the vault in our case)
+    /// CHECK: This account is chekced in the instruction
+    pub leaf_delegate: UncheckedAccount<'info>, // we could actually remove this and just use leaf_owner instead
+    /// CHECK: This account is neither written to nor read from.
+    pub new_leaf_owner: UncheckedAccount<'info>, // receiver
+    #[account(mut)]
+    /// CHECK: This account is modified in the downstream program
+    pub merkle_tree: UncheckedAccount<'info>,
+    pub log_wrapper: Program<'info, Noop>,
+    pub compression_program: Program<'info, SplAccountCompression>,
+    pub bubblegum_program: Program<'info, MplBubblegum>,
+    pub system_program: Program<'info, System>,
+}
+
+#[derive(Accounts)]
+pub struct WithdrawTwo<'info> {
+    #[account(
+        seeds = [merkle_tree1.key().as_ref()],
+        bump, 
+        seeds::program = bubblegum_program.key()
+    )]
+    /// CHECK: This account is neither written to nor read from.
+    pub tree_authority1: Account<'info, TreeConfig>,
+    #[account(
+        seeds = [b"cNFT-vault"],
+        bump,
+    )]
+    /// CHECK: This account doesnt even exist (it is just the pda to sign)
+    pub leaf_owner: UncheckedAccount<'info>, // you might need two accounts if the nfts are owned by two different PDAs
+    /// CHECK: This account is neither written to nor read from.
+    pub new_leaf_owner1: UncheckedAccount<'info>, // receiver
+    #[account(mut)]
+    /// CHECK: This account is modified in the downstream program
+    pub merkle_tree1: UncheckedAccount<'info>,
+    
+    #[account(
+        seeds = [merkle_tree2.key().as_ref()],
+        bump, 
+        seeds::program = bubblegum_program.key()
+    )]
+    /// CHECK: This account is neither written to nor read from.
+    pub tree_authority2: Account<'info, TreeConfig>,
+    /// CHECK: This account is neither written to nor read from.
+    pub new_leaf_owner2: UncheckedAccount<'info>, // receiver
+    #[account(mut)]
+    /// CHECK: This account is modified in the downstream program
+    pub merkle_tree2: UncheckedAccount<'info>,
+
+    pub log_wrapper: Program<'info, Noop>,
+    pub compression_program: Program<'info, SplAccountCompression>,
+    pub bubblegum_program: Program<'info, MplBubblegum>,
+    pub system_program: Program<'info, System>,
+}
+

+ 11 - 0
tokens/cnft-vault/tsconfig.json

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