Created
November 22, 2024 06:36
-
-
Save crypt0miester/c0b75b6c7fd2c7394d24ad354f077212 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @module all-domain-service | |
* utility functions for managing svm domain names | |
*/ | |
import { | |
ANS_PROGRAM_ID, | |
TLD_HOUSE_PROGRAM_ID, | |
getHashedName, | |
NameRecordHeader, | |
getNameAccountKeyWithBump, | |
} from "@onsol/tldparser"; | |
import { | |
AccountInfo, | |
Connection, | |
GetProgramAccountsResponse, | |
PublicKey, | |
} from "@solana/web3.js"; | |
import { BN } from "bn.js"; | |
import pLimit from "p-limit"; | |
// types for improved type safety | |
export interface TldHouseData { | |
pubkey?: PublicKey; | |
tld?: string; | |
parentKey?: PublicKey; | |
} | |
export interface NameRecordWithTldData { | |
nameAccount: PublicKey; | |
domain: string | undefined; | |
tldHouse: PublicKey | undefined; | |
parentName: PublicKey | undefined; | |
tld: string | undefined; | |
owner: PublicKey | undefined; | |
expiresAt: Date; | |
createdAt: Date; | |
nonTransferable: boolean; | |
name: string | undefined; | |
} | |
/** | |
* extracts tld from tld house account data | |
* @param tldHouseData - raw account info buffer | |
* @returns extracted tld string | |
*/ | |
export function getTldFromTldHouseAccountInfo( | |
tldHouseData: AccountInfo<Buffer>, | |
): string { | |
// offset past discriminator and keys | |
const tldStart = 8 + 32 + 32 + 32; | |
const tldBuffer = tldHouseData?.data?.subarray(tldStart); | |
const nameLength = new BN(tldBuffer?.subarray(0, 4), "le").toNumber(); | |
return tldBuffer | |
.subarray(4, 4 + nameLength) | |
.toString() | |
.replace(/\0.*$/g, ""); | |
} | |
/** | |
* extracts parent account from tld house data | |
* @param tldHouseData - raw account info buffer | |
* @returns parent account public key | |
*/ | |
export function getParentAccountFromTldHouseAccountInfo( | |
tldHouseData: AccountInfo<Buffer>, | |
): PublicKey { | |
const parentAccountStart = 8 + 32 + 32; | |
const parentAccountBuffer = tldHouseData?.data?.subarray( | |
parentAccountStart, | |
parentAccountStart + 32, | |
); | |
return new PublicKey(parentAccountBuffer); | |
} | |
/** | |
* fetches all tld houses from the solana network | |
* @param connection - solana connection instance | |
* @returns array of tld house data | |
*/ | |
export const getAllTldHouses = async ( | |
connection: Connection, | |
): Promise<TldHouseData[]> => { | |
// filter for tld house program accounts | |
const filters = [ | |
{ | |
memcmp: { | |
offset: 0, | |
bytes: "iQgos3SdaVE", | |
}, | |
}, | |
]; | |
const allTldHouses = await connection.getProgramAccounts( | |
TLD_HOUSE_PROGRAM_ID, | |
{ filters }, | |
); | |
return allTldHouses.map((data) => { | |
try { | |
const tld = getTldFromTldHouseAccountInfo(data.account); | |
const parentKey = getParentAccountFromTldHouseAccountInfo(data.account); | |
return { pubkey: data.pubkey, tld, parentKey }; | |
} catch (error) { | |
console.debug( | |
"failed to parse tld house data for", | |
data.pubkey.toString(), | |
); | |
return { pubkey: undefined, tld: undefined, parentKey: undefined }; | |
} | |
}); | |
}; | |
/** | |
* combines name accounts with their corresponding tld house data | |
* @param nameAccounts - raw name account data | |
* @param allTldHousesData - processed tld house data | |
* @returns combined name records with tld data | |
*/ | |
const insertTldHouseIntoData = ( | |
nameAccounts: GetProgramAccountsResponse, | |
allTldHousesData: TldHouseData[], | |
): (NameRecordWithTldData | undefined)[] => { | |
const allNameAccountsWithData = nameAccounts.map((account) => ({ | |
pubkey: account.pubkey, | |
nameRecordData: NameRecordHeader.fromAccountInfo(account.account), | |
})); | |
return allNameAccountsWithData | |
.map((nameAccount, index) => { | |
// find matching tld house by parent key | |
const tldHouseData = allTldHousesData.find( | |
(tldHouse) => | |
tldHouse.parentKey?.toString() === | |
nameAccount.nameRecordData.parentName.toString(), | |
); | |
if ( | |
tldHouseData && | |
isDomainValid(nameAccount.nameRecordData.expiresAt) | |
) { | |
return { | |
nameAccount: nameAccounts[index].pubkey, | |
tldHouse: tldHouseData.pubkey, | |
domain: undefined, | |
parentName: tldHouseData.parentKey, | |
tld: tldHouseData.tld, | |
owner: nameAccount.nameRecordData.owner, | |
expiresAt: nameAccount.nameRecordData.expiresAt, | |
createdAt: nameAccount.nameRecordData.createdAt, | |
nonTransferable: nameAccount.nameRecordData.nonTransferable, | |
name: undefined, | |
}; | |
} | |
return undefined; | |
}) | |
.filter(Boolean); | |
}; | |
/** | |
* performs batched reverse lookup for domain names | |
* @param connection - solana connection instance | |
* @param nameAccountsWithData - name records with tld data | |
* @returns enriched name records with domain names | |
*/ | |
export async function performReverseLookupBatchedTurbo( | |
connection: Connection, | |
nameAccountsWithData: (NameRecordWithTldData | undefined)[], | |
): Promise<(NameRecordWithTldData | undefined)[]> { | |
// helper to get reverse lookup accounts | |
const getReverseLookUpAccounts = async ( | |
accounts: (NameRecordWithTldData | undefined)[], | |
): Promise<(PublicKey | undefined)[]> => { | |
const promises = accounts.map(async (account) => { | |
if (!account) return; | |
const reverseLookupHashedName = await getHashedName( | |
account.nameAccount.toBase58(), | |
); | |
const [reverseLookUpAccount] = getNameAccountKeyWithBump( | |
reverseLookupHashedName, | |
account.tldHouse, | |
undefined, | |
); | |
return reverseLookUpAccount; | |
}); | |
return Promise.all(promises); | |
}; | |
// process in batches of 100 | |
const batches = []; | |
for (let i = 0; i < nameAccountsWithData.length; i += 100) { | |
batches.push(nameAccountsWithData.slice(i, i + 100)); | |
} | |
// limit concurrent requests | |
const limit = pLimit(20); | |
const batchResults = await Promise.all( | |
batches.map((batch) => | |
limit(async () => { | |
const reverseLookUpAccounts = await getReverseLookUpAccounts(batch); | |
if (reverseLookUpAccounts.includes(undefined)) { | |
throw new Error("invalid reverse lookup accounts"); | |
} | |
const reverseLookupAccountInfos = | |
await connection.getMultipleAccountsInfo( | |
reverseLookUpAccounts as PublicKey[], | |
); | |
return reverseLookupAccountInfos.map((info) => | |
info?.data.subarray(200, info.data.length).toString(), | |
); | |
}), | |
), | |
); | |
const reverseLookupDomains = batchResults.flat(); | |
// enrich name accounts with domain data | |
return nameAccountsWithData.map((nameAccountWithData, index) => { | |
if (!nameAccountWithData) return nameAccountWithData; | |
const domain = reverseLookupDomains[index]; | |
return { | |
...nameAccountWithData, | |
domain, | |
name: domain ? `${domain}${nameAccountWithData.tld}` : undefined, | |
}; | |
}); | |
} | |
/** | |
* checks if a domain is expired (including grace period) | |
* @param expiryDate - domain expiry date | |
* @returns boolean indicating if domain is valid | |
*/ | |
export function isDomainValid(expiryDate: Date): boolean { | |
if (!expiryDate) return true; | |
// permanent domain | |
if (expiryDate.getTime() === 0) return true; | |
const oneDay = 1000 * 3600 * 24; | |
const gracePeriod = oneDay * 50; // 50 days grace period | |
return expiryDate.getTime() + gracePeriod >= new Date().getTime(); | |
} | |
/** | |
* fetches all domains owned by a user | |
* @param connection - solana connection instance | |
* @param owner - owner's public key | |
* @returns array of name records with full domain data | |
*/ | |
async function getAllUserDomains( | |
connection: Connection, | |
owner: PublicKey, | |
): Promise<(NameRecordWithTldData | undefined)[]> { | |
// filter for owner's accounts | |
const filters = [ | |
{ | |
memcmp: { | |
offset: 40, | |
bytes: owner.toString(), | |
}, | |
}, | |
]; | |
const allNameAccountsRaw = await connection.getProgramAccounts( | |
ANS_PROGRAM_ID, | |
{ filters }, | |
); | |
const allTldHouses = await getAllTldHouses(connection); | |
let nameAccountsWithData = insertTldHouseIntoData( | |
allNameAccountsRaw, | |
allTldHouses, | |
); | |
return performReverseLookupBatchedTurbo(connection, nameAccountsWithData); | |
} | |
/** | |
* fetches domains for a specific user | |
* @param userPubkey - user's public key | |
* @returns array of user's domain records | |
*/ | |
export async function getUserDomain( | |
userPubkey: PublicKey, | |
): Promise<(NameRecordWithTldData | undefined)[]> { | |
const connection = new Connection(""); | |
const nameAccounts = await getAllUserDomains(connection, userPubkey); | |
return nameAccounts; | |
} | |
// Run the function to get the domains | |
// getUserDomain( | |
// new PublicKey("2EGGxj2qbNAJNgLCPKca8sxZYetyTjnoRspTPjzN2D67"), | |
// ).catch(console.error); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment