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 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions packages/registrar_client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,24 +35,18 @@ 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:

```typescript
const client = new RegistrarClient({ baseURl: "https://registrar.dev4.grid.tf/v1", privateKey: your_private_key });
const client = new RegistrarClient({ baseURl: "https://registrar.dev4.grid.tf/v1", mnemonicOrSeed: "your_mnemonic_or_seed" });
```

To be able to create a farm you need to have a Stellar wallet and provide your Stellar address. For more details on how to create a Stellar wallet and generate a Stellar address, please refer to the [Stellar Account Viewer](https://www.stellar.org/account-viewer/#!/) or the [Stellar Documentation](https://developers.stellar.org/docs/tutorials/create-account/).
Expand All @@ -62,7 +56,7 @@ To be able to create a farm you need to have a Stellar wallet and provide your S
Here is an example of how to use the Registrar Client:

```typescript
const client = new RegistrarClient({ baseUrl: URl, privateKey: your_private_key });
const client = new RegistrarClient({ baseUrl: URl, mnemonicOrSeed: your_mnemonic_or_seed });

// Example: Create an account
const accountRequest: CreateAccountRequest = {
Expand Down
2 changes: 2 additions & 0 deletions packages/registrar_client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,10 @@
"typescript": "^5.7.3"
},
"dependencies": {
"@polkadot/keyring": "^13.4.4",
"@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
8 changes: 4 additions & 4 deletions packages/registrar_client/scripts/create_farm.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 { deriveKeyPair } from "../src/utils";

async function getAccount(client: RegistrarClient): Promise<Account> {
const publicKey = tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(config.privateKey)).publicKey;
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(publicKey));
const keyPair = await deriveKeyPair(config.mnemonicOrSeed);
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(keyPair.publicKey));
log("================= Getting Account =================");
log(account);
log("================= Getting 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
8 changes: 4 additions & 4 deletions packages/registrar_client/scripts/create_node.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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 { deriveKeyPair } from "../src/utils";

async function getAccount(client: RegistrarClient): Promise<Account> {
const publicKey = tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(config.privateKey)).publicKey;
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(publicKey));
const keyPair = await deriveKeyPair(config.mnemonicOrSeed);
const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(keyPair.publicKey));
log("================= Getting Account =================");
log(account);
log("================= Getting 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
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
47 changes: 37 additions & 10 deletions packages/registrar_client/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
import * as base64 from "base64-js";
import * as tweetnacl from "tweetnacl";
import { AxiosRequestConfig } from "axios";
import { Buffer } from "buffer";
import { Keyring } from "@polkadot/keyring";
import { KeypairType } from "@polkadot/util-crypto/types";
import { KeyringPair } from "@polkadot/keyring/types";
import { validateMnemonic } from "bip39";
import { cryptoWaitReady } from "@polkadot/util-crypto";

function createSignatureForChallenge(challenge: string, privateKey: string): string {
const signature = tweetnacl.sign.detached(Buffer.from(challenge, "utf-8"), base64.toByteArray(privateKey));
const SUPPORTED_KEYPAIR_TYPES: KeypairType[] = ["sr25519", "ed25519"];



function createSignatureForChallenge(challenge: string, keypair: KeyringPair): string {
const signature = keypair.sign(Buffer.from(challenge));
return base64.fromByteArray(signature);
}

export function createSignatureWithPublicKey(publicKey: string, privateKey: string): { signature: string; timestamp: number } {
if (publicKey === "") {
throw new Error("Public key is required");
export async function deriveKeyPair(
mnemonicOrSeed: string,
keypairType: KeypairType = SUPPORTED_KEYPAIR_TYPES[0],
): Promise<KeyringPair> {
if (!SUPPORTED_KEYPAIR_TYPES.includes(keypairType)) {
throw new Error(`Unsupported keypair type: ${keypairType}`);
}

if (!validateMnemonic(mnemonicOrSeed) && !mnemonicOrSeed.startsWith("0x")) {
throw new Error("Invalid seed or mnemonic");
}
await cryptoWaitReady();
const keyring = new Keyring({ type: keypairType });
const keypair = keyring.addFromUri(mnemonicOrSeed);
return keypair;
}

export async function createSignatureWithPublicKey(
mnemonicOrSeed: string,
): Promise<{ signature: string; publicKey: string; timestamp: number }> {
const keypair = await deriveKeyPair(mnemonicOrSeed);
const publicKey = base64.fromByteArray(keypair.publicKey);
const timestamp = Math.floor(Date.now() / 1000);
const challenge = `${timestamp}:${publicKey}`;
const signature = createSignatureForChallenge(challenge, privateKey);
return { signature, timestamp };
const signature = createSignatureForChallenge(challenge, keypair);
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 keypair = await deriveKeyPair(mnemonicOrSeed);
const timestamp = Math.floor(Date.now() / 1000);
const challenge = `${timestamp}:${twinID}`;
const signature = createSignatureForChallenge(challenge, privateKey);
const signature = createSignatureForChallenge(challenge, keypair);
const header = {
"X-Auth": `${Buffer.from(challenge).toString("base64")}:${signature}`,
};
Expand Down
Loading