Skip to content

Commit

Permalink
feat: add getMultipleFavoriteDomains
Browse files Browse the repository at this point in the history
  • Loading branch information
dr497 committed Jan 8, 2024
1 parent 1dc20b8 commit 4ed8e2e
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 30 deletions.
85 changes: 84 additions & 1 deletion js/src/favorite-domain.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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;
};
14 changes: 11 additions & 3 deletions js/src/nft/name-tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
);
}
}
Expand All @@ -80,7 +88,7 @@ export class NftRecord {
*/
export const getRecordFromMint = async (
connection: Connection,
mint: PublicKey
mint: PublicKey,
) => {
const filters = [
{
Expand Down
10 changes: 5 additions & 5 deletions js/src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const resolve = async (connection: Connection, domain: string) => {

const { registry, nftOwner } = await NameRegistryState.retrieve(
connection,
pubkey
pubkey,
);

if (nftOwner) {
Expand All @@ -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;
Expand All @@ -45,7 +45,7 @@ export const resolve = async (connection: Connection, domain: string) => {
const solV1Owner = await resolveSolRecordV1(
connection,
registry.owner,
domain
domain,
);

return solV1Owner;
Expand All @@ -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);
Expand All @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions js/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
};
71 changes: 50 additions & 21 deletions js/tests/favorite.test.ts
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -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));
});

0 comments on commit 4ed8e2e

Please sign in to comment.