Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 0ed080a

Browse files
authored
Add name-service program and js bindings (#1600)
1 parent 3dd6767 commit 0ed080a

23 files changed

+6469
-4
lines changed

Cargo.lock

Lines changed: 17 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ members = [
1111
"feature-proposal/cli",
1212
"libraries/math",
1313
"memo/program",
14+
"name-service/program",
1415
"record/program",
1516
"shared-memory/program",
1617
"stake-pool/cli",

docs/src/name-service.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# SPL Name Service
2+
3+
A spl program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, twitter handles, ipfs cid's etc..
4+
5+
This program could be used for dns, pubkey etc lookups via a browser extension for example, the goal is to create an easy way to identify Solana public keys with various links.
6+
Broader use cases are also imaginable.
7+
8+
Key points:
9+
- A Name is a string that maps to a record (program derived account) which can hold data.
10+
- Each name is of a certain class and has a certain owner, both are identified by their pubkeys. The class of a name needs to sign the issuance of it.
11+
- A name can have a parent name that is identified by the address of its record. The owner of the parent name (when it exists) needs to sign the issuance of the child name.
12+
- The data of a name registry is controlled byb the class keypair or, when it is set to `Pubkey::default()`, by the name owner keypair.
13+
- Only the owner can delete a name registry.
14+
15+
16+
Remarks and use cases:
17+
- Domain name declarations: One could arbitrarly set-up a class that we can call Top-Level-Domain names. Names in this class can only be issued with the permission of the class keypair, ie the administrator, who can enforce that TLD names are of the type `".something"`. From then on one could create and own the TLD `".sol"` and create a class of ".sol" sub-domains, administrating the issuance of the `"something.sol"` sub-domains that way (by setting tha parent name to the address of the `".sol"` registry).\
18+
An off-chain browser extension could then, similarly to DNS, parse the user SPL name service URL input and descend the chain of names, verifying that the names exist with the correct parenthood, and finally use the data of the last child name (or also a combination of the parents data) in order to resolve this call towards a real DNS URL or any kind of data.\
19+
Although the ownership and class system makes the administration a given class centralized, the creation of new classes is permissionless and as a class owner any kind of decentralized governance signing program could be used.
20+
21+
- Twitter handles can be added as names of one specific name class. The class authority of will therefore hold the right to add a twitter handle name. This enables the verification of twitter accounts for example by asking the user to tweet his pubkey or a signed message. A bot that holds the private issuing authority key can then sign the Create instruction (with a metadata_authority that is the tweeted pubkey) and send it back to the user who will then submit it to the program.\
22+
In this case the class will still be able to control the data of the name registry, and not the user for example.
23+
Therefore, another way of using this program would be to create a name (`"verified-twitter-handles"` for example) with the `Pubkey::default()` class and with the owner being the authority. That way verified twitter names could be issued as child names of this parent by the owner, leaving the user as being to able to modify the data of his twitter name registry.

name-service/.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Cargo
2+
# will have compiled files and executables
3+
src/target/
4+
5+
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
6+
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
7+
Cargo.lock
8+
9+
# These are backup files generated by rustfmt
10+
**/*.rs.bk
11+
12+
target
13+
.vscode
14+
hfuzz*
15+
third_party/
16+
17+
*/node_modules
18+
js/dist
19+
js/lib
20+
js/docs
21+
js/src/secret.ts

name-service/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Name Service program
2+
3+
A spl program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, twitter handles, ipfs cid's, metadata, etc..
4+
5+
This program provides an interface and implementation that third parties can
6+
utilize to create and use their own version of a name service of any kind.
7+
8+
Full documentation is available at https://spl.solana.com/name-service
9+
10+
JavaScript binding are available in the `./js` directory.

name-service/js/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "spl-name-service",
3+
"version": "0.1.0",
4+
"license": "MIT",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/solana-labs/solana-program-library"
8+
},
9+
"main": "dist/index.js",
10+
"types": "dist/index.d.ts",
11+
"scripts": {
12+
"dev": "tsc && node --trace-warnings dist/test.js",
13+
"build": "tsc",
14+
"prepublish": "tsc",
15+
"lint": "yarn pretty && eslint .",
16+
"lint:fix": "yarn pretty:fix && eslint . --fix",
17+
"pretty": "prettier --check 'src/*.[jt]s'",
18+
"pretty:fix": "prettier --write 'src/*.[jt]s'",
19+
"doc": "yarn typedoc src/index.ts"
20+
},
21+
"devDependencies": {
22+
"@tsconfig/recommended": "^1.0.1",
23+
"@types/bs58": "^4.0.1",
24+
"@types/node": "^14.14.20",
25+
"babel-eslint": "^10.1.0",
26+
"eslint": "^7.17.0",
27+
"eslint-plugin-import": "^2.22.1",
28+
"nodemon": "^2.0.7",
29+
"prettier": "^2.2.1",
30+
"save-dev": "0.0.1-security",
31+
"ts-node": "^9.1.1",
32+
"tslib": "^2.2.0",
33+
"typedoc": "^0.20.35",
34+
"typescript": "^4.1.3"
35+
},
36+
"dependencies": {
37+
"@project-serum/sol-wallet-adapter": "^0.1.5",
38+
"@solana/spl-token": "0.1.3",
39+
"@solana/web3.js": "^1.2.6",
40+
"bip32": "^2.0.6",
41+
"bn.js": "^5.1.3",
42+
"@bonfida/borsh-js": "^0.3.1",
43+
"bs58": "4.0.1",
44+
"buffer-layout": "^1.2.0",
45+
"core-util-is": "^1.0.2",
46+
"crypto": "^1.0.1",
47+
"crypto-ts": "^1.0.2",
48+
"fs": "^0.0.1-security",
49+
"tweetnacl": "^1.0.3",
50+
"webpack-dev-server": "^3.11.2"
51+
}
52+
}

name-service/js/src/bindings.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import {
2+
Account,
3+
Connection,
4+
PublicKey,
5+
SystemProgram,
6+
TransactionInstruction,
7+
} from "@solana/web3.js";
8+
import { Numberu64 } from "./utils";
9+
import { updateInstruction, transferInstruction, createInstruction, deleteInstruction } from "./instructions";
10+
import { createHash, HashOptions } from 'crypto';
11+
import { getHashedName, getNameAccountKey, getNameOwner, Numberu32 } from ".";
12+
import { hash } from "tweetnacl";
13+
import { NameRegistryState } from "./state";
14+
15+
////////////////////////////////////////////////////////////
16+
17+
export const NAME_PROGRAM_ID = new PublicKey(
18+
"Gh9eN9nDuS3ysmAkKf4QJ6yBzf3YNqsn6MD8Ms3TsXmA"
19+
);
20+
export const HASH_PREFIX = "SPL Name Service";
21+
22+
////////////////////////////////////////////////////////////
23+
24+
/**
25+
* Creates a name account with the given rent budget, allocated space, owner and class.
26+
*
27+
* @param connection The solana connection object to the RPC node
28+
* @param name The name of the new account
29+
* @param space The space in bytes allocated to the account
30+
* @param payerKey The allocation cost payer
31+
* @param nameOwner The pubkey to be set as owner of the new name account
32+
* @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption
33+
* @param nameClass The class of this new name
34+
* @param parentName The parent name of the new name. If specified its owner needs to sign
35+
* @returns
36+
*/
37+
export async function createNameRegistry(
38+
connection: Connection,
39+
name: string,
40+
space: number,
41+
payerKey: PublicKey,
42+
nameOwner: PublicKey,
43+
lamports?: number,
44+
nameClass?: PublicKey,
45+
parentName?: PublicKey,
46+
): Promise<TransactionInstruction> {
47+
let hashed_name = await getHashedName(name);
48+
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, parentName);
49+
50+
let balance = lamports
51+
? lamports
52+
: await connection.getMinimumBalanceForRentExemption(space);
53+
54+
let nameParentOwner: PublicKey | undefined;
55+
if (!!parentName) {
56+
let parentAccount = await getNameOwner(connection, parentName);
57+
}
58+
59+
let createNameInstr = createInstruction(
60+
NAME_PROGRAM_ID,
61+
SystemProgram.programId,
62+
nameAccountKey,
63+
nameOwner,
64+
payerKey,
65+
hashed_name,
66+
new Numberu64(balance),
67+
new Numberu32(space),
68+
nameClass,
69+
parentName,
70+
nameParentOwner
71+
);
72+
73+
return createNameInstr;
74+
}
75+
76+
/**
77+
* Overwrite the data of the given name registry.
78+
*
79+
* @param connection The solana connection object to the RPC node
80+
* @param name The name of the name registry to update
81+
* @param offset The offset to which the data should be written into the registry
82+
* @param input_data The data to be written
83+
* @param nameClass The class of this name, if it exsists
84+
* @param nameParent The parent name of this name, if it exists
85+
*/
86+
export async function updateNameRegistryData(
87+
connection: Connection,
88+
name: string,
89+
offset: number,
90+
input_data: Buffer,
91+
nameClass?: PublicKey,
92+
nameParent?: PublicKey,
93+
): Promise<TransactionInstruction> {
94+
let hashed_name = await getHashedName(name);
95+
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
96+
97+
let signer: PublicKey;
98+
if (!!nameClass) {
99+
signer = nameClass;
100+
} else {
101+
signer = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
102+
}
103+
104+
let updateInstr = updateInstruction(
105+
NAME_PROGRAM_ID,
106+
nameAccountKey,
107+
new Numberu32(offset),
108+
input_data,
109+
signer
110+
);
111+
112+
return updateInstr;
113+
}
114+
115+
/**
116+
* Cahnge the owner of a given name account.
117+
*
118+
* @param connection The solana connection object to the RPC node
119+
* @param name The name of the name account
120+
* @param newOwner The new owner to be set
121+
* @param curentNameOwner the current name Owner
122+
* @param nameClass The class of this name, if it exsists
123+
* @param nameParent The parent name of this name, if it exists
124+
* @returns
125+
*/
126+
export async function transferNameOwnership(
127+
connection: Connection,
128+
name: string,
129+
newOwner: PublicKey,
130+
currentNameOwner: PublicKey,
131+
nameClass?: PublicKey,
132+
nameParent?: PublicKey,
133+
): Promise<TransactionInstruction> {
134+
let hashed_name = await getHashedName(name);
135+
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
136+
137+
let curentNameOwner: PublicKey;
138+
if (!!nameClass) {
139+
curentNameOwner = nameClass;
140+
} else {
141+
curentNameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
142+
}
143+
144+
let transferInstr = transferInstruction(
145+
NAME_PROGRAM_ID,
146+
nameAccountKey,
147+
newOwner,
148+
curentNameOwner,
149+
nameClass
150+
);
151+
152+
return transferInstr;
153+
}
154+
155+
/**
156+
* Delete the name account and transfer the rent to the target.
157+
*
158+
* @param connection The solana connection object to the RPC node
159+
* @param name The name of the name account
160+
* @param refundTargetKey The refund destination address
161+
* @param nameClass The class of this name, if it exsists
162+
* @param nameParent The parent name of this name, if it exists
163+
* @returns
164+
*/
165+
export async function deleteNameRegistry(
166+
connection: Connection,
167+
name: string,
168+
refundTargetKey: PublicKey,
169+
nameClass?: PublicKey,
170+
nameParent?: PublicKey,
171+
): Promise<TransactionInstruction> {
172+
let hashed_name = await getHashedName(name);
173+
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
174+
175+
let nameOwner: PublicKey;
176+
if (!!nameClass) {
177+
nameOwner = nameClass;
178+
} else {
179+
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
180+
}
181+
182+
let changeAuthoritiesInstr = deleteInstruction(
183+
NAME_PROGRAM_ID,
184+
nameAccountKey,
185+
refundTargetKey,
186+
nameOwner
187+
);
188+
189+
return changeAuthoritiesInstr;
190+
}

name-service/js/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./utils";
2+
export * from "./bindings";

0 commit comments

Comments
 (0)