Skip to content

Commit 3c224fa

Browse files
authored
Merge pull request #24 from loopcreativeandy/main
add cNFT vault example by Solandy
2 parents 666c29f + a0e20e3 commit 3c224fa

File tree

16 files changed

+914
-0
lines changed

16 files changed

+914
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
**/*.rs.bk
6+
node_modules
7+
test-ledger
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[features]
2+
seeds = false
3+
skip-lint = false
4+
[programs.devnet]
5+
cnft_vault = "CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk"
6+
7+
[registry]
8+
url = "https://api.apr.dev"
9+
10+
[provider]
11+
cluster = "Devnet"
12+
wallet = "~/.config/solana/id.json"
13+
14+
[scripts]
15+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[workspace]
2+
members = [
3+
"programs/*"
4+
]
5+
6+
[profile.release]
7+
overflow-checks = true
8+
lto = "fat"
9+
codegen-units = 1
10+
[profile.release.build-override]
11+
opt-level = 3
12+
incremental = false
13+
codegen-units = 1
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Solana Program cNFT Transfer example
2+
3+
This repo contains example code of how you can work with Metaplex compressed NFTs inside of Solana Anchor programs.
4+
5+
The basic idea is to allow for transfering cNFTs that are owned by a PDA account. So our program will have a vault (this PDA) that you can send cNFTs to manually and then withdraw them using the program instructions.
6+
7+
There are two instructions: one simple transfer that can withdraw one cNFT, and one instructions that can withdraw two cNFTs at the same time.
8+
9+
This program can be used as an inspiration on how to work with cNFTs in Solana programs.
10+
11+
## Components
12+
13+
The Anchor program can be found in the *programs* folder and *tests* some clientside tests. There are also some typescript node scripts in *tests/scripts* to run them individually (plus there is one called *withdrawWithLookup.ts* which demonstrates the use of the program with account lookup tables).
14+
15+
## Deployment
16+
17+
The program is deployed on devnet at `CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk`.
18+
You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml.
19+
20+
## Limitations
21+
22+
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).
23+
Furthermore it is not optimized for using lowest possible compute. It is intended as a proof of concept and reference implemention only.
24+
25+
## Further resources
26+
27+
A video about the creation of this code which also contains further explanations has been publised on Solandy's YouTube channel:
28+
https://youtu.be/qzr-q_E7H0M
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"scripts": {
3+
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4+
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5+
},
6+
"dependencies": {
7+
"@project-serum/anchor": "^0.26.0"
8+
},
9+
"devDependencies": {
10+
"chai": "^4.3.4",
11+
"mocha": "^9.0.3",
12+
"ts-mocha": "^10.0.0",
13+
"@types/bn.js": "^5.1.0",
14+
"@types/chai": "^4.3.0",
15+
"@types/mocha": "^9.0.0",
16+
"typescript": "^4.3.5",
17+
"prettier": "^2.6.2"
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "cnft-vault"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "cnft_vault"
10+
11+
[features]
12+
no-entrypoint = []
13+
no-idl = []
14+
no-log-ix-name = []
15+
cpi = ["no-entrypoint"]
16+
default = []
17+
18+
[dependencies]
19+
anchor-lang = "0.26.0"
20+
solana-program = "*"
21+
spl-account-compression = { version="0.1.8", features = ["cpi"] }
22+
mpl-bubblegum = { version = "0.7.0", features = ["no-entrypoint", "cpi"] }
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
use anchor_lang::prelude::*;
2+
use solana_program::{pubkey::Pubkey};
3+
use spl_account_compression::{
4+
program::SplAccountCompression, Noop,
5+
};
6+
use mpl_bubblegum::{state::TreeConfig};
7+
8+
declare_id!("CNftyK7T8udPwYRzZUMWzbh79rKrz9a5GwV2wv7iEHpk");
9+
10+
#[derive(Clone)]
11+
pub struct MplBubblegum;
12+
13+
impl anchor_lang::Id for MplBubblegum {
14+
fn id() -> Pubkey {
15+
mpl_bubblegum::id()
16+
}
17+
}
18+
19+
// first 8 bytes of SHA256("global:transfer")
20+
const TRANSFER_DISCRIMINATOR: &'static [u8;8] = &[163, 52, 200, 231, 140, 3, 69, 186];
21+
22+
23+
#[program]
24+
pub mod cnft_vault {
25+
26+
use super::*;
27+
28+
pub fn withdraw_cnft<'info>(ctx: Context<'_, '_, '_, 'info, Withdraw<'info>>,
29+
root: [u8; 32],
30+
data_hash: [u8; 32],
31+
creator_hash: [u8; 32],
32+
nonce: u64,
33+
index: u32,) -> Result<()> {
34+
msg!("attempting to send nft {} from tree {}", index, ctx.accounts.merkle_tree.key());
35+
36+
let mut accounts: Vec<solana_program::instruction::AccountMeta> = vec![
37+
AccountMeta::new_readonly(ctx.accounts.tree_authority.key(), false),
38+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
39+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), false),
40+
AccountMeta::new_readonly(ctx.accounts.new_leaf_owner.key(), false),
41+
AccountMeta::new(ctx.accounts.merkle_tree.key(), false),
42+
AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
43+
AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
44+
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
45+
];
46+
47+
let mut data: Vec<u8> = vec![];
48+
data.extend(TRANSFER_DISCRIMINATOR);
49+
data.extend(root);
50+
data.extend(data_hash);
51+
data.extend(creator_hash);
52+
data.extend(nonce.to_le_bytes());
53+
data.extend(index.to_le_bytes());
54+
55+
let mut account_infos: Vec<AccountInfo> = vec![
56+
ctx.accounts.tree_authority.to_account_info(),
57+
ctx.accounts.leaf_owner.to_account_info(),
58+
ctx.accounts.leaf_owner.to_account_info(),
59+
ctx.accounts.new_leaf_owner.to_account_info(),
60+
ctx.accounts.merkle_tree.to_account_info(),
61+
ctx.accounts.log_wrapper.to_account_info(),
62+
ctx.accounts.compression_program.to_account_info(),
63+
ctx.accounts.system_program.to_account_info(),
64+
];
65+
66+
// add "accounts" (hashes) that make up the merkle proof
67+
for acc in ctx.remaining_accounts.iter() {
68+
accounts.push(AccountMeta::new_readonly(acc.key(), false));
69+
account_infos.push(acc.to_account_info());
70+
}
71+
72+
msg!("manual cpi call");
73+
solana_program::program::invoke_signed(
74+
& solana_program::instruction::Instruction {
75+
program_id: ctx.accounts.bubblegum_program.key(),
76+
accounts: accounts,
77+
data: data,
78+
},
79+
&account_infos[..],
80+
&[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])
81+
.map_err(Into::into)
82+
83+
}
84+
85+
pub fn withdraw_two_cnfts<'info>(ctx: Context<'_, '_, '_, 'info, WithdrawTwo<'info>>,
86+
root1: [u8; 32],
87+
data_hash1: [u8; 32],
88+
creator_hash1: [u8; 32],
89+
nonce1: u64,
90+
index1: u32,
91+
proof_1_length: u8,
92+
root2: [u8; 32],
93+
data_hash2: [u8; 32],
94+
creator_hash2: [u8; 32],
95+
nonce2: u64,
96+
index2: u32,
97+
_proof_2_length: u8 // we don't actually need this (proof_2_length = remaining_accounts_len - proof_1_length)
98+
) -> Result<()> {
99+
let merkle_tree1 = ctx.accounts.merkle_tree1.key();
100+
let merkle_tree2 = ctx.accounts.merkle_tree2.key();
101+
msg!("attempting to send nfts from trees {} and {}", merkle_tree1, merkle_tree2);
102+
103+
104+
// Note: in this example anyone can withdraw any NFT from the vault
105+
// in productions you should check if nft transfers are valid (correct NFT, correct authority)
106+
107+
let mut accounts1: Vec<solana_program::instruction::AccountMeta> = vec![
108+
AccountMeta::new_readonly(ctx.accounts.tree_authority1.key(), false),
109+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
110+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), false),
111+
AccountMeta::new_readonly(ctx.accounts.new_leaf_owner1.key(), false),
112+
AccountMeta::new(ctx.accounts.merkle_tree1.key(), false),
113+
AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
114+
AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
115+
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
116+
];
117+
118+
let mut accounts2: Vec<solana_program::instruction::AccountMeta> = vec![
119+
AccountMeta::new_readonly(ctx.accounts.tree_authority2.key(), false),
120+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), true),
121+
AccountMeta::new_readonly(ctx.accounts.leaf_owner.key(), false),
122+
AccountMeta::new_readonly(ctx.accounts.new_leaf_owner2.key(), false),
123+
AccountMeta::new(ctx.accounts.merkle_tree2.key(), false),
124+
AccountMeta::new_readonly(ctx.accounts.log_wrapper.key(), false),
125+
AccountMeta::new_readonly(ctx.accounts.compression_program.key(), false),
126+
AccountMeta::new_readonly(ctx.accounts.system_program.key(), false),
127+
];
128+
129+
let mut data1: Vec<u8> = vec![];
130+
data1.extend(TRANSFER_DISCRIMINATOR);
131+
data1.extend(root1);
132+
data1.extend(data_hash1);
133+
data1.extend(creator_hash1);
134+
data1.extend(nonce1.to_le_bytes());
135+
data1.extend(index1.to_le_bytes());
136+
let mut data2: Vec<u8> = vec![];
137+
data2.extend(TRANSFER_DISCRIMINATOR);
138+
data2.extend(root2);
139+
data2.extend(data_hash2);
140+
data2.extend(creator_hash2);
141+
data2.extend(nonce2.to_le_bytes());
142+
data2.extend(index2.to_le_bytes());
143+
144+
let mut account_infos1: Vec<AccountInfo> = vec![
145+
ctx.accounts.tree_authority1.to_account_info(),
146+
ctx.accounts.leaf_owner.to_account_info(),
147+
ctx.accounts.leaf_owner.to_account_info(),
148+
ctx.accounts.new_leaf_owner1.to_account_info(),
149+
ctx.accounts.merkle_tree1.to_account_info(),
150+
ctx.accounts.log_wrapper.to_account_info(),
151+
ctx.accounts.compression_program.to_account_info(),
152+
ctx.accounts.system_program.to_account_info(),
153+
];
154+
let mut account_infos2: Vec<AccountInfo> = vec![
155+
ctx.accounts.tree_authority2.to_account_info(),
156+
ctx.accounts.leaf_owner.to_account_info(),
157+
ctx.accounts.leaf_owner.to_account_info(),
158+
ctx.accounts.new_leaf_owner2.to_account_info(),
159+
ctx.accounts.merkle_tree2.to_account_info(),
160+
ctx.accounts.log_wrapper.to_account_info(),
161+
ctx.accounts.compression_program.to_account_info(),
162+
ctx.accounts.system_program.to_account_info(),
163+
];
164+
165+
let mut i = 0u8;
166+
for acc in ctx.remaining_accounts.iter() {
167+
if i < proof_1_length {
168+
accounts1.push(AccountMeta::new_readonly(acc.key(), false));
169+
account_infos1.push(acc.to_account_info());
170+
} else {
171+
accounts2.push(AccountMeta::new_readonly(acc.key(), false));
172+
account_infos2.push(acc.to_account_info());
173+
}
174+
i+=1;
175+
}
176+
177+
msg!("withdrawing cNFT#1");
178+
solana_program::program::invoke_signed(
179+
& solana_program::instruction::Instruction {
180+
program_id: ctx.accounts.bubblegum_program.key(),
181+
accounts: accounts1,
182+
data: data1,
183+
},
184+
&account_infos1[..],
185+
&[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])?;
186+
187+
msg!("withdrawing cNFT#2");
188+
solana_program::program::invoke_signed(
189+
& solana_program::instruction::Instruction {
190+
program_id: ctx.accounts.bubblegum_program.key(),
191+
accounts: accounts2,
192+
data: data2,
193+
},
194+
&account_infos2[..],
195+
&[&[b"cNFT-vault", &[*ctx.bumps.get("leaf_owner").unwrap()]]])?;
196+
197+
msg!("successfully sent cNFTs");
198+
Ok(())
199+
200+
}
201+
202+
}
203+
204+
#[derive(Accounts)]
205+
pub struct Withdraw<'info> {
206+
#[account(
207+
seeds = [merkle_tree.key().as_ref()],
208+
bump,
209+
seeds::program = bubblegum_program.key()
210+
)]
211+
/// CHECK: This account is neither written to nor read from.
212+
pub tree_authority: Account<'info, TreeConfig>,
213+
214+
#[account(
215+
seeds = [b"cNFT-vault"],
216+
bump,
217+
)]
218+
/// CHECK: This account doesnt even exist (it is just the pda to sign)
219+
pub leaf_owner: UncheckedAccount<'info>, // sender (the vault in our case)
220+
/// CHECK: This account is neither written to nor read from.
221+
pub new_leaf_owner: UncheckedAccount<'info>, // receiver
222+
#[account(mut)]
223+
/// CHECK: This account is modified in the downstream program
224+
pub merkle_tree: UncheckedAccount<'info>,
225+
pub log_wrapper: Program<'info, Noop>,
226+
pub compression_program: Program<'info, SplAccountCompression>,
227+
pub bubblegum_program: Program<'info, MplBubblegum>,
228+
pub system_program: Program<'info, System>,
229+
}
230+
231+
#[derive(Accounts)]
232+
pub struct WithdrawTwo<'info> {
233+
#[account(
234+
seeds = [merkle_tree1.key().as_ref()],
235+
bump,
236+
seeds::program = bubblegum_program.key()
237+
)]
238+
/// CHECK: This account is neither written to nor read from.
239+
pub tree_authority1: Account<'info, TreeConfig>,
240+
#[account(
241+
seeds = [b"cNFT-vault"],
242+
bump,
243+
)]
244+
/// CHECK: This account doesnt even exist (it is just the pda to sign)
245+
pub leaf_owner: UncheckedAccount<'info>, // you might need two accounts if the nfts are owned by two different PDAs
246+
/// CHECK: This account is neither written to nor read from.
247+
pub new_leaf_owner1: UncheckedAccount<'info>, // receiver
248+
#[account(mut)]
249+
/// CHECK: This account is modified in the downstream program
250+
pub merkle_tree1: UncheckedAccount<'info>,
251+
252+
#[account(
253+
seeds = [merkle_tree2.key().as_ref()],
254+
bump,
255+
seeds::program = bubblegum_program.key()
256+
)]
257+
/// CHECK: This account is neither written to nor read from.
258+
pub tree_authority2: Account<'info, TreeConfig>,
259+
/// CHECK: This account is neither written to nor read from.
260+
pub new_leaf_owner2: UncheckedAccount<'info>, // receiver
261+
#[account(mut)]
262+
/// CHECK: This account is modified in the downstream program
263+
pub merkle_tree2: UncheckedAccount<'info>,
264+
265+
pub log_wrapper: Program<'info, Noop>,
266+
pub compression_program: Program<'info, SplAccountCompression>,
267+
pub bubblegum_program: Program<'info, MplBubblegum>,
268+
pub system_program: Program<'info, System>,
269+
}
270+

0 commit comments

Comments
 (0)