Skip to content

Commit 32dc874

Browse files
authored
* initial setup * delete unused * Update README.md * refactor * add account data setup
1 parent 04ba98b commit 32dc874

20 files changed

+1354
-0
lines changed

compression/cutils/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
.anchor
3+
.DS_Store
4+
target
5+
**/*.rs.bk
6+
node_modules
7+
test-ledger
8+
.local_keys

compression/cutils/Anchor.toml

+15
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+
cutils = "burZc1SfqbrAP35XG63YZZ82C9Zd22QUwhCXoEUZWNF"
6+
7+
[registry]
8+
url = "https://api.apr.dev"
9+
10+
[provider]
11+
cluster = "devnet"
12+
wallet = "~/.config/solana/test.json"
13+
14+
[scripts]
15+
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

compression/cutils/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[workspace]
2+
members = [
3+
"programs/*"
4+
]

compression/cutils/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Solana Program cNFT utils
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 custom logic in your own Solana program by doing a CPI to the bubblegum minting instruction. Two instructions:
6+
7+
1. **mint**: mints a cNFT to your collection by doing a CPI to bubblegum. You could initialise your own program-specific PDA in this instruction
8+
2. **verify**: verifies that the owner of the cNFT did in fact actuate the instruction. This is more of a utility function, which is to be used for future program-specific use-cases.
9+
10+
This program can be used as an inspiration on how to work with cNFTs in Solana programs.
11+
12+
## Components
13+
- **programs**: the Solana program
14+
- There is a validate/actuate setup which allows you to validate some constraints through an `access_control` macro. This might be useful to use in conjunction with the cNFT verification logic.
15+
16+
- **tests**:
17+
- `setup.ts` which is to be executed first if you don't already have a collection with merkle tree(s).
18+
- `tests.ts` for running individual minting and verification tests
19+
20+
## Deployment
21+
22+
The program is deployed on devnet at `burZc1SfqbrAP35XG63YZZ82C9Zd22QUwhCXoEUZWNF`.
23+
You can deploy it yourself by changing the respective values in lib.rs and Anchor.toml.
24+
25+
## Limitations
26+
27+
This is just an example implementation. Use at your own discretion
28+
29+
**This only works on anchor 0.26.0 for now due to mpl-bubblegum dependencies**
30+
31+
## Further resources
32+
A video about the creation of this code which also contains further explanations has been publised on Burger Bob's YouTube channel: COMING SOON
33+
34+
## How-to
35+
1. Configure RPC path in _utils/readAPI.ts_. Personal preference: Helius RPCs.
36+
2. cd root folder
37+
2. Install packages: `yarn`
38+
3. Optional: run `npx ts-node tests/setup.ts` to setup a NFT collection and its underlying merkle tree.
39+
4. Comment-out the tests you don't want to execute in `tests/tests.ts`
40+
5. If minting, change to your appropriate NFT uri
41+
6. If verifying, change to your appropriate assetId (cNFT mint address)
42+
7. Run `anchor test --skip-build --skip-deploy --skip-local-validator`
43+
8. You can check your cNFTs on devnet through the Solflare wallet (thanks [@SolPlay_jonas](https://twitter.com/SolPlay_jonas))
44+
3. You might want to change the wallet-path in `Anchor.toml`
45+
46+
47+
## Acknowledgements
48+
This repo would not have been possible without the work of:
49+
- [@nickfrosty](https://twitter.com/nickfrosty) for providing sample code and doing a live demo [here](https://youtu.be/LxhTxS9DexU)
50+
- [@HeyAndyS](https://twitter.com/HeyAndyS) for laying the groundwork with cnft-vault
51+
- The kind folks responding to this [thread](https://twitter.com/burger606/status/1669289672076320771?s=20)
52+
- [Switchboard VRF-flip](https://github.com/switchboard-xyz/vrf-flip/tree/main/client) for inspiring the validate/actuate setup.

compression/cutils/package.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"scripts": {
3+
"lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w",
4+
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
5+
},
6+
"dependencies": {
7+
"@coral-xyz/anchor": "^0.27.0",
8+
"@metaplex-foundation/js": "^0.19.4",
9+
"@metaplex-foundation/mpl-bubblegum": "^0.7.0",
10+
"@solana/spl-account-compression": "^0.1.8",
11+
"@solana/spl-token": "^0.3.8",
12+
"@solana/web3.js": "^1.77.3",
13+
"axios": "^1.4.0"
14+
},
15+
"devDependencies": {
16+
"@types/bn.js": "^5.1.0",
17+
"@types/chai": "^4.3.0",
18+
"@types/mocha": "^9.0.0",
19+
"chai": "^4.3.4",
20+
"mocha": "^9.0.3",
21+
"prettier": "^2.6.2",
22+
"ts-mocha": "^10.0.0",
23+
"ts-node": "^10.9.1",
24+
"typescript": "^4.3.5"
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "cutils"
3+
version = "0.1.0"
4+
description = "Created with Anchor"
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "lib"]
9+
name = "cutils"
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 = "~1.14.18"
21+
spl-account-compression = { version="0.1.8", features = ["cpi"] }
22+
mpl-bubblegum = { version = "0.7.0", features = ["no-entrypoint", "cpi"] }
23+
24+
# Added due to anchor and solana-cli wonkyness as of late
25+
getrandom = { version = "0.2.10", features = ["custom"] }
26+
winnow = "=0.4.1"
27+
toml_datetime = "=0.6.1"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[target.bpfel-unknown-unknown.dependencies.std]
2+
features = []
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use crate::*;
2+
use mpl_bubblegum::{
3+
state::{
4+
TreeConfig,
5+
COLLECTION_CPI_PREFIX,
6+
metaplex_adapter::{Collection, Creator, MetadataArgs, TokenProgramVersion, TokenStandard},
7+
metaplex_anchor::{
8+
TokenMetadata, MplTokenMetadata
9+
},
10+
}
11+
};
12+
13+
#[derive(Accounts)]
14+
#[instruction(params: MintParams)]
15+
pub struct Mint<'info> {
16+
// #[account(
17+
// init,
18+
// seeds = [
19+
// SEED_DATA,
20+
// data.tree,
21+
// data.tree_nonce
22+
// // assetId directly?
23+
// ],
24+
// bump,
25+
// payer = payer,
26+
// space = Data::LEN,
27+
// )]
28+
// pub data: Account<'info, Data>,
29+
30+
pub payer: Signer<'info>,
31+
32+
// Bubblegum cNFT stuff MintToCollectionV1
33+
#[account(
34+
mut,
35+
seeds = [merkle_tree.key().as_ref()],
36+
seeds::program = bubblegum_program.key(),
37+
bump,
38+
)]
39+
pub tree_authority: Box<Account<'info, TreeConfig>>,
40+
41+
/// CHECK: This account is neither written to nor read from.
42+
pub leaf_owner: AccountInfo<'info>,
43+
44+
/// CHECK: This account is neither written to nor read from.
45+
pub leaf_delegate: AccountInfo<'info>,
46+
47+
#[account(mut)]
48+
/// CHECK: unsafe
49+
pub merkle_tree: UncheckedAccount<'info>,
50+
51+
pub tree_delegate: Signer<'info>,
52+
53+
pub collection_authority: Signer<'info>,
54+
55+
/// CHECK: Optional collection authority record PDA.
56+
/// If there is no collecton authority record PDA then
57+
/// this must be the Bubblegum program address.
58+
pub collection_authority_record_pda: UncheckedAccount<'info>,
59+
60+
/// CHECK: This account is checked in the instruction
61+
pub collection_mint: UncheckedAccount<'info>,
62+
63+
#[account(mut)]
64+
pub collection_metadata: Box<Account<'info, TokenMetadata>>,
65+
66+
/// CHECK: This account is checked in the instruction
67+
pub edition_account: UncheckedAccount<'info>,
68+
69+
/// CHECK: This is just used as a signing PDA.
70+
#[account(
71+
seeds = [COLLECTION_CPI_PREFIX.as_ref()],
72+
seeds::program = bubblegum_program.key(),
73+
bump,
74+
)]
75+
pub bubblegum_signer: UncheckedAccount<'info>,
76+
pub log_wrapper: Program<'info, Noop>,
77+
pub compression_program: Program<'info, SplAccountCompression>,
78+
pub token_metadata_program: Program<'info, MplTokenMetadata>,
79+
pub bubblegum_program: Program<'info, MplBubblegum>,
80+
pub system_program: Program<'info, System>,
81+
}
82+
83+
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
84+
pub struct MintParams {
85+
uri: String,
86+
}
87+
88+
impl Mint<'_> {
89+
pub fn validate(
90+
&self,
91+
_ctx: &Context<Self>,
92+
_params: &MintParams,
93+
) -> Result<()> {
94+
Ok(())
95+
}
96+
97+
pub fn actuate<'info>(
98+
ctx: Context<'_, '_, '_, 'info, Mint<'info>>,
99+
params: MintParams
100+
) -> Result<()> {
101+
mpl_bubblegum::cpi::mint_to_collection_v1(
102+
CpiContext::new(
103+
ctx.accounts.bubblegum_program.to_account_info(),
104+
mpl_bubblegum::cpi::accounts::MintToCollectionV1 {
105+
tree_authority: ctx.accounts.tree_authority.to_account_info(),
106+
leaf_owner: ctx.accounts.leaf_owner.to_account_info(),
107+
leaf_delegate: ctx.accounts.leaf_delegate.to_account_info(),
108+
merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
109+
payer: ctx.accounts.payer.to_account_info(),
110+
tree_delegate: ctx.accounts.tree_delegate.to_account_info(),
111+
collection_authority: ctx.accounts.collection_authority.to_account_info(),
112+
collection_authority_record_pda: ctx.accounts.collection_authority_record_pda.to_account_info(),
113+
collection_mint: ctx.accounts.collection_mint.to_account_info(),
114+
collection_metadata: ctx.accounts.collection_metadata.to_account_info(),
115+
edition_account: ctx.accounts.edition_account.to_account_info(),
116+
bubblegum_signer: ctx.accounts.bubblegum_signer.to_account_info(),
117+
log_wrapper: ctx.accounts.log_wrapper.to_account_info(),
118+
compression_program: ctx.accounts.compression_program.to_account_info(),
119+
token_metadata_program: ctx.accounts.token_metadata_program.to_account_info(),
120+
system_program: ctx.accounts.system_program.to_account_info(),
121+
}
122+
),
123+
MetadataArgs {
124+
name: "BURGER".to_string(),
125+
symbol: "BURG".to_string(),
126+
uri: params.uri,
127+
creators: vec![
128+
Creator {address: ctx.accounts.collection_authority.key(), verified: false, share: 100},
129+
],
130+
seller_fee_basis_points: 0,
131+
primary_sale_happened: false,
132+
is_mutable: false,
133+
edition_nonce: Some(0),
134+
uses: None,
135+
collection: Some(Collection {verified: false, key: ctx.accounts.collection_mint.key()}),
136+
token_program_version: TokenProgramVersion::Original,
137+
token_standard: Some(TokenStandard::NonFungible),
138+
}
139+
)?;
140+
141+
Ok(())
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pub mod mint;
2+
pub use mint::*;
3+
4+
pub mod verify;
5+
pub use verify::*;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
use crate::*;
2+
use mpl_bubblegum::state::leaf_schema::LeafSchema;
3+
use mpl_bubblegum::utils::get_asset_id;
4+
use spl_account_compression::{
5+
program::SplAccountCompression
6+
};
7+
8+
#[derive(Accounts)]
9+
#[instruction(params: VerifyParams)]
10+
pub struct Verify<'info> {
11+
pub leaf_owner: Signer<'info>,
12+
13+
/// CHECK: This account is neither written to nor read from.
14+
pub leaf_delegate: AccountInfo<'info>,
15+
16+
/// CHECK: unsafe
17+
pub merkle_tree: UncheckedAccount<'info>,
18+
19+
pub compression_program: Program<'info, SplAccountCompression>,
20+
}
21+
22+
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
23+
pub struct VerifyParams {
24+
root: [u8; 32],
25+
data_hash: [u8; 32],
26+
creator_hash: [u8; 32],
27+
nonce: u64,
28+
index: u32,
29+
}
30+
31+
impl Verify<'_> {
32+
pub fn validate(
33+
&self,
34+
_ctx: &Context<Self>,
35+
_params: &VerifyParams
36+
) -> Result<()> {
37+
Ok(())
38+
}
39+
40+
pub fn actuate<'info>(ctx: Context<'_, '_, '_, 'info, Verify<'info>>, params: &VerifyParams) -> Result<()> {
41+
let asset_id = get_asset_id(&ctx.accounts.merkle_tree.key(), params.nonce);
42+
let leaf = LeafSchema::new_v0(
43+
asset_id,
44+
ctx.accounts.leaf_owner.key(),
45+
ctx.accounts.leaf_delegate.key(),
46+
params.nonce,
47+
params.data_hash,
48+
params.creator_hash,
49+
);
50+
51+
let cpi_ctx = CpiContext::new(
52+
ctx.accounts.compression_program.to_account_info(),
53+
spl_account_compression::cpi::accounts::VerifyLeaf {
54+
merkle_tree: ctx.accounts.merkle_tree.to_account_info(),
55+
},
56+
).with_remaining_accounts(ctx.remaining_accounts.to_vec());
57+
58+
spl_account_compression::cpi::verify_leaf(
59+
cpi_ctx,
60+
params.root,
61+
leaf.to_node(),
62+
params.index,
63+
)?;
64+
65+
Ok(())
66+
}
67+
}

0 commit comments

Comments
 (0)