From b8ea9317487607f7b6eadfa80b49e81f4333d274 Mon Sep 17 00:00:00 2001 From: DR497 <47689875+dr497@users.noreply.github.com> Date: Mon, 8 Jan 2024 18:26:12 +0800 Subject: [PATCH] feat: add `getMultipleFavoriteDomains` --- js/src/favorite-domain.ts | 85 +++++++++++++++++++++++++++++++++++- js/src/nft/name-tokenizer.ts | 14 ++++-- js/src/resolve.ts | 10 ++--- js/src/utils.ts | 25 +++++++++++ js/tests/favorite.test.ts | 71 +++++++++++++++++++++--------- 5 files changed, 175 insertions(+), 30 deletions(-) diff --git a/js/src/favorite-domain.ts b/js/src/favorite-domain.ts index facf7c06..e0621851 100644 --- a/js/src/favorite-domain.ts +++ b/js/src/favorite-domain.ts @@ -1,9 +1,18 @@ import { Buffer } from "buffer"; import { deserialize, Schema } from "borsh"; -import { getReverseKeySync, reverseLookup } from "./utils"; +import { + deserializeReverse, + getReverseKeyFromDomainKey, + reverseLookup, +} from "./utils"; import { PublicKey, Connection } from "@solana/web3.js"; import { ErrorType, SNSError } from "./error"; import { resolve } from "./resolve"; +import { getDomainMint } from "./nft/name-tokenizer"; +import { + AccountLayout, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; export const NAME_OFFERS_ID = new PublicKey( "85iDfUvr3HJyLM2zcq5BXSiDvUWfw6cSE1FfNBo8Ap29", @@ -107,3 +116,77 @@ export const getFavoriteDomain = async ( stale: !owner.equals(domainOwner), }; }; + +/** + * This function can be used to retrieve the favorite domains for multiple wallets, up to a maximum of 100. + * If a wallet does not have a favorite domain, the result will be 'undefined' instead of the human readable domain as a string. + * This function is optimized for network efficiency, making only four RPC calls, three of which are executed in parallel using Promise.all, thereby reducing the overall execution time. + * @param connection The Solana RPC connection object + * @param wallets An array of PublicKeys representing the wallets + * @returns A promise that resolves to an array of strings or undefined, representing the favorite domains or lack thereof for each wallet + */ +export const getMultipleFavoriteDomains = async ( + connection: Connection, + wallets: PublicKey[], +): Promise<(string | undefined)[]> => { + const result: (string | undefined)[] = []; + + const favKeys = wallets.map( + (e) => FavouriteDomain.getKeySync(NAME_OFFERS_ID, e)[0], + ); + const favDomains = (await connection.getMultipleAccountsInfo(favKeys)).map( + (e) => { + if (!!e?.data) { + return FavouriteDomain.deserialize(e?.data).nameAccount; + } + return PublicKey.default; + }, + ); + const revKeys = favDomains.map((e) => getReverseKeyFromDomainKey(e)); + const atas = favDomains.map((e, idx) => { + const mint = getDomainMint(e); + const ata = getAssociatedTokenAddressSync(mint, wallets[idx], true); + return ata; + }); + + const [domainInfos, revs, tokenAccs] = await Promise.all([ + connection.getMultipleAccountsInfo(favDomains), + connection.getMultipleAccountsInfo(revKeys), + connection.getMultipleAccountsInfo(atas), + ]); + + for (let i = 0; i < wallets.length; i++) { + const domainInfo = domainInfos[i]; + const rev = revs[i]; + const tokenAcc = tokenAccs[i]; + + if (!domainInfo || !rev) { + result.push(undefined); + continue; + } + + const nativeOwner = new PublicKey(domainInfo?.data.slice(32, 64)); + + if (nativeOwner.equals(wallets[i])) { + result.push(deserializeReverse(rev?.data.slice(96))); + continue; + } + // Either tokenized or stale + if (!tokenAcc) { + result.push(undefined); + continue; + } + + const decoded = AccountLayout.decode(tokenAcc.data); + // Tokenized + if (Number(decoded.amount) === 1) { + result.push(deserializeReverse(rev?.data.slice(96))); + continue; + } + + // Stale + result.push(undefined); + } + + return result; +}; diff --git a/js/src/nft/name-tokenizer.ts b/js/src/nft/name-tokenizer.ts index 189df3f6..2a588e30 100644 --- a/js/src/nft/name-tokenizer.ts +++ b/js/src/nft/name-tokenizer.ts @@ -3,11 +3,19 @@ import { Connection, PublicKey } from "@solana/web3.js"; import { Buffer } from "buffer"; export const NAME_TOKENIZER_ID = new PublicKey( - "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk" + "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, @@ -66,7 +74,7 @@ export class NftRecord { static async findKey(nameAccount: PublicKey, programId: PublicKey) { return await PublicKey.findProgramAddress( [Buffer.from("nft_record"), nameAccount.toBuffer()], - programId + programId, ); } } @@ -80,7 +88,7 @@ export class NftRecord { */ export const getRecordFromMint = async ( connection: Connection, - mint: PublicKey + mint: PublicKey, ) => { const filters = [ { diff --git a/js/src/resolve.ts b/js/src/resolve.ts index c4616b47..cb1890cb 100644 --- a/js/src/resolve.ts +++ b/js/src/resolve.ts @@ -19,7 +19,7 @@ export const resolve = async (connection: Connection, domain: string) => { const { registry, nftOwner } = await NameRegistryState.retrieve( connection, - pubkey + pubkey, ); if (nftOwner) { @@ -33,7 +33,7 @@ export const resolve = async (connection: Connection, domain: string) => { const solV2Owner = await resolveSolRecordV2( connection, registry.owner, - domain + domain, ); if (solV2Owner !== undefined) { return solV2Owner; @@ -45,7 +45,7 @@ export const resolve = async (connection: Connection, domain: string) => { const solV1Owner = await resolveSolRecordV1( connection, registry.owner, - domain + domain, ); return solV1Owner; @@ -63,7 +63,7 @@ export const resolve = async (connection: Connection, domain: string) => { const resolveSolRecordV1 = async ( connection: Connection, owner: PublicKey, - domain: string + domain: string, ) => { const recordKey = getRecordKeySync(domain, Record.SOL); const solRecord = await getSolRecord(connection, domain); @@ -90,7 +90,7 @@ const resolveSolRecordV1 = async ( const resolveSolRecordV2 = async ( connection: Connection, owner: PublicKey, - domain: string + domain: string, ) => { try { const recordV2Key = getRecordV2Key(domain, Record.SOL); diff --git a/js/src/utils.ts b/js/src/utils.ts index f8f8380c..21dd8e33 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -280,6 +280,25 @@ export const getReverseKeySync = (domain: string, isSub?: boolean) => { return reverseLookupAccount; }; +/** + * This function can be used to get the reverse key from a domain key + * @param domainKey The domain key to compute the reverse for + * @param parent The parent public key + * @returns The public key of the reverse account + */ +export const getReverseKeyFromDomainKey = ( + domainKey: PublicKey, + parent?: PublicKey, +) => { + const hashedReverseLookup = getHashedNameSync(domainKey.toBase58()); + const reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + REVERSE_LOOKUP_CLASS, + parent, + ); + return reverseLookupAccount; +}; + export const check = (bool: boolean, errorType: ErrorType) => { if (!bool) { throw new SNSError(errorType); @@ -336,3 +355,9 @@ export const getDomainPriceFromName = (name: string) => { return 20; } }; + +export const deserializeReverse = (data: Buffer | undefined) => { + if (!data) return undefined; + const nameLength = new BN(data.slice(0, 4), "le").toNumber(); + return data.slice(4, 4 + nameLength).toString(); +}; diff --git a/js/tests/favorite.test.ts b/js/tests/favorite.test.ts index 1e26441f..9dea0a70 100644 --- a/js/tests/favorite.test.ts +++ b/js/tests/favorite.test.ts @@ -1,32 +1,34 @@ require("dotenv").config(); import { test, expect, jest } from "@jest/globals"; -import { getFavoriteDomain } from "../src/favorite-domain"; -import { PublicKey, Connection } from "@solana/web3.js"; +import { + getFavoriteDomain, + getMultipleFavoriteDomains, +} from "../src/favorite-domain"; +import { PublicKey, Connection, Keypair } from "@solana/web3.js"; jest.setTimeout(10_000); -const items = [ - { - user: new PublicKey("FidaeBkZkvDqi1GXNEwB8uWmj9Ngx2HXSS5nyGRuVFcZ"), - favorite: { - domain: new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb"), - reverse: "bonfida", - stale: true, - }, - }, - { - user: new PublicKey("HKKp49qGWXd639QsuH7JiLijfVW5UtCVY4s1n2HANwEA"), - favorite: { - domain: new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb"), - reverse: "bonfida", - stale: false, - }, - }, -]; - const connection = new Connection(process.env.RPC_URL!); test("Favorite domain", async () => { + const items = [ + { + user: new PublicKey("FidaeBkZkvDqi1GXNEwB8uWmj9Ngx2HXSS5nyGRuVFcZ"), + favorite: { + domain: new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb"), + reverse: "bonfida", + stale: true, + }, + }, + { + user: new PublicKey("HKKp49qGWXd639QsuH7JiLijfVW5UtCVY4s1n2HANwEA"), + favorite: { + domain: new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb"), + reverse: "bonfida", + stale: false, + }, + }, + ]; for (let item of items) { const fav = await getFavoriteDomain(connection, item.user); @@ -35,3 +37,30 @@ test("Favorite domain", async () => { expect(fav.stale).toBe(item.favorite.stale); } }); + +test("Multiple favorite domains", async () => { + const items = [ + // Non tokenized + { + wallet: new PublicKey("HKKp49qGWXd639QsuH7JiLijfVW5UtCVY4s1n2HANwEA"), + domain: "bonfida", + }, + // Stale non tokenized + { + wallet: new PublicKey("FidaeBkZkvDqi1GXNEwB8uWmj9Ngx2HXSS5nyGRuVFcZ"), + domain: undefined, + }, + // Random pubkey + { wallet: Keypair.generate().publicKey, domain: undefined }, + // Tokenized + { + wallet: new PublicKey("36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4"), + domain: "fav-tokenized", + }, + ]; + const result = await getMultipleFavoriteDomains( + connection, + items.map((e) => e.wallet), + ); + result.forEach((x, idx) => expect(x).toBe(items[idx].domain)); +});