├── .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 | --------------------------------------------------------------------------------