Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ANS integration #455

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/components/SendDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ import {
getNameKey,
} from '../utils/name-service';

import {
getOwnerFromDomainTld,
} from '../utils/alt-name-service';

const WUSDC_MINT = new PublicKey(
'BXXkv6z8ykpG1yuvUDPgh732wzVHB69RnB9YgSYh3itW',
);
Expand Down Expand Up @@ -282,6 +286,24 @@ function SendSplDialog({ onClose, publicKey, balanceInfo, onSubmitRef }) {
setIsDomainName(true);
setDomainOwner(domainOwner);
}

if (!domainOwner && destinationAddress.split('.').length === 2) {
let domainOwner;

domainOwner = await getOwnerFromDomainTld(wallet.connection, destinationAddress);

if (!domainOwner) {
setAddressHelperText(
`This domain name is not registered`,
);
setPassValidation(undefined);
setShouldShowOverride(undefined);
return;
}
setIsDomainName(true);
setDomainOwner(domainOwner);
}

if (!destinationAddress) {
setAddressHelperText(defaultAddressHelperText);
setPassValidation(undefined);
Expand Down
79 changes: 79 additions & 0 deletions src/utils/alt-name-service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { PublicKey, Connection } from '@solana/web3.js';

import {
findOwnedNameAccountsForUser,
getHashedName,
getNameAccountKeyWithBump,
getNameOwner,
getOriginNameAccountKey,
} from './utils';


export const ORIGIN_TLD = 'ANS';
export const ANS_PROGRAM_ID = new PublicKey(
'ALTNSZ46uaAUU7XUV6awvdorLGqAsPwa9shm7h4uP2FK',
);
/**
* retrieves all nameaccounts for any user in a particular tld.
*
* @param connection sol connection
* @param userAccount user publickey or string
* @param tld tld to be retrieved from
*/
export async function getAllUserDomainsFromTld(
connection: Connection,
userAccount: PublicKey | string,
tld: string,
): Promise<PublicKey[]> {
const tldName = '.' + tld;

const nameOriginTldKey = await getOriginNameAccountKey();
const parentHashedName = getHashedName(tldName);
const [parentAccountKey] = await getNameAccountKeyWithBump(
parentHashedName,
undefined,
nameOriginTldKey,
);
if (typeof userAccount == 'string') {
userAccount = new PublicKey(userAccount);
}
const allDomains = await findOwnedNameAccountsForUser(
connection,
userAccount,
parentAccountKey,
);
return allDomains;
}

/**
* retrieves owner of a particular Name Account from domain.tld.
*
* @param connection sol connection
* @param domainTld full string of domain and tld e.g. "miester.poor"
*/
export async function getOwnerFromDomainTld(
connection: Connection,
domainTld: string,
): Promise<PublicKey | undefined> {
const domainTldSplit = domainTld.split('.');
const domain = domainTldSplit[0];
const tldName = '.' + domainTldSplit[1];

const nameOriginTldKey = await getOriginNameAccountKey();
const parentHashedName = getHashedName(tldName);
const [parentAccountKey] = await getNameAccountKeyWithBump(
parentHashedName,
undefined,
nameOriginTldKey,
);

const domainHashedName = getHashedName(domain);
const [domainAccountKey] = await getNameAccountKeyWithBump(
domainHashedName,
undefined,
parentAccountKey,
);

const nameOwner = await getNameOwner(connection, domainAccountKey);
return nameOwner;
}
92 changes: 92 additions & 0 deletions src/utils/alt-name-service/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Connection, PublicKey } from '@solana/web3.js';
import { deserializeUnchecked, Schema } from 'borsh';

/**
* Holds the data for the {@link NameRecordHeader} Account and provides de/serialization
* functionality for that data
*/
export class NameRecordHeader {
constructor(obj: {
parentName: Uint8Array;
owner: Uint8Array;
nclass: Uint8Array;
}) {
this.parentName = new PublicKey(obj.parentName);
this.owner = new PublicKey(obj.owner);
this.nclass = new PublicKey(obj.nclass);
}

parentName: PublicKey;
owner: PublicKey;
nclass: PublicKey;
data: Buffer | undefined;

static DISCRIMINATOR = [68, 72, 88, 44, 15, 167, 103, 243];
static HASH_PREFIX = 'ALT Name Service';

/**
* NameRecordHeader Schema across all name service accounts
*/
static schema: Schema = new Map([
[
NameRecordHeader,
{
kind: 'struct',
fields: [
['discriminator', [8]],
['parentName', [32]],
['owner', [32]],
['nclass', [32]],
['padding', [96]],
],
},
],
]);

/**
* Returns the minimum size of a {@link Buffer} holding the serialized data of
* {@link NameRecordHeader}
*/
static get byteSize() {
return 8 + 32 + 32 + 32 + 96;
}

/**
* Retrieves the account info from the provided address and deserializes
* the {@link NameRecordHeader} from its data.
*/
public static async fromAccountAddress(
connection: Connection,
nameAccountKey: PublicKey,
): Promise<NameRecordHeader | undefined> {
const nameAccount = await connection.getAccountInfo(
nameAccountKey,
'confirmed',
);
if (!nameAccount) {
return undefined;
}

const res: NameRecordHeader = deserializeUnchecked(
this.schema,
NameRecordHeader,
nameAccount.data,
);

res.data = nameAccount.data?.subarray(this.byteSize);

return res;
}

/**
* Returns a readable version of {@link NameRecordHeader} properties
* and can be used to convert to JSON and/or logging
*/
pretty() {
return {
parentName: this.parentName.toBase58(),
owner: this.owner.toBase58(),
nclass: this.nclass.toBase58(),
};
}
}
106 changes: 106 additions & 0 deletions src/utils/alt-name-service/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Connection, PublicKey } from '@solana/web3.js';
import { createHash } from 'crypto';
import { ANS_PROGRAM_ID, ORIGIN_TLD } from '.';

import { NameRecordHeader } from './state';

/**
* retrieves raw name account
*
* @param hashedName hashed name of the name account
* @param nameClass defaults to pubkey::default()
* @param parentName defaults to pubkey::default()
*/
export async function getNameAccountKeyWithBump(
hashedName: Buffer,
nameClass?: PublicKey,
parentName?: PublicKey,
): Promise<[PublicKey, number]> {
const seeds = [
hashedName,
nameClass ? nameClass.toBuffer() : Buffer.alloc(32),
parentName ? parentName.toBuffer() : Buffer.alloc(32),
];

return await PublicKey.findProgramAddress(seeds, ANS_PROGRAM_ID);
}

/**
* retrieves owner of the name account
*
* @param connection sol connection
* @param nameAccountKey defaults to pubkey::default()
*/
export async function getNameOwner(
connection: Connection,
nameAccountKey: PublicKey,
): Promise<PublicKey | undefined> {
return (await NameRecordHeader.fromAccountAddress(connection, nameAccountKey))?.owner;
}

/**
* computes hashed name
*
* @param name any string or domain name
*/

export function getHashedName(name: string): Buffer {
const input = NameRecordHeader.HASH_PREFIX + name;
const buffer = createHash('sha256').update(input, 'utf8').digest();
return buffer;
}

/**
* A constant in tld house.
*
* get origin name account should always equal to 3mX9b4AZaQehNoQGfckVcmgmA6bkBoFcbLj9RMmMyNcU
*
* @param originTld
*/
export async function getOriginNameAccountKey(
originTld: string = ORIGIN_TLD,
): Promise<PublicKey> {
const hashed_name = getHashedName(originTld);
const [nameAccountKey] = await getNameAccountKeyWithBump(
hashed_name,
undefined,
undefined,
);
return nameAccountKey;
}

/**
* finds list of all name accounts for a particular user.
*
* @param connection sol connection
* @param userAccount user's public key
* @param parentAccount nameAccount's parentName
*/
export async function findOwnedNameAccountsForUser(
connection: Connection,
userAccount: PublicKey,
parentAccount: PublicKey | undefined,
): Promise<PublicKey[]> {
const filters: any = [
{
memcmp: {
offset: 40,
bytes: userAccount.toBase58(),
},
},
];

if (parentAccount) {
filters.push({
memcmp: {
offset: 8,
bytes: parentAccount.toBase58(),
},
});
}

const accounts = await connection.getProgramAccounts(ANS_PROGRAM_ID, {
filters: filters,
});
return accounts.map((a: any) => a.pubkey);
}