diff --git a/js/benchmarks/nft.ts b/js/benchmarks/nft.ts new file mode 100644 index 0000000..45513ca --- /dev/null +++ b/js/benchmarks/nft.ts @@ -0,0 +1,79 @@ +require("dotenv").config(); +import { Connection, PublicKey } from "@solana/web3.js"; +import { retrieveNftOwner, retrieveNftOwnerV2 } from "../src/nft"; +import { mean, median } from "mathjs"; + +const benchmark = async () => { + const connection = new Connection(process.env.RPC_URL!); + + const nameAccounts = [ + new PublicKey("5RDpkiLAvYh5StaFbFdvkMnezQCFAN2vGyhVEeQHMTf"), + new PublicKey("AVdePc1a3CXg56GzHLz8jxB5MxV4Fh3iKU7z7xbgDTEY"), + new PublicKey("AQzjja886kuZpY3Bm8ataXDvJemwrwV91dHypZ45vaei"), + new PublicKey("2nD7dijypykUCBxzLmPc21trzAZd2CWoQurWBRt5qg3D"), + new PublicKey("Fwyp251aoe5LEis4WNjAvs9D7z4bSZSi4inangghgwf3"), + new PublicKey("CT3kcSzzkaay8UpYJmYvGvXJLQPziFqJCgyetr7GhSpB"), + new PublicKey("797fVNoYuyMcLf3d6oztSCAJSrevJ4dWrGy9B9xKWujy"), + new PublicKey("F9ESKiA79dsHxhC3h7iLGTnVU5iPcaCKWaRDmTm9tiEa"), + ]; + + const results: any[] = []; + const times1: number[] = []; + const times2: number[] = []; + + for (const nameAccount of nameAccounts) { + const accountBase58 = nameAccount.toBase58(); + + console.time(`retrieveNftOwner-${accountBase58}`); + const start1 = performance.now(); + const owner1 = await retrieveNftOwner(connection, nameAccount); + const end1 = performance.now(); + console.timeEnd(`retrieveNftOwner-${accountBase58}`); + const time1 = end1 - start1; + times1.push(time1); + + console.time(`retrieveNftOwnerV2-${accountBase58}`); + const start2 = performance.now(); + const owner2 = await retrieveNftOwnerV2(connection, nameAccount); + const end2 = performance.now(); + console.timeEnd(`retrieveNftOwnerV2-${accountBase58}`); + const time2 = end2 - start2; + times2.push(time2); + + results.push({ + Account: accountBase58, + Owner1: owner1?.toBase58() || "N/A", + Time1: `${time1.toFixed(2)} ms`, + Owner2: owner2?.toBase58() || "N/A", + Time2: `${time2.toFixed(2)} ms`, + }); + } + + console.table(results); + + const avgTime1 = mean(times1); + const medianTime1 = median(times1); + const avgTime2 = mean(times2); + const medianTime2 = median(times2); + + console.table([ + { + Metric: "Average Time (retrieveNftOwner)", + Value: `${avgTime1.toFixed(2)} ms`, + }, + { + Metric: "Median Time (retrieveNftOwner)", + Value: `${medianTime1.toFixed(2)} ms`, + }, + { + Metric: "Average Time (retrieveNftOwnerV2)", + Value: `${avgTime2.toFixed(2)} ms`, + }, + { + Metric: "Median Time (retrieveNftOwnerV2)", + Value: `${medianTime2.toFixed(2)} ms`, + }, + ]); +}; + +benchmark().catch(console.error); diff --git a/js/package-lock.json b/js/package-lock.json index 0cede56..66e0b75 100644 --- a/js/package-lock.json +++ b/js/package-lock.json @@ -1,23 +1,22 @@ { "name": "@bonfida/spl-name-service", - "version": "2.5.4", + "version": "3.0.0-alpha.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@bonfida/spl-name-service", - "version": "2.5.4", + "version": "3.0.0-alpha.2", "license": "MIT", "dependencies": { "@bonfida/sns-records": "0.0.1", - "@noble/curves": "^1.3.0", - "@scure/base": "^1.1.5", - "@solana/buffer-layout": "^4.0.1", - "@solana/spl-token": "0.3.9", + "@noble/curves": "^1.4.0", + "@scure/base": "^1.1.6", + "@solana/spl-token": "0.4.6", "borsh": "2.0.0", "buffer": "^6.0.3", "graphemesplit": "^2.4.4", - "ipaddr.js": "^2.1.0", + "ipaddr.js": "^2.2.0", "punycode": "^2.3.1" }, "devDependencies": { @@ -31,7 +30,6 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.5", - "@solana/web3.js": "^1.87.6", "@tsconfig/recommended": "^1.0.3", "@types/bn.js": "^5.1.5", "@types/bs58": "^4.0.4", @@ -42,7 +40,9 @@ "eslint": "^8.55.0", "eslint-plugin-import": "^2.29.0", "jest": "^29.7.0", + "mathjs": "^12.4.2", "prettier": "^3.1.0", + "rollup-plugin-multi-input": "^1.4.1", "rollup-plugin-visualizer": "^5.12.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -502,9 +502,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -2424,6 +2424,24 @@ "node-fetch": "^2.6.7" } }, + "node_modules/@metaplex-foundation/js/node_modules/@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.88.0" + } + }, "node_modules/@metaplex-foundation/mpl-auction-house": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-auction-house/-/mpl-auction-house-2.5.1.tgz", @@ -2449,6 +2467,24 @@ "debug": "^4.3.3" } }, + "node_modules/@metaplex-foundation/mpl-auction-house/node_modules/@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.88.0" + } + }, "node_modules/@metaplex-foundation/mpl-bubblegum": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-0.6.2.tgz", @@ -2577,6 +2613,24 @@ "debug": "^4.3.4" } }, + "node_modules/@metaplex-foundation/mpl-candy-machine/node_modules/@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.88.0" + } + }, "node_modules/@metaplex-foundation/mpl-token-metadata": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.13.0.tgz", @@ -2604,6 +2658,24 @@ "debug": "^4.3.4" } }, + "node_modules/@metaplex-foundation/mpl-token-metadata/node_modules/@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.88.0" + } + }, "node_modules/@near-js/crypto": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@near-js/crypto/-/crypto-0.0.3.tgz", @@ -2871,11 +2943,11 @@ } }, "node_modules/@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", "dependencies": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -2894,9 +2966,9 @@ ] }, "node_modules/@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "engines": { "node": ">= 16" }, @@ -3167,9 +3239,9 @@ } }, "node_modules/@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -3289,6 +3361,98 @@ "node": ">= 10" } }, + "node_modules/@solana/codecs": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-preview.2.tgz", + "integrity": "sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==", + "dependencies": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-data-structures": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/codecs-strings": "2.0.0-preview.2", + "@solana/options": "2.0.0-preview.2" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-preview.2.tgz", + "integrity": "sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg==", + "dependencies": { + "@solana/errors": "2.0.0-preview.2" + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz", + "integrity": "sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==", + "dependencies": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.2.tgz", + "integrity": "sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw==", + "dependencies": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + } + }, + "node_modules/@solana/codecs-strings": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.2.tgz", + "integrity": "sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==", + "dependencies": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22" + } + }, + "node_modules/@solana/errors": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-preview.2.tgz", + "integrity": "sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA==", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.0.0" + }, + "bin": { + "errors": "bin/cli.js" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/errors/node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@solana/options": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-preview.2.tgz", + "integrity": "sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w==", + "dependencies": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2" + } + }, "node_modules/@solana/spl-account-compression": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@solana/spl-account-compression/-/spl-account-compression-0.1.10.tgz", @@ -3342,31 +3506,74 @@ } }, "node_modules/@solana/spl-token": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.9.tgz", - "integrity": "sha512-1EXHxKICMnab35MvvY/5DBc/K/uQAOJCYnDZXw83McCAYUAfi+rwq6qfd6MmITmSTEhcfBcl/zYxmW/OSN0RmA==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.6.tgz", + "integrity": "sha512-1nCnUqfHVtdguFciVWaY/RKcQz1IF4b31jnKgAmjU9QVN1q7dRUkTEWJZgTYIEtsULjVnC9jRqlhgGN39WbKKA==", "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-group": "^0.0.4", + "@solana/spl-token-metadata": "^0.1.4", "buffer": "^6.0.3" }, "engines": { "node": ">=16" }, "peerDependencies": { - "@solana/web3.js": "^1.47.4" + "@solana/web3.js": "^1.91.6" + } + }, + "node_modules/@solana/spl-token-group": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.4.tgz", + "integrity": "sha512-7+80nrEMdUKlK37V6kOe024+T7J4nNss0F8LQ9OOPYdWCCfJmsGUzVx2W3oeizZR4IHM6N4yC9v1Xqwc3BTPWw==", + "dependencies": { + "@solana/codecs": "2.0.0-preview.2", + "@solana/spl-type-length-value": "0.1.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.91.6" + } + }, + "node_modules/@solana/spl-token-metadata": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.4.tgz", + "integrity": "sha512-N3gZ8DlW6NWDV28+vCCDJoTqaCZiF/jDUnk3o8GRkAFzHObiR60Bs1gXHBa8zCPdvOwiG6Z3dg5pg7+RW6XNsQ==", + "dependencies": { + "@solana/codecs": "2.0.0-preview.2", + "@solana/spl-type-length-value": "0.1.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.91.6" + } + }, + "node_modules/@solana/spl-type-length-value": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz", + "integrity": "sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==", + "dependencies": { + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" } }, "node_modules/@solana/web3.js": { - "version": "1.87.6", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.87.6.tgz", - "integrity": "sha512-LkqsEBgTZztFiccZZXnawWa8qNCATEqE97/d0vIwjTclmVlc8pBpD1DmjfVHtZ1HS5fZorFlVhXfpwnCNDZfyg==", + "version": "1.91.8", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.91.8.tgz", + "integrity": "sha512-USa6OS1jbh8zOapRJ/CBZImZ8Xb7AJjROZl5adql9TpOoBN9BUzyyouS5oPuZHft7S7eB8uJPuXWYjMi6BHgOw==", "dependencies": { - "@babel/runtime": "^7.23.2", - "@noble/curves": "^1.2.0", - "@noble/hashes": "^1.3.1", - "@solana/buffer-layout": "^4.0.0", - "agentkeepalive": "^4.3.0", + "@babel/runtime": "^7.24.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", "bigint-buffer": "^1.1.5", "bn.js": "^5.2.1", "borsh": "^0.7.0", @@ -3374,8 +3581,8 @@ "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.0", - "node-fetch": "^2.6.12", - "rpc-websockets": "^7.5.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^7.11.0", "superstruct": "^0.14.2" } }, @@ -4847,6 +5054,19 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "node_modules/complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5074,6 +5294,12 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "node_modules/dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -5414,6 +5640,12 @@ "node": ">=6" } }, + "node_modules/escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "dev": true + }, "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -5978,6 +6210,34 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -5995,6 +6255,12 @@ "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "peer": true + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -6136,6 +6402,19 @@ "node": ">= 6" } }, + "node_modules/fraction.js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -6752,9 +7031,9 @@ } }, "node_modules/ipaddr.js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", - "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", "engines": { "node": ">= 10" } @@ -7255,6 +7534,12 @@ "node": ">=8" } }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "node_modules/jayson": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", @@ -9330,6 +9615,29 @@ "node": ">= 12" } }, + "node_modules/mathjs": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.2.tgz", + "integrity": "sha512-lW14EzwAFgbNN7AZikxplmhs7wiXDhMphBOGCA3KS6T29ECEkHJsBtbEW5cnCz7sXtl4nDyvTdR+DqVsZyiiEw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.24.4", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -9347,6 +9655,15 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/merkletreejs": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.11.tgz", @@ -10412,9 +10729,9 @@ } }, "node_modules/rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "optional": true, "peer": true, @@ -10422,12 +10739,25 @@ "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=10.0.0" + "node": ">=14.18.0", + "npm": ">=8.0.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, + "node_modules/rollup-plugin-multi-input": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-multi-input/-/rollup-plugin-multi-input-1.4.1.tgz", + "integrity": "sha512-ybvotObZFFDEbqw6MDrYUa/TXmF+1qCVX3svpAddmIOLP3/to5zkSKP0MJV5bNBZfFFpblwChurz4tsPR/zJew==", + "dev": true, + "dependencies": { + "fast-glob": "3.2.12" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/rollup-plugin-visualizer": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", @@ -10464,11 +10794,10 @@ } }, "node_modules/rpc-websockets": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.6.2.tgz", - "integrity": "sha512-+M1fOYMPxvOQDHbSItkD/an4fRwPZ1Nft1zv48G84S0TyChG2A1GXmjWkbs3o2NxW+q36H9nM2uLo5yojTrPaA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.11.0.tgz", + "integrity": "sha512-IkLYjayPv6Io8C/TdCL5gwgzd1hFz2vmBZrjMw/SPEXo51ETOhnzgS4Qy5GWi2JQN7HKHa66J3+2mv0fgNh/7w==", "dependencies": { - "@babel/runtime": "^7.17.2", "eventemitter3": "^4.0.7", "uuid": "^8.3.2", "ws": "^8.5.0" @@ -10483,9 +10812,9 @@ } }, "node_modules/rpc-websockets/node_modules/ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "engines": { "node": ">=10.0.0" }, @@ -10627,6 +10956,12 @@ "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "dev": true }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -11082,6 +11417,12 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dev": true + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -11440,6 +11781,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-function": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz", + "integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/typedoc": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz", @@ -12224,9 +12574,9 @@ } }, "@babel/runtime": { - "version": "7.23.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", - "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -13586,6 +13936,20 @@ "merkletreejs": "^0.3.11", "mime": "^3.0.0", "node-fetch": "^2.6.7" + }, + "dependencies": { + "@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + } + } } }, "@metaplex-foundation/mpl-auction-house": { @@ -13612,6 +13976,18 @@ "bn.js": "^5.2.0", "debug": "^4.3.3" } + }, + "@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + } } } }, @@ -13716,6 +14092,18 @@ "bs58": "^5.0.0", "debug": "^4.3.4" } + }, + "@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + } } } }, @@ -13771,6 +14159,18 @@ "bs58": "^5.0.0", "debug": "^4.3.4" } + }, + "@solana/spl-token": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.11.tgz", + "integrity": "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==", + "dev": true, + "requires": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-metadata": "^0.1.2", + "buffer": "^6.0.3" + } } } }, @@ -14047,11 +14447,11 @@ } }, "@noble/curves": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.3.0.tgz", - "integrity": "sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.0.tgz", + "integrity": "sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==", "requires": { - "@noble/hashes": "1.3.3" + "@noble/hashes": "1.4.0" } }, "@noble/ed25519": { @@ -14061,9 +14461,9 @@ "dev": true }, "@noble/hashes": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.3.tgz", - "integrity": "sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==" + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==" }, "@nodelib/fs.scandir": { "version": "2.1.5", @@ -14220,9 +14620,9 @@ } }, "@scure/base": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.5.tgz", - "integrity": "sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ==" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", + "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==" }, "@scure/bip32": { "version": "1.3.1", @@ -14313,6 +14713,85 @@ "bignumber.js": "^9.0.1" } }, + "@solana/codecs": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-preview.2.tgz", + "integrity": "sha512-4HHzCD5+pOSmSB71X6w9ptweV48Zj1Vqhe732+pcAQ2cMNnN0gMPMdDq7j3YwaZDZ7yrILVV/3+HTnfT77t2yA==", + "requires": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-data-structures": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/codecs-strings": "2.0.0-preview.2", + "@solana/options": "2.0.0-preview.2" + } + }, + "@solana/codecs-core": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-preview.2.tgz", + "integrity": "sha512-gLhCJXieSCrAU7acUJjbXl+IbGnqovvxQLlimztPoGgfLQ1wFYu+XJswrEVQqknZYK1pgxpxH3rZ+OKFs0ndQg==", + "requires": { + "@solana/errors": "2.0.0-preview.2" + } + }, + "@solana/codecs-data-structures": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-preview.2.tgz", + "integrity": "sha512-Xf5vIfromOZo94Q8HbR04TbgTwzigqrKII0GjYr21K7rb3nba4hUW2ir8kguY7HWFBcjHGlU5x3MevKBOLp3Zg==", + "requires": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + } + }, + "@solana/codecs-numbers": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-preview.2.tgz", + "integrity": "sha512-aLZnDTf43z4qOnpTcDsUVy1Ci9im1Md8thWipSWbE+WM9ojZAx528oAql+Cv8M8N+6ALKwgVRhPZkto6E59ARw==", + "requires": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + } + }, + "@solana/codecs-strings": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-preview.2.tgz", + "integrity": "sha512-EgBwY+lIaHHgMJIqVOGHfIfpdmmUDNoNO/GAUGeFPf+q0dF+DtwhJPEMShhzh64X2MeCZcmSO6Kinx0Bvmmz2g==", + "requires": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2", + "@solana/errors": "2.0.0-preview.2" + } + }, + "@solana/errors": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-preview.2.tgz", + "integrity": "sha512-H2DZ1l3iYF5Rp5pPbJpmmtCauWeQXRJapkDg8epQ8BJ7cA2Ut/QEtC3CMmw/iMTcuS6uemFNLcWvlOfoQhvQuA==", + "requires": { + "chalk": "^5.3.0", + "commander": "^12.0.0" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, + "commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==" + } + } + }, + "@solana/options": { + "version": "2.0.0-preview.2", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-preview.2.tgz", + "integrity": "sha512-FAHqEeH0cVsUOTzjl5OfUBw2cyT8d5Oekx4xcn5hn+NyPAfQJgM3CEThzgRD6Q/4mM5pVUnND3oK/Mt1RzSE/w==", + "requires": { + "@solana/codecs-core": "2.0.0-preview.2", + "@solana/codecs-numbers": "2.0.0-preview.2" + } + }, "@solana/spl-account-compression": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/@solana/spl-account-compression/-/spl-account-compression-0.1.10.tgz", @@ -14364,25 +14843,53 @@ } }, "@solana/spl-token": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.3.9.tgz", - "integrity": "sha512-1EXHxKICMnab35MvvY/5DBc/K/uQAOJCYnDZXw83McCAYUAfi+rwq6qfd6MmITmSTEhcfBcl/zYxmW/OSN0RmA==", + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.6.tgz", + "integrity": "sha512-1nCnUqfHVtdguFciVWaY/RKcQz1IF4b31jnKgAmjU9QVN1q7dRUkTEWJZgTYIEtsULjVnC9jRqlhgGN39WbKKA==", "requires": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-group": "^0.0.4", + "@solana/spl-token-metadata": "^0.1.4", + "buffer": "^6.0.3" + } + }, + "@solana/spl-token-group": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.4.tgz", + "integrity": "sha512-7+80nrEMdUKlK37V6kOe024+T7J4nNss0F8LQ9OOPYdWCCfJmsGUzVx2W3oeizZR4IHM6N4yC9v1Xqwc3BTPWw==", + "requires": { + "@solana/codecs": "2.0.0-preview.2", + "@solana/spl-type-length-value": "0.1.0" + } + }, + "@solana/spl-token-metadata": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.4.tgz", + "integrity": "sha512-N3gZ8DlW6NWDV28+vCCDJoTqaCZiF/jDUnk3o8GRkAFzHObiR60Bs1gXHBa8zCPdvOwiG6Z3dg5pg7+RW6XNsQ==", + "requires": { + "@solana/codecs": "2.0.0-preview.2", + "@solana/spl-type-length-value": "0.1.0" + } + }, + "@solana/spl-type-length-value": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@solana/spl-type-length-value/-/spl-type-length-value-0.1.0.tgz", + "integrity": "sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==", + "requires": { "buffer": "^6.0.3" } }, "@solana/web3.js": { - "version": "1.87.6", - "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.87.6.tgz", - "integrity": "sha512-LkqsEBgTZztFiccZZXnawWa8qNCATEqE97/d0vIwjTclmVlc8pBpD1DmjfVHtZ1HS5fZorFlVhXfpwnCNDZfyg==", + "version": "1.91.8", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.91.8.tgz", + "integrity": "sha512-USa6OS1jbh8zOapRJ/CBZImZ8Xb7AJjROZl5adql9TpOoBN9BUzyyouS5oPuZHft7S7eB8uJPuXWYjMi6BHgOw==", "requires": { - "@babel/runtime": "^7.23.2", - "@noble/curves": "^1.2.0", - "@noble/hashes": "^1.3.1", - "@solana/buffer-layout": "^4.0.0", - "agentkeepalive": "^4.3.0", + "@babel/runtime": "^7.24.5", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", "bigint-buffer": "^1.1.5", "bn.js": "^5.2.1", "borsh": "^0.7.0", @@ -14390,8 +14897,8 @@ "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.0", - "node-fetch": "^2.6.12", - "rpc-websockets": "^7.5.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^7.11.0", "superstruct": "^0.14.2" }, "dependencies": { @@ -15558,6 +16065,12 @@ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", "dev": true }, + "complex.js": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz", + "integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg==", + "dev": true + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -15740,6 +16253,12 @@ "ms": "2.1.2" } }, + "decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true + }, "dedent": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", @@ -16003,6 +16522,12 @@ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true }, + "escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==", + "dev": true + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -16433,6 +16958,30 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -16450,6 +16999,12 @@ "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" }, + "fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "peer": true + }, "fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -16553,6 +17108,12 @@ "mime-types": "^2.1.12" } }, + "fraction.js": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.4.tgz", + "integrity": "sha512-pwiTgt0Q7t+GHZA4yaLjObx4vXmmdcS0iSJ19o8d/goUGgItX9UZWKWNnLHehxviD8wU2IWRsnR8cD5+yOJP2Q==", + "dev": true + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -17001,9 +17562,9 @@ } }, "ipaddr.js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", - "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==" }, "is-array-buffer": { "version": "3.0.2", @@ -17349,6 +17910,12 @@ "istanbul-lib-report": "^3.0.0" } }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true + }, "jayson": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.1.0.tgz", @@ -18915,6 +19482,23 @@ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, + "mathjs": { + "version": "12.4.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-12.4.2.tgz", + "integrity": "sha512-lW14EzwAFgbNN7AZikxplmhs7wiXDhMphBOGCA3KS6T29ECEkHJsBtbEW5cnCz7sXtl4nDyvTdR+DqVsZyiiEw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.24.4", + "complex.js": "^2.1.1", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "4.3.4", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.1.1" + } + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -18932,6 +19516,12 @@ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, "merkletreejs": { "version": "0.3.11", "resolved": "https://registry.npmjs.org/merkletreejs/-/merkletreejs-0.3.11.tgz", @@ -19703,9 +20293,9 @@ } }, "rollup": { - "version": "2.79.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", - "integrity": "sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==", + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, "optional": true, "peer": true, @@ -19713,6 +20303,15 @@ "fsevents": "~2.3.2" } }, + "rollup-plugin-multi-input": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-multi-input/-/rollup-plugin-multi-input-1.4.1.tgz", + "integrity": "sha512-ybvotObZFFDEbqw6MDrYUa/TXmF+1qCVX3svpAddmIOLP3/to5zkSKP0MJV5bNBZfFFpblwChurz4tsPR/zJew==", + "dev": true, + "requires": { + "fast-glob": "3.2.12" + } + }, "rollup-plugin-visualizer": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", @@ -19734,11 +20333,10 @@ } }, "rpc-websockets": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.6.2.tgz", - "integrity": "sha512-+M1fOYMPxvOQDHbSItkD/an4fRwPZ1Nft1zv48G84S0TyChG2A1GXmjWkbs3o2NxW+q36H9nM2uLo5yojTrPaA==", + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.11.0.tgz", + "integrity": "sha512-IkLYjayPv6Io8C/TdCL5gwgzd1hFz2vmBZrjMw/SPEXo51ETOhnzgS4Qy5GWi2JQN7HKHa66J3+2mv0fgNh/7w==", "requires": { - "@babel/runtime": "^7.17.2", "bufferutil": "^4.0.1", "eventemitter3": "^4.0.7", "utf-8-validate": "^5.0.2", @@ -19747,9 +20345,9 @@ }, "dependencies": { "ws": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", - "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz", + "integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==", "requires": {} } } @@ -19837,6 +20435,12 @@ } } }, + "seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "dev": true + }, "semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -20201,6 +20805,12 @@ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "dev": true + }, "tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -20453,6 +21063,12 @@ "is-typed-array": "^1.1.9" } }, + "typed-function": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.1.tgz", + "integrity": "sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==", + "dev": true + }, "typedoc": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.4.tgz", diff --git a/js/package.json b/js/package.json index ae873ed..366c4a5 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "@bonfida/spl-name-service", - "version": "2.5.4", + "version": "3.0.0-alpha.2", "license": "MIT", "files": [ "dist" @@ -44,7 +44,6 @@ "@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.5", - "@solana/web3.js": "^1.87.6", "@tsconfig/recommended": "^1.0.3", "@types/bn.js": "^5.1.5", "@types/bs58": "^4.0.4", @@ -55,7 +54,9 @@ "eslint": "^8.55.0", "eslint-plugin-import": "^2.29.0", "jest": "^29.7.0", + "mathjs": "^12.4.2", "prettier": "^3.1.0", + "rollup-plugin-multi-input": "^1.4.1", "rollup-plugin-visualizer": "^5.12.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", @@ -65,14 +66,13 @@ }, "dependencies": { "@bonfida/sns-records": "0.0.1", - "@noble/curves": "^1.3.0", - "@scure/base": "^1.1.5", - "@solana/buffer-layout": "^4.0.1", - "@solana/spl-token": "0.3.9", + "@noble/curves": "^1.4.0", + "@scure/base": "^1.1.6", + "@solana/spl-token": "0.4.6", "borsh": "2.0.0", "buffer": "^6.0.3", "graphemesplit": "^2.4.4", - "ipaddr.js": "^2.1.0", + "ipaddr.js": "^2.2.0", "punycode": "^2.3.1" }, "peerDependencies": { diff --git a/js/rollup.config.mjs b/js/rollup.config.mjs index 80a8b9d..79561bf 100644 --- a/js/rollup.config.mjs +++ b/js/rollup.config.mjs @@ -6,28 +6,45 @@ import { nodeResolve } from "@rollup/plugin-node-resolve"; import replace from "@rollup/plugin-replace"; import babel from "@rollup/plugin-babel"; import { visualizer } from "rollup-plugin-visualizer"; +import multiInput from "rollup-plugin-multi-input"; +/** + * @type {import('rollup').RollupOptions} + */ export default { - input: "src/index.ts", + input: [ + "src/index.ts", + "src/record_v2/**/*.ts", + "src/utils/**/*.ts", + "src/twitter/**/*.ts", + "src/resolve/**/*.ts", + "src/record/**/*.ts", + "src/nft/**/*.ts", + "src/bindings/**/*.ts", + "src/instructions/**/*.ts", + ], output: [ { - file: "dist/index.mjs", + dir: "dist/", format: "esm", sourcemap: true, + entryFileNames: "[name].mjs", + exports: "named", }, - { file: "dist/index.cjs", format: "cjs", sourcemap: true }, + { dir: "dist/", format: "cjs", sourcemap: true }, ], external: ["@solana/web3.js"], plugins: [ - typescript(), - commonjs(), - babel({ babelHelpers: "bundled" }), - json(), + multiInput.default(), nodeResolve({ browser: true, preferBuiltins: false, dedupe: ["borsh", "@solana/spl-token", "bn.js", "buffer"], }), + commonjs(), + typescript(), + babel({ babelHelpers: "bundled" }), + json(), replace({ "process.env.NODE_ENV": JSON.stringify("production"), preventAssignment: false, @@ -35,6 +52,10 @@ export default { terser(), visualizer(), ], + treeshake: { + moduleSideEffects: false, + preset: "smallest", + }, onwarn: function (warning, handler) { if (warning.code === "THIS_IS_UNDEFINED") return; handler(warning); diff --git a/js/src/bindings.ts b/js/src/bindings.ts deleted file mode 100644 index 16d343d..0000000 --- a/js/src/bindings.ts +++ /dev/null @@ -1,1100 +0,0 @@ -import { Buffer } from "buffer"; -import { - Connection, - PublicKey, - SystemProgram, - TransactionInstruction, - SYSVAR_RENT_PUBKEY, -} from "@solana/web3.js"; -import { - createInstruction, - deleteInstruction, - transferInstruction, - updateInstruction, - createReverseInstruction, - createInstructionV3, - burnInstruction, - createWithNftInstruction, - registerFavoriteInstruction, - createSplitV2Instruction, -} from "./instructions"; -import { NameRegistryState } from "./state"; -import { Numberu64, Numberu32 } from "./int"; -import { getNameOwner } from "./deprecated/utils"; -import { - NAME_PROGRAM_ID, - ROOT_DOMAIN_ACCOUNT, - REGISTER_PROGRAM_ID, - REFERRERS, - USDC_MINT, - PYTH_FEEDS, - PYTH_MAPPING_ACC, - VAULT_OWNER, - REVERSE_LOOKUP_CLASS, - WOLVES_COLLECTION_METADATA, - METAPLEX_ID, - PYTH_PULL_FEEDS, -} from "./constants"; -import { - check, - getDomainKeySync, - getHashedNameSync, - getNameAccountKeySync, - getPythFeedAccountKey, - getReverseKeySync, -} from "./utils"; -import { - TOKEN_PROGRAM_ID, - getAssociatedTokenAddressSync, - createAssociatedTokenAccountIdempotentInstruction, -} from "@solana/spl-token"; -import { ErrorType, SNSError } from "./error"; -import { serializeRecord, serializeSolRecord } from "./record"; -import { Record, RecordVersion } from "./types/record"; -import { serializeRecordV2Content } from "./record_v2"; -import { - editRecord, - allocateAndPostRecord, - SNS_RECORDS_ID, - deleteRecord, - validateSolanaSignature, - validateEthSignature, - Validation, - writeRoa, -} from "@bonfida/sns-records"; -import { FavouriteDomain, NAME_OFFERS_ID } from "./favorite-domain"; - -/** - * Creates a name account with the given rent budget, allocated space, owner and class. - * - * @param connection The solana connection object to the RPC node - * @param name The name of the new account - * @param space The space in bytes allocated to the account - * @param payerKey The allocation cost payer - * @param nameOwner The pubkey to be set as owner of the new name account - * @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption - * @param nameClass The class of this new name - * @param parentName The parent name of the new name. If specified its owner needs to sign - * @returns - */ -export async function createNameRegistry( - connection: Connection, - name: string, - space: number, - payerKey: PublicKey, - nameOwner: PublicKey, - lamports?: number, - nameClass?: PublicKey, - parentName?: PublicKey, -): Promise { - const hashed_name = getHashedNameSync(name); - const nameAccountKey = getNameAccountKeySync( - hashed_name, - nameClass, - parentName, - ); - - const balance = lamports - ? lamports - : await connection.getMinimumBalanceForRentExemption(space); - - let nameParentOwner: PublicKey | undefined; - if (parentName) { - const { registry: parentAccount } = await getNameOwner( - connection, - parentName, - ); - nameParentOwner = parentAccount.owner; - } - - const createNameInstr = createInstruction( - NAME_PROGRAM_ID, - SystemProgram.programId, - nameAccountKey, - nameOwner, - payerKey, - hashed_name, - new Numberu64(balance), - new Numberu32(space), - nameClass, - parentName, - nameParentOwner, - ); - - return createNameInstr; -} - -/** - * Overwrite the data of the given name registry. - * - * @param connection The solana connection object to the RPC node - * @param name The name of the name registry to update - * @param offset The offset to which the data should be written into the registry - * @param input_data The data to be written - * @param nameClass The class of this name, if it exsists - * @param nameParent The parent name of this name, if it exists - */ -export async function updateNameRegistryData( - connection: Connection, - name: string, - offset: number, - input_data: Buffer, - nameClass?: PublicKey, - nameParent?: PublicKey, -): Promise { - const hashed_name = getHashedNameSync(name); - const nameAccountKey = getNameAccountKeySync( - hashed_name, - nameClass, - nameParent, - ); - - let signer: PublicKey; - if (nameClass) { - signer = nameClass; - } else { - signer = (await NameRegistryState.retrieve(connection, nameAccountKey)) - .registry.owner; - } - - const updateInstr = updateInstruction( - NAME_PROGRAM_ID, - nameAccountKey, - new Numberu32(offset), - input_data, - signer, - ); - - return updateInstr; -} - -/** - * Change the owner of a given name account. - * - * @param connection The solana connection object to the RPC node - * @param name The name of the name account - * @param newOwner The new owner to be set - * @param nameClass The class of this name, if it exsists - * @param nameParent The parent name of this name, if it exists - * @param parentOwner Parent name owner - * @returns - */ -export async function transferNameOwnership( - connection: Connection, - name: string, - newOwner: PublicKey, - nameClass?: PublicKey, - nameParent?: PublicKey, - parentOwner?: PublicKey, -): Promise { - const hashed_name = getHashedNameSync(name); - const nameAccountKey = getNameAccountKeySync( - hashed_name, - nameClass, - nameParent, - ); - - let curentNameOwner: PublicKey; - if (nameClass) { - curentNameOwner = nameClass; - } else { - curentNameOwner = ( - await NameRegistryState.retrieve(connection, nameAccountKey) - ).registry.owner; - } - - const transferInstr = transferInstruction( - NAME_PROGRAM_ID, - nameAccountKey, - newOwner, - curentNameOwner, - nameClass, - nameParent, - parentOwner, - ); - - return transferInstr; -} - -/** - * Delete the name account and transfer the rent to the target. - * - * @param connection The solana connection object to the RPC node - * @param name The name of the name account - * @param refundTargetKey The refund destination address - * @param nameClass The class of this name, if it exsists - * @param nameParent The parent name of this name, if it exists - * @returns - */ -export async function deleteNameRegistry( - connection: Connection, - name: string, - refundTargetKey: PublicKey, - nameClass?: PublicKey, - nameParent?: PublicKey, -): Promise { - const hashed_name = getHashedNameSync(name); - const nameAccountKey = getNameAccountKeySync( - hashed_name, - nameClass, - nameParent, - ); - - let nameOwner: PublicKey; - if (nameClass) { - nameOwner = nameClass; - } else { - nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)) - .registry.owner; - } - - const changeAuthoritiesInstr = deleteInstruction( - NAME_PROGRAM_ID, - nameAccountKey, - refundTargetKey, - nameOwner, - ); - - return changeAuthoritiesInstr; -} - -/** - * @deprecated This function is deprecated and will be removed in future releases. Use `registerDomainNameV2` instead. - * 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 registerDomainName = 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_FEEDS.get(mint.toBase58()); - - if (!pythFeed) { - throw new SNSError(ErrorType.PythFeedNotFound); - } - - const ix = new createInstructionV3({ - name, - space, - referrerIdxOpt: refIdx != -1 ? refIdx : null, - }).getInstruction( - REGISTER_PROGRAM_ID, - NAME_PROGRAM_ID, - ROOT_DOMAIN_ACCOUNT, - nameAccount, - reverseLookupAccount, - SystemProgram.programId, - cs, - buyer, - buyerTokenAccount, - PYTH_MAPPING_ACC, - new PublicKey(pythFeed.product), - new PublicKey(pythFeed.price), - vault, - TOKEN_PROGRAM_ID, - SYSVAR_RENT_PUBKEY, - derived_state, - refTokenAccount, - ); - ixs.push(ix); - - 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 - * @param name The name of the domain - * @param feePayer The fee payer of the transaction - * @param parentName The parent name account - * @param parentNameOwner The parent name owner - * @returns - */ -export const createReverseName = async ( - nameAccount: PublicKey, - name: string, - feePayer: PublicKey, - parentName?: PublicKey, - parentNameOwner?: PublicKey, -) => { - let [centralState] = await PublicKey.findProgramAddress( - [REGISTER_PROGRAM_ID.toBuffer()], - REGISTER_PROGRAM_ID, - ); - - let hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); - let reverseLookupAccount = getNameAccountKeySync( - hashedReverseLookup, - centralState, - parentName, - ); - - let initCentralStateInstruction = new createReverseInstruction({ - name, - }).getInstruction( - REGISTER_PROGRAM_ID, - NAME_PROGRAM_ID, - ROOT_DOMAIN_ACCOUNT, - reverseLookupAccount, - SystemProgram.programId, - centralState, - feePayer, - SYSVAR_RENT_PUBKEY, - parentName, - parentNameOwner, - ); - - let instructions = [initCentralStateInstruction]; - - return [[], instructions]; -}; - -/** - * This function can be used to create a subdomain - * @param connection The Solana RPC connection object - * @param subdomain The subdomain to create with or without .sol e.g something.bonfida.sol or something.bonfida - * @param owner The owner of the parent domain creating the subdomain - * @param space The space to allocate to the subdomain (defaults to 2kb) - * @param feePayer Optional: Specifies a fee payer different from the parent owner - */ -export const createSubdomain = async ( - connection: Connection, - subdomain: string, - owner: PublicKey, - space = 2_000, - feePayer?: PublicKey, -) => { - const ixs: TransactionInstruction[] = []; - const sub = subdomain.split(".")[0]; - if (!sub) { - throw new SNSError(ErrorType.InvalidSubdomain); - } - - const { parent, pubkey } = getDomainKeySync(subdomain); - - // Space allocated to the subdomains - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - - const ix_create = await createNameRegistry( - connection, - "\0".concat(sub), - space, // Hardcode space to 2kB - feePayer || owner, - owner, - lamports, - undefined, - parent, - ); - ixs.push(ix_create); - - // Create the reverse name - const reverseKey = getReverseKeySync(subdomain, true); - const info = await connection.getAccountInfo(reverseKey); - if (!info?.data) { - const [, ix_reverse] = await createReverseName( - pubkey, - "\0".concat(sub), - feePayer || owner, - parent, - owner, - ); - ixs.push(...ix_reverse); - } - - return [[], ixs]; -}; - -/** - * This function can be used be create a record V1, it handles the serialization of the record data - * To create a SOL record use `createSolRecordInstruction` - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param record The record enum object - * @param data The data (as a UTF-8 string) to store in the record account - * @param owner The owner of the domain - * @param payer The fee payer of the transaction - * @returns - */ -export const createRecordInstruction = async ( - connection: Connection, - domain: string, - record: Record, - data: string, - owner: PublicKey, - payer: PublicKey, -) => { - check(record !== Record.SOL, ErrorType.UnsupportedRecord); - const { pubkey, hashed, parent } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V1, - ); - - const serialized = serializeRecord(data, record); - const space = serialized.length; - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - - const ix = createInstruction( - NAME_PROGRAM_ID, - SystemProgram.programId, - pubkey, - owner, - payer, - hashed, - new Numberu64(lamports), - new Numberu32(space), - undefined, - parent, - owner, - ); - - return ix; -}; - -/** - * This function can be used be create a record V2, it handles the serialization of the record data following SNS-IP 1 guidelines - * @param domain The .sol domain name - * @param record The record enum object - * @param recordV2 The `RecordV2` object that will be serialized into the record via the update instruction - * @param owner The owner of the domain - * @param payer The fee payer of the transaction - * @returns - */ -export const createRecordV2Instruction = ( - domain: string, - record: Record, - content: string, - owner: PublicKey, - payer: PublicKey, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - - const ix = allocateAndPostRecord( - payer, - pubkey, - parent, - owner, - NAME_PROGRAM_ID, - `\x02`.concat(record as string), - serializeRecordV2Content(content, record), - SNS_RECORDS_ID, - ); - return ix; -}; - -export const updateRecordInstruction = async ( - connection: Connection, - domain: string, - record: Record, - data: string, - owner: PublicKey, - payer: PublicKey, -) => { - check(record !== Record.SOL, ErrorType.UnsupportedRecord); - const { pubkey } = getDomainKeySync(`${record}.${domain}`, RecordVersion.V1); - - const info = await connection.getAccountInfo(pubkey); - check(!!info?.data, ErrorType.AccountDoesNotExist); - - const serialized = serializeRecord(data, record); - if (info?.data.slice(96).length !== serialized.length) { - // Delete + create until we can realloc accounts - return [ - deleteInstruction(NAME_PROGRAM_ID, pubkey, payer, owner), - await createRecordInstruction( - connection, - domain, - record, - data, - owner, - payer, - ), - ]; - } - - const ix = updateInstruction( - NAME_PROGRAM_ID, - pubkey, - new Numberu32(0), - serialized, - owner, - ); - - return [ix]; -}; - -/** - * This function updates the content of a record V2. The data serialization follows the SNS-IP 1 guidelines - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param record The record enum object - * @param recordV2 The `RecordV2` object to serialize into the record - * @param owner The owner of the record/domain - * @param payer The fee payer of the transaction - * @returns The update record instructions - */ -export const updateRecordV2Instruction = ( - domain: string, - record: Record, - content: string, - owner: PublicKey, - payer: PublicKey, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - - const ix = editRecord( - payer, - pubkey, - parent, - owner, - NAME_PROGRAM_ID, - `\x02`.concat(record as string), - serializeRecordV2Content(content, record), - SNS_RECORDS_ID, - ); - - return ix; -}; - -/** - * This function deletes a record v2 and returns the rent to the fee payer - * @param domain The .sol domain name - * @param record The record type enum - * @param owner The owner of the record to delete - * @param payer The fee payer of the transaction - * @returns The delete transaction instruction - */ -export const deleteRecordV2 = ( - domain: string, - record: Record, - owner: PublicKey, - payer: PublicKey, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - - const ix = deleteRecord( - payer, - parent, - owner, - pubkey, - NAME_PROGRAM_ID, - SNS_RECORDS_ID, - ); - return ix; -}; - -export const validateRecordV2Content = ( - staleness: boolean, - domain: string, - record: Record, - owner: PublicKey, - payer: PublicKey, - verifier: PublicKey, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - - const ix = validateSolanaSignature( - payer, - pubkey, - parent, - owner, - verifier, - NAME_PROGRAM_ID, - staleness, - SNS_RECORDS_ID, - ); - return ix; -}; - -export const writRoaRecordV2 = ( - domain: string, - record: Record, - owner: PublicKey, - payer: PublicKey, - roaId: PublicKey, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - const ix = writeRoa( - payer, - NAME_PROGRAM_ID, - pubkey, - parent, - owner, - roaId, - SNS_RECORDS_ID, - ); - return ix; -}; - -export const ethValidateRecordV2Content = ( - domain: string, - record: Record, - owner: PublicKey, - payer: PublicKey, - signature: Buffer, - expectedPubkey: Buffer, -) => { - let { pubkey, parent, isSub } = getDomainKeySync( - `${record}.${domain}`, - RecordVersion.V2, - ); - - if (isSub) { - parent = getDomainKeySync(domain).pubkey; - } - - if (!parent) { - throw new Error("Invalid parent"); - } - - const ix = validateEthSignature( - payer, - pubkey, - parent, - owner, - NAME_PROGRAM_ID, - Validation.Ethereum, - signature, - expectedPubkey, - SNS_RECORDS_ID, - ); - return ix; -}; - -/** - * This function can be used to create a SOL record (V1) - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param content The content of the SOL record i.e the public key to store as destination of the domain - * @param signer The signer of the SOL record i.e the owner of the domain - * @param signature The signature of the record - * @param payer The fee payer of the transaction - * @returns - */ -export const createSolRecordInstruction = async ( - connection: Connection, - domain: string, - content: PublicKey, - signer: PublicKey, - signature: Uint8Array, - payer: PublicKey, -) => { - const { pubkey, hashed, parent } = getDomainKeySync( - `${Record.SOL}.${domain}`, - RecordVersion.V1, - ); - const serialized = serializeSolRecord(content, pubkey, signer, signature); - const space = serialized.length; - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - - const ix = createInstruction( - NAME_PROGRAM_ID, - SystemProgram.programId, - pubkey, - signer, - payer, - hashed, - new Numberu64(lamports), - new Numberu32(space), - undefined, - parent, - signer, - ); - - return [ix]; -}; - -export const updateSolRecordInstruction = async ( - connection: Connection, - domain: string, - content: PublicKey, - signer: PublicKey, - signature: Uint8Array, - payer: PublicKey, -) => { - const { pubkey } = getDomainKeySync( - `${Record.SOL}.${domain}`, - RecordVersion.V1, - ); - - const info = await connection.getAccountInfo(pubkey); - check(!!info?.data, ErrorType.AccountDoesNotExist); - - if (info?.data.length !== 96) { - return [ - deleteInstruction(NAME_PROGRAM_ID, pubkey, payer, signer), - await createSolRecordInstruction( - connection, - domain, - content, - signer, - signature, - payer, - ), - ]; - } - - const serialized = serializeSolRecord(content, pubkey, signer, signature); - const ix = updateInstruction( - NAME_PROGRAM_ID, - pubkey, - new Numberu32(0), - serialized, - signer, - ); - - return [ix]; -}; - -export const burnDomain = ( - domain: string, - owner: PublicKey, - target: PublicKey, -) => { - const { pubkey } = getDomainKeySync(domain); - const [state] = PublicKey.findProgramAddressSync( - [pubkey.toBuffer()], - REGISTER_PROGRAM_ID, - ); - const [resellingState] = PublicKey.findProgramAddressSync( - [pubkey.toBuffer(), Uint8Array.from([1, 1])], - REGISTER_PROGRAM_ID, - ); - - const ix = new burnInstruction().getInstruction( - REGISTER_PROGRAM_ID, - NAME_PROGRAM_ID, - SystemProgram.programId, - pubkey, - getReverseKeySync(domain), - resellingState, - state, - REVERSE_LOOKUP_CLASS, - owner, - target, - ); - return ix; -}; - -export const registerWithNft = ( - name: string, - space: number, - nameAccount: PublicKey, - reverseLookupAccount: PublicKey, - buyer: PublicKey, - nftSource: PublicKey, - nftMetadata: PublicKey, - nftMint: PublicKey, - masterEdition: PublicKey, -) => { - const [state] = PublicKey.findProgramAddressSync( - [nameAccount.toBuffer()], - REGISTER_PROGRAM_ID, - ); - const ix = new createWithNftInstruction({ space, name }).getInstruction( - REGISTER_PROGRAM_ID, - NAME_PROGRAM_ID, - ROOT_DOMAIN_ACCOUNT, - nameAccount, - reverseLookupAccount, - SystemProgram.programId, - REVERSE_LOOKUP_CLASS, - buyer, - nftSource, - nftMetadata, - nftMint, - masterEdition, - WOLVES_COLLECTION_METADATA, - TOKEN_PROGRAM_ID, - SYSVAR_RENT_PUBKEY, - state, - METAPLEX_ID, - ); - return ix; -}; - -/** - * This function is used to transfer the ownership of a subdomain in the Solana Name Service. - * - * @param {Connection} connection - The Solana RPC connection object. - * @param {string} subdomain - The subdomain to transfer. It can be with or without .sol suffix (e.g., 'something.bonfida.sol' or 'something.bonfida'). - * @param {PublicKey} newOwner - The public key of the new owner of the subdomain. - * @param {boolean} [isParentOwnerSigner=false] - A flag indicating whether the parent name owner is signing this transfer. - * @param {PublicKey} [owner] - The public key of the current owner of the subdomain. This is an optional parameter. If not provided, the owner will be resolved automatically. This can be helpful to build transactions when the subdomain does not exist yet. - * - * @returns {Promise} - A promise that resolves to a Solana instruction for the transfer operation. - */ -export const transferSubdomain = async ( - connection: Connection, - subdomain: string, - newOwner: PublicKey, - isParentOwnerSigner?: boolean, - owner?: PublicKey, -): Promise => { - const { pubkey, isSub, parent } = getDomainKeySync(subdomain); - - if (!parent || !isSub) { - throw new SNSError(ErrorType.InvalidSubdomain); - } - - if (!owner) { - const { registry } = await NameRegistryState.retrieve(connection, pubkey); - owner = registry.owner; - } - - let nameParent: PublicKey | undefined = undefined; - let nameParentOwner: PublicKey | undefined = undefined; - - if (isParentOwnerSigner) { - nameParent = parent; - nameParentOwner = (await NameRegistryState.retrieve(connection, parent)) - .registry.owner; - } - - const ix = transferInstruction( - NAME_PROGRAM_ID, - pubkey, - newOwner, - owner, - undefined, - nameParent, - nameParentOwner, - ); - - return ix; -}; - -/** - * This function can be used to register a domain name as favorite - * @param nameAccount The name account being registered as favorite - * @param owner The owner of the name account - * @param programId The name offer program ID - * @returns - */ -export const registerFavorite = (nameAccount: PublicKey, owner: PublicKey) => { - const [favKey] = FavouriteDomain.getKeySync(NAME_OFFERS_ID, owner); - const ix = new registerFavoriteInstruction().getInstruction( - NAME_OFFERS_ID, - nameAccount, - favKey, - owner, - SystemProgram.programId, - ); - return [ix]; -}; diff --git a/js/src/bindings/burnDomain.ts b/js/src/bindings/burnDomain.ts new file mode 100644 index 0000000..c64ec77 --- /dev/null +++ b/js/src/bindings/burnDomain.ts @@ -0,0 +1,39 @@ +import { PublicKey, SystemProgram } from "@solana/web3.js"; +import { burnInstruction } from "../instructions/burnInstruction"; +import { + NAME_PROGRAM_ID, + REGISTER_PROGRAM_ID, + REVERSE_LOOKUP_CLASS, +} from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { getReverseKeySync } from "../utils/getReverseKeySync"; + +export const burnDomain = ( + domain: string, + owner: PublicKey, + target: PublicKey, +) => { + const { pubkey } = getDomainKeySync(domain); + const [state] = PublicKey.findProgramAddressSync( + [pubkey.toBuffer()], + REGISTER_PROGRAM_ID, + ); + const [resellingState] = PublicKey.findProgramAddressSync( + [pubkey.toBuffer(), Uint8Array.from([1, 1])], + REGISTER_PROGRAM_ID, + ); + + const ix = new burnInstruction().getInstruction( + REGISTER_PROGRAM_ID, + NAME_PROGRAM_ID, + SystemProgram.programId, + pubkey, + getReverseKeySync(domain), + resellingState, + state, + REVERSE_LOOKUP_CLASS, + owner, + target, + ); + return ix; +}; diff --git a/js/src/bindings/createNameRegistry.ts b/js/src/bindings/createNameRegistry.ts new file mode 100644 index 0000000..7d9c2a5 --- /dev/null +++ b/js/src/bindings/createNameRegistry.ts @@ -0,0 +1,72 @@ +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { createInstruction } from "../instructions/createInstruction"; +import { Numberu64, Numberu32 } from "../int"; +import { getNameOwner } from "../deprecated/utils"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * Creates a name account with the given rent budget, allocated space, owner and class. + * + * @param connection The solana connection object to the RPC node + * @param name The name of the new account + * @param space The space in bytes allocated to the account + * @param payerKey The allocation cost payer + * @param nameOwner The pubkey to be set as owner of the new name account + * @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption + * @param nameClass The class of this new name + * @param parentName The parent name of the new name. If specified its owner needs to sign + * @returns + */ +export async function createNameRegistry( + connection: Connection, + name: string, + space: number, + payerKey: PublicKey, + nameOwner: PublicKey, + lamports?: number, + nameClass?: PublicKey, + parentName?: PublicKey, +): Promise { + const hashed_name = getHashedNameSync(name); + const nameAccountKey = getNameAccountKeySync( + hashed_name, + nameClass, + parentName, + ); + + const balance = lamports + ? lamports + : await connection.getMinimumBalanceForRentExemption(space); + + let nameParentOwner: PublicKey | undefined; + if (parentName) { + const { registry: parentAccount } = await getNameOwner( + connection, + parentName, + ); + nameParentOwner = parentAccount.owner; + } + + const createNameInstr = createInstruction( + NAME_PROGRAM_ID, + SystemProgram.programId, + nameAccountKey, + nameOwner, + payerKey, + hashed_name, + new Numberu64(balance), + new Numberu32(space), + nameClass, + parentName, + nameParentOwner, + ); + + return createNameInstr; +} diff --git a/js/src/bindings/createRecordInstruction.ts b/js/src/bindings/createRecordInstruction.ts new file mode 100644 index 0000000..ec3791e --- /dev/null +++ b/js/src/bindings/createRecordInstruction.ts @@ -0,0 +1,63 @@ +import { Connection, PublicKey, SystemProgram } from "@solana/web3.js"; +import { createInstruction } from "../instructions/createInstruction"; +import { NameRegistryState } from "../state"; +import { Numberu64, Numberu32 } from "../int"; +import { NAME_PROGRAM_ID } from "../constants"; +import { check } from "../utils/check"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { serializeRecord } from "../record/serializeRecord"; +import { Record, RecordVersion } from "../types/record"; +import { UnsupportedRecordError } from "../error"; + +/** + * This function can be used be create a record V1, it handles the serialization of the record data + * To create a SOL record use `createSolRecordInstruction` + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param record The record enum object + * @param data The data (as a UTF-8 string) to store in the record account + * @param owner The owner of the domain + * @param payer The fee payer of the transaction + * @returns + */ +export const createRecordInstruction = async ( + connection: Connection, + domain: string, + record: Record, + data: string, + owner: PublicKey, + payer: PublicKey, +) => { + check( + record !== Record.SOL, + new UnsupportedRecordError( + "SOL record is not supported for this instruction", + ), + ); + const { pubkey, hashed, parent } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V1, + ); + + const serialized = serializeRecord(data, record); + const space = serialized.length; + const lamports = await connection.getMinimumBalanceForRentExemption( + space + NameRegistryState.HEADER_LEN, + ); + + const ix = createInstruction( + NAME_PROGRAM_ID, + SystemProgram.programId, + pubkey, + owner, + payer, + hashed, + new Numberu64(lamports), + new Numberu32(space), + undefined, + parent, + owner, + ); + + return ix; +}; diff --git a/js/src/bindings/createRecordV2Instruction.ts b/js/src/bindings/createRecordV2Instruction.ts new file mode 100644 index 0000000..ba73317 --- /dev/null +++ b/js/src/bindings/createRecordV2Instruction.ts @@ -0,0 +1,49 @@ +import { allocateAndPostRecord, SNS_RECORDS_ID } from "@bonfida/sns-records"; +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { serializeRecordV2Content } from "../record_v2/serializeRecordV2Content"; +import { InvalidParrentError } from "../error"; + +/** + * This function can be used be create a record V2, it handles the serialization of the record data following SNS-IP 1 guidelines + * @param domain The .sol domain name + * @param record The record enum object + * @param recordV2 The `RecordV2` object that will be serialized into the record via the update instruction + * @param owner The owner of the domain + * @param payer The fee payer of the transaction + * @returns + */ +export const createRecordV2Instruction = ( + domain: string, + record: Record, + content: string, + owner: PublicKey, + payer: PublicKey, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + + const ix = allocateAndPostRecord( + payer, + pubkey, + parent, + owner, + NAME_PROGRAM_ID, + `\x02`.concat(record as string), + serializeRecordV2Content(content, record), + SNS_RECORDS_ID, + ); + return ix; +}; diff --git a/js/src/bindings/createReverseName.ts b/js/src/bindings/createReverseName.ts new file mode 100644 index 0000000..888e255 --- /dev/null +++ b/js/src/bindings/createReverseName.ts @@ -0,0 +1,53 @@ +import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; +import { createReverseInstruction } from "../instructions/createReverseInstruction"; +import { + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + REGISTER_PROGRAM_ID, + CENTRAL_STATE, +} from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * + * @param nameAccount The name account to create the reverse account for + * @param name The name of the domain + * @param feePayer The fee payer of the transaction + * @param parentName The parent name account + * @param parentNameOwner The parent name owner + * @returns + */ +export const createReverseName = async ( + nameAccount: PublicKey, + name: string, + feePayer: PublicKey, + parentName?: PublicKey, + parentNameOwner?: PublicKey, +) => { + let hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); + let reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + CENTRAL_STATE, + parentName, + ); + + let initCentralStateInstruction = new createReverseInstruction({ + name, + }).getInstruction( + REGISTER_PROGRAM_ID, + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + reverseLookupAccount, + SystemProgram.programId, + CENTRAL_STATE, + feePayer, + SYSVAR_RENT_PUBKEY, + parentName, + parentNameOwner, + ); + + let instructions = [initCentralStateInstruction]; + + return instructions; +}; diff --git a/js/src/bindings/createSolRecordInstruction.ts b/js/src/bindings/createSolRecordInstruction.ts new file mode 100644 index 0000000..4b0a340 --- /dev/null +++ b/js/src/bindings/createSolRecordInstruction.ts @@ -0,0 +1,53 @@ +import { Connection, PublicKey, SystemProgram } from "@solana/web3.js"; +import { createInstruction } from "../instructions/createInstruction"; +import { NameRegistryState } from "../state"; +import { Numberu64, Numberu32 } from "../int"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { serializeSolRecord } from "../record/serializeSolRecord"; +import { Record, RecordVersion } from "../types/record"; + +/** + * This function can be used to create a SOL record (V1) + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param content The content of the SOL record i.e the public key to store as destination of the domain + * @param signer The signer of the SOL record i.e the owner of the domain + * @param signature The signature of the record + * @param payer The fee payer of the transaction + * @returns + */ +export const createSolRecordInstruction = async ( + connection: Connection, + domain: string, + content: PublicKey, + signer: PublicKey, + signature: Uint8Array, + payer: PublicKey, +) => { + const { pubkey, hashed, parent } = getDomainKeySync( + `${Record.SOL}.${domain}`, + RecordVersion.V1, + ); + const serialized = serializeSolRecord(content, pubkey, signer, signature); + const space = serialized.length; + const lamports = await connection.getMinimumBalanceForRentExemption( + space + NameRegistryState.HEADER_LEN, + ); + + const ix = createInstruction( + NAME_PROGRAM_ID, + SystemProgram.programId, + pubkey, + signer, + payer, + hashed, + new Numberu64(lamports), + new Numberu32(space), + undefined, + parent, + signer, + ); + + return ix; +}; diff --git a/js/src/bindings/createSubdomain.ts b/js/src/bindings/createSubdomain.ts new file mode 100644 index 0000000..3c1fb54 --- /dev/null +++ b/js/src/bindings/createSubdomain.ts @@ -0,0 +1,65 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { NameRegistryState } from "../state"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { getReverseKeySync } from "../utils/getReverseKeySync"; +import { InvalidDomainError } from "../error"; + +import { createNameRegistry } from "./createNameRegistry"; +import { createReverseName } from "./createReverseName"; + +/** + * This function can be used to create a subdomain + * @param connection The Solana RPC connection object + * @param subdomain The subdomain to create with or without .sol e.g something.bonfida.sol or something.bonfida + * @param owner The owner of the parent domain creating the subdomain + * @param space The space to allocate to the subdomain (defaults to 2kb) + * @param feePayer Optional: Specifies a fee payer different from the parent owner + */ +export const createSubdomain = async ( + connection: Connection, + subdomain: string, + owner: PublicKey, + space = 2_000, + feePayer?: PublicKey, +) => { + const ixs: TransactionInstruction[] = []; + const sub = subdomain.split(".")[0]; + if (!sub) { + throw new InvalidDomainError("The subdomain name is malformed"); + } + + const { parent, pubkey } = getDomainKeySync(subdomain); + + // Space allocated to the subdomains + const lamports = await connection.getMinimumBalanceForRentExemption( + space + NameRegistryState.HEADER_LEN, + ); + + const ix_create = await createNameRegistry( + connection, + "\0".concat(sub), + space, // Hardcode space to 2kB + feePayer || owner, + owner, + lamports, + undefined, + parent, + ); + ixs.push(ix_create); + + // Create the reverse name + const reverseKey = getReverseKeySync(subdomain, true); + const info = await connection.getAccountInfo(reverseKey); + if (!info?.data) { + const ix_reverse = await createReverseName( + pubkey, + "\0".concat(sub), + feePayer || owner, + parent, + owner, + ); + ixs.push(...ix_reverse); + } + + return ixs; +}; diff --git a/js/src/bindings/deleteNameRegistry.ts b/js/src/bindings/deleteNameRegistry.ts new file mode 100644 index 0000000..67378bb --- /dev/null +++ b/js/src/bindings/deleteNameRegistry.ts @@ -0,0 +1,48 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { deleteInstruction } from "../instructions/deleteInstruction"; +import { NameRegistryState } from "../state"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * Delete the name account and transfer the rent to the target. + * + * @param connection The solana connection object to the RPC node + * @param name The name of the name account + * @param refundTargetKey The refund destination address + * @param nameClass The class of this name, if it exsists + * @param nameParent The parent name of this name, if it exists + * @returns + */ +export async function deleteNameRegistry( + connection: Connection, + name: string, + refundTargetKey: PublicKey, + nameClass?: PublicKey, + nameParent?: PublicKey, +): Promise { + const hashed_name = getHashedNameSync(name); + const nameAccountKey = getNameAccountKeySync( + hashed_name, + nameClass, + nameParent, + ); + + let nameOwner: PublicKey; + if (nameClass) { + nameOwner = nameClass; + } else { + nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)) + .registry.owner; + } + + const changeAuthoritiesInstr = deleteInstruction( + NAME_PROGRAM_ID, + nameAccountKey, + refundTargetKey, + nameOwner, + ); + + return changeAuthoritiesInstr; +} diff --git a/js/src/bindings/deleteRecordV2.ts b/js/src/bindings/deleteRecordV2.ts new file mode 100644 index 0000000..c5ee4e6 --- /dev/null +++ b/js/src/bindings/deleteRecordV2.ts @@ -0,0 +1,44 @@ +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { SNS_RECORDS_ID, deleteRecord } from "@bonfida/sns-records"; +import { InvalidParrentError } from "../error"; + +/** + * This function deletes a record v2 and returns the rent to the fee payer + * @param domain The .sol domain name + * @param record The record type enum + * @param owner The owner of the record to delete + * @param payer The fee payer of the transaction + * @returns The delete transaction instruction + */ +export const deleteRecordV2 = ( + domain: string, + record: Record, + owner: PublicKey, + payer: PublicKey, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + + const ix = deleteRecord( + payer, + parent, + owner, + pubkey, + NAME_PROGRAM_ID, + SNS_RECORDS_ID, + ); + return ix; +}; diff --git a/js/src/bindings/ethValidateRecordV2Content.ts b/js/src/bindings/ethValidateRecordV2Content.ts new file mode 100644 index 0000000..cccb5f0 --- /dev/null +++ b/js/src/bindings/ethValidateRecordV2Content.ts @@ -0,0 +1,46 @@ +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { + SNS_RECORDS_ID, + validateEthSignature, + Validation, +} from "@bonfida/sns-records"; +import { InvalidParrentError } from "../error"; + +export const ethValidateRecordV2Content = ( + domain: string, + record: Record, + owner: PublicKey, + payer: PublicKey, + signature: Buffer, + expectedPubkey: Buffer, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + + const ix = validateEthSignature( + payer, + pubkey, + parent, + owner, + NAME_PROGRAM_ID, + Validation.Ethereum, + signature, + expectedPubkey, + SNS_RECORDS_ID, + ); + return ix; +}; diff --git a/js/src/bindings/registerDomainName.ts b/js/src/bindings/registerDomainName.ts new file mode 100644 index 0000000..1aea2fc --- /dev/null +++ b/js/src/bindings/registerDomainName.ts @@ -0,0 +1,127 @@ +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, + SYSVAR_RENT_PUBKEY, +} from "@solana/web3.js"; +import { createInstructionV3 } from "../instructions/createInstructionV3"; +import { + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + REGISTER_PROGRAM_ID, + REFERRERS, + USDC_MINT, + PYTH_FEEDS, + PYTH_MAPPING_ACC, + VAULT_OWNER, + CENTRAL_STATE, +} from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, +} from "@solana/spl-token"; +import { InvalidDomainError, PythFeedNotFoundError } from "../error"; + +/** + * @deprecated This function is deprecated and will be removed in future releases. Use `registerDomainNameV2` instead. + * 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 registerDomainName = 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 InvalidDomainError("The domain name is malformed"); + } + + const hashed = getHashedNameSync(name); + const nameAccount = getNameAccountKeySync( + hashed, + undefined, + ROOT_DOMAIN_ACCOUNT, + ); + + const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); + const reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + CENTRAL_STATE, + ); + + 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_FEEDS.get(mint.toBase58()); + + if (!pythFeed) { + throw new PythFeedNotFoundError( + "The Pyth account for the provided mint was not found", + ); + } + + const ix = new createInstructionV3({ + name, + space, + referrerIdxOpt: refIdx != -1 ? refIdx : null, + }).getInstruction( + REGISTER_PROGRAM_ID, + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + nameAccount, + reverseLookupAccount, + SystemProgram.programId, + CENTRAL_STATE, + buyer, + buyerTokenAccount, + PYTH_MAPPING_ACC, + new PublicKey(pythFeed.product), + new PublicKey(pythFeed.price), + vault, + TOKEN_PROGRAM_ID, + SYSVAR_RENT_PUBKEY, + derived_state, + refTokenAccount, + ); + ixs.push(ix); + + return ixs; +}; diff --git a/js/src/bindings/registerDomainNameV2.ts b/js/src/bindings/registerDomainNameV2.ts new file mode 100644 index 0000000..85a6b04 --- /dev/null +++ b/js/src/bindings/registerDomainNameV2.ts @@ -0,0 +1,128 @@ +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, + SYSVAR_RENT_PUBKEY, +} from "@solana/web3.js"; +import { createSplitV2Instruction } from "../instructions/createSplitV2Instruction"; +import { + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + REGISTER_PROGRAM_ID, + REFERRERS, + USDC_MINT, + VAULT_OWNER, + PYTH_PULL_FEEDS, + CENTRAL_STATE, +} from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { getPythFeedAccountKey } from "../utils/getPythFeedAccountKey"; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, +} from "@solana/spl-token"; +import { InvalidDomainError, PythFeedNotFoundError } from "../error"; + +/** + * 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 InvalidDomainError("The domain name is malformed"); + } + + const hashed = getHashedNameSync(name); + const nameAccount = getNameAccountKeySync( + hashed, + undefined, + ROOT_DOMAIN_ACCOUNT, + ); + + const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); + const reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + CENTRAL_STATE, + ); + + 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 PythFeedNotFoundError( + "The Pyth account for the provided mint was not found", + ); + } + + 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, + CENTRAL_STATE, + buyer, + buyer, + buyer, + buyerTokenAccount, + pythFeedAccount, + vault, + TOKEN_PROGRAM_ID, + SYSVAR_RENT_PUBKEY, + derived_state, + refTokenAccount, + ); + ixs.push(ix); + + return ixs; +}; diff --git a/js/src/bindings/registerFavorite.ts b/js/src/bindings/registerFavorite.ts new file mode 100644 index 0000000..b0b01a3 --- /dev/null +++ b/js/src/bindings/registerFavorite.ts @@ -0,0 +1,40 @@ +import { Connection, PublicKey, SystemProgram } from "@solana/web3.js"; +import { registerFavoriteInstruction } from "../instructions/registerFavoriteInstruction"; +import { FavouriteDomain, NAME_OFFERS_ID } from "../favorite-domain"; +import { NameRegistryState } from "../state"; +import { ROOT_DOMAIN_ACCOUNT } from "../constants"; + +/** + * This function can be used to register a domain name as favorite + * @param nameAccount The name account being registered as favorite + * @param owner The owner of the name account + * @param programId The name offer program ID + * @returns + */ +export const registerFavorite = async ( + connection: Connection, + nameAccount: PublicKey, + owner: PublicKey, +) => { + let parent: PublicKey | undefined = undefined; + const { registry } = await NameRegistryState.retrieve( + connection, + nameAccount, + ); + if (!registry.parentName.equals(ROOT_DOMAIN_ACCOUNT)) { + parent = registry.parentName; + } + + const [favKey] = await FavouriteDomain.getKey(NAME_OFFERS_ID, owner); + const ix = new registerFavoriteInstruction().getInstruction( + NAME_OFFERS_ID, + nameAccount, + favKey, + owner, + SystemProgram.programId, + parent, + ); + return ix; +}; + +export { registerFavorite as setPrimaryDomain }; diff --git a/js/src/bindings/registerWithNft.ts b/js/src/bindings/registerWithNft.ts new file mode 100644 index 0000000..706f079 --- /dev/null +++ b/js/src/bindings/registerWithNft.ts @@ -0,0 +1,48 @@ +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey, SystemProgram, SYSVAR_RENT_PUBKEY } from "@solana/web3.js"; +import { createWithNftInstruction } from "../instructions/createWithNftInstruction"; +import { + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + REGISTER_PROGRAM_ID, + REVERSE_LOOKUP_CLASS, + WOLVES_COLLECTION_METADATA, + METAPLEX_ID, +} from "../constants"; + +export const registerWithNft = ( + name: string, + space: number, + nameAccount: PublicKey, + reverseLookupAccount: PublicKey, + buyer: PublicKey, + nftSource: PublicKey, + nftMetadata: PublicKey, + nftMint: PublicKey, + masterEdition: PublicKey, +) => { + const [state] = PublicKey.findProgramAddressSync( + [nameAccount.toBuffer()], + REGISTER_PROGRAM_ID, + ); + const ix = new createWithNftInstruction({ space, name }).getInstruction( + REGISTER_PROGRAM_ID, + NAME_PROGRAM_ID, + ROOT_DOMAIN_ACCOUNT, + nameAccount, + reverseLookupAccount, + SystemProgram.programId, + REVERSE_LOOKUP_CLASS, + buyer, + nftSource, + nftMetadata, + nftMint, + masterEdition, + WOLVES_COLLECTION_METADATA, + TOKEN_PROGRAM_ID, + SYSVAR_RENT_PUBKEY, + state, + METAPLEX_ID, + ); + return ix; +}; diff --git a/js/src/bindings/transferNameOwnership.ts b/js/src/bindings/transferNameOwnership.ts new file mode 100644 index 0000000..f9bfbca --- /dev/null +++ b/js/src/bindings/transferNameOwnership.ts @@ -0,0 +1,54 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { transferInstruction } from "../instructions/transferInstruction"; +import { NameRegistryState } from "../state"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * Change the owner of a given name account. + * + * @param connection The solana connection object to the RPC node + * @param name The name of the name account + * @param newOwner The new owner to be set + * @param nameClass The class of this name, if it exsists + * @param nameParent The parent name of this name, if it exists + * @param parentOwner Parent name owner + * @returns + */ +export async function transferNameOwnership( + connection: Connection, + name: string, + newOwner: PublicKey, + nameClass?: PublicKey, + nameParent?: PublicKey, + parentOwner?: PublicKey, +): Promise { + const hashed_name = getHashedNameSync(name); + const nameAccountKey = getNameAccountKeySync( + hashed_name, + nameClass, + nameParent, + ); + + let curentNameOwner: PublicKey; + if (nameClass) { + curentNameOwner = nameClass; + } else { + curentNameOwner = ( + await NameRegistryState.retrieve(connection, nameAccountKey) + ).registry.owner; + } + + const transferInstr = transferInstruction( + NAME_PROGRAM_ID, + nameAccountKey, + newOwner, + curentNameOwner, + nameClass, + nameParent, + parentOwner, + ); + + return transferInstr; +} diff --git a/js/src/bindings/transferSubdomain.ts b/js/src/bindings/transferSubdomain.ts new file mode 100644 index 0000000..16686a9 --- /dev/null +++ b/js/src/bindings/transferSubdomain.ts @@ -0,0 +1,57 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { transferInstruction } from "../instructions/transferInstruction"; +import { NameRegistryState } from "../state"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { InvalidSubdomainError } from "../error"; + +/** + * This function is used to transfer the ownership of a subdomain in the Solana Name Service. + * + * @param {Connection} connection - The Solana RPC connection object. + * @param {string} subdomain - The subdomain to transfer. It can be with or without .sol suffix (e.g., 'something.bonfida.sol' or 'something.bonfida'). + * @param {PublicKey} newOwner - The public key of the new owner of the subdomain. + * @param {boolean} [isParentOwnerSigner=false] - A flag indicating whether the parent name owner is signing this transfer. + * @param {PublicKey} [owner] - The public key of the current owner of the subdomain. This is an optional parameter. If not provided, the owner will be resolved automatically. This can be helpful to build transactions when the subdomain does not exist yet. + * + * @returns {Promise} - A promise that resolves to a Solana instruction for the transfer operation. + */ +export const transferSubdomain = async ( + connection: Connection, + subdomain: string, + newOwner: PublicKey, + isParentOwnerSigner?: boolean, + owner?: PublicKey, +): Promise => { + const { pubkey, isSub, parent } = getDomainKeySync(subdomain); + + if (!parent || !isSub) { + throw new InvalidSubdomainError("The subdomain is not valid"); + } + + if (!owner) { + const { registry } = await NameRegistryState.retrieve(connection, pubkey); + owner = registry.owner; + } + + let nameParent: PublicKey | undefined = undefined; + let nameParentOwner: PublicKey | undefined = undefined; + + if (isParentOwnerSigner) { + nameParent = parent; + nameParentOwner = (await NameRegistryState.retrieve(connection, parent)) + .registry.owner; + } + + const ix = transferInstruction( + NAME_PROGRAM_ID, + pubkey, + newOwner, + owner, + undefined, + nameParent, + nameParentOwner, + ); + + return ix; +}; diff --git a/js/src/bindings/updateNameRegistryData.ts b/js/src/bindings/updateNameRegistryData.ts new file mode 100644 index 0000000..f2f3f1d --- /dev/null +++ b/js/src/bindings/updateNameRegistryData.ts @@ -0,0 +1,52 @@ +import { Buffer } from "buffer"; +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { updateInstruction } from "../instructions/updateInstruction"; +import { NameRegistryState } from "../state"; +import { Numberu32 } from "../int"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * Overwrite the data of the given name registry. + * + * @param connection The solana connection object to the RPC node + * @param name The name of the name registry to update + * @param offset The offset to which the data should be written into the registry + * @param input_data The data to be written + * @param nameClass The class of this name, if it exsists + * @param nameParent The parent name of this name, if it exists + */ +export async function updateNameRegistryData( + connection: Connection, + name: string, + offset: number, + input_data: Buffer, + nameClass?: PublicKey, + nameParent?: PublicKey, +): Promise { + const hashed_name = getHashedNameSync(name); + const nameAccountKey = getNameAccountKeySync( + hashed_name, + nameClass, + nameParent, + ); + + let signer: PublicKey; + if (nameClass) { + signer = nameClass; + } else { + signer = (await NameRegistryState.retrieve(connection, nameAccountKey)) + .registry.owner; + } + + const updateInstr = updateInstruction( + NAME_PROGRAM_ID, + nameAccountKey, + new Numberu32(offset), + input_data, + signer, + ); + + return updateInstr; +} diff --git a/js/src/bindings/updateRecordInstruction.ts b/js/src/bindings/updateRecordInstruction.ts new file mode 100644 index 0000000..42739b0 --- /dev/null +++ b/js/src/bindings/updateRecordInstruction.ts @@ -0,0 +1,60 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { deleteInstruction } from "../instructions/deleteInstruction"; +import { updateInstruction } from "../instructions/updateInstruction"; +import { Numberu32 } from "../int"; +import { NAME_PROGRAM_ID } from "../constants"; +import { check } from "../utils/check"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { serializeRecord } from "../record/serializeRecord"; +import { Record, RecordVersion } from "../types/record"; +import { AccountDoesNotExistError, UnsupportedRecordError } from "../error"; +import { createRecordInstruction } from "./createRecordInstruction"; + +export const updateRecordInstruction = async ( + connection: Connection, + domain: string, + record: Record, + data: string, + owner: PublicKey, + payer: PublicKey, +) => { + check( + record !== Record.SOL, + new UnsupportedRecordError( + "SOL record is not supported for this instruction", + ), + ); + const { pubkey } = getDomainKeySync(`${record}.${domain}`, RecordVersion.V1); + + const info = await connection.getAccountInfo(pubkey); + check( + !!info?.data, + new AccountDoesNotExistError("The record account does not exist"), + ); + + const serialized = serializeRecord(data, record); + if (info?.data.slice(96).length !== serialized.length) { + // Delete + create until we can realloc accounts + return [ + deleteInstruction(NAME_PROGRAM_ID, pubkey, payer, owner), + await createRecordInstruction( + connection, + domain, + record, + data, + owner, + payer, + ), + ]; + } + + const ix = updateInstruction( + NAME_PROGRAM_ID, + pubkey, + new Numberu32(0), + serialized, + owner, + ); + + return ix; +}; diff --git a/js/src/bindings/updateRecordV2Instruction.ts b/js/src/bindings/updateRecordV2Instruction.ts new file mode 100644 index 0000000..b272295 --- /dev/null +++ b/js/src/bindings/updateRecordV2Instruction.ts @@ -0,0 +1,51 @@ +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { serializeRecordV2Content } from "../record_v2/serializeRecordV2Content"; +import { editRecord, SNS_RECORDS_ID } from "@bonfida/sns-records"; +import { InvalidParrentError } from "../error"; + +/** + * This function updates the content of a record V2. The data serialization follows the SNS-IP 1 guidelines + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param record The record enum object + * @param recordV2 The `RecordV2` object to serialize into the record + * @param owner The owner of the record/domain + * @param payer The fee payer of the transaction + * @returns The update record instructions + */ +export const updateRecordV2Instruction = ( + domain: string, + record: Record, + content: string, + owner: PublicKey, + payer: PublicKey, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + + const ix = editRecord( + payer, + pubkey, + parent, + owner, + NAME_PROGRAM_ID, + `\x02`.concat(record as string), + serializeRecordV2Content(content, record), + SNS_RECORDS_ID, + ); + + return ix; +}; diff --git a/js/src/bindings/updateSolRecordInstruction.ts b/js/src/bindings/updateSolRecordInstruction.ts new file mode 100644 index 0000000..d3fc4e5 --- /dev/null +++ b/js/src/bindings/updateSolRecordInstruction.ts @@ -0,0 +1,57 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { deleteInstruction } from "../instructions/deleteInstruction"; +import { updateInstruction } from "../instructions/updateInstruction"; +import { Numberu32 } from "../int"; +import { NAME_PROGRAM_ID } from "../constants"; +import { check } from "../utils/check"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { serializeSolRecord } from "../record/serializeSolRecord"; +import { Record, RecordVersion } from "../types/record"; +import { AccountDoesNotExistError } from "../error"; + +import { createSolRecordInstruction } from "./createSolRecordInstruction"; + +export const updateSolRecordInstruction = async ( + connection: Connection, + domain: string, + content: PublicKey, + signer: PublicKey, + signature: Uint8Array, + payer: PublicKey, +) => { + const { pubkey } = getDomainKeySync( + `${Record.SOL}.${domain}`, + RecordVersion.V1, + ); + + const info = await connection.getAccountInfo(pubkey); + check( + !!info?.data, + new AccountDoesNotExistError("The record account does not exist"), + ); + + if (info?.data.length !== 96) { + return [ + deleteInstruction(NAME_PROGRAM_ID, pubkey, payer, signer), + await createSolRecordInstruction( + connection, + domain, + content, + signer, + signature, + payer, + ), + ]; + } + + const serialized = serializeSolRecord(content, pubkey, signer, signature); + const ix = updateInstruction( + NAME_PROGRAM_ID, + pubkey, + new Numberu32(0), + serialized, + signer, + ); + + return ix; +}; diff --git a/js/src/bindings/validateRecordV2Content.ts b/js/src/bindings/validateRecordV2Content.ts new file mode 100644 index 0000000..4615275 --- /dev/null +++ b/js/src/bindings/validateRecordV2Content.ts @@ -0,0 +1,40 @@ +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { SNS_RECORDS_ID, validateSolanaSignature } from "@bonfida/sns-records"; +import { InvalidParrentError } from "../error"; + +export const validateRecordV2Content = ( + staleness: boolean, + domain: string, + record: Record, + owner: PublicKey, + payer: PublicKey, + verifier: PublicKey, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + + const ix = validateSolanaSignature( + payer, + pubkey, + parent, + owner, + verifier, + NAME_PROGRAM_ID, + staleness, + SNS_RECORDS_ID, + ); + return ix; +}; diff --git a/js/src/bindings/writRoaRecordV2.ts b/js/src/bindings/writRoaRecordV2.ts new file mode 100644 index 0000000..fe53236 --- /dev/null +++ b/js/src/bindings/writRoaRecordV2.ts @@ -0,0 +1,37 @@ +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { Record, RecordVersion } from "../types/record"; +import { SNS_RECORDS_ID, writeRoa } from "@bonfida/sns-records"; +import { InvalidParrentError } from "../error"; + +export const writRoaRecordV2 = ( + domain: string, + record: Record, + owner: PublicKey, + payer: PublicKey, + roaId: PublicKey, +) => { + let { pubkey, parent, isSub } = getDomainKeySync( + `${record}.${domain}`, + RecordVersion.V2, + ); + + if (isSub) { + parent = getDomainKeySync(domain).pubkey; + } + + if (!parent) { + throw new InvalidParrentError("Parent could not be found"); + } + const ix = writeRoa( + payer, + NAME_PROGRAM_ID, + pubkey, + parent, + owner, + roaId, + SNS_RECORDS_ID, + ); + return ix; +}; diff --git a/js/src/constants.ts b/js/src/constants.ts index f142d30..a0a103b 100644 --- a/js/src/constants.ts +++ b/js/src/constants.ts @@ -26,20 +26,6 @@ export const REGISTER_PROGRAM_ID = new PublicKey( "jCebN34bUfdeUYJT13J1yG16XWQpt5PDx6Mse9GUqhR", ); -/** - * The FIDA Pyth price feed - */ -export const PYTH_FIDA_PRICE_ACC = new PublicKey( - "ETp9eKXVv1dWwHSpsXRUuXHmw24PwRkttCGVgpZEY9zF", -); - -/** - * The FIDA buy and burn address - */ -export const BONFIDA_FIDA_BNB = new PublicKey( - "AUoZ3YAhV3b2rZeEH93UMZHXUZcTramBvb4d9YEVySkc", -); - /** * The reverse look up class */ @@ -47,6 +33,8 @@ export const REVERSE_LOOKUP_CLASS = new PublicKey( "33m47vH6Eav6jr5Ry86XjhRft2jRBLDnDgPSHoquXi2Z", ); +export const CENTRAL_STATE = REVERSE_LOOKUP_CLASS; + /** * The `.twitter` TLD authority */ @@ -61,15 +49,6 @@ export const TWITTER_ROOT_PARENT_REGISTRY_KEY = new PublicKey( "4YcexoW3r78zz16J2aqmukBLRwGq6rAvWzJpkYAXqebv", ); -/** - * The length of the SOL record signature - */ -export const SOL_RECORD_SIG_LEN = 96; - -export const BONFIDA_USDC_BNB = new PublicKey( - "DmSyHDSM9eSLyvoLsPvDr5fRRFZ7Bfr3h3ULvWpgQaq7", -); - export const USDC_MINT = new PublicKey( "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", ); diff --git a/js/src/custom-bg.ts b/js/src/custom-bg.ts index abb7acf..181d4ef 100644 --- a/js/src/custom-bg.ts +++ b/js/src/custom-bg.ts @@ -1,8 +1,9 @@ import { PublicKey } from "@solana/web3.js"; import { CUSTOM_BG_TLD } from "./constants"; import { CustomBg } from "./types/custom-bg"; -import { getHashedNameSync, getNameAccountKeySync } from "./utils"; -import { ErrorType, SNSError } from "./error"; +import { getHashedNameSync } from "./utils/getHashedNameSync"; +import { getNameAccountKeySync } from "./utils/getNameAccountKeySync"; +import { InvalidCustomBgError } from "./error"; const DEGEN_POET_KEY = new PublicKey( "ART5dr4bDic2sQVZoFheEmUxwQq5VGSx9he7JxHcXNQD", @@ -41,6 +42,6 @@ export const getArtistPubkey = (bg: CustomBg): PublicKey => { case CustomBg.Retardio3: return RETARDIO_KEY; default: - throw new SNSError(ErrorType.InvalidCustomBg); + throw new InvalidCustomBgError("The given background is invalid"); } }; diff --git a/js/src/deprecated/utils.ts b/js/src/deprecated/utils.ts index fb90350..255d3d6 100644 --- a/js/src/deprecated/utils.ts +++ b/js/src/deprecated/utils.ts @@ -8,7 +8,11 @@ import { import { NameRegistryState } from "../state"; import { REVERSE_LOOKUP_CLASS } from "../constants"; import { Buffer } from "buffer"; -import { SNSError, ErrorType } from "../error"; +import { + AccountDoesNotExistError, + InvalidInputError, + NoAccountDataError, +} from "../error"; /** * @deprecated Use {@link resolve} instead @@ -19,7 +23,7 @@ export async function getNameOwner( ) { const nameAccount = await connection.getAccountInfo(nameAccountKey); if (!nameAccount) { - throw new SNSError(ErrorType.AccountDoesNotExist); + throw new AccountDoesNotExistError("The name account does exist"); } return NameRegistryState.retrieve(connection, nameAccountKey); } @@ -81,7 +85,7 @@ export async function performReverseLookup( reverseLookupAccount, ); if (!registry.data) { - throw new SNSError(ErrorType.NoAccountData); + throw new NoAccountDataError("The registry data is empty"); } const nameLength = registry.data.slice(0, 4).readUInt32LE(0); return registry.data.slice(4, 4 + nameLength).toString(); @@ -162,7 +166,7 @@ export const getDomainKey = async (domain: string, record = false) => { const result = await _derive(recordPrefix.concat(splitted[0]), subKey); return { ...result, isSub: true, parent: parentKey, isSubRecord: true }; } else if (splitted.length >= 3) { - throw new SNSError(ErrorType.InvalidInput); + throw new InvalidInputError("The domain is malformed"); } const result = await _derive(domain, ROOT_DOMAIN_ACCOUNT); return { ...result, isSub: false, parent: undefined }; diff --git a/js/src/devnet.ts b/js/src/devnet.ts index aa8882b..a500e70 100644 --- a/js/src/devnet.ts +++ b/js/src/devnet.ts @@ -6,16 +6,14 @@ import { TransactionInstruction, SYSVAR_RENT_PUBKEY, } from "@solana/web3.js"; -import { - createInstruction, - deleteInstruction, - transferInstruction, - updateInstruction, - createReverseInstruction, - createInstructionV3, - burnInstruction, - createSplitV2Instruction, -} from "./instructions"; +import { createInstruction } from "./instructions/createInstruction"; +import { deleteInstruction } from "./instructions/deleteInstruction"; +import { transferInstruction } from "./instructions/transferInstruction"; +import { updateInstruction } from "./instructions/updateInstruction"; +import { createReverseInstruction } from "./instructions/createReverseInstruction"; +import { createInstructionV3 } from "./instructions/createInstructionV3"; +import { burnInstruction } from "./instructions/burnInstruction"; +import { createSplitV2Instruction } from "./instructions/createSplitV2Instruction"; import { NameRegistryState } from "./state"; import { Numberu64, Numberu32 } from "./int"; import { getHashedName, getNameOwner } from "./deprecated/utils"; @@ -24,12 +22,16 @@ import { getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction, } from "@solana/spl-token"; -import { ErrorType, SNSError } from "./error"; import { - deserializeReverse, - getHashedNameSync, - getPythFeedAccountKey, -} from "./utils"; + InvalidDomainError, + InvalidInputError, + InvalidSubdomainError, + NoAccountDataError, + PythFeedNotFoundError, +} from "./error"; +import { deserializeReverse } from "./utils/deserializeReverse"; +import { getHashedNameSync } from "./utils/getHashedNameSync"; +import { getPythFeedAccountKey } from "./utils/getPythFeedAccountKey"; import { PYTH_PULL_FEEDS } from "./constants"; const constants = { @@ -161,7 +163,7 @@ const reverseLookup = async ( reverseLookupAccount, ); if (!registry.data) { - throw new SNSError(ErrorType.NoAccountData); + throw new NoAccountDataError("The registry data is empty"); } return deserializeReverse(registry.data); }; @@ -189,7 +191,7 @@ const getDomainKeySync = (domain: string) => { const result = _deriveSync(sub, parentKey); return { ...result, isSub: true, parent: parentKey }; } else if (splitted.length >= 3) { - throw new SNSError(ErrorType.InvalidInput); + throw new InvalidInputError("The domain is malformed"); } const result = _deriveSync(domain, constants.ROOT_DOMAIN_ACCOUNT); return { ...result, isSub: false, parent: undefined }; @@ -423,7 +425,7 @@ const registerDomainName = async ( ) => { // Basic validation if (name.includes(".") || name.trim().toLowerCase() !== name) { - throw new SNSError(ErrorType.InvalidDomain); + throw new InvalidDomainError("The domain is malformed"); } const [cs] = PublicKey.findProgramAddressSync( [constants.REGISTER_PROGRAM_ID.toBuffer()], @@ -468,7 +470,9 @@ const registerDomainName = async ( const pythFeed = devnet.constants.PYTH_FEEDS.get(mint.toBase58()); if (!pythFeed) { - throw new SNSError(ErrorType.PythFeedNotFound); + throw new PythFeedNotFoundError( + "The Pyth account for the provided mint was not found", + ); } const ix = new createInstructionV3({ @@ -563,7 +567,7 @@ const createSubdomain = async ( const ixs: TransactionInstruction[] = []; const sub = subdomain.split(".")[0]; if (!sub) { - throw new SNSError(ErrorType.InvalidSubdomain); + throw new InvalidSubdomainError("The subdomain is malformed"); } const { parent, pubkey } = getDomainKeySync(subdomain); @@ -649,7 +653,7 @@ const transferSubdomain = async ( const { pubkey, isSub, parent } = getDomainKeySync(subdomain); if (!parent || !isSub) { - throw new SNSError(ErrorType.InvalidSubdomain); + throw new InvalidSubdomainError("The subdomain is not valid"); } if (!owner) { @@ -701,7 +705,7 @@ const registerDomainNameV2 = async ( ) => { // Basic validation if (name.includes(".") || name.trim().toLowerCase() !== name) { - throw new SNSError(ErrorType.InvalidDomain); + throw new InvalidDomainError("The domain name is malformed"); } const [cs] = PublicKey.findProgramAddressSync( [constants.REGISTER_PROGRAM_ID.toBuffer()], @@ -750,7 +754,9 @@ const registerDomainNameV2 = async ( const pythFeed = PYTH_PULL_FEEDS.get(mint.toBase58()); if (!pythFeed) { - throw new SNSError(ErrorType.PythFeedNotFound); + throw new PythFeedNotFoundError( + "The Pyth account for the provided mint was not found", + ); } const [pythFeedAccount] = getPythFeedAccountKey(0, pythFeed); diff --git a/js/src/error.ts b/js/src/error.ts index a7fe252..33b0c3c 100644 --- a/js/src/error.ts +++ b/js/src/error.ts @@ -29,6 +29,15 @@ export enum ErrorType { InvalidSolRecordV2 = "InvalidSolRecordV2", MissingVerifier = "MissingVerifier", PythFeedNotFound = "PythFeedNotFound", + InvalidRoA = "InvalidRoA", + InvalidPda = "InvalidPda", + InvalidParrent = "InvalidParrent", + NftRecordNotFound = "NftRecordNotFound", + PdaOwnerNotAllowed = "PdaOwnerNotAllowed", + DomainDoesNotExist = "DomainDoesNotExist", + RecordMalformed = "RecordMalformed", + CouldNotFindNftOwner = "CouldNotFindNftOwner", + WrongValidation = "WrongValidation", } export class SNSError extends Error { @@ -44,3 +53,236 @@ export class SNSError extends Error { } } } + +export class SymbolNotFoundError extends SNSError { + constructor(message?: string) { + super(ErrorType.SymbolNotFound, message); + } +} + +export class InvalidSubdomainError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidSubdomain, message); + } +} + +export class FavouriteDomainNotFoundError extends SNSError { + constructor(message?: string) { + super(ErrorType.FavouriteDomainNotFound, message); + } +} + +export class MissingParentOwnerError extends SNSError { + constructor(message?: string) { + super(ErrorType.MissingParentOwner, message); + } +} + +export class U32OverflowError extends SNSError { + constructor(message?: string) { + super(ErrorType.U32Overflow, message); + } +} + +export class InvalidBufferLengthError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidBufferLength, message); + } +} + +export class U64OverflowError extends SNSError { + constructor(message?: string) { + super(ErrorType.U64Overflow, message); + } +} + +export class NoRecordDataError extends SNSError { + constructor(message?: string) { + super(ErrorType.NoRecordData, message); + } +} + +export class InvalidRecordDataError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidRecordData, message); + } +} + +export class UnsupportedRecordError extends SNSError { + constructor(message?: string) { + super(ErrorType.UnsupportedRecord, message); + } +} + +export class InvalidEvmAddressError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidEvmAddress, message); + } +} + +export class InvalidInjectiveAddressError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidInjectiveAddress, message); + } +} + +export class InvalidARecordError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidARecord, message); + } +} + +export class InvalidAAAARecordError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidAAAARecord, message); + } +} + +export class InvalidRecordInputError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidRecordInput, message); + } +} + +export class InvalidSignatureError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidSignature, message); + } +} + +export class AccountDoesNotExistError extends SNSError { + constructor(message?: string) { + super(ErrorType.AccountDoesNotExist, message); + } +} + +export class MultipleRegistriesError extends SNSError { + constructor(message?: string) { + super(ErrorType.MultipleRegistries, message); + } +} +export class InvalidReverseTwitterError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidReverseTwitter, message); + } +} + +export class NoAccountDataError extends SNSError { + constructor(message?: string) { + super(ErrorType.NoAccountData, message); + } +} + +export class InvalidInputError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidInput, message); + } +} + +export class InvalidDomainError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidDomain, message); + } +} + +export class InvalidCustomBgError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidCustomBg, message); + } +} + +export class UnsupportedSignatureError extends SNSError { + constructor(message?: string) { + super(ErrorType.UnsupportedSignature, message); + } +} + +export class RecordDoestNotSupportGuardianSigError extends SNSError { + constructor(message?: string) { + super(ErrorType.RecordDoestNotSupportGuardianSig, message); + } +} + +export class RecordIsNotSignedError extends SNSError { + constructor(message?: string) { + super(ErrorType.RecordIsNotSigned, message); + } +} + +export class UnsupportedSignatureTypeError extends SNSError { + constructor(message?: string) { + super(ErrorType.UnsupportedSignatureType, message); + } +} + +export class InvalidSolRecordV2Error extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidSolRecordV2, message); + } +} + +export class MissingVerifierError extends SNSError { + constructor(message?: string) { + super(ErrorType.MissingVerifier, message); + } +} + +export class PythFeedNotFoundError extends SNSError { + constructor(message?: string) { + super(ErrorType.PythFeedNotFound, message); + } +} + +export class InvalidRoAError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidRoA, message); + } +} + +export class InvalidPdaError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidPda, message); + } +} + +export class InvalidParrentError extends SNSError { + constructor(message?: string) { + super(ErrorType.InvalidParrent, message); + } +} + +export class NftRecordNotFoundError extends SNSError { + constructor(message?: string) { + super(ErrorType.NftRecordNotFound, message); + } +} + +export class PdaOwnerNotAllowed extends SNSError { + constructor(message?: string) { + super(ErrorType.PdaOwnerNotAllowed, message); + } +} + +export class DomainDoesNotExist extends SNSError { + constructor(message?: string) { + super(ErrorType.DomainDoesNotExist, message); + } +} + +export class RecordMalformed extends SNSError { + constructor(message?: string) { + super(ErrorType.RecordMalformed, message); + } +} + +export class CouldNotFindNftOwner extends SNSError { + constructor(message?: string) { + super(ErrorType.CouldNotFindNftOwner, message); + } +} + +export class WrongValidation extends SNSError { + constructor(message?: string) { + super(ErrorType.WrongValidation, message); + } +} diff --git a/js/src/favorite-domain.ts b/js/src/favorite-domain.ts index eea2ca1..0be66d0 100644 --- a/js/src/favorite-domain.ts +++ b/js/src/favorite-domain.ts @@ -1,19 +1,17 @@ import { Buffer } from "buffer"; -import { deserialize, Schema } from "borsh"; -import { - deserializeReverse, - getReverseKeyFromDomainKey, - reverseLookup, -} from "./utils"; +import { deserialize } from "borsh"; 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"; +import { deserializeReverse } from "./utils/deserializeReverse"; +import { getReverseKeyFromDomainKey } from "./utils/getReverseKeyFromDomainKey"; +import { reverseLookup } from "./utils/reverseLookup"; +import { FavouriteDomainNotFoundError } from "./error"; +import { getDomainMint } from "./nft/getDomainMint"; import { NameRegistryState } from "./state"; +import { NAME_PROGRAM_ID, ROOT_DOMAIN_ACCOUNT } from "./constants"; export const NAME_OFFERS_ID = new PublicKey( "85iDfUvr3HJyLM2zcq5BXSiDvUWfw6cSE1FfNBo8Ap29", @@ -52,7 +50,9 @@ export class FavouriteDomain { static async retrieve(connection: Connection, key: PublicKey) { const accountInfo = await connection.getAccountInfo(key); if (!accountInfo || !accountInfo.data) { - throw new SNSError(ErrorType.FavouriteDomainNotFound); + throw new FavouriteDomainNotFoundError( + "The favourite account does not exist", + ); } return this.deserialize(accountInfo.data); } @@ -84,6 +84,8 @@ export class FavouriteDomain { } } +export { FavouriteDomain as PrimaryDomain }; + /** * This function can be used to retrieve the favorite domain of a user * @param connection The Solana RPC connection object @@ -98,16 +100,26 @@ export const getFavoriteDomain = async ( NAME_OFFERS_ID, new PublicKey(owner), ); - const favorite = await FavouriteDomain.retrieve(connection, favKey); - - const reverse = await reverseLookup(connection, favorite.nameAccount); const { registry, nftOwner } = await NameRegistryState.retrieve( connection, favorite.nameAccount, ); const domainOwner = nftOwner || registry.owner; + let reverse = await reverseLookup( + connection, + favorite.nameAccount, + registry.parentName.equals(ROOT_DOMAIN_ACCOUNT) + ? undefined + : registry.parentName, + ); + + if (!registry.parentName.equals(ROOT_DOMAIN_ACCOUNT)) { + const parentReverse = await reverseLookup(connection, registry.parentName); + reverse += `.${parentReverse}`; + } + return { domain: favorite.nameAccount, reverse, @@ -115,6 +127,8 @@ export const getFavoriteDomain = async ( }; }; +export { getFavoriteDomain as getPrimaryDomain }; + /** * 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. @@ -140,22 +154,38 @@ export const getMultipleFavoriteDomains = async ( return PublicKey.default; }, ); - const revKeys = favDomains.map((e) => getReverseKeyFromDomainKey(e)); + + const domainInfos = await connection.getMultipleAccountsInfo(favDomains); + const parentRevKeys: PublicKey[] = []; + const revKeys = domainInfos.map((e, idx) => { + const parent = new PublicKey(e?.data.slice(0, 32) ?? Buffer.alloc(32)); + const isSub = + e?.owner.equals(NAME_PROGRAM_ID) && !parent.equals(ROOT_DOMAIN_ACCOUNT); + parentRevKeys.push( + isSub ? getReverseKeyFromDomainKey(parent) : PublicKey.default, + ); + return getReverseKeyFromDomainKey( + favDomains[idx], + isSub ? parent : undefined, + ); + }); 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), + const [revs, tokenAccs, parentRevs] = await Promise.all([ connection.getMultipleAccountsInfo(revKeys), connection.getMultipleAccountsInfo(atas), + connection.getMultipleAccountsInfo(parentRevKeys), ]); for (let i = 0; i < wallets.length; i++) { + let parentRev = ""; const domainInfo = domainInfos[i]; const rev = revs[i]; + const parentRevAccount = parentRevs[i]; const tokenAcc = tokenAccs[i]; if (!domainInfo || !rev) { @@ -163,10 +193,15 @@ export const getMultipleFavoriteDomains = async ( continue; } + if (parentRevAccount && parentRevAccount.owner.equals(NAME_PROGRAM_ID)) { + const des = deserializeReverse(parentRevAccount.data.slice(96)); + parentRev += `.${des}`; + } + const nativeOwner = new PublicKey(domainInfo?.data.slice(32, 64)); if (nativeOwner.equals(wallets[i])) { - result.push(deserializeReverse(rev?.data.slice(96))); + result.push(deserializeReverse(rev?.data.slice(96)) + parentRev); continue; } // Either tokenized or stale @@ -178,7 +213,7 @@ export const getMultipleFavoriteDomains = async ( const decoded = AccountLayout.decode(tokenAcc.data); // Tokenized if (Number(decoded.amount) === 1) { - result.push(deserializeReverse(rev?.data.slice(96))); + result.push(deserializeReverse(rev?.data.slice(96)) + parentRev); continue; } @@ -188,3 +223,5 @@ export const getMultipleFavoriteDomains = async ( return result; }; + +export { getMultipleFavoriteDomains as getMultiplePrimaryDomains }; diff --git a/js/src/index.ts b/js/src/index.ts index b6bf712..168cfef 100644 --- a/js/src/index.ts +++ b/js/src/index.ts @@ -1,20 +1,132 @@ -export * from "./bindings"; +export * from "./bindings/burnDomain"; +export * from "./bindings/createNameRegistry"; +export * from "./bindings/createRecordInstruction"; +export * from "./bindings/createRecordV2Instruction"; +export * from "./bindings/createReverseName"; +export * from "./bindings/createSolRecordInstruction"; +export * from "./bindings/createSubdomain"; +export * from "./bindings/deleteNameRegistry"; +export * from "./bindings/deleteRecordV2"; +export * from "./bindings/ethValidateRecordV2Content"; +export * from "./bindings/registerDomainName"; +export * from "./bindings/registerDomainNameV2"; +export * from "./bindings/registerFavorite"; +export * from "./bindings/registerWithNft"; +export * from "./bindings/transferNameOwnership"; +export * from "./bindings/transferSubdomain"; +export * from "./bindings/updateNameRegistryData"; +export * from "./bindings/updateRecordInstruction"; +export * from "./bindings/updateRecordV2Instruction"; +export * from "./bindings/updateSolRecordInstruction"; +export * from "./bindings/validateRecordV2Content"; +export * from "./bindings/writRoaRecordV2"; + export * from "./state"; -export * from "./twitter_bindings"; -export * from "./utils"; -export * from "./instructions"; -export * from "./nft"; -export { getDomainMint } from "./nft/name-tokenizer"; + +export * from "./twitter/ReverseTwitterRegistryState"; +export * from "./twitter/changeTwitterRegistryData"; +export * from "./twitter/changeVerifiedPubkey"; +export * from "./twitter/createReverseTwitterRegistry"; +export * from "./twitter/createVerifiedTwitterRegistry"; +export * from "./twitter/deleteTwitterRegistry"; +export * from "./twitter/getHandleAndRegistryKey"; +export * from "./twitter/getTwitterHandleandRegistryKeyViaFilters"; +export * from "./twitter/getTwitterRegistry"; +export * from "./twitter/getTwitterRegistryData"; +export * from "./twitter/getTwitterRegistryKey"; + +export * from "./utils/check"; +export * from "./utils/deserializeReverse"; +export * from "./utils/findSubdomains"; +export * from "./utils/getAllDomains"; +export * from "./utils/getAllRegisteredDomains"; +export * from "./utils/getDomainKeySync"; +export * from "./utils/getDomainKeysWithReverses"; +export * from "./utils/getDomainPriceFromName"; +export * from "./utils/getHashedNameSync"; +export * from "./utils/getNameAccountKeySync"; +export * from "./utils/getPythFeedAccountKey"; +export * from "./utils/getReverseKeyFromDomainKey"; +export * from "./utils/getReverseKeySync"; +export * from "./utils/getTokenizedDomains"; +export * from "./utils/reverseLookup"; +export * from "./utils/reverseLookupBatch"; + +export * from "./instructions/burnInstruction"; +export * from "./instructions/createInstruction"; +export * from "./instructions/createInstructionV3"; +export * from "./instructions/createReverseInstruction"; +export * from "./instructions/createSplitV2Instruction"; +export * from "./instructions/createV2Instruction"; +export * from "./instructions/createWithNftInstruction"; +export * from "./instructions/deleteInstruction"; +export * from "./instructions/reallocInstruction"; +export * from "./instructions/registerFavoriteInstruction"; +export * from "./instructions/transferInstruction"; +export * from "./instructions/updateInstruction"; +export * from "./instructions/types"; + +export * from "./nft/getDomainMint"; +export * from "./nft/retrieveNftOwnerV2"; +export * from "./nft/retrieveNftOwner"; +export * from "./nft/retrieveNfts"; +export * from "./nft/getRecordFromMint"; +export * from "./nft/retrieveRecords"; +export * from "./nft/const"; +export * from "./nft/state"; + export * from "./favorite-domain"; export * from "./constants"; export * from "./int"; -export * from "./record"; + +export * from "./record/checkSolRecord"; +export * from "./record/deserializeRecord"; +export * from "./record/getRecord"; +export * from "./record/getRecordKeySync"; +export * from "./record/getRecords"; +export * from "./record/serializeRecord"; +export * from "./record/serializeSolRecord"; + +export * from "./record/helpers/getArweaveRecord"; +export * from "./record/helpers/getBackgroundRecord"; +export * from "./record/helpers/getBackpackRecord"; +export * from "./record/helpers/getBtcRecord"; +export * from "./record/helpers/getBscRecord"; +export * from "./record/helpers/getDiscordRecord"; +export * from "./record/helpers/getDogeRecord"; +export * from "./record/helpers/getEmailRecord"; +export * from "./record/helpers/getEthRecord"; +export * from "./record/helpers/getGithubRecord"; +export * from "./record/helpers/getInjectiveRecord"; +export * from "./record/helpers/getIpfsRecord"; +export * from "./record/helpers/getLtcRecord"; +export * from "./record/helpers/getPicRecord"; +export * from "./record/helpers/getPointRecord"; +export * from "./record/helpers/getRedditRecord"; +export * from "./record/helpers/getShdwRecord"; +export * from "./record/helpers/getSolRecord"; +export * from "./record/helpers/getTelegramRecord"; +export * from "./record/helpers/getTwitterRecord"; +export * from "./record/helpers/getUrlRecord"; + export * from "./types/record"; export * from "./types/custom-bg"; -export * from "./resolve"; + +export * from "./resolve/resolve"; +export * from "./resolve/resolveSolRecordV1"; +export * from "./resolve/resolveSolRecordV2"; + export * from "./deprecated/utils"; export * from "./error"; export * from "./custom-bg"; -export * from "./record_v2"; + +export * from "./record_v2/const"; +export * from "./record_v2/deserializeRecordV2Content"; +export * from "./record_v2/serializeRecordV2Content"; +export * from "./record_v2/getRecordV2"; +export * from "./record_v2/getRecordV2Key"; +export * from "./record_v2/getMultipleRecordsV2"; +export * from "./record_v2/verifyRightOfAssociation"; export * from "./record_v2/utils"; + export * from "./devnet"; diff --git a/js/src/instructions.ts b/js/src/instructions.ts deleted file mode 100644 index f8c4878..0000000 --- a/js/src/instructions.ts +++ /dev/null @@ -1,998 +0,0 @@ -import { Buffer } from "buffer"; -import { - PublicKey, - TransactionInstruction, - SystemProgram, -} from "@solana/web3.js"; -import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; -import { Numberu32, Numberu64 } from "./int"; -import { serialize } from "borsh"; - -export interface AccountKey { - pubkey: PublicKey; - isSigner: boolean; - isWritable: boolean; -} - -export function createInstruction( - nameProgramId: PublicKey, - systemProgramId: PublicKey, - nameKey: PublicKey, - nameOwnerKey: PublicKey, - payerKey: PublicKey, - hashed_name: Buffer, - lamports: Numberu64, - space: Numberu32, - nameClassKey?: PublicKey, - nameParent?: PublicKey, - nameParentOwner?: PublicKey, -): TransactionInstruction { - const buffers = [ - Buffer.from(Int8Array.from([0])), - new Numberu32(hashed_name.length).toBuffer(), - hashed_name, - lamports.toBuffer(), - space.toBuffer(), - ]; - - const data = Buffer.concat(buffers); - - const keys = [ - { - pubkey: systemProgramId, - isSigner: false, - isWritable: false, - }, - { - pubkey: payerKey, - isSigner: true, - isWritable: true, - }, - { - pubkey: nameKey, - isSigner: false, - isWritable: true, - }, - { - pubkey: nameOwnerKey, - isSigner: false, - isWritable: false, - }, - ]; - - if (nameClassKey) { - keys.push({ - pubkey: nameClassKey, - isSigner: true, - isWritable: false, - }); - } else { - keys.push({ - pubkey: new PublicKey(Buffer.alloc(32)), - isSigner: false, - isWritable: false, - }); - } - if (nameParent) { - keys.push({ - pubkey: nameParent, - isSigner: false, - isWritable: false, - }); - } else { - keys.push({ - pubkey: new PublicKey(Buffer.alloc(32)), - isSigner: false, - isWritable: false, - }); - } - if (nameParentOwner) { - keys.push({ - pubkey: nameParentOwner, - isSigner: true, - isWritable: false, - }); - } - - return new TransactionInstruction({ - keys, - programId: nameProgramId, - data, - }); -} - -export function updateInstruction( - nameProgramId: PublicKey, - nameAccountKey: PublicKey, - offset: Numberu32, - input_data: Buffer, - nameUpdateSigner: PublicKey, -): TransactionInstruction { - const buffers = [ - Buffer.from(Int8Array.from([1])), - offset.toBuffer(), - new Numberu32(input_data.length).toBuffer(), - input_data, - ]; - - const data = Buffer.concat(buffers); - const keys = [ - { - pubkey: nameAccountKey, - isSigner: false, - isWritable: true, - }, - { - pubkey: nameUpdateSigner, - isSigner: true, - isWritable: false, - }, - ]; - - return new TransactionInstruction({ - keys, - programId: nameProgramId, - data, - }); -} - -export function transferInstruction( - nameProgramId: PublicKey, - nameAccountKey: PublicKey, - newOwnerKey: PublicKey, - currentNameOwnerKey: PublicKey, - nameClassKey?: PublicKey, - nameParent?: PublicKey, - parentOwner?: PublicKey, -): TransactionInstruction { - const buffers = [Buffer.from(Int8Array.from([2])), newOwnerKey.toBuffer()]; - - const data = Buffer.concat(buffers); - - const keys = [ - { - pubkey: nameAccountKey, - isSigner: false, - isWritable: true, - }, - { - pubkey: parentOwner ? parentOwner : currentNameOwnerKey, - isSigner: true, - isWritable: false, - }, - ]; - - if (nameClassKey) { - keys.push({ - pubkey: nameClassKey, - isSigner: true, - isWritable: false, - }); - } - - if (parentOwner && nameParent) { - if (!nameClassKey) { - keys.push({ - pubkey: PublicKey.default, - isSigner: false, - isWritable: false, - }); - } - keys.push({ - pubkey: nameParent, - isSigner: false, - isWritable: false, - }); - } - - return new TransactionInstruction({ - keys, - programId: nameProgramId, - data, - }); -} - -export function deleteInstruction( - nameProgramId: PublicKey, - nameAccountKey: PublicKey, - refundTargetKey: PublicKey, - nameOwnerKey: PublicKey, -): TransactionInstruction { - const buffers = [Buffer.from(Int8Array.from([3]))]; - - const data = Buffer.concat(buffers); - const keys = [ - { - pubkey: nameAccountKey, - isSigner: false, - isWritable: true, - }, - { - pubkey: nameOwnerKey, - isSigner: true, - isWritable: false, - }, - { - pubkey: refundTargetKey, - isSigner: false, - isWritable: true, - }, - ]; - - return new TransactionInstruction({ - keys, - programId: nameProgramId, - data, - }); -} - -export class createV2Instruction { - tag: number; - name: string; - space: number; - - static schema = { - struct: { - tag: "u8", - name: "string", - space: "u32", - }, - }; - - constructor(obj: { name: string; space: number }) { - this.tag = 9; - this.name = obj.name; - this.space = obj.space; - } - - serialize(): Uint8Array { - return serialize(createV2Instruction.schema, this); - } - - getInstruction( - programId: PublicKey, - rentSysvarAccount: PublicKey, - nameProgramId: PublicKey, - rootDomain: PublicKey, - nameAccount: PublicKey, - reverseLookupAccount: PublicKey, - centralState: PublicKey, - buyer: PublicKey, - buyerTokenAccount: PublicKey, - usdcVault: PublicKey, - state: PublicKey, - ): TransactionInstruction { - const data = Buffer.from(this.serialize()); - const keys = [ - { - pubkey: rentSysvarAccount, - isSigner: false, - isWritable: false, - }, - { - pubkey: nameProgramId, - isSigner: false, - isWritable: false, - }, - { - pubkey: rootDomain, - isSigner: false, - isWritable: false, - }, - { - pubkey: nameAccount, - isSigner: false, - isWritable: true, - }, - { - pubkey: reverseLookupAccount, - isSigner: false, - isWritable: true, - }, - { - pubkey: SystemProgram.programId, - isSigner: false, - isWritable: false, - }, - { - pubkey: centralState, - isSigner: false, - isWritable: false, - }, - { - pubkey: buyer, - isSigner: true, - isWritable: true, - }, - { - pubkey: buyerTokenAccount, - isSigner: false, - isWritable: true, - }, - { - pubkey: usdcVault, - isSigner: false, - isWritable: true, - }, - { - pubkey: TOKEN_PROGRAM_ID, - isSigner: false, - isWritable: false, - }, - { - pubkey: state, - isSigner: false, - isWritable: false, - }, - ]; - - return new TransactionInstruction({ - keys, - programId, - data, - }); - } -} - -export class createReverseInstruction { - tag: number; - name: string; - static schema = { - struct: { - tag: "u8", - name: "string", - }, - }; - - constructor(obj: { name: string }) { - this.tag = 12; - this.name = obj.name; - } - serialize(): Uint8Array { - return serialize(createReverseInstruction.schema, this); - } - getInstruction( - programId: PublicKey, - namingServiceProgram: PublicKey, - rootDomain: PublicKey, - reverseLookup: PublicKey, - systemProgram: PublicKey, - centralState: PublicKey, - feePayer: PublicKey, - rentSysvar: PublicKey, - parentName?: PublicKey, - parentNameOwner?: 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: reverseLookup, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: systemProgram, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: centralState, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: feePayer, - isSigner: true, - isWritable: true, - }); - keys.push({ - pubkey: rentSysvar, - isSigner: false, - isWritable: false, - }); - if (!!parentName) { - keys.push({ - pubkey: parentName, - isSigner: false, - isWritable: true, - }); - } - if (!!parentNameOwner) { - keys.push({ - pubkey: parentNameOwner, - isSigner: true, - isWritable: true, - }); - } - return new TransactionInstruction({ - keys, - programId, - data, - }); - } -} -export class createInstructionV3 { - 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 = 13; - this.name = obj.name; - this.space = obj.space; - this.referrerIdxOpt = obj.referrerIdxOpt; - } - serialize(): Uint8Array { - return serialize(createInstructionV3.schema, this); - } - getInstruction( - programId: PublicKey, - namingServiceProgram: PublicKey, - rootDomain: PublicKey, - name: PublicKey, - reverseLookup: PublicKey, - systemProgram: PublicKey, - centralState: PublicKey, - buyer: PublicKey, - buyerTokenSource: PublicKey, - pythMappingAcc: PublicKey, - pythProductAcc: PublicKey, - pythPriceAcc: 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: buyerTokenSource, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: pythMappingAcc, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: pythProductAcc, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: pythPriceAcc, - 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, - }); - } -} - -export class createWithNftInstruction { - tag: number; - name: string; - space: number; - static schema = { - struct: { - tag: "u8", - name: "string", - space: "u32", - }, - }; - - constructor(obj: { name: string; space: number }) { - this.tag = 17; - this.name = obj.name; - this.space = obj.space; - } - serialize(): Uint8Array { - return serialize(createWithNftInstruction.schema, this); - } - getInstruction( - programId: PublicKey, - namingServiceProgram: PublicKey, - rootDomain: PublicKey, - name: PublicKey, - reverseLookup: PublicKey, - systemProgram: PublicKey, - centralState: PublicKey, - buyer: PublicKey, - nftSource: PublicKey, - nftMetadata: PublicKey, - nftMint: PublicKey, - masterEdition: PublicKey, - collection: PublicKey, - splTokenProgram: PublicKey, - rentSysvar: PublicKey, - state: PublicKey, - mplTokenMetadata: 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: nftSource, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: nftMetadata, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: nftMint, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: masterEdition, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: collection, - 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, - }); - keys.push({ - pubkey: mplTokenMetadata, - isSigner: false, - isWritable: false, - }); - return new TransactionInstruction({ - keys, - programId, - data, - }); - } -} - -export class burnInstruction { - tag: number; - static schema = { - struct: { - tag: "u8", - }, - }; - - constructor() { - this.tag = 16; - } - serialize(): Uint8Array { - return serialize(burnInstruction.schema, this); - } - getInstruction( - programId: PublicKey, - nameServiceId: PublicKey, - systemProgram: PublicKey, - domain: PublicKey, - reverse: PublicKey, - resellingState: PublicKey, - state: PublicKey, - centralState: PublicKey, - owner: PublicKey, - target: PublicKey, - ): TransactionInstruction { - const data = Buffer.from(this.serialize()); - let keys: AccountKey[] = []; - keys.push({ - pubkey: nameServiceId, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: systemProgram, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: domain, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: reverse, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: resellingState, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: state, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: centralState, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: owner, - isSigner: true, - isWritable: false, - }); - keys.push({ - pubkey: target, - isSigner: false, - isWritable: true, - }); - return new TransactionInstruction({ - keys, - programId, - data, - }); - } -} - -export function reallocInstruction( - nameProgramId: PublicKey, - systemProgramId: PublicKey, - payerKey: PublicKey, - nameAccountKey: PublicKey, - nameOwnerKey: PublicKey, - space: Numberu32, -): TransactionInstruction { - const buffers = [Buffer.from(Int8Array.from([4])), space.toBuffer()]; - - const data = Buffer.concat(buffers); - const keys = [ - { - pubkey: systemProgramId, - isSigner: false, - isWritable: false, - }, - { - pubkey: payerKey, - isSigner: true, - isWritable: true, - }, - { - pubkey: nameAccountKey, - isSigner: false, - isWritable: true, - }, - { - pubkey: nameOwnerKey, - isSigner: true, - isWritable: false, - }, - ]; - - return new TransactionInstruction({ - keys, - programId: nameProgramId, - data, - }); -} - -export class registerFavoriteInstruction { - tag: number; - static schema = { - struct: { - tag: "u8", - }, - }; - constructor() { - this.tag = 6; - } - serialize(): Uint8Array { - return serialize(registerFavoriteInstruction.schema, this); - } - getInstruction( - programId: PublicKey, - nameAccount: PublicKey, - favouriteAccount: PublicKey, - owner: PublicKey, - systemProgram: PublicKey, - ): TransactionInstruction { - const data = Buffer.from(this.serialize()); - let keys: AccountKey[] = []; - keys.push({ - pubkey: nameAccount, - isSigner: false, - isWritable: false, - }); - keys.push({ - pubkey: favouriteAccount, - isSigner: false, - isWritable: true, - }); - keys.push({ - pubkey: owner, - isSigner: true, - isWritable: true, - }); - keys.push({ - pubkey: systemProgram, - isSigner: false, - isWritable: false, - }); - return new TransactionInstruction({ - keys, - programId, - data, - }); - } -} - -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/instructions/burnInstruction.ts b/js/src/instructions/burnInstruction.ts new file mode 100644 index 0000000..3c22e0a --- /dev/null +++ b/js/src/instructions/burnInstruction.ts @@ -0,0 +1,85 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +export class burnInstruction { + tag: number; + static schema = { + struct: { + tag: "u8", + }, + }; + + constructor() { + this.tag = 16; + } + serialize(): Uint8Array { + return serialize(burnInstruction.schema, this); + } + getInstruction( + programId: PublicKey, + nameServiceId: PublicKey, + systemProgram: PublicKey, + domain: PublicKey, + reverse: PublicKey, + resellingState: PublicKey, + state: PublicKey, + centralState: PublicKey, + owner: PublicKey, + target: PublicKey, + ): TransactionInstruction { + const data = Buffer.from(this.serialize()); + let keys: AccountKey[] = []; + keys.push({ + pubkey: nameServiceId, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: systemProgram, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: domain, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: reverse, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: resellingState, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: state, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: centralState, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: owner, + isSigner: true, + isWritable: false, + }); + keys.push({ + pubkey: target, + isSigner: false, + isWritable: true, + }); + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/instructions/createInstruction.ts b/js/src/instructions/createInstruction.ts new file mode 100644 index 0000000..173a44e --- /dev/null +++ b/js/src/instructions/createInstruction.ts @@ -0,0 +1,90 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { Numberu32, Numberu64 } from "../int"; + +export function createInstruction( + nameProgramId: PublicKey, + systemProgramId: PublicKey, + nameKey: PublicKey, + nameOwnerKey: PublicKey, + payerKey: PublicKey, + hashed_name: Buffer, + lamports: Numberu64, + space: Numberu32, + nameClassKey?: PublicKey, + nameParent?: PublicKey, + nameParentOwner?: PublicKey, +): TransactionInstruction { + const buffers = [ + Buffer.from(Int8Array.from([0])), + new Numberu32(hashed_name.length).toBuffer(), + hashed_name, + lamports.toBuffer(), + space.toBuffer(), + ]; + + const data = Buffer.concat(buffers); + + const keys = [ + { + pubkey: systemProgramId, + isSigner: false, + isWritable: false, + }, + { + pubkey: payerKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: nameKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: nameOwnerKey, + isSigner: false, + isWritable: false, + }, + ]; + + if (nameClassKey) { + keys.push({ + pubkey: nameClassKey, + isSigner: true, + isWritable: false, + }); + } else { + keys.push({ + pubkey: new PublicKey(Buffer.alloc(32)), + isSigner: false, + isWritable: false, + }); + } + if (nameParent) { + keys.push({ + pubkey: nameParent, + isSigner: false, + isWritable: false, + }); + } else { + keys.push({ + pubkey: new PublicKey(Buffer.alloc(32)), + isSigner: false, + isWritable: false, + }); + } + if (nameParentOwner) { + keys.push({ + pubkey: nameParentOwner, + isSigner: true, + isWritable: false, + }); + } + + return new TransactionInstruction({ + keys, + programId: nameProgramId, + data, + }); +} diff --git a/js/src/instructions/createInstructionV3.ts b/js/src/instructions/createInstructionV3.ts new file mode 100644 index 0000000..02132e6 --- /dev/null +++ b/js/src/instructions/createInstructionV3.ts @@ -0,0 +1,142 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +export class createInstructionV3 { + 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 = 13; + this.name = obj.name; + this.space = obj.space; + this.referrerIdxOpt = obj.referrerIdxOpt; + } + serialize(): Uint8Array { + return serialize(createInstructionV3.schema, this); + } + getInstruction( + programId: PublicKey, + namingServiceProgram: PublicKey, + rootDomain: PublicKey, + name: PublicKey, + reverseLookup: PublicKey, + systemProgram: PublicKey, + centralState: PublicKey, + buyer: PublicKey, + buyerTokenSource: PublicKey, + pythMappingAcc: PublicKey, + pythProductAcc: PublicKey, + pythPriceAcc: 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: buyerTokenSource, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: pythMappingAcc, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: pythProductAcc, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: pythPriceAcc, + 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/instructions/createReverseInstruction.ts b/js/src/instructions/createReverseInstruction.ts new file mode 100644 index 0000000..79ddd72 --- /dev/null +++ b/js/src/instructions/createReverseInstruction.ts @@ -0,0 +1,92 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +export class createReverseInstruction { + tag: number; + name: string; + static schema = { + struct: { + tag: "u8", + name: "string", + }, + }; + + constructor(obj: { name: string }) { + this.tag = 12; + this.name = obj.name; + } + serialize(): Uint8Array { + return serialize(createReverseInstruction.schema, this); + } + getInstruction( + programId: PublicKey, + namingServiceProgram: PublicKey, + rootDomain: PublicKey, + reverseLookup: PublicKey, + systemProgram: PublicKey, + centralState: PublicKey, + feePayer: PublicKey, + rentSysvar: PublicKey, + parentName?: PublicKey, + parentNameOwner?: 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: reverseLookup, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: systemProgram, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: centralState, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: feePayer, + isSigner: true, + isWritable: true, + }); + keys.push({ + pubkey: rentSysvar, + isSigner: false, + isWritable: false, + }); + if (!!parentName) { + keys.push({ + pubkey: parentName, + isSigner: false, + isWritable: true, + }); + } + if (!!parentNameOwner) { + keys.push({ + pubkey: parentNameOwner, + isSigner: true, + isWritable: true, + }); + } + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/instructions/createSplitV2Instruction.ts b/js/src/instructions/createSplitV2Instruction.ts new file mode 100644 index 0000000..89c780b --- /dev/null +++ b/js/src/instructions/createSplitV2Instruction.ts @@ -0,0 +1,141 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +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/instructions/createV2Instruction.ts b/js/src/instructions/createV2Instruction.ts new file mode 100644 index 0000000..dfc7adc --- /dev/null +++ b/js/src/instructions/createV2Instruction.ts @@ -0,0 +1,116 @@ +import { Buffer } from "buffer"; +import { + PublicKey, + TransactionInstruction, + SystemProgram, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { serialize } from "borsh"; + +export class createV2Instruction { + tag: number; + name: string; + space: number; + + static schema = { + struct: { + tag: "u8", + name: "string", + space: "u32", + }, + }; + + constructor(obj: { name: string; space: number }) { + this.tag = 9; + this.name = obj.name; + this.space = obj.space; + } + + serialize(): Uint8Array { + return serialize(createV2Instruction.schema, this); + } + + getInstruction( + programId: PublicKey, + rentSysvarAccount: PublicKey, + nameProgramId: PublicKey, + rootDomain: PublicKey, + nameAccount: PublicKey, + reverseLookupAccount: PublicKey, + centralState: PublicKey, + buyer: PublicKey, + buyerTokenAccount: PublicKey, + usdcVault: PublicKey, + state: PublicKey, + ): TransactionInstruction { + const data = Buffer.from(this.serialize()); + const keys = [ + { + pubkey: rentSysvarAccount, + isSigner: false, + isWritable: false, + }, + { + pubkey: nameProgramId, + isSigner: false, + isWritable: false, + }, + { + pubkey: rootDomain, + isSigner: false, + isWritable: false, + }, + { + pubkey: nameAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: reverseLookupAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: centralState, + isSigner: false, + isWritable: false, + }, + { + pubkey: buyer, + isSigner: true, + isWritable: true, + }, + { + pubkey: buyerTokenAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: usdcVault, + isSigner: false, + isWritable: true, + }, + { + pubkey: TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: state, + isSigner: false, + isWritable: false, + }, + ]; + + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/instructions/createWithNftInstruction.ts b/js/src/instructions/createWithNftInstruction.ts new file mode 100644 index 0000000..6fd4661 --- /dev/null +++ b/js/src/instructions/createWithNftInstruction.ts @@ -0,0 +1,133 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +export class createWithNftInstruction { + tag: number; + name: string; + space: number; + static schema = { + struct: { + tag: "u8", + name: "string", + space: "u32", + }, + }; + + constructor(obj: { name: string; space: number }) { + this.tag = 17; + this.name = obj.name; + this.space = obj.space; + } + serialize(): Uint8Array { + return serialize(createWithNftInstruction.schema, this); + } + getInstruction( + programId: PublicKey, + namingServiceProgram: PublicKey, + rootDomain: PublicKey, + name: PublicKey, + reverseLookup: PublicKey, + systemProgram: PublicKey, + centralState: PublicKey, + buyer: PublicKey, + nftSource: PublicKey, + nftMetadata: PublicKey, + nftMint: PublicKey, + masterEdition: PublicKey, + collection: PublicKey, + splTokenProgram: PublicKey, + rentSysvar: PublicKey, + state: PublicKey, + mplTokenMetadata: 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: nftSource, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: nftMetadata, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: nftMint, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: masterEdition, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: collection, + 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, + }); + keys.push({ + pubkey: mplTokenMetadata, + isSigner: false, + isWritable: false, + }); + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/instructions/deleteInstruction.ts b/js/src/instructions/deleteInstruction.ts new file mode 100644 index 0000000..35b8337 --- /dev/null +++ b/js/src/instructions/deleteInstruction.ts @@ -0,0 +1,36 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; + +export function deleteInstruction( + nameProgramId: PublicKey, + nameAccountKey: PublicKey, + refundTargetKey: PublicKey, + nameOwnerKey: PublicKey, +): TransactionInstruction { + const buffers = [Buffer.from(Int8Array.from([3]))]; + + const data = Buffer.concat(buffers); + const keys = [ + { + pubkey: nameAccountKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: nameOwnerKey, + isSigner: true, + isWritable: false, + }, + { + pubkey: refundTargetKey, + isSigner: false, + isWritable: true, + }, + ]; + + return new TransactionInstruction({ + keys, + programId: nameProgramId, + data, + }); +} diff --git a/js/src/instructions/reallocInstruction.ts b/js/src/instructions/reallocInstruction.ts new file mode 100644 index 0000000..08894d9 --- /dev/null +++ b/js/src/instructions/reallocInstruction.ts @@ -0,0 +1,44 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { Numberu32 } from "../int"; + +export function reallocInstruction( + nameProgramId: PublicKey, + systemProgramId: PublicKey, + payerKey: PublicKey, + nameAccountKey: PublicKey, + nameOwnerKey: PublicKey, + space: Numberu32, +): TransactionInstruction { + const buffers = [Buffer.from(Int8Array.from([4])), space.toBuffer()]; + + const data = Buffer.concat(buffers); + const keys = [ + { + pubkey: systemProgramId, + isSigner: false, + isWritable: false, + }, + { + pubkey: payerKey, + isSigner: true, + isWritable: true, + }, + { + pubkey: nameAccountKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: nameOwnerKey, + isSigner: true, + isWritable: false, + }, + ]; + + return new TransactionInstruction({ + keys, + programId: nameProgramId, + data, + }); +} diff --git a/js/src/instructions/registerFavoriteInstruction.ts b/js/src/instructions/registerFavoriteInstruction.ts new file mode 100644 index 0000000..8cdcd41 --- /dev/null +++ b/js/src/instructions/registerFavoriteInstruction.ts @@ -0,0 +1,62 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { serialize } from "borsh"; +import type { AccountKey } from "./types"; + +export class registerFavoriteInstruction { + tag: number; + static schema = { + struct: { + tag: "u8", + }, + }; + constructor() { + this.tag = 6; + } + serialize(): Uint8Array { + return serialize(registerFavoriteInstruction.schema, this); + } + getInstruction( + programId: PublicKey, + nameAccount: PublicKey, + favouriteAccount: PublicKey, + owner: PublicKey, + systemProgram: PublicKey, + optParent?: PublicKey, + ): TransactionInstruction { + const data = Buffer.from(this.serialize()); + let keys: AccountKey[] = []; + keys.push({ + pubkey: nameAccount, + isSigner: false, + isWritable: false, + }); + keys.push({ + pubkey: favouriteAccount, + isSigner: false, + isWritable: true, + }); + keys.push({ + pubkey: owner, + isSigner: true, + isWritable: true, + }); + keys.push({ + pubkey: systemProgram, + isSigner: false, + isWritable: false, + }); + if (!!optParent) { + keys.push({ + pubkey: optParent, + isSigner: false, + isWritable: false, + }); + } + return new TransactionInstruction({ + keys, + programId, + data, + }); + } +} diff --git a/js/src/instructions/transferInstruction.ts b/js/src/instructions/transferInstruction.ts new file mode 100644 index 0000000..8eecb71 --- /dev/null +++ b/js/src/instructions/transferInstruction.ts @@ -0,0 +1,58 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; + +export function transferInstruction( + nameProgramId: PublicKey, + nameAccountKey: PublicKey, + newOwnerKey: PublicKey, + currentNameOwnerKey: PublicKey, + nameClassKey?: PublicKey, + nameParent?: PublicKey, + parentOwner?: PublicKey, +): TransactionInstruction { + const buffers = [Buffer.from(Int8Array.from([2])), newOwnerKey.toBuffer()]; + + const data = Buffer.concat(buffers); + + const keys = [ + { + pubkey: nameAccountKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: parentOwner ? parentOwner : currentNameOwnerKey, + isSigner: true, + isWritable: false, + }, + ]; + + if (nameClassKey) { + keys.push({ + pubkey: nameClassKey, + isSigner: true, + isWritable: false, + }); + } + + if (parentOwner && nameParent) { + if (!nameClassKey) { + keys.push({ + pubkey: PublicKey.default, + isSigner: false, + isWritable: false, + }); + } + keys.push({ + pubkey: nameParent, + isSigner: false, + isWritable: false, + }); + } + + return new TransactionInstruction({ + keys, + programId: nameProgramId, + data, + }); +} diff --git a/js/src/instructions/types.ts b/js/src/instructions/types.ts new file mode 100644 index 0000000..a8849f3 --- /dev/null +++ b/js/src/instructions/types.ts @@ -0,0 +1,7 @@ +import { PublicKey } from "@solana/web3.js"; + +export interface AccountKey { + pubkey: PublicKey; + isSigner: boolean; + isWritable: boolean; +} diff --git a/js/src/instructions/updateInstruction.ts b/js/src/instructions/updateInstruction.ts new file mode 100644 index 0000000..bd17436 --- /dev/null +++ b/js/src/instructions/updateInstruction.ts @@ -0,0 +1,38 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { Numberu32 } from "../int"; + +export function updateInstruction( + nameProgramId: PublicKey, + nameAccountKey: PublicKey, + offset: Numberu32, + input_data: Buffer, + nameUpdateSigner: PublicKey, +): TransactionInstruction { + const buffers = [ + Buffer.from(Int8Array.from([1])), + offset.toBuffer(), + new Numberu32(input_data.length).toBuffer(), + input_data, + ]; + + const data = Buffer.concat(buffers); + const keys = [ + { + pubkey: nameAccountKey, + isSigner: false, + isWritable: true, + }, + { + pubkey: nameUpdateSigner, + isSigner: true, + isWritable: false, + }, + ]; + + return new TransactionInstruction({ + keys, + programId: nameProgramId, + data, + }); +} diff --git a/js/src/int.ts b/js/src/int.ts index 6120f9a..bd0eabe 100644 --- a/js/src/int.ts +++ b/js/src/int.ts @@ -1,5 +1,5 @@ import { Buffer } from "buffer"; -import { ErrorType, SNSError } from "./error"; +import { InvalidBufferLengthError, U64OverflowError } from "./error"; export class Numberu32 { value: bigint; @@ -21,8 +21,7 @@ export class Numberu32 { */ static fromBuffer(buffer: Buffer): Numberu32 { if (buffer.length !== 4) { - throw new SNSError( - ErrorType.InvalidBufferLength, + throw new InvalidBufferLengthError( `Invalid buffer length: ${buffer.length}`, ); } @@ -61,10 +60,7 @@ export class Numberu64 { */ static fromBuffer(buffer: Buffer): Numberu64 { if (buffer.length !== 8) { - throw new SNSError( - ErrorType.U64Overflow, - `Invalid buffer length: ${buffer.length}`, - ); + new U64OverflowError(`Invalid buffer length: ${buffer.length}`); } const value = buffer.readBigUInt64LE(0); diff --git a/js/src/nft/const.ts b/js/src/nft/const.ts new file mode 100644 index 0000000..33f722a --- /dev/null +++ b/js/src/nft/const.ts @@ -0,0 +1,8 @@ +import { PublicKey } from "@solana/web3.js"; +import { Buffer } from "buffer"; + +export const NAME_TOKENIZER_ID = new PublicKey( + "nftD3vbNkNqfj2Sd3HZwbpw4BxxKWr4AjGb9X38JeZk", +); + +export const MINT_PREFIX = Buffer.from("tokenized_name"); diff --git a/js/src/nft/getDomainMint.ts b/js/src/nft/getDomainMint.ts new file mode 100644 index 0000000..ee978b0 --- /dev/null +++ b/js/src/nft/getDomainMint.ts @@ -0,0 +1,10 @@ +import { PublicKey } from "@solana/web3.js"; +import { MINT_PREFIX, NAME_TOKENIZER_ID } from "./const"; + +export const getDomainMint = (domain: PublicKey) => { + const [mint] = PublicKey.findProgramAddressSync( + [MINT_PREFIX, domain.toBuffer()], + NAME_TOKENIZER_ID, + ); + return mint; +}; diff --git a/js/src/nft/getRecordFromMint.ts b/js/src/nft/getRecordFromMint.ts new file mode 100644 index 0000000..e2bf89f --- /dev/null +++ b/js/src/nft/getRecordFromMint.ts @@ -0,0 +1,41 @@ +import { + Connection, + PublicKey, + GetProgramAccountsFilter, +} from "@solana/web3.js"; +import { NAME_TOKENIZER_ID } from "./const"; +import { NftRecord } from "./state"; + +/** + * This function can be used to retrieve a NFT Record given a mint + * + * @param connection A solana RPC connection + * @param mint The mint of the NFT Record + * @returns + */ +export const getRecordFromMint = async ( + connection: Connection, + mint: PublicKey, +) => { + const filters: GetProgramAccountsFilter[] = [ + { dataSize: NftRecord.LEN }, + { + memcmp: { + offset: 0, + bytes: "3", + }, + }, + { + memcmp: { + offset: 1 + 1 + 32 + 32, + bytes: mint.toBase58(), + }, + }, + ]; + + const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { + filters, + }); + + return result; +}; diff --git a/js/src/nft/index.ts b/js/src/nft/index.ts deleted file mode 100644 index ec5ee82..0000000 --- a/js/src/nft/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - PublicKey, - Connection, - GetProgramAccountsFilter, - MemcmpFilter, -} from "@solana/web3.js"; -import { - getMint, - TOKEN_PROGRAM_ID, - RawAccount, - AccountLayout, -} from "@solana/spl-token"; -import { - NAME_TOKENIZER_ID, - NftRecord, - getRecordFromMint, - getDomainMint, -} from "./name-tokenizer"; - -/** - * This function can be used to retrieve the owner of a tokenized domain name - * - * @param connection The solana connection object to the RPC node - * @param nameAccount The key of the domain name - * @returns - */ -export const retrieveNftOwner = async ( - connection: Connection, - nameAccount: PublicKey, -) => { - try { - const mint = getDomainMint(nameAccount); - - const mintInfo = await getMint(connection, mint); - if (mintInfo.supply.toString() === "0") { - return undefined; - } - - const filters: GetProgramAccountsFilter[] = [ - { - memcmp: { - offset: 0, - bytes: mint.toBase58(), - }, - }, - { - memcmp: { - offset: 64, - bytes: "2", - }, - }, - { dataSize: 165 }, - ]; - - const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { - filters, - }); - - if (result.length != 1) { - return undefined; - } - - return new PublicKey(result[0].account.data.slice(32, 64)); - } catch { - return undefined; - } -}; - -/** - * This function can be used to retrieve all the tokenized domains name - * - * @param connection The solana connection object to the RPC node - * @returns - */ -export const retrieveNfts = async (connection: Connection) => { - const filters = [ - { - memcmp: { - offset: 0, - bytes: "3", - }, - }, - ]; - - const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { - filters, - }); - const offset = 1 + 1 + 32 + 32; - return result.map( - (e) => new PublicKey(e.account.data.slice(offset, offset + 32)), - ); -}; - -const getFilter = (owner: string) => { - const filters: MemcmpFilter[] = [ - { - memcmp: { offset: 32, bytes: owner }, - }, - { memcmp: { offset: 64, bytes: "2" } }, - ]; - return filters; -}; - -const closure = async (connection: Connection, acc: RawAccount) => { - const record = await getRecordFromMint(connection, acc.mint); - if (record.length === 1) { - return NftRecord.deserialize(record[0].account.data); - } -}; - -export const retrieveRecords = async ( - connection: Connection, - owner: PublicKey, -) => { - const filters: GetProgramAccountsFilter[] = [ - ...getFilter(owner.toBase58()), - { dataSize: 165 }, - ]; - const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { - filters, - }); - - const tokenAccs = result.map((e) => AccountLayout.decode(e.account.data)); - - const promises = tokenAccs.map((acc) => closure(connection, acc)); - const records = await Promise.all(promises); - - return records.filter((e) => e !== undefined) as NftRecord[]; -}; diff --git a/js/src/nft/retrieveNftOwner.ts b/js/src/nft/retrieveNftOwner.ts new file mode 100644 index 0000000..f0b56af --- /dev/null +++ b/js/src/nft/retrieveNftOwner.ts @@ -0,0 +1,56 @@ +import { + Connection, + GetProgramAccountsFilter, + PublicKey, +} from "@solana/web3.js"; +import { getDomainMint } from "./getDomainMint"; +import { TOKEN_PROGRAM_ID, getMint } from "@solana/spl-token"; + +/** + * This function can be used to retrieve the owner of a tokenized domain name + * + * @param connection The solana connection object to the RPC node + * @param nameAccount The key of the domain name + * @returns + */ +export const retrieveNftOwner = async ( + connection: Connection, + nameAccount: PublicKey, +) => { + try { + const mint = getDomainMint(nameAccount); + + const mintInfo = await getMint(connection, mint); + if (mintInfo.supply.toString() === "0") { + return undefined; + } + + const filters: GetProgramAccountsFilter[] = [ + { + memcmp: { + offset: 0, + bytes: mint.toBase58(), + }, + }, + { + memcmp: { + offset: 64, + bytes: "2", + }, + }, + { dataSize: 165 }, + ]; + + const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { + filters, + }); + + if (result.length != 1) { + return undefined; + } + + return new PublicKey(result[0].account.data.slice(32, 64)); + } catch { + return undefined; + } +}; diff --git a/js/src/nft/retrieveNftOwnerV2.ts b/js/src/nft/retrieveNftOwnerV2.ts new file mode 100644 index 0000000..2d1b64b --- /dev/null +++ b/js/src/nft/retrieveNftOwnerV2.ts @@ -0,0 +1,37 @@ +import { PublicKey, Connection, SolanaJSONRPCError } from "@solana/web3.js"; +import { getDomainMint } from "./getDomainMint"; +import { AccountLayout } from "@solana/spl-token"; + +export const retrieveNftOwnerV2 = async ( + connection: Connection, + nameAccount: PublicKey, +) => { + try { + const mint = getDomainMint(nameAccount); + + const largestAccounts = await connection.getTokenLargestAccounts(mint); + if (largestAccounts.value.length === 0) { + return null; + } + + const largestAccountInfo = await connection.getAccountInfo( + largestAccounts.value[0].address, + ); + + if (!largestAccountInfo) { + return null; + } + + const decoded = AccountLayout.decode(largestAccountInfo.data); + if (decoded.amount.toString() === "1") { + return decoded.owner; + } + return null; + } catch (err) { + if (err instanceof SolanaJSONRPCError && err.code === -32602) { + // Mint does not exist + return null; + } + throw err; + } +}; diff --git a/js/src/nft/retrieveNfts.ts b/js/src/nft/retrieveNfts.ts new file mode 100644 index 0000000..f82688d --- /dev/null +++ b/js/src/nft/retrieveNfts.ts @@ -0,0 +1,33 @@ +import { + Connection, + GetProgramAccountsFilter, + PublicKey, +} from "@solana/web3.js"; +import { NAME_TOKENIZER_ID } from "./const"; +import { NftRecord } from "./state"; + +/** + * This function can be used to retrieve all the tokenized domains name + * + * @param connection The solana connection object to the RPC node + * @returns + */ +export const retrieveNfts = async (connection: Connection) => { + const filters: GetProgramAccountsFilter[] = [ + { dataSize: NftRecord.LEN }, + { + memcmp: { + offset: 0, + bytes: "3", + }, + }, + ]; + + const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { + filters, + }); + const offset = 1 + 1 + 32 + 32; + return result.map( + (e) => new PublicKey(e.account.data.slice(offset, offset + 32)), + ); +}; diff --git a/js/src/nft/retrieveRecords.ts b/js/src/nft/retrieveRecords.ts new file mode 100644 index 0000000..a8e9bb5 --- /dev/null +++ b/js/src/nft/retrieveRecords.ts @@ -0,0 +1,46 @@ +import { + PublicKey, + Connection, + GetProgramAccountsFilter, + MemcmpFilter, +} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID, RawAccount, AccountLayout } from "@solana/spl-token"; +import { NftRecord } from "./state"; +import { getRecordFromMint } from "./getRecordFromMint"; + +const getFilter = (owner: string) => { + const filters: MemcmpFilter[] = [ + { + memcmp: { offset: 32, bytes: owner }, + }, + { memcmp: { offset: 64, bytes: "2" } }, + ]; + return filters; +}; + +const closure = async (connection: Connection, acc: RawAccount) => { + const record = await getRecordFromMint(connection, acc.mint); + if (record.length === 1) { + return NftRecord.deserialize(record[0].account.data); + } +}; + +export const retrieveRecords = async ( + connection: Connection, + owner: PublicKey, +) => { + const filters: GetProgramAccountsFilter[] = [ + ...getFilter(owner.toBase58()), + { dataSize: 165 }, + ]; + const result = await connection.getProgramAccounts(TOKEN_PROGRAM_ID, { + filters, + }); + + const tokenAccs = result.map((e) => AccountLayout.decode(e.account.data)); + + const promises = tokenAccs.map((acc) => closure(connection, acc)); + const records = await Promise.all(promises); + + return records.filter((e) => e !== undefined) as NftRecord[]; +}; diff --git a/js/src/nft/name-tokenizer.ts b/js/src/nft/state.ts similarity index 58% rename from js/src/nft/name-tokenizer.ts rename to js/src/nft/state.ts index 2074853..da3d887 100644 --- a/js/src/nft/name-tokenizer.ts +++ b/js/src/nft/state.ts @@ -1,20 +1,7 @@ -import { deserialize, Schema } from "borsh"; +import { deserialize } from "borsh"; import { Connection, PublicKey } from "@solana/web3.js"; import { Buffer } from "buffer"; - -export const NAME_TOKENIZER_ID = new PublicKey( - "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; -}; +import { NftRecordNotFoundError } from "../error"; export enum Tag { Uninitialized = 0, @@ -30,6 +17,8 @@ export class NftRecord { owner: PublicKey; nftMint: PublicKey; + static LEN = 1 + 1 + 32 + 32 + 32; + static schema = { struct: { tag: "u8", @@ -61,7 +50,9 @@ export class NftRecord { static async retrieve(connection: Connection, key: PublicKey) { const accountInfo = await connection.getAccountInfo(key); if (!accountInfo || !accountInfo.data) { - throw new Error("NFT record not found"); + throw new NftRecordNotFoundError( + "NFT record not found: " + key.toBase58(), + ); } return this.deserialize(accountInfo.data); } @@ -71,37 +62,10 @@ export class NftRecord { programId, ); } + static findKeySync(nameAccount: PublicKey, programId: PublicKey) { + return PublicKey.findProgramAddressSync( + [Buffer.from("nft_record"), nameAccount.toBuffer()], + programId, + ); + } } - -/** - * This function can be used to retrieve a NFT Record given a mint - * - * @param connection A solana RPC connection - * @param mint The mint of the NFT Record - * @returns - */ -export const getRecordFromMint = async ( - connection: Connection, - mint: PublicKey, -) => { - const filters = [ - { - memcmp: { - offset: 0, - bytes: "3", - }, - }, - { - memcmp: { - offset: 1 + 1 + 32 + 32, - bytes: mint.toBase58(), - }, - }, - ]; - - const result = await connection.getProgramAccounts(NAME_TOKENIZER_ID, { - filters, - }); - - return result; -}; diff --git a/js/src/record.ts b/js/src/record.ts deleted file mode 100644 index 87edc7e..0000000 --- a/js/src/record.ts +++ /dev/null @@ -1,523 +0,0 @@ -import { RECORD_V1_SIZE, Record, RecordVersion } from "./types/record"; -import { Connection, PublicKey } from "@solana/web3.js"; -import { getDomainKeySync } from "./utils"; -import { NameRegistryState } from "./state"; -import { Buffer } from "buffer"; -import { encode as bs58Encode } from "bs58"; -import { - isValid as isValidIp, - fromByteArray as ipFromByteArray, - parse as parseIp, -} from "ipaddr.js"; -import { encode as encodePunycode, decode as decodePunnyCode } from "punycode"; -import { check } from "./utils"; -import { ErrorType, SNSError } from "./error"; -import { ed25519 } from "@noble/curves/ed25519"; -import { bech32 } from "@scure/base"; - -const trimNullPaddingIdx = (buffer: Buffer): number => { - const arr = Array.from(buffer); - const lastNonNull = - arr.length - 1 - arr.reverse().findIndex((byte) => byte !== 0); - return lastNonNull + 1; -}; - -/** - * This function can be used to verify the validity of a SOL record - * @param record The record data to verify - * @param signedRecord The signed data - * @param pubkey The public key of the signer - * @returns - */ -export const checkSolRecord = ( - record: Uint8Array, - signedRecord: Uint8Array, - pubkey: PublicKey, -) => { - return ed25519.verify(signedRecord, record, pubkey.toBytes()); -}; - -/** - * This function can be used to derive a record key - * @param domain The .sol domain name - * @param record The record to derive the key for - * @returns - */ -export const getRecordKeySync = (domain: string, record: Record) => { - const { pubkey } = getDomainKeySync(record + "." + domain, RecordVersion.V1); - return pubkey; -}; - -// Overload signature for the case where deserialize is true. -export async function getRecord( - connection: Connection, - domain: string, - record: Record, - deserialize: true, -): Promise; - -// Overload signature for the case where deserialize is false or undefined. -export async function getRecord( - connection: Connection, - domain: string, - record: Record, - deserialize?: false, -): Promise; - -/** - * This function can be used to retrieve a specified record for the given domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param record The record to search for - * @returns - */ -export async function getRecord( - connection: Connection, - domain: string, - record: Record, - deserialize?: boolean, -) { - const pubkey = getRecordKeySync(domain, record); - let { registry } = await NameRegistryState.retrieve(connection, pubkey); - - if (!registry.data) { - throw new SNSError(ErrorType.NoRecordData); - } - - if (deserialize) { - return deserializeRecord(registry, record, pubkey); - } - const recordSize = RECORD_V1_SIZE.get(record); - registry.data = registry.data.slice(0, recordSize); - - return registry; -} - -// Overload signature for the case where deserialize is true. -export async function getRecords( - connection: Connection, - domain: string, - records: Record[], - deserialize: true, -): Promise; - -// Overload signature for the case where deserialize is false or undefined. -export async function getRecords( - connection: Connection, - domain: string, - records: Record[], - deserialize?: false, -): Promise; - -export async function getRecords( - connection: Connection, - domain: string, - records: Record[], - deserialize?: boolean, -) { - const pubkeys = records.map((record) => getRecordKeySync(domain, record)); - const registries = await NameRegistryState.retrieveBatch(connection, pubkeys); - - if (deserialize) { - return registries.map((e, idx) => { - if (!e) return undefined; - return deserializeRecord( - e, - records[idx], - getRecordKeySync(domain, records[idx]), - ); - }); - } - return registries; -} - -/** - * This function can be used to retrieve the IPFS record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getIpfsRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.IPFS, true); -}; - -/** - * This function can be used to retrieve the Arweave record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getArweaveRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.ARWV, true); -}; - -/** - * This function can be used to retrieve the ETH record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getEthRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.ETH, true); -}; - -/** - * This function can be used to retrieve the BTC record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getBtcRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.BTC, true); -}; - -/** - * This function can be used to retrieve the LTC record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getLtcRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.LTC, true); -}; - -/** - * This function can be used to retrieve the DOGE record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getDogeRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.DOGE, true); -}; - -/** - * This function can be used to retrieve the email record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getEmailRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Email, true); -}; - -/** - * This function can be used to retrieve the URL record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getUrlRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.Url, true); -}; - -/** - * This function can be used to retrieve the Discord record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getDiscordRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Discord, true); -}; - -/** - * This function can be used to retrieve the Github record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getGithubRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Github, true); -}; - -/** - * This function can be used to retrieve the Reddit record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getRedditRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Reddit, true); -}; - -/** - * This function can be used to retrieve the Twitter record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getTwitterRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Twitter, true); -}; - -/** - * This function can be used to retrieve the Telegram record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getTelegramRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Telegram, true); -}; - -/** - * This function can be used to retrieve the pic record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getPicRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.Pic, true); -}; - -/** - * This function can be used to retrieve the SHDW record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getShdwRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.SHDW, true); -}; - -/** - * This function can be used to retrieve the SOL record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getSolRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.SOL); -}; - -/** - * This function can be used to retrieve the POINT record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getPointRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.POINT, true); -}; - -/** - * This function can be used to retrieve the BSC record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getBscRecord = async (connection: Connection, domain: string) => { - return await getRecord(connection, domain, Record.BSC, true); -}; - -/** - * This function can be used to retrieve the Injective record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getInjectiveRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Injective, true); -}; - -/** - * This function can be used to retrieve the Backpack record of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getBackpackRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Backpack, true); -}; - -/** - - * This function can be used to deserialize the content of a record. If the content is invalid it will throw an error - * This function can be used to retrieve the Background record (V1) of a domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @returns - */ -export const getBackgroundRecord = async ( - connection: Connection, - domain: string, -) => { - return await getRecord(connection, domain, Record.Background, true); -}; - -/** - * This function can be used to deserialize the content of a record (V1). If the content is invalid it will throw an error - * @param registry The name registry state object of the record being deserialized - * @param record The record enum being deserialized - * @param recordKey The public key of the record being deserialized - * @returns - */ -export const deserializeRecord = ( - registry: NameRegistryState | undefined, - record: Record, - recordKey: PublicKey, -): string | undefined => { - const buffer = registry?.data; - if (!buffer) return undefined; - if (buffer.compare(Buffer.alloc(buffer.length)) === 0) return undefined; - - const size = RECORD_V1_SIZE.get(record); - const idx = trimNullPaddingIdx(buffer); - - if (!size) { - const str = buffer.slice(0, idx).toString("utf-8"); - if (record === Record.CNAME || record === Record.TXT) { - return decodePunnyCode(str); - } - return str; - } - - // Handle SOL record first whether it's over allocated or not - if (record === Record.SOL) { - const encoder = new TextEncoder(); - const expectedBuffer = Buffer.concat([ - buffer.slice(0, 32), - recordKey.toBuffer(), - ]); - const expected = encoder.encode(expectedBuffer.toString("hex")); - const valid = checkSolRecord( - expected, - buffer.slice(32, 96), - registry.owner, - ); - if (valid) { - return bs58Encode(buffer.slice(0, 32)); - } - } - - // Old record UTF-8 encoded - if (size && idx !== size) { - const address = buffer.slice(0, idx).toString("utf-8"); - if (record === Record.Injective) { - const decoded = bech32.decodeToBytes(address); - if (decoded.prefix === "inj" && decoded.bytes.length === 20) { - return address; - } - } else if (record === Record.BSC || record === Record.ETH) { - const prefix = address.slice(0, 2); - const hex = address.slice(2); - if (prefix === "0x" && Buffer.from(hex, "hex").length === 20) { - return address; - } - } else if (record === Record.A || record === Record.AAAA) { - if (isValidIp(address)) { - return address; - } - } - throw new SNSError(ErrorType.InvalidRecordData); - } - - if (record === Record.ETH || record === Record.BSC) { - return "0x" + buffer.slice(0, size).toString("hex"); - } else if (record === Record.Injective) { - return bech32.encode("inj", bech32.toWords(buffer.slice(0, size))); - } else if (record === Record.A || record === Record.AAAA) { - return ipFromByteArray([...buffer.slice(0, size)]).toString(); - } else if (record === Record.Background) { - return new PublicKey(buffer.slice(0, size)).toString(); - } - throw new SNSError(ErrorType.InvalidRecordData); -}; - -/** - * This function can be used to serialize a user input string into a buffer that will be stored into a record account data - * For serializing SOL records use `serializeSolRecord` - * @param str The string being serialized into the record account data - * @param record The record enum being serialized - * @returns - */ -export const serializeRecord = (str: string, record: Record): Buffer => { - const size = RECORD_V1_SIZE.get(record); - - if (!size) { - if (record === Record.CNAME || record === Record.TXT) { - str = encodePunycode(str); - } - return Buffer.from(str, "utf-8"); - } - - if (record === Record.SOL) { - throw new SNSError( - ErrorType.UnsupportedRecord, - "Use `serializeSolRecord` for SOL record", - ); - } else if (record === Record.ETH || record === Record.BSC) { - check(str.slice(0, 2) === "0x", ErrorType.InvalidEvmAddress); - return Buffer.from(str.slice(2), "hex"); - } else if (record === Record.Injective) { - const decoded = bech32.decodeToBytes(str); - check(decoded.prefix === "inj", ErrorType.InvalidInjectiveAddress); - check(decoded.bytes.length === 20, ErrorType.InvalidInjectiveAddress); - return Buffer.from(decoded.bytes); - } else if (record === Record.A) { - const array = parseIp(str).toByteArray(); - check(array.length === 4, ErrorType.InvalidARecord); - return Buffer.from(array); - } else if (record === Record.AAAA) { - const array = parseIp(str).toByteArray(); - check(array.length === 16, ErrorType.InvalidAAAARecord); - return Buffer.from(array); - } else if (record === Record.Background) { - return new PublicKey(str).toBuffer(); - } - - throw new SNSError(ErrorType.InvalidRecordInput); -}; - -/** - * This function can be used to build the content of a SOL record - * @param content The public key being stored in the SOL record - * @param recordKey The record public key - * @param signer The signer of the record i.e the domain owner - * @param signature The signature of the record's content - * @returns - */ -export const serializeSolRecord = ( - content: PublicKey, - recordKey: PublicKey, - signer: PublicKey, - signature: Uint8Array, -): Buffer => { - const expected = Buffer.concat([content.toBuffer(), recordKey.toBuffer()]); - const encodedMessage = new TextEncoder().encode(expected.toString("hex")); - const valid = checkSolRecord(encodedMessage, signature, signer); - check(valid, ErrorType.InvalidSignature); - - return Buffer.concat([content.toBuffer(), signature]); -}; diff --git a/js/src/record/checkSolRecord.ts b/js/src/record/checkSolRecord.ts new file mode 100644 index 0000000..55184fb --- /dev/null +++ b/js/src/record/checkSolRecord.ts @@ -0,0 +1,17 @@ +import { PublicKey } from "@solana/web3.js"; +import { ed25519 } from "@noble/curves/ed25519"; + +/** + * This function can be used to verify the validity of a SOL record + * @param record The record data to verify + * @param signedRecord The signed data + * @param pubkey The public key of the signer + * @returns + */ +export const checkSolRecord = ( + record: Uint8Array, + signedRecord: Uint8Array, + pubkey: PublicKey, +) => { + return ed25519.verify(signedRecord, record, pubkey.toBytes()); +}; diff --git a/js/src/record/deserializeRecord.ts b/js/src/record/deserializeRecord.ts new file mode 100644 index 0000000..864a5e2 --- /dev/null +++ b/js/src/record/deserializeRecord.ts @@ -0,0 +1,100 @@ +import { Buffer } from "buffer"; +import { encode as bs58Encode } from "bs58"; +import { PublicKey } from "@solana/web3.js"; +import { bech32 } from "@scure/base"; +import { decode as decodePunnyCode } from "punycode"; +import { + isValid as isValidIp, + fromByteArray as ipFromByteArray, +} from "ipaddr.js"; +import { RECORD_V1_SIZE, Record } from "../types/record"; +import { NameRegistryState } from "../state"; +import { InvalidRecordDataError } from "../error"; + +import { checkSolRecord } from "./checkSolRecord"; + +const trimNullPaddingIdx = (buffer: Buffer): number => { + const arr = Array.from(buffer); + const lastNonNull = + arr.length - 1 - arr.reverse().findIndex((byte) => byte !== 0); + return lastNonNull + 1; +}; + +/** + * This function can be used to deserialize the content of a record (V1). If the content is invalid it will throw an error + * @param registry The name registry state object of the record being deserialized + * @param record The record enum being deserialized + * @param recordKey The public key of the record being deserialized + * @returns + */ +export const deserializeRecord = ( + registry: NameRegistryState | undefined, + record: Record, + recordKey: PublicKey, +): string | undefined => { + const buffer = registry?.data; + if (!buffer) return undefined; + if (buffer.compare(Buffer.alloc(buffer.length)) === 0) return undefined; + + const size = RECORD_V1_SIZE.get(record); + const idx = trimNullPaddingIdx(buffer); + + if (!size) { + const str = buffer.slice(0, idx).toString("utf-8"); + if (record === Record.CNAME || record === Record.TXT) { + return decodePunnyCode(str); + } + return str; + } + + // Handle SOL record first whether it's over allocated or not + if (record === Record.SOL) { + const encoder = new TextEncoder(); + const expectedBuffer = Buffer.concat([ + buffer.slice(0, 32), + recordKey.toBuffer(), + ]); + const expected = encoder.encode(expectedBuffer.toString("hex")); + const valid = checkSolRecord( + expected, + buffer.slice(32, 96), + registry.owner, + ); + if (valid) { + return bs58Encode(buffer.slice(0, 32)); + } + } + + // Old record UTF-8 encoded + if (size && idx !== size) { + const address = buffer.slice(0, idx).toString("utf-8"); + if (record === Record.Injective) { + const decoded = bech32.decodeToBytes(address); + if (decoded.prefix === "inj" && decoded.bytes.length === 20) { + return address; + } + } else if (record === Record.BSC || record === Record.ETH) { + const prefix = address.slice(0, 2); + const hex = address.slice(2); + if (prefix === "0x" && Buffer.from(hex, "hex").length === 20) { + return address; + } + } else if (record === Record.A || record === Record.AAAA) { + if (isValidIp(address)) { + return address; + } + } + throw new InvalidRecordDataError("The record data is malformed"); + } + + if (record === Record.ETH || record === Record.BSC) { + return "0x" + buffer.slice(0, size).toString("hex"); + } else if (record === Record.Injective) { + return bech32.encode("inj", bech32.toWords(buffer.slice(0, size))); + } else if (record === Record.A || record === Record.AAAA) { + return ipFromByteArray([...buffer.slice(0, size)]).toString(); + } else if (record === Record.Background) { + return new PublicKey(buffer.slice(0, size)).toString(); + } + throw new InvalidRecordDataError("The record data is malformed"); +}; diff --git a/js/src/record/getRecord.ts b/js/src/record/getRecord.ts new file mode 100644 index 0000000..a8961da --- /dev/null +++ b/js/src/record/getRecord.ts @@ -0,0 +1,52 @@ +import { Connection } from "@solana/web3.js"; +import { RECORD_V1_SIZE, Record } from "../types/record"; +import { NameRegistryState } from "../state"; +import { NoRecordDataError } from "../error"; + +import { getRecordKeySync } from "./getRecordKeySync"; +import { deserializeRecord } from "./deserializeRecord"; + +// Overload signature for the case where deserialize is true. +export async function getRecord( + connection: Connection, + domain: string, + record: Record, + deserialize: true, +): Promise; + +// Overload signature for the case where deserialize is false or undefined. +export async function getRecord( + connection: Connection, + domain: string, + record: Record, + deserialize?: false, +): Promise; + +/** + * This function can be used to retrieve a specified record for the given domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param record The record to search for + * @returns + */ +export async function getRecord( + connection: Connection, + domain: string, + record: Record, + deserialize?: boolean, +) { + const pubkey = getRecordKeySync(domain, record); + let { registry } = await NameRegistryState.retrieve(connection, pubkey); + + if (!registry.data) { + throw new NoRecordDataError(`The record data is empty`); + } + + if (deserialize) { + return deserializeRecord(registry, record, pubkey); + } + const recordSize = RECORD_V1_SIZE.get(record); + registry.data = registry.data.slice(0, recordSize); + + return registry; +} diff --git a/js/src/record/getRecordKeySync.ts b/js/src/record/getRecordKeySync.ts new file mode 100644 index 0000000..30f2b86 --- /dev/null +++ b/js/src/record/getRecordKeySync.ts @@ -0,0 +1,13 @@ +import { Record, RecordVersion } from "../types/record"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; + +/** + * This function can be used to derive a record key + * @param domain The .sol domain name + * @param record The record to derive the key for + * @returns + */ +export const getRecordKeySync = (domain: string, record: Record) => { + const { pubkey } = getDomainKeySync(record + "." + domain, RecordVersion.V1); + return pubkey; +}; diff --git a/js/src/record/getRecords.ts b/js/src/record/getRecords.ts new file mode 100644 index 0000000..11bb48f --- /dev/null +++ b/js/src/record/getRecords.ts @@ -0,0 +1,43 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../types/record"; +import { NameRegistryState } from "../state"; +import { getRecordKeySync } from "./getRecordKeySync"; +import { deserializeRecord } from "./deserializeRecord"; + +// Overload signature for the case where deserialize is true. +export async function getRecords( + connection: Connection, + domain: string, + records: Record[], + deserialize: true, +): Promise; + +// Overload signature for the case where deserialize is false or undefined. +export async function getRecords( + connection: Connection, + domain: string, + records: Record[], + deserialize?: false, +): Promise; + +export async function getRecords( + connection: Connection, + domain: string, + records: Record[], + deserialize?: boolean, +) { + const pubkeys = records.map((record) => getRecordKeySync(domain, record)); + const registries = await NameRegistryState.retrieveBatch(connection, pubkeys); + + if (deserialize) { + return registries.map((e, idx) => { + if (!e) return undefined; + return deserializeRecord( + e, + records[idx], + getRecordKeySync(domain, records[idx]), + ); + }); + } + return registries; +} diff --git a/js/src/record/helpers/getArweaveRecord.ts b/js/src/record/helpers/getArweaveRecord.ts new file mode 100644 index 0000000..ce9c876 --- /dev/null +++ b/js/src/record/helpers/getArweaveRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Arweave record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getArweaveRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.ARWV, true); +}; diff --git a/js/src/record/helpers/getBackgroundRecord.ts b/js/src/record/helpers/getBackgroundRecord.ts new file mode 100644 index 0000000..a3d7ef0 --- /dev/null +++ b/js/src/record/helpers/getBackgroundRecord.ts @@ -0,0 +1,14 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to deserialize the content of a record. If the content is invalid it will throw an error + * This function can be used to retrieve the Background record (V1) of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getBackgroundRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Background, true); +}; diff --git a/js/src/record/helpers/getBackpackRecord.ts b/js/src/record/helpers/getBackpackRecord.ts new file mode 100644 index 0000000..036142b --- /dev/null +++ b/js/src/record/helpers/getBackpackRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Backpack record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getBackpackRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Backpack, true); +}; diff --git a/js/src/record/helpers/getBscRecord.ts b/js/src/record/helpers/getBscRecord.ts new file mode 100644 index 0000000..09e9756 --- /dev/null +++ b/js/src/record/helpers/getBscRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the BSC record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getBscRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.BSC, true); +}; diff --git a/js/src/record/helpers/getBtcRecord.ts b/js/src/record/helpers/getBtcRecord.ts new file mode 100644 index 0000000..3c61537 --- /dev/null +++ b/js/src/record/helpers/getBtcRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the BTC record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getBtcRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.BTC, true); +}; diff --git a/js/src/record/helpers/getDiscordRecord.ts b/js/src/record/helpers/getDiscordRecord.ts new file mode 100644 index 0000000..cfb6716 --- /dev/null +++ b/js/src/record/helpers/getDiscordRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Discord record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getDiscordRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Discord, true); +}; diff --git a/js/src/record/helpers/getDogeRecord.ts b/js/src/record/helpers/getDogeRecord.ts new file mode 100644 index 0000000..f8a2beb --- /dev/null +++ b/js/src/record/helpers/getDogeRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the DOGE record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getDogeRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.DOGE, true); +}; diff --git a/js/src/record/helpers/getEmailRecord.ts b/js/src/record/helpers/getEmailRecord.ts new file mode 100644 index 0000000..bdcf2de --- /dev/null +++ b/js/src/record/helpers/getEmailRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the email record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getEmailRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Email, true); +}; diff --git a/js/src/record/helpers/getEthRecord.ts b/js/src/record/helpers/getEthRecord.ts new file mode 100644 index 0000000..a2819a2 --- /dev/null +++ b/js/src/record/helpers/getEthRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the ETH record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getEthRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.ETH, true); +}; diff --git a/js/src/record/helpers/getGithubRecord.ts b/js/src/record/helpers/getGithubRecord.ts new file mode 100644 index 0000000..2793d4f --- /dev/null +++ b/js/src/record/helpers/getGithubRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Github record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getGithubRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Github, true); +}; diff --git a/js/src/record/helpers/getInjectiveRecord.ts b/js/src/record/helpers/getInjectiveRecord.ts new file mode 100644 index 0000000..d3b2c9b --- /dev/null +++ b/js/src/record/helpers/getInjectiveRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Injective record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getInjectiveRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Injective, true); +}; diff --git a/js/src/record/helpers/getIpfsRecord.ts b/js/src/record/helpers/getIpfsRecord.ts new file mode 100644 index 0000000..09d6305 --- /dev/null +++ b/js/src/record/helpers/getIpfsRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the IPFS record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getIpfsRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.IPFS, true); +}; diff --git a/js/src/record/helpers/getLtcRecord.ts b/js/src/record/helpers/getLtcRecord.ts new file mode 100644 index 0000000..2ffb100 --- /dev/null +++ b/js/src/record/helpers/getLtcRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the LTC record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getLtcRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.LTC, true); +}; diff --git a/js/src/record/helpers/getPicRecord.ts b/js/src/record/helpers/getPicRecord.ts new file mode 100644 index 0000000..b57416f --- /dev/null +++ b/js/src/record/helpers/getPicRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the pic record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getPicRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Pic, true); +}; diff --git a/js/src/record/helpers/getPointRecord.ts b/js/src/record/helpers/getPointRecord.ts new file mode 100644 index 0000000..fe4a156 --- /dev/null +++ b/js/src/record/helpers/getPointRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the POINT record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getPointRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.POINT, true); +}; diff --git a/js/src/record/helpers/getRedditRecord.ts b/js/src/record/helpers/getRedditRecord.ts new file mode 100644 index 0000000..cbe1843 --- /dev/null +++ b/js/src/record/helpers/getRedditRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Reddit record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getRedditRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Reddit, true); +}; diff --git a/js/src/record/helpers/getShdwRecord.ts b/js/src/record/helpers/getShdwRecord.ts new file mode 100644 index 0000000..6efdff0 --- /dev/null +++ b/js/src/record/helpers/getShdwRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the SHDW record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getShdwRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.SHDW, true); +}; diff --git a/js/src/record/helpers/getSolRecord.ts b/js/src/record/helpers/getSolRecord.ts new file mode 100644 index 0000000..40aabb4 --- /dev/null +++ b/js/src/record/helpers/getSolRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the SOL record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getSolRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.SOL); +}; diff --git a/js/src/record/helpers/getTelegramRecord.ts b/js/src/record/helpers/getTelegramRecord.ts new file mode 100644 index 0000000..6a11429 --- /dev/null +++ b/js/src/record/helpers/getTelegramRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Telegram record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getTelegramRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Telegram, true); +}; diff --git a/js/src/record/helpers/getTwitterRecord.ts b/js/src/record/helpers/getTwitterRecord.ts new file mode 100644 index 0000000..2bf0302 --- /dev/null +++ b/js/src/record/helpers/getTwitterRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the Twitter record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getTwitterRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Twitter, true); +}; diff --git a/js/src/record/helpers/getUrlRecord.ts b/js/src/record/helpers/getUrlRecord.ts new file mode 100644 index 0000000..8d71a1e --- /dev/null +++ b/js/src/record/helpers/getUrlRecord.ts @@ -0,0 +1,13 @@ +import { Connection } from "@solana/web3.js"; +import { Record } from "../../types/record"; +import { getRecord } from "../getRecord"; + +/** + * This function can be used to retrieve the URL record of a domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @returns + */ +export const getUrlRecord = (connection: Connection, domain: string) => { + return getRecord(connection, domain, Record.Url, true); +}; diff --git a/js/src/record/serializeRecord.ts b/js/src/record/serializeRecord.ts new file mode 100644 index 0000000..aed79ca --- /dev/null +++ b/js/src/record/serializeRecord.ts @@ -0,0 +1,73 @@ +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import { bech32 } from "@scure/base"; +import { encode as encodePunycode } from "punycode"; +import { parse as parseIp } from "ipaddr.js"; +import { RECORD_V1_SIZE, Record } from "../types/record"; +import { check } from "../utils/check"; +import { + InvalidAAAARecordError, + InvalidARecordError, + InvalidEvmAddressError, + InvalidInjectiveAddressError, + InvalidRecordInputError, + UnsupportedRecordError, +} from "../error"; + +/** + * This function can be used to serialize a user input string into a buffer that will be stored into a record account data + * For serializing SOL records use `serializeSolRecord` + * @param str The string being serialized into the record account data + * @param record The record enum being serialized + * @returns + */ +export const serializeRecord = (str: string, record: Record): Buffer => { + const size = RECORD_V1_SIZE.get(record); + + if (!size) { + if (record === Record.CNAME || record === Record.TXT) { + str = encodePunycode(str); + } + return Buffer.from(str, "utf-8"); + } + + if (record === Record.SOL) { + throw new UnsupportedRecordError("Use `serializeSolRecord` for SOL record"); + } else if (record === Record.ETH || record === Record.BSC) { + check( + str.slice(0, 2) === "0x", + new InvalidEvmAddressError("The record content must start with `0x`"), + ); + return Buffer.from(str.slice(2), "hex"); + } else if (record === Record.Injective) { + const decoded = bech32.decodeToBytes(str); + check( + decoded.prefix === "inj", + new InvalidInjectiveAddressError( + "The record content must start with `inj", + ), + ); + check( + decoded.bytes.length === 20, + new InvalidInjectiveAddressError(`The record data must be 20 bytes long`), + ); + return Buffer.from(decoded.bytes); + } else if (record === Record.A) { + const array = parseIp(str).toByteArray(); + check( + array.length === 4, + new InvalidARecordError(`The record content must be 4 bytes long`), + ); + return Buffer.from(array); + } else if (record === Record.AAAA) { + const array = parseIp(str).toByteArray(); + check( + array.length === 16, + new InvalidAAAARecordError(`The record content must be 16 bytes logn`), + ); + return Buffer.from(array); + } else if (record === Record.Background) { + return new PublicKey(str).toBuffer(); + } + throw new InvalidRecordInputError(`The provided record data is invalid`); +}; diff --git a/js/src/record/serializeSolRecord.ts b/js/src/record/serializeSolRecord.ts new file mode 100644 index 0000000..a31a153 --- /dev/null +++ b/js/src/record/serializeSolRecord.ts @@ -0,0 +1,28 @@ +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import { check } from "../utils/check"; +import { InvalidSignatureError } from "../error"; + +import { checkSolRecord } from "./checkSolRecord"; + +/** + * This function can be used to build the content of a SOL record + * @param content The public key being stored in the SOL record + * @param recordKey The record public key + * @param signer The signer of the record i.e the domain owner + * @param signature The signature of the record's content + * @returns + */ +export const serializeSolRecord = ( + content: PublicKey, + recordKey: PublicKey, + signer: PublicKey, + signature: Uint8Array, +): Buffer => { + const expected = Buffer.concat([content.toBuffer(), recordKey.toBuffer()]); + const encodedMessage = new TextEncoder().encode(expected.toString("hex")); + const valid = checkSolRecord(encodedMessage, signature, signer); + check(valid, new InvalidSignatureError("The SOL signature is invalid")); + + return Buffer.concat([content.toBuffer(), signature]); +}; diff --git a/js/src/record_v2/const.ts b/js/src/record_v2/const.ts new file mode 100644 index 0000000..1989598 --- /dev/null +++ b/js/src/record_v2/const.ts @@ -0,0 +1,61 @@ +import { Record } from "../types/record"; +import { PublicKey } from "@solana/web3.js"; + +/** + * A map that associates each record type with a public key, known as guardians. + */ +export const GUARDIANS = new Map([ + [Record.Url, new PublicKey("ExXjtfdQe8JacoqP9Z535WzQKjF4CzW1TTRKRgpxvya3")], + [Record.CNAME, new PublicKey("ExXjtfdQe8JacoqP9Z535WzQKjF4CzW1TTRKRgpxvya3")], +]); + +/** + * Set of records that utilize secp256k1 for verification purposes + */ +export const ETH_ROA_RECORDS = new Set([ + Record.ETH, + Record.Injective, + Record.BSC, + Record.BASE, +]); + +export const EVM_RECORDS = new Set([ + Record.ETH, + Record.BSC, + Record.BASE, +]); + +/** + * Set of records that are UTF-8 encoded strings + */ +export const UTF8_ENCODED = new Set([ + Record.IPFS, + Record.ARWV, + Record.LTC, + Record.DOGE, + Record.Email, + Record.Url, + Record.Discord, + Record.Github, + Record.Reddit, + Record.Twitter, + Record.Telegram, + Record.Pic, + Record.SHDW, + Record.POINT, + Record.Backpack, + Record.TXT, + Record.CNAME, + Record.BTC, + Record.IPNS, +]); + +/** + * Set of records that are self signed i.e signed by the public key contained + * in the record itself. + */ +export const SELF_SIGNED = new Set([ + Record.ETH, + Record.Injective, + Record.SOL, +]); diff --git a/js/src/record_v2/deserializeRecordV2Content.ts b/js/src/record_v2/deserializeRecordV2Content.ts new file mode 100644 index 0000000..179f928 --- /dev/null +++ b/js/src/record_v2/deserializeRecordV2Content.ts @@ -0,0 +1,40 @@ +import { Record } from "../types/record"; +import { InvalidRecordDataError } from "../error"; +import { PublicKey } from "@solana/web3.js"; +import { decode as decodePunnycode } from "punycode"; +import { bech32 } from "@scure/base"; +import { fromByteArray as ipFromByteArray } from "ipaddr.js"; + +import { UTF8_ENCODED, EVM_RECORDS } from "./const"; + +/** + * This function deserializes a buffer based on the type of record it corresponds to + * If the record is not properly serialized according to SNS-IP 1 this function will throw an error + * @param content The content to deserialize + * @param record The type of record + * @returns The deserialized content as a string + */ +export const deserializeRecordV2Content = ( + content: Buffer, + record: Record, +): string => { + const utf8Encoded = UTF8_ENCODED.has(record); + + if (utf8Encoded) { + const decoded = content.toString("utf-8"); + if (record === Record.CNAME || record === Record.TXT) { + return decodePunnycode(decoded); + } + return decoded; + } else if (record === Record.SOL) { + return new PublicKey(content).toBase58(); + } else if (EVM_RECORDS.has(record)) { + return "0x" + content.toString("hex"); + } else if (record === Record.Injective) { + return bech32.encode("inj", bech32.toWords(content)); + } else if (record === Record.A || record === Record.AAAA) { + return ipFromByteArray([...content]).toString(); + } else { + throw new InvalidRecordDataError("The record content is malformed"); + } +}; diff --git a/js/src/record_v2/getMultipleRecordsV2.ts b/js/src/record_v2/getMultipleRecordsV2.ts new file mode 100644 index 0000000..a7aba17 --- /dev/null +++ b/js/src/record_v2/getMultipleRecordsV2.ts @@ -0,0 +1,55 @@ +import { Record } from "../types/record"; +import { Connection } from "@solana/web3.js"; +import { Record as SnsRecord } from "@bonfida/sns-records"; + +import { getRecordV2Key } from "./getRecordV2Key"; +import { deserializeRecordV2Content } from "./deserializeRecordV2Content"; + +interface GetRecordV2Options { + deserialize?: boolean; +} + +interface RecordResult { + retrievedRecord: SnsRecord; + record: Record; + deserializedContent?: string; +} + +/** + * This function can be used to retrieve multiple records V2 for a given domain + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param record The record to search for + * @returns + */ +export async function getMultipleRecordsV2( + connection: Connection, + domain: string, + records: Record[], + options: GetRecordV2Options = {}, +): Promise<(RecordResult | undefined)[]> { + const pubkeys = records.map((record) => getRecordV2Key(domain, record)); + const retrievedRecords = await SnsRecord.retrieveBatch(connection, pubkeys); + + if (options.deserialize) { + return retrievedRecords.map((e, idx) => { + if (!e) return undefined; + return { + retrievedRecord: e, + record: records[idx], + deserializedContent: deserializeRecordV2Content( + e.getContent(), + records[idx], + ), + }; + }); + } + + return retrievedRecords.map((e, idx) => { + if (!e) return undefined; + return { + retrievedRecord: e, + record: records[idx], + }; + }); +} diff --git a/js/src/record_v2/getRecordV2.ts b/js/src/record_v2/getRecordV2.ts new file mode 100644 index 0000000..9b9e692 --- /dev/null +++ b/js/src/record_v2/getRecordV2.ts @@ -0,0 +1,47 @@ +import { Record } from "../types/record"; +import { Connection } from "@solana/web3.js"; +import { Record as SnsRecord } from "@bonfida/sns-records"; + +import { getRecordV2Key } from "./getRecordV2Key"; +import { deserializeRecordV2Content } from "./deserializeRecordV2Content"; + +interface GetRecordV2Options { + deserialize?: boolean; +} + +interface RecordResult { + retrievedRecord: SnsRecord; + record: Record; + deserializedContent?: string; +} + +type SingleRecordResult = Omit; + +/** + * This function can be used to retrieve a specified record V2 for the given domain name + * @param connection The Solana RPC connection object + * @param domain The .sol domain name + * @param record The record to search for + * @returns + */ +export async function getRecordV2( + connection: Connection, + domain: string, + record: Record, + options: GetRecordV2Options = {}, +): Promise { + const pubkey = getRecordV2Key(domain, record); + const retrievedRecord = await SnsRecord.retrieve(connection, pubkey); + + if (options.deserialize) { + return { + retrievedRecord, + deserializedContent: deserializeRecordV2Content( + retrievedRecord.getContent(), + record, + ), + }; + } + + return { retrievedRecord }; +} diff --git a/js/src/record_v2/getRecordV2Key.ts b/js/src/record_v2/getRecordV2Key.ts new file mode 100644 index 0000000..c4e10fd --- /dev/null +++ b/js/src/record_v2/getRecordV2Key.ts @@ -0,0 +1,18 @@ +import { PublicKey } from "@solana/web3.js"; +import { CENTRAL_STATE_SNS_RECORDS } from "@bonfida/sns-records"; +import { Record } from "../types/record"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +/** + * This function derives a record v2 key + * @param domain The .sol domain name + * @param record The record to derive the key for + * @returns Public key of the record + */ +export const getRecordV2Key = (domain: string, record: Record): PublicKey => { + const { pubkey } = getDomainKeySync(domain); + const hashed = getHashedNameSync(`\x02`.concat(record as string)); + return getNameAccountKeySync(hashed, CENTRAL_STATE_SNS_RECORDS, pubkey); +}; diff --git a/js/src/record_v2/index.ts b/js/src/record_v2/index.ts deleted file mode 100644 index ec82f30..0000000 --- a/js/src/record_v2/index.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { Record } from "../types/record"; -import { ErrorType, SNSError } from "../error"; -import { Connection, PublicKey } from "@solana/web3.js"; -import { encode as encodePunycode, decode as decodePunnycode } from "punycode"; -import { - check, - getDomainKeySync, - getHashedNameSync, - getNameAccountKeySync, -} from "../utils"; -import { bech32 } from "@scure/base"; -import { fromByteArray as ipFromByteArray, parse as parseIp } from "ipaddr.js"; -import { - CENTRAL_STATE_SNS_RECORDS, - Record as SnsRecord, - Validation, -} from "@bonfida/sns-records"; - -/** - * A map that associates each record type with a public key, known as guardians. - */ -export const GUARDIANS = new Map([ - [Record.Url, new PublicKey("ExXjtfdQe8JacoqP9Z535WzQKjF4CzW1TTRKRgpxvya3")], - [Record.CNAME, new PublicKey("ExXjtfdQe8JacoqP9Z535WzQKjF4CzW1TTRKRgpxvya3")], -]); - -/** - * Set of records that utilize secp256k1 for verification purposes - */ -export const ETH_ROA_RECORDS = new Set([ - Record.ETH, - Record.Injective, - Record.BSC, - Record.BASE, -]); - -export const EVM_RECORDS = new Set([ - Record.ETH, - Record.BSC, - Record.BASE, -]); - -/** - * - * This function verifies the right of association of a record. - * Note: This function does not verify if the record is stale. - * Users must verify staleness in addition to the right of association. - * @param {Connection} connection - The Solana RPC connection object - * @param {Record} record - The record to be verified. - * @param {string} domain - The domain associated with the record. - * @param {Buffer} verifier - The optional verifier to be used in the verification process. - * @returns {Promise} - Returns a promise that resolves to a boolean indicating whether the record has the right of association. - */ -export const verifyRightOfAssociation = async ( - connection: Connection, - record: Record, - domain: string, - verifier?: Buffer, -) => { - const recordKey = getRecordV2Key(domain, record); - const recordObj = await SnsRecord.retrieve(connection, recordKey); - - const roaId = recordObj.getRoAId(); - - const validation = ETH_ROA_RECORDS.has(record) - ? Validation.Ethereum - : Validation.Solana; - - verifier = verifier ?? GUARDIANS.get(record)?.toBuffer(); - if (!verifier) throw new SNSError(ErrorType.MissingVerifier); - - return ( - verifier.compare(roaId) === 0 && - recordObj.header.rightOfAssociationValidation === validation - ); -}; - -/** - * Set of records that are UTF-8 encoded strings - */ -export const UTF8_ENCODED = new Set([ - Record.IPFS, - Record.ARWV, - Record.LTC, - Record.DOGE, - Record.Email, - Record.Url, - Record.Discord, - Record.Github, - Record.Reddit, - Record.Twitter, - Record.Telegram, - Record.Pic, - Record.SHDW, - Record.POINT, - Record.Backpack, - Record.TXT, - Record.CNAME, - Record.BTC, -]); - -/** - * Set of records that are self signed i.e signed by the public key contained - * in the record itself. - */ -export const SELF_SIGNED = new Set([ - Record.ETH, - Record.Injective, - Record.SOL, -]); - -/** - * This function deserializes a buffer based on the type of record it corresponds to - * If the record is not properly serialized according to SNS-IP 1 this function will throw an error - * @param content The content to deserialize - * @param record The type of record - * @returns The deserialized content as a string - */ -export const deserializeRecordV2Content = ( - content: Buffer, - record: Record, -): string => { - const utf8Encoded = UTF8_ENCODED.has(record); - - if (utf8Encoded) { - const decoded = content.toString("utf-8"); - if (record === Record.CNAME || record === Record.TXT) { - return decodePunnycode(decoded); - } - return decoded; - } else if (record === Record.SOL) { - return new PublicKey(content).toBase58(); - } else if (EVM_RECORDS.has(record)) { - return "0x" + content.toString("hex"); - } else if (record === Record.Injective) { - return bech32.encode("inj", bech32.toWords(content)); - } else if (record === Record.A || record === Record.AAAA) { - return ipFromByteArray([...content]).toString(); - } else { - throw new SNSError(ErrorType.InvalidARecord); - } -}; - -/** - * This function serializes a string based on the type of record it corresponds to - * The serialization follows the SNS-IP 1 guideline - * @param content The content to serialize - * @param record The type of record - * @returns The serialized content as a buffer - */ -export const serializeRecordV2Content = ( - content: string, - record: Record, -): Buffer => { - const utf8Encoded = UTF8_ENCODED.has(record); - if (utf8Encoded) { - if (record === Record.CNAME || record === Record.TXT) { - content = encodePunycode(content); - } - return Buffer.from(content, "utf-8"); - } else if (record === Record.SOL) { - return new PublicKey(content).toBuffer(); - } else if (EVM_RECORDS.has(record)) { - check(content.slice(0, 2) === "0x", ErrorType.InvalidEvmAddress); - return Buffer.from(content.slice(2), "hex"); - } else if (record === Record.Injective) { - const decoded = bech32.decodeToBytes(content); - check(decoded.prefix === "inj", ErrorType.InvalidInjectiveAddress); - check(decoded.bytes.length === 20, ErrorType.InvalidInjectiveAddress); - return Buffer.from(decoded.bytes); - } else if (record === Record.A) { - const array = parseIp(content).toByteArray(); - check(array.length === 4, ErrorType.InvalidARecord); - return Buffer.from(array); - } else if (record === Record.AAAA) { - const array = parseIp(content).toByteArray(); - check(array.length === 16, ErrorType.InvalidAAAARecord); - return Buffer.from(array); - } else { - throw new SNSError(ErrorType.InvalidARecord); - } -}; - -/** - * This function derives a record v2 key - * @param domain The .sol domain name - * @param record The record to derive the key for - * @returns Public key of the record - */ -export const getRecordV2Key = (domain: string, record: Record): PublicKey => { - const { pubkey } = getDomainKeySync(domain); - const hashed = getHashedNameSync(`\x02`.concat(record as string)); - return getNameAccountKeySync(hashed, CENTRAL_STATE_SNS_RECORDS, pubkey); -}; - -export interface GetRecordV2Options { - deserialize?: boolean; -} - -export interface RecordResult { - retrievedRecord: SnsRecord; - record: Record; - deserializedContent?: string; -} - -export type SingleRecordResult = Omit; - -/** - * This function can be used to retrieve a specified record V2 for the given domain name - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param record The record to search for - * @returns - */ -export async function getRecordV2( - connection: Connection, - domain: string, - record: Record, - options: GetRecordV2Options = {}, -): Promise { - const pubkey = getRecordV2Key(domain, record); - const retrievedRecord = await SnsRecord.retrieve(connection, pubkey); - - if (options.deserialize) { - return { - retrievedRecord, - deserializedContent: deserializeRecordV2Content( - retrievedRecord.getContent(), - record, - ), - }; - } - - return { retrievedRecord }; -} - -/** - * This function can be used to retrieve multiple records V2 for a given domain - * @param connection The Solana RPC connection object - * @param domain The .sol domain name - * @param record The record to search for - * @returns - */ -export async function getMultipleRecordsV2( - connection: Connection, - domain: string, - records: Record[], - options: GetRecordV2Options = {}, -): Promise<(RecordResult | undefined)[]> { - const pubkeys = records.map((record) => getRecordV2Key(domain, record)); - const retrievedRecords = await SnsRecord.retrieveBatch(connection, pubkeys); - - if (options.deserialize) { - return retrievedRecords.map((e, idx) => { - if (!e) return undefined; - return { - retrievedRecord: e, - record: records[idx], - deserializedContent: deserializeRecordV2Content( - e.getContent(), - records[idx], - ), - }; - }); - } - - return retrievedRecords.map((e, idx) => { - if (!e) return undefined; - return { - retrievedRecord: e, - record: records[idx], - }; - }); -} diff --git a/js/src/record_v2/serializeRecordV2Content.ts b/js/src/record_v2/serializeRecordV2Content.ts new file mode 100644 index 0000000..45f9eef --- /dev/null +++ b/js/src/record_v2/serializeRecordV2Content.ts @@ -0,0 +1,72 @@ +import { PublicKey } from "@solana/web3.js"; +import { encode as encodePunycode } from "punycode"; +import { bech32 } from "@scure/base"; +import { parse as parseIp } from "ipaddr.js"; +import { check } from "../utils/check"; +import { Record } from "../types/record"; +import { + InvalidAAAARecordError, + InvalidARecordError, + InvalidEvmAddressError, + InvalidInjectiveAddressError, + InvalidRecordInputError, +} from "../error"; + +import { UTF8_ENCODED, EVM_RECORDS } from "./const"; + +/** + * This function serializes a string based on the type of record it corresponds to + * The serialization follows the SNS-IP 1 guideline + * @param content The content to serialize + * @param record The type of record + * @returns The serialized content as a buffer + */ +export const serializeRecordV2Content = ( + content: string, + record: Record, +): Buffer => { + const utf8Encoded = UTF8_ENCODED.has(record); + if (utf8Encoded) { + if (record === Record.CNAME || record === Record.TXT) { + content = encodePunycode(content); + } + return Buffer.from(content, "utf-8"); + } else if (record === Record.SOL) { + return new PublicKey(content).toBuffer(); + } else if (EVM_RECORDS.has(record)) { + check( + content.slice(0, 2) === "0x", + new InvalidEvmAddressError("The record content must start with `0x`"), + ); + return Buffer.from(content.slice(2), "hex"); + } else if (record === Record.Injective) { + const decoded = bech32.decodeToBytes(content); + check( + decoded.prefix === "inj", + new InvalidInjectiveAddressError( + "The record content must start with `inj", + ), + ); + check( + decoded.bytes.length === 20, + new InvalidInjectiveAddressError(`The record data must be 20 bytes long`), + ); + return Buffer.from(decoded.bytes); + } else if (record === Record.A) { + const array = parseIp(content).toByteArray(); + check( + array.length === 4, + new InvalidARecordError("The record content must be 4 bytes long"), + ); + return Buffer.from(array); + } else if (record === Record.AAAA) { + const array = parseIp(content).toByteArray(); + check( + array.length === 16, + new InvalidAAAARecordError("The record content must be 16 bytes long"), + ); + return Buffer.from(array); + } else { + throw new InvalidRecordInputError("The record content is malformed"); + } +}; diff --git a/js/src/record_v2/utils.ts b/js/src/record_v2/utils.ts index a0f51da..bc9234a 100644 --- a/js/src/record_v2/utils.ts +++ b/js/src/record_v2/utils.ts @@ -1,9 +1,9 @@ import { Connection, PublicKey } from "@solana/web3.js"; import { Record } from "../types/record"; -import { getRecordV2Key } from "."; +import { getRecordV2Key } from "./getRecordV2Key"; import { Record as SnsRecord, Validation } from "@bonfida/sns-records"; import { NameRegistryState } from "../state"; -import { getDomainKeySync } from "../utils"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; /** * This function verifies the staleness of a record. diff --git a/js/src/record_v2/verifyRightOfAssociation.ts b/js/src/record_v2/verifyRightOfAssociation.ts new file mode 100644 index 0000000..79a84d1 --- /dev/null +++ b/js/src/record_v2/verifyRightOfAssociation.ts @@ -0,0 +1,44 @@ +import { Record } from "../types/record"; +import { MissingVerifierError } from "../error"; +import { Connection } from "@solana/web3.js"; +import { Record as SnsRecord, Validation } from "@bonfida/sns-records"; + +import { getRecordV2Key } from "./getRecordV2Key"; +import { ETH_ROA_RECORDS, GUARDIANS } from "./const"; + +/** + * + * This function verifies the right of association of a record. + * Note: This function does not verify if the record is stale. + * Users must verify staleness in addition to the right of association. + * @param {Connection} connection - The Solana RPC connection object + * @param {Record} record - The record to be verified. + * @param {string} domain - The domain associated with the record. + * @param {Buffer} verifier - The optional verifier to be used in the verification process. + * @returns {Promise} - Returns a promise that resolves to a boolean indicating whether the record has the right of association. + */ +export const verifyRightOfAssociation = async ( + connection: Connection, + record: Record, + domain: string, + verifier?: Buffer, +) => { + const recordKey = getRecordV2Key(domain, record); + const recordObj = await SnsRecord.retrieve(connection, recordKey); + + const roaId = recordObj.getRoAId(); + + const validation = ETH_ROA_RECORDS.has(record) + ? Validation.Ethereum + : Validation.Solana; + + verifier = verifier ?? GUARDIANS.get(record)?.toBuffer(); + if (!verifier) { + throw new MissingVerifierError("You must specify the verifier"); + } + + return ( + verifier.compare(roaId) === 0 && + recordObj.header.rightOfAssociationValidation === validation + ); +}; diff --git a/js/src/resolve.ts b/js/src/resolve.ts deleted file mode 100644 index 886f769..0000000 --- a/js/src/resolve.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Connection, PublicKey } from "@solana/web3.js"; -import { getRecordKeySync, getSolRecord, checkSolRecord } from "./record"; -import { getDomainKeySync } from "./utils"; -import { NameRegistryState } from "./state"; -import { Record } from "./types/record"; -import { Buffer } from "buffer"; -import { ErrorType, SNSError } from "./error"; -import { getRecordV2Key } from "./record_v2"; -import { Record as SnsRecord, Validation } from "@bonfida/sns-records"; - -/** - * This function can be used to resolve a domain name to transfer funds - * @param connection The Solana RPC connection object - * @param domain The domain to resolve - * @returns - */ -export const resolve = async (connection: Connection, domain: string) => { - const { pubkey } = getDomainKeySync(domain); - - const { registry, nftOwner } = await NameRegistryState.retrieve( - connection, - pubkey, - ); - - if (nftOwner) { - return nftOwner; - } - - try { - /** - * Handle SOL record V2 - */ - const solV2Owner = await resolveSolRecordV2( - connection, - registry.owner, - domain, - ); - if (solV2Owner !== undefined) { - return solV2Owner; - } - - /** - * Handle SOL record v1 - */ - const solV1Owner = await resolveSolRecordV1( - connection, - registry.owner, - domain, - ); - - return solV1Owner; - } catch (err) { - if (err instanceof Error) { - if (err.name === "FetchError") { - throw err; - } - } - } - - return registry.owner; -}; - -export const resolveSolRecordV1 = async ( - connection: Connection, - owner: PublicKey, - domain: string, -) => { - const recordKey = getRecordKeySync(domain, Record.SOL); - const solRecord = await getSolRecord(connection, domain); - - if (!solRecord?.data) { - throw new SNSError(ErrorType.NoRecordData); - } - - const encoder = new TextEncoder(); - const expectedBuffer = Buffer.concat([ - solRecord.data.slice(0, 32), - recordKey.toBuffer(), - ]); - const expected = encoder.encode(expectedBuffer.toString("hex")); - const valid = checkSolRecord(expected, solRecord.data.slice(32), owner); - - if (!valid) { - throw new SNSError(ErrorType.InvalidSignature); - } - - return new PublicKey(solRecord.data.slice(0, 32)); -}; - -export const resolveSolRecordV2 = async ( - connection: Connection, - owner: PublicKey, - domain: string, -) => { - try { - const recordV2Key = getRecordV2Key(domain, Record.SOL); - const solV2Record = await SnsRecord.retrieve(connection, recordV2Key); - const stalenessId = solV2Record.getStalenessId(); - const roaId = solV2Record.getRoAId(); - const content = solV2Record.getContent(); - - if ( - // The record must signed by the current owner - stalenessId.compare(owner.toBuffer()) === 0 && - solV2Record.header.stalenessValidation === Validation.Solana && - // The record must signed by the destination - roaId.compare(content) === 0 && - solV2Record.header.rightOfAssociationValidation === Validation.Solana - ) { - return new PublicKey(content); - } - } catch (err) { - if (err instanceof Error) { - if (err.name === "FetchError") { - throw err; - } - } - } -}; diff --git a/js/src/resolve/resolve.ts b/js/src/resolve/resolve.ts new file mode 100644 index 0000000..f1786b5 --- /dev/null +++ b/js/src/resolve/resolve.ts @@ -0,0 +1,167 @@ +import { + Connection, + PublicKey, + SIGNATURE_LENGTH_IN_BYTES, +} from "@solana/web3.js"; +import { getDomainKeySync } from "../utils/getDomainKeySync"; +import { NftRecord, Tag } from "../nft/state"; +import { NAME_TOKENIZER_ID } from "../nft/const"; +import { getRecordKeySync } from "../record/getRecordKeySync"; +import { Record } from "../types/record"; +import { getRecordV2Key } from "../record_v2/getRecordV2Key"; +import { Record as RecordV2, Validation } from "@bonfida/sns-records"; +import { + CouldNotFindNftOwner, + DomainDoesNotExist, + InvalidRoAError, + PdaOwnerNotAllowed, + RecordMalformed, + WrongValidation, +} from "../error"; +import { NameRegistryState } from "../state"; +import { checkSolRecord } from "../record/checkSolRecord"; +import { retrieveNftOwnerV2 } from "../nft/retrieveNftOwnerV2"; + +export type AllowPda = "any" | boolean; + +type ResolveConfig = AllowPda extends true + ? { + allowPda: true; + programIds: PublicKey[]; + } + : { + allowPda: AllowPda; + programIds?: PublicKey[]; + }; + +/** + * Resolve function according to SNS-IP 5 + * @param connection + * @param domain + * @param config + * @returns + */ +export const resolve = async ( + connection: Connection, + domain: string, + config: ResolveConfig = { allowPda: false }, +): Promise => { + const { pubkey } = getDomainKeySync(domain); + const [nftRecordKey] = NftRecord.findKeySync(pubkey, NAME_TOKENIZER_ID); + const solRecordV1Key = getRecordKeySync(domain, Record.SOL); + const solRecordV2Key = getRecordV2Key(domain, Record.SOL); + const [nftRecordInfo, solRecordV1Info, solRecordV2Info, registryInfo] = + await connection.getMultipleAccountsInfo([ + nftRecordKey, + solRecordV1Key, + solRecordV2Key, + pubkey, + ]); + + if (!registryInfo?.data) { + throw new DomainDoesNotExist(`Domain ${domain} does not exist`); + } + + const registry = NameRegistryState.deserialize(registryInfo.data); + + // If NFT record active -> NFT owner is the owner + if (nftRecordInfo?.data) { + const nftRecord = NftRecord.deserialize(nftRecordInfo.data); + if (nftRecord.tag === Tag.ActiveRecord) { + const nftOwner = await retrieveNftOwnerV2(connection, pubkey); + if (!nftOwner) { + throw new CouldNotFindNftOwner(); + } + return nftOwner; + } + } + + // Check SOL record V2 + recordV2: if (solRecordV2Info?.data) { + const recordV2 = RecordV2.deserialize(solRecordV2Info.data); + const stalenessId = recordV2.getStalenessId(); + const roaId = recordV2.getRoAId(); + const content = recordV2.getContent(); + + if (content.length !== 32) { + throw new RecordMalformed(`Record is malformed`); + } + + if ( + recordV2.header.rightOfAssociationValidation !== Validation.Solana || + recordV2.header.stalenessValidation !== Validation.Solana + ) { + throw new WrongValidation(); + } + + if (!stalenessId.equals(registry.owner.toBuffer())) { + break recordV2; + } + + if (roaId.equals(content)) { + return new PublicKey(content); + } + + throw new InvalidRoAError( + `The RoA ID shoudl be ${new PublicKey( + content, + ).toBase58()} but is ${new PublicKey(roaId).toBase58()} `, + ); + } + + // Check SOL record V1 + if (solRecordV1Info?.data) { + const encoder = new TextEncoder(); + const expectedBuffer = Buffer.concat([ + solRecordV1Info.data.slice( + NameRegistryState.HEADER_LEN, + NameRegistryState.HEADER_LEN + 32, + ), + solRecordV1Key.toBuffer(), + ]); + + const expected = encoder.encode(expectedBuffer.toString("hex")); + const valid = checkSolRecord( + expected, + solRecordV1Info.data.slice( + NameRegistryState.HEADER_LEN + 32, + NameRegistryState.HEADER_LEN + 32 + SIGNATURE_LENGTH_IN_BYTES, + ), + registry.owner, + ); + + if (valid) { + return new PublicKey( + solRecordV1Info.data.slice( + NameRegistryState.HEADER_LEN, + NameRegistryState.HEADER_LEN + 32, + ), + ); + } + } + + // Check if the registry owner is a PDA + const isOnCurve = PublicKey.isOnCurve(registry.owner); + if (!isOnCurve) { + if (config.allowPda === "any") { + return registry.owner; + } else if (config.allowPda) { + const ownerInfo = await connection.getAccountInfo(registry.owner); + const isAllowed = config.programIds?.some( + (e) => ownerInfo?.owner?.equals(e), + ); + + if (isAllowed) { + return registry.owner; + } + + throw new PdaOwnerNotAllowed( + `The Program ${ownerInfo?.owner.toBase58()} is not allowed`, + ); + } else { + throw new PdaOwnerNotAllowed(); + } + } + + return registry.owner; +}; diff --git a/js/src/resolve/resolveSolRecordV1.ts b/js/src/resolve/resolveSolRecordV1.ts new file mode 100644 index 0000000..2e2f69e --- /dev/null +++ b/js/src/resolve/resolveSolRecordV1.ts @@ -0,0 +1,34 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { getRecordKeySync } from "../record/getRecordKeySync"; +import { getSolRecord } from "../record/helpers/getSolRecord"; +import { checkSolRecord } from "../record/checkSolRecord"; +import { Record } from "../types/record"; +import { Buffer } from "buffer"; +import { InvalidSignatureError, NoRecordDataError } from "../error"; + +export const resolveSolRecordV1 = async ( + connection: Connection, + owner: PublicKey, + domain: string, +) => { + const recordKey = getRecordKeySync(domain, Record.SOL); + const solRecord = await getSolRecord(connection, domain); + + if (!solRecord?.data) { + throw new NoRecordDataError("The SOL record V1 data is empty"); + } + + const encoder = new TextEncoder(); + const expectedBuffer = Buffer.concat([ + solRecord.data.slice(0, 32), + recordKey.toBuffer(), + ]); + const expected = encoder.encode(expectedBuffer.toString("hex")); + const valid = checkSolRecord(expected, solRecord.data.slice(32), owner); + + if (!valid) { + throw new InvalidSignatureError("The SOL record V1 signature is invalid"); + } + + return new PublicKey(solRecord.data.slice(0, 32)); +}; diff --git a/js/src/resolve/resolveSolRecordV2.ts b/js/src/resolve/resolveSolRecordV2.ts new file mode 100644 index 0000000..ec7030b --- /dev/null +++ b/js/src/resolve/resolveSolRecordV2.ts @@ -0,0 +1,35 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { Record } from "../types/record"; +import { getRecordV2Key } from "../record_v2/getRecordV2Key"; +import { Record as SnsRecord, Validation } from "@bonfida/sns-records"; + +export const resolveSolRecordV2 = async ( + connection: Connection, + owner: PublicKey, + domain: string, +) => { + try { + const recordV2Key = getRecordV2Key(domain, Record.SOL); + const solV2Record = await SnsRecord.retrieve(connection, recordV2Key); + const stalenessId = solV2Record.getStalenessId(); + const roaId = solV2Record.getRoAId(); + const content = solV2Record.getContent(); + + if ( + // The record must signed by the current owner + stalenessId.compare(owner.toBuffer()) === 0 && + solV2Record.header.stalenessValidation === Validation.Solana && + // The record must signed by the destination + roaId.compare(content) === 0 && + solV2Record.header.rightOfAssociationValidation === Validation.Solana + ) { + return new PublicKey(content); + } + } catch (err) { + if (err instanceof Error) { + if (err.name === "FetchError") { + throw err; + } + } + } +}; diff --git a/js/src/state.ts b/js/src/state.ts index 502b575..86770b3 100644 --- a/js/src/state.ts +++ b/js/src/state.ts @@ -1,8 +1,8 @@ import { Connection, PublicKey } from "@solana/web3.js"; -import { retrieveNftOwner } from "./nft"; +import { retrieveNftOwnerV2 } from "./nft/retrieveNftOwnerV2"; import { Buffer } from "buffer"; -import { ErrorType, SNSError } from "./error"; import { deserialize } from "borsh"; +import { AccountDoesNotExistError } from "./error"; export class NameRegistryState { static HEADER_LEN = 96; @@ -42,7 +42,7 @@ export class NameRegistryState { ) { const nameAccount = await connection.getAccountInfo(nameAccountKey); if (!nameAccount) { - throw new SNSError(ErrorType.AccountDoesNotExist); + throw new AccountDoesNotExistError(`The name account does not exist`); } const res = new NameRegistryState( @@ -50,7 +50,7 @@ export class NameRegistryState { ); res.data = nameAccount.data?.slice(this.HEADER_LEN); - const nftOwner = await retrieveNftOwner(connection, nameAccountKey); + const nftOwner = await retrieveNftOwnerV2(connection, nameAccountKey); return { registry: res, nftOwner }; } diff --git a/js/src/twitter/ReverseTwitterRegistryState.ts b/js/src/twitter/ReverseTwitterRegistryState.ts new file mode 100644 index 0000000..b5e7dba --- /dev/null +++ b/js/src/twitter/ReverseTwitterRegistryState.ts @@ -0,0 +1,45 @@ +import { deserialize } from "borsh"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { NameRegistryState } from "../state"; +import { InvalidReverseTwitterError } from "../error"; + +export class ReverseTwitterRegistryState { + twitterRegistryKey: Uint8Array; + twitterHandle: string; + + static schema = { + struct: { + twitterRegistryKey: { array: { type: "u8", len: 32 } }, + twitterHandle: "string", + }, + }; + + constructor(obj: { twitterRegistryKey: Uint8Array; twitterHandle: string }) { + this.twitterRegistryKey = obj.twitterRegistryKey; + this.twitterHandle = obj.twitterHandle; + } + + public static async retrieve( + connection: Connection, + reverseTwitterAccountKey: PublicKey, + ): Promise { + let reverseTwitterAccount = await connection.getAccountInfo( + reverseTwitterAccountKey, + "processed", + ); + if (!reverseTwitterAccount) { + throw new InvalidReverseTwitterError( + "The reverse twitter account was not found", + ); + } + + const res = new ReverseTwitterRegistryState( + deserialize( + ReverseTwitterRegistryState.schema, + reverseTwitterAccount.data.slice(NameRegistryState.HEADER_LEN), + ) as any, + ); + + return res; + } +} diff --git a/js/src/twitter/changeTwitterRegistryData.ts b/js/src/twitter/changeTwitterRegistryData.ts new file mode 100644 index 0000000..abf88a4 --- /dev/null +++ b/js/src/twitter/changeTwitterRegistryData.ts @@ -0,0 +1,38 @@ +import { Buffer } from "buffer"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { updateInstruction } from "../instructions/updateInstruction"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { Numberu32 } from "../int"; + +// Overwrite the data that is written in the user facing registry +// Signed by the verified pubkey +export async function changeTwitterRegistryData( + twitterHandle: string, + verifiedPubkey: PublicKey, + offset: number, // The offset at which to write the input data into the NameRegistryData + input_data: Buffer, +): Promise { + const hashedTwitterHandle = getHashedNameSync(twitterHandle); + const twitterHandleRegistryKey = getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + const instructions = [ + updateInstruction( + NAME_PROGRAM_ID, + twitterHandleRegistryKey, + new Numberu32(offset), + input_data, + verifiedPubkey, + ), + ]; + + return instructions; +} diff --git a/js/src/twitter/changeVerifiedPubkey.ts b/js/src/twitter/changeVerifiedPubkey.ts new file mode 100644 index 0000000..6574395 --- /dev/null +++ b/js/src/twitter/changeVerifiedPubkey.ts @@ -0,0 +1,62 @@ +import { Connection, PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { deleteNameRegistry } from "../bindings/deleteNameRegistry"; +import { transferInstruction } from "../instructions/transferInstruction"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { createReverseTwitterRegistry } from "./createReverseTwitterRegistry"; + +// Change the verified pubkey for a given twitter handle +// Signed by the Authority, the verified pubkey and the payer +export async function changeVerifiedPubkey( + connection: Connection, + twitterHandle: string, + currentVerifiedPubkey: PublicKey, + newVerifiedPubkey: PublicKey, + payerKey: PublicKey, +): Promise { + const hashedTwitterHandle = getHashedNameSync(twitterHandle); + const twitterHandleRegistryKey = getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + // Transfer the user-facing registry ownership + let instructions = [ + transferInstruction( + NAME_PROGRAM_ID, + twitterHandleRegistryKey, + newVerifiedPubkey, + currentVerifiedPubkey, + undefined, + ), + ]; + + instructions.push( + await deleteNameRegistry( + connection, + currentVerifiedPubkey.toString(), + payerKey, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ), + ); + + // Create the new reverse registry + instructions = instructions.concat( + await createReverseTwitterRegistry( + connection, + twitterHandle, + twitterHandleRegistryKey, + newVerifiedPubkey, + payerKey, + ), + ); + + return instructions; +} diff --git a/js/src/twitter/createReverseTwitterRegistry.ts b/js/src/twitter/createReverseTwitterRegistry.ts new file mode 100644 index 0000000..50b0b6c --- /dev/null +++ b/js/src/twitter/createReverseTwitterRegistry.ts @@ -0,0 +1,69 @@ +import { serialize } from "borsh"; +import { Buffer } from "buffer"; +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { createInstruction } from "../instructions/createInstruction"; +import { updateInstruction } from "../instructions/updateInstruction"; +import { NameRegistryState } from "../state"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { Numberu32, Numberu64 } from "../int"; +import { ReverseTwitterRegistryState } from "./ReverseTwitterRegistryState"; + +export async function createReverseTwitterRegistry( + connection: Connection, + twitterHandle: string, + twitterRegistryKey: PublicKey, + verifiedPubkey: PublicKey, + payerKey: PublicKey, +): Promise { + // Create the reverse lookup registry + const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); + const reverseRegistryKey = getNameAccountKeySync( + hashedVerifiedPubkey, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + let reverseTwitterRegistryStateBuff = serialize( + ReverseTwitterRegistryState.schema, + new ReverseTwitterRegistryState({ + twitterRegistryKey: twitterRegistryKey.toBytes(), + twitterHandle, + }), + ); + return [ + createInstruction( + NAME_PROGRAM_ID, + SystemProgram.programId, + reverseRegistryKey, + verifiedPubkey, + payerKey, + hashedVerifiedPubkey, + new Numberu64( + await connection.getMinimumBalanceForRentExemption( + reverseTwitterRegistryStateBuff.length + NameRegistryState.HEADER_LEN, + ), + ), + new Numberu32(reverseTwitterRegistryStateBuff.length), + TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries + TWITTER_ROOT_PARENT_REGISTRY_KEY, // Reverse registries are also children of the root + TWITTER_VERIFICATION_AUTHORITY, + ), + updateInstruction( + NAME_PROGRAM_ID, + reverseRegistryKey, + new Numberu32(0), + Buffer.from(reverseTwitterRegistryStateBuff), + TWITTER_VERIFICATION_AUTHORITY, + ), + ]; +} diff --git a/js/src/twitter/createVerifiedTwitterRegistry.ts b/js/src/twitter/createVerifiedTwitterRegistry.ts new file mode 100644 index 0000000..d815a37 --- /dev/null +++ b/js/src/twitter/createVerifiedTwitterRegistry.ts @@ -0,0 +1,67 @@ +import { + Connection, + PublicKey, + SystemProgram, + TransactionInstruction, +} from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { createInstruction } from "../instructions/createInstruction"; +import { NameRegistryState } from "../state"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { Numberu32, Numberu64 } from "../int"; + +import { createReverseTwitterRegistry } from "./createReverseTwitterRegistry"; + +// Signed by the authority, the payer and the verified pubkey +export async function createVerifiedTwitterRegistry( + connection: Connection, + twitterHandle: string, + verifiedPubkey: PublicKey, + space: number, // The space that the user will have to write data into the verified registry + payerKey: PublicKey, +): Promise { + // Create user facing registry + const hashedTwitterHandle = getHashedNameSync(twitterHandle); + const twitterHandleRegistryKey = getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + const lamports = await connection.getMinimumBalanceForRentExemption( + space + NameRegistryState.HEADER_LEN, + ); + + let instructions = [ + createInstruction( + NAME_PROGRAM_ID, + SystemProgram.programId, + twitterHandleRegistryKey, + verifiedPubkey, + payerKey, + hashedTwitterHandle, + new Numberu64(lamports), + new Numberu32(space), + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as owner of the parent for all user-facing registries + ), + ]; + + instructions = instructions.concat( + await createReverseTwitterRegistry( + connection, + twitterHandle, + twitterHandleRegistryKey, + verifiedPubkey, + payerKey, + ), + ); + + return instructions; +} diff --git a/js/src/twitter/deleteTwitterRegistry.ts b/js/src/twitter/deleteTwitterRegistry.ts new file mode 100644 index 0000000..a75d6a8 --- /dev/null +++ b/js/src/twitter/deleteTwitterRegistry.ts @@ -0,0 +1,49 @@ +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { deleteInstruction } from "../instructions/deleteInstruction"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +// Delete the verified registry for a given twitter handle +// Signed by the verified pubkey +export async function deleteTwitterRegistry( + twitterHandle: string, + verifiedPubkey: PublicKey, +): Promise { + const hashedTwitterHandle = getHashedNameSync(twitterHandle); + const twitterHandleRegistryKey = getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); + const reverseRegistryKey = getNameAccountKeySync( + hashedVerifiedPubkey, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + const instructions = [ + // Delete the user facing registry + deleteInstruction( + NAME_PROGRAM_ID, + twitterHandleRegistryKey, + verifiedPubkey, + verifiedPubkey, + ), + // Delete the reverse registry + deleteInstruction( + NAME_PROGRAM_ID, + reverseRegistryKey, + verifiedPubkey, + verifiedPubkey, + ), + ]; + + return instructions; +} diff --git a/js/src/twitter/getHandleAndRegistryKey.ts b/js/src/twitter/getHandleAndRegistryKey.ts new file mode 100644 index 0000000..a67701b --- /dev/null +++ b/js/src/twitter/getHandleAndRegistryKey.ts @@ -0,0 +1,29 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; +import { ReverseTwitterRegistryState } from "./ReverseTwitterRegistryState"; + +export async function getHandleAndRegistryKey( + connection: Connection, + verifiedPubkey: PublicKey, +): Promise<[string, PublicKey]> { + const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); + const reverseRegistryKey = getNameAccountKeySync( + hashedVerifiedPubkey, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + + let reverseRegistryState = await ReverseTwitterRegistryState.retrieve( + connection, + reverseRegistryKey, + ); + return [ + reverseRegistryState.twitterHandle, + new PublicKey(reverseRegistryState.twitterRegistryKey), + ]; +} diff --git a/js/src/twitter/getTwitterHandleandRegistryKeyViaFilters.ts b/js/src/twitter/getTwitterHandleandRegistryKeyViaFilters.ts new file mode 100644 index 0000000..d57a11d --- /dev/null +++ b/js/src/twitter/getTwitterHandleandRegistryKeyViaFilters.ts @@ -0,0 +1,54 @@ +import { deserialize } from "borsh"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_VERIFICATION_AUTHORITY, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { NameRegistryState } from "../state"; +import { AccountDoesNotExistError } from "../error"; + +import { ReverseTwitterRegistryState } from "./ReverseTwitterRegistryState"; + +// Uses the RPC node filtering feature, execution speed may vary +export async function getTwitterHandleandRegistryKeyViaFilters( + connection: Connection, + verifiedPubkey: PublicKey, +): Promise<[string, PublicKey]> { + const filters = [ + { + memcmp: { + offset: 0, + bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(), + }, + }, + { + memcmp: { + offset: 32, + bytes: verifiedPubkey.toBase58(), + }, + }, + { + memcmp: { + offset: 64, + bytes: TWITTER_VERIFICATION_AUTHORITY.toBase58(), + }, + }, + ]; + const filteredAccounts = await connection.getProgramAccounts( + NAME_PROGRAM_ID, + { filters }, + ); + + for (const f of filteredAccounts) { + if (f.account.data.length > NameRegistryState.HEADER_LEN + 32) { + const data = f.account.data.slice(NameRegistryState.HEADER_LEN); + const state = new ReverseTwitterRegistryState( + deserialize(ReverseTwitterRegistryState.schema, data) as any, + ); + return [state.twitterHandle, new PublicKey(state.twitterRegistryKey)]; + } + } + + throw new AccountDoesNotExistError("The twitter account does not exist"); +} diff --git a/js/src/twitter/getTwitterRegistry.ts b/js/src/twitter/getTwitterRegistry.ts new file mode 100644 index 0000000..4468dc7 --- /dev/null +++ b/js/src/twitter/getTwitterRegistry.ts @@ -0,0 +1,22 @@ +import { Connection } from "@solana/web3.js"; +import { TWITTER_ROOT_PARENT_REGISTRY_KEY } from "../constants"; +import { NameRegistryState } from "../state"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +export async function getTwitterRegistry( + connection: Connection, + twitter_handle: string, +): Promise { + const hashedTwitterHandle = getHashedNameSync(twitter_handle); + const twitterHandleRegistryKey = getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); + const { registry } = await NameRegistryState.retrieve( + connection, + twitterHandleRegistryKey, + ); + return registry; +} diff --git a/js/src/twitter/getTwitterRegistryData.ts b/js/src/twitter/getTwitterRegistryData.ts new file mode 100644 index 0000000..1d6a93a --- /dev/null +++ b/js/src/twitter/getTwitterRegistryData.ts @@ -0,0 +1,47 @@ +import { Buffer } from "buffer"; +import { Connection, PublicKey } from "@solana/web3.js"; +import { + NAME_PROGRAM_ID, + TWITTER_ROOT_PARENT_REGISTRY_KEY, +} from "../constants"; +import { NameRegistryState } from "../state"; +import { MultipleRegistriesError } from "../error"; + +// Uses the RPC node filtering feature, execution speed may vary +// Does not give you the handle, but is an alternative to getHandlesAndKeysFromVerifiedPubkey + getTwitterRegistry to get the data +export async function getTwitterRegistryData( + connection: Connection, + verifiedPubkey: PublicKey, +): Promise { + const filters = [ + { + memcmp: { + offset: 0, + bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(), + }, + }, + { + memcmp: { + offset: 32, + bytes: verifiedPubkey.toBase58(), + }, + }, + { + memcmp: { + offset: 64, + bytes: new PublicKey(Buffer.alloc(32, 0)).toBase58(), + }, + }, + ]; + + const filteredAccounts = await connection.getProgramAccounts( + NAME_PROGRAM_ID, + { filters }, + ); + + if (filteredAccounts.length > 1) { + throw new MultipleRegistriesError("More than 1 accounts were found"); + } + + return filteredAccounts[0].account.data.slice(NameRegistryState.HEADER_LEN); +} diff --git a/js/src/twitter/getTwitterRegistryKey.ts b/js/src/twitter/getTwitterRegistryKey.ts new file mode 100644 index 0000000..3ed5869 --- /dev/null +++ b/js/src/twitter/getTwitterRegistryKey.ts @@ -0,0 +1,16 @@ +import { PublicKey } from "@solana/web3.js"; +import { TWITTER_ROOT_PARENT_REGISTRY_KEY } from "../constants"; +import { getHashedNameSync } from "../utils/getHashedNameSync"; +import { getNameAccountKeySync } from "../utils/getNameAccountKeySync"; + +// Returns the key of the user-facing registry +export async function getTwitterRegistryKey( + twitter_handle: string, +): Promise { + const hashedTwitterHandle = getHashedNameSync(twitter_handle); + return getNameAccountKeySync( + hashedTwitterHandle, + undefined, + TWITTER_ROOT_PARENT_REGISTRY_KEY, + ); +} diff --git a/js/src/twitter_bindings.ts b/js/src/twitter_bindings.ts deleted file mode 100644 index 80633d8..0000000 --- a/js/src/twitter_bindings.ts +++ /dev/null @@ -1,420 +0,0 @@ -import { - Connection, - PublicKey, - SystemProgram, - TransactionInstruction, -} from "@solana/web3.js"; -import { - NAME_PROGRAM_ID, - TWITTER_VERIFICATION_AUTHORITY, - TWITTER_ROOT_PARENT_REGISTRY_KEY, -} from "./constants"; -import { deleteNameRegistry } from "./bindings"; -import { - createInstruction, - deleteInstruction, - transferInstruction, - updateInstruction, -} from "./instructions"; -import { NameRegistryState } from "./state"; -import { getHashedNameSync, getNameAccountKeySync } from "./utils"; -import { Numberu32, Numberu64 } from "./int"; -import { deserialize, serialize } from "borsh"; -import { Buffer } from "buffer"; -import { ErrorType, SNSError } from "./error"; - -//////////////////////////////////////////////////// -// Bindings - -// Signed by the authority, the payer and the verified pubkey -export async function createVerifiedTwitterRegistry( - connection: Connection, - twitterHandle: string, - verifiedPubkey: PublicKey, - space: number, // The space that the user will have to write data into the verified registry - payerKey: PublicKey, -): Promise { - // Create user facing registry - const hashedTwitterHandle = getHashedNameSync(twitterHandle); - const twitterHandleRegistryKey = getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - const lamports = await connection.getMinimumBalanceForRentExemption( - space + NameRegistryState.HEADER_LEN, - ); - - let instructions = [ - createInstruction( - NAME_PROGRAM_ID, - SystemProgram.programId, - twitterHandleRegistryKey, - verifiedPubkey, - payerKey, - hashedTwitterHandle, - new Numberu64(lamports), - new Numberu32(space), - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as owner of the parent for all user-facing registries - ), - ]; - - instructions = instructions.concat( - await createReverseTwitterRegistry( - connection, - twitterHandle, - twitterHandleRegistryKey, - verifiedPubkey, - payerKey, - ), - ); - - return instructions; -} - -// Overwrite the data that is written in the user facing registry -// Signed by the verified pubkey -export async function changeTwitterRegistryData( - twitterHandle: string, - verifiedPubkey: PublicKey, - offset: number, // The offset at which to write the input data into the NameRegistryData - input_data: Buffer, -): Promise { - const hashedTwitterHandle = getHashedNameSync(twitterHandle); - const twitterHandleRegistryKey = getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - const instructions = [ - updateInstruction( - NAME_PROGRAM_ID, - twitterHandleRegistryKey, - new Numberu32(offset), - input_data, - verifiedPubkey, - ), - ]; - - return instructions; -} - -// Change the verified pubkey for a given twitter handle -// Signed by the Authority, the verified pubkey and the payer -export async function changeVerifiedPubkey( - connection: Connection, - twitterHandle: string, - currentVerifiedPubkey: PublicKey, - newVerifiedPubkey: PublicKey, - payerKey: PublicKey, -): Promise { - const hashedTwitterHandle = getHashedNameSync(twitterHandle); - const twitterHandleRegistryKey = getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - // Transfer the user-facing registry ownership - let instructions = [ - transferInstruction( - NAME_PROGRAM_ID, - twitterHandleRegistryKey, - newVerifiedPubkey, - currentVerifiedPubkey, - undefined, - ), - ]; - - instructions.push( - await deleteNameRegistry( - connection, - currentVerifiedPubkey.toString(), - payerKey, - TWITTER_VERIFICATION_AUTHORITY, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ), - ); - - // Create the new reverse registry - instructions = instructions.concat( - await createReverseTwitterRegistry( - connection, - twitterHandle, - twitterHandleRegistryKey, - newVerifiedPubkey, - payerKey, - ), - ); - - return instructions; -} - -// Delete the verified registry for a given twitter handle -// Signed by the verified pubkey -export async function deleteTwitterRegistry( - twitterHandle: string, - verifiedPubkey: PublicKey, -): Promise { - const hashedTwitterHandle = getHashedNameSync(twitterHandle); - const twitterHandleRegistryKey = getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); - const reverseRegistryKey = getNameAccountKeySync( - hashedVerifiedPubkey, - TWITTER_VERIFICATION_AUTHORITY, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - const instructions = [ - // Delete the user facing registry - deleteInstruction( - NAME_PROGRAM_ID, - twitterHandleRegistryKey, - verifiedPubkey, - verifiedPubkey, - ), - // Delete the reverse registry - deleteInstruction( - NAME_PROGRAM_ID, - reverseRegistryKey, - verifiedPubkey, - verifiedPubkey, - ), - ]; - - return instructions; -} - -////////////////////////////////////////// -// Getter Functions - -// Returns the key of the user-facing registry -export async function getTwitterRegistryKey( - twitter_handle: string, -): Promise { - const hashedTwitterHandle = getHashedNameSync(twitter_handle); - return getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); -} - -export async function getTwitterRegistry( - connection: Connection, - twitter_handle: string, -): Promise { - const hashedTwitterHandle = getHashedNameSync(twitter_handle); - const twitterHandleRegistryKey = getNameAccountKeySync( - hashedTwitterHandle, - undefined, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - const { registry } = await NameRegistryState.retrieve( - connection, - twitterHandleRegistryKey, - ); - return registry; -} - -export async function getHandleAndRegistryKey( - connection: Connection, - verifiedPubkey: PublicKey, -): Promise<[string, PublicKey]> { - const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); - const reverseRegistryKey = getNameAccountKeySync( - hashedVerifiedPubkey, - TWITTER_VERIFICATION_AUTHORITY, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - - let reverseRegistryState = await ReverseTwitterRegistryState.retrieve( - connection, - reverseRegistryKey, - ); - return [ - reverseRegistryState.twitterHandle, - new PublicKey(reverseRegistryState.twitterRegistryKey), - ]; -} - -// Uses the RPC node filtering feature, execution speed may vary -export async function getTwitterHandleandRegistryKeyViaFilters( - connection: Connection, - verifiedPubkey: PublicKey, -): Promise<[string, PublicKey]> { - const filters = [ - { - memcmp: { - offset: 0, - bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(), - }, - }, - { - memcmp: { - offset: 32, - bytes: verifiedPubkey.toBase58(), - }, - }, - { - memcmp: { - offset: 64, - bytes: TWITTER_VERIFICATION_AUTHORITY.toBase58(), - }, - }, - ]; - const filteredAccounts = await connection.getProgramAccounts( - NAME_PROGRAM_ID, - { filters }, - ); - - for (const f of filteredAccounts) { - if (f.account.data.length > NameRegistryState.HEADER_LEN + 32) { - const data = f.account.data.slice(NameRegistryState.HEADER_LEN); - const state = new ReverseTwitterRegistryState( - deserialize(ReverseTwitterRegistryState.schema, data) as any, - ); - return [state.twitterHandle, new PublicKey(state.twitterRegistryKey)]; - } - } - throw new SNSError(ErrorType.AccountDoesNotExist); -} - -// Uses the RPC node filtering feature, execution speed may vary -// Does not give you the handle, but is an alternative to getHandlesAndKeysFromVerifiedPubkey + getTwitterRegistry to get the data -export async function getTwitterRegistryData( - connection: Connection, - verifiedPubkey: PublicKey, -): Promise { - const filters = [ - { - memcmp: { - offset: 0, - bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBase58(), - }, - }, - { - memcmp: { - offset: 32, - bytes: verifiedPubkey.toBase58(), - }, - }, - { - memcmp: { - offset: 64, - bytes: new PublicKey(Buffer.alloc(32, 0)).toBase58(), - }, - }, - ]; - - const filteredAccounts = await connection.getProgramAccounts( - NAME_PROGRAM_ID, - { filters }, - ); - - if (filteredAccounts.length > 1) { - throw new SNSError(ErrorType.MultipleRegistries); - } - - return filteredAccounts[0].account.data.slice(NameRegistryState.HEADER_LEN); -} - -////////////////////////////////////////////// -// Utils - -export class ReverseTwitterRegistryState { - twitterRegistryKey: Uint8Array; - twitterHandle: string; - - static schema = { - struct: { - twitterRegistryKey: { array: { type: "u8", len: 32 } }, - twitterHandle: "string", - }, - }; - - constructor(obj: { twitterRegistryKey: Uint8Array; twitterHandle: string }) { - this.twitterRegistryKey = obj.twitterRegistryKey; - this.twitterHandle = obj.twitterHandle; - } - - public static async retrieve( - connection: Connection, - reverseTwitterAccountKey: PublicKey, - ): Promise { - let reverseTwitterAccount = await connection.getAccountInfo( - reverseTwitterAccountKey, - "processed", - ); - if (!reverseTwitterAccount) { - throw new SNSError(ErrorType.InvalidReverseTwitter); - } - - const res = new ReverseTwitterRegistryState( - deserialize( - ReverseTwitterRegistryState.schema, - reverseTwitterAccount.data.slice(NameRegistryState.HEADER_LEN), - ) as any, - ); - - return res; - } -} - -export async function createReverseTwitterRegistry( - connection: Connection, - twitterHandle: string, - twitterRegistryKey: PublicKey, - verifiedPubkey: PublicKey, - payerKey: PublicKey, -): Promise { - // Create the reverse lookup registry - const hashedVerifiedPubkey = getHashedNameSync(verifiedPubkey.toString()); - const reverseRegistryKey = getNameAccountKeySync( - hashedVerifiedPubkey, - TWITTER_VERIFICATION_AUTHORITY, - TWITTER_ROOT_PARENT_REGISTRY_KEY, - ); - let reverseTwitterRegistryStateBuff = serialize( - ReverseTwitterRegistryState.schema, - new ReverseTwitterRegistryState({ - twitterRegistryKey: twitterRegistryKey.toBytes(), - twitterHandle, - }), - ); - return [ - createInstruction( - NAME_PROGRAM_ID, - SystemProgram.programId, - reverseRegistryKey, - verifiedPubkey, - payerKey, - hashedVerifiedPubkey, - new Numberu64( - await connection.getMinimumBalanceForRentExemption( - reverseTwitterRegistryStateBuff.length + NameRegistryState.HEADER_LEN, - ), - ), - new Numberu32(reverseTwitterRegistryStateBuff.length), - TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries - TWITTER_ROOT_PARENT_REGISTRY_KEY, // Reverse registries are also children of the root - TWITTER_VERIFICATION_AUTHORITY, - ), - updateInstruction( - NAME_PROGRAM_ID, - reverseRegistryKey, - new Numberu32(0), - Buffer.from(reverseTwitterRegistryStateBuff), - TWITTER_VERIFICATION_AUTHORITY, - ), - ]; -} diff --git a/js/src/types/record.ts b/js/src/types/record.ts index a4324a2..cca5180 100644 --- a/js/src/types/record.ts +++ b/js/src/types/record.ts @@ -28,6 +28,7 @@ export enum Record { TXT = "TXT", Background = "background", BASE = "BASE", + IPNS = "IPNS", } export const RECORD_V1_SIZE: Map = new Map([ diff --git a/js/src/utils.ts b/js/src/utils.ts deleted file mode 100644 index 968a746..0000000 --- a/js/src/utils.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { Connection, PublicKey, MemcmpFilter } from "@solana/web3.js"; -import { sha256 } from "@noble/hashes/sha256"; -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"; -import { ErrorType, SNSError } from "./error"; -import { CENTRAL_STATE_SNS_RECORDS } from "@bonfida/sns-records"; -import { RecordVersion } from "./types/record"; -import { retrieveRecords } from "./nft"; -import splitGraphemes from "graphemesplit"; - -export const getHashedNameSync = (name: string): Buffer => { - const input = HASH_PREFIX + name; - const hashed = sha256(Buffer.from(input, "utf8")); - return Buffer.from(hashed); -}; - -export const getNameAccountKeySync = ( - hashed_name: Buffer, - nameClass?: PublicKey, - nameParent?: PublicKey, -): PublicKey => { - const seeds = [hashed_name]; - if (nameClass) { - seeds.push(nameClass.toBuffer()); - } else { - seeds.push(Buffer.alloc(32)); - } - if (nameParent) { - seeds.push(nameParent.toBuffer()); - } else { - seeds.push(Buffer.alloc(32)); - } - const [nameAccountKey] = PublicKey.findProgramAddressSync( - seeds, - NAME_PROGRAM_ID, - ); - return nameAccountKey; -}; - -/** - * This function can be used to perform a reverse look up - * @param connection The Solana RPC connection - * @param nameAccount The public key of the domain to look up - * @returns The human readable domain name - */ -export async function reverseLookup( - connection: Connection, - nameAccount: PublicKey, -): Promise { - const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); - const reverseLookupAccount = getNameAccountKeySync( - hashedReverseLookup, - REVERSE_LOOKUP_CLASS, - ); - - const { registry } = await NameRegistryState.retrieve( - connection, - reverseLookupAccount, - ); - if (!registry.data) { - throw new SNSError(ErrorType.NoAccountData); - } - - return deserializeReverse(registry.data); -} - -/** - * This function can be used to perform a reverse look up - * @param connection The Solana RPC connection - * @param nameAccount The public keys of the domains to look up - * @returns The human readable domain names - */ -export async function reverseLookupBatch( - connection: Connection, - nameAccounts: PublicKey[], -): Promise<(string | undefined)[]> { - let reverseLookupAccounts: PublicKey[] = []; - for (let nameAccount of nameAccounts) { - const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); - const reverseLookupAccount = getNameAccountKeySync( - hashedReverseLookup, - REVERSE_LOOKUP_CLASS, - ); - reverseLookupAccounts.push(reverseLookupAccount); - } - - let names = await NameRegistryState.retrieveBatch( - connection, - reverseLookupAccounts, - ); - - return names.map((name) => { - if (name === undefined || name.data === undefined) { - return undefined; - } - return deserializeReverse(name.data); - }); -} - -/** - * - * @param connection The Solana RPC connection object - * @param parentKey The parent you want to find sub-domains for - * @returns - */ -export const findSubdomains = async ( - connection: Connection, - parentKey: PublicKey, -): Promise => { - // Fetch reverse accounts - const filtersRevs: MemcmpFilter[] = [ - { - memcmp: { - offset: 0, - bytes: parentKey.toBase58(), - }, - }, - { - memcmp: { - offset: 64, - bytes: REVERSE_LOOKUP_CLASS.toBase58(), - }, - }, - ]; - const reverses = await connection.getProgramAccounts(NAME_PROGRAM_ID, { - filters: filtersRevs, - }); - - const filtersSubs: MemcmpFilter[] = [ - { - memcmp: { - offset: 0, - bytes: parentKey.toBase58(), - }, - }, - ]; - const subs = await connection.getProgramAccounts(NAME_PROGRAM_ID, { - filters: filtersSubs, - dataSlice: { offset: 0, length: 0 }, - }); - - const map = new Map( - reverses.map((e) => [ - e.pubkey.toBase58(), - deserializeReverse(e.account.data.slice(96)), - ]), - ); - - const result: string[] = []; - subs.forEach((e) => { - const revKey = getReverseKeyFromDomainKey(e.pubkey, parentKey).toBase58(); - const rev = map.get(revKey); - if (!!rev) { - result.push(rev.replace("\0", "")); - } - }); - - return result; -}; - -const _deriveSync = ( - name: string, - parent: PublicKey = ROOT_DOMAIN_ACCOUNT, - classKey?: PublicKey, -) => { - let hashed = getHashedNameSync(name); - let pubkey = getNameAccountKeySync(hashed, classKey, parent); - return { pubkey, hashed }; -}; - -/** - * This function can be used to compute the public key of a domain or subdomain - * @param domain The domain to compute the public key for (e.g `bonfida.sol`, `dex.bonfida.sol`) - * @param record Optional parameter: If the domain being resolved is a record - * @returns - */ -export const getDomainKeySync = (domain: string, record?: RecordVersion) => { - if (domain.endsWith(".sol")) { - domain = domain.slice(0, -4); - } - const recordClass = - record === RecordVersion.V2 ? CENTRAL_STATE_SNS_RECORDS : undefined; - const splitted = domain.split("."); - if (splitted.length === 2) { - const prefix = Buffer.from([record ? record : 0]).toString(); - const sub = prefix.concat(splitted[0]); - const { pubkey: parentKey } = _deriveSync(splitted[1]); - const result = _deriveSync(sub, parentKey, recordClass); - return { ...result, isSub: true, parent: parentKey }; - } else if (splitted.length === 3 && !!record) { - // Parent key - const { pubkey: parentKey } = _deriveSync(splitted[2]); - // Sub domain - const { pubkey: subKey } = _deriveSync("\0".concat(splitted[1]), parentKey); - // Sub record - const recordPrefix = record === RecordVersion.V2 ? `\x02` : `\x01`; - const result = _deriveSync( - recordPrefix.concat(splitted[0]), - subKey, - recordClass, - ); - return { ...result, isSub: true, parent: parentKey, isSubRecord: true }; - } else if (splitted.length >= 3) { - throw new SNSError(ErrorType.InvalidInput); - } - const result = _deriveSync(domain, ROOT_DOMAIN_ACCOUNT); - return { ...result, isSub: false, parent: undefined }; -}; - -/** - * This function can be used to retrieve all domain names owned by `wallet` - * @param connection The Solana RPC connection object - * @param wallet The wallet you want to search domain names for - * @returns - */ -export async function getAllDomains( - connection: Connection, - wallet: PublicKey, -): Promise { - const filters = [ - { - memcmp: { - offset: 32, - bytes: wallet.toBase58(), - }, - }, - { - memcmp: { - offset: 0, - bytes: ROOT_DOMAIN_ACCOUNT.toBase58(), - }, - }, - ]; - const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, { - filters, - }); - return accounts.map((a) => a.pubkey); -} - -/** - * This function can be used to retrieve all domain names owned by `wallet` in a human readable format - * @param connection The Solana RPC connection object - * @param wallet The wallet you want to search domain names for - * @returns Array of pubkeys and the corresponding human readable domain names - */ -export async function getDomainKeysWithReverses( - connection: Connection, - wallet: PublicKey, -) { - const encodedNameArr = await getAllDomains(connection, wallet); - const names = await reverseLookupBatch(connection, encodedNameArr); - - return encodedNameArr.map((pubKey, index) => ({ - pubKey, - domain: names[index], - })); -} - -/** - * This function can be used to retrieve all the registered `.sol` domains. - * The account data is sliced to avoid enormous payload and only the owner is returned - * @param connection The Solana RPC connection object - * @returns - */ -export const getAllRegisteredDomains = async (connection: Connection) => { - const filters = [ - { - memcmp: { - offset: 0, - bytes: ROOT_DOMAIN_ACCOUNT.toBase58(), - }, - }, - ]; - const dataSlice = { offset: 32, length: 32 }; - - const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, { - dataSlice, - filters, - }); - return accounts; -}; - -/** - * This function can be used to get the key of the reverse account - * @param domain The domain to compute the reverse for - * @param isSub Whether the domain is a subdomain or not - * @returns The public key of the reverse account - */ -export const getReverseKeySync = (domain: string, isSub?: boolean) => { - const { pubkey, parent } = getDomainKeySync(domain); - const hashedReverseLookup = getHashedNameSync(pubkey.toBase58()); - const reverseLookupAccount = getNameAccountKeySync( - hashedReverseLookup, - REVERSE_LOOKUP_CLASS, - isSub ? parent : undefined, - ); - 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); - } -}; - -/** - * This function can be used to retrieve all the tokenized domains of an owner - * @param connection The Solana RPC connection object - * @param owner The owner of the tokenized domains - * @returns - */ -export const getTokenizedDomains = async ( - connection: Connection, - owner: PublicKey, -) => { - const nftRecords = await retrieveRecords(connection, owner); - - const names = await reverseLookupBatch( - connection, - nftRecords.map((e) => e.nameAccount), - ); - - return names - .map((e, idx) => { - return { - key: nftRecords[idx].nameAccount, - mint: nftRecords[idx].nftMint, - reverse: e, - }; - }) - .filter((e) => !!e.reverse); -}; - -/** - * This function can be used to retrieve the registration cost in USD of a domain - * from its name - * @param name - Domain name - * @returns price - */ -export const getDomainPriceFromName = (name: string) => { - const split = splitGraphemes(name); - - switch (split.length) { - case 1: - return 750; - case 2: - return 700; - case 3: - return 640; - case 4: - return 160; - default: - return 20; - } -}; - -export function deserializeReverse(data: Buffer): string; -export function deserializeReverse(data: undefined): undefined; - -export function deserializeReverse( - data: Buffer | undefined, -): string | undefined { - if (!data) return undefined; - 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/src/utils/check.ts b/js/src/utils/check.ts new file mode 100644 index 0000000..8d5bb0d --- /dev/null +++ b/js/src/utils/check.ts @@ -0,0 +1,7 @@ +import { SNSError } from "../error"; + +export const check = (bool: boolean, error: T) => { + if (!bool) { + throw error; + } +}; diff --git a/js/src/utils/deserializeReverse.ts b/js/src/utils/deserializeReverse.ts new file mode 100644 index 0000000..eab71a9 --- /dev/null +++ b/js/src/utils/deserializeReverse.ts @@ -0,0 +1,15 @@ +import { Buffer } from "buffer"; + +export function deserializeReverse(data: Buffer): string; +export function deserializeReverse(data: undefined): undefined; + +export function deserializeReverse( + data: Buffer | undefined, +): string | undefined { + if (!data) return undefined; + const nameLength = data.slice(0, 4).readUInt32LE(0); + return data + .slice(4, 4 + nameLength) + .toString() + .replace(/\0/g, ""); +} diff --git a/js/src/utils/findSubdomains.ts b/js/src/utils/findSubdomains.ts new file mode 100644 index 0000000..e1213d0 --- /dev/null +++ b/js/src/utils/findSubdomains.ts @@ -0,0 +1,67 @@ +import { Connection, PublicKey, MemcmpFilter } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; +import { REVERSE_LOOKUP_CLASS } from "../constants"; + +import { deserializeReverse } from "./deserializeReverse"; +import { getReverseKeyFromDomainKey } from "./getReverseKeyFromDomainKey"; + +/** + * + * @param connection The Solana RPC connection object + * @param parentKey The parent you want to find sub-domains for + * @returns + */ +export const findSubdomains = async ( + connection: Connection, + parentKey: PublicKey, +): Promise => { + // Fetch reverse accounts + const filtersRevs: MemcmpFilter[] = [ + { + memcmp: { + offset: 0, + bytes: parentKey.toBase58(), + }, + }, + { + memcmp: { + offset: 64, + bytes: REVERSE_LOOKUP_CLASS.toBase58(), + }, + }, + ]; + const reverses = await connection.getProgramAccounts(NAME_PROGRAM_ID, { + filters: filtersRevs, + }); + + const filtersSubs: MemcmpFilter[] = [ + { + memcmp: { + offset: 0, + bytes: parentKey.toBase58(), + }, + }, + ]; + const subs = await connection.getProgramAccounts(NAME_PROGRAM_ID, { + filters: filtersSubs, + dataSlice: { offset: 0, length: 0 }, + }); + + const map = new Map( + reverses.map((e) => [ + e.pubkey.toBase58(), + deserializeReverse(e.account.data.slice(96)), + ]), + ); + + const result: string[] = []; + subs.forEach((e) => { + const revKey = getReverseKeyFromDomainKey(e.pubkey, parentKey).toBase58(); + const rev = map.get(revKey); + if (!!rev) { + result.push(rev.replace("\0", "")); + } + }); + + return result; +}; diff --git a/js/src/utils/getAllDomains.ts b/js/src/utils/getAllDomains.ts new file mode 100644 index 0000000..2bb25e4 --- /dev/null +++ b/js/src/utils/getAllDomains.ts @@ -0,0 +1,34 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID, ROOT_DOMAIN_ACCOUNT } from "../constants"; + +/** + * This function can be used to retrieve all domain names owned by `wallet` + * @param connection The Solana RPC connection object + * @param wallet The wallet you want to search domain names for + * @returns + */ +export async function getAllDomains( + connection: Connection, + wallet: PublicKey, +): Promise { + const filters = [ + { + memcmp: { + offset: 32, + bytes: wallet.toBase58(), + }, + }, + { + memcmp: { + offset: 0, + bytes: ROOT_DOMAIN_ACCOUNT.toBase58(), + }, + }, + ]; + const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, { + filters, + // Only the public keys matter, not the data + dataSlice: { offset: 0, length: 0 }, + }); + return accounts.map((a) => a.pubkey); +} diff --git a/js/src/utils/getAllRegisteredDomains.ts b/js/src/utils/getAllRegisteredDomains.ts new file mode 100644 index 0000000..d5c8ad5 --- /dev/null +++ b/js/src/utils/getAllRegisteredDomains.ts @@ -0,0 +1,26 @@ +import { Connection } from "@solana/web3.js"; +import { NAME_PROGRAM_ID, ROOT_DOMAIN_ACCOUNT } from "../constants"; + +/** + * This function can be used to retrieve all the registered `.sol` domains. + * The account data is sliced to avoid enormous payload and only the owner is returned + * @param connection The Solana RPC connection object + * @returns + */ +export const getAllRegisteredDomains = async (connection: Connection) => { + const filters = [ + { + memcmp: { + offset: 0, + bytes: ROOT_DOMAIN_ACCOUNT.toBase58(), + }, + }, + ]; + const dataSlice = { offset: 32, length: 32 }; + + const accounts = await connection.getProgramAccounts(NAME_PROGRAM_ID, { + dataSlice, + filters, + }); + return accounts; +}; diff --git a/js/src/utils/getDomainKeySync.ts b/js/src/utils/getDomainKeySync.ts new file mode 100644 index 0000000..b63524f --- /dev/null +++ b/js/src/utils/getDomainKeySync.ts @@ -0,0 +1,58 @@ +import { PublicKey } from "@solana/web3.js"; +import { ROOT_DOMAIN_ACCOUNT } from "../constants"; +import { Buffer } from "buffer"; +import { CENTRAL_STATE_SNS_RECORDS } from "@bonfida/sns-records"; +import { RecordVersion } from "../types/record"; +import { InvalidInputError } from "../error"; + +import { getHashedNameSync } from "./getHashedNameSync"; +import { getNameAccountKeySync } from "./getNameAccountKeySync"; + +const _deriveSync = ( + name: string, + parent: PublicKey = ROOT_DOMAIN_ACCOUNT, + classKey?: PublicKey, +) => { + let hashed = getHashedNameSync(name); + let pubkey = getNameAccountKeySync(hashed, classKey, parent); + return { pubkey, hashed }; +}; + +/** + * This function can be used to compute the public key of a domain or subdomain + * @param domain The domain to compute the public key for (e.g `bonfida.sol`, `dex.bonfida.sol`) + * @param record Optional parameter: If the domain being resolved is a record + * @returns + */ +export const getDomainKeySync = (domain: string, record?: RecordVersion) => { + if (domain.endsWith(".sol")) { + domain = domain.slice(0, -4); + } + const recordClass = + record === RecordVersion.V2 ? CENTRAL_STATE_SNS_RECORDS : undefined; + const splitted = domain.split("."); + if (splitted.length === 2) { + const prefix = Buffer.from([record ? record : 0]).toString(); + const sub = prefix.concat(splitted[0]); + const { pubkey: parentKey } = _deriveSync(splitted[1]); + const result = _deriveSync(sub, parentKey, recordClass); + return { ...result, isSub: true, parent: parentKey }; + } else if (splitted.length === 3 && !!record) { + // Parent key + const { pubkey: parentKey } = _deriveSync(splitted[2]); + // Sub domain + const { pubkey: subKey } = _deriveSync("\0".concat(splitted[1]), parentKey); + // Sub record + const recordPrefix = record === RecordVersion.V2 ? `\x02` : `\x01`; + const result = _deriveSync( + recordPrefix.concat(splitted[0]), + subKey, + recordClass, + ); + return { ...result, isSub: true, parent: parentKey, isSubRecord: true }; + } else if (splitted.length >= 3) { + throw new InvalidInputError("The domain is malformed"); + } + const result = _deriveSync(domain, ROOT_DOMAIN_ACCOUNT); + return { ...result, isSub: false, parent: undefined }; +}; diff --git a/js/src/utils/getDomainKeysWithReverses.ts b/js/src/utils/getDomainKeysWithReverses.ts new file mode 100644 index 0000000..78fca63 --- /dev/null +++ b/js/src/utils/getDomainKeysWithReverses.ts @@ -0,0 +1,23 @@ +import { Connection, PublicKey } from "@solana/web3.js"; + +import { reverseLookupBatch } from "./reverseLookupBatch"; +import { getAllDomains } from "./getAllDomains"; + +/** + * This function can be used to retrieve all domain names owned by `wallet` in a human readable format + * @param connection The Solana RPC connection object + * @param wallet The wallet you want to search domain names for + * @returns Array of pubkeys and the corresponding human readable domain names + */ +export async function getDomainKeysWithReverses( + connection: Connection, + wallet: PublicKey, +) { + const encodedNameArr = await getAllDomains(connection, wallet); + const names = await reverseLookupBatch(connection, encodedNameArr); + + return encodedNameArr.map((pubKey, index) => ({ + pubKey, + domain: names[index], + })); +} diff --git a/js/src/utils/getDomainPriceFromName.ts b/js/src/utils/getDomainPriceFromName.ts new file mode 100644 index 0000000..b4f695c --- /dev/null +++ b/js/src/utils/getDomainPriceFromName.ts @@ -0,0 +1,24 @@ +import splitGraphemes from "graphemesplit"; + +/** + * This function can be used to retrieve the registration cost in USD of a domain + * from its name + * @param name - Domain name + * @returns price + */ +export const getDomainPriceFromName = (name: string) => { + const split = splitGraphemes(name); + + switch (split.length) { + case 1: + return 750; + case 2: + return 700; + case 3: + return 640; + case 4: + return 160; + default: + return 20; + } +}; diff --git a/js/src/utils/getHashedNameSync.ts b/js/src/utils/getHashedNameSync.ts new file mode 100644 index 0000000..35764e5 --- /dev/null +++ b/js/src/utils/getHashedNameSync.ts @@ -0,0 +1,9 @@ +import { Buffer } from "buffer"; +import { sha256 } from "@noble/hashes/sha256"; +import { HASH_PREFIX } from "../constants"; + +export const getHashedNameSync = (name: string): Buffer => { + const input = HASH_PREFIX + name; + const hashed = sha256(Buffer.from(input, "utf8")); + return Buffer.from(hashed); +}; diff --git a/js/src/utils/getNameAccountKeySync.ts b/js/src/utils/getNameAccountKeySync.ts new file mode 100644 index 0000000..3f3d4c1 --- /dev/null +++ b/js/src/utils/getNameAccountKeySync.ts @@ -0,0 +1,26 @@ +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import { NAME_PROGRAM_ID } from "../constants"; + +export const getNameAccountKeySync = ( + hashed_name: Buffer, + nameClass?: PublicKey, + nameParent?: PublicKey, +): PublicKey => { + const seeds = [hashed_name]; + if (nameClass) { + seeds.push(nameClass.toBuffer()); + } else { + seeds.push(Buffer.alloc(32)); + } + if (nameParent) { + seeds.push(nameParent.toBuffer()); + } else { + seeds.push(Buffer.alloc(32)); + } + const [nameAccountKey] = PublicKey.findProgramAddressSync( + seeds, + NAME_PROGRAM_ID, + ); + return nameAccountKey; +}; diff --git a/js/src/utils/getPythFeedAccountKey.ts b/js/src/utils/getPythFeedAccountKey.ts new file mode 100644 index 0000000..f544ae5 --- /dev/null +++ b/js/src/utils/getPythFeedAccountKey.ts @@ -0,0 +1,12 @@ +import { Buffer } from "buffer"; +import { PublicKey } from "@solana/web3.js"; +import { DEFAULT_PYTH_PUSH_PROGRAM } from "../constants"; + +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/src/utils/getReverseKeyFromDomainKey.ts b/js/src/utils/getReverseKeyFromDomainKey.ts new file mode 100644 index 0000000..68fda1b --- /dev/null +++ b/js/src/utils/getReverseKeyFromDomainKey.ts @@ -0,0 +1,24 @@ +import { PublicKey } from "@solana/web3.js"; +import { REVERSE_LOOKUP_CLASS } from "../constants"; + +import { getHashedNameSync } from "./getHashedNameSync"; +import { getNameAccountKeySync } from "./getNameAccountKeySync"; + +/** + * 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; +}; diff --git a/js/src/utils/getReverseKeySync.ts b/js/src/utils/getReverseKeySync.ts new file mode 100644 index 0000000..c26222f --- /dev/null +++ b/js/src/utils/getReverseKeySync.ts @@ -0,0 +1,22 @@ +import { REVERSE_LOOKUP_CLASS } from "../constants"; + +import { getHashedNameSync } from "./getHashedNameSync"; +import { getNameAccountKeySync } from "./getNameAccountKeySync"; +import { getDomainKeySync } from "./getDomainKeySync"; + +/** + * This function can be used to get the key of the reverse account + * @param domain The domain to compute the reverse for + * @param isSub Whether the domain is a subdomain or not + * @returns The public key of the reverse account + */ +export const getReverseKeySync = (domain: string, isSub?: boolean) => { + const { pubkey, parent } = getDomainKeySync(domain); + const hashedReverseLookup = getHashedNameSync(pubkey.toBase58()); + const reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + REVERSE_LOOKUP_CLASS, + isSub ? parent : undefined, + ); + return reverseLookupAccount; +}; diff --git a/js/src/utils/getTokenizedDomains.ts b/js/src/utils/getTokenizedDomains.ts new file mode 100644 index 0000000..8371600 --- /dev/null +++ b/js/src/utils/getTokenizedDomains.ts @@ -0,0 +1,31 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { retrieveRecords } from "../nft/retrieveRecords"; +import { reverseLookupBatch } from "./reverseLookupBatch"; + +/** + * This function can be used to retrieve all the tokenized domains of an owner + * @param connection The Solana RPC connection object + * @param owner The owner of the tokenized domains + * @returns + */ +export const getTokenizedDomains = async ( + connection: Connection, + owner: PublicKey, +) => { + const nftRecords = await retrieveRecords(connection, owner); + + const names = await reverseLookupBatch( + connection, + nftRecords.map((e) => e.nameAccount), + ); + + return names + .map((e, idx) => { + return { + key: nftRecords[idx].nameAccount, + mint: nftRecords[idx].nftMint, + reverse: e, + }; + }) + .filter((e) => !!e.reverse); +}; diff --git a/js/src/utils/reverseLookup.ts b/js/src/utils/reverseLookup.ts new file mode 100644 index 0000000..7d7ab96 --- /dev/null +++ b/js/src/utils/reverseLookup.ts @@ -0,0 +1,26 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { NameRegistryState } from "../state"; +import { NoAccountDataError } from "../error"; +import { deserializeReverse } from "./deserializeReverse"; +import { getReverseKeyFromDomainKey } from "./getReverseKeyFromDomainKey"; + +/** + * This function can be used to perform a reverse look up + * @param connection The Solana RPC connection + * @param nameAccount The public key of the domain to look up + * @returns The human readable domain name + */ +export async function reverseLookup( + connection: Connection, + nameAccount: PublicKey, + parent?: PublicKey, +): Promise { + const reverseKey = getReverseKeyFromDomainKey(nameAccount, parent); + + const { registry } = await NameRegistryState.retrieve(connection, reverseKey); + if (!registry.data) { + throw new NoAccountDataError("The registry data is empty"); + } + + return deserializeReverse(registry.data); +} diff --git a/js/src/utils/reverseLookupBatch.ts b/js/src/utils/reverseLookupBatch.ts new file mode 100644 index 0000000..20e6049 --- /dev/null +++ b/js/src/utils/reverseLookupBatch.ts @@ -0,0 +1,40 @@ +import { Connection, PublicKey } from "@solana/web3.js"; +import { NameRegistryState } from "../state"; +import { REVERSE_LOOKUP_CLASS } from "../constants"; + +import { getHashedNameSync } from "./getHashedNameSync"; +import { getNameAccountKeySync } from "./getNameAccountKeySync"; +import { deserializeReverse } from "./deserializeReverse"; + +/** + * This function can be used to perform a reverse look up + * @param connection The Solana RPC connection + * @param nameAccount The public keys of the domains to look up + * @returns The human readable domain names + */ +export async function reverseLookupBatch( + connection: Connection, + nameAccounts: PublicKey[], +): Promise<(string | undefined)[]> { + let reverseLookupAccounts: PublicKey[] = []; + for (let nameAccount of nameAccounts) { + const hashedReverseLookup = getHashedNameSync(nameAccount.toBase58()); + const reverseLookupAccount = getNameAccountKeySync( + hashedReverseLookup, + REVERSE_LOOKUP_CLASS, + ); + reverseLookupAccounts.push(reverseLookupAccount); + } + + let names = await NameRegistryState.retrieveBatch( + connection, + reverseLookupAccounts, + ); + + return names.map((name) => { + if (name === undefined || name.data === undefined) { + return undefined; + } + return deserializeReverse(name.data); + }); +} diff --git a/js/tests/all-registered.test.ts b/js/tests/all-registered.test.ts index b06d3eb..09db080 100644 --- a/js/tests/all-registered.test.ts +++ b/js/tests/all-registered.test.ts @@ -1,7 +1,7 @@ require("dotenv").config(); -import { test, expect, jest } from "@jest/globals"; -import { getAllRegisteredDomains } from "../src/utils"; import { Connection } from "@solana/web3.js"; +import { test, expect, jest } from "@jest/globals"; +import { getAllRegisteredDomains } from "../src/utils/getAllRegisteredDomains"; jest.setTimeout(4 * 60_000); diff --git a/js/tests/burn.test.ts b/js/tests/burn.test.ts index 7ac9000..0116b3f 100644 --- a/js/tests/burn.test.ts +++ b/js/tests/burn.test.ts @@ -1,7 +1,7 @@ require("dotenv").config(); import { test, jest } from "@jest/globals"; import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { burnDomain } from "../src/bindings"; +import { burnDomain } from "../src/bindings/burnDomain"; jest.setTimeout(20_000); diff --git a/js/tests/derivation.test.ts b/js/tests/derivation.test.ts index b04e217..d8beb99 100644 --- a/js/tests/derivation.test.ts +++ b/js/tests/derivation.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "@jest/globals"; import { getDomainKey } from "../src/deprecated/utils"; -import { getDomainKeySync } from "../src/utils"; +import { getDomainKeySync } from "../src/utils/getDomainKeySync"; const items = [ { @@ -27,6 +27,6 @@ test("Derivation", async () => { expect(pubkey.toBase58()).toBe(item.address); } items.forEach((e) => - expect(getDomainKeySync(e.domain).pubkey.toBase58()).toBe(e.address) + expect(getDomainKeySync(e.domain).pubkey.toBase58()).toBe(e.address), ); }); diff --git a/js/tests/devnet.test.ts b/js/tests/devnet.test.ts index dc5156f..15ede4c 100644 --- a/js/tests/devnet.test.ts +++ b/js/tests/devnet.test.ts @@ -50,7 +50,6 @@ test("Registration V2", async () => { tx.recentBlockhash = blockhash; tx.feePayer = OWNER2; const res = await connection.simulateTransaction(tx); - console.log(res.value.logs); expect(res.value.err).toBe(null); }); diff --git a/js/tests/favorite.test.ts b/js/tests/favorite.test.ts index 5c0f785..f34b630 100644 --- a/js/tests/favorite.test.ts +++ b/js/tests/favorite.test.ts @@ -5,8 +5,8 @@ import { getMultipleFavoriteDomains, } from "../src/favorite-domain"; import { PublicKey, Connection, Keypair, Transaction } from "@solana/web3.js"; -import { registerFavorite } from "../src/bindings"; -import { getDomainKeySync } from "../src/utils"; +import { registerFavorite } from "../src/bindings/registerFavorite"; +import { getDomainKeySync } from "../src/utils/getDomainKeySync"; jest.setTimeout(10_000); @@ -30,6 +30,14 @@ test("Favorite domain", async () => { stale: false, }, }, + { + user: new PublicKey("A41TAGFpQkFpJidLwH37ydunE7Q3jpBaS228RkoXiRQk"), + favorite: { + domain: new PublicKey("BaQq8Uib3Aw5SPBedC8MdYCvpfEC9iLkUMHc5M74sAjv"), + reverse: "1.00728", + stale: false, + }, + }, ]; for (let item of items) { const fav = await getFavoriteDomain(connection, item.user); @@ -59,6 +67,10 @@ test("Multiple favorite domains", async () => { wallet: new PublicKey("36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4"), domain: "fav-tokenized", }, + { + wallet: new PublicKey("A41TAGFpQkFpJidLwH37ydunE7Q3jpBaS228RkoXiRQk"), + domain: "1.00728", + }, ]; const result = await getMultipleFavoriteDomains( connection, @@ -70,8 +82,12 @@ test("Multiple favorite domains", async () => { test("Register fav", async () => { const owner = new PublicKey("Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8"); const tx = new Transaction(); - const ix = registerFavorite(getDomainKeySync("wallet-guide-3").pubkey, owner); - tx.add(...ix); + const ix = await registerFavorite( + connection, + getDomainKeySync("wallet-guide-3").pubkey, + owner, + ); + tx.add(ix); const { blockhash } = await connection.getLatestBlockhash(); tx.recentBlockhash = blockhash; tx.feePayer = owner; diff --git a/js/tests/get-domain-price-from-name.test.ts b/js/tests/get-domain-price-from-name.test.ts index 9b5ca38..5577538 100644 --- a/js/tests/get-domain-price-from-name.test.ts +++ b/js/tests/get-domain-price-from-name.test.ts @@ -1,30 +1,30 @@ require("dotenv").config(); import { describe, test, expect } from "@jest/globals"; -import { getDomainPriceFromName } from "../src/utils"; +import { getDomainPriceFromName } from "../src/utils/getDomainPriceFromName"; describe("getDomainPriceFromName", () => { test.each([ - ['1', 750], - ['βœ…', 750], - ['μš”', 750], - ['πŸ‘©β€πŸ‘©β€πŸ‘§', 750], + ["1", 750], + ["βœ…", 750], + ["μš”", 750], + ["πŸ‘©β€πŸ‘©β€πŸ‘§", 750], - ['10', 700], - ['1βœ…', 700], - ['πŸ‘©β€πŸ‘©β€πŸ‘§βœ…', 700], - ['독도', 700], + ["10", 700], + ["1βœ…", 700], + ["πŸ‘©β€πŸ‘©β€πŸ‘§βœ…", 700], + ["독도", 700], - ['100', 640], - ['10βœ…', 640], - ['1독도', 640], + ["100", 640], + ["10βœ…", 640], + ["1독도", 640], - ['1000', 160], - ['100βœ…', 160], + ["1000", 160], + ["100βœ…", 160], - ['10000', 20], - ['1000βœ…', 20], - ['fΓͺtes', 20], - ])('value %s to be %s', (value, expected) => { + ["10000", 20], + ["1000βœ…", 20], + ["fΓͺtes", 20], + ])("value %s to be %s", (value, expected) => { expect(getDomainPriceFromName(value)).toBe(expected); - }) + }); }); diff --git a/js/tests/get-domains-reversed.test.ts b/js/tests/get-domains-reversed.test.ts index a8c45b0..aacd9ca 100644 --- a/js/tests/get-domains-reversed.test.ts +++ b/js/tests/get-domains-reversed.test.ts @@ -1,6 +1,6 @@ require("dotenv").config(); import { test, expect, jest } from "@jest/globals"; -import { getDomainKeysWithReverses } from "../src/utils"; +import { getDomainKeysWithReverses } from "../src/utils/getDomainKeysWithReverses"; import { PublicKey, Connection } from "@solana/web3.js"; jest.setTimeout(10_000); diff --git a/js/tests/get-domains.test.ts b/js/tests/get-domains.test.ts index 77da791..a626b7f 100644 --- a/js/tests/get-domains.test.ts +++ b/js/tests/get-domains.test.ts @@ -1,7 +1,7 @@ require("dotenv").config(); -import { test, expect, jest } from "@jest/globals"; -import { getAllDomains } from "../src/utils"; import { PublicKey, Connection } from "@solana/web3.js"; +import { test, expect, jest } from "@jest/globals"; +import { getAllDomains } from "../src/utils/getAllDomains"; jest.setTimeout(10_000); @@ -24,7 +24,7 @@ const connection = new Connection(process.env.RPC_URL!); test("Get domains", async () => { for (let item of items) { const domains = (await getAllDomains(connection, item.user)).map((e) => - e.toBase58() + e.toBase58(), ); domains.sort(); expect(domains).toEqual(item.domain); diff --git a/js/tests/nft.test.ts b/js/tests/nft.test.ts index 5028c9f..54eee60 100644 --- a/js/tests/nft.test.ts +++ b/js/tests/nft.test.ts @@ -1,8 +1,9 @@ require("dotenv").config(); import { test, jest } from "@jest/globals"; import { PublicKey, Connection } from "@solana/web3.js"; -import { getDomainKeySync, getTokenizedDomains } from "../src/utils"; -import { getDomainMint } from "../src/nft/name-tokenizer"; +import { getDomainKeySync } from "../src/utils/getDomainKeySync"; +import { getTokenizedDomains } from "../src/utils/getTokenizedDomains"; +import { getDomainMint } from "../src/nft/getDomainMint"; jest.setTimeout(10_000); const connection = new Connection(process.env.RPC_URL!); @@ -29,7 +30,7 @@ test("Get tokenized domains", async () => { const domains = ( await getTokenizedDomains( connection, - new PublicKey("Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8") + new PublicKey("Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8"), ) ).map((e) => { return { diff --git a/js/tests/pyth.test.ts b/js/tests/pyth.test.ts index 762cf89..281352a 100644 --- a/js/tests/pyth.test.ts +++ b/js/tests/pyth.test.ts @@ -5,7 +5,7 @@ import { } from "@pythnetwork/client"; import { Connection } from "@solana/web3.js"; import { PYTH_FEEDS, PYTH_PULL_FEEDS, TOKENS_SYM_MINT } from "../src/constants"; -import { getPythFeedAccountKey } from "../src/utils"; +import { getPythFeedAccountKey } from "../src/utils/getPythFeedAccountKey"; const connection = new Connection(process.env.RPC_URL!); diff --git a/js/tests/records-v2.test.ts b/js/tests/records-v2.test.ts index 5217eae..0e337fe 100644 --- a/js/tests/records-v2.test.ts +++ b/js/tests/records-v2.test.ts @@ -1,22 +1,18 @@ require("dotenv").config(); import { test, expect } from "@jest/globals"; -import { - deserializeRecordV2Content, - getMultipleRecordsV2, - getRecordV2, - getRecordV2Key, - serializeRecordV2Content, -} from "../src/record_v2"; +import { deserializeRecordV2Content } from "../src/record_v2/deserializeRecordV2Content"; +import { getMultipleRecordsV2 } from "../src/record_v2/getMultipleRecordsV2"; +import { getRecordV2 } from "../src/record_v2/getRecordV2"; +import { getRecordV2Key } from "../src/record_v2/getRecordV2Key"; +import { serializeRecordV2Content } from "../src/record_v2/serializeRecordV2Content"; import { Record } from "../src/types/record"; import { Keypair, Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { - createRecordV2Instruction, - deleteRecordV2, - ethValidateRecordV2Content, - updateRecordV2Instruction, - validateRecordV2Content, - writRoaRecordV2, -} from "../src/bindings"; +import { createRecordV2Instruction } from "../src/bindings/createRecordV2Instruction"; +import { deleteRecordV2 } from "../src/bindings/deleteRecordV2"; +import { ethValidateRecordV2Content } from "../src/bindings/ethValidateRecordV2Content"; +import { updateRecordV2Instruction } from "../src/bindings/updateRecordV2Instruction"; +import { validateRecordV2Content } from "../src/bindings/validateRecordV2Content"; +import { writRoaRecordV2 } from "../src/bindings/writRoaRecordV2"; jest.setTimeout(50_000); @@ -62,6 +58,10 @@ test("Records V2 des/ser", () => { content: "username", record: Record.Discord, }, + { + content: "k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8", + record: Record.IPNS, + }, ]; items.forEach((e) => { diff --git a/js/tests/records.test.ts b/js/tests/records.test.ts index 564dc28..bcf28b6 100644 --- a/js/tests/records.test.ts +++ b/js/tests/records.test.ts @@ -1,12 +1,30 @@ require("dotenv").config(); import { test, jest, expect } from "@jest/globals"; -import * as record from "../src/record"; -import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { getIpfsRecord } from "../src/record/helpers/getIpfsRecord"; +import { getArweaveRecord } from "../src/record/helpers/getArweaveRecord"; +import { getEthRecord } from "../src/record/helpers/getEthRecord"; +import { getBtcRecord } from "../src/record/helpers/getBtcRecord"; +import { getLtcRecord } from "../src/record/helpers/getLtcRecord"; +import { getDogeRecord } from "../src/record/helpers/getDogeRecord"; +import { getEmailRecord } from "../src/record/helpers/getEmailRecord"; +import { getUrlRecord } from "../src/record/helpers/getUrlRecord"; +import { getDiscordRecord } from "../src/record/helpers/getDiscordRecord"; +import { getGithubRecord } from "../src/record/helpers/getGithubRecord"; +import { getRedditRecord } from "../src/record/helpers/getRedditRecord"; +import { getTwitterRecord } from "../src/record/helpers/getTwitterRecord"; +import { getTelegramRecord } from "../src/record/helpers/getTelegramRecord"; +import { getBscRecord } from "../src/record/helpers/getBscRecord"; + +import { getRecords } from "../src/record/getRecords"; +import { serializeRecord } from "../src/record/serializeRecord"; +import { deserializeRecord } from "../src/record/deserializeRecord"; + +import { Connection, PublicKey, Transaction } from "@solana/web3.js"; import { Record } from "../src/types/record"; -import { createRecordInstruction } from "../src/bindings"; -import { resolveSolRecordV1 } from "../src/resolve"; +import { createRecordInstruction } from "../src/bindings/createRecordInstruction"; +import { resolveSolRecordV1 } from "../src/resolve/resolveSolRecordV1"; import { NameRegistryState } from "../src/state"; -import { getRecordKeySync } from "../src/record"; +import { getRecordKeySync } from "../src/record/getRecordKeySync"; jest.setTimeout(20_000); @@ -14,67 +32,59 @@ const connection = new Connection(process.env.RPC_URL!); test("Records", async () => { const domain = "🍍"; - record.getIpfsRecord(connection, domain).then((e) => { + getIpfsRecord(connection, domain).then((e) => { expect(e).toBe("QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"); }); - record - .getArweaveRecord(connection, domain) - .then((e) => expect(e).toBe("some-arweave-hash")); + getArweaveRecord(connection, domain).then((e) => + expect(e).toBe("some-arweave-hash"), + ); - record - .getEthRecord(connection, domain) - .then((e) => expect(e).toBe("0x570eDC13f9D406a2b4E6477Ddf75D5E9cCF51cd6")); + getEthRecord(connection, domain).then((e) => + expect(e).toBe("0x570eDC13f9D406a2b4E6477Ddf75D5E9cCF51cd6"), + ); - record - .getBtcRecord(connection, domain) - .then((e) => expect(e).toBe("3JfBcjv7TbYN9yQsyfcNeHGLcRjgoHhV3z")); + getBtcRecord(connection, domain).then((e) => + expect(e).toBe("3JfBcjv7TbYN9yQsyfcNeHGLcRjgoHhV3z"), + ); - record - .getLtcRecord(connection, domain) - .then((e) => expect(e).toBe("MK6deR3Mi6dUsim9M3GPDG2xfSeSAgSrpQ")); + getLtcRecord(connection, domain).then((e) => + expect(e).toBe("MK6deR3Mi6dUsim9M3GPDG2xfSeSAgSrpQ"), + ); - record - .getDogeRecord(connection, domain) - .then((e) => expect(e).toBe("DC79kjg58VfDZeMj9cWNqGuDfYfGJg9DjZ")); + getDogeRecord(connection, domain).then((e) => + expect(e).toBe("DC79kjg58VfDZeMj9cWNqGuDfYfGJg9DjZ"), + ); - record - .getEmailRecord(connection, domain) - .then((e) => expect(e).toBe("🍍@gmail.com")); + getEmailRecord(connection, domain).then((e) => + expect(e).toBe("🍍@gmail.com"), + ); - record.getUrlRecord(connection, domain).then((e) => expect(e).toBe("🍍.io")); + getUrlRecord(connection, domain).then((e) => expect(e).toBe("🍍.io")); - record - .getDiscordRecord(connection, domain) - .then((e) => expect(e).toBe("@🍍#7493")); + getDiscordRecord(connection, domain).then((e) => expect(e).toBe("@🍍#7493")); - record - .getGithubRecord(connection, domain) - .then((e) => expect(expect(e).toBe("@🍍_dev"))); + getGithubRecord(connection, domain).then((e) => + expect(expect(e).toBe("@🍍_dev")), + ); - record - .getRedditRecord(connection, domain) - .then((e) => expect(e).toBe("@reddit-🍍")); + getRedditRecord(connection, domain).then((e) => expect(e).toBe("@reddit-🍍")); - record - .getTwitterRecord(connection, domain) - .then((e) => expect(e).toBe("@🍍")); + getTwitterRecord(connection, domain).then((e) => expect(e).toBe("@🍍")); - return record - .getTelegramRecord(connection, domain) - .then((e) => expect(e).toBe("@🍍-tg")); + return getTelegramRecord(connection, domain).then((e) => + expect(e).toBe("@🍍-tg"), + ); }); const sub = "test.πŸ‡ΊπŸ‡Έ.sol"; test("Sub records", async () => { - record - .getEmailRecord(connection, sub) - .then((e) => expect(e).toBe("test@test.com")); + getEmailRecord(connection, sub).then((e) => expect(e).toBe("test@test.com")); }); test("Get multiple records", async () => { - const records = await record.getRecords( + const records = await getRecords( connection, "🍍", [Record.Telegram, Record.Github, Record.Backpack], @@ -86,7 +96,7 @@ test("Get multiple records", async () => { }); test("BSC", async () => { - const res = await record.getBscRecord(connection, "aanda.sol"); + const res = await getBscRecord(connection, "aanda.sol"); expect(res).toBe("0x4170ad697176fe6d660763f6e4dfcf25018e8b63"); }); @@ -152,14 +162,14 @@ test("Des/ser", () => { ]; items.forEach((e) => { - const ser = record.serializeRecord(e.content, e.record); + const ser = serializeRecord(e.content, e.record); const registry: NameRegistryState = { data: ser, parentName: PublicKey.default, class: PublicKey.default, owner: PublicKey.default, }; - const des = record.deserializeRecord(registry, e.record, PublicKey.default); + const des = deserializeRecord(registry, e.record, PublicKey.default); expect(des).toBe(e.content); }); }); diff --git a/js/tests/registration.test.ts b/js/tests/registration.test.ts index 64dd57a..c915009 100644 --- a/js/tests/registration.test.ts +++ b/js/tests/registration.test.ts @@ -1,15 +1,14 @@ require("dotenv").config(); import { test, jest } from "@jest/globals"; import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { - registerDomainName, - registerDomainNameV2, - registerWithNft, -} from "../src/bindings"; +import { registerDomainName } from "../src/bindings/registerDomainName"; +import { registerDomainNameV2 } from "../src/bindings/registerDomainNameV2"; +import { registerWithNft } from "../src/bindings/registerWithNft"; import { randomBytes } from "crypto"; import { REFERRERS, USDC_MINT } from "../src/constants"; import { getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { getDomainKeySync, getReverseKeySync } from "../src/utils"; +import { getDomainKeySync } from "../src/utils/getDomainKeySync"; +import { getReverseKeySync } from "../src/utils/getReverseKeySync"; import { Metaplex } from "@metaplex-foundation/js"; jest.setTimeout(20_000); @@ -24,7 +23,7 @@ const VAULT_OWNER = new PublicKey( test("Registration", async () => { const tx = new Transaction(); - const [, ix] = await registerDomainName( + const ix = await registerDomainName( connection, randomBytes(10).toString("hex"), 1_000, @@ -42,7 +41,7 @@ test("Registration", async () => { test("Registration with ref", async () => { const tx = new Transaction(); - const [, ix] = await registerDomainName( + const ix = await registerDomainName( connection, randomBytes(10).toString("hex"), 1_000, @@ -94,7 +93,7 @@ test("Register with NFT", async () => { test("Indempotent ATA creation ref", async () => { const tx = new Transaction(); for (let i = 0; i < 3; i++) { - const [, ix] = await registerDomainName( + const ix = await registerDomainName( connection, randomBytes(10).toString("hex"), 1_000, @@ -128,7 +127,6 @@ test("Register V2", async () => { 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); }); diff --git a/js/tests/resolve.test.ts b/js/tests/resolve.test.ts index 8dcc3ad..2d58075 100644 --- a/js/tests/resolve.test.ts +++ b/js/tests/resolve.test.ts @@ -1,64 +1,144 @@ require("dotenv").config(); -import { test, jest, expect } from "@jest/globals"; -import { Connection } from "@solana/web3.js"; -import { resolve } from "../src/resolve"; +import { test, jest, expect, describe } from "@jest/globals"; +import { Connection, SystemProgram } from "@solana/web3.js"; +import { AllowPda, resolve } from "../src/resolve/resolve"; +import { + InvalidRoAError, + PdaOwnerNotAllowed, + WrongValidation, +} from "../src/error"; jest.setTimeout(50_000); const connection = new Connection(process.env.RPC_URL!); -const LIST = [ - { - domain: "wallet-guide-5.sol", - owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", - }, - { - domain: "wallet-guide-4.sol", - owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", - }, - { - domain: "wallet-guide-3.sol", - owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", - }, - { - domain: "wallet-guide-2.sol", - owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", - }, - { - domain: "wallet-guide-1.sol", - owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", - }, - { - domain: "wallet-guide-0.sol", - owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", - }, - { - domain: "sub-0.wallet-guide-3.sol", - owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", - }, - { - domain: "sub-1.wallet-guide-3.sol", - owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", - }, +describe("resolve", () => { + test.each([ + { + domain: "sns-ip-5-wallet-1", + result: "ALd1XSrQMCPSRayYUoUZnp6KcP6gERfJhWzkP49CkXKs", + }, + { + domain: "sns-ip-5-wallet-2", + result: "AxwzQXhZNJb9zLyiHUQA12L2GL7CxvUNrp6neee6r3cA", + }, + { + domain: "sns-ip-5-wallet-4", + result: "7PLHHJawDoa4PGJUK3mUnusV7SEVwZwEyV5csVzm86J4", + }, + { + domain: "sns-ip-5-wallet-5", + result: "96GKJgm2W3P8Bae78brPrJf4Yi9AN1wtPJwg2XVQ2rMr", + config: { allowPda: true, programIds: [SystemProgram.programId] }, + }, + { + domain: "sns-ip-5-wallet-5", + result: "96GKJgm2W3P8Bae78brPrJf4Yi9AN1wtPJwg2XVQ2rMr", + config: { allowPda: "any" as AllowPda }, + }, + { + domain: "sns-ip-5-wallet-7", + result: "53Ujp7go6CETvC7LTyxBuyopp5ivjKt6VSfixLm1pQrH", + config: undefined, + }, - // Record V2 - { - domain: "wallet-guide-6", - owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", - }, - { - domain: "wallet-guide-7", - owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", - }, - { - domain: "wallet-guide-8", - owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", - }, -]; + // { + // domain: "wallet-guide-4", + // result: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", + // config: undefined, + // }, -test("Resolve domains", async () => { - for (let x of LIST) { - const owner = await resolve(connection, x.domain); - expect(x.owner).toBe(owner.toBase58()); - } + { + domain: "sns-ip-5-wallet-8", + result: "ALd1XSrQMCPSRayYUoUZnp6KcP6gERfJhWzkP49CkXKs", + config: undefined, + }, + { + domain: "sns-ip-5-wallet-9", + result: "ALd1XSrQMCPSRayYUoUZnp6KcP6gERfJhWzkP49CkXKs", + }, + { + domain: "sns-ip-5-wallet-10", + result: "96GKJgm2W3P8Bae78brPrJf4Yi9AN1wtPJwg2XVQ2rMr", + config: { allowPda: true, programIds: [SystemProgram.programId] }, + }, + { + domain: "sns-ip-5-wallet-10", + result: "96GKJgm2W3P8Bae78brPrJf4Yi9AN1wtPJwg2XVQ2rMr", + config: { allowPda: "any" as AllowPda }, + }, + ])("$domain resolves correctly", async (e) => { + const resolvedValue = await resolve(connection, e.domain, e?.config); + expect(resolvedValue.toBase58()).toBe(e.result); + }); + + test.each([ + { + domain: "sns-ip-5-wallet-3", + error: new WrongValidation(), + }, + { + domain: "sns-ip-5-wallet-6", + error: new PdaOwnerNotAllowed(), + }, + { + domain: "sns-ip-5-wallet-11", + error: new PdaOwnerNotAllowed(), + }, + { + domain: "sns-ip-5-wallet-12", + error: new InvalidRoAError(), + }, + ])("$domain throws an error", async (e) => { + await expect(resolve(connection, e.domain)).rejects.toThrow(e.error); + }); + + test.each([ + { + domain: "wallet-guide-5.sol", + owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", + }, + { + domain: "wallet-guide-4.sol", + owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", + }, + { + domain: "wallet-guide-3.sol", + owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", + }, + { + domain: "wallet-guide-2.sol", + owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", + }, + { + domain: "wallet-guide-1.sol", + owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", + }, + { + domain: "wallet-guide-0.sol", + owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", + }, + { + domain: "sub-0.wallet-guide-3.sol", + owner: "Fxuoy3gFjfJALhwkRcuKjRdechcgffUApeYAfMWck6w8", + }, + { + domain: "sub-1.wallet-guide-3.sol", + owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", + }, + + // Record V2 + { + domain: "wallet-guide-6", + owner: "Hf4daCT4tC2Vy9RCe9q8avT68yAsNJ1dQe6xiQqyGuqZ", + }, + + { + domain: "wallet-guide-8", + owner: "36Dn3RWhB8x4c83W6ebQ2C2eH9sh5bQX2nMdkP2cWaA4", + }, + ])("$domain resolves correctly (backward compatibility)", async (e) => { + const resolvedValue = await resolve(connection, e.domain); + expect(resolvedValue.toBase58()).toBe(e.owner); + }); }); diff --git a/js/tests/reverse-lookup.test.ts b/js/tests/reverse-lookup.test.ts index e424927..e51b63e 100644 --- a/js/tests/reverse-lookup.test.ts +++ b/js/tests/reverse-lookup.test.ts @@ -1,6 +1,7 @@ require("dotenv").config(); import { test, jest, expect } from "@jest/globals"; -import { reverseLookupBatch, reverseLookup } from "../src/utils"; +import { reverseLookupBatch } from "../src/utils/reverseLookupBatch"; +import { reverseLookup } from "../src/utils/reverseLookup"; import { Connection, PublicKey } from "@solana/web3.js"; import { performReverseLookupBatch } from "../src/deprecated/utils"; @@ -11,10 +12,10 @@ const domain = new PublicKey("Crf8hzfthWGbGbLTVCiqRqV5MVnbpHB1L9KQMd6gsinb"); test("Reverse lookup", async () => { performReverseLookupBatch(connection, [domain]).then((e) => - expect(e).toStrictEqual(["bonfida"]) + expect(e).toStrictEqual(["bonfida"]), ); reverseLookupBatch(connection, [domain]).then((e) => - expect(e).toStrictEqual(["bonfida"]) + expect(e).toStrictEqual(["bonfida"]), ); reverseLookup(connection, domain).then((e) => expect(e).toBe("bonfida")); }); diff --git a/js/tests/reverse.test.ts b/js/tests/reverse.test.ts index 202ee67..9587bb2 100644 --- a/js/tests/reverse.test.ts +++ b/js/tests/reverse.test.ts @@ -1,11 +1,11 @@ require("dotenv").config(); import { test, jest, expect } from "@jest/globals"; import { Connection, Transaction } from "@solana/web3.js"; -import { createSubdomain } from "../src/bindings"; +import { createSubdomain } from "../src/bindings/createSubdomain"; import { VAULT_OWNER } from "../src/constants"; -import { resolve } from "../src/resolve"; +import { resolve } from "../src/resolve/resolve"; -jest.setTimeout(5_000); +jest.setTimeout(50_000); const connection = new Connection(process.env.RPC_URL!); @@ -14,7 +14,7 @@ test("Create sub", async () => { const parent = "bonfida.sol"; const parentOwner = await resolve(connection, parent); - const [, ix] = await createSubdomain( + const ix = await createSubdomain( connection, sub + "." + parent, parentOwner, diff --git a/js/tests/sub.test.ts b/js/tests/sub.test.ts index 69244eb..0f5aef1 100644 --- a/js/tests/sub.test.ts +++ b/js/tests/sub.test.ts @@ -1,11 +1,13 @@ require("dotenv").config(); import { test, jest } from "@jest/globals"; import { Connection, PublicKey, Transaction } from "@solana/web3.js"; -import { createSubdomain, transferSubdomain } from "../src/bindings"; +import { createSubdomain } from "../src/bindings/createSubdomain"; +import { transferSubdomain } from "../src/bindings/transferSubdomain"; import { randomBytes } from "crypto"; import { VAULT_OWNER } from "../src/constants"; -import { findSubdomains, getDomainKeySync } from "../src/utils"; -import { resolve } from "../src/resolve"; +import { findSubdomains } from "../src/utils/findSubdomains"; +import { getDomainKeySync } from "../src/utils/getDomainKeySync"; +import { resolve } from "../src/resolve/resolve"; jest.setTimeout(20_000); @@ -13,7 +15,7 @@ const connection = new Connection(process.env.RPC_URL!); test("Create sub", async () => { const tx = new Transaction(); - const [, ix] = await createSubdomain( + const ix = await createSubdomain( connection, randomBytes(10).toString("hex") + ".bonfida", new PublicKey("HKKp49qGWXd639QsuH7JiLijfVW5UtCVY4s1n2HANwEA"), @@ -29,9 +31,9 @@ test("Create sub", async () => { test("Transfer sub", async () => { let tx = new Transaction(); - const owner = new PublicKey("J6QDztZCegYTWnGUYtjqVS9d7AZoS43UbEQmMcdGeP5s"); + const owner = new PublicKey("A41TAGFpQkFpJidLwH37ydunE7Q3jpBaS228RkoXiRQk"); const parentOwner = new PublicKey( - "J6QDztZCegYTWnGUYtjqVS9d7AZoS43UbEQmMcdGeP5s", + "A41TAGFpQkFpJidLwH37ydunE7Q3jpBaS228RkoXiRQk", ); let ix = await transferSubdomain( connection, @@ -76,7 +78,7 @@ test("Create sub - Fee payer ", async () => { const feePayer = VAULT_OWNER; const parentOwner = await resolve(connection, parent); - const [, ix] = await createSubdomain( + const ix = await createSubdomain( connection, sub + "." + parent, parentOwner, diff --git a/js/tests/twitter.test.ts b/js/tests/twitter.test.ts index 5cf2ad9..1e23834 100644 --- a/js/tests/twitter.test.ts +++ b/js/tests/twitter.test.ts @@ -1,14 +1,14 @@ require("dotenv").config(); import { test, expect } from "@jest/globals"; -import { - getTwitterHandleandRegistryKeyViaFilters, - getHandleAndRegistryKey, - createVerifiedTwitterRegistry, - deleteTwitterRegistry, - getTwitterRegistryKey, - getTwitterRegistry, - ReverseTwitterRegistryState, -} from "../src/twitter_bindings"; + +import { getTwitterHandleandRegistryKeyViaFilters } from "../src/twitter/getTwitterHandleandRegistryKeyViaFilters"; +import { getHandleAndRegistryKey } from "../src/twitter/getHandleAndRegistryKey"; +import { createVerifiedTwitterRegistry } from "../src/twitter/createVerifiedTwitterRegistry"; +import { deleteTwitterRegistry } from "../src/twitter/deleteTwitterRegistry"; +import { getTwitterRegistryKey } from "../src/twitter/getTwitterRegistryKey"; +import { getTwitterRegistry } from "../src/twitter/getTwitterRegistry"; + +import { ReverseTwitterRegistryState } from "../src/twitter/ReverseTwitterRegistryState"; import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; import { randomBytes } from "crypto"; import { TWITTER_ROOT_PARENT_REGISTRY_KEY } from "../src/constants"; diff --git a/js/tsconfig.json b/js/tsconfig.json index 2fe1b1d..f42a8e7 100644 --- a/js/tsconfig.json +++ b/js/tsconfig.json @@ -24,14 +24,17 @@ "paths": { "*": ["node_modules/*", "src/types/*"] }, - "resolveJsonModule": true + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "strict": true }, "include": [ "src/*", "src/.ts", "src/deprecated/.ts", - "src/nft/index.ts", + "src/nft/retrieveRecords.ts", "src/record_v2/index.ts" ], - "exclude": ["src/**/*.test.ts", "**/node_modules", "dist"] + "exclude": ["src/**/*.test.ts", "**/node_modules", "dist", "benchmarks"] }