Skip to content

Development support mnemonics in registrar client #9

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions packages/registrar_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,12 @@ This package provides a client for interacting with the TFGrid v4 Node Registrar

## Getting Started

To initialize the Registrar Client, you need to provide the base url of registrar and Base64-encoded, 64-byte raw Ed25519 private key (nacl format).
To initialize the Registrar Client, you need to provide the base url of registrar and your mnemonics or 64 character hex seed

To generate a 64-byte ed25519 private key, you can use tweetnacl library to generate key:
To generate 64 character hex seed:

```typescript
import nacl from "tweetnacl";
import base64 from "base64-js";

const keyPair = nacl.sign.keyPair();
const privateKey = base64.fromByteArray(keyPair.secretKey);

console.log("Your 64-byte ed25519 private key:", privateKey);
```bash
openssl rand -hex 32
```

Here is an example:
Expand Down
1 change: 1 addition & 0 deletions packages/registrar_client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"dependencies": {
"@stellar/stellar-base": "^12.1.1",
"@types/jest": "^29.5.14",
"bip39": "^3.1.0",
"dotenv": "^16.4.7",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ yarn install

## Getting Started

- Set base URL, your private key, and Stellar address (if needed) in `scripts/config.json`.
- Set base URL, your mnemonic or seed, and Stellar address (if needed) in `scripts/config.json`.

## Usage

Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/config.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"baseUrl": "http://localhost:8080/v1", "privateKey": "", "stellarAddress": ""}
{"baseUrl": "https://registrar.dev4.grid.tf/v1", "mnemonicOrSeed": "", "stellarAddress": ""}
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/create_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ async function createAccount(client: RegistrarClient) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
await createAccount(client);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/registrar_client/scripts/create_farm.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { log } from "console";
import { Account, RegistrarClient } from "../src/";
import config from "./config.json";
import * as tweetnacl from "tweetnacl";
import * as base64 from "base64-js";
import { derivePublicKey } from "./utils";

async function getAccount(client: RegistrarClient): Promise<Account> {
const publicKey = tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(config.privateKey)).publicKey;
const publicKey = await derivePublicKey(config.mnemonicOrSeed);
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(publicKey));
log("================= Getting Account =================");
log(account);
Expand All @@ -29,7 +29,7 @@ async function getFarm(client: RegistrarClient, farmID: number) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const account = await getAccount(client);
const twinID = account.twin_id;
const farmID = await createFarm(client, twinID, config.stellarAddress);
Expand Down
6 changes: 3 additions & 3 deletions packages/registrar_client/scripts/create_node.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { log } from "console";
import { Account, RegistrarClient, NodeRegistrationRequest } from "../src/";
import config from "./config.json";
import * as tweetnacl from "tweetnacl";
import * as base64 from "base64-js";
import { derivePublicKey } from "./utils";

async function getAccount(client: RegistrarClient): Promise<Account> {
const publicKey = tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(config.privateKey)).publicKey;
const publicKey = await derivePublicKey(config.mnemonicOrSeed);
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(publicKey));
log("================= Getting Account =================");
log(account);
Expand All @@ -29,7 +29,7 @@ async function getNode(client: RegistrarClient, nodeID: number) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const account = await getAccount(client);
const twinID = account.twin_id;
const node: NodeRegistrationRequest = {
Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/list_farms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async function listFarms(client: RegistrarClient, filter: NodesFilter) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
await listFarms(client, {});
}

Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/list_nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ async function listNodes(client: RegistrarClient, filter: NodesFilter) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const filter: NodesFilter = {
farm_id: 70,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/update_account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function getAccount(client: RegistrarClient, twinID: number) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const update: UpdateAccountRequest = {
relays: ["relay1", "relay2"],
rmb_enc_key: "rmb_enc_key",
Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/update_farm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function getFarm(client: RegistrarClient, farmID: number) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const twinID = 143;
const farmID = 46;
const farmName = "testfarm";
Expand Down
2 changes: 1 addition & 1 deletion packages/registrar_client/scripts/update_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ async function getNode(client: RegistrarClient, nodeID: number) {
}

async function main() {
const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: config.privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed });
const update: UpdateNodeRequest = {
farm_id: 46,
interfaces: [
Expand Down
15 changes: 15 additions & 0 deletions packages/registrar_client/scripts/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { validateMnemonic, mnemonicToSeed } from "bip39";
import { sign } from "tweetnacl";

export async function derivePublicKey(mnemonicOrSeed: string): Promise<Uint8Array> {
let seed: Buffer;
if (validateMnemonic(mnemonicOrSeed)) {
seed = (await mnemonicToSeed(mnemonicOrSeed)).subarray(0, 32);
} else if (mnemonicOrSeed.startsWith("0x")) {
seed = Buffer.from(mnemonicOrSeed.slice(2), "hex");
} else {
throw new Error("Invalid seed or mnemonic");
}
const keyPair = sign.keyPair.fromSeed(seed);
return keyPair.publicKey
}
31 changes: 23 additions & 8 deletions packages/registrar_client/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Accounts } from "../modules/accounts";
import { Farms } from "../modules/farms";
import { Nodes } from "../modules/nodes";
import { Zos } from "../modules/zos";

import {validateMnemonic} from "bip39";
export abstract class BaseClient {
private client: AxiosInstance;

Expand Down Expand Up @@ -35,25 +35,40 @@ export abstract class BaseClient {
}
interface Config {
baseURL: string;
privateKey: string;
mnemonicOrSeed: string;
}

export class RegistrarClient extends BaseClient {
public readonly privateKey: string;
public readonly mnemonicOrSeed: string;
accounts: Accounts;
farms: Farms;
nodes: Nodes;
zos: Zos;

constructor({ baseURL, privateKey }: Config) {
_validateSeed(seed: string): string {
if (!seed.startsWith("0x")) {
seed = `0x${seed}`;
}
if (!seed.match(/^0x[a-fA-F0-9]{64}$/)) {
return "";
}
return seed;
}

constructor({ baseURL, mnemonicOrSeed }: Config) {
if (!baseURL) {
throw new Error("Base URL is required");
}
if (!privateKey) {
throw new Error("Private key is required");
}
super(baseURL);
this.privateKey = privateKey;

if (!validateMnemonic(mnemonicOrSeed)) {
mnemonicOrSeed = this._validateSeed(mnemonicOrSeed);
}
if (!mnemonicOrSeed) {
throw new Error("Invalid mnemonic or seed");
}

this.mnemonicOrSeed = mnemonicOrSeed;
this.accounts = new Accounts(this);
this.farms = new Farms(this);
this.nodes = new Nodes(this);
Expand Down
10 changes: 2 additions & 8 deletions packages/registrar_client/src/modules/accounts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { RegistrarClient } from "../client/client";
import { Account, CreateAccountRequest, UpdateAccountRequest } from "../types/account";
import * as tweetnacl from "tweetnacl";
import * as base64 from "base64-js";
import { createSignatureWithPublicKey, createAuthHeader } from "../utils";

export class Accounts {
Expand All @@ -14,11 +12,7 @@ export class Accounts {
}

async createAccount(request: Partial<CreateAccountRequest>): Promise<Account | null> {
const privateKey = this.client.privateKey;
const keyPair = tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(privateKey));

const publicKey = base64.fromByteArray(keyPair.publicKey);
const { signature, timestamp } = createSignatureWithPublicKey(publicKey, privateKey);
const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(this.client.mnemonicOrSeed);

request.public_key = publicKey;
request.signature = signature;
Expand Down Expand Up @@ -59,7 +53,7 @@ export class Accounts {

async updateAccount(twinID: number, body: UpdateAccountRequest): Promise<any> {
try {
const headers = createAuthHeader(twinID, this.client.privateKey);
const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed);

const data = await this.client.patch<any>(`${this.accountUri}/${twinID}`, body, { headers });
return data;
Expand Down
4 changes: 2 additions & 2 deletions packages/registrar_client/src/modules/farms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class Farms {
}

const farm = { farm_name: farmName, dedicated, twin_id: twinID, stellar_address: stellarAddress };
const headers = createAuthHeader(twinID, this.client.privateKey);
const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed);
try {
const data = await this.client.post<FarmCreationResponse>(`${this.farmUri}/`, farm, { headers });
return data;
Expand Down Expand Up @@ -94,7 +94,7 @@ export class Farms {
throw new Error("Invalid stellar address");
}

const headers = createAuthHeader(twinID, this.client.privateKey);
const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed);
const farm : FarmUpdateRequest = { farm_name: name, stellar_address: stellarAddress };
try {
const data = await this.client.patch<any>(`${this.farmUri}/${farmID}`,farm , { headers });
Expand Down
6 changes: 3 additions & 3 deletions packages/registrar_client/src/modules/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class Nodes {

async registerNode(node: NodeRegistrationRequest): Promise<NodeRegistrationResponse> {
this._validateNodeData(node);
const headers = createAuthHeader(node.twin_id, this.client.privateKey);
const headers = await createAuthHeader(node.twin_id, this.client.mnemonicOrSeed);
try {
const data = await this.client.post<NodeRegistrationResponse>(`${this.nodeUri}/`, node, { headers });
return data;
Expand Down Expand Up @@ -50,7 +50,7 @@ export class Nodes {

async updateNode(nodeID: number, twinID: number, node: UpdateNodeRequest): Promise<any> {
this._validateNodeData(node);
const headers = createAuthHeader(twinID, this.client.privateKey);
const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed);
try {
const data = await this.client.patch<Node>(`${this.nodeUri}/${nodeID}`, node, { headers });
return data;
Expand All @@ -60,7 +60,7 @@ export class Nodes {
}

async reportNodeUptime(nodeID: number, twinID: number, uptime: UptimeReportRequest): Promise<any> {
const headers = createAuthHeader(twinID, this.client.privateKey);
const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed);
try {
const data = await this.client.post<any>(`${this.nodeUri}/${nodeID}/uptime`, uptime, { headers });
return data;
Expand Down
28 changes: 23 additions & 5 deletions packages/registrar_client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,40 @@ import * as base64 from "base64-js";
import * as tweetnacl from "tweetnacl";
import { AxiosRequestConfig } from "axios";
import { Buffer } from "buffer";
import { mnemonicToSeed, validateMnemonic} from "bip39";

function createSignatureForChallenge(challenge: string, privateKey: string): string {
const signature = tweetnacl.sign.detached(Buffer.from(challenge, "utf-8"), base64.toByteArray(privateKey));
return base64.fromByteArray(signature);
}

export function createSignatureWithPublicKey(publicKey: string, privateKey: string): { signature: string; timestamp: number } {
if (publicKey === "") {
throw new Error("Public key is required");
async function deriveKeyPair(mnemonicOrSeed: string): Promise<{ publicKey: string; privateKey: string; }> {
let seed : Buffer;
if (validateMnemonic(mnemonicOrSeed)) {
seed = (await mnemonicToSeed(mnemonicOrSeed)).subarray(0, 32);

} else if(mnemonicOrSeed.startsWith("0x")) {
seed = Buffer.from(mnemonicOrSeed.slice(2), "hex");
} else {
throw new Error("Invalid seed or mnemonic");
}
const keyPair = tweetnacl.sign.keyPair.fromSeed(seed);
return {
publicKey: base64.fromByteArray(keyPair.publicKey),
privateKey: base64.fromByteArray(keyPair.secretKey),
};
}

export async function createSignatureWithPublicKey(mnemonicOrSeed: string): Promise<{ signature: string; publicKey: string; timestamp: number; }> {
const { publicKey, privateKey } = await deriveKeyPair(mnemonicOrSeed);
const timestamp = Math.floor(Date.now() / 1000);
const challenge = `${timestamp}:${publicKey}`;
const signature = createSignatureForChallenge(challenge, privateKey);
return { signature, timestamp };
return { signature, publicKey, timestamp };
}

export function createAuthHeader(twinID: number, privateKey: string): AxiosRequestConfig["headers"] {
export async function createAuthHeader(twinID: number, mnemonicOrSeed: string): Promise<AxiosRequestConfig["headers"]> {
const { privateKey } = await deriveKeyPair(mnemonicOrSeed);
const timestamp = Math.floor(Date.now() / 1000);
const challenge = `${timestamp}:${twinID}`;
const signature = createSignatureForChallenge(challenge, privateKey);
Expand All @@ -27,3 +44,4 @@ export function createAuthHeader(twinID: number, privateKey: string): AxiosReque
};
return header;
}

19 changes: 15 additions & 4 deletions packages/registrar_client/tests/integration_tests/account.spec.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import { describe, test, expect } from "@jest/globals";
import { RegistrarClient } from "../../src/client/client";
import { UpdateAccountRequest } from "../../src/types/account";
import { generateKeypair } from "../utils";
import {generateMnemonic} from "bip39";

import config from "../config.json";
import { derivePublicKey, generateRandomSeed } from "../utils";
describe("test account module", () => {
const { publicKey, privateKey } = generateKeypair();
const mnemonic = generateMnemonic();

const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: privateKey });
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: mnemonic });

let twinID = 1;
test("create account", async () => {
test("create account with mnemonic", async () => {
const account = await client.accounts.createAccount({});
expect(account).not.toBeNull();
if (account) {
twinID = account.twin_id;
}
});

test("create account with seed", async () => {
const seed = generateRandomSeed();
const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: seed });
const account = await client.accounts.createAccount({});
expect(account).not.toBeNull();
});


test.skip("create account with same private key", async () => {
await expect(client.accounts.createAccount({})).rejects.toThrowError("Failed to create account: 409 Conflict");
});

test("get account by public key", async () => {
const publicKey = await derivePublicKey(mnemonic);
const account = await client.accounts.getAccountByPublicKey(publicKey);
expect(account).not.toBeNull();
});
Expand Down
Loading