├── .gitignore
├── README.md
├── js
├── jest.config.cjs
├── package-lock.json
├── package.json
├── rollup.config.mjs
├── src
│ ├── bindings.ts
│ ├── index.ts
│ ├── raw_instructions.ts
│ ├── secondary_bindings.ts
│ └── state.ts
├── tests
│ ├── end_to_end.test.ts
│ ├── pda.test.ts
│ └── utils.ts
└── tsconfig.json
├── program
├── Anchor.toml
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── build.sh
├── idl.json
├── rust-toolchain.toml
├── src
│ ├── cpi.rs
│ ├── entrypoint.rs
│ ├── error.rs
│ ├── instruction.rs
│ ├── lib.rs
│ ├── processor.rs
│ ├── processor
│ │ ├── create_collection.rs
│ │ ├── create_mint.rs
│ │ ├── create_nft.rs
│ │ ├── edit_data.rs
│ │ ├── redeem_nft.rs
│ │ ├── unverify_nft.rs
│ │ └── withdraw_tokens.rs
│ ├── state.rs
│ ├── state
│ │ ├── central_state.rs
│ │ └── nft_record.rs
│ └── utils.rs
└── tests
│ ├── common
│ ├── mod.rs
│ └── utils.rs
│ └── functional.rs
└── python
└── src
└── raw_instructions.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | target/
3 | node_modules/
4 | dist/
5 | .vscode/
6 | __pycache__
7 | js/stats.html
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Name tokenizer
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Tokenize domain name into Metaplex NFTs
16 |
17 |
18 |
19 |
20 |

21 |

22 |
23 |
24 |
25 | Table of contents
26 |
27 |
28 | 1. [Program ID](#program-id)
29 | 2. [Introduction](#introduction)
30 | 3. [Security](#security)
31 | 4. [Reproducible build](#build)
32 | 5. [Collection](#collection)
33 | 6. [Mint](#mint)
34 | 7. [NFT](#nft)
35 | 8. [Tests](#tests)
36 | - Rust
37 | - JS
38 |
39 |
40 |
41 | Program ID
42 |
43 |
44 | Mainnet program ID `nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk`
45 |
46 |
47 |
48 | Introduction
49 |
50 |
51 | This program allows people to tokenize their domain name in NFTs that follow the [Metaplex standard](https://github.com/metaplex-foundation/metaplex-program-library/tree/master/token-metadata) with a creation/redemption mechanism.
52 |
53 |
54 |
55 | Reproducible build
56 |
57 |
58 | A reproducible build script (`build.sh`) can be used to build the program using docker
59 |
60 |
61 |
62 | Security
63 |
64 |
65 | For security disclosures or to report a bug, please visit [ImmuneFi](https://immunefi.com/bounty/bonfida/) for more information on our bug bounty program.
66 |
67 |
68 |
69 | Collection
70 |
71 |
72 | NFTs are all part of a verified collection `E5ZnBpH9DYcxRkumKdS4ayJ3Ftb6o3E8wSbXw4N92GWg`.
73 |
74 |
75 |
76 | Mint
77 |
78 |
79 | NFT mints are PDAs derived from the domain name key they represent. The derivation is made as follow:
80 |
81 | ```rust
82 | pub const MINT_PREFIX: &[u8; 14] = b"tokenized_name";
83 |
84 | // ...
85 |
86 | let (mint, mint_nonce) = Pubkey::find_program_address(
87 | &[MINT_PREFIX, &accounts.name_account.key.to_bytes()],
88 | program_id,
89 | );
90 | ```
91 |
92 |
93 |
94 | NFT
95 |
96 |
97 | When a domain name is tokenized its ownership is transfered to a PDA that will be holding the domain while it's tokenized. In exchange, the program mints an NFT for the user. When redeeming the domain is transfered back to the NFT holder and the NFT burned.
98 |
99 | During the tokenization process an `NftRecord` is created with the following state:
100 |
101 | ```rust
102 | pub struct NftRecord {
103 | /// Tag
104 | pub tag: Tag,
105 |
106 | /// Nonce
107 | pub nonce: u8,
108 |
109 | /// Name account of the record
110 | pub name_account: Pubkey,
111 |
112 | /// Record owner
113 | pub owner: Pubkey,
114 |
115 | /// NFT mint
116 | pub nft_mint: Pubkey,
117 | }
118 | ```
119 |
120 | If funds are sent by mistake to the `NftRecord` instead of the NFT holder while the domain is tokenized the owner has the possibility to withdraw them. The "correct owner" is determined as follow:
121 |
122 | - If the `NftRecord` is active i.e domain is tokenized: The correct owner is the NFT holder
123 | - If `NftRecord` is inactive i.e the NFT has been redeemed: The correct owner is the last person who redeemed (`owner` field in the `NftRecord`)
124 |
125 |
126 |
127 | Tests
128 |
129 |
130 | ### Rust
131 |
132 | Functional Rust tests can be run with
133 |
134 | ```
135 | cargo test-bpf --features devnet
136 | ```
137 |
138 | ### JS
139 |
140 | End to end tests can be run with
141 |
142 | ```
143 | yarn jest
144 | ```
145 |
--------------------------------------------------------------------------------
/js/jest.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
2 | module.exports = {
3 | preset: "ts-jest",
4 | testEnvironment: "node",
5 | testPathIgnorePatterns: ["/node_modules/", "/dist/"],
6 | };
7 |
--------------------------------------------------------------------------------
/js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@bonfida/name-tokenizer",
3 | "version": "0.1.0",
4 | "main": "./dist/index.cjs",
5 | "module": "./dist/index.mjs",
6 | "types": "./dist/index.d.ts",
7 | "license": "MIT",
8 | "type": "module",
9 | "repository": {
10 | "type": "git"
11 | },
12 | "publishConfig": {
13 | "access": "public"
14 | },
15 | "exports": {
16 | ".": {
17 | "import": "./dist/index.mjs",
18 | "require": "./dist/index.cjs",
19 | "types": "./dist/index.d.ts"
20 | }
21 | },
22 | "files": [
23 | "dist"
24 | ],
25 | "scripts": {
26 | "test": "jest --detectOpenHandles --coverage",
27 | "dev": "tsc && node dist/test.js",
28 | "prebuild": "rm -rf dist",
29 | "build": "rollup -c",
30 | "build:rm": "rollup -c && rm -rf node_modules",
31 | "prepublish": "rm -rf dist && rollup -c",
32 | "lint": "yarn pretty && eslint .",
33 | "lint:fix": "yarn pretty:fix && eslint . --fix",
34 | "pretty": "prettier --check 'src/*.[jt]s'",
35 | "pretty:fix": "prettier --write 'src/*.[jt]s'",
36 | "doc": "yarn typedoc src/index.ts"
37 | },
38 | "devDependencies": {
39 | "@bonfida/spl-name-service": "^1.5.0",
40 | "@bonfida/utils": "^0.0.7",
41 | "@metaplex-foundation/mpl-token-metadata": "^1.2.5",
42 | "@rollup/plugin-babel": "^6.0.4",
43 | "@rollup/plugin-commonjs": "^25.0.7",
44 | "@rollup/plugin-json": "^6.0.1",
45 | "@rollup/plugin-node-resolve": "^15.2.3",
46 | "@rollup/plugin-replace": "^5.0.4",
47 | "@rollup/plugin-terser": "^0.4.4",
48 | "@rollup/plugin-typescript": "^11.1.5",
49 | "@solana/spl-token": "^0.3.7",
50 | "@solana/web3.js": "^1.87.3",
51 | "@tsconfig/recommended": "^1.0.3",
52 | "@types/bs58": "^4.0.1",
53 | "@types/jest": "^29.5.1",
54 | "@types/node": "^14.14.20",
55 | "@types/tmp": "^0.2.2",
56 | "babel-eslint": "^10.1.0",
57 | "eslint": "^7.17.0",
58 | "eslint-plugin-import": "^2.22.1",
59 | "jest": "^29.5.0",
60 | "nodemon": "^2.0.7",
61 | "prettier": "^2.2.1",
62 | "rollup": "^4.3.0",
63 | "rollup-plugin-visualizer": "^5.12.0",
64 | "save-dev": "0.0.1-security",
65 | "tmp": "^0.2.1",
66 | "ts-jest": "^29.1.0",
67 | "ts-node": "^9.1.1",
68 | "tslib": "^2.2.0",
69 | "typedoc": "^0.24.6",
70 | "typescript": "^5.0.4"
71 | },
72 | "dependencies": {
73 | "@solana/spl-token": "^0.3.11",
74 | "borsh": "2.0.0",
75 | "buffer": "^6.0.3"
76 | },
77 | "peerDependencies": {
78 | "@solana/web3.js": "^1.87.3"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/js/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | import typescript from "@rollup/plugin-typescript";
2 | import commonjs from "@rollup/plugin-commonjs";
3 | import terser from "@rollup/plugin-terser";
4 | import { nodeResolve } from "@rollup/plugin-node-resolve";
5 | import replace from "@rollup/plugin-replace";
6 | import babel from "@rollup/plugin-babel";
7 | import { visualizer } from "rollup-plugin-visualizer";
8 |
9 | export default {
10 | input: "src/index.ts",
11 | output: [
12 | {
13 | file: "dist/index.mjs",
14 | format: "esm",
15 | sourcemap: true,
16 | },
17 | { file: "dist/index.cjs", format: "cjs", sourcemap: true },
18 | ],
19 | external: ["@solana/web3.js"],
20 | plugins: [
21 | typescript({ sourceMap: true }),
22 | commonjs(),
23 | babel({ babelHelpers: "bundled" }),
24 | nodeResolve({ browser: true, preferBuiltins: false }),
25 | replace({
26 | "process.env.NODE_ENV": JSON.stringify("production"),
27 | preventAssignment: false,
28 | }),
29 | terser(),
30 | visualizer(),
31 | ],
32 | onwarn: function (warning, handler) {
33 | if (warning.code === "THIS_IS_UNDEFINED") return;
34 | handler(warning);
35 | },
36 | };
37 |
--------------------------------------------------------------------------------
/js/src/bindings.ts:
--------------------------------------------------------------------------------
1 | import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js";
2 | import {
3 | withdrawTokensInstruction,
4 | createNftInstruction,
5 | createMintInstruction,
6 | redeemNftInstruction,
7 | createCollectionInstruction,
8 | } from "./raw_instructions";
9 | import {
10 | COLLECTION_PREFIX,
11 | MINT_PREFIX,
12 | NftRecord,
13 | METADATA_SIGNER,
14 | } from "./state";
15 | import {
16 | TOKEN_PROGRAM_ID,
17 | getAssociatedTokenAddressSync,
18 | ASSOCIATED_TOKEN_PROGRAM_ID,
19 | } from "@solana/spl-token";
20 | import { Buffer } from "buffer";
21 |
22 | const NAME_PROGRAM_ID = new PublicKey(
23 | "namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX"
24 | );
25 |
26 | const METADATA_ID = new PublicKey(
27 | "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
28 | );
29 |
30 | const PREFIX = "metadata";
31 | const EDITION = "edition";
32 |
33 | export const getMetadataPda = (mint: PublicKey) => {
34 | return PublicKey.findProgramAddressSync(
35 | [Buffer.from(PREFIX), METADATA_ID.toBuffer(), mint.toBuffer()],
36 | METADATA_ID
37 | )[0];
38 | };
39 |
40 | export const getMasterEditionPda = (mint: PublicKey) => {
41 | return PublicKey.findProgramAddressSync(
42 | [
43 | Buffer.from(PREFIX),
44 | METADATA_ID.toBuffer(),
45 | mint.toBuffer(),
46 | Buffer.from(EDITION),
47 | ],
48 | METADATA_ID
49 | )[0];
50 | };
51 |
52 | /**
53 | * Mainnet program ID
54 | */
55 | export const NAME_TOKENIZER_ID = new PublicKey(
56 | "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk"
57 | );
58 |
59 | /**
60 | * Devnet program ID (might not have the latest version deployed!)
61 | */
62 | export const NAME_TOKENIZER_ID_DEVNET = new PublicKey(
63 | "45gRSRZmK6NDEJrCZ72MMddjA1ozufq9YQpm41poPXCE"
64 | );
65 |
66 | /**
67 | * This function can be used to create the mint of a domain name
68 | * @param nameAccount The domain name the mint represents
69 | * @param feePayer The fee payer of the transaction
70 | * @param programId The Name tokenizer program ID
71 | * @returns
72 | */
73 | export const createMint = (
74 | nameAccount: PublicKey,
75 | feePayer: PublicKey,
76 | programId: PublicKey
77 | ) => {
78 | const [centralKey] = PublicKey.findProgramAddressSync(
79 | [programId.toBuffer()],
80 | programId
81 | );
82 |
83 | const [mint] = PublicKey.findProgramAddressSync(
84 | [MINT_PREFIX, nameAccount.toBuffer()],
85 | programId
86 | );
87 |
88 | const ix = new createMintInstruction().getInstruction(
89 | programId,
90 | mint,
91 | nameAccount,
92 | centralKey,
93 | TOKEN_PROGRAM_ID,
94 | SystemProgram.programId,
95 | SYSVAR_RENT_PUBKEY,
96 | feePayer
97 | );
98 |
99 | return [ix];
100 | };
101 |
102 | /**
103 | * This function can be used to create the central state collection
104 | * @param feePayer The fee payer of the transaction
105 | * @param programId The Name tokenizer program ID
106 | * @returns
107 | */
108 | export const createCollection = (feePayer: PublicKey, programId: PublicKey) => {
109 | const [centralKey] = PublicKey.findProgramAddressSync(
110 | [programId.toBuffer()],
111 | programId
112 | );
113 | const [collectionMint] = PublicKey.findProgramAddressSync(
114 | [COLLECTION_PREFIX, programId.toBuffer()],
115 | programId
116 | );
117 | const collectionMetadata = getMetadataPda(collectionMint);
118 | const editionAccount = getMasterEditionPda(collectionMint);
119 | const centralStateAta = getAssociatedTokenAddressSync(
120 | collectionMint,
121 | centralKey,
122 | true
123 | );
124 |
125 | const ix = new createCollectionInstruction().getInstruction(
126 | programId,
127 | collectionMint,
128 | editionAccount,
129 | collectionMetadata,
130 | centralKey,
131 | centralStateAta,
132 | feePayer,
133 | TOKEN_PROGRAM_ID,
134 | METADATA_ID,
135 | SystemProgram.programId,
136 | NAME_PROGRAM_ID,
137 | ASSOCIATED_TOKEN_PROGRAM_ID,
138 | SYSVAR_RENT_PUBKEY
139 | );
140 |
141 | return [ix];
142 | };
143 |
144 | /**
145 | * This function can be used to create to wrap a domain name into an NFT
146 | * @param name The domain name (without .sol)
147 | * @param uri The URI of the metadata
148 | * @param nameAccount The domain name key
149 | * @param nameOwner The owner of the domain name to tokenize
150 | * @param feePayer The fee payer of the transaction
151 | * @param programId The Name tokenizer program ID
152 | * @returns
153 | */
154 | export const createNft = (
155 | name: string,
156 | uri: string,
157 | nameAccount: PublicKey,
158 | nameOwner: PublicKey,
159 | feePayer: PublicKey,
160 | programId: PublicKey
161 | ) => {
162 | const [centralKey] = PublicKey.findProgramAddressSync(
163 | [programId.toBuffer()],
164 | programId
165 | );
166 |
167 | const [mint] = PublicKey.findProgramAddressSync(
168 | [MINT_PREFIX, nameAccount.toBuffer()],
169 | programId
170 | );
171 |
172 | const [nftRecord] = NftRecord.findKeySync(nameAccount, programId);
173 | const nftDestination = getAssociatedTokenAddressSync(mint, nameOwner);
174 |
175 | const metadataAccount = getMetadataPda(mint);
176 |
177 | const [collectionMint] = PublicKey.findProgramAddressSync(
178 | [COLLECTION_PREFIX, programId.toBuffer()],
179 | programId
180 | );
181 | const [collectionMetadata] = PublicKey.findProgramAddressSync(
182 | [Buffer.from(PREFIX), METADATA_ID.toBuffer(), collectionMint.toBuffer()],
183 | METADATA_ID
184 | );
185 | const editionAccount = getMasterEditionPda(collectionMint);
186 |
187 | const ix = new createNftInstruction({ name, uri }).getInstruction(
188 | programId,
189 | mint,
190 | nftDestination,
191 | nameAccount,
192 | nftRecord,
193 | nameOwner,
194 | metadataAccount,
195 | editionAccount,
196 | collectionMetadata,
197 | collectionMint,
198 | centralKey,
199 | feePayer,
200 | TOKEN_PROGRAM_ID,
201 | METADATA_ID,
202 | SystemProgram.programId,
203 | NAME_PROGRAM_ID,
204 | SYSVAR_RENT_PUBKEY,
205 | METADATA_SIGNER
206 | );
207 |
208 | return [ix];
209 | };
210 |
211 | /**
212 | * This function can be used to unwrap a domain name that has been tokenized
213 | * @param nameAccount The domain name key
214 | * @param nftOwner The owner of the NFT to redeem
215 | * @param programId The Name tokenizer program ID
216 | * @returns
217 | */
218 | export const redeemNft = (
219 | nameAccount: PublicKey,
220 | nftOwner: PublicKey,
221 | programId: PublicKey
222 | ) => {
223 | const [mint] = PublicKey.findProgramAddressSync(
224 | [MINT_PREFIX, nameAccount.toBuffer()],
225 | programId
226 | );
227 |
228 | const [nftRecord] = NftRecord.findKeySync(nameAccount, programId);
229 | const nftSource = getAssociatedTokenAddressSync(mint, nftOwner);
230 |
231 | const ix = new redeemNftInstruction().getInstruction(
232 | programId,
233 | mint,
234 | nftSource,
235 | nftOwner,
236 | nftRecord,
237 | nameAccount,
238 | TOKEN_PROGRAM_ID,
239 | NAME_PROGRAM_ID
240 | );
241 |
242 | return [ix];
243 | };
244 |
245 | /**
246 | * This function can be used to withdraw funds sent by mistake to an NftRecord while the domain was tokenized
247 | * @param nftMint The mint of the NFT
248 | * @param tokenMint The mint of the token to withdraw from the NftRecord
249 | * @param nftOwner The owner of the NFT (if the NFT has been redeemed it should be the latest person who redeemed)
250 | * @param nftRecord The NftRecord to which the funds were sent to
251 | * @param programId The Name tokenizer program ID
252 | * @returns
253 | */
254 | export const withdrawTokens = (
255 | nftMint: PublicKey,
256 | tokenMint: PublicKey,
257 | nftOwner: PublicKey,
258 | nftRecord: PublicKey,
259 | programId: PublicKey
260 | ) => {
261 | const tokenDestination = getAssociatedTokenAddressSync(tokenMint, nftOwner);
262 | const tokenSource = getAssociatedTokenAddressSync(tokenMint, nftRecord, true);
263 | const nft = getAssociatedTokenAddressSync(nftMint, nftOwner);
264 |
265 | const ix = new withdrawTokensInstruction().getInstruction(
266 | programId,
267 | nft,
268 | nftOwner,
269 | nftRecord,
270 | tokenDestination,
271 | tokenSource,
272 | TOKEN_PROGRAM_ID,
273 | SystemProgram.programId
274 | );
275 |
276 | return [ix];
277 | };
278 |
--------------------------------------------------------------------------------
/js/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./bindings";
2 | export * from "./raw_instructions";
3 | export * from "./state";
4 | export * from "./secondary_bindings";
5 |
--------------------------------------------------------------------------------
/js/src/raw_instructions.ts:
--------------------------------------------------------------------------------
1 | // This file is auto-generated. DO NOT EDIT
2 | import { serialize } from "borsh";
3 | import { PublicKey, TransactionInstruction } from "@solana/web3.js";
4 |
5 | export interface AccountKey {
6 | pubkey: PublicKey;
7 | isSigner: boolean;
8 | isWritable: boolean;
9 | }
10 | export class unverifyNftInstruction {
11 | tag: number;
12 | static schema = {
13 | struct: {
14 | tag: "u8",
15 | },
16 | };
17 | constructor() {
18 | this.tag = 6;
19 | }
20 | serialize(): Uint8Array {
21 | return serialize(unverifyNftInstruction.schema, this);
22 | }
23 | getInstruction(
24 | programId: PublicKey,
25 | metadataAccount: PublicKey,
26 | editionAccount: PublicKey,
27 | collectionMetadata: PublicKey,
28 | collectionMint: PublicKey,
29 | centralState: PublicKey,
30 | feePayer: PublicKey,
31 | metadataProgram: PublicKey,
32 | systemProgram: PublicKey,
33 | rentAccount: PublicKey,
34 | metadataSigner: PublicKey
35 | ): TransactionInstruction {
36 | const data = Buffer.from(this.serialize());
37 | let keys: AccountKey[] = [];
38 | keys.push({
39 | pubkey: metadataAccount,
40 | isSigner: false,
41 | isWritable: true,
42 | });
43 | keys.push({
44 | pubkey: editionAccount,
45 | isSigner: false,
46 | isWritable: false,
47 | });
48 | keys.push({
49 | pubkey: collectionMetadata,
50 | isSigner: false,
51 | isWritable: false,
52 | });
53 | keys.push({
54 | pubkey: collectionMint,
55 | isSigner: false,
56 | isWritable: false,
57 | });
58 | keys.push({
59 | pubkey: centralState,
60 | isSigner: false,
61 | isWritable: true,
62 | });
63 | keys.push({
64 | pubkey: feePayer,
65 | isSigner: true,
66 | isWritable: true,
67 | });
68 | keys.push({
69 | pubkey: metadataProgram,
70 | isSigner: false,
71 | isWritable: false,
72 | });
73 | keys.push({
74 | pubkey: systemProgram,
75 | isSigner: false,
76 | isWritable: false,
77 | });
78 | keys.push({
79 | pubkey: rentAccount,
80 | isSigner: false,
81 | isWritable: false,
82 | });
83 | keys.push({
84 | pubkey: metadataSigner,
85 | isSigner: true,
86 | isWritable: false,
87 | });
88 | return new TransactionInstruction({
89 | keys,
90 | programId,
91 | data,
92 | });
93 | }
94 | }
95 | export class editDataInstruction {
96 | tag: number;
97 | offset: number;
98 | data: number[];
99 | static schema = {
100 | struct: {
101 | tag: "u8",
102 | offset: "u32",
103 | data: { array: { type: "u8" } },
104 | },
105 | };
106 | constructor(obj: { offset: number; data: number[] }) {
107 | this.tag = 5;
108 | this.offset = obj.offset;
109 | this.data = obj.data;
110 | }
111 | serialize(): Uint8Array {
112 | return serialize(editDataInstruction.schema, this);
113 | }
114 | getInstruction(
115 | programId: PublicKey,
116 | nftOwner: PublicKey,
117 | nftAccount: PublicKey,
118 | nftRecord: PublicKey,
119 | nameAccount: PublicKey,
120 | splTokenProgram: PublicKey,
121 | splNameServiceProgram: PublicKey
122 | ): TransactionInstruction {
123 | const data = Buffer.from(this.serialize());
124 | let keys: AccountKey[] = [];
125 | keys.push({
126 | pubkey: nftOwner,
127 | isSigner: true,
128 | isWritable: false,
129 | });
130 | keys.push({
131 | pubkey: nftAccount,
132 | isSigner: false,
133 | isWritable: false,
134 | });
135 | keys.push({
136 | pubkey: nftRecord,
137 | isSigner: false,
138 | isWritable: false,
139 | });
140 | keys.push({
141 | pubkey: nameAccount,
142 | isSigner: false,
143 | isWritable: true,
144 | });
145 | keys.push({
146 | pubkey: splTokenProgram,
147 | isSigner: false,
148 | isWritable: false,
149 | });
150 | keys.push({
151 | pubkey: splNameServiceProgram,
152 | isSigner: false,
153 | isWritable: false,
154 | });
155 | return new TransactionInstruction({
156 | keys,
157 | programId,
158 | data,
159 | });
160 | }
161 | }
162 | export class withdrawTokensInstruction {
163 | tag: number;
164 | static schema = {
165 | struct: {
166 | tag: "u8",
167 | },
168 | };
169 | constructor() {
170 | this.tag = 4;
171 | }
172 | serialize(): Uint8Array {
173 | return serialize(withdrawTokensInstruction.schema, this);
174 | }
175 | getInstruction(
176 | programId: PublicKey,
177 | nft: PublicKey,
178 | nftOwner: PublicKey,
179 | nftRecord: PublicKey,
180 | tokenDestination: PublicKey,
181 | tokenSource: PublicKey,
182 | splTokenProgram: PublicKey,
183 | systemProgram: PublicKey
184 | ): TransactionInstruction {
185 | const data = Buffer.from(this.serialize());
186 | let keys: AccountKey[] = [];
187 | keys.push({
188 | pubkey: nft,
189 | isSigner: false,
190 | isWritable: true,
191 | });
192 | keys.push({
193 | pubkey: nftOwner,
194 | isSigner: true,
195 | isWritable: true,
196 | });
197 | keys.push({
198 | pubkey: nftRecord,
199 | isSigner: false,
200 | isWritable: true,
201 | });
202 | keys.push({
203 | pubkey: tokenDestination,
204 | isSigner: false,
205 | isWritable: true,
206 | });
207 | keys.push({
208 | pubkey: tokenSource,
209 | isSigner: false,
210 | isWritable: true,
211 | });
212 | keys.push({
213 | pubkey: splTokenProgram,
214 | isSigner: false,
215 | isWritable: false,
216 | });
217 | keys.push({
218 | pubkey: systemProgram,
219 | isSigner: false,
220 | isWritable: false,
221 | });
222 | return new TransactionInstruction({
223 | keys,
224 | programId,
225 | data,
226 | });
227 | }
228 | }
229 | export class createCollectionInstruction {
230 | tag: number;
231 | static schema = {
232 | struct: {
233 | tag: "u8",
234 | },
235 | };
236 | constructor() {
237 | this.tag = 1;
238 | }
239 | serialize(): Uint8Array {
240 | return serialize(createCollectionInstruction.schema, this);
241 | }
242 | getInstruction(
243 | programId: PublicKey,
244 | collectionMint: PublicKey,
245 | edition: PublicKey,
246 | metadataAccount: PublicKey,
247 | centralState: PublicKey,
248 | centralStateNftAta: PublicKey,
249 | feePayer: PublicKey,
250 | splTokenProgram: PublicKey,
251 | metadataProgram: PublicKey,
252 | systemProgram: PublicKey,
253 | splNameServiceProgram: PublicKey,
254 | ataProgram: PublicKey,
255 | rentAccount: PublicKey
256 | ): TransactionInstruction {
257 | const data = Buffer.from(this.serialize());
258 | let keys: AccountKey[] = [];
259 | keys.push({
260 | pubkey: collectionMint,
261 | isSigner: false,
262 | isWritable: true,
263 | });
264 | keys.push({
265 | pubkey: edition,
266 | isSigner: false,
267 | isWritable: true,
268 | });
269 | keys.push({
270 | pubkey: metadataAccount,
271 | isSigner: false,
272 | isWritable: true,
273 | });
274 | keys.push({
275 | pubkey: centralState,
276 | isSigner: false,
277 | isWritable: false,
278 | });
279 | keys.push({
280 | pubkey: centralStateNftAta,
281 | isSigner: false,
282 | isWritable: true,
283 | });
284 | keys.push({
285 | pubkey: feePayer,
286 | isSigner: false,
287 | isWritable: false,
288 | });
289 | keys.push({
290 | pubkey: splTokenProgram,
291 | isSigner: false,
292 | isWritable: false,
293 | });
294 | keys.push({
295 | pubkey: metadataProgram,
296 | isSigner: false,
297 | isWritable: false,
298 | });
299 | keys.push({
300 | pubkey: systemProgram,
301 | isSigner: false,
302 | isWritable: false,
303 | });
304 | keys.push({
305 | pubkey: splNameServiceProgram,
306 | isSigner: false,
307 | isWritable: false,
308 | });
309 | keys.push({
310 | pubkey: ataProgram,
311 | isSigner: false,
312 | isWritable: false,
313 | });
314 | keys.push({
315 | pubkey: rentAccount,
316 | isSigner: false,
317 | isWritable: false,
318 | });
319 | return new TransactionInstruction({
320 | keys,
321 | programId,
322 | data,
323 | });
324 | }
325 | }
326 | export class createNftInstruction {
327 | tag: number;
328 | name: string;
329 | uri: string;
330 | static schema = {
331 | struct: {
332 | tag: "u8",
333 | name: "string",
334 | uri: "string",
335 | },
336 | };
337 | constructor(obj: { name: string; uri: string }) {
338 | this.tag = 2;
339 | this.name = obj.name;
340 | this.uri = obj.uri;
341 | }
342 | serialize(): Uint8Array {
343 | return serialize(createNftInstruction.schema, this);
344 | }
345 | getInstruction(
346 | programId: PublicKey,
347 | mint: PublicKey,
348 | nftDestination: PublicKey,
349 | nameAccount: PublicKey,
350 | nftRecord: PublicKey,
351 | nameOwner: PublicKey,
352 | metadataAccount: PublicKey,
353 | editionAccount: PublicKey,
354 | collectionMetadata: PublicKey,
355 | collectionMint: PublicKey,
356 | centralState: PublicKey,
357 | feePayer: PublicKey,
358 | splTokenProgram: PublicKey,
359 | metadataProgram: PublicKey,
360 | systemProgram: PublicKey,
361 | splNameServiceProgram: PublicKey,
362 | rentAccount: PublicKey,
363 | metadataSigner: PublicKey
364 | ): TransactionInstruction {
365 | const data = Buffer.from(this.serialize());
366 | let keys: AccountKey[] = [];
367 | keys.push({
368 | pubkey: mint,
369 | isSigner: false,
370 | isWritable: true,
371 | });
372 | keys.push({
373 | pubkey: nftDestination,
374 | isSigner: false,
375 | isWritable: true,
376 | });
377 | keys.push({
378 | pubkey: nameAccount,
379 | isSigner: false,
380 | isWritable: true,
381 | });
382 | keys.push({
383 | pubkey: nftRecord,
384 | isSigner: false,
385 | isWritable: true,
386 | });
387 | keys.push({
388 | pubkey: nameOwner,
389 | isSigner: true,
390 | isWritable: true,
391 | });
392 | keys.push({
393 | pubkey: metadataAccount,
394 | isSigner: false,
395 | isWritable: true,
396 | });
397 | keys.push({
398 | pubkey: editionAccount,
399 | isSigner: false,
400 | isWritable: false,
401 | });
402 | keys.push({
403 | pubkey: collectionMetadata,
404 | isSigner: false,
405 | isWritable: false,
406 | });
407 | keys.push({
408 | pubkey: collectionMint,
409 | isSigner: false,
410 | isWritable: false,
411 | });
412 | keys.push({
413 | pubkey: centralState,
414 | isSigner: false,
415 | isWritable: true,
416 | });
417 | keys.push({
418 | pubkey: feePayer,
419 | isSigner: true,
420 | isWritable: true,
421 | });
422 | keys.push({
423 | pubkey: splTokenProgram,
424 | isSigner: false,
425 | isWritable: false,
426 | });
427 | keys.push({
428 | pubkey: metadataProgram,
429 | isSigner: false,
430 | isWritable: false,
431 | });
432 | keys.push({
433 | pubkey: systemProgram,
434 | isSigner: false,
435 | isWritable: false,
436 | });
437 | keys.push({
438 | pubkey: splNameServiceProgram,
439 | isSigner: false,
440 | isWritable: false,
441 | });
442 | keys.push({
443 | pubkey: rentAccount,
444 | isSigner: false,
445 | isWritable: false,
446 | });
447 | keys.push({
448 | pubkey: metadataSigner,
449 | isSigner: true,
450 | isWritable: false,
451 | });
452 | return new TransactionInstruction({
453 | keys,
454 | programId,
455 | data,
456 | });
457 | }
458 | }
459 | export class createMintInstruction {
460 | tag: number;
461 | static schema = {
462 | struct: {
463 | tag: "u8",
464 | },
465 | };
466 | constructor() {
467 | this.tag = 0;
468 | }
469 | serialize(): Uint8Array {
470 | return serialize(createMintInstruction.schema, this);
471 | }
472 | getInstruction(
473 | programId: PublicKey,
474 | mint: PublicKey,
475 | nameAccount: PublicKey,
476 | centralState: PublicKey,
477 | splTokenProgram: PublicKey,
478 | systemProgram: PublicKey,
479 | rentAccount: PublicKey,
480 | feePayer: PublicKey
481 | ): TransactionInstruction {
482 | const data = Buffer.from(this.serialize());
483 | let keys: AccountKey[] = [];
484 | keys.push({
485 | pubkey: mint,
486 | isSigner: false,
487 | isWritable: true,
488 | });
489 | keys.push({
490 | pubkey: nameAccount,
491 | isSigner: false,
492 | isWritable: true,
493 | });
494 | keys.push({
495 | pubkey: centralState,
496 | isSigner: false,
497 | isWritable: false,
498 | });
499 | keys.push({
500 | pubkey: splTokenProgram,
501 | isSigner: false,
502 | isWritable: false,
503 | });
504 | keys.push({
505 | pubkey: systemProgram,
506 | isSigner: false,
507 | isWritable: false,
508 | });
509 | keys.push({
510 | pubkey: rentAccount,
511 | isSigner: false,
512 | isWritable: false,
513 | });
514 | keys.push({
515 | pubkey: feePayer,
516 | isSigner: false,
517 | isWritable: false,
518 | });
519 | return new TransactionInstruction({
520 | keys,
521 | programId,
522 | data,
523 | });
524 | }
525 | }
526 | export class redeemNftInstruction {
527 | tag: number;
528 | static schema = {
529 | struct: {
530 | tag: "u8",
531 | },
532 | };
533 | constructor() {
534 | this.tag = 3;
535 | }
536 | serialize(): Uint8Array {
537 | return serialize(redeemNftInstruction.schema, this);
538 | }
539 | getInstruction(
540 | programId: PublicKey,
541 | mint: PublicKey,
542 | nftSource: PublicKey,
543 | nftOwner: PublicKey,
544 | nftRecord: PublicKey,
545 | nameAccount: PublicKey,
546 | splTokenProgram: PublicKey,
547 | splNameServiceProgram: PublicKey
548 | ): TransactionInstruction {
549 | const data = Buffer.from(this.serialize());
550 | let keys: AccountKey[] = [];
551 | keys.push({
552 | pubkey: mint,
553 | isSigner: false,
554 | isWritable: true,
555 | });
556 | keys.push({
557 | pubkey: nftSource,
558 | isSigner: false,
559 | isWritable: true,
560 | });
561 | keys.push({
562 | pubkey: nftOwner,
563 | isSigner: true,
564 | isWritable: true,
565 | });
566 | keys.push({
567 | pubkey: nftRecord,
568 | isSigner: false,
569 | isWritable: true,
570 | });
571 | keys.push({
572 | pubkey: nameAccount,
573 | isSigner: false,
574 | isWritable: true,
575 | });
576 | keys.push({
577 | pubkey: splTokenProgram,
578 | isSigner: false,
579 | isWritable: false,
580 | });
581 | keys.push({
582 | pubkey: splNameServiceProgram,
583 | isSigner: false,
584 | isWritable: false,
585 | });
586 | return new TransactionInstruction({
587 | keys,
588 | programId,
589 | data,
590 | });
591 | }
592 | }
593 |
--------------------------------------------------------------------------------
/js/src/secondary_bindings.ts:
--------------------------------------------------------------------------------
1 | import { Connection, PublicKey } from "@solana/web3.js";
2 | import { NAME_TOKENIZER_ID } from "./bindings";
3 | import { MINT_PREFIX } from "./state";
4 | import { Buffer } from "buffer";
5 | import { MintLayout } from "@solana/spl-token";
6 |
7 | /**
8 | * This function can be used to retrieve the NFTs of an owner
9 | * @param connection A solana RPC connection
10 | * @param owner The owner to retrieve NFTs for
11 | * @returns
12 | */
13 | export const getNftForOwner = async (
14 | connection: Connection,
15 | owner: PublicKey
16 | ) => {
17 | const filters = [
18 | {
19 | memcmp: {
20 | offset: 0,
21 | bytes: "3",
22 | },
23 | },
24 | {
25 | memcmp: {
26 | offset: 1 + 1 + 32,
27 | bytes: owner.toBase58(),
28 | },
29 | },
30 | ];
31 |
32 | const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, {
33 | filters,
34 | });
35 |
36 | return result;
37 | };
38 |
39 | /**
40 | * This function can used to retrieve the NFT record for a name account
41 | * @param connection A solana RPC connection
42 | * @param nameAccount The name account to retrieve the NftRecord for
43 | * @returns
44 | */
45 | export const getMintFromNameAccount = async (
46 | connection: Connection,
47 | nameAccount: PublicKey
48 | ) => {
49 | const filters = [
50 | {
51 | memcmp: {
52 | offset: 0,
53 | bytes: "3",
54 | },
55 | },
56 | {
57 | memcmp: {
58 | offset: 1 + 1,
59 | bytes: nameAccount.toBase58(),
60 | },
61 | },
62 | ];
63 |
64 | const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, {
65 | filters,
66 | });
67 |
68 | return result;
69 | };
70 |
71 | /**
72 | * This function can be used to retrieve a NFT Record given a mint
73 | *
74 | * @param connection A solana RPC connection
75 | * @param mint The mint of the NFT Record
76 | * @returns
77 | */
78 | export const getRecordFromMint = async (
79 | connection: Connection,
80 | mint: PublicKey
81 | ) => {
82 | const filters = [
83 | {
84 | memcmp: {
85 | offset: 0,
86 | bytes: "3",
87 | },
88 | },
89 | {
90 | memcmp: {
91 | offset: 1 + 1 + 32 + 32,
92 | bytes: mint.toBase58(),
93 | },
94 | },
95 | ];
96 |
97 | const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, {
98 | filters,
99 | });
100 |
101 | return result;
102 | };
103 |
104 | /**
105 | * This function can be used to retrieve all the active NFT record
106 | * @param connection A solana RPC connection
107 | * @returns
108 | */
109 | export const getActiveRecords = async (connection: Connection) => {
110 | const filters = [
111 | {
112 | memcmp: {
113 | offset: 0,
114 | bytes: "3",
115 | },
116 | },
117 | ];
118 |
119 | const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, {
120 | filters,
121 | });
122 |
123 | return result;
124 | };
125 |
126 | export const getMint = (domain: PublicKey) => {
127 | const [mint] = PublicKey.findProgramAddressSync(
128 | [MINT_PREFIX, domain.toBuffer()],
129 | NAME_TOKENIZER_ID
130 | );
131 | return mint;
132 | };
133 |
134 | export const isTokenized = async (
135 | connection: Connection,
136 | domain: PublicKey
137 | ) => {
138 | const mint = getMint(domain);
139 | const info = await connection.getAccountInfo(mint);
140 | if (!info) return false;
141 | const decoded = MintLayout.decode(info.data);
142 | return decoded.supply.toString() === "1";
143 | };
144 |
--------------------------------------------------------------------------------
/js/src/state.ts:
--------------------------------------------------------------------------------
1 | import { deserialize } from "borsh";
2 | import { Connection, PublicKey } from "@solana/web3.js";
3 | import { Buffer } from "buffer";
4 |
5 | export const MINT_PREFIX = Buffer.from("tokenized_name");
6 | export const COLLECTION_PREFIX = Buffer.from("collection");
7 |
8 | export const METADATA_SIGNER = new PublicKey(
9 | "Es33LnWSTZ9GbW6yBaRkSLUaFibVd7iS54e4AvBg76LX"
10 | );
11 |
12 | export enum Tag {
13 | Uninitialized = 0,
14 | CentralState = 1,
15 | ActiveRecord = 2,
16 | InactiveRecord = 3,
17 | }
18 |
19 | export class NftRecord {
20 | tag: Tag;
21 | nonce: number;
22 | nameAccount: PublicKey;
23 | owner: PublicKey;
24 | nftMint: PublicKey;
25 |
26 | static schema = {
27 | struct: {
28 | tag: "u8",
29 | nonce: "u8",
30 | nameAccount: { array: { type: "u8", len: 32 } },
31 | owner: { array: { type: "u8", len: 32 } },
32 | nftMint: { array: { type: "u8", len: 32 } },
33 | },
34 | };
35 |
36 | constructor(obj: {
37 | tag: number;
38 | nonce: number;
39 | nameAccount: Uint8Array;
40 | owner: Uint8Array;
41 | nftMint: Uint8Array;
42 | }) {
43 | this.tag = obj.tag as Tag;
44 | this.nonce = obj.nonce;
45 | this.nameAccount = new PublicKey(obj.nameAccount);
46 | this.owner = new PublicKey(obj.owner);
47 | this.nftMint = new PublicKey(obj.nftMint);
48 | }
49 |
50 | static deserialize(data: Buffer): NftRecord {
51 | return new NftRecord(deserialize(this.schema, data) as any);
52 | }
53 |
54 | static async retrieve(connection: Connection, key: PublicKey) {
55 | const accountInfo = await connection.getAccountInfo(key);
56 | if (!accountInfo || !accountInfo.data) {
57 | throw new Error("NFT record not found");
58 | }
59 | return this.deserialize(accountInfo.data);
60 | }
61 | static async findKey(nameAccount: PublicKey, programId: PublicKey) {
62 | return await PublicKey.findProgramAddress(
63 | [Buffer.from("nft_record"), nameAccount.toBuffer()],
64 | programId
65 | );
66 | }
67 |
68 | static findKeySync(nameAccount: PublicKey, programId: PublicKey) {
69 | return PublicKey.findProgramAddressSync(
70 | [Buffer.from("nft_record"), nameAccount.toBuffer()],
71 | programId
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/js/tests/end_to_end.test.ts:
--------------------------------------------------------------------------------
1 | import { beforeAll, expect, jest, test } from "@jest/globals";
2 | import {
3 | Connection,
4 | Keypair,
5 | LAMPORTS_PER_SOL,
6 | PublicKey,
7 | } from "@solana/web3.js";
8 |
9 | import {
10 | createAssociatedTokenAccountInstruction,
11 | getAssociatedTokenAddressSync,
12 | createTransferInstruction,
13 | MintLayout,
14 | } from "@solana/spl-token";
15 | import { TokenMint, signAndSendInstructions } from "@bonfida/utils";
16 | import {
17 | createNameRegistry,
18 | getNameAccountKey,
19 | getHashedName,
20 | } from "@bonfida/spl-name-service";
21 | import crypto from "crypto";
22 | import {
23 | createMint,
24 | createNft,
25 | redeemNft,
26 | withdrawTokens,
27 | NAME_TOKENIZER_ID_DEVNET,
28 | } from "../src/bindings";
29 | import { Tag, MINT_PREFIX, NftRecord } from "../src/state";
30 | import { Metadata } from "@metaplex-foundation/mpl-token-metadata";
31 |
32 | // Global state initialized once in test startup and cleaned up at test
33 | // teardown.
34 | let connection: Connection;
35 | let feePayer: Keypair;
36 | let programId: PublicKey;
37 |
38 | beforeAll(async () => {
39 | connection = new Connection(
40 | "https://explorer-api.devnet.solana.com/ ",
41 | "confirmed"
42 | );
43 | feePayer = Keypair.generate();
44 | const tx = await connection.requestAirdrop(
45 | feePayer.publicKey,
46 | LAMPORTS_PER_SOL
47 | );
48 | await connection.confirmTransaction(tx, "confirmed");
49 | console.log(`Fee payer airdropped tx ${tx}`);
50 | programId = NAME_TOKENIZER_ID_DEVNET;
51 | });
52 |
53 | jest.setTimeout(1_500_000);
54 |
55 | /**
56 | * Test scenario
57 | *
58 | * Create mint
59 | * Create collection
60 | * Create NFT
61 | * Send funds to the tokenized domain (tokens + SOL)
62 | * Withdraw funds
63 | * Transfer NFT to new wallet
64 | * Sends funds to the tokenized domain (tokens + SOL)
65 | * Withdraw funds
66 | * Sends funds to the tokenized domain (tokens + SOL)
67 | * Redeem NFT
68 | * Withdraw funds
69 | * Create NFT again
70 | * Verify metadata
71 | */
72 |
73 | test("End to end test", async () => {
74 | /**
75 | * Test variables
76 | */
77 | const decimals = Math.pow(10, 6);
78 | const token = await TokenMint.init(connection, feePayer);
79 | const alice = Keypair.generate();
80 | const bob = Keypair.generate();
81 | const uri =
82 | "https://cloudflare-ipfs.com/ipfs/QmcvZWy8eanJvc96iraVdwNXNyT2bQ8ZQsZhETEcbrZJcJ";
83 | const mintAmount = 20 * decimals;
84 | const [centralKey] = await PublicKey.findProgramAddress(
85 | [NAME_TOKENIZER_ID_DEVNET.toBuffer()],
86 | NAME_TOKENIZER_ID_DEVNET
87 | );
88 |
89 | // Expected balances
90 | const bobExpectedBalance = { sol: 0, token: 0 };
91 | const aliceExpectedBalance = { sol: 0, token: 0 };
92 |
93 | /**
94 | * Create token ATA for Alice and Bob
95 | */
96 |
97 | const aliceTokenAtaKey = getAssociatedTokenAddressSync(
98 | token.token,
99 | alice.publicKey
100 | );
101 | const bobTokenAtaKey = getAssociatedTokenAddressSync(
102 | token.token,
103 | bob.publicKey
104 | );
105 | let ix = [
106 | createAssociatedTokenAccountInstruction(
107 | feePayer.publicKey,
108 | aliceTokenAtaKey,
109 | alice.publicKey,
110 | token.token
111 | ),
112 | createAssociatedTokenAccountInstruction(
113 | feePayer.publicKey,
114 | bobTokenAtaKey,
115 | bob.publicKey,
116 | token.token
117 | ),
118 | ];
119 | let tx = await signAndSendInstructions(connection, [], feePayer, ix);
120 |
121 | /**
122 | * Airdrop Alice
123 | */
124 | tx = await connection.requestAirdrop(alice.publicKey, LAMPORTS_PER_SOL);
125 | await connection.confirmTransaction(tx, "confirmed");
126 | aliceExpectedBalance.sol += LAMPORTS_PER_SOL;
127 |
128 | /**
129 | * Create domain name
130 | */
131 | const size = 100 + 96;
132 | const lamports = await connection.getMinimumBalanceForRentExemption(size);
133 | const name = crypto.randomBytes(10).toString();
134 | const hashedName = await getHashedName(name);
135 | const nameKey = await getNameAccountKey(hashedName);
136 | ix = [
137 | await createNameRegistry(
138 | connection,
139 | name,
140 | size,
141 | feePayer.publicKey,
142 | alice.publicKey,
143 | lamports
144 | ),
145 | ];
146 | tx = await signAndSendInstructions(connection, [], feePayer, ix);
147 | console.log(`Create domain tx ${tx}`);
148 |
149 | /**
150 | * Create mint
151 | */
152 | const [mintKey] = await PublicKey.findProgramAddress(
153 | [MINT_PREFIX, nameKey.toBuffer()],
154 | programId
155 | );
156 | ix = await createMint(nameKey, feePayer.publicKey, programId);
157 | tx = await signAndSendInstructions(connection, [], feePayer, ix);
158 |
159 | console.log(`Create mint ${tx}`);
160 |
161 | /**
162 | * Create Collection
163 | */
164 |
165 | // ix = await createCollection(feePayer.publicKey, programId);
166 | // tx = await signAndSendInstructions(connection, [], feePayer, ix);
167 |
168 | // console.log(`Create collection ${tx}`);
169 |
170 | /**
171 | * Create ATAs for Alice and Bob
172 | */
173 | const aliceNftAtaKey = await getAssociatedTokenAddressSync(
174 | mintKey,
175 | alice.publicKey
176 | );
177 | const bobNftAtaKey = await getAssociatedTokenAddressSync(
178 | mintKey,
179 | bob.publicKey
180 | );
181 |
182 | ix = [
183 | createAssociatedTokenAccountInstruction(
184 | feePayer.publicKey,
185 | aliceNftAtaKey,
186 | alice.publicKey,
187 | mintKey
188 | ),
189 | createAssociatedTokenAccountInstruction(
190 | feePayer.publicKey,
191 | bobNftAtaKey,
192 | bob.publicKey,
193 | mintKey
194 | ),
195 | ];
196 | tx = await signAndSendInstructions(connection, [], feePayer, ix);
197 |
198 | console.log(`Create Alice and Bob ATAs`);
199 |
200 | /**
201 | * Verify state
202 | */
203 | let info = await connection.getAccountInfo(mintKey);
204 | let mintInfo = MintLayout.decode(info?.data!);
205 | expect(mintInfo.decimals).toBe(0);
206 | expect(mintInfo.freezeAuthority?.toBase58()).toBe(centralKey.toBase58());
207 | expect(mintInfo.isInitialized).toBe(true);
208 | expect(mintInfo.mintAuthority?.toBase58()).toBe(centralKey.toBase58());
209 | expect(Number(mintInfo.supply)).toBe(0);
210 |
211 | /**
212 | * Create NFT
213 | */
214 | ix = await createNft(
215 | name,
216 | uri,
217 | nameKey,
218 | alice.publicKey,
219 | feePayer.publicKey,
220 | programId
221 | );
222 | tx = await signAndSendInstructions(connection, [alice], feePayer, ix);
223 |
224 | console.log(`Create NFT tx ${tx}`);
225 |
226 | /**
227 | * Verify state
228 | */
229 | info = await connection.getAccountInfo(mintKey);
230 | mintInfo = MintLayout.decode(info?.data!);
231 | expect(Number(mintInfo.supply)).toBe(1);
232 |
233 | const [nftRecordKey, nftRecordNonce] = await NftRecord.findKey(
234 | nameKey,
235 | programId
236 | );
237 | let nftRecord = await NftRecord.retrieve(connection, nftRecordKey);
238 | expect(nftRecord.nameAccount.toBase58()).toBe(nameKey.toBase58());
239 | expect(nftRecord.nftMint.toBase58()).toBe(mintKey.toBase58());
240 | expect(nftRecord.nonce).toBe(nftRecordNonce);
241 | expect(nftRecord.owner.toBase58()).toBe(alice.publicKey.toBase58());
242 | expect(nftRecord.tag).toBe(Tag.ActiveRecord);
243 |
244 | let aliceNftAta = await connection.getTokenAccountBalance(aliceNftAtaKey);
245 | expect(aliceNftAta.value.uiAmount).toBe(1);
246 |
247 | /**
248 | * Send funds to the tokenized domain (tokens + SOL)
249 | */
250 | const nftRecordTokenAtaKey = getAssociatedTokenAddressSync(
251 | token.token,
252 | nftRecordKey,
253 | true
254 | );
255 | ix = [
256 | createAssociatedTokenAccountInstruction(
257 | feePayer.publicKey,
258 | nftRecordTokenAtaKey,
259 | nftRecordKey,
260 | token.token
261 | ),
262 | ];
263 | await signAndSendInstructions(connection, [], feePayer, ix);
264 | await token.mintInto(nftRecordTokenAtaKey, mintAmount);
265 | await connection.requestAirdrop(nftRecordKey, LAMPORTS_PER_SOL / 2);
266 |
267 | aliceExpectedBalance.sol += LAMPORTS_PER_SOL / 2;
268 | aliceExpectedBalance.token += mintAmount;
269 |
270 | /**
271 | * Withdraw funds
272 | */
273 | ix = await withdrawTokens(
274 | mintKey,
275 | token.token,
276 | alice.publicKey,
277 | nftRecordKey,
278 | programId
279 | );
280 | tx = await signAndSendInstructions(connection, [alice], feePayer, ix);
281 | console.log(`Alice withdrew tokens ${tx}`);
282 |
283 | /**
284 | * Verify state
285 | */
286 | let fetchedSolBalance = await connection.getBalance(alice.publicKey);
287 | let fetchedTokenBalance = await connection.getTokenAccountBalance(
288 | aliceTokenAtaKey
289 | );
290 |
291 | expect(aliceExpectedBalance.sol).toBe(fetchedSolBalance);
292 | expect(aliceExpectedBalance.token.toString()).toBe(
293 | fetchedTokenBalance.value.amount
294 | );
295 |
296 | /**
297 | * Transfer NFT to new wallet
298 | */
299 | ix = [
300 | createTransferInstruction(aliceNftAtaKey, bobNftAtaKey, alice.publicKey, 1),
301 | ];
302 | tx = await signAndSendInstructions(connection, [alice], feePayer, ix);
303 | console.log(`Transfer NFT from Alice to Bob`);
304 |
305 | /**
306 | * Send funds to the tokenized domain (tokens + SOL)
307 | */
308 | await token.mintInto(nftRecordTokenAtaKey, mintAmount);
309 | await connection.requestAirdrop(nftRecordKey, LAMPORTS_PER_SOL / 2);
310 |
311 | bobExpectedBalance.sol += LAMPORTS_PER_SOL / 2;
312 | bobExpectedBalance.token += mintAmount;
313 |
314 | /**
315 | * Withdraw funds
316 | */
317 | ix = await withdrawTokens(
318 | mintKey,
319 | token.token,
320 | bob.publicKey,
321 | nftRecordKey,
322 | programId
323 | );
324 | tx = await signAndSendInstructions(connection, [bob], feePayer, ix);
325 | console.log(`Bob withdrew tokens ${tx}`);
326 |
327 | /**
328 | * Verify state
329 | */
330 | fetchedSolBalance = await connection.getBalance(bob.publicKey);
331 | fetchedTokenBalance = await connection.getTokenAccountBalance(bobTokenAtaKey);
332 |
333 | expect(bobExpectedBalance.sol).toBe(fetchedSolBalance);
334 | expect(bobExpectedBalance.token.toString()).toBe(
335 | fetchedTokenBalance.value.amount
336 | );
337 |
338 | /**
339 | * Sends funds to the tokenized domain (tokens + SOL)
340 | */
341 | await token.mintInto(nftRecordTokenAtaKey, mintAmount);
342 | await connection.requestAirdrop(nftRecordKey, LAMPORTS_PER_SOL / 2);
343 |
344 | bobExpectedBalance.sol += LAMPORTS_PER_SOL / 2;
345 | bobExpectedBalance.token += mintAmount;
346 |
347 | /**
348 | * Redeem NFT
349 | */
350 | ix = await redeemNft(nameKey, bob.publicKey, programId);
351 | tx = await signAndSendInstructions(connection, [bob], feePayer, ix);
352 | console.log(`Bob redeemed NFT ${tx}`);
353 |
354 | /**
355 | * Verify state
356 | */
357 | info = await connection.getAccountInfo(mintKey);
358 | mintInfo = MintLayout.decode(info?.data!);
359 | expect(Number(mintInfo.supply)).toBe(0);
360 |
361 | nftRecord = await NftRecord.retrieve(connection, nftRecordKey);
362 | expect(nftRecord.nameAccount.toBase58()).toBe(nameKey.toBase58());
363 | expect(nftRecord.nftMint.toBase58()).toBe(mintKey.toBase58());
364 | expect(nftRecord.nonce).toBe(nftRecordNonce);
365 | expect(nftRecord.owner.toBase58()).toBe(bob.publicKey.toBase58());
366 | expect(nftRecord.tag).toBe(Tag.InactiveRecord);
367 |
368 | /**
369 | * Withdraw funds
370 | */
371 | ix = await withdrawTokens(
372 | mintKey,
373 | token.token,
374 | bob.publicKey,
375 | nftRecordKey,
376 | programId
377 | );
378 | tx = await signAndSendInstructions(connection, [bob], feePayer, ix);
379 | console.log(`Bob withdrew tokens ${tx}`);
380 |
381 | /**
382 | * Verify state
383 | */
384 | fetchedSolBalance = await connection.getBalance(bob.publicKey);
385 | fetchedTokenBalance = await connection.getTokenAccountBalance(bobTokenAtaKey);
386 |
387 | expect(bobExpectedBalance.sol).toBe(fetchedSolBalance);
388 | expect(bobExpectedBalance.token.toString()).toBe(
389 | fetchedTokenBalance.value.amount
390 | );
391 |
392 | /**
393 | * Create NFT again
394 | */
395 | ix = await createNft(
396 | name,
397 | uri,
398 | nameKey,
399 | bob.publicKey,
400 | feePayer.publicKey,
401 | programId
402 | );
403 | tx = await signAndSendInstructions(connection, [bob], feePayer, ix);
404 |
405 | /**
406 | * Verify state
407 | */
408 |
409 | info = await connection.getAccountInfo(mintKey);
410 | mintInfo = MintLayout.decode(info?.data!);
411 | expect(mintInfo.decimals).toBe(0);
412 | expect(mintInfo.freezeAuthority?.toBase58()).toBe(centralKey.toBase58());
413 | expect(mintInfo.isInitialized).toBe(true);
414 | expect(mintInfo.mintAuthority?.toBase58()).toBe(centralKey.toBase58());
415 | expect(Number(mintInfo.supply)).toBe(1);
416 |
417 | nftRecord = await NftRecord.retrieve(connection, nftRecordKey);
418 | expect(nftRecord.nameAccount.toBase58()).toBe(nameKey.toBase58());
419 | expect(nftRecord.nftMint.toBase58()).toBe(mintKey.toBase58());
420 | expect(nftRecord.nonce).toBe(nftRecordNonce);
421 | expect(nftRecord.owner.toBase58()).toBe(bob.publicKey.toBase58());
422 | expect(nftRecord.tag).toBe(Tag.ActiveRecord);
423 |
424 | /**
425 | * Verify metadata
426 | */
427 | const metadata = await Metadata.findByMint(connection, mintKey);
428 |
429 | expect(metadata.data.data.name).toBe(name);
430 | expect(metadata.data.data.sellerFeeBasisPoints).toBe(500);
431 | expect(metadata.data.data.symbol).toBe(".sol");
432 | expect(metadata.data.data.uri).toBe(uri);
433 | expect(metadata.data.isMutable).toBe(1);
434 | expect(metadata.data.mint).toBe(mintKey.toBase58());
435 | expect(metadata.data.updateAuthority).toBe(centralKey.toBase58());
436 |
437 | expect(JSON.stringify(metadata.data.data.creators)).toBe(
438 | `[{"address":"${centralKey.toBase58()}","verified":1,"share":0},{"address":"94xt1Eyc56YDU6MtV7KsG8xfeRqd7z272g14tBHztnUM","verified":0,"share":100}]`
439 | );
440 | });
441 |
--------------------------------------------------------------------------------
/js/tests/pda.test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MasterEdition,
3 | Metadata,
4 | } from "@metaplex-foundation/mpl-token-metadata";
5 | import { test, expect } from "@jest/globals";
6 | import { PublicKey, Keypair } from "@solana/web3.js";
7 | import { getMasterEditionPda, getMetadataPda } from "../src/bindings";
8 |
9 | test("Metaplex PDA", async () => {
10 | const mint = Keypair.generate().publicKey;
11 | const metadata = await Metadata.getPDA(mint);
12 | expect(metadata.toBase58()).toBe(getMetadataPda(mint).toBase58());
13 | const master = await MasterEdition.getPDA(mint);
14 | expect(master.toBase58()).toBe(getMasterEditionPda(mint).toBase58());
15 | });
16 |
--------------------------------------------------------------------------------
/js/tests/utils.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Keypair,
3 | PublicKey,
4 | Connection,
5 | LAMPORTS_PER_SOL,
6 | TransactionInstruction,
7 | Transaction,
8 | } from "@solana/web3.js";
9 | import * as path from "path";
10 | import { readFileSync, writeSync, closeSync } from "fs";
11 | import { ChildProcess, spawn, execSync } from "child_process";
12 | import tmp from "tmp";
13 |
14 | const programName = "name_tokenizer";
15 |
16 | // Spawns a local solana test validator. Caller is responsible for killing the
17 | // process.
18 | export async function spawnLocalSolana(): Promise {
19 | const ledger = tmp.dirSync();
20 | return spawn("solana-test-validator", ["-l", ledger.name]);
21 | }
22 |
23 | // Returns a keypair and key file name.
24 | export function initializePayer(): [Keypair, string] {
25 | const key = new Keypair();
26 | const tmpobj = tmp.fileSync();
27 | writeSync(tmpobj.fd, JSON.stringify(Array.from(key.secretKey)));
28 | closeSync(tmpobj.fd);
29 | return [key, tmpobj.name];
30 | }
31 |
32 | // Deploys the agnostic order book program. Fees are paid with the fee payer
33 | // whose key is in the given key file.
34 | export function deployProgram(
35 | payerKeyFile: string,
36 | compile: boolean,
37 | compileFlag?: string,
38 | testBpf?: boolean
39 | ): PublicKey {
40 | const programDirectory = path.join(path.dirname(__filename), "../../program");
41 | const program = path.join(
42 | programDirectory,
43 | `target/deploy/${programName}.so`
44 | );
45 | const keyfile = path.join(
46 | path.dirname(program),
47 | `${programName}-keypair.json`
48 | );
49 | let compileCmd = "cargo build-bpf";
50 | if (compileFlag) {
51 | compileCmd += ` --features ${compileFlag}`;
52 | }
53 | if (compile) {
54 | execSync(compileCmd, {
55 | cwd: programDirectory,
56 | });
57 | }
58 | if (testBpf) {
59 | execSync("cargo test-bpf --features devnet", {
60 | cwd: programDirectory,
61 | });
62 | }
63 |
64 | const bytes = readFileSync(keyfile, "utf-8");
65 | const keypair = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(bytes)));
66 | execSync(
67 | [
68 | "solana program deploy",
69 | program,
70 | "--program-id",
71 | keyfile,
72 | "-u localhost",
73 | "-k",
74 | payerKeyFile,
75 | "--commitment finalized",
76 | ].join(" ")
77 | );
78 | spawn("solana", ["logs", "-u", "localhost"], { stdio: "inherit" });
79 | return keypair.publicKey;
80 | }
81 |
82 | // Funds the given account. Sleeps until the connection is ready.
83 | export async function airdropPayer(connection: Connection, key: PublicKey) {
84 | while (true) {
85 | try {
86 | const signature = await connection.requestAirdrop(
87 | key,
88 | 1 * LAMPORTS_PER_SOL
89 | );
90 | console.log(`Airdrop signature ${signature}`);
91 | await connection.confirmTransaction(signature, "finalized");
92 | return;
93 | } catch (e) {
94 | console.log(`Error airdropping ${e}`);
95 | await new Promise((resolve) => setTimeout(resolve, 1000));
96 | continue;
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/js/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@tsconfig/recommended/tsconfig.json",
3 | "ts-node": {
4 | "compilerOptions": {
5 | "module": "commonjs",
6 | "baseUrl": "./",
7 | "paths": {
8 | "*": ["types/*"]
9 | }
10 | }
11 | },
12 | "compilerOptions": {
13 | "allowJs": true,
14 | "module": "esnext",
15 | "esModuleInterop": true,
16 | "allowSyntheticDefaultImports": true,
17 | "target": "es2019",
18 | "outDir": "dist",
19 | "rootDir": "./src",
20 | "declaration": true,
21 | "noImplicitAny": false,
22 | "moduleResolution": "node",
23 | "sourceMap": false,
24 | "baseUrl": ".",
25 | "paths": {
26 | "*": ["node_modules/*", "src/types/*"]
27 | },
28 | "resolveJsonModule": true
29 | },
30 | "include": ["src/*", "src/.ts"],
31 | "exclude": ["src/**/*.test.ts", "**/node_modules", "dist"]
32 | }
33 |
--------------------------------------------------------------------------------
/program/Anchor.toml:
--------------------------------------------------------------------------------
1 | anchor_version = "0.25.0"
2 |
3 |
4 | [workspace]
5 | members = ["./"]
6 |
7 |
8 | [provider]
9 | cluster = "mainnet"
10 | wallet = "~/.config/solana/id.json"
11 |
12 |
13 | [programs.mainnet]
14 | name-tokenizer = "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk"
--------------------------------------------------------------------------------
/program/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "name-tokenizer"
3 | version = "2.0.0"
4 | edition = "2018"
5 | description = "A Solana program which enables the tokenization of a Solana Name Service into an NFT"
6 | license = "MIT"
7 | repository = "https://github.com/bonfida/name-tokenizer"
8 |
9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
10 |
11 | [features]
12 | no-entrypoint = []
13 | test-bpf = []
14 | devnet = []
15 |
16 |
17 | [dependencies]
18 | solana-program = "1.18.11"
19 | num_enum = "0.5.4"
20 | borsh = "0.10.3"
21 | thiserror = "1.0.44"
22 | num-traits = "0.2"
23 | num-derive = "0.3"
24 | enumflags2 = "0.7.1"
25 | spl-token = {version="4.0.0", features= ["no-entrypoint"]}
26 | bonfida-utils = "0.6.0"
27 | spl-associated-token-account = {version = "2.3.0", features = ["no-entrypoint"]}
28 | spl-name-service = { version = "0.3.0", features = ["no-entrypoint"] }
29 | mpl-token-metadata = { version = "4.0.0" }
30 | solana-security-txt = "1.1.1"
31 |
32 |
33 |
34 | [dev-dependencies]
35 | hexdump = "0.1.0"
36 | solana-sdk = "1.18.11"
37 | rand = "0.8.4"
38 | arrayref = "0.3.6"
39 | solana-program-test = "1.18.11"
40 | tokio = {version="1.6", features = ["macros"]}
41 |
42 |
43 |
44 | [lib]
45 | crate-type = ["cdylib", "lib"]
46 |
--------------------------------------------------------------------------------
/program/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM rust:1.87.0
2 |
3 | ENV HOME="/root"
4 | ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
5 |
6 | #
7 | RUN apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \
8 | build-essential libssl-dev libudev-dev
9 |
10 |
11 | # Install Solana tools.
12 | RUN sh -c "$(curl -sSfL https://release.anza.xyz/v2.1.21/install)"
13 |
14 | RUN cargo install bonfida-cli --version 0.6.9 --locked
15 |
16 | RUN bonfida autoproject dummy
17 |
18 | WORKDIR /dummy/program
19 |
20 | RUN cargo build-sbf
21 |
22 | WORKDIR /
23 |
24 | RUN rm -r dummy
25 |
26 | WORKDIR /workdir
--------------------------------------------------------------------------------
/program/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Check if the Docker image exists
3 | set -e
4 | docker build -t name_tokenizer .
5 |
6 |
7 | solana program dump metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s target/deploy/mpl_token_metadata.so
8 | solana program dump namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX target/deploy/spl_name_service.so
9 |
10 |
11 | if [[ ${1} == "build-only" ]]; then
12 | echo "Only building..."
13 | docker run -it --net=host --mount type=bind,source=$(pwd),target=/workdir name_tokenizer:latest /bin/bash -c "cargo build-sbf"
14 | elif [[ ${1} == "test" ]]; then
15 | echo "Running tests..."
16 | docker run -it --net=host --mount type=bind,source=$(pwd),target=/workdir name_tokenizer:latest /bin/bash -c "cargo test-sbf --features devnet"
17 | else
18 | echo "Running tests + building..."
19 | docker run -it --net=host --mount type=bind,source=$(pwd),target=/workdir name_tokenizer:latest /bin/bash -c "cargo test-sbf --features devnet && cargo build-sbf"
20 | fi
--------------------------------------------------------------------------------
/program/idl.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "name": "name-tokenizer",
4 | "instructions": [
5 | {
6 | "name": "edit_data",
7 | "accounts": [
8 | {
9 | "name": "nftOwner",
10 | "isMut": false,
11 | "isSigner": true
12 | },
13 | {
14 | "name": "nftAccount",
15 | "isMut": false,
16 | "isSigner": false
17 | },
18 | {
19 | "name": "nftRecord",
20 | "isMut": false,
21 | "isSigner": false
22 | },
23 | {
24 | "name": "nameAccount",
25 | "isMut": true,
26 | "isSigner": false
27 | },
28 | {
29 | "name": "splTokenProgram",
30 | "isMut": false,
31 | "isSigner": false
32 | },
33 | {
34 | "name": "splNameServiceProgram",
35 | "isMut": false,
36 | "isSigner": false
37 | }
38 | ],
39 | "args": [
40 | {
41 | "name": "offset",
42 | "type": "u32"
43 | },
44 | {
45 | "name": "data",
46 | "type": {
47 | "vec": "u8"
48 | }
49 | }
50 | ]
51 | },
52 | {
53 | "name": "create_nft",
54 | "accounts": [
55 | {
56 | "name": "mint",
57 | "isMut": true,
58 | "isSigner": false
59 | },
60 | {
61 | "name": "nftDestination",
62 | "isMut": true,
63 | "isSigner": false
64 | },
65 | {
66 | "name": "nameAccount",
67 | "isMut": true,
68 | "isSigner": false
69 | },
70 | {
71 | "name": "nftRecord",
72 | "isMut": true,
73 | "isSigner": false
74 | },
75 | {
76 | "name": "nameOwner",
77 | "isMut": true,
78 | "isSigner": true
79 | },
80 | {
81 | "name": "metadataAccount",
82 | "isMut": true,
83 | "isSigner": false
84 | },
85 | {
86 | "name": "editionAccount",
87 | "isMut": false,
88 | "isSigner": false
89 | },
90 | {
91 | "name": "collectionMetadata",
92 | "isMut": false,
93 | "isSigner": false
94 | },
95 | {
96 | "name": "collectionMint",
97 | "isMut": false,
98 | "isSigner": false
99 | },
100 | {
101 | "name": "centralState",
102 | "isMut": true,
103 | "isSigner": false
104 | },
105 | {
106 | "name": "feePayer",
107 | "isMut": true,
108 | "isSigner": true
109 | },
110 | {
111 | "name": "splTokenProgram",
112 | "isMut": false,
113 | "isSigner": false
114 | },
115 | {
116 | "name": "metadataProgram",
117 | "isMut": false,
118 | "isSigner": false
119 | },
120 | {
121 | "name": "systemProgram",
122 | "isMut": false,
123 | "isSigner": false
124 | },
125 | {
126 | "name": "splNameServiceProgram",
127 | "isMut": false,
128 | "isSigner": false
129 | },
130 | {
131 | "name": "rentAccount",
132 | "isMut": false,
133 | "isSigner": false
134 | },
135 | {
136 | "name": "metadataSigner",
137 | "isMut": false,
138 | "isSigner": true
139 | }
140 | ],
141 | "args": [
142 | {
143 | "name": "name",
144 | "type": "string"
145 | },
146 | {
147 | "name": "uri",
148 | "type": "string"
149 | }
150 | ]
151 | },
152 | {
153 | "name": "create_collection",
154 | "accounts": [
155 | {
156 | "name": "collectionMint",
157 | "isMut": true,
158 | "isSigner": false
159 | },
160 | {
161 | "name": "edition",
162 | "isMut": true,
163 | "isSigner": false
164 | },
165 | {
166 | "name": "metadataAccount",
167 | "isMut": true,
168 | "isSigner": false
169 | },
170 | {
171 | "name": "centralState",
172 | "isMut": false,
173 | "isSigner": false
174 | },
175 | {
176 | "name": "centralStateNftAta",
177 | "isMut": true,
178 | "isSigner": false
179 | },
180 | {
181 | "name": "feePayer",
182 | "isMut": false,
183 | "isSigner": false
184 | },
185 | {
186 | "name": "splTokenProgram",
187 | "isMut": false,
188 | "isSigner": false
189 | },
190 | {
191 | "name": "metadataProgram",
192 | "isMut": false,
193 | "isSigner": false
194 | },
195 | {
196 | "name": "systemProgram",
197 | "isMut": false,
198 | "isSigner": false
199 | },
200 | {
201 | "name": "splNameServiceProgram",
202 | "isMut": false,
203 | "isSigner": false
204 | },
205 | {
206 | "name": "ataProgram",
207 | "isMut": false,
208 | "isSigner": false
209 | },
210 | {
211 | "name": "rentAccount",
212 | "isMut": false,
213 | "isSigner": false
214 | }
215 | ],
216 | "args": []
217 | },
218 | {
219 | "name": "withdraw_tokens",
220 | "accounts": [
221 | {
222 | "name": "nft",
223 | "isMut": true,
224 | "isSigner": false
225 | },
226 | {
227 | "name": "nftOwner",
228 | "isMut": true,
229 | "isSigner": true
230 | },
231 | {
232 | "name": "nftRecord",
233 | "isMut": true,
234 | "isSigner": false
235 | },
236 | {
237 | "name": "tokenDestination",
238 | "isMut": true,
239 | "isSigner": false
240 | },
241 | {
242 | "name": "tokenSource",
243 | "isMut": true,
244 | "isSigner": false
245 | },
246 | {
247 | "name": "splTokenProgram",
248 | "isMut": false,
249 | "isSigner": false
250 | },
251 | {
252 | "name": "systemProgram",
253 | "isMut": false,
254 | "isSigner": false
255 | }
256 | ],
257 | "args": []
258 | },
259 | {
260 | "name": "create_mint",
261 | "accounts": [
262 | {
263 | "name": "mint",
264 | "isMut": true,
265 | "isSigner": false
266 | },
267 | {
268 | "name": "nameAccount",
269 | "isMut": true,
270 | "isSigner": false
271 | },
272 | {
273 | "name": "centralState",
274 | "isMut": false,
275 | "isSigner": false
276 | },
277 | {
278 | "name": "splTokenProgram",
279 | "isMut": false,
280 | "isSigner": false
281 | },
282 | {
283 | "name": "systemProgram",
284 | "isMut": false,
285 | "isSigner": false
286 | },
287 | {
288 | "name": "rentAccount",
289 | "isMut": false,
290 | "isSigner": false
291 | },
292 | {
293 | "name": "feePayer",
294 | "isMut": false,
295 | "isSigner": false
296 | }
297 | ],
298 | "args": []
299 | },
300 | {
301 | "name": "redeem_nft",
302 | "accounts": [
303 | {
304 | "name": "mint",
305 | "isMut": true,
306 | "isSigner": false
307 | },
308 | {
309 | "name": "nftSource",
310 | "isMut": true,
311 | "isSigner": false
312 | },
313 | {
314 | "name": "nftOwner",
315 | "isMut": true,
316 | "isSigner": true
317 | },
318 | {
319 | "name": "nftRecord",
320 | "isMut": true,
321 | "isSigner": false
322 | },
323 | {
324 | "name": "nameAccount",
325 | "isMut": true,
326 | "isSigner": false
327 | },
328 | {
329 | "name": "splTokenProgram",
330 | "isMut": false,
331 | "isSigner": false
332 | },
333 | {
334 | "name": "splNameServiceProgram",
335 | "isMut": false,
336 | "isSigner": false
337 | }
338 | ],
339 | "args": []
340 | }
341 | ],
342 | "accounts": [
343 | {
344 | "name": "NftRecord",
345 | "type": {
346 | "kind": "struct",
347 | "fields": [
348 | {
349 | "name": "tag",
350 | "type": "u8"
351 | },
352 | {
353 | "name": "nonce",
354 | "type": "u8"
355 | },
356 | {
357 | "name": "nameAccount",
358 | "type": "publicKey"
359 | },
360 | {
361 | "name": "owner",
362 | "type": "publicKey"
363 | },
364 | {
365 | "name": "nftMint",
366 | "type": "publicKey"
367 | }
368 | ]
369 | }
370 | },
371 | {
372 | "name": "CentralState",
373 | "type": {
374 | "kind": "struct",
375 | "fields": [
376 | {
377 | "name": "tag",
378 | "type": "u8"
379 | }
380 | ]
381 | }
382 | }
383 | ]
384 | }
--------------------------------------------------------------------------------
/program/rust-toolchain.toml:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | profile = "default"
3 | channel = "1.87.0"
4 |
--------------------------------------------------------------------------------
/program/src/cpi.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::AccountInfo, entrypoint::ProgramResult, msg, program::invoke_signed,
3 | program_error::ProgramError, program_pack::Pack, pubkey::Pubkey, rent::Rent,
4 | system_instruction, system_instruction::create_account, sysvar::Sysvar,
5 | };
6 |
7 | #[allow(missing_docs)]
8 | pub struct Cpi {}
9 |
10 | impl Cpi {
11 | #[allow(missing_docs)]
12 | pub fn create_account<'a>(
13 | program_id: &Pubkey,
14 | system_program: &AccountInfo<'a>,
15 | fee_payer: &AccountInfo<'a>,
16 | account_to_create: &AccountInfo<'a>,
17 | signer_seeds: &[&[u8]],
18 | space: usize,
19 | ) -> ProgramResult {
20 | let account_lamports = account_to_create.lamports();
21 | if account_lamports != 0 && account_to_create.data_is_empty() {
22 | let defund_created_account = system_instruction::transfer(
23 | account_to_create.key,
24 | fee_payer.key,
25 | account_lamports,
26 | );
27 | invoke_signed(
28 | &defund_created_account,
29 | &[
30 | system_program.clone(),
31 | fee_payer.clone(),
32 | account_to_create.clone(),
33 | ],
34 | &[signer_seeds],
35 | )?;
36 | }
37 | let create_state_instruction = create_account(
38 | fee_payer.key,
39 | account_to_create.key,
40 | Rent::get()?.minimum_balance(space),
41 | space as u64,
42 | program_id,
43 | );
44 |
45 | invoke_signed(
46 | &create_state_instruction,
47 | &[
48 | system_program.clone(),
49 | fee_payer.clone(),
50 | account_to_create.clone(),
51 | ],
52 | &[signer_seeds],
53 | )
54 | }
55 |
56 | #[allow(clippy::too_many_arguments)]
57 | pub fn allocate_and_create_token_account<'a>(
58 | token_account_owner: &Pubkey,
59 | spl_token_program: &AccountInfo<'a>,
60 | payer_info: &AccountInfo<'a>,
61 | signer_seeds: &[&[u8]],
62 | token_account: &AccountInfo<'a>,
63 | mint_account: &AccountInfo<'a>,
64 | rent_account: &AccountInfo<'a>,
65 | system_program_info: &AccountInfo<'a>,
66 | ) -> Result<(), ProgramError> {
67 | msg!("Initializing token account");
68 | let size = spl_token::state::Account::LEN;
69 | let required_lamports = Rent::get()?.minimum_balance(size);
70 | let ix_allocate = create_account(
71 | payer_info.key,
72 | token_account.key,
73 | required_lamports,
74 | size as u64,
75 | &spl_token::ID,
76 | );
77 | invoke_signed(
78 | &ix_allocate,
79 | &[
80 | system_program_info.clone(),
81 | payer_info.clone(),
82 | token_account.clone(),
83 | ],
84 | &[signer_seeds],
85 | )?;
86 | let ix_initialize = spl_token::instruction::initialize_account2(
87 | &spl_token::ID,
88 | token_account.key,
89 | mint_account.key,
90 | token_account_owner,
91 | )?;
92 | invoke_signed(
93 | &ix_initialize,
94 | &[
95 | spl_token_program.clone(),
96 | token_account.clone(),
97 | mint_account.clone(),
98 | rent_account.clone(),
99 | ],
100 | &[signer_seeds],
101 | )?;
102 | Ok(())
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/program/src/entrypoint.rs:
--------------------------------------------------------------------------------
1 | use crate::{error::OfferError, processor::Processor};
2 |
3 | use {
4 | num_traits::FromPrimitive,
5 | solana_program::{
6 | account_info::AccountInfo, decode_error::DecodeError, entrypoint::ProgramResult, msg,
7 | program_error::PrintProgramError, pubkey::Pubkey,
8 | },
9 | };
10 |
11 | #[cfg(not(feature = "no-entrypoint"))]
12 | use solana_program::entrypoint;
13 | #[cfg(not(feature = "no-entrypoint"))]
14 | entrypoint!(process_instruction);
15 |
16 | /// The entrypoint to the program
17 | pub fn process_instruction(
18 | program_id: &Pubkey,
19 | accounts: &[AccountInfo],
20 | instruction_data: &[u8],
21 | ) -> ProgramResult {
22 | msg!("Entrypoint");
23 | if let Err(error) = Processor::process_instruction(program_id, accounts, instruction_data) {
24 | // catch the error so we can print it
25 | error.print::();
26 | return Err(error);
27 | }
28 | Ok(())
29 | }
30 |
31 | impl PrintProgramError for OfferError {
32 | fn print(&self)
33 | where
34 | E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive,
35 | {
36 | match self {
37 | OfferError::AlreadyInitialized => msg!("Error: This account is already initialized"),
38 | OfferError::DataTypeMismatch => msg!("Error: Data type mismatch"),
39 | OfferError::WrongOwner => msg!("Error: Wrong account owner"),
40 | OfferError::Uninitialized => msg!("Error: Account is uninitialized"),
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/program/src/error.rs:
--------------------------------------------------------------------------------
1 | use {
2 | num_derive::FromPrimitive,
3 | solana_program::{decode_error::DecodeError, program_error::ProgramError},
4 | thiserror::Error,
5 | };
6 |
7 | #[derive(Clone, Debug, Error, FromPrimitive)]
8 | pub enum OfferError {
9 | #[error("This account is already initialized")]
10 | AlreadyInitialized,
11 | #[error("Data type mismatch")]
12 | DataTypeMismatch,
13 | #[error("Wrong account owner")]
14 | WrongOwner,
15 | #[error("Account is uninitialized")]
16 | Uninitialized,
17 | }
18 |
19 | impl From for ProgramError {
20 | fn from(e: OfferError) -> Self {
21 | ProgramError::Custom(e as u32)
22 | }
23 | }
24 |
25 | impl DecodeError for OfferError {
26 | fn type_of() -> &'static str {
27 | "OfferError"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/program/src/instruction.rs:
--------------------------------------------------------------------------------
1 | pub use crate::processor::{
2 | create_collection, create_mint, create_nft, edit_data, redeem_nft, unverify_nft,
3 | withdraw_tokens,
4 | };
5 | use {
6 | bonfida_utils::InstructionsAccount,
7 | borsh::{BorshDeserialize, BorshSerialize},
8 | num_derive::FromPrimitive,
9 | solana_program::{instruction::Instruction, pubkey::Pubkey},
10 | };
11 | #[allow(missing_docs)]
12 | #[derive(BorshDeserialize, BorshSerialize, FromPrimitive)]
13 | pub enum ProgramInstruction {
14 | /// Create the NFT mint
15 | ///
16 | /// | Index | Writable | Signer | Description |
17 | /// | --------------------------------------------------------- |
18 | /// | 0 | ✅ | ❌ | The mint of the NFT |
19 | /// | 1 | ✅ | ❌ | The domain name account |
20 | /// | 2 | ❌ | ❌ | The central state account |
21 | /// | 3 | ❌ | ❌ | The SPL token program account |
22 | /// | 4 | ❌ | ❌ | The system program account |
23 | /// | 5 | ❌ | ❌ | Rent sysvar account |
24 | /// | 6 | ❌ | ❌ | Fee payer account |
25 | CreateMint,
26 | /// Create a verified collection
27 | ///
28 | /// | Index | Writable | Signer | Description |
29 | /// | ----------------------------------------------------------------------------------------- |
30 | /// | 0 | ✅ | ❌ | The mint of the collection |
31 | /// | 1 | ✅ | ❌ | |
32 | /// | 2 | ✅ | ❌ | The metadata account |
33 | /// | 3 | ❌ | ❌ | The central state account |
34 | /// | 4 | ✅ | ❌ | Token account of the central state to hold the master edition |
35 | /// | 5 | ❌ | ❌ | The fee payer account |
36 | /// | 6 | ❌ | ❌ | The SPL token program account |
37 | /// | 7 | ❌ | ❌ | The metadata program account |
38 | /// | 8 | ❌ | ❌ | The system program account |
39 | /// | 9 | ❌ | ❌ | The SPL name service program account |
40 | /// | 10 | ❌ | ❌ | |
41 | /// | 11 | ❌ | ❌ | Rent sysvar account |
42 | CreateCollection,
43 | /// Tokenize a domain name
44 | ///
45 | /// | Index | Writable | Signer | Description |
46 | /// | ---------------------------------------------------------------- |
47 | /// | 0 | ✅ | ❌ | The mint of the NFT |
48 | /// | 1 | ✅ | ❌ | The NFT token destination |
49 | /// | 2 | ✅ | ❌ | The domain name account |
50 | /// | 3 | ✅ | ❌ | The NFT record account |
51 | /// | 4 | ✅ | ✅ | The domain name owner |
52 | /// | 5 | ✅ | ❌ | The metadata account |
53 | /// | 6 | ❌ | ❌ | Master edition account |
54 | /// | 7 | ❌ | ❌ | Collection |
55 | /// | 8 | ❌ | ❌ | Mint of the collection |
56 | /// | 9 | ✅ | ❌ | The central state account |
57 | /// | 10 | ✅ | ✅ | The fee payer account |
58 | /// | 11 | ❌ | ❌ | The SPL token program account |
59 | /// | 12 | ❌ | ❌ | The metadata program account |
60 | /// | 13 | ❌ | ❌ | The system program account |
61 | /// | 14 | ❌ | ❌ | The SPL name service program account |
62 | /// | 15 | ❌ | ❌ | Rent sysvar account |
63 | /// | 16 | ❌ | ✅ | The metadata signer |
64 | CreateNft,
65 | /// Redeem a tokenized domain name
66 | ///
67 | /// | Index | Writable | Signer | Description |
68 | /// | --------------------------------------------------------------------- |
69 | /// | 0 | ✅ | ❌ | The mint of the NFT |
70 | /// | 1 | ✅ | ❌ | The current token account holding the NFT |
71 | /// | 2 | ✅ | ✅ | The NFT owner account |
72 | /// | 3 | ✅ | ❌ | The NFT record account |
73 | /// | 4 | ✅ | ❌ | The domain name account |
74 | /// | 5 | ❌ | ❌ | The SPL token program account |
75 | /// | 6 | ❌ | ❌ | The SPL name service program account |
76 | RedeemNft,
77 | /// Withdraw funds that have been sent to the escrow
78 | /// while the domain was tokenized
79 | ///
80 | /// | Index | Writable | Signer | Description |
81 | /// | ---------------------------------------------------------------------- |
82 | /// | 0 | ✅ | ❌ | The token account holding the NFT |
83 | /// | 1 | ✅ | ✅ | The owner of the NFT token account |
84 | /// | 2 | ✅ | ❌ | The NFT record account |
85 | /// | 3 | ✅ | ❌ | The destination for tokens being withdrawn |
86 | /// | 4 | ✅ | ❌ | The source for tokens being withdrawn |
87 | /// | 5 | ❌ | ❌ | The SPL token program account |
88 | /// | 6 | ❌ | ❌ | The system program account |
89 | WithdrawTokens,
90 | /// Edit the data registry of a tokenized domain name
91 | ///
92 | /// | Index | Writable | Signer | Description |
93 | /// | ---------------------------------------------------------------- |
94 | /// | 0 | ❌ | ✅ | The NFT owner account |
95 | /// | 1 | ❌ | ❌ | The NFT account |
96 | /// | 2 | ❌ | ❌ | The NFT record account |
97 | /// | 3 | ✅ | ❌ | The domain name account |
98 | /// | 4 | ❌ | ❌ | The SPL token program account |
99 | /// | 5 | ❌ | ❌ | The SPL name service program account |
100 | EditData,
101 | /// Unverify an NFT
102 | ///
103 | /// | Index | Writable | Signer | Description |
104 | /// | -------------------------------------------------------- |
105 | /// | 0 | ✅ | ❌ | The metadata account |
106 | /// | 1 | ❌ | ❌ | Master edition account |
107 | /// | 2 | ❌ | ❌ | Collection |
108 | /// | 3 | ❌ | ❌ | Mint of the collection |
109 | /// | 4 | ✅ | ❌ | The central state account |
110 | /// | 5 | ✅ | ✅ | The fee payer account |
111 | /// | 6 | ❌ | ❌ | The metadata program account |
112 | /// | 7 | ❌ | ❌ | The system program account |
113 | /// | 8 | ❌ | ❌ | Rent sysvar account |
114 | /// | 9 | ❌ | ✅ | The metadata signer |
115 | UnverifyNft,
116 | }
117 | #[allow(missing_docs)]
118 | pub fn create_mint(
119 | accounts: create_mint::Accounts,
120 | params: create_mint::Params,
121 | ) -> Instruction {
122 | accounts.get_instruction(crate::ID, ProgramInstruction::CreateMint as u8, params)
123 | }
124 | #[allow(missing_docs)]
125 | pub fn create_nft(
126 | accounts: create_nft::Accounts,
127 | params: create_nft::Params,
128 | ) -> Instruction {
129 | accounts.get_instruction(crate::ID, ProgramInstruction::CreateNft as u8, params)
130 | }
131 | #[allow(missing_docs)]
132 | pub fn redeem_nft(
133 | accounts: redeem_nft::Accounts,
134 | params: redeem_nft::Params,
135 | ) -> Instruction {
136 | accounts.get_instruction(crate::ID, ProgramInstruction::RedeemNft as u8, params)
137 | }
138 | #[allow(missing_docs)]
139 | pub fn withdraw_tokens(
140 | accounts: withdraw_tokens::Accounts,
141 | params: withdraw_tokens::Params,
142 | ) -> Instruction {
143 | accounts.get_instruction(crate::ID, ProgramInstruction::WithdrawTokens as u8, params)
144 | }
145 | #[allow(missing_docs)]
146 | pub fn create_collection(
147 | accounts: create_collection::Accounts,
148 | params: create_collection::Params,
149 | ) -> Instruction {
150 | accounts.get_instruction(
151 | crate::ID,
152 | ProgramInstruction::CreateCollection as u8,
153 | params,
154 | )
155 | }
156 | #[allow(missing_docs)]
157 | pub fn edit_data(accounts: edit_data::Accounts, params: edit_data::Params) -> Instruction {
158 | accounts.get_instruction(crate::ID, ProgramInstruction::EditData as u8, params)
159 | }
160 |
161 | #[allow(missing_docs)]
162 | pub fn unverify_nft(
163 | accounts: unverify_nft::Accounts,
164 | params: unverify_nft::Params,
165 | ) -> Instruction {
166 | accounts.get_instruction(crate::ID, ProgramInstruction::UnverifyNft as u8, params)
167 | }
168 |
--------------------------------------------------------------------------------
/program/src/lib.rs:
--------------------------------------------------------------------------------
1 | use bonfida_utils::declare_id_with_central_state;
2 |
3 | #[doc(hidden)]
4 | pub mod entrypoint;
5 | #[doc(hidden)]
6 | pub mod error;
7 | /// Program instructions and their CPI-compatible bindings
8 | pub mod instruction;
9 | /// Describes the different data structures that the program uses to encode state
10 | pub mod state;
11 |
12 | #[doc(hidden)]
13 | pub(crate) mod processor;
14 | pub(crate) mod utils;
15 |
16 | #[allow(missing_docs)]
17 | pub mod cpi;
18 |
19 | declare_id_with_central_state!("nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk");
20 |
21 | #[cfg(not(feature = "no-entrypoint"))]
22 | solana_security_txt::security_txt! {
23 | name: env!("CARGO_PKG_NAME"),
24 | project_url: "http://bonfida.org",
25 | contacts: "email:security@bonfida.com,link:https://twitter.com/bonfida",
26 | policy: "https://immunefi.com/bounty/bonfida",
27 | preferred_languages: "en",
28 | auditors: "Halborn"
29 | }
30 |
--------------------------------------------------------------------------------
/program/src/processor.rs:
--------------------------------------------------------------------------------
1 | use {
2 | borsh::BorshDeserialize,
3 | num_traits::FromPrimitive,
4 | solana_program::{
5 | account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
6 | pubkey::Pubkey,
7 | },
8 | };
9 |
10 | use crate::instruction::ProgramInstruction;
11 |
12 | pub mod create_collection;
13 | pub mod create_mint;
14 | pub mod create_nft;
15 | pub mod edit_data;
16 | pub mod redeem_nft;
17 | pub mod unverify_nft;
18 | pub mod withdraw_tokens;
19 |
20 | pub struct Processor {}
21 |
22 | impl Processor {
23 | pub fn process_instruction(
24 | program_id: &Pubkey,
25 | accounts: &[AccountInfo],
26 | instruction_data: &[u8],
27 | ) -> ProgramResult {
28 | msg!("Beginning processing");
29 | let instruction = FromPrimitive::from_u8(instruction_data[0])
30 | .ok_or(ProgramError::InvalidInstructionData)?;
31 | let instruction_data = &instruction_data[1..];
32 | msg!("Instruction unpacked");
33 |
34 | match instruction {
35 | ProgramInstruction::CreateMint => {
36 | msg!("Instruction: Create mint");
37 | create_mint::process(program_id, accounts)?;
38 | }
39 | ProgramInstruction::CreateCollection => {
40 | msg!("Instruction: Create collection");
41 | create_collection::process(program_id, accounts)?;
42 | }
43 | ProgramInstruction::CreateNft => {
44 | msg!("Instruction: Create NFT");
45 | let params = create_nft::Params::try_from_slice(instruction_data)
46 | .map_err(|_| ProgramError::InvalidInstructionData)?;
47 | create_nft::process(program_id, accounts, params)?;
48 | }
49 | ProgramInstruction::RedeemNft => {
50 | msg!("Instruction: Redeem NFT");
51 | redeem_nft::process(program_id, accounts)?;
52 | }
53 | ProgramInstruction::WithdrawTokens => {
54 | msg!("Instruction: Withdraw tokens");
55 | withdraw_tokens::process(program_id, accounts)?
56 | }
57 | ProgramInstruction::EditData => {
58 | msg!("Instruction: Edit data");
59 | let params = edit_data::Params::try_from_slice(instruction_data)
60 | .map_err(|_| ProgramError::InvalidInstructionData)?;
61 | edit_data::process(program_id, accounts, params)?
62 | }
63 | ProgramInstruction::UnverifyNft => {
64 | msg!("Instruction: Unverify NFT");
65 | let params = unverify_nft::Params::try_from_slice(instruction_data)
66 | .map_err(|_| ProgramError::InvalidInstructionData)?;
67 | unverify_nft::process(program_id, accounts, params)?
68 | }
69 | }
70 |
71 | Ok(())
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/program/src/processor/create_collection.rs:
--------------------------------------------------------------------------------
1 | //! Create a verified collection
2 |
3 | use mpl_token_metadata::{
4 | accounts::{MasterEdition, Metadata},
5 | instructions::{
6 | CreateMasterEditionV3Cpi, CreateMasterEditionV3CpiAccounts,
7 | CreateMasterEditionV3InstructionArgs, CreateMetadataAccountV3Cpi,
8 | CreateMetadataAccountV3CpiAccounts, CreateMetadataAccountV3InstructionArgs,
9 | },
10 | types::DataV2,
11 | };
12 |
13 | use crate::{
14 | cpi::Cpi,
15 | state::{COLLECTION_NAME, COLLECTION_PREFIX, COLLECTION_URI, META_SYMBOL},
16 | };
17 |
18 | use {
19 | bonfida_utils::{
20 | checks::{check_account_key, check_account_owner, check_signer},
21 | BorshSize, InstructionsAccount,
22 | },
23 | borsh::{BorshDeserialize, BorshSerialize},
24 | mpl_token_metadata::types::Creator,
25 | solana_program::{
26 | account_info::{next_account_info, AccountInfo},
27 | entrypoint::ProgramResult,
28 | msg,
29 | program::{invoke, invoke_signed},
30 | program_error::ProgramError,
31 | program_pack::Pack,
32 | pubkey::Pubkey,
33 | system_program, sysvar,
34 | },
35 | spl_associated_token_account::instruction::create_associated_token_account,
36 | spl_token::{
37 | instruction::{initialize_mint, mint_to},
38 | state::Mint,
39 | },
40 | };
41 |
42 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
43 | pub struct Params {}
44 |
45 | #[derive(InstructionsAccount)]
46 | pub struct Accounts<'a, T> {
47 | /// The mint of the collection
48 | #[cons(writable)]
49 | pub collection_mint: &'a T,
50 |
51 | #[cons(writable)]
52 | pub edition: &'a T,
53 |
54 | /// The metadata account
55 | #[cons(writable)]
56 | pub metadata_account: &'a T,
57 |
58 | /// The central state account
59 | pub central_state: &'a T,
60 |
61 | #[cons(writable)]
62 | /// Token account of the central state to hold the master edition
63 | pub central_state_nft_ata: &'a T,
64 |
65 | /// The fee payer account
66 | pub fee_payer: &'a T,
67 |
68 | /// The SPL token program account
69 | pub spl_token_program: &'a T,
70 |
71 | /// The metadata program account
72 | pub metadata_program: &'a T,
73 |
74 | /// The system program account
75 | pub system_program: &'a T,
76 |
77 | /// The SPL name service program account
78 | pub spl_name_service_program: &'a T,
79 |
80 | pub ata_program: &'a T,
81 |
82 | /// Rent sysvar account
83 | pub rent_account: &'a T,
84 | }
85 |
86 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
87 | pub fn parse(
88 | accounts: &'a [AccountInfo<'b>],
89 | _program_id: &Pubkey,
90 | ) -> Result {
91 | let accounts_iter = &mut accounts.iter();
92 | let accounts = Accounts {
93 | collection_mint: next_account_info(accounts_iter)?,
94 | edition: next_account_info(accounts_iter)?,
95 | metadata_account: next_account_info(accounts_iter)?,
96 | central_state: next_account_info(accounts_iter)?,
97 | central_state_nft_ata: next_account_info(accounts_iter)?,
98 | fee_payer: next_account_info(accounts_iter)?,
99 | spl_token_program: next_account_info(accounts_iter)?,
100 | metadata_program: next_account_info(accounts_iter)?,
101 | system_program: next_account_info(accounts_iter)?,
102 | spl_name_service_program: next_account_info(accounts_iter)?,
103 | ata_program: next_account_info(accounts_iter)?,
104 | rent_account: next_account_info(accounts_iter)?,
105 | };
106 |
107 | // Check keys
108 | check_account_key(accounts.central_state, &crate::central_state::KEY)?;
109 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
110 | check_account_key(accounts.metadata_program, &mpl_token_metadata::ID)?;
111 | check_account_key(accounts.system_program, &system_program::ID)?;
112 | check_account_key(accounts.spl_name_service_program, &spl_name_service::ID)?;
113 | check_account_key(accounts.ata_program, &spl_associated_token_account::ID)?;
114 | check_account_key(accounts.rent_account, &sysvar::rent::ID)?;
115 |
116 | // Check owners
117 | check_account_owner(accounts.collection_mint, &system_program::ID)?;
118 | check_account_owner(accounts.edition, &system_program::ID)?;
119 | check_account_owner(accounts.metadata_account, &system_program::ID)?;
120 | check_account_owner(accounts.central_state_nft_ata, &system_program::ID)?;
121 |
122 | // Check signer
123 | check_signer(accounts.fee_payer)?;
124 |
125 | Ok(accounts)
126 | }
127 | }
128 |
129 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
130 | let accounts = Accounts::parse(accounts, program_id)?;
131 |
132 | let (collection_mint, collection_mint_nonce) =
133 | Pubkey::find_program_address(&[COLLECTION_PREFIX, &program_id.to_bytes()], program_id);
134 | check_account_key(accounts.collection_mint, &collection_mint)?;
135 |
136 | let (metadata_key, _) = Metadata::find_pda(&collection_mint);
137 | check_account_key(accounts.metadata_account, &metadata_key)?;
138 |
139 | let (edition_key, _) = MasterEdition::find_pda(&collection_mint);
140 | check_account_key(accounts.edition, &edition_key)?;
141 |
142 | // Create mint account
143 | msg!("+ Creating mint");
144 | let seeds: &[&[u8]] = &[
145 | COLLECTION_PREFIX,
146 | &program_id.to_bytes(),
147 | &[collection_mint_nonce],
148 | ];
149 | Cpi::create_account(
150 | &spl_token::ID,
151 | accounts.system_program,
152 | accounts.fee_payer,
153 | &accounts.collection_mint.clone(),
154 | seeds,
155 | Mint::LEN,
156 | )?;
157 | msg!("+ Initialize mint");
158 | // Initialize mint
159 | let ix = initialize_mint(
160 | &spl_token::ID,
161 | &collection_mint,
162 | &crate::central_state::KEY,
163 | Some(&crate::central_state::KEY),
164 | 0,
165 | )?;
166 | invoke_signed(
167 | &ix,
168 | &[
169 | accounts.spl_token_program.clone(),
170 | accounts.collection_mint.clone(),
171 | accounts.rent_account.clone(),
172 | ],
173 | &[seeds],
174 | )?;
175 |
176 | // Create central state ATA
177 | msg!("+ Creating central state ATA");
178 | let ix = create_associated_token_account(
179 | accounts.fee_payer.key,
180 | &crate::central_state::KEY,
181 | &collection_mint,
182 | &spl_token::ID,
183 | );
184 | invoke(
185 | &ix,
186 | &[
187 | accounts.ata_program.clone(),
188 | accounts.fee_payer.clone(),
189 | accounts.central_state_nft_ata.clone(),
190 | accounts.central_state.clone(),
191 | accounts.collection_mint.clone(),
192 | accounts.system_program.clone(),
193 | accounts.spl_token_program.clone(),
194 | accounts.rent_account.clone(),
195 | ],
196 | )?;
197 |
198 | // Mint NFT
199 | // (because the master edition ix requires mint supply === 1)
200 | msg!("+ Minting NFT");
201 | let seeds: &[&[u8]] = &[&program_id.to_bytes(), &[crate::central_state::NONCE]];
202 | let ix = mint_to(
203 | &spl_token::ID,
204 | &collection_mint,
205 | accounts.central_state_nft_ata.key,
206 | &crate::central_state::KEY,
207 | &[],
208 | 1,
209 | )?;
210 |
211 | invoke_signed(
212 | &ix,
213 | &[
214 | accounts.spl_token_program.clone(),
215 | accounts.collection_mint.clone(),
216 | accounts.central_state_nft_ata.clone(),
217 | accounts.central_state.clone(),
218 | ],
219 | &[seeds],
220 | )?;
221 |
222 | // Create collection
223 | msg!("+ Creating collection");
224 | let central_creator = Creator {
225 | address: crate::central_state::KEY,
226 | verified: true,
227 | share: 100,
228 | };
229 | CreateMetadataAccountV3Cpi::new(
230 | accounts.metadata_program,
231 | CreateMetadataAccountV3CpiAccounts {
232 | metadata: accounts.metadata_account,
233 | mint: accounts.collection_mint,
234 | mint_authority: accounts.central_state,
235 | payer: accounts.fee_payer,
236 | update_authority: (accounts.central_state, true),
237 | system_program: accounts.system_program,
238 | rent: Some(accounts.rent_account),
239 | },
240 | CreateMetadataAccountV3InstructionArgs {
241 | data: DataV2 {
242 | name: COLLECTION_NAME.to_string(),
243 | uri: COLLECTION_URI.to_string(),
244 | symbol: META_SYMBOL.to_string(),
245 | seller_fee_basis_points: 0,
246 | creators: Some(vec![central_creator]),
247 | uses: None,
248 | collection: None,
249 | },
250 | is_mutable: true,
251 | collection_details: None,
252 | },
253 | )
254 | .invoke_signed(&[seeds])?;
255 |
256 | // Create master edition
257 | msg!("+ Creating master edition");
258 | CreateMasterEditionV3Cpi::new(
259 | accounts.metadata_program,
260 | CreateMasterEditionV3CpiAccounts {
261 | edition: accounts.edition,
262 | mint: accounts.collection_mint,
263 | update_authority: accounts.central_state,
264 | token_program: accounts.spl_token_program,
265 | system_program: accounts.system_program,
266 | rent: Some(accounts.rent_account),
267 | mint_authority: accounts.central_state,
268 | metadata: accounts.metadata_account,
269 | payer: accounts.fee_payer,
270 | },
271 | CreateMasterEditionV3InstructionArgs {
272 | max_supply: Some(0),
273 | },
274 | )
275 | .invoke_signed(&[seeds])?;
276 |
277 | Ok(())
278 | }
279 |
--------------------------------------------------------------------------------
/program/src/processor/create_mint.rs:
--------------------------------------------------------------------------------
1 | //! Create the NFT mint
2 |
3 | use crate::{cpi::Cpi, state::MINT_PREFIX};
4 |
5 | use {
6 | bonfida_utils::{
7 | checks::{check_account_key, check_account_owner, check_signer},
8 | BorshSize, InstructionsAccount,
9 | },
10 | borsh::{BorshDeserialize, BorshSerialize},
11 | solana_program::{
12 | account_info::{next_account_info, AccountInfo},
13 | entrypoint::ProgramResult,
14 | msg,
15 | program::invoke_signed,
16 | program_error::ProgramError,
17 | program_pack::Pack,
18 | pubkey::Pubkey,
19 | system_program, sysvar,
20 | },
21 | spl_token::{instruction::initialize_mint, state::Mint},
22 | };
23 |
24 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
25 | pub struct Params {}
26 |
27 | #[derive(InstructionsAccount)]
28 | pub struct Accounts<'a, T> {
29 | /// The mint of the NFT
30 | #[cons(writable)]
31 | pub mint: &'a T,
32 |
33 | /// The domain name account
34 | #[cons(writable)]
35 | pub name_account: &'a T,
36 |
37 | /// The central state account
38 | pub central_state: &'a T,
39 |
40 | /// The SPL token program account
41 | pub spl_token_program: &'a T,
42 |
43 | /// The system program account
44 | pub system_program: &'a T,
45 |
46 | /// Rent sysvar account
47 | pub rent_account: &'a T,
48 |
49 | /// Fee payer account
50 | pub fee_payer: &'a T,
51 | }
52 |
53 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
54 | pub fn parse(
55 | accounts: &'a [AccountInfo<'b>],
56 | _program_id: &Pubkey,
57 | ) -> Result {
58 | let accounts_iter = &mut accounts.iter();
59 | let accounts = Accounts {
60 | mint: next_account_info(accounts_iter)?,
61 | name_account: next_account_info(accounts_iter)?,
62 | central_state: next_account_info(accounts_iter)?,
63 | spl_token_program: next_account_info(accounts_iter)?,
64 | system_program: next_account_info(accounts_iter)?,
65 | rent_account: next_account_info(accounts_iter)?,
66 | fee_payer: next_account_info(accounts_iter)?,
67 | };
68 |
69 | // Check keys
70 | check_account_key(accounts.central_state, &crate::central_state::KEY)?;
71 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
72 | check_account_key(accounts.system_program, &system_program::ID)?;
73 | check_account_key(accounts.rent_account, &sysvar::rent::ID)?;
74 |
75 | // Check owners
76 | check_account_owner(accounts.mint, &system_program::ID)?;
77 | check_account_owner(accounts.name_account, &spl_name_service::ID)?;
78 |
79 | // Check signer
80 | check_signer(accounts.fee_payer)?;
81 |
82 | Ok(accounts)
83 | }
84 | }
85 |
86 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
87 | let accounts = Accounts::parse(accounts, program_id)?;
88 |
89 | let (mint, mint_nonce) = Pubkey::find_program_address(
90 | &[MINT_PREFIX, &accounts.name_account.key.to_bytes()],
91 | program_id,
92 | );
93 | check_account_key(accounts.mint, &mint)?;
94 |
95 | msg!("+ Creating mint");
96 |
97 | // Create mint account
98 | let seeds: &[&[u8]] = &[
99 | MINT_PREFIX,
100 | &accounts.name_account.key.to_bytes(),
101 | &[mint_nonce],
102 | ];
103 | Cpi::create_account(
104 | &spl_token::ID,
105 | accounts.system_program,
106 | accounts.fee_payer,
107 | accounts.mint,
108 | seeds,
109 | Mint::LEN,
110 | )?;
111 |
112 | // Initialize mint
113 | let ix = initialize_mint(
114 | &spl_token::ID,
115 | &mint,
116 | &crate::central_state::KEY,
117 | Some(&crate::central_state::KEY),
118 | 0,
119 | )?;
120 | invoke_signed(
121 | &ix,
122 | &[
123 | accounts.spl_token_program.clone(),
124 | accounts.mint.clone(),
125 | accounts.rent_account.clone(),
126 | ],
127 | &[seeds],
128 | )?;
129 |
130 | Ok(())
131 | }
132 |
--------------------------------------------------------------------------------
/program/src/processor/create_nft.rs:
--------------------------------------------------------------------------------
1 | //! Tokenize a domain name
2 |
3 | use mpl_token_metadata::{
4 | accounts::{MasterEdition, Metadata},
5 | instructions::{
6 | CreateMetadataAccountV3Cpi, CreateMetadataAccountV3CpiAccounts,
7 | CreateMetadataAccountV3InstructionArgs, SetAndVerifyCollectionCpi,
8 | SetAndVerifyCollectionCpiAccounts, UnverifyCollectionCpi, UnverifyCollectionCpiAccounts,
9 | UpdateMetadataAccountV2Cpi, UpdateMetadataAccountV2CpiAccounts,
10 | UpdateMetadataAccountV2InstructionArgs,
11 | },
12 | };
13 |
14 | use crate::{
15 | cpi::Cpi,
16 | state::{
17 | NftRecord, Tag, COLLECTION_PREFIX, CREATOR_FEE, METADATA_SIGNER, META_SYMBOL, MINT_PREFIX,
18 | SELLER_BASIS,
19 | },
20 | utils::check_name,
21 | };
22 |
23 | use {
24 | bonfida_utils::{
25 | checks::{check_account_key, check_account_owner, check_signer},
26 | BorshSize, InstructionsAccount,
27 | },
28 | borsh::{BorshDeserialize, BorshSerialize},
29 | mpl_token_metadata::types::{Creator, DataV2},
30 | solana_program::{
31 | account_info::{next_account_info, AccountInfo},
32 | entrypoint::ProgramResult,
33 | msg,
34 | program::{invoke, invoke_signed},
35 | program_error::ProgramError,
36 | program_pack::Pack,
37 | pubkey::Pubkey,
38 | system_program, sysvar,
39 | },
40 | spl_name_service::instruction::transfer,
41 | spl_token::{instruction::mint_to, state::Mint},
42 | };
43 |
44 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
45 | pub struct Params {
46 | /// The domain name (without .sol)
47 | pub name: String,
48 |
49 | /// The URI of the metadata
50 | pub uri: String,
51 | }
52 |
53 | #[derive(InstructionsAccount)]
54 | pub struct Accounts<'a, T> {
55 | /// The mint of the NFT
56 | #[cons(writable)]
57 | pub mint: &'a T,
58 |
59 | /// The NFT token destination
60 | #[cons(writable)]
61 | pub nft_destination: &'a T,
62 |
63 | /// The domain name account
64 | #[cons(writable)]
65 | pub name_account: &'a T,
66 |
67 | /// The NFT record account
68 | #[cons(writable)]
69 | pub nft_record: &'a T,
70 |
71 | /// The domain name owner
72 | #[cons(writable, signer)]
73 | pub name_owner: &'a T,
74 |
75 | /// The metadata account
76 | #[cons(writable)]
77 | pub metadata_account: &'a T,
78 |
79 | /// Master edition account
80 | pub edition_account: &'a T,
81 |
82 | /// Collection
83 | pub collection_metadata: &'a T,
84 |
85 | /// Mint of the collection
86 | pub collection_mint: &'a T,
87 |
88 | /// The central state account
89 | #[cons(writable)]
90 | pub central_state: &'a T,
91 |
92 | /// The fee payer account
93 | #[cons(writable, signer)]
94 | pub fee_payer: &'a T,
95 |
96 | /// The SPL token program account
97 | pub spl_token_program: &'a T,
98 |
99 | /// The metadata program account
100 | pub metadata_program: &'a T,
101 |
102 | /// The system program account
103 | pub system_program: &'a T,
104 |
105 | /// The SPL name service program account
106 | pub spl_name_service_program: &'a T,
107 |
108 | /// Rent sysvar account
109 | pub rent_account: &'a T,
110 |
111 | /// The metadata signer
112 | #[cons(signer)]
113 | #[cfg(not(feature = "devnet"))]
114 | pub metadata_signer: &'a T,
115 | }
116 |
117 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
118 | pub fn parse(
119 | accounts: &'a [AccountInfo<'b>],
120 | program_id: &Pubkey,
121 | ) -> Result {
122 | let accounts_iter = &mut accounts.iter();
123 | let accounts = Accounts {
124 | mint: next_account_info(accounts_iter)?,
125 | nft_destination: next_account_info(accounts_iter)?,
126 | name_account: next_account_info(accounts_iter)?,
127 | nft_record: next_account_info(accounts_iter)?,
128 | name_owner: next_account_info(accounts_iter)?,
129 | metadata_account: next_account_info(accounts_iter)?,
130 | edition_account: next_account_info(accounts_iter)?,
131 | collection_metadata: next_account_info(accounts_iter)?,
132 | collection_mint: next_account_info(accounts_iter)?,
133 | central_state: next_account_info(accounts_iter)?,
134 | fee_payer: next_account_info(accounts_iter)?,
135 | spl_token_program: next_account_info(accounts_iter)?,
136 | metadata_program: next_account_info(accounts_iter)?,
137 | system_program: next_account_info(accounts_iter)?,
138 | spl_name_service_program: next_account_info(accounts_iter)?,
139 | rent_account: next_account_info(accounts_iter)?,
140 | #[cfg(not(feature = "devnet"))]
141 | metadata_signer: next_account_info(accounts_iter)?,
142 | };
143 |
144 | // Check keys
145 | check_account_key(accounts.central_state, &crate::central_state::KEY)?;
146 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
147 | check_account_key(accounts.metadata_program, &mpl_token_metadata::ID)?;
148 | check_account_key(accounts.system_program, &system_program::ID)?;
149 | check_account_key(accounts.spl_name_service_program, &spl_name_service::ID)?;
150 | check_account_key(accounts.rent_account, &sysvar::rent::ID)?;
151 | #[cfg(not(feature = "devnet"))]
152 | check_account_key(accounts.metadata_signer, &METADATA_SIGNER)?;
153 |
154 | // Check owners
155 | check_account_owner(accounts.mint, &spl_token::ID)?;
156 | check_account_owner(accounts.nft_destination, &spl_token::ID)?;
157 | check_account_owner(accounts.name_account, &spl_name_service::ID)?;
158 | check_account_owner(accounts.nft_record, &system_program::ID)
159 | .or_else(|_| check_account_owner(accounts.nft_record, program_id))?;
160 | check_account_owner(accounts.metadata_account, &system_program::ID)
161 | .or_else(|_| check_account_owner(accounts.metadata_account, &mpl_token_metadata::ID))?;
162 | check_account_owner(accounts.edition_account, &mpl_token_metadata::ID)?;
163 | check_account_owner(accounts.collection_metadata, &mpl_token_metadata::ID)?;
164 | check_account_owner(accounts.collection_mint, &spl_token::ID)?;
165 |
166 | // Check signer
167 | check_signer(accounts.name_owner)?;
168 | #[cfg(not(feature = "devnet"))]
169 | check_signer(accounts.metadata_signer)?;
170 |
171 | Ok(accounts)
172 | }
173 | }
174 |
175 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
176 | let accounts = Accounts::parse(accounts, program_id)?;
177 | let Params { name, uri } = params;
178 |
179 | let (mint, _) = Pubkey::find_program_address(
180 | &[MINT_PREFIX, &accounts.name_account.key.to_bytes()],
181 | program_id,
182 | );
183 | check_account_key(accounts.mint, &mint)?;
184 |
185 | // Create NFT record
186 | let (nft_record_key, nft_record_nonce) =
187 | NftRecord::find_key(accounts.name_account.key, program_id);
188 | check_account_key(accounts.nft_record, &nft_record_key)?;
189 |
190 | // Verify name derivation
191 | check_name(&name, accounts.name_account)?;
192 |
193 | // Verify metadata PDA
194 | let (metadata_key, _) = Metadata::find_pda(&mint);
195 | check_account_key(accounts.metadata_account, &metadata_key)?;
196 |
197 | // Verify edition PDA
198 | let (collection_mint, _) =
199 | Pubkey::find_program_address(&[COLLECTION_PREFIX, &program_id.to_bytes()], program_id);
200 | check_account_key(accounts.collection_mint, &collection_mint)?;
201 |
202 | let (edition_key, _) = MasterEdition::find_pda(&collection_mint);
203 | check_account_key(accounts.edition_account, &edition_key)?;
204 |
205 | // Verify collection metadata PDA
206 | let (collection_metadata, _) = Metadata::find_pda(&collection_mint);
207 | check_account_key(accounts.collection_metadata, &collection_metadata)?;
208 |
209 | // Verify mint
210 | let mint_info = Mint::unpack(&accounts.mint.data.borrow())?;
211 | if mint_info.supply != 0 {
212 | msg!("Expected supply == 0 and received {}", mint_info.supply);
213 | return Err(ProgramError::InvalidAccountData);
214 | }
215 |
216 | if accounts.nft_record.data_is_empty() {
217 | msg!("+ Creating NFT record");
218 | let nft_record = NftRecord::new(
219 | nft_record_nonce,
220 | *accounts.name_owner.key,
221 | *accounts.name_account.key,
222 | mint,
223 | );
224 | let seeds: &[&[u8]] = &[
225 | NftRecord::SEED,
226 | &accounts.name_account.key.to_bytes(),
227 | &[nft_record_nonce],
228 | ];
229 | Cpi::create_account(
230 | program_id,
231 | accounts.system_program,
232 | accounts.fee_payer,
233 | accounts.nft_record,
234 | seeds,
235 | nft_record.borsh_len(),
236 | )?;
237 |
238 | nft_record.save(&mut accounts.nft_record.data.borrow_mut());
239 | } else {
240 | msg!("+ NFT record already exists");
241 | let mut nft_record =
242 | NftRecord::from_account_info(accounts.nft_record, Tag::InactiveRecord)?;
243 |
244 | check_account_key(accounts.mint, &nft_record.nft_mint)?;
245 |
246 | nft_record.tag = Tag::ActiveRecord;
247 | nft_record.owner = *accounts.name_owner.key;
248 |
249 | nft_record.save(&mut accounts.nft_record.data.borrow_mut());
250 | }
251 |
252 | // Mint token
253 | let ix = mint_to(
254 | &spl_token::ID,
255 | &mint,
256 | accounts.nft_destination.key,
257 | &crate::central_state::KEY,
258 | &[],
259 | 1,
260 | )?;
261 | let seeds: &[&[u8]] = &[&program_id.to_bytes(), &[crate::central_state::NONCE]];
262 |
263 | invoke_signed(
264 | &ix,
265 | &[
266 | accounts.spl_token_program.clone(),
267 | accounts.mint.clone(),
268 | accounts.nft_destination.clone(),
269 | accounts.central_state.clone(),
270 | ],
271 | &[seeds],
272 | )?;
273 |
274 | // Create metadata
275 | let central_creator = Creator {
276 | address: crate::central_state::KEY,
277 | verified: true,
278 | share: 0,
279 | };
280 | if accounts.metadata_account.data_is_empty() {
281 | msg!("+ Creating metadata");
282 | CreateMetadataAccountV3Cpi::new(
283 | accounts.metadata_program,
284 | CreateMetadataAccountV3CpiAccounts {
285 | metadata: accounts.metadata_account,
286 | mint: accounts.mint,
287 | mint_authority: accounts.central_state,
288 | payer: accounts.fee_payer,
289 | update_authority: (accounts.central_state, true),
290 | system_program: accounts.system_program,
291 | rent: Some(accounts.rent_account),
292 | },
293 | CreateMetadataAccountV3InstructionArgs {
294 | data: DataV2 {
295 | name,
296 | symbol: META_SYMBOL.to_string(),
297 | uri,
298 | seller_fee_basis_points: SELLER_BASIS,
299 | creators: Some(vec![central_creator, CREATOR_FEE]),
300 | collection: None,
301 | uses: None,
302 | },
303 | is_mutable: true,
304 | collection_details: None,
305 | },
306 | )
307 | .invoke_signed(&[seeds])?;
308 | } else {
309 | msg!("+ Metadata already exists");
310 | // Unverify collection first
311 | UnverifyCollectionCpi::new(
312 | accounts.metadata_program,
313 | UnverifyCollectionCpiAccounts {
314 | metadata: accounts.metadata_account,
315 | collection_authority: accounts.central_state,
316 | collection_mint: accounts.collection_mint,
317 | collection: accounts.collection_metadata,
318 | collection_master_edition_account: accounts.edition_account,
319 | collection_authority_record: None,
320 | },
321 | )
322 | .invoke_signed(&[seeds])?;
323 |
324 | let data = DataV2 {
325 | name,
326 | symbol: META_SYMBOL.to_string(),
327 | uri,
328 | seller_fee_basis_points: SELLER_BASIS,
329 | creators: Some(vec![central_creator, CREATOR_FEE]),
330 | collection: None,
331 | uses: None,
332 | };
333 | UpdateMetadataAccountV2Cpi::new(
334 | accounts.metadata_program,
335 | UpdateMetadataAccountV2CpiAccounts {
336 | metadata: accounts.metadata_account,
337 | update_authority: accounts.central_state,
338 | },
339 | UpdateMetadataAccountV2InstructionArgs {
340 | data: Some(data),
341 | new_update_authority: Some(crate::central_state::KEY),
342 | primary_sale_happened: None,
343 | is_mutable: None,
344 | },
345 | )
346 | .invoke_signed(&[seeds])?;
347 | }
348 |
349 | msg!("+ Verifying collection");
350 | SetAndVerifyCollectionCpi::new(
351 | accounts.metadata_program,
352 | SetAndVerifyCollectionCpiAccounts {
353 | metadata: accounts.metadata_account,
354 | update_authority: accounts.central_state,
355 | collection_authority: accounts.central_state,
356 | payer: accounts.fee_payer,
357 | collection_mint: accounts.collection_mint,
358 | collection: accounts.collection_metadata,
359 | collection_master_edition_account: accounts.edition_account,
360 | collection_authority_record: None,
361 | },
362 | )
363 | .invoke_signed(&[seeds])?;
364 |
365 | // Transfer domain
366 | let ix = transfer(
367 | spl_name_service::ID,
368 | nft_record_key,
369 | *accounts.name_account.key,
370 | *accounts.name_owner.key,
371 | None,
372 | )?;
373 | invoke(
374 | &ix,
375 | &[
376 | accounts.spl_name_service_program.clone(),
377 | accounts.nft_record.clone(),
378 | accounts.name_account.clone(),
379 | accounts.name_owner.clone(),
380 | ],
381 | )?;
382 |
383 | Ok(())
384 | }
385 |
--------------------------------------------------------------------------------
/program/src/processor/edit_data.rs:
--------------------------------------------------------------------------------
1 | //! Edit the data registry of a tokenized domain name
2 |
3 | use {
4 | bonfida_utils::{
5 | checks::{check_account_key, check_account_owner, check_signer},
6 | BorshSize, InstructionsAccount,
7 | },
8 | borsh::{BorshDeserialize, BorshSerialize},
9 | solana_program::{
10 | account_info::{next_account_info, AccountInfo},
11 | entrypoint::ProgramResult,
12 | program::invoke_signed,
13 | program_error::ProgramError,
14 | pubkey::Pubkey,
15 | },
16 | };
17 |
18 | use solana_program::{msg, program_pack::Pack};
19 | use spl_name_service::instruction::update;
20 | use spl_token::state::Account;
21 |
22 | use crate::state::{NftRecord, Tag};
23 |
24 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
25 | pub struct Params {
26 | /// Offset at which the data should be written into the domain name registry
27 | pub offset: u32,
28 | /// The data to be written into the registry (overwrites any previous data)
29 | pub data: Vec,
30 | }
31 |
32 | #[derive(InstructionsAccount, Debug)]
33 | pub struct Accounts<'a, T> {
34 | /// The NFT owner account
35 | #[cons(signer)]
36 | pub nft_owner: &'a T,
37 |
38 | /// The NFT account
39 | pub nft_account: &'a T,
40 |
41 | /// The NFT record account
42 | pub nft_record: &'a T,
43 |
44 | /// The domain name account
45 | #[cons(writable)]
46 | pub name_account: &'a T,
47 |
48 | /// The SPL token program account
49 | pub spl_token_program: &'a T,
50 |
51 | /// The SPL name service program account
52 | pub spl_name_service_program: &'a T,
53 | }
54 |
55 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
56 | pub fn parse(
57 | accounts: &'a [AccountInfo<'b>],
58 | program_id: &Pubkey,
59 | ) -> Result {
60 | let accounts_iter = &mut accounts.iter();
61 | let accounts = Accounts {
62 | nft_owner: next_account_info(accounts_iter)?,
63 | nft_account: next_account_info(accounts_iter)?,
64 | nft_record: next_account_info(accounts_iter)?,
65 | name_account: next_account_info(accounts_iter)?,
66 | spl_token_program: next_account_info(accounts_iter)?,
67 | spl_name_service_program: next_account_info(accounts_iter)?,
68 | };
69 |
70 | // Check keys
71 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
72 | check_account_key(accounts.spl_name_service_program, &spl_name_service::ID)?;
73 |
74 | // Check owners
75 | check_account_owner(accounts.nft_account, &spl_token::ID)?;
76 | check_account_owner(accounts.nft_record, program_id)?;
77 | check_account_owner(accounts.name_account, &spl_name_service::ID)?;
78 |
79 | // Check signer
80 | check_signer(accounts.nft_owner)?;
81 |
82 | Ok(accounts)
83 | }
84 | }
85 |
86 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], params: Params) -> ProgramResult {
87 | let accounts = Accounts::parse(accounts, program_id)?;
88 |
89 | let (nft_record_key, _) = NftRecord::find_key(accounts.name_account.key, program_id);
90 | check_account_key(accounts.nft_record, &nft_record_key)?;
91 |
92 | let nft_record = NftRecord::from_account_info(accounts.nft_record, Tag::ActiveRecord)?;
93 | let nft = Account::unpack(&accounts.nft_account.data.borrow())?;
94 |
95 | if nft.mint != nft_record.nft_mint {
96 | msg!("+ NFT mint mismatch");
97 | return Err(ProgramError::InvalidArgument);
98 | }
99 | if nft.amount != 1 {
100 | msg!("+ Invalid NFT amount, received {}", nft.amount);
101 | return Err(ProgramError::InvalidArgument);
102 | }
103 | check_account_key(accounts.nft_owner, &nft.owner)?;
104 |
105 | let ix = update(
106 | spl_name_service::ID,
107 | params.offset,
108 | params.data,
109 | *accounts.name_account.key,
110 | *accounts.nft_record.key,
111 | None,
112 | )?;
113 | let seeds: &[&[u8]] = &[
114 | NftRecord::SEED,
115 | &accounts.name_account.key.to_bytes(),
116 | &[nft_record.nonce],
117 | ];
118 | invoke_signed(
119 | &ix,
120 | &[
121 | accounts.spl_name_service_program.clone(),
122 | accounts.nft_record.clone(),
123 | accounts.name_account.clone(),
124 | ],
125 | &[seeds],
126 | )?;
127 |
128 | Ok(())
129 | }
130 |
--------------------------------------------------------------------------------
/program/src/processor/redeem_nft.rs:
--------------------------------------------------------------------------------
1 | //! Redeem a tokenized domain name
2 |
3 | use {
4 | bonfida_utils::{
5 | checks::{check_account_key, check_account_owner, check_signer},
6 | BorshSize, InstructionsAccount,
7 | },
8 | borsh::{BorshDeserialize, BorshSerialize},
9 | solana_program::{
10 | account_info::{next_account_info, AccountInfo},
11 | entrypoint::ProgramResult,
12 | program::{invoke, invoke_signed},
13 | program_error::ProgramError,
14 | pubkey::Pubkey,
15 | },
16 | spl_name_service::instruction::transfer,
17 | spl_token::instruction::burn,
18 | };
19 |
20 | use crate::state::{NftRecord, Tag, MINT_PREFIX};
21 |
22 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
23 | pub struct Params {}
24 |
25 | #[derive(InstructionsAccount)]
26 | pub struct Accounts<'a, T> {
27 | /// The mint of the NFT
28 | #[cons(writable)]
29 | pub mint: &'a T,
30 |
31 | /// The current token account holding the NFT
32 | #[cons(writable)]
33 | pub nft_source: &'a T,
34 |
35 | /// The NFT owner account
36 | #[cons(writable, signer)]
37 | pub nft_owner: &'a T,
38 |
39 | /// The NFT record account
40 | #[cons(writable)]
41 | pub nft_record: &'a T,
42 |
43 | /// The domain name account
44 | #[cons(writable)]
45 | pub name_account: &'a T,
46 |
47 | /// The SPL token program account
48 | pub spl_token_program: &'a T,
49 |
50 | /// The SPL name service program account
51 | pub spl_name_service_program: &'a T,
52 | }
53 |
54 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
55 | pub fn parse(
56 | accounts: &'a [AccountInfo<'b>],
57 | program_id: &Pubkey,
58 | ) -> Result {
59 | let accounts_iter = &mut accounts.iter();
60 | let accounts = Accounts {
61 | mint: next_account_info(accounts_iter)?,
62 | nft_source: next_account_info(accounts_iter)?,
63 | nft_owner: next_account_info(accounts_iter)?,
64 | nft_record: next_account_info(accounts_iter)?,
65 | name_account: next_account_info(accounts_iter)?,
66 | spl_token_program: next_account_info(accounts_iter)?,
67 | spl_name_service_program: next_account_info(accounts_iter)?,
68 | };
69 |
70 | // Check keys
71 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
72 | check_account_key(accounts.spl_name_service_program, &spl_name_service::ID)?;
73 |
74 | // Check owners
75 | check_account_owner(accounts.mint, &spl_token::ID)?;
76 | check_account_owner(accounts.nft_source, &spl_token::ID)?;
77 | check_account_owner(accounts.nft_record, program_id)?;
78 | check_account_owner(accounts.name_account, &spl_name_service::ID)?;
79 |
80 | // Check signer
81 | check_signer(accounts.nft_owner)?;
82 |
83 | Ok(accounts)
84 | }
85 | }
86 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
87 | let accounts = Accounts::parse(accounts, program_id)?;
88 | let mut nft_record = NftRecord::from_account_info(accounts.nft_record, Tag::ActiveRecord)?;
89 |
90 | let (nft_record_key, _) = NftRecord::find_key(accounts.name_account.key, program_id);
91 | check_account_key(accounts.nft_record, &nft_record_key)?;
92 |
93 | let (mint, _) = Pubkey::find_program_address(
94 | &[MINT_PREFIX, &accounts.name_account.key.to_bytes()],
95 | program_id,
96 | );
97 | check_account_key(accounts.mint, &mint)?;
98 |
99 | // Burn NFT
100 | let ix = burn(
101 | &spl_token::ID,
102 | accounts.nft_source.key,
103 | &nft_record.nft_mint,
104 | accounts.nft_owner.key,
105 | &[],
106 | 1,
107 | )?;
108 | invoke(
109 | &ix,
110 | &[
111 | accounts.spl_token_program.clone(),
112 | accounts.nft_source.clone(),
113 | accounts.mint.clone(),
114 | accounts.nft_owner.clone(),
115 | ],
116 | )?;
117 |
118 | // Transfer domain
119 | let ix = transfer(
120 | spl_name_service::ID,
121 | *accounts.nft_owner.key,
122 | *accounts.name_account.key,
123 | *accounts.nft_record.key,
124 | None,
125 | )?;
126 | let seeds: &[&[u8]] = &[
127 | NftRecord::SEED,
128 | &accounts.name_account.key.to_bytes(),
129 | &[nft_record.nonce],
130 | ];
131 | invoke_signed(
132 | &ix,
133 | &[
134 | accounts.spl_name_service_program.clone(),
135 | accounts.nft_owner.clone(),
136 | accounts.name_account.clone(),
137 | accounts.nft_record.clone(),
138 | ],
139 | &[seeds],
140 | )?;
141 |
142 | // Update NFT record
143 | nft_record.tag = Tag::InactiveRecord;
144 | nft_record.owner = *accounts.nft_owner.key;
145 |
146 | nft_record.save(&mut accounts.nft_record.data.borrow_mut());
147 |
148 | Ok(())
149 | }
150 |
--------------------------------------------------------------------------------
/program/src/processor/unverify_nft.rs:
--------------------------------------------------------------------------------
1 | //! Unverify an NFT
2 |
3 | use mpl_token_metadata::{
4 | accounts::{MasterEdition, Metadata},
5 | instructions::{UnverifyCollectionCpi, UnverifyCollectionCpiAccounts},
6 | };
7 |
8 | use crate::state::{COLLECTION_PREFIX, METADATA_SIGNER};
9 |
10 | use {
11 | bonfida_utils::{
12 | checks::{check_account_key, check_account_owner, check_signer},
13 | BorshSize, InstructionsAccount,
14 | },
15 | borsh::{BorshDeserialize, BorshSerialize},
16 | solana_program::{
17 | account_info::{next_account_info, AccountInfo},
18 | entrypoint::ProgramResult,
19 | program_error::ProgramError,
20 | pubkey::Pubkey,
21 | system_program, sysvar,
22 | },
23 | };
24 |
25 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
26 | pub struct Params {}
27 |
28 | #[derive(InstructionsAccount)]
29 | pub struct Accounts<'a, T> {
30 | /// The metadata account
31 | #[cons(writable)]
32 | pub metadata_account: &'a T,
33 |
34 | /// Master edition account
35 | pub edition_account: &'a T,
36 |
37 | /// Collection
38 | pub collection_metadata: &'a T,
39 |
40 | /// Mint of the collection
41 | pub collection_mint: &'a T,
42 |
43 | /// The central state account
44 | #[cons(writable)]
45 | pub central_state: &'a T,
46 |
47 | /// The fee payer account
48 | #[cons(writable, signer)]
49 | pub fee_payer: &'a T,
50 |
51 | /// The metadata program account
52 | pub metadata_program: &'a T,
53 |
54 | /// The system program account
55 | pub system_program: &'a T,
56 |
57 | /// Rent sysvar account
58 | pub rent_account: &'a T,
59 |
60 | /// The metadata signer
61 | #[cons(signer)]
62 | #[cfg(not(feature = "devnet"))]
63 | pub metadata_signer: &'a T,
64 | }
65 |
66 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
67 | pub fn parse(accounts: &'a [AccountInfo<'b>]) -> Result {
68 | let accounts_iter = &mut accounts.iter();
69 | let accounts = Accounts {
70 | metadata_account: next_account_info(accounts_iter)?,
71 | edition_account: next_account_info(accounts_iter)?,
72 | collection_metadata: next_account_info(accounts_iter)?,
73 | collection_mint: next_account_info(accounts_iter)?,
74 | central_state: next_account_info(accounts_iter)?,
75 | fee_payer: next_account_info(accounts_iter)?,
76 | metadata_program: next_account_info(accounts_iter)?,
77 | system_program: next_account_info(accounts_iter)?,
78 | rent_account: next_account_info(accounts_iter)?,
79 | #[cfg(not(feature = "devnet"))]
80 | metadata_signer: next_account_info(accounts_iter)?,
81 | };
82 |
83 | // Check keys
84 | check_account_key(accounts.central_state, &crate::central_state::KEY)?;
85 | check_account_key(accounts.metadata_program, &mpl_token_metadata::ID)?;
86 | check_account_key(accounts.system_program, &system_program::ID)?;
87 | check_account_key(accounts.rent_account, &sysvar::rent::ID)?;
88 | #[cfg(not(feature = "devnet"))]
89 | check_account_key(accounts.metadata_signer, &METADATA_SIGNER)?;
90 |
91 | // Check owners
92 | check_account_owner(accounts.metadata_account, &mpl_token_metadata::ID)?;
93 | check_account_owner(accounts.edition_account, &mpl_token_metadata::ID)?;
94 | check_account_owner(accounts.collection_metadata, &mpl_token_metadata::ID)?;
95 | check_account_owner(accounts.collection_mint, &spl_token::ID)?;
96 |
97 | #[cfg(not(feature = "devnet"))]
98 | check_signer(accounts.metadata_signer)?;
99 |
100 | Ok(accounts)
101 | }
102 | }
103 |
104 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], _params: Params) -> ProgramResult {
105 | let accounts = Accounts::parse(accounts)?;
106 |
107 | // Verify edition PDA
108 | let (collection_mint, _) =
109 | Pubkey::find_program_address(&[COLLECTION_PREFIX, &program_id.to_bytes()], program_id);
110 | check_account_key(accounts.collection_mint, &collection_mint)?;
111 |
112 | let (edition_key, _) = MasterEdition::find_pda(&collection_mint);
113 | check_account_key(accounts.edition_account, &edition_key)?;
114 |
115 | // Verify collection metadata PDA
116 | let (collection_metadata, _) = Metadata::find_pda(&collection_mint);
117 | check_account_key(accounts.collection_metadata, &collection_metadata)?;
118 |
119 | let seeds: &[&[u8]] = &[&program_id.to_bytes(), &[crate::central_state::NONCE]];
120 |
121 | UnverifyCollectionCpi::new(
122 | accounts.metadata_program,
123 | UnverifyCollectionCpiAccounts {
124 | metadata: accounts.metadata_account,
125 | collection_authority: accounts.central_state,
126 | collection_mint: accounts.collection_mint,
127 | collection: accounts.collection_metadata,
128 | collection_master_edition_account: accounts.edition_account,
129 | collection_authority_record: None,
130 | },
131 | )
132 | .invoke_signed(&[seeds])?;
133 |
134 | Ok(())
135 | }
136 |
--------------------------------------------------------------------------------
/program/src/processor/withdraw_tokens.rs:
--------------------------------------------------------------------------------
1 | //! Withdraw funds that have been sent to the escrow
2 | //! while the domain was tokenized
3 | use {
4 | bonfida_utils::{
5 | checks::{check_account_key, check_account_owner, check_signer},
6 | BorshSize, InstructionsAccount,
7 | },
8 | borsh::{BorshDeserialize, BorshSerialize},
9 | solana_program::{
10 | account_info::{next_account_info, AccountInfo},
11 | entrypoint::ProgramResult,
12 | msg,
13 | program::invoke_signed,
14 | program_error::ProgramError,
15 | pubkey::Pubkey,
16 | rent::Rent,
17 | system_program,
18 | sysvar::Sysvar,
19 | },
20 | spl_token::state::Account,
21 | };
22 |
23 | use solana_program::program_pack::Pack;
24 |
25 | use crate::state::{NftRecord, Tag};
26 |
27 | #[derive(BorshDeserialize, BorshSerialize, BorshSize)]
28 | pub struct Params {}
29 |
30 | #[derive(InstructionsAccount)]
31 | pub struct Accounts<'a, T> {
32 | /// The token account holding the NFT
33 | #[cons(writable)]
34 | pub nft: &'a T,
35 |
36 | /// The owner of the NFT token account
37 | #[cons(writable, signer)]
38 | pub nft_owner: &'a T,
39 |
40 | /// The NFT record account
41 | #[cons(writable)]
42 | pub nft_record: &'a T,
43 |
44 | /// The destination for tokens being withdrawn
45 | #[cons(writable)]
46 | pub token_destination: &'a T,
47 |
48 | /// The source for tokens being withdrawn
49 | #[cons(writable)]
50 | pub token_source: &'a T,
51 |
52 | /// The SPL token program account
53 | pub spl_token_program: &'a T,
54 |
55 | /// The system program account
56 | pub system_program: &'a T,
57 | }
58 |
59 | impl<'a, 'b: 'a> Accounts<'a, AccountInfo<'b>> {
60 | pub fn parse(
61 | accounts: &'a [AccountInfo<'b>],
62 | program_id: &Pubkey,
63 | ) -> Result {
64 | let accounts_iter = &mut accounts.iter();
65 | let accounts = Accounts {
66 | nft: next_account_info(accounts_iter)?,
67 | nft_owner: next_account_info(accounts_iter)?,
68 | nft_record: next_account_info(accounts_iter)?,
69 | token_destination: next_account_info(accounts_iter)?,
70 | token_source: next_account_info(accounts_iter)?,
71 | spl_token_program: next_account_info(accounts_iter)?,
72 | system_program: next_account_info(accounts_iter)?,
73 | };
74 |
75 | // Check keys
76 | check_account_key(accounts.spl_token_program, &spl_token::ID)?;
77 | check_account_key(accounts.system_program, &system_program::ID)?;
78 |
79 | // Check owners
80 | check_account_owner(accounts.nft, &spl_token::ID)?;
81 | check_account_owner(accounts.nft_record, program_id)?;
82 | check_account_owner(accounts.token_destination, &spl_token::ID)?;
83 | check_account_owner(accounts.token_source, &spl_token::ID)?;
84 |
85 | // Check signer
86 | check_signer(accounts.nft_owner)?;
87 |
88 | Ok(accounts)
89 | }
90 | }
91 |
92 | // NFT record is active -> Correct owner is the token holder
93 | // NFT record is inactive -> Correct owner is the latest person who redeemed
94 |
95 | pub fn process(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
96 | let accounts = Accounts::parse(accounts, program_id)?;
97 |
98 | let mut nft_record = NftRecord::from_account_info(accounts.nft_record, Tag::ActiveRecord)
99 | .or_else(|_| NftRecord::from_account_info(accounts.nft_record, Tag::InactiveRecord))?;
100 |
101 | let nft = Account::unpack(&accounts.nft.data.borrow())?;
102 |
103 | if nft.mint != nft_record.nft_mint {
104 | msg!("+ NFT mint mismatch");
105 | return Err(ProgramError::InvalidArgument);
106 | }
107 |
108 | if nft_record.is_active() {
109 | check_account_key(accounts.nft_owner, &nft.owner)?;
110 | if nft.amount != 1 {
111 | msg!("+ Invalid NFT amount, received {}", nft.amount);
112 | return Err(ProgramError::InvalidArgument);
113 | }
114 | } else {
115 | check_account_key(accounts.nft_owner, &nft_record.owner)?
116 | }
117 |
118 | // Withdraw SPL token
119 | let token_account = Account::unpack(&accounts.token_source.data.borrow())?;
120 |
121 | msg!("+ Withdrawing tokens {}", token_account.amount);
122 |
123 | let ix = spl_token::instruction::transfer(
124 | &spl_token::ID,
125 | accounts.token_source.key,
126 | accounts.token_destination.key,
127 | accounts.nft_record.key,
128 | &[],
129 | token_account.amount,
130 | )?;
131 | let seeds: &[&[u8]] = &[
132 | NftRecord::SEED,
133 | &nft_record.name_account.to_bytes(),
134 | &[nft_record.nonce],
135 | ];
136 | invoke_signed(
137 | &ix,
138 | &[
139 | accounts.spl_token_program.clone(),
140 | accounts.token_source.clone(),
141 | accounts.token_destination.clone(),
142 | accounts.nft_record.clone(),
143 | ],
144 | &[seeds],
145 | )?;
146 |
147 | // Withdraw native SOL if any
148 | let minimum_rent = Rent::get()?.minimum_balance(accounts.nft_record.data_len());
149 | let lamports_to_withdraw = accounts
150 | .nft_record
151 | .lamports()
152 | .checked_sub(minimum_rent)
153 | .unwrap();
154 |
155 | msg!("+ Withdrawing native SOL {}", lamports_to_withdraw);
156 | let mut nft_record_lamports = accounts.nft_record.lamports.borrow_mut();
157 | let mut nft_owner_lamports = accounts.nft_owner.lamports.borrow_mut();
158 |
159 | **nft_record_lamports -= lamports_to_withdraw;
160 | **nft_owner_lamports += lamports_to_withdraw;
161 |
162 | // Update NFT record owner
163 | nft_record.owner = *accounts.nft_owner.key;
164 | nft_record.save(&mut accounts.nft_record.data.borrow_mut());
165 |
166 | Ok(())
167 | }
168 |
--------------------------------------------------------------------------------
/program/src/state.rs:
--------------------------------------------------------------------------------
1 | use {
2 | bonfida_utils::BorshSize,
3 | borsh::{BorshDeserialize, BorshSerialize},
4 | mpl_token_metadata::types::Creator,
5 | solana_program::{pubkey, pubkey::Pubkey},
6 | };
7 |
8 | mod central_state;
9 | mod nft_record;
10 |
11 | pub use central_state::CentralState;
12 | pub use nft_record::NftRecord;
13 |
14 | pub const ROOT_DOMAIN_ACCOUNT: Pubkey = pubkey!("58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx");
15 |
16 | pub const MINT_PREFIX: &[u8; 14] = b"tokenized_name";
17 |
18 | pub const SELLER_BASIS: u16 = 500;
19 |
20 | pub const META_SYMBOL: &str = ".sol";
21 |
22 | pub const CREATOR_KEY: Pubkey = pubkey!("5D2zKog251d6KPCyFyLMt3KroWwXXPWSgTPyhV22K2gR");
23 |
24 | pub const CREATOR_FEE: Creator = Creator {
25 | address: CREATOR_KEY,
26 | verified: false,
27 | share: 100,
28 | };
29 |
30 | pub const COLLECTION_PREFIX: &[u8; 10] = b"collection";
31 |
32 | pub const COLLECTION_NAME: &str = "Solana name service collection";
33 |
34 | pub const COLLECTION_URI: &str =
35 | "https://cloudflare-ipfs.com/ipfs/QmPeTioTicb19seM6itP8KD39syNZVJS2KHXNkxauSGXAJ";
36 |
37 | pub const METADATA_SIGNER: Pubkey = pubkey!("Es33LnWSTZ9GbW6yBaRkSLUaFibVd7iS54e4AvBg76LX");
38 |
39 | #[derive(BorshSerialize, BorshDeserialize, BorshSize, PartialEq)]
40 | #[allow(missing_docs)]
41 | pub enum Tag {
42 | Uninitialized,
43 | CentralState,
44 | ActiveRecord,
45 | InactiveRecord,
46 | }
47 |
--------------------------------------------------------------------------------
/program/src/state/central_state.rs:
--------------------------------------------------------------------------------
1 | use bonfida_utils::BorshSize;
2 | use borsh::{BorshDeserialize, BorshSerialize};
3 | use solana_program::pubkey::Pubkey;
4 |
5 | use super::Tag;
6 |
7 | #[derive(BorshSerialize, BorshDeserialize, BorshSize)]
8 | #[allow(missing_docs)]
9 | pub struct CentralState {
10 | pub tag: Tag,
11 | }
12 |
13 | impl CentralState {
14 | pub fn find_key(program_id: &Pubkey) -> (Pubkey, u8) {
15 | let seeds: &[&[u8]] = &[&program_id.to_bytes()];
16 | Pubkey::find_program_address(seeds, program_id)
17 | }
18 |
19 | pub fn save(&self, mut dst: &mut [u8]) {
20 | self.serialize(&mut dst).unwrap()
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/program/src/state/nft_record.rs:
--------------------------------------------------------------------------------
1 | use bonfida_utils::BorshSize;
2 | use borsh::{BorshDeserialize, BorshSerialize};
3 | use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
4 |
5 | use crate::error::OfferError;
6 |
7 | use super::Tag;
8 |
9 | #[derive(BorshSerialize, BorshDeserialize, BorshSize)]
10 | #[allow(missing_docs)]
11 | pub struct NftRecord {
12 | /// Tag
13 | pub tag: Tag,
14 |
15 | /// Nonce
16 | pub nonce: u8,
17 |
18 | /// Name account of the record
19 | pub name_account: Pubkey,
20 |
21 | /// Record owner
22 | pub owner: Pubkey,
23 |
24 | /// NFT mint
25 | pub nft_mint: Pubkey,
26 | }
27 |
28 | #[allow(missing_docs)]
29 | impl NftRecord {
30 | pub const SEED: &'static [u8; 10] = b"nft_record";
31 |
32 | pub fn new(nonce: u8, owner: Pubkey, name_account: Pubkey, nft_mint: Pubkey) -> Self {
33 | Self {
34 | tag: Tag::ActiveRecord,
35 | nonce,
36 | owner,
37 | name_account,
38 | nft_mint,
39 | }
40 | }
41 |
42 | pub fn find_key(name_account: &Pubkey, program_id: &Pubkey) -> (Pubkey, u8) {
43 | let seeds: &[&[u8]] = &[NftRecord::SEED, &name_account.to_bytes()];
44 | Pubkey::find_program_address(seeds, program_id)
45 | }
46 |
47 | pub fn save(&self, mut dst: &mut [u8]) {
48 | self.serialize(&mut dst).unwrap()
49 | }
50 |
51 | pub fn from_account_info(a: &AccountInfo, tag: Tag) -> Result {
52 | let mut data = &a.data.borrow() as &[u8];
53 | if data[0] != tag as u8 {
54 | return Err(OfferError::DataTypeMismatch.into());
55 | }
56 | let result = NftRecord::deserialize(&mut data)?;
57 | Ok(result)
58 | }
59 |
60 | pub fn is_active(&self) -> bool {
61 | self.tag == Tag::ActiveRecord
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/program/src/utils.rs:
--------------------------------------------------------------------------------
1 | use {
2 | bonfida_utils::checks::check_account_owner,
3 | solana_program::{
4 | account_info::AccountInfo, entrypoint::ProgramResult, hash::hashv, msg,
5 | program_error::ProgramError,
6 | },
7 | spl_name_service::state::{get_seeds_and_key, HASH_PREFIX},
8 | };
9 |
10 | use crate::state::ROOT_DOMAIN_ACCOUNT;
11 |
12 | pub fn check_name(name: &str, account: &AccountInfo) -> ProgramResult {
13 | check_account_owner(account, &spl_name_service::ID)?;
14 |
15 | let hashed_name = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()])
16 | .as_ref()
17 | .to_vec();
18 |
19 | if hashed_name.len() != 32 {
20 | msg!("Invalid seed length");
21 | return Err(ProgramError::InvalidArgument);
22 | }
23 |
24 | let (name_account_key, _) = get_seeds_and_key(
25 | &spl_name_service::ID,
26 | hashed_name,
27 | None,
28 | Some(&ROOT_DOMAIN_ACCOUNT),
29 | );
30 |
31 | if &name_account_key != account.key {
32 | msg!("Provided wrong name account");
33 | #[cfg(not(feature = "devnet"))]
34 | return Err(ProgramError::InvalidArgument);
35 | }
36 |
37 | Ok(())
38 | }
39 |
--------------------------------------------------------------------------------
/program/tests/common/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod utils;
2 |
--------------------------------------------------------------------------------
/program/tests/common/utils.rs:
--------------------------------------------------------------------------------
1 | use std::str::FromStr;
2 |
3 | use solana_program::instruction::Instruction;
4 | use solana_program::program_pack::Pack;
5 | use solana_program::pubkey::Pubkey;
6 | use solana_program_test::{BanksClientError, ProgramTest, ProgramTestContext};
7 | use solana_sdk::account::Account;
8 | use solana_sdk::signature::Signer;
9 | use solana_sdk::{signature::Keypair, transaction::Transaction};
10 | use spl_token::state::Mint;
11 |
12 | // Utils
13 | pub async fn sign_send_instructions(
14 | ctx: &mut ProgramTestContext,
15 | instructions: Vec,
16 | signers: Vec<&Keypair>,
17 | ) -> Result<(), BanksClientError> {
18 | let mut transaction = Transaction::new_with_payer(&instructions, Some(&ctx.payer.pubkey()));
19 | let mut payer_signers = vec![&ctx.payer];
20 | for s in signers {
21 | payer_signers.push(s);
22 | }
23 | transaction.partial_sign(&payer_signers, ctx.last_blockhash);
24 | ctx.banks_client.process_transaction(transaction).await
25 | }
26 |
27 | pub fn mint_bootstrap(
28 | address: Option<&str>,
29 | decimals: u8,
30 | program_test: &mut ProgramTest,
31 | mint_authority: &Pubkey,
32 | ) -> (Pubkey, Mint) {
33 | let address = address
34 | .map(|s| Pubkey::from_str(s).unwrap())
35 | .unwrap_or_else(Pubkey::new_unique);
36 | let mint_info = Mint {
37 | mint_authority: Some(*mint_authority).into(),
38 | supply: u32::MAX.into(),
39 | decimals,
40 | is_initialized: true,
41 | freeze_authority: None.into(),
42 | };
43 | let mut data = [0; Mint::LEN];
44 | mint_info.pack_into_slice(&mut data);
45 | program_test.add_account(
46 | address,
47 | Account {
48 | lamports: u32::MAX.into(),
49 | data: data.into(),
50 | owner: spl_token::ID,
51 | executable: false,
52 | ..Account::default()
53 | },
54 | );
55 | (address, mint_info)
56 | }
57 |
--------------------------------------------------------------------------------
/program/tests/functional.rs:
--------------------------------------------------------------------------------
1 | use {
2 | borsh::{BorshDeserialize, BorshSerialize},
3 | name_tokenizer::{
4 | entrypoint::process_instruction,
5 | instruction::{
6 | create_collection, create_mint, create_nft, redeem_nft, unverify_nft, withdraw_tokens,
7 | },
8 | state::{
9 | CentralState, NftRecord, COLLECTION_PREFIX, METADATA_SIGNER, MINT_PREFIX,
10 | ROOT_DOMAIN_ACCOUNT,
11 | },
12 | },
13 | solana_program::{hash::hashv, pubkey::Pubkey, system_instruction, system_program, sysvar},
14 | solana_program_test::{processor, ProgramTest},
15 | solana_sdk::{
16 | account::Account,
17 | signer::{keypair::Keypair, Signer},
18 | },
19 | spl_associated_token_account::{
20 | get_associated_token_address, instruction::create_associated_token_account,
21 | },
22 | spl_name_service::state::{get_seeds_and_key, HASH_PREFIX},
23 | };
24 |
25 | pub mod common;
26 |
27 | use mpl_token_metadata::accounts::{MasterEdition, Metadata};
28 | use name_tokenizer::instruction::edit_data;
29 |
30 | use crate::common::utils::{mint_bootstrap, sign_send_instructions};
31 |
32 | #[tokio::test]
33 | async fn test_offer() {
34 | // Create program and test environment
35 | let alice = Keypair::new();
36 | let bob = Keypair::new();
37 | let mint_authority = Keypair::new();
38 |
39 | let mut program_test = ProgramTest::new(
40 | "name_tokenizer",
41 | name_tokenizer::ID,
42 | processor!(process_instruction),
43 | );
44 | program_test.add_program("spl_name_service", spl_name_service::ID, None);
45 | program_test.add_program("mpl_token_metadata", mpl_token_metadata::ID, None);
46 |
47 | // Create domain name
48 | let name = "something_domain_name";
49 | let hashed_name = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()])
50 | .as_ref()
51 | .to_vec();
52 |
53 | let (name_key, _) = get_seeds_and_key(
54 | &spl_name_service::ID,
55 | hashed_name,
56 | None,
57 | Some(&ROOT_DOMAIN_ACCOUNT),
58 | );
59 |
60 | let name_domain_data = [
61 | spl_name_service::state::NameRecordHeader {
62 | parent_name: ROOT_DOMAIN_ACCOUNT,
63 | owner: alice.pubkey(),
64 | class: Pubkey::default(),
65 | }
66 | .try_to_vec()
67 | .unwrap(),
68 | vec![0; 1000],
69 | ]
70 | .concat();
71 |
72 | program_test.add_account(
73 | name_key,
74 | Account {
75 | lamports: 1_000_000,
76 | data: name_domain_data,
77 | owner: spl_name_service::id(),
78 | ..Account::default()
79 | },
80 | );
81 |
82 | program_test.add_account(
83 | alice.pubkey(),
84 | Account {
85 | lamports: 100_000_000_000,
86 | ..Account::default()
87 | },
88 | );
89 | program_test.add_account(
90 | bob.pubkey(),
91 | Account {
92 | lamports: 100_000_000_000,
93 | ..Account::default()
94 | },
95 | );
96 |
97 | //
98 | // Create mint
99 | //
100 | let (usdc_mint, _) = mint_bootstrap(None, 6, &mut program_test, &mint_authority.pubkey());
101 |
102 | ////
103 | // Create test context
104 | ////
105 | let mut prg_test_ctx = program_test.start_with_context().await;
106 |
107 | /////
108 | // Create central state
109 | ////
110 | let (central_key, _) = CentralState::find_key(&name_tokenizer::ID);
111 |
112 | ////
113 | // Create mint
114 | ////
115 | let (nft_mint, _) =
116 | Pubkey::find_program_address(&[MINT_PREFIX, &name_key.to_bytes()], &name_tokenizer::ID);
117 |
118 | let ix = create_mint(
119 | create_mint::Accounts {
120 | mint: &nft_mint,
121 | central_state: ¢ral_key,
122 | name_account: &name_key,
123 | spl_token_program: &spl_token::ID,
124 | rent_account: &sysvar::rent::ID,
125 | fee_payer: &prg_test_ctx.payer.pubkey(),
126 | system_program: &system_program::ID,
127 | },
128 | create_mint::Params {},
129 | );
130 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
131 | .await
132 | .unwrap();
133 |
134 | ////
135 | // Create central state ATA for collection mint
136 | ////
137 |
138 | let (collection_mint, _) = Pubkey::find_program_address(
139 | &[COLLECTION_PREFIX, &name_tokenizer::ID.to_bytes()],
140 | &name_tokenizer::ID,
141 | );
142 | let central_state_collection_ata =
143 | get_associated_token_address(&name_tokenizer::central_state::KEY, &collection_mint);
144 |
145 | ////
146 | // Create collection
147 | ////
148 | let (edition_key, _) = MasterEdition::find_pda(&collection_mint);
149 | let (collection_metadata_key, _) = Metadata::find_pda(&collection_mint);
150 | let ix = create_collection(
151 | create_collection::Accounts {
152 | collection_mint: &collection_mint,
153 | edition: &edition_key,
154 | metadata_account: &collection_metadata_key,
155 | central_state: ¢ral_key,
156 | central_state_nft_ata: ¢ral_state_collection_ata,
157 | fee_payer: &prg_test_ctx.payer.pubkey(),
158 | spl_token_program: &spl_token::ID,
159 | metadata_program: &mpl_token_metadata::ID,
160 | system_program: &system_program::ID,
161 | spl_name_service_program: &spl_name_service::ID,
162 | rent_account: &sysvar::rent::ID,
163 | ata_program: &spl_associated_token_account::ID,
164 | },
165 | create_collection::Params {},
166 | );
167 |
168 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
169 | .await
170 | .unwrap();
171 |
172 | ////
173 | // Create Alice and Bob ATAs
174 | ////
175 |
176 | let ix = create_associated_token_account(
177 | &alice.pubkey(),
178 | &alice.pubkey(),
179 | &nft_mint,
180 | &spl_token::ID,
181 | );
182 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
183 | .await
184 | .unwrap();
185 | let ix =
186 | create_associated_token_account(&bob.pubkey(), &bob.pubkey(), &nft_mint, &spl_token::ID);
187 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&bob])
188 | .await
189 | .unwrap();
190 |
191 | ////
192 | // Create NFT
193 | ////
194 |
195 | let alice_nft_ata = get_associated_token_address(&alice.pubkey(), &nft_mint);
196 | let bob_nft_ata = get_associated_token_address(&bob.pubkey(), &nft_mint);
197 | let (nft_record, _) = NftRecord::find_key(&name_key, &name_tokenizer::ID);
198 | let (metadata_key, _) = Metadata::find_pda(&nft_mint);
199 |
200 | let ix = create_nft(
201 | create_nft::Accounts {
202 | mint: &nft_mint,
203 | nft_destination: &alice_nft_ata,
204 | name_account: &name_key,
205 | nft_record: &nft_record,
206 | name_owner: &alice.pubkey(),
207 | metadata_account: &metadata_key,
208 | central_state: ¢ral_key,
209 | spl_token_program: &spl_token::ID,
210 | metadata_program: &mpl_token_metadata::ID,
211 | system_program: &system_program::ID,
212 | spl_name_service_program: &spl_name_service::ID,
213 | rent_account: &sysvar::rent::ID,
214 | fee_payer: &prg_test_ctx.payer.pubkey(),
215 | edition_account: &edition_key,
216 | collection_metadata: &collection_metadata_key,
217 | collection_mint: &collection_mint,
218 | #[cfg(not(feature = "devnet"))]
219 | metadata_signer: &METADATA_SIGNER,
220 | },
221 | create_nft::Params {
222 | name: name.to_string(),
223 | uri: "test".to_string(),
224 | },
225 | );
226 |
227 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
228 | .await
229 | .unwrap();
230 |
231 | ////
232 | // Edit data
233 | ////
234 | let ix = edit_data(
235 | name_tokenizer::instruction::edit_data::Accounts {
236 | nft_owner: &alice.pubkey(),
237 | nft_record: &nft_record,
238 | name_account: &name_key,
239 | spl_token_program: &spl_token::ID,
240 | spl_name_service_program: &spl_name_service::ID,
241 | nft_account: &alice_nft_ata,
242 | },
243 | edit_data::Params {
244 | offset: 0,
245 | data: vec![1],
246 | },
247 | );
248 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
249 | .await
250 | .unwrap();
251 |
252 | ////
253 | // Withdraw NFT
254 | ////
255 | let ix = redeem_nft(
256 | redeem_nft::Accounts {
257 | mint: &nft_mint,
258 | nft_source: &alice_nft_ata,
259 | nft_owner: &alice.pubkey(),
260 | nft_record: &nft_record,
261 | name_account: &name_key,
262 | spl_token_program: &spl_token::ID,
263 | spl_name_service_program: &spl_name_service::ID,
264 | },
265 | redeem_nft::Params {},
266 | );
267 |
268 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
269 | .await
270 | .unwrap();
271 |
272 | ////
273 | // Send tokens
274 | ////
275 | let usdc_ata_program = get_associated_token_address(&nft_record, &usdc_mint);
276 | let usdc_ata_alice = get_associated_token_address(&alice.pubkey(), &usdc_mint);
277 | let usdc_ata_bob = get_associated_token_address(&bob.pubkey(), &usdc_mint);
278 |
279 | let ix = create_associated_token_account(
280 | &prg_test_ctx.payer.pubkey(),
281 | &nft_record,
282 | &usdc_mint,
283 | &spl_token::ID,
284 | );
285 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
286 | .await
287 | .unwrap();
288 |
289 | let ix = create_associated_token_account(
290 | &prg_test_ctx.payer.pubkey(),
291 | &alice.pubkey(),
292 | &usdc_mint,
293 | &spl_token::ID,
294 | );
295 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
296 | .await
297 | .unwrap();
298 | let ix = create_associated_token_account(
299 | &prg_test_ctx.payer.pubkey(),
300 | &bob.pubkey(),
301 | &usdc_mint,
302 | &spl_token::ID,
303 | );
304 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
305 | .await
306 | .unwrap();
307 |
308 | let ix = spl_token::instruction::mint_to(
309 | &spl_token::ID,
310 | &usdc_mint,
311 | &usdc_ata_program,
312 | &mint_authority.pubkey(),
313 | &[],
314 | 10_000_000_000,
315 | )
316 | .unwrap();
317 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&mint_authority])
318 | .await
319 | .unwrap();
320 |
321 | // Also send some SOL
322 |
323 | let ix = system_instruction::transfer(&prg_test_ctx.payer.pubkey(), &nft_record, 10_000_000);
324 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
325 | .await
326 | .unwrap();
327 |
328 | ////
329 | // Withdraw sent tokens
330 | ////
331 |
332 | let ix = withdraw_tokens(
333 | withdraw_tokens::Accounts {
334 | nft: &alice_nft_ata,
335 | nft_owner: &alice.pubkey(),
336 | nft_record: &nft_record,
337 | token_source: &usdc_ata_program,
338 | token_destination: &usdc_ata_alice,
339 | spl_token_program: &spl_token::ID,
340 | system_program: &system_program::ID,
341 | },
342 | withdraw_tokens::Params {},
343 | );
344 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
345 | .await
346 | .unwrap();
347 |
348 | ////
349 | // Bob tries to withdraw tokens
350 | ////
351 |
352 | let ix = withdraw_tokens(
353 | withdraw_tokens::Accounts {
354 | nft: &alice_nft_ata,
355 | nft_owner: &bob.pubkey(),
356 | nft_record: &nft_record,
357 | token_source: &usdc_ata_program,
358 | token_destination: &usdc_ata_bob,
359 | spl_token_program: &spl_token::ID,
360 | system_program: &system_program::ID,
361 | },
362 | withdraw_tokens::Params {},
363 | );
364 | let err = sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&bob])
365 | .await
366 | .is_err();
367 | assert!(err);
368 |
369 | ////
370 | // Alice transfer domain to Bob
371 | ////
372 | let ix = spl_name_service::instruction::transfer(
373 | spl_name_service::ID,
374 | bob.pubkey(),
375 | name_key,
376 | alice.pubkey(),
377 | None,
378 | )
379 | .unwrap();
380 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&alice])
381 | .await
382 | .unwrap();
383 |
384 | ////
385 | // Bob creates NFT again
386 | ////
387 | let ix = create_nft(
388 | create_nft::Accounts {
389 | mint: &nft_mint,
390 | nft_destination: &bob_nft_ata,
391 | name_account: &name_key,
392 | nft_record: &nft_record,
393 | name_owner: &bob.pubkey(),
394 | metadata_account: &metadata_key,
395 | central_state: ¢ral_key,
396 | spl_token_program: &spl_token::ID,
397 | metadata_program: &mpl_token_metadata::ID,
398 | system_program: &system_program::ID,
399 | spl_name_service_program: &spl_name_service::ID,
400 | rent_account: &sysvar::rent::ID,
401 | fee_payer: &prg_test_ctx.payer.pubkey(),
402 | edition_account: &edition_key,
403 | collection_metadata: &collection_metadata_key,
404 | collection_mint: &collection_mint,
405 | #[cfg(not(feature = "devnet"))]
406 | metadata_signer: &METADATA_SIGNER,
407 | },
408 | create_nft::Params {
409 | name: name.to_string(),
410 | uri: "test".to_string(),
411 | },
412 | );
413 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&bob])
414 | .await
415 | .unwrap();
416 | let ix = withdraw_tokens(
417 | withdraw_tokens::Accounts {
418 | nft: &bob_nft_ata,
419 | nft_owner: &bob.pubkey(),
420 | nft_record: &nft_record,
421 | token_source: &usdc_ata_program,
422 | token_destination: &usdc_ata_bob,
423 | spl_token_program: &spl_token::ID,
424 | system_program: &system_program::ID,
425 | },
426 | withdraw_tokens::Params {},
427 | );
428 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![&bob])
429 | .await
430 | .unwrap();
431 |
432 | //////
433 | // Unverify NFT
434 | //////
435 | let info = prg_test_ctx
436 | .banks_client
437 | .get_account(metadata_key)
438 | .await
439 | .unwrap()
440 | .unwrap();
441 |
442 | let des = Metadata::safe_deserialize(&info.data).unwrap();
443 | assert!(des.collection.unwrap().verified);
444 |
445 | let ix = unverify_nft(
446 | unverify_nft::Accounts {
447 | metadata_account: &metadata_key,
448 | edition_account: &edition_key,
449 | collection_metadata: &collection_metadata_key,
450 | collection_mint: &collection_mint,
451 | central_state: ¢ral_key,
452 | fee_payer: &prg_test_ctx.payer.pubkey(),
453 | metadata_program: &mpl_token_metadata::ID,
454 | system_program: &system_program::ID,
455 | rent_account: &sysvar::rent::ID,
456 | #[cfg(not(feature = "devnet"))]
457 | metadata_signer: &METADATA_SIGNER,
458 | },
459 | unverify_nft::Params {},
460 | );
461 | sign_send_instructions(&mut prg_test_ctx, vec![ix], vec![])
462 | .await
463 | .unwrap();
464 | let info = prg_test_ctx
465 | .banks_client
466 | .get_account(metadata_key)
467 | .await
468 | .unwrap()
469 | .unwrap();
470 |
471 | let des = Metadata::safe_deserialize(&info.data).unwrap();
472 | assert!(!des.collection.unwrap().verified);
473 | }
474 |
--------------------------------------------------------------------------------
/python/src/raw_instructions.py:
--------------------------------------------------------------------------------
1 | from typing import List
2 | from borsh_construct import U8, String, CStruct
3 | from solana.transaction import TransactionInstruction, AccountMeta
4 | from solana.publickey import PublicKey
5 |
6 |
7 | class CreateCollectionInstruction:
8 | schema = CStruct(
9 | "tag" / U8,
10 | )
11 |
12 | def serialize(
13 | self,
14 | ) -> str:
15 | return self.schema.build(
16 | {
17 | "tag": 1,
18 | }
19 | )
20 |
21 | def getInstruction(
22 | self,
23 | programId: PublicKey,
24 | collection_mint: PublicKey,
25 | edition: PublicKey,
26 | metadata_account: PublicKey,
27 | central_state: PublicKey,
28 | central_state_nft_ata: PublicKey,
29 | fee_payer: PublicKey,
30 | spl_token_program: PublicKey,
31 | metadata_program: PublicKey,
32 | system_program: PublicKey,
33 | spl_name_service_program: PublicKey,
34 | ata_program: PublicKey,
35 | rent_account: PublicKey,
36 | ) -> TransactionInstruction:
37 | data = self.serialize()
38 | keys: List[AccountMeta] = []
39 | keys.append(AccountMeta(collection_mint, False, True))
40 | keys.append(AccountMeta(edition, False, True))
41 | keys.append(AccountMeta(metadata_account, False, True))
42 | keys.append(AccountMeta(central_state, False, False))
43 | keys.append(AccountMeta(central_state_nft_ata, False, True))
44 | keys.append(AccountMeta(fee_payer, False, False))
45 | keys.append(AccountMeta(spl_token_program, False, False))
46 | keys.append(AccountMeta(metadata_program, False, False))
47 | keys.append(AccountMeta(system_program, False, False))
48 | keys.append(AccountMeta(spl_name_service_program, False, False))
49 | keys.append(AccountMeta(ata_program, False, False))
50 | keys.append(AccountMeta(rent_account, False, False))
51 | return TransactionInstruction(keys, programId, data)
52 |
53 |
54 | class CreateMintInstruction:
55 | schema = CStruct(
56 | "tag" / U8,
57 | )
58 |
59 | def serialize(
60 | self,
61 | ) -> str:
62 | return self.schema.build(
63 | {
64 | "tag": 0,
65 | }
66 | )
67 |
68 | def getInstruction(
69 | self,
70 | programId: PublicKey,
71 | mint: PublicKey,
72 | name_account: PublicKey,
73 | central_state: PublicKey,
74 | spl_token_program: PublicKey,
75 | system_program: PublicKey,
76 | rent_account: PublicKey,
77 | fee_payer: PublicKey,
78 | ) -> TransactionInstruction:
79 | data = self.serialize()
80 | keys: List[AccountMeta] = []
81 | keys.append(AccountMeta(mint, False, True))
82 | keys.append(AccountMeta(name_account, False, True))
83 | keys.append(AccountMeta(central_state, False, False))
84 | keys.append(AccountMeta(spl_token_program, False, False))
85 | keys.append(AccountMeta(system_program, False, False))
86 | keys.append(AccountMeta(rent_account, False, False))
87 | keys.append(AccountMeta(fee_payer, False, False))
88 | return TransactionInstruction(keys, programId, data)
89 |
90 |
91 | class CreateNftInstruction:
92 | schema = CStruct(
93 | "tag" / U8,
94 | "name" / String,
95 | "uri" / String,
96 | )
97 |
98 | def serialize(
99 | self,
100 | name: str,
101 | uri: str,
102 | ) -> str:
103 | return self.schema.build(
104 | {
105 | "tag": 2,
106 | "name": name,
107 | "uri": uri,
108 | }
109 | )
110 |
111 | def getInstruction(
112 | self,
113 | programId: PublicKey,
114 | mint: PublicKey,
115 | nft_destination: PublicKey,
116 | name_account: PublicKey,
117 | nft_record: PublicKey,
118 | name_owner: PublicKey,
119 | metadata_account: PublicKey,
120 | edition_account: PublicKey,
121 | collection_metadata: PublicKey,
122 | collection_mint: PublicKey,
123 | central_state: PublicKey,
124 | fee_payer: PublicKey,
125 | spl_token_program: PublicKey,
126 | metadata_program: PublicKey,
127 | system_program: PublicKey,
128 | spl_name_service_program: PublicKey,
129 | rent_account: PublicKey,
130 | metadata_signer: PublicKey,
131 | name: str,
132 | uri: str,
133 | ) -> TransactionInstruction:
134 | data = self.serialize(
135 | name,
136 | uri,
137 | )
138 | keys: List[AccountMeta] = []
139 | keys.append(AccountMeta(mint, False, True))
140 | keys.append(AccountMeta(nft_destination, False, True))
141 | keys.append(AccountMeta(name_account, False, True))
142 | keys.append(AccountMeta(nft_record, False, True))
143 | keys.append(AccountMeta(name_owner, True, True))
144 | keys.append(AccountMeta(metadata_account, False, True))
145 | keys.append(AccountMeta(edition_account, False, False))
146 | keys.append(AccountMeta(collection_metadata, False, False))
147 | keys.append(AccountMeta(collection_mint, False, False))
148 | keys.append(AccountMeta(central_state, False, True))
149 | keys.append(AccountMeta(fee_payer, True, True))
150 | keys.append(AccountMeta(spl_token_program, False, False))
151 | keys.append(AccountMeta(metadata_program, False, False))
152 | keys.append(AccountMeta(system_program, False, False))
153 | keys.append(AccountMeta(spl_name_service_program, False, False))
154 | keys.append(AccountMeta(rent_account, False, False))
155 | keys.append(AccountMeta(metadata_signer, True, False))
156 | return TransactionInstruction(keys, programId, data)
157 |
158 |
159 | class RedeemNftInstruction:
160 | schema = CStruct(
161 | "tag" / U8,
162 | )
163 |
164 | def serialize(
165 | self,
166 | ) -> str:
167 | return self.schema.build(
168 | {
169 | "tag": 3,
170 | }
171 | )
172 |
173 | def getInstruction(
174 | self,
175 | programId: PublicKey,
176 | mint: PublicKey,
177 | nft_source: PublicKey,
178 | nft_owner: PublicKey,
179 | nft_record: PublicKey,
180 | name_account: PublicKey,
181 | spl_token_program: PublicKey,
182 | spl_name_service_program: PublicKey,
183 | ) -> TransactionInstruction:
184 | data = self.serialize()
185 | keys: List[AccountMeta] = []
186 | keys.append(AccountMeta(mint, False, True))
187 | keys.append(AccountMeta(nft_source, False, True))
188 | keys.append(AccountMeta(nft_owner, True, True))
189 | keys.append(AccountMeta(nft_record, False, True))
190 | keys.append(AccountMeta(name_account, False, True))
191 | keys.append(AccountMeta(spl_token_program, False, False))
192 | keys.append(AccountMeta(spl_name_service_program, False, False))
193 | return TransactionInstruction(keys, programId, data)
194 |
195 |
196 | class WithdrawTokensInstruction:
197 | schema = CStruct(
198 | "tag" / U8,
199 | )
200 |
201 | def serialize(
202 | self,
203 | ) -> str:
204 | return self.schema.build(
205 | {
206 | "tag": 4,
207 | }
208 | )
209 |
210 | def getInstruction(
211 | self,
212 | programId: PublicKey,
213 | nft: PublicKey,
214 | nft_owner: PublicKey,
215 | nft_record: PublicKey,
216 | token_destination: PublicKey,
217 | token_source: PublicKey,
218 | spl_token_program: PublicKey,
219 | system_program: PublicKey,
220 | ) -> TransactionInstruction:
221 | data = self.serialize()
222 | keys: List[AccountMeta] = []
223 | keys.append(AccountMeta(nft, False, True))
224 | keys.append(AccountMeta(nft_owner, True, True))
225 | keys.append(AccountMeta(nft_record, False, True))
226 | keys.append(AccountMeta(token_destination, False, True))
227 | keys.append(AccountMeta(token_source, False, True))
228 | keys.append(AccountMeta(spl_token_program, False, False))
229 | keys.append(AccountMeta(system_program, False, False))
230 | return TransactionInstruction(keys, programId, data)
231 |
232 |
233 | class EditDataInstruction:
234 | schema = CStruct(
235 | "tag" / U8,
236 | "offset" / U32,
237 | "data" / Vec(U8),
238 | )
239 |
240 | def serialize(
241 | self,
242 | offset: int,
243 | data: List[int],
244 | ) -> str:
245 | return self.schema.build(
246 | {
247 | "tag": 5,
248 | "offset": offset,
249 | "data": data,
250 | }
251 | )
252 |
253 | def getInstruction(
254 | self,
255 | programId: PublicKey,
256 | nft_owner: PublicKey,
257 | nft_account: PublicKey,
258 | nft_record: PublicKey,
259 | name_account: PublicKey,
260 | spl_token_program: PublicKey,
261 | spl_name_service_program: PublicKey,
262 | offset: int,
263 | data: List[int],
264 | ) -> TransactionInstruction:
265 | data = self.serialize(
266 | offset,
267 | data,
268 | )
269 | keys: List[AccountMeta] = []
270 | keys.append(AccountMeta(nft_owner, True, False))
271 | keys.append(AccountMeta(nft_account, False, False))
272 | keys.append(AccountMeta(nft_record, False, False))
273 | keys.append(AccountMeta(name_account, False, True))
274 | keys.append(AccountMeta(spl_token_program, False, False))
275 | keys.append(AccountMeta(spl_name_service_program, False, False))
276 | return TransactionInstruction(keys, programId, data)
277 |
--------------------------------------------------------------------------------