├── .prettierignore ├── programs └── mpl-corenft-staking │ ├── Xargo.toml │ ├── src │ ├── constant.rs │ ├── instructions │ │ ├── mod.rs │ │ ├── lock_corenft.rs │ │ └── unlock_corenft.rs │ ├── error.rs │ ├── state.rs │ └── lib.rs │ └── Cargo.toml ├── .gitignore ├── lib ├── constant.ts ├── util.ts └── scripts.ts ├── Cargo.toml ├── tsconfig.json ├── Anchor.toml ├── tests └── mpl-corenft-pnft-staking.ts ├── README.md ├── package.json └── cli ├── command.ts └── scripts.ts /.prettierignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | node_modules 5 | dist 6 | build 7 | test-ledger 8 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/constant.rs: -------------------------------------------------------------------------------- 1 | pub const GLOBAL_AUTHORITY_SEED: &str = "global-authority"; 2 | 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | **/*.rs.bk 5 | node_modules 6 | test-ledger 7 | .yarn 8 | migrations 9 | app 10 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod lock_corenft; 2 | pub use lock_corenft::*; 3 | pub mod unlock_corenft; 4 | pub use unlock_corenft::*; 5 | -------------------------------------------------------------------------------- /lib/constant.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js'; 2 | 3 | export const GLOBAL_AUTHORITY_SEED = 'global-authority'; 4 | 5 | export const PROGRAM_ID = new PublicKey( 6 | 'your program ID' 7 | ); -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | resolver = "2" 6 | 7 | [profile.release] 8 | overflow-checks = true 9 | lto = "fat" 10 | codegen-units = 1 11 | [profile.release.build-override] 12 | opt-level = 3 13 | incremental = false 14 | codegen-units = 1 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true, 9 | "resolveJsonModule": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | 3 | [features] 4 | resolution = true 5 | skip-lint = false 6 | 7 | [programs.devnet] 8 | mpl_corenft_pnft_staking = "your program ID" 9 | 10 | [registry] 11 | url = "https://api.apr.dev" 12 | 13 | [provider] 14 | cluster = "Devnet" 15 | wallet = "~/topstar/deployer.json" 16 | 17 | [scripts] 18 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 19 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/error.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[error_code] 4 | pub enum StakingError { 5 | #[msg("Admin address dismatch")] 6 | InvalidAdmin, 7 | #[msg("Metadata address is invalid")] 8 | InvalidMetadata, 9 | #[msg("Collection is invalid")] 10 | InvalidCollection, 11 | #[msg("Can not parse creators in metadata")] 12 | MetadataCreatorParseError, 13 | #[msg("Insufficient Reward Vault Balance")] 14 | LackVaultBalance, 15 | #[msg("NFT Owner key mismatch")] 16 | InvalidOwner, 17 | #[msg("No Matching NFT to withdraw")] 18 | InvalidNFTAddress, 19 | #[msg("Reward is disabled")] 20 | DisabledReward, 21 | } 22 | -------------------------------------------------------------------------------- /tests/mpl-corenft-pnft-staking.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { MplCorenftPnftStaking } from "../target/types/mpl_corenft_pnft_staking"; 4 | 5 | describe("mpl-corenft-pnft-staking", () => { 6 | // Configure the client to use the local cluster. 7 | anchor.setProvider(anchor.AnchorProvider.env()); 8 | 9 | const program = anchor.workspace.MplCorenftPnftStaking as Program; 10 | 11 | it("Is initialized!", async () => { 12 | // Add your test here. 13 | const tx = await program.methods.initialize().rpc(); 14 | console.log("Your transaction signature", tx); 15 | }); 16 | }); -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/state.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[account] 4 | pub struct GlobalPool { 5 | pub admin: Pubkey, 6 | pub total_pnft_staked_count: u64, 7 | pub total_corenft_staked_count: u64, 8 | pub extra: u128, 9 | } 10 | 11 | impl Default for GlobalPool { //struct initialize. 12 | #[inline] //inline when this code is compiled. 13 | fn default() -> GlobalPool { 14 | GlobalPool { 15 | admin: Pubkey::default(), 16 | total_pnft_staked_count: 0, 17 | total_corenft_staked_count: 0, 18 | extra: 0, 19 | } 20 | } 21 | } 22 | 23 | impl GlobalPool { 24 | pub const DATA_SIZE: usize = 8 + std::mem::size_of::(); 25 | } -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{ prelude::*, AnchorDeserialize }; 2 | 3 | pub mod constant; 4 | pub mod error; 5 | pub mod instructions; 6 | pub mod state; 7 | use constant::*; 8 | use error::*; 9 | use instructions::*; 10 | use state::*; 11 | 12 | declare_id!("your program ID"); 13 | 14 | #[program] 15 | pub mod mpl_corenft_pnft_staking { 16 | use super::*; 17 | 18 | /** 19 | * User can lock Core NFTs from specific collection 20 | */ 21 | pub fn lock_corenft(ctx: Context) -> Result<()> { 22 | lock_corenft::process_instruction(ctx) 23 | } 24 | 25 | /** 26 | * User can unlock Core NFTs when they want 27 | */ 28 | pub fn unlock_corenft(ctx: Context) -> Result<()> { 29 | unlock_corenft::process_instruction(ctx) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mpl-corenft-staking" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "mpl_corenft_staking" 10 | 11 | [features] 12 | default = [] 13 | cpi = ["no-entrypoint"] 14 | no-entrypoint = [] 15 | no-idl = [] 16 | no-log-ix-name = [] 17 | idl-build = [ 18 | "anchor-lang/idl-build", 19 | "anchor-spl/idl-build", 20 | ] 21 | 22 | [dependencies] 23 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 24 | anchor-lang-idl = { version = "0.1.1", features = ["convert"] } 25 | anchor-spl = "0.30.1" 26 | bytemuck = "1.16.1" 27 | mpl-core = { version = "0.8.0", features = ["anchor"] } 28 | mpl-token-metadata = "4.1.2" 29 | solana-program = "2.0.11" 30 | toml_datetime = "0.6.6" 31 | winnow = "0.6.13" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Metaplex Core NFT Staking & Unstaking 2 | 3 | Anchor-based staking program based on Metaplex Core NFTs, utilizing the Metaplex Core standard. This allows users to lock up their Solana MPL Core NFTs and later unstake them using Solana Web3 interactions. 4 | 5 | ## Provide 6 | Anchor Rust program + CLI Web3 integration 7 | 8 | ### Core Functions 9 | - Stake NFT: Initiates the staking of an NFT and creates a new staking account for the user. 10 | - Unstake NFT: Unlocks the NFT from the staking account, allowing the user to reclaim their NFT and any associated rewards. 11 | 12 | #### Dependecies Version 13 | ``` rust 14 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 15 | anchor-lang-idl = { version = "0.1.1", features = ["convert"] } 16 | anchor-spl = "0.30.1" 17 | bytemuck = "1.16.1" 18 | mpl-core = { version = "0.8.0", features = ["anchor"] } 19 | mpl-token-metadata = "4.1.2" 20 | solana-program = "2.0.11" 21 | toml_datetime = "0.6.6" 22 | winnow = "0.6.13" 23 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 5 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 6 | "hook": "ts-node ./cli/hook.ts", 7 | "script": "export ANCHOR_WALLET=./deploy.json && ts-node ./cli/command.ts" 8 | }, 9 | "dependencies": { 10 | "@coral-xyz/anchor": "^0.30.1", 11 | "@metaplex-foundation/digital-asset-standard-api": "^1.0.3", 12 | "@metaplex-foundation/mpl-core": "^1.0.2", 13 | "@metaplex-foundation/mpl-token-auth-rules": "^2.0.0", 14 | "@metaplex-foundation/mpl-token-metadata": "^3.2.1", 15 | "@metaplex-foundation/umi": "^0.9.2", 16 | "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", 17 | "@metaplex-foundation/umi-uploader-irys": "^0.9.1", 18 | "@project-serum/anchor": "^0.26.0", 19 | "@solana/spl-token": "^0.4.6", 20 | "@solana/web3.js": "^1.91.7", 21 | "@types/node": "^20.12.7", 22 | "bs58": "^5.0.0", 23 | "commander": "^9.5.0" 24 | }, 25 | "devDependencies": { 26 | "@types/bn.js": "^5.1.0", 27 | "@types/chai": "^4.3.0", 28 | "@types/mocha": "^9.0.0", 29 | "chai": "^4.3.4", 30 | "mocha": "^9.0.3", 31 | "prettier": "^2.6.2", 32 | "ts-mocha": "^10.0.0", 33 | "ts-node": "^10.9.2", 34 | "typescript": "^5.4.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/instructions/lock_corenft.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use anchor_lang::prelude::Clock; 3 | use mpl_core::{ 4 | ID as CORE_PROGRAM_ID, 5 | accounts::{BaseAssetV1, BaseCollectionV1}, 6 | instructions::{AddPluginV1CpiBuilder}, 7 | types::{ FreezeDelegate, Plugin, UpdateAuthority }, 8 | }; 9 | 10 | #[derive(Accounts)] 11 | pub struct LockCoreNFT<'info> { 12 | pub owner: Signer<'info>, 13 | 14 | #[account(mut)] 15 | pub user: Signer<'info>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [GLOBAL_AUTHORITY_SEED], 20 | bump 21 | )] 22 | pub global_pool: Account<'info, GlobalPool>, 23 | 24 | #[account( 25 | mut, 26 | has_one = owner @ StakingError::InvalidAdmin, 27 | constraint = asset.update_authority == UpdateAuthority::Collection(collection.key()), 28 | )] 29 | pub asset: Account<'info, BaseAssetV1>, 30 | 31 | // #[account( 32 | // mut, 33 | // )] 34 | // pub collection: Account<'info, BaseCollectionV1>, 35 | 36 | #[account(address = CORE_PROGRAM_ID)] 37 | /// CHECK: this will be checked by core 38 | pub core_program: UncheckedAccount<'info>, 39 | pub system_program: Program<'info, System>, 40 | } 41 | 42 | pub fn process_instruction(ctx: Context) -> Result<()> { 43 | let global_pool = &mut ctx.accounts.global_pool; 44 | 45 | // Freeze the asset 46 | AddPluginV1CpiBuilder::new(&ctx.accounts.core_program.to_account_info()) 47 | .asset(&ctx.accounts.asset.to_account_info()) 48 | .collection(Some(&ctx.accounts.collection.to_account_info())) 49 | .payer(&ctx.accounts.user.to_account_info()) 50 | .system_program(&ctx.accounts.system_program.to_account_info()) 51 | .plugin(Plugin::FreezeDelegate( FreezeDelegate{ frozen: true } )) 52 | .invoke()?; 53 | 54 | global_pool.total_corenft_staked_count += 1; 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /programs/mpl-corenft-staking/src/instructions/unlock_corenft.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use anchor_lang::prelude::Clock; 3 | use mpl_core::{ 4 | ID as CORE_PROGRAM_ID, 5 | accounts::{BaseAssetV1, BaseCollectionV1}, 6 | instructions::{ RemovePluginV1CpiBuilder, UpdatePluginV1CpiBuilder}, 7 | types::{ FreezeDelegate, Plugin, PluginType, UpdateAuthority}, 8 | }; 9 | 10 | #[derive(Accounts)] 11 | pub struct UnlockCoreNFT<'info> { 12 | pub owner: Signer<'info>, 13 | 14 | #[account(mut)] 15 | pub user: Signer<'info>, 16 | 17 | #[account( 18 | mut, 19 | seeds = [GLOBAL_AUTHORITY_SEED], 20 | bump 21 | )] 22 | pub global_pool: Account<'info, GlobalPool>, 23 | 24 | 25 | #[account( 26 | mut, 27 | has_one = owner @ StakingError::InvalidAdmin, 28 | constraint = asset.update_authority == UpdateAuthority::Collection(collection.key()), 29 | )] 30 | pub asset: Account<'info, BaseAssetV1>, 31 | 32 | #[account( 33 | mut, 34 | )] 35 | pub collection: Account<'info, BaseCollectionV1>, 36 | 37 | #[account(address = CORE_PROGRAM_ID)] 38 | /// CHECK: this will be checked by core 39 | pub core_program: UncheckedAccount<'info>, 40 | pub system_program: Program<'info, System>, 41 | } 42 | 43 | pub fn process_instruction(ctx: Context) -> Result<()> { 44 | let global_pool = &mut ctx.accounts.global_pool; 45 | 46 | // Check payer is admin or owner 47 | if !ctx.accounts.user.key().eq(&ctx.accounts.owner.key()) { 48 | require!(global_pool.admin.eq(&ctx.accounts.user.key()), StakingError::InvalidAdmin); 49 | } 50 | 51 | // Unfreeze the asset 52 | UpdatePluginV1CpiBuilder::new(&ctx.accounts.core_program.to_account_info()) 53 | .asset(&ctx.accounts.asset.to_account_info()) 54 | .collection(Some(&ctx.accounts.collection.to_account_info())) 55 | .payer(&ctx.accounts.user.to_account_info()) 56 | .system_program(&ctx.accounts.system_program.to_account_info()) 57 | .plugin(Plugin::FreezeDelegate( FreezeDelegate{ frozen: false } )) 58 | .invoke()?; 59 | 60 | // Remove the FreezeDelegate Plugin 61 | RemovePluginV1CpiBuilder::new(&ctx.accounts.core_program) 62 | .asset(&ctx.accounts.asset.to_account_info()) 63 | .collection(Some(&ctx.accounts.collection.to_account_info())) 64 | .payer(&ctx.accounts.user) 65 | .system_program(&ctx.accounts.system_program) 66 | .plugin_type(PluginType::FreezeDelegate) 67 | .invoke()?; 68 | 69 | global_pool.total_corenft_staked_count -= 1; 70 | 71 | Ok(()) 72 | } -------------------------------------------------------------------------------- /cli/command.ts: -------------------------------------------------------------------------------- 1 | import { program } from 'commander'; 2 | import { PublicKey } from '@solana/web3.js'; 3 | import { 4 | lockCorenft, 5 | unlockCorenft, 6 | setClusterConfig, 7 | } from './scripts'; 8 | 9 | // program.version('0.0.1'); 10 | 11 | programCommand('init') 12 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 13 | .action(async (directory, cmd) => { 14 | const { env, keypair, rpc } = cmd.opts(); 15 | 16 | console.log('Solana Cluster:', env); 17 | console.log('Keypair Path:', keypair); 18 | console.log('RPC URL:', rpc); 19 | 20 | await setClusterConfig(env, keypair, rpc); 21 | 22 | await initProject(); 23 | }); 24 | 25 | programCommand('lock') 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | .option('-t, --nftType ') 28 | .option('-m, --mint ') 29 | .action(async (directory, cmd) => { 30 | const { env, keypair, rpc, mint } = cmd.opts(); 31 | 32 | await setClusterConfig(env, keypair, rpc); 33 | if (mint === undefined) { 34 | console.log('Error token amount Input'); 35 | return; 36 | } 37 | 38 | switch(nftType) { 39 | case "Corenft": { 40 | await await lockCorenft(mint, keypair); 41 | break; 42 | } 43 | default: { 44 | console.log('Nft Type is invalid'); 45 | return; 46 | } 47 | } 48 | 49 | 50 | }); 51 | 52 | programCommand('unlock') 53 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 54 | .option('-t, --nftType ') 55 | .option('-m, --mint ') 56 | .option('-o, --owner ') // nft owner address if force unstaking from admin 57 | .action(async (directory, cmd) => { 58 | const { env, keypair, rpc, mint, owner } = cmd.opts(); 59 | 60 | await setClusterConfig(env, keypair, rpc); 61 | if (mint === undefined) { 62 | console.log('Error token amount Input'); 63 | return; 64 | } 65 | 66 | switch(nftType) { 67 | case "Corenft": { 68 | await unlockCorenft( mint, !owner ? undefined : new PublicKey(owner), keypair); 69 | break; 70 | } 71 | default: { 72 | console.log('Mission Type is invalid'); 73 | return; 74 | } 75 | } 76 | }); 77 | 78 | function programCommand(name: string) { 79 | return ( 80 | program 81 | .command(name) 82 | .option('-e, --env ', 'Solana cluster env name', 'devnet') //mainnet-beta, testnet, devnet 83 | .option( 84 | '-r, --rpc ', 85 | 'Solana cluster RPC name', 86 | 'your rpc url' 87 | ) 88 | .option( 89 | '-k, --keypair ', 90 | 'Solana wallet Keypair Path', 91 | 'your keypair url' 92 | ) 93 | ); 94 | } 95 | 96 | program.parse(process.argv); 97 | -------------------------------------------------------------------------------- /cli/scripts.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import fs from "fs"; 3 | import NodeWallet from "@coral-xyz/anchor/dist/cjs/nodewallet"; 4 | import { PROGRAM_ID } from "../lib/constant"; 5 | import { 6 | ComputeBudgetProgram, 7 | Connection, 8 | Keypair, 9 | PublicKey, 10 | Transaction, 11 | } from "@solana/web3.js"; 12 | 13 | import { 14 | createInitializeTx, 15 | createLockCorenftTx, 16 | createUnlockCorenftTx, 17 | } from "../lib/scripts"; 18 | 19 | import idl from '../target/idl/mpl_corenft_pnft_staking.json'; 20 | import { MplCorenftPnftStaking } from '../target/types/mpl_corenft_pnft_staking'; 21 | 22 | const IDL: MplCorenftPnftStaking = idl as MplCorenftPnftStaking; 23 | 24 | let solConnection: Connection = null; 25 | let program: anchor.Program = null; 26 | let provider: anchor.Provider = null; 27 | let payer: NodeWallet = null; 28 | 29 | // Address of the deployed program. 30 | let programId = new anchor.web3.PublicKey(PROGRAM_ID); 31 | 32 | /** 33 | * Set cluster, provider, program 34 | * If rpc != null use rpc, otherwise use cluster param 35 | * @param cluster - cluster ex. mainnet-beta, devnet ... 36 | * @param keypair - wallet keypair 37 | * @param rpc - rpc 38 | */ 39 | export const setClusterConfig = async ( 40 | cluster: anchor.web3.Cluster, 41 | keypair: string, 42 | rpc?: string 43 | ) => { 44 | if (!rpc) { 45 | solConnection = new anchor.web3.Connection(anchor.web3.clusterApiUrl(cluster)); 46 | } else { 47 | solConnection = new anchor.web3.Connection(rpc); 48 | } 49 | 50 | const walletKeypair = Keypair.fromSecretKey( 51 | Uint8Array.from(JSON.parse(fs.readFileSync(keypair, "utf-8"))), 52 | { skipValidation: true } 53 | ); 54 | 55 | const wallet = new NodeWallet(walletKeypair); 56 | 57 | // Configure the client to use the local cluster. 58 | anchor.setProvider( 59 | new anchor.AnchorProvider(solConnection, wallet, { 60 | skipPreflight: false, 61 | commitment: "confirmed", 62 | }) 63 | ); 64 | payer = wallet; 65 | 66 | provider = anchor.getProvider(); 67 | console.log("Wallet Address: ", wallet.publicKey.toBase58()); 68 | 69 | // Generate the program client from IDL. 70 | console.log("Program ID: ", programId); 71 | program = new anchor.Program(IDL as anchor.Idl, provider); 72 | 73 | }; 74 | 75 | /** 76 | * Initialize global pool, vault 77 | */ 78 | export const initProject = async () => { 79 | try { 80 | const updateCpIx = ComputeBudgetProgram.setComputeUnitPrice({ 81 | microLamports: 5_000_000, 82 | }); 83 | const updateCuIx = ComputeBudgetProgram.setComputeUnitLimit({ 84 | units: 200_000, 85 | }); 86 | 87 | const tx = new Transaction().add( 88 | updateCpIx, 89 | updateCuIx, 90 | await createInitializeTx(payer.publicKey, program) 91 | ); 92 | const { blockhash, lastValidBlockHeight } = 93 | await solConnection.getLatestBlockhash(); 94 | tx.recentBlockhash = blockhash; 95 | tx.feePayer = payer.publicKey; 96 | 97 | console.dir(tx, { depth: null }); 98 | 99 | console.log(provider.publicKey.toBase58()); 100 | 101 | console.log(await solConnection.simulateTransaction(tx, [payer.payer])) 102 | const txId = await solConnection.sendTransaction(tx, [payer.payer], { 103 | preflightCommitment: "confirmed", 104 | }); 105 | 106 | console.log("txHash: ", txId); 107 | } catch (e) { 108 | console.log("error!!!!!!!!!"); 109 | console.log(e); 110 | } 111 | }; 112 | 113 | export const lockCorenft = async (asset: string, keypair: string) => { 114 | try { 115 | const tx = await createLockCorenftTx( 116 | payer as anchor.Wallet, 117 | asset, 118 | program, 119 | solConnection, 120 | keypair 121 | ); 122 | 123 | await addAdminSignAndConfirm(tx); 124 | 125 | } catch (e) { 126 | console.log(e); 127 | } 128 | }; 129 | 130 | export const unlockCorenft = async (asset: string, owner: PublicKey, keypair: string) => { 131 | try { 132 | const tx = await createUnlockCorenftTx( 133 | payer as anchor.Wallet, 134 | asset, 135 | program, 136 | solConnection, 137 | keypair, 138 | owner 139 | ); 140 | 141 | await addAdminSignAndConfirm(tx); 142 | } catch (e) { 143 | console.log(e); 144 | } 145 | }; 146 | 147 | 148 | export const addAdminSignAndConfirm = async (txData: Buffer) => { 149 | // Deserialize the transaction 150 | let tx = Transaction.from(txData); 151 | 152 | // Sign the transaction with admin's Keypair 153 | // tx = await adminWallet.signTransaction(tx); 154 | // console.log("signed admin: ", adminWallet.publicKey.toBase58()); 155 | 156 | const sTx = tx.serialize(); 157 | 158 | // Send the raw transaction 159 | const options = { 160 | commitment: 'confirmed', 161 | skipPreflight: false, 162 | }; 163 | // Confirm the transaction 164 | const signature = await solConnection.sendRawTransaction(sTx, options); 165 | await solConnection.confirmTransaction(signature, 'confirmed'); 166 | 167 | console.log('Transaction confirmed:', signature); 168 | }; 169 | -------------------------------------------------------------------------------- /lib/util.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as anchor from "@coral-xyz/anchor"; 3 | import { 4 | PublicKey, 5 | } from "@solana/web3.js"; 6 | import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"; 7 | 8 | export const METAPLEX = new PublicKey('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s'); 9 | export const MPL_DEFAULT_RULE_SET = new PublicKey( 10 | "AdH2Utn6Fus15ZhtenW4hZBQnvtLgM1YCW2MfVp7pYS5" 11 | ); 12 | 13 | const getAssociatedTokenAccount = async ( 14 | ownerPubkey: PublicKey, 15 | mintPk: PublicKey 16 | ): Promise => { 17 | let associatedTokenAccountPubkey = (PublicKey.findProgramAddressSync( 18 | [ 19 | ownerPubkey.toBuffer(), 20 | TOKEN_PROGRAM_ID.toBuffer(), 21 | mintPk.toBuffer(), // mint address 22 | ], 23 | ASSOCIATED_TOKEN_PROGRAM_ID 24 | ))[0]; 25 | 26 | return associatedTokenAccountPubkey; 27 | } 28 | 29 | const createAssociatedTokenAccountInstruction = ( 30 | associatedTokenAddress: anchor.web3.PublicKey, 31 | payer: anchor.web3.PublicKey, 32 | walletAddress: anchor.web3.PublicKey, 33 | splTokenMintAddress: anchor.web3.PublicKey 34 | ) => { 35 | const keys = [ 36 | { pubkey: payer, isSigner: true, isWritable: true }, 37 | { pubkey: associatedTokenAddress, isSigner: false, isWritable: true }, 38 | { pubkey: walletAddress, isSigner: false, isWritable: false }, 39 | { pubkey: splTokenMintAddress, isSigner: false, isWritable: false }, 40 | { 41 | pubkey: anchor.web3.SystemProgram.programId, 42 | isSigner: false, 43 | isWritable: false, 44 | }, 45 | { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, 46 | { 47 | pubkey: anchor.web3.SYSVAR_RENT_PUBKEY, 48 | isSigner: false, 49 | isWritable: false, 50 | }, 51 | ]; 52 | return new anchor.web3.TransactionInstruction({ 53 | keys, 54 | programId: ASSOCIATED_TOKEN_PROGRAM_ID, 55 | data: Buffer.from([]), 56 | }); 57 | } 58 | 59 | const getATokenAccountsNeedCreate = async ( 60 | connection: anchor.web3.Connection, 61 | walletAddress: anchor.web3.PublicKey, 62 | owner: anchor.web3.PublicKey, 63 | nfts: anchor.web3.PublicKey[], 64 | ) => { 65 | let instructions = [], destinationAccounts = []; 66 | for (const mint of nfts) { 67 | const destinationPubkey = await getAssociatedTokenAccount(owner, mint); 68 | let response = await connection.getAccountInfo(destinationPubkey); 69 | if (!response) { 70 | const createATAIx = createAssociatedTokenAccountInstruction( 71 | destinationPubkey, 72 | walletAddress, 73 | owner, 74 | mint, 75 | ); 76 | instructions.push(createATAIx); 77 | } 78 | destinationAccounts.push(destinationPubkey); 79 | if (walletAddress != owner) { 80 | const userAccount = await getAssociatedTokenAccount(walletAddress, mint); 81 | response = await connection.getAccountInfo(userAccount); 82 | if (!response) { 83 | const createATAIx = createAssociatedTokenAccountInstruction( 84 | userAccount, 85 | walletAddress, 86 | walletAddress, 87 | mint, 88 | ); 89 | instructions.push(createATAIx); 90 | } 91 | } 92 | } 93 | return { 94 | instructions, 95 | destinationAccounts, 96 | }; 97 | } 98 | 99 | /** Get metaplex mint metadata account address */ 100 | const getMetadata = async (mint: PublicKey): Promise => { 101 | return ( 102 | await PublicKey.findProgramAddress([Buffer.from('metadata'), METAPLEX.toBuffer(), mint.toBuffer()], METAPLEX) 103 | )[0]; 104 | }; 105 | 106 | const getMasterEdition = async ( 107 | mint: anchor.web3.PublicKey 108 | ): Promise => { 109 | return ( 110 | await anchor.web3.PublicKey.findProgramAddress( 111 | [ 112 | Buffer.from("metadata"), 113 | METAPLEX.toBuffer(), 114 | mint.toBuffer(), 115 | Buffer.from("edition"), 116 | ], 117 | METAPLEX 118 | ) 119 | )[0]; 120 | }; 121 | 122 | export function findTokenRecordPda( 123 | mint: PublicKey, 124 | token: PublicKey 125 | ): PublicKey { 126 | return PublicKey.findProgramAddressSync( 127 | [ 128 | Buffer.from("metadata"), 129 | METAPLEX.toBuffer(), 130 | mint.toBuffer(), 131 | Buffer.from("token_record"), 132 | token.toBuffer(), 133 | ], 134 | METAPLEX 135 | )[0]; 136 | } 137 | 138 | export function getUTCTimestamps(date: string) { //type: "2024-10-04T15:34:00" 139 | let d = new Date(); 140 | let offset = d.getTimezoneOffset(); 141 | let now = new Date(date).getTime()/1000; 142 | return now + offset * 60; 143 | } 144 | 145 | export { 146 | getAssociatedTokenAccount, 147 | getATokenAccountsNeedCreate, 148 | getMetadata, 149 | getMasterEdition 150 | } -------------------------------------------------------------------------------- /lib/scripts.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import fs from "fs"; 3 | import { 4 | PublicKey, 5 | Connection, 6 | SystemProgram, 7 | SYSVAR_INSTRUCTIONS_PUBKEY, 8 | SYSVAR_RENT_PUBKEY, 9 | Transaction as web3Transaction, 10 | } from "@solana/web3.js"; 11 | import { Wallet } from "@coral-xyz/anchor"; 12 | 13 | import { 14 | TOKEN_PROGRAM_ID, 15 | } from "@solana/spl-token"; 16 | import { PROGRAM_ID as TOKEN_AUTH_RULES_ID } from "@metaplex-foundation/mpl-token-auth-rules"; 17 | 18 | import { createUmi } from "@metaplex-foundation/umi-bundle-defaults"; 19 | import { 20 | createSignerFromKeypair, 21 | signerIdentity, 22 | publicKey 23 | } from "@metaplex-foundation/umi"; 24 | import { 25 | MPL_CORE_PROGRAM_ID, 26 | fetchAsset, 27 | } from "@metaplex-foundation/mpl-core"; 28 | 29 | import { 30 | METAPLEX, 31 | MPL_DEFAULT_RULE_SET, 32 | findTokenRecordPda, 33 | getAssociatedTokenAccount, 34 | getMasterEdition, 35 | getMetadata, 36 | getUTCTimestamps, 37 | } from "./util"; 38 | import { 39 | GLOBAL_AUTHORITY_SEED 40 | } from "./constant"; 41 | 42 | export const createInitializeTx = async ( 43 | admin: PublicKey, 44 | program: anchor.Program 45 | ) => { 46 | const [globalPool] = PublicKey.findProgramAddressSync( 47 | [Buffer.from(GLOBAL_AUTHORITY_SEED)], 48 | program.programId 49 | ); 50 | console.log("globalPool: ", globalPool.toBase58()); 51 | 52 | const tx = await program.methods 53 | .initialize() 54 | .accounts({ 55 | admin, 56 | globalPool, 57 | systemProgram: SystemProgram.programId, 58 | rent: SYSVAR_RENT_PUBKEY, 59 | }) 60 | .transaction(); 61 | 62 | return tx; 63 | }; 64 | 65 | 66 | export const createLockCorenftTx = async ( 67 | wallet: Wallet, 68 | assetStr: string, 69 | program: anchor.Program, 70 | connection: Connection, 71 | keypair: string 72 | ) => { 73 | const json = Uint8Array.from(JSON.parse(fs.readFileSync(keypair, "utf-8"))); 74 | const umi = createUmi("https://api.devnet.solana.com", "finalized"); 75 | 76 | let keyPair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(json)); 77 | const myKeypairSigner = createSignerFromKeypair(umi, keyPair); 78 | umi.use(signerIdentity(myKeypairSigner)); 79 | 80 | const asset = publicKey(assetStr); 81 | const collection = publicKey("CORE_COLLECTION_ADDRESS"); 82 | 83 | const assetData = await fetchAsset(umi, asset); 84 | 85 | if (assetData.updateAuthority.address != "CORE_COLLECTION_ADDRESS") { 86 | throw "collection is incorrect"; 87 | } 88 | 89 | if(!assetData.freezeDelegate){ 90 | 91 | const userAddress = wallet.publicKey; 92 | 93 | const tx = new web3Transaction(); 94 | 95 | const txId = await program.methods 96 | .lockCorenft() 97 | .accounts({ 98 | owner: userAddress, 99 | user: userAddress, 100 | asset: asset, 101 | collection: collection, 102 | coreProgram: MPL_CORE_PROGRAM_ID, 103 | systemProgram: SystemProgram.programId 104 | }) 105 | .transaction(); 106 | 107 | tx.add(txId); 108 | 109 | tx.feePayer = userAddress; 110 | tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 111 | 112 | const txData = await wallet.signTransaction(tx); 113 | 114 | return txData.serialize({ requireAllSignatures: false }); 115 | } else if(assetData.freezeDelegate.frozen) { 116 | throw "already staked"; 117 | } 118 | }; 119 | 120 | export const createUnlockCorenftTx = async ( 121 | wallet: Wallet, // Owner or admin 122 | assetStr: string, 123 | program: anchor.Program, 124 | connection: Connection, 125 | keypair: string, 126 | stakedSeed?: number // Seed finding may take long time 127 | ) => { 128 | const json = Uint8Array.from(JSON.parse(fs.readFileSync(keypair, "utf-8"))); 129 | const umi = createUmi("https://api.devnet.solana.com", "finalized"); 130 | 131 | let keyPair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(json)); 132 | const myKeypairSigner = createSignerFromKeypair(umi, keyPair); 133 | umi.use(signerIdentity(myKeypairSigner)); 134 | 135 | const asset = publicKey(assetStr); 136 | const assetData = await fetchAsset(umi, asset); 137 | const collection = publicKey("CORE_COLLECTION_ADDRESS"); 138 | 139 | if (assetData.updateAuthority.address != "CORE_COLLECTION_ADDRESS") { 140 | throw "collection is incorrect"; 141 | } 142 | 143 | if(!assetData.freezeDelegate){ 144 | throw "non staked mint"; 145 | } else { 146 | const userAddress = wallet.publicKey; 147 | 148 | const tx = new web3Transaction(); 149 | 150 | const txId = await program.methods 151 | .unlockMission1() 152 | .accounts({ 153 | owner: userAddress, 154 | user: userAddress, 155 | asset: asset, 156 | collection: collection, 157 | coreProgram: MPL_CORE_PROGRAM_ID, 158 | systemProgram: SystemProgram.programId 159 | }) 160 | .transaction(); 161 | 162 | tx.add(txId); 163 | 164 | tx.feePayer = userAddress; 165 | tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; 166 | 167 | const txData = await wallet.signTransaction(tx); 168 | 169 | return txData.serialize({ requireAllSignatures: false }); 170 | } 171 | }; 172 | --------------------------------------------------------------------------------