From 57c35d2bd059a6eba24df76f2d644b353ac90820 Mon Sep 17 00:00:00 2001 From: DR497 <47689875+dr497@users.noreply.github.com> Date: Thu, 30 May 2024 12:54:22 +0800 Subject: [PATCH] js: split nft bindings --- js/rollup.config.mjs | 7 +- js/src/favorite-domain.ts | 2 +- js/src/index.ts | 11 +- js/src/nft/const.ts | 8 + js/src/nft/getDomainMint.ts | 10 ++ js/src/nft/getRecordFromMint.ts | 41 +++++ js/src/nft/index.ts | 165 --------------------- js/src/nft/retrieveNftOwner.ts | 56 +++++++ js/src/nft/retrieveNftOwnerV2.ts | 37 +++++ js/src/nft/retrieveNfts.ts | 33 +++++ js/src/nft/retrieveRecords.ts | 46 ++++++ js/src/nft/{name-tokenizer.ts => state.ts} | 60 +------- js/src/state.ts | 2 +- js/src/utils/getTokenizedDomains.ts | 2 +- js/tests/nft.test.ts | 2 +- js/tests/reverse.test.ts | 2 +- js/tsconfig.json | 2 +- 17 files changed, 257 insertions(+), 229 deletions(-) create mode 100644 js/src/nft/const.ts create mode 100644 js/src/nft/getDomainMint.ts create mode 100644 js/src/nft/getRecordFromMint.ts delete mode 100644 js/src/nft/index.ts create mode 100644 js/src/nft/retrieveNftOwner.ts create mode 100644 js/src/nft/retrieveNftOwnerV2.ts create mode 100644 js/src/nft/retrieveNfts.ts create mode 100644 js/src/nft/retrieveRecords.ts rename js/src/nft/{name-tokenizer.ts => state.ts} (59%) diff --git a/js/rollup.config.mjs b/js/rollup.config.mjs index 1a2003c8..73766ebb 100644 --- a/js/rollup.config.mjs +++ b/js/rollup.config.mjs @@ -6,7 +6,7 @@ import { nodeResolve } from "@rollup/plugin-node-resolve"; import replace from "@rollup/plugin-replace"; import babel from "@rollup/plugin-babel"; import { visualizer } from "rollup-plugin-visualizer"; -import multiInput from 'rollup-plugin-multi-input'; +import multiInput from "rollup-plugin-multi-input"; /** * @type {import('rollup').RollupOptions} @@ -19,6 +19,7 @@ export default { "src/twitter/**/*.ts", "src/resolve/**/*.ts", "src/record/**/*.ts", + "src/nft/**/*.ts", "src/bindings/**/*.ts", "src/instructions/**/*.ts", ], @@ -28,8 +29,8 @@ export default { dir: "dist/", format: "esm", sourcemap: true, - entryFileNames: '[name].mjs', - exports: "named" + entryFileNames: "[name].mjs", + exports: "named", }, { dir: "dist/", format: "cjs", sourcemap: true }, ], diff --git a/js/src/favorite-domain.ts b/js/src/favorite-domain.ts index e42329d2..2e758e69 100644 --- a/js/src/favorite-domain.ts +++ b/js/src/favorite-domain.ts @@ -9,7 +9,7 @@ import { deserializeReverse } from "./utils/deserializeReverse"; import { getReverseKeyFromDomainKey } from "./utils/getReverseKeyFromDomainKey"; import { reverseLookup } from "./utils/reverseLookup"; import { FavouriteDomainNotFoundError } from "./error"; -import { getDomainMint } from "./nft/name-tokenizer"; +import { getDomainMint } from "./nft/getDomainMint"; import { NameRegistryState } from "./state"; export const NAME_OFFERS_ID = new PublicKey( diff --git a/js/src/index.ts b/js/src/index.ts index d3978204..168cfef7 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -66,8 +66,15 @@ export * from "./instructions/transferInstruction"; export * from "./instructions/updateInstruction"; export * from "./instructions/types"; -export * from "./nft"; -export { getDomainMint } from "./nft/name-tokenizer"; +export * from "./nft/getDomainMint"; +export * from "./nft/retrieveNftOwnerV2"; +export * from "./nft/retrieveNftOwner"; +export * from "./nft/retrieveNfts"; +export * from "./nft/getRecordFromMint"; +export * from "./nft/retrieveRecords"; +export * from "./nft/const"; +export * from "./nft/state"; + export * from "./favorite-domain"; export * from "./constants"; export * from "./int"; diff --git a/js/src/nft/const.ts b/js/src/nft/const.ts new file mode 100644 index 00000000..33f722a2 --- /dev/null +++ b/js/src/nft/const.ts @@ -0,0 +1,8 @@ +import { PublicKey } from "@solana/web3.js"; +import { Buffer } from "buffer"; + +export const NAME_TOKENIZER_ID = new PublicKey( + "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk", +); + +export const MINT_PREFIX = Buffer.from("tokenized_name"); diff --git a/js/src/nft/getDomainMint.ts b/js/src/nft/getDomainMint.ts new file mode 100644 index 00000000..ee978b08 --- /dev/null +++ b/js/src/nft/getDomainMint.ts @@ -0,0 +1,10 @@ +import { PublicKey } from "@solana/web3.js"; +import { MINT_PREFIX, NAME_TOKENIZER_ID } from "./const"; + +export const getDomainMint = (domain: PublicKey) => { + const [mint] = PublicKey.findProgramAddressSync( + [MINT_PREFIX, domain.toBuffer()], + NAME_TOKENIZER_ID, + ); + return mint; +}; diff --git a/js/src/nft/getRecordFromMint.ts b/js/src/nft/getRecordFromMint.ts new file mode 100644 index 00000000..e2bf89f2 --- /dev/null +++ b/js/src/nft/getRecordFromMint.ts @@ -0,0 +1,41 @@ +import { + Connection, + PublicKey, + GetProgramAccountsFilter, +} from "@solana/web3.js"; +import { NAME_TOKENIZER_ID } from "./const"; +import { NftRecord } from "./state"; + +/** + * This function can be used to retrieve a NFT Record given a mint + * + * @param connection A solana RPC connection + * @param mint The mint of the NFT Record + * @returns + */ +export const getRecordFromMint = async ( + connection: Connection, + mint: PublicKey, +) => { + const filters: GetProgramAccountsFilter[] = [ + { dataSize: NftRecord.LEN }, + { + memcmp: { + offset: 0, + bytes: "3", + }, + }, + { + memcmp: { + offset: 1 + 1 + 32 + 32, + bytes: mint.toBase58(), + }, + }, + ]; + + const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { + filters, + }); + + return result; +}; diff --git a/js/src/nft/index.ts b/js/src/nft/index.ts deleted file mode 100644 index 1c997e56..00000000 --- a/js/src/nft/index.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { - PublicKey, - Connection, - GetProgramAccountsFilter, - MemcmpFilter, - SolanaJSONRPCError, -} from "@solana/web3.js"; -import { - getMint, - TOKEN_PROGRAM_ID, - RawAccount, - AccountLayout, -} from "@solana/spl-token"; -import { - NAME_TOKENIZER_ID, - NftRecord, - getRecordFromMint, - getDomainMint, -} from "./name-tokenizer"; - -export const retrieveNftOwnerV2 = async ( - connection: Connection, - nameAccount: PublicKey, -) => { - try { - const mint = getDomainMint(nameAccount); - - const largestAccounts = await connection.getTokenLargestAccounts(mint); - if (largestAccounts.value.length === 0) { - return null; - } - - const largestAccountInfo = await connection.getAccountInfo( - largestAccounts.value[0].address, - ); - - if (!largestAccountInfo) { - return null; - } - - const decoded = AccountLayout.decode(largestAccountInfo.data); - if (decoded.amount.toString() === "1") { - return decoded.owner; - } - return null; - } catch (err) { - if (err instanceof SolanaJSONRPCError && err.code === -32602) { - // Mint does not exist - return null; - } - throw err; - } -}; - -/** - * This function can be used to retrieve the owner of a tokenized domain name - * - * @param connection The solana connection object to the RPC node - * @param nameAccount The key of the domain name - * @returns - */ -export const retrieveNftOwner = async ( - connection: Connection, - nameAccount: PublicKey, -) => { - try { - const mint = getDomainMint(nameAccount); - - const mintInfo = await getMint(connection, mint); - if (mintInfo.supply.toString() === "0") { - return undefined; - } - - const filters: GetProgramAccountsFilter[] = [ - { - memcmp: { - offset: 0, - bytes: mint.toBase58(), - }, - }, - { - memcmp: { - offset: 64, - bytes: "2", - }, - }, - { dataSize: 165 }, - ]; - - const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { - filters, - }); - - if (result.length != 1) { - return undefined; - } - - return new PublicKey(result[0].account.data.slice(32, 64)); - } catch { - return undefined; - } -}; - -/** - * This function can be used to retrieve all the tokenized domains name - * - * @param connection The solana connection object to the RPC node - * @returns - */ -export const retrieveNfts = async (connection: Connection) => { - const filters: GetProgramAccountsFilter[] = [ - { dataSize: NftRecord.LEN }, - { - memcmp: { - offset: 0, - bytes: "3", - }, - }, - ]; - - const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { - filters, - }); - const offset = 1 + 1 + 32 + 32; - return result.map( - (e) => new PublicKey(e.account.data.slice(offset, offset + 32)), - ); -}; - -const getFilter = (owner: string) => { - const filters: MemcmpFilter[] = [ - { - memcmp: { offset: 32, bytes: owner }, - }, - { memcmp: { offset: 64, bytes: "2" } }, - ]; - return filters; -}; - -const closure = async (connection: Connection, acc: RawAccount) => { - const record = await getRecordFromMint(connection, acc.mint); - if (record.length === 1) { - return NftRecord.deserialize(record[0].account.data); - } -}; - -export const retrieveRecords = async ( - connection: Connection, - owner: PublicKey, -) => { - const filters: GetProgramAccountsFilter[] = [ - ...getFilter(owner.toBase58()), - { dataSize: 165 }, - ]; - const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { - filters, - }); - - const tokenAccs = result.map((e) => AccountLayout.decode(e.account.data)); - - const promises = tokenAccs.map((acc) => closure(connection, acc)); - const records = await Promise.all(promises); - - return records.filter((e) => e !== undefined) as NftRecord[]; -}; diff --git a/js/src/nft/retrieveNftOwner.ts b/js/src/nft/retrieveNftOwner.ts new file mode 100644 index 00000000..f0b56af5 --- /dev/null +++ b/js/src/nft/retrieveNftOwner.ts @@ -0,0 +1,56 @@ +import { + Connection, + GetProgramAccountsFilter, + PublicKey, +} from "@solana/web3.js"; +import { getDomainMint } from "./getDomainMint"; +import { TOKEN_PROGRAM_ID, getMint } from "@solana/spl-token"; + +/** + * This function can be used to retrieve the owner of a tokenized domain name + * + * @param connection The solana connection object to the RPC node + * @param nameAccount The key of the domain name + * @returns + */ +export const retrieveNftOwner = async ( + connection: Connection, + nameAccount: PublicKey, +) => { + try { + const mint = getDomainMint(nameAccount); + + const mintInfo = await getMint(connection, mint); + if (mintInfo.supply.toString() === "0") { + return undefined; + } + + const filters: GetProgramAccountsFilter[] = [ + { + memcmp: { + offset: 0, + bytes: mint.toBase58(), + }, + }, + { + memcmp: { + offset: 64, + bytes: "2", + }, + }, + { dataSize: 165 }, + ]; + + const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { + filters, + }); + + if (result.length != 1) { + return undefined; + } + + return new PublicKey(result[0].account.data.slice(32, 64)); + } catch { + return undefined; + } +}; diff --git a/js/src/nft/retrieveNftOwnerV2.ts b/js/src/nft/retrieveNftOwnerV2.ts new file mode 100644 index 00000000..2d1b64b1 --- /dev/null +++ b/js/src/nft/retrieveNftOwnerV2.ts @@ -0,0 +1,37 @@ +import { PublicKey, Connection, SolanaJSONRPCError } from "@solana/web3.js"; +import { getDomainMint } from "./getDomainMint"; +import { AccountLayout } from "@solana/spl-token"; + +export const retrieveNftOwnerV2 = async ( + connection: Connection, + nameAccount: PublicKey, +) => { + try { + const mint = getDomainMint(nameAccount); + + const largestAccounts = await connection.getTokenLargestAccounts(mint); + if (largestAccounts.value.length === 0) { + return null; + } + + const largestAccountInfo = await connection.getAccountInfo( + largestAccounts.value[0].address, + ); + + if (!largestAccountInfo) { + return null; + } + + const decoded = AccountLayout.decode(largestAccountInfo.data); + if (decoded.amount.toString() === "1") { + return decoded.owner; + } + return null; + } catch (err) { + if (err instanceof SolanaJSONRPCError && err.code === -32602) { + // Mint does not exist + return null; + } + throw err; + } +}; diff --git a/js/src/nft/retrieveNfts.ts b/js/src/nft/retrieveNfts.ts new file mode 100644 index 00000000..f82688d1 --- /dev/null +++ b/js/src/nft/retrieveNfts.ts @@ -0,0 +1,33 @@ +import { + Connection, + GetProgramAccountsFilter, + PublicKey, +} from "@solana/web3.js"; +import { NAME_TOKENIZER_ID } from "./const"; +import { NftRecord } from "./state"; + +/** + * This function can be used to retrieve all the tokenized domains name + * + * @param connection The solana connection object to the RPC node + * @returns + */ +export const retrieveNfts = async (connection: Connection) => { + const filters: GetProgramAccountsFilter[] = [ + { dataSize: NftRecord.LEN }, + { + memcmp: { + offset: 0, + bytes: "3", + }, + }, + ]; + + const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { + filters, + }); + const offset = 1 + 1 + 32 + 32; + return result.map( + (e) => new PublicKey(e.account.data.slice(offset, offset + 32)), + ); +}; diff --git a/js/src/nft/retrieveRecords.ts b/js/src/nft/retrieveRecords.ts new file mode 100644 index 00000000..a8e9bb5d --- /dev/null +++ b/js/src/nft/retrieveRecords.ts @@ -0,0 +1,46 @@ +import { + PublicKey, + Connection, + GetProgramAccountsFilter, + MemcmpFilter, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID, RawAccount, AccountLayout } from "@solana/spl-token"; +import { NftRecord } from "./state"; +import { getRecordFromMint } from "./getRecordFromMint"; + +const getFilter = (owner: string) => { + const filters: MemcmpFilter[] = [ + { + memcmp: { offset: 32, bytes: owner }, + }, + { memcmp: { offset: 64, bytes: "2" } }, + ]; + return filters; +}; + +const closure = async (connection: Connection, acc: RawAccount) => { + const record = await getRecordFromMint(connection, acc.mint); + if (record.length === 1) { + return NftRecord.deserialize(record[0].account.data); + } +}; + +export const retrieveRecords = async ( + connection: Connection, + owner: PublicKey, +) => { + const filters: GetProgramAccountsFilter[] = [ + ...getFilter(owner.toBase58()), + { dataSize: 165 }, + ]; + const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { + filters, + }); + + const tokenAccs = result.map((e) => AccountLayout.decode(e.account.data)); + + const promises = tokenAccs.map((acc) => closure(connection, acc)); + const records = await Promise.all(promises); + + return records.filter((e) => e !== undefined) as NftRecord[]; +}; diff --git a/js/src/nft/name-tokenizer.ts b/js/src/nft/state.ts similarity index 59% rename from js/src/nft/name-tokenizer.ts rename to js/src/nft/state.ts index 5ef5b6c2..da3d887d 100644 --- a/js/src/nft/name-tokenizer.ts +++ b/js/src/nft/state.ts @@ -1,26 +1,8 @@ import { deserialize } from "borsh"; -import { - Connection, - GetProgramAccountsFilter, - PublicKey, -} from "@solana/web3.js"; +import { Connection, PublicKey } from "@solana/web3.js"; import { Buffer } from "buffer"; import { NftRecordNotFoundError } from "../error"; -export const NAME_TOKENIZER_ID = new PublicKey( - "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk", -); - -export const MINT_PREFIX = Buffer.from("tokenized_name"); - -export const getDomainMint = (domain: PublicKey) => { - const [mint] = PublicKey.findProgramAddressSync( - [MINT_PREFIX, domain.toBuffer()], - NAME_TOKENIZER_ID, - ); - return mint; -}; - export enum Tag { Uninitialized = 0, CentralState = 1, @@ -80,38 +62,10 @@ export class NftRecord { programId, ); } + static findKeySync(nameAccount: PublicKey, programId: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("nft_record"), nameAccount.toBuffer()], + programId, + ); + } } - -/** - * This function can be used to retrieve a NFT Record given a mint - * - * @param connection A solana RPC connection - * @param mint The mint of the NFT Record - * @returns - */ -export const getRecordFromMint = async ( - connection: Connection, - mint: PublicKey, -) => { - const filters: GetProgramAccountsFilter[] = [ - { dataSize: NftRecord.LEN }, - { - memcmp: { - offset: 0, - bytes: "3", - }, - }, - { - memcmp: { - offset: 1 + 1 + 32 + 32, - bytes: mint.toBase58(), - }, - }, - ]; - - const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { - filters, - }); - - return result; -}; diff --git a/js/src/state.ts b/js/src/state.ts index f303890c..86770b37 100644 --- a/js/src/state.ts +++ b/js/src/state.ts @@ -1,5 +1,5 @@ import { Connection, PublicKey } from "@solana/web3.js"; -import { retrieveNftOwnerV2 } from "./nft"; +import { retrieveNftOwnerV2 } from "./nft/retrieveNftOwnerV2"; import { Buffer } from "buffer"; import { deserialize } from "borsh"; import { AccountDoesNotExistError } from "./error"; diff --git a/js/src/utils/getTokenizedDomains.ts b/js/src/utils/getTokenizedDomains.ts index 531eb5d1..8371600e 100644 --- a/js/src/utils/getTokenizedDomains.ts +++ b/js/src/utils/getTokenizedDomains.ts @@ -1,5 +1,5 @@ import { Connection, PublicKey } from "@solana/web3.js"; -import { retrieveRecords } from "../nft"; +import { retrieveRecords } from "../nft/retrieveRecords"; import { reverseLookupBatch } from "./reverseLookupBatch"; /** diff --git a/js/tests/nft.test.ts b/js/tests/nft.test.ts index 8670a992..54eee605 100644 --- a/js/tests/nft.test.ts +++ b/js/tests/nft.test.ts @@ -3,7 +3,7 @@ import { test, jest } from "@jest/globals"; import { PublicKey, Connection } from "@solana/web3.js"; import { getDomainKeySync } from "../src/utils/getDomainKeySync"; import { getTokenizedDomains } from "../src/utils/getTokenizedDomains"; -import { getDomainMint } from "../src/nft/name-tokenizer"; +import { getDomainMint } from "../src/nft/getDomainMint"; jest.setTimeout(10_000); const connection = new Connection(process.env.RPC_URL!); diff --git a/js/tests/reverse.test.ts b/js/tests/reverse.test.ts index d3016108..9587bb27 100644 --- a/js/tests/reverse.test.ts +++ b/js/tests/reverse.test.ts @@ -5,7 +5,7 @@ import { createSubdomain } from "../src/bindings/createSubdomain"; import { VAULT_OWNER } from "../src/constants"; import { resolve } from "../src/resolve/resolve"; -jest.setTimeout(5_000); +jest.setTimeout(50_000); const connection = new Connection(process.env.RPC_URL!); diff --git a/js/tsconfig.json b/js/tsconfig.json index 73ae0d60..f42a8e75 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -33,7 +33,7 @@ "src/*", "src/.ts", "src/deprecated/.ts", - "src/nft/index.ts", + "src/nft/retrieveRecords.ts", "src/record_v2/index.ts" ], "exclude": ["src/**/*.test.ts", "**/node_modules", "dist", "benchmarks"]