diff --git a/js/package-lock.json b/js/package-lock.json index a359b520..a5293cdc 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,12 +1,12 @@ { "name": "@bonfida/spl-name-service", - "version": "2.4.2", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bonfida/spl-name-service", - "version": "2.4.2", + "version": "2.5.0", "license": "MIT", "dependencies": { "@bonfida/sns-records": "0.0.1", diff --git a/js/package.json b/js/package.json index 66236ebc..049d13f7 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "@bonfida/spl-name-service", - "version": "2.4.2", + "version": "2.5.0", "license": "MIT", "files": [ "dist" diff --git a/js/src/bindings.ts b/js/src/bindings.ts index ceb80b74..d089e056 100644 --- a/js/src/bindings.ts +++ b/js/src/bindings.ts @@ -16,6 +16,7 @@ import { burnInstruction, createWithNftInstruction, registerFavoriteInstruction, + createSplitV2Instruction, } from "./instructions"; import { NameRegistryState } from "./state"; import { Numberu64, Numberu32 } from "./int"; @@ -32,12 +33,14 @@ import { REVERSE_LOOKUP_CLASS, WOLVES_COLLECTION_METADATA, METAPLEX_ID, + PYTH_PULL_FEEDS, } from "./constants"; import { check, getDomainKeySync, getHashedNameSync, getNameAccountKeySync, + getPythFeedAccountKey, getReverseKeySync, } from "./utils"; import { @@ -353,6 +356,106 @@ export const registerDomainName = async ( return [[], ixs]; }; +/** + * This function can be used to register a .sol domain + * @param connection The Solana RPC connection object + * @param name The domain name to register e.g bonfida if you want to register bonfida.sol + * @param space The domain name account size (max 10kB) + * @param buyer The public key of the buyer + * @param buyerTokenAccount The buyer token account (USDC) + * @param mint Optional mint used to purchase the domain, defaults to USDC + * @param referrerKey Optional referrer key + * @returns + */ +export const registerDomainNameV2 = async ( + connection: Connection, + name: string, + space: number, + buyer: PublicKey, + buyerTokenAccount: PublicKey, + mint = USDC_MINT, + referrerKey?: PublicKey, +) => { + // Basic validation + if (name.includes(".") || name.trim().toLowerCase() !== name) { + throw new SNSError(ErrorType.InvalidDomain); + } + const [cs] = PublicKey.findProgramAddressSync( + [REGISTER_PROGRAM_ID.toBuffer()], + REGISTER_PROGRAM_ID, + ); + + const hashed = getHashedNameSync(name); + const nameAccount = getNameAccountKeySync( + hashed, + undefined, + ROOT_DOMAIN_ACCOUNT, + ); + + const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); + const reverseLookupAccount = getNameAccountKeySync(hashedReverseLookup, cs); + + const [derived_state] = PublicKey.findProgramAddressSync( + [nameAccount.toBuffer()], + REGISTER_PROGRAM_ID, + ); + + const refIdx = REFERRERS.findIndex((e) => referrerKey?.equals(e)); + let refTokenAccount: PublicKey | undefined = undefined; + + const ixs: TransactionInstruction[] = []; + + if (refIdx !== -1 && !!referrerKey) { + refTokenAccount = getAssociatedTokenAddressSync(mint, referrerKey, true); + const acc = await connection.getAccountInfo(refTokenAccount); + if (!acc?.data) { + const ix = createAssociatedTokenAccountIdempotentInstruction( + buyer, + refTokenAccount, + referrerKey, + mint, + ); + ixs.push(ix); + } + } + + const vault = getAssociatedTokenAddressSync(mint, VAULT_OWNER, true); + const pythFeed = PYTH_PULL_FEEDS.get(mint.toBase58()); + + if (!pythFeed) { + throw new SNSError(ErrorType.PythFeedNotFound); + } + + const [pythFeedAccount] = getPythFeedAccountKey(0, pythFeed); + + const ix = new createSplitV2Instruction({ + name, + space, + referrerIdxOpt: refIdx != -1 ? refIdx : null, + }).getInstruction( + REGISTER_PROGRAM_ID, + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + nameAccount, + reverseLookupAccount, + SystemProgram.programId, + cs, + buyer, + buyer, + buyer, + buyerTokenAccount, + pythFeedAccount, + vault, + TOKEN_PROGRAM_ID, + SYSVAR_RENT_PUBKEY, + derived_state, + refTokenAccount, + ); + ixs.push(ix); + + return ixs; +}; + /** * * @param nameAccount The name account to create the reverse account for diff --git a/js/src/constants.ts b/js/src/constants.ts index 41789e89..f142d30c 100644 --- a/js/src/constants.ts +++ b/js/src/constants.ts @@ -198,3 +198,82 @@ export const WOLVES_COLLECTION_METADATA = new PublicKey( export const METAPLEX_ID = new PublicKey( "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", ); + +export const DEFAULT_PYTH_PUSH_PROGRAM = new PublicKey( + "pythWSnswVUd12oZpeFP8e9CVaEqJg25g1Vtc2biRsT", +); + +export const PYTH_PULL_FEEDS = new Map([ + [ + "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + [ + 234, 160, 32, 198, 28, 196, 121, 113, 40, 19, 70, 28, 225, 83, 137, 74, + 150, 166, 192, 11, 33, 237, 12, 252, 39, 152, 209, 249, 169, 233, 201, 74, + ], + ], + [ + "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", + [ + 43, 137, 185, 220, 143, 223, 159, 52, 112, 154, 91, 16, 107, 71, 47, 15, + 57, 187, 108, 169, 206, 4, 176, 253, 127, 46, 151, 22, 136, 226, 229, 59, + ], + ], + [ + "So11111111111111111111111111111111111111112", + [ + 239, 13, 139, 111, 218, 44, 235, 164, 29, 161, 93, 64, 149, 209, 218, 57, + 42, 13, 47, 142, 208, 198, 199, 188, 15, 76, 250, 200, 194, 128, 181, 109, + ], + ], + [ + "EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", + [ + 200, 6, 87, 183, 246, 243, 234, 194, 114, 24, 208, 157, 90, 78, 84, 228, + 123, 37, 118, 141, 159, 94, 16, 172, 21, 254, 44, 249, 0, 136, 20, 0, + ], + ], + [ + "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", + [ + 194, 40, 154, 106, 67, 210, 206, 145, 198, 245, 92, 174, 195, 112, 244, + 172, 195, 138, 46, 212, 119, 245, 136, 19, 51, 76, 109, 3, 116, 159, 242, + 164, + ], + ], + [ + "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", + [ + 114, 176, 33, 33, 124, 163, 254, 104, 146, 42, 25, 170, 249, 144, 16, 156, + 185, 216, 78, 154, 208, 4, 180, 210, 2, 90, 214, 245, 41, 49, 68, 25, + ], + ], + [ + "EPeUFDgHRxs9xxEPVaL6kfGQvCon7jmAWKVUHuux1Tpz", + [ + 142, 134, 15, 183, 78, 96, 229, 115, 107, 69, 93, 130, 246, 11, 55, 40, 4, + 156, 52, 142, 148, 150, 26, 221, 95, 150, 27, 2, 253, 238, 37, 53, + ], + ], + [ + "HZ1JovNiVvGrGNiiYvEozEVgZ58xaU3RKwX8eACQBCt3", + [ + 11, 191, 40, 233, 168, 65, 161, 204, 120, 143, 106, 54, 27, 23, 202, 7, + 45, 14, 163, 9, 138, 30, 93, 241, 195, 146, 45, 6, 113, 149, 121, 255, + ], + ], + [ + "bSo13r4TkiE4KumL71LsHTPpL2euBYLFx6h9HP3piy1", + [ + 137, 135, 83, 121, 231, 15, 143, 186, 220, 23, 174, 243, 21, 173, 243, + 168, 213, 209, 96, 184, 17, 67, 85, 55, 224, 60, 151, 232, 170, 201, 125, + 156, + ], + ], + [ + "6McPRfPV6bY1e9hLxWyG54W9i9Epq75QBvXg2oetBVTB", + [ + 122, 91, 193, 210, 181, 106, 208, 41, 4, 140, 214, 57, 100, 179, 173, 39, + 118, 234, 223, 129, 46, 220, 26, 67, 163, 20, 6, 203, 84, 191, 245, 146, + ], + ], +]); diff --git a/js/src/instructions.ts b/js/src/instructions.ts index 0262c303..f8c48782 100644 --- a/js/src/instructions.ts +++ b/js/src/instructions.ts @@ -859,3 +859,140 @@ export class registerFavoriteInstruction { }); } } + +export class createSplitV2Instruction { + tag: number; + name: string; + space: number; + referrerIdxOpt: number | null; + static schema = { + struct: { + tag: "u8", + name: "string", + space: "u32", + referrerIdxOpt: { option: "u16" }, + }, + }; + constructor(obj: { + name: string; + space: number; + referrerIdxOpt: number | null; + }) { + this.tag = 20; + this.name = obj.name; + this.space = obj.space; + this.referrerIdxOpt = obj.referrerIdxOpt; + } + serialize(): Uint8Array { + return serialize(createSplitV2Instruction.schema, this); + } + getInstruction( + programId: PublicKey, + namingServiceProgram: PublicKey, + rootDomain: PublicKey, + name: PublicKey, + reverseLookup: PublicKey, + systemProgram: PublicKey, + centralState: PublicKey, + buyer: PublicKey, + domainOwner: PublicKey, + feePayer: PublicKey, + buyerTokenSource: PublicKey, + pythFeedAccount: PublicKey, + vault: PublicKey, + splTokenProgram: PublicKey, + rentSysvar: PublicKey, + state: PublicKey, + referrerAccountOpt?: PublicKey, + ): TransactionInstruction { + const data = Buffer.from(this.serialize()); + let keys: AccountKey[] = []; + keys.push({ + pubkey: namingServiceProgram, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: rootDomain, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: name, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: reverseLookup, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: systemProgram, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: centralState, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: buyer, + isSigner: true, + isWritable: true, + }); + keys.push({ + pubkey: domainOwner, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: feePayer, + isSigner: true, + isWritable: true, + }); + keys.push({ + pubkey: buyerTokenSource, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: pythFeedAccount, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: vault, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: splTokenProgram, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: rentSysvar, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: state, + isSigner: false, + isWritable: false, + }); + if (!!referrerAccountOpt) { + keys.push({ + pubkey: referrerAccountOpt, + isSigner: false, + isWritable: true, + }); + } + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/utils.ts b/js/src/utils.ts index e863f365..968a746b 100644 --- a/js/src/utils.ts +++ b/js/src/utils.ts @@ -1,6 +1,11 @@ import { Connection, PublicKey, MemcmpFilter } from "@solana/web3.js"; import { sha256 } from "@noble/hashes/sha256"; -import { HASH_PREFIX, NAME_PROGRAM_ID, ROOT_DOMAIN_ACCOUNT } from "./constants"; +import { + DEFAULT_PYTH_PUSH_PROGRAM, + HASH_PREFIX, + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, +} from "./constants"; import { NameRegistryState } from "./state"; import { REVERSE_LOOKUP_CLASS } from "./constants"; import { Buffer } from "buffer"; @@ -385,3 +390,12 @@ export function deserializeReverse( const nameLength = data.slice(0, 4).readUInt32LE(0); return data.slice(4, 4 + nameLength).toString(); } + +export const getPythFeedAccountKey = (shard: number, priceFeed: number[]) => { + const buffer = Buffer.alloc(2); + buffer.writeUint16LE(shard); + return PublicKey.findProgramAddressSync( + [buffer, Buffer.from(priceFeed)], + DEFAULT_PYTH_PUSH_PROGRAM, + ); +}; diff --git a/js/tests/pyth.test.ts b/js/tests/pyth.test.ts index 6cac4118..762cf89d 100644 --- a/js/tests/pyth.test.ts +++ b/js/tests/pyth.test.ts @@ -4,7 +4,8 @@ import { getPythProgramKeyForCluster, } from "@pythnetwork/client"; import { Connection } from "@solana/web3.js"; -import { PYTH_FEEDS, TOKENS_SYM_MINT } from "../src/constants"; +import { PYTH_FEEDS, PYTH_PULL_FEEDS, TOKENS_SYM_MINT } from "../src/constants"; +import { getPythFeedAccountKey } from "../src/utils"; const connection = new Connection(process.env.RPC_URL!); @@ -29,3 +30,28 @@ test("Price & Product keys", async () => { expect(productData.price_account).toBe(price); } }); + +test("Pyth Pull derivation", () => { + const sample = [ + { + mint: "So11111111111111111111111111111111111111112", + key: "7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE", + }, + { + mint: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + key: "Dpw1EAVrSB1ibxiDQyTAW6Zip3J4Btk2x4SgApQCeFbX", + }, + { + mint: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So", + key: "5CKzb9j4ChgLUt8Gfm5CNGLN6khXKiqMbnGAW4cgXgxK", + }, + { + mint: "EchesyfXePKdLtoiZSL8pBe8Myagyy8ZRqsACNCFGnvp", + key: "2cfmeuVBf7bvBJcjKBQgAwfvpUvdZV7K8NZxUEuccrub", + }, + ]; + sample.forEach((e) => { + const [key] = getPythFeedAccountKey(0, PYTH_PULL_FEEDS.get(e.mint)!); + expect(key.toBase58()).toBe(e.key); + }); +}); diff --git a/js/tests/registration.test.ts b/js/tests/registration.test.ts index a9f45122..64dd57a8 100644 --- a/js/tests/registration.test.ts +++ b/js/tests/registration.test.ts @@ -1,7 +1,11 @@ require("dotenv").config(); import { test, jest } from "@jest/globals"; import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { registerDomainName, registerWithNft } from "../src/bindings"; +import { + registerDomainName, + registerDomainNameV2, + registerWithNft, +} from "../src/bindings"; import { randomBytes } from "crypto"; import { REFERRERS, USDC_MINT } from "../src/constants"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; @@ -107,3 +111,42 @@ test("Indempotent ATA creation ref", async () => { const res = await connection.simulateTransaction(tx); expect(res.value.err).toBe(null); }); + +test("Register V2", async () => { + const tx = new Transaction(); + const ix = await registerDomainNameV2( + connection, + randomBytes(10).toString("hex"), + 1_000, + VAULT_OWNER, + getAssociatedTokenAddressSync(FIDA_MINT, VAULT_OWNER, true), + FIDA_MINT, + REFERRERS[1], + ); + tx.add(...ix); + const { blockhash } = await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = VAULT_OWNER; + const res = await connection.simulateTransaction(tx); + console.log(res.value.unitsConsumed, "Consummed"); + expect(res.value.err).toBe(null); +}); + +test("Registration V2 with ref", async () => { + const tx = new Transaction(); + const ix = await registerDomainNameV2( + connection, + randomBytes(10).toString("hex"), + 1_000, + VAULT_OWNER, + getAssociatedTokenAddressSync(FIDA_MINT, VAULT_OWNER, true), + FIDA_MINT, + REFERRERS[1], + ); + tx.add(...ix); + const { blockhash } = await connection.getLatestBlockhash(); + tx.recentBlockhash = blockhash; + tx.feePayer = VAULT_OWNER; + const res = await connection.simulateTransaction(tx); + expect(res.value.err).toBe(null); +});