Skip to content

Development registrar client #4

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 25 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
381fc7c
implement account, farm, node services for registrar
SalmaElsoly Feb 17, 2025
3010a97
refactor to take private key in the intilization of the client
SalmaElsoly Feb 17, 2025
56136ee
fix return type of farm creation + add error handling in config
SalmaElsoly Feb 17, 2025
41a10fb
add integration tests
SalmaElsoly Feb 17, 2025
5f5f17f
add intial README
SalmaElsoly Feb 17, 2025
b737d50
configure eslint
SalmaElsoly Feb 18, 2025
a546fa6
add utils for signature and auth header + add keyed parameters to client
SalmaElsoly Feb 18, 2025
809aa51
fix json writing convention
SalmaElsoly Feb 19, 2025
397ab7f
update repo README
SalmaElsoly Feb 24, 2025
10f5896
adjust package exports
SalmaElsoly Feb 25, 2025
fc8e8a0
update dirs structure + use latest js + update package.json
SalmaElsoly Feb 25, 2025
232d833
add more tests and validation for node data
SalmaElsoly Feb 26, 2025
db36618
add scripts for usage
SalmaElsoly Feb 27, 2025
bec1da1
update README
SalmaElsoly Feb 27, 2025
4664bac
skip failed tests because of server bug
SalmaElsoly Feb 27, 2025
0d30268
update scripts to use config.json + update yarn version
SalmaElsoly Mar 3, 2025
0443d0c
update declarations in tsconfig
SalmaElsoly Mar 4, 2025
1b6ab88
updates tests to config.json + fix build module types
SalmaElsoly Mar 4, 2025
79df76d
fix uri
SalmaElsoly Mar 5, 2025
1e321b4
support stellar address in farm creation and update + fix tests
SalmaElsoly Mar 10, 2025
142087d
update scripts + fix package.json
SalmaElsoly Mar 10, 2025
6b287e9
disable workspace in pkg.json
SalmaElsoly Mar 10, 2025
f4d8f84
update readme
SalmaElsoly Mar 10, 2025
1e4e107
fix scripts + update readme
SalmaElsoly Mar 11, 2025
0bab692
fix requests timeout
SalmaElsoly Mar 11, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules/
13 changes: 13 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "auto"
}
30 changes: 30 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// eslint.config.mjs
import js from "@eslint/js";
import ts from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import prettier from "eslint-plugin-prettier";
import prettierConfig from "eslint-config-prettier";
import globals from "globals";


export default [
js.configs.recommended,
{
files : ["**/*.ts"],
languageOptions: {
parser: tsParser,
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.node
}
},
plugins: {
"@typescript-eslint": ts,
prettier,
},
rules: {
},
},
prettierConfig,
];
5 changes: 5 additions & 0 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent",
"npmClient": "yarn"
}
21 changes: 21 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "root",
"private": true,
"workspaces": [
"packages/*"
],
"dependencies": {
"cosmiconfig": "^9.0.0",
"globals": "^15.15.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
"eslint": "^9.20.1",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"lerna": "^8.1.9",
"prettier": "^3.5.1",
"typescript": "^5.7.3"
}
}
32 changes: 32 additions & 0 deletions packages/registrar_client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Registrar Client

This package provides a client for interacting with the TFGrid v4 Node Registrar.

## Getting Started

Set the `REGISTRAR_URL` in your environment variables to point to the TFGrid v4 Node Registrar.

```sh
export REGISTRAR_URL=https://your-registrar-url
```

## Usage

Here is an example of how to use the Registrar Client:

```typescript
const privateKey = "your_private_key";
const client = new RegistrarClient(privateKey);

const accountRequest: CreateAccountRequest = {
// your create account request data
};
client.account
.createAccount(accountRequest)
.then(account => {
console.log("Account created:", account);
})
.catch(error => {
console.error("Failed to create account:", error);
});
```
15 changes: 15 additions & 0 deletions packages/registrar_client/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// jest.config.js
module.exports = {
preset: "ts-jest/presets/default-esm",
transform: {
"^.+\\.(ts|tsx)$": ["ts-jest", { useESM: true }],
},
extensionsToTreatAsEsm: [".ts", ".tsx"],
globals: {
"ts-jest": {
useESM: true,
},
},
testEnvironment: "node",
setupFiles: ["dotenv/config"],
};
41 changes: 41 additions & 0 deletions packages/registrar_client/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "registrar_client",
"version": "1.0.0",
"description": "Now I’m the model of a modern major general / The venerated Virginian veteran whose men are all / Lining up, to put me up on a pedestal / Writin’ letters to relatives / Embellishin’ my elegance and eloquence / But the elephant is in the room / The truth is in ya face when ya hear the British cannons go / BOOM",
"keywords": [],
"author": "Salma Elsoly <salmaelsoly@gmail.com>",
"license": "ISC",
"main": "lib/registrar_client.js",
"directories": {
"lib": "src",
"test": "tests"
},
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git+https://github.com/threefoldtech/tfgridv4-sdk-ts.git"
},
"scripts": {
"test": "node ./__tests__/registrar_client.test.js"
},
"bugs": {
"url": "https://github.com/threefoldtech/tfgridv4-sdk-ts/issues"
},
"homepage": "https://github.com/threefoldtech/tfgridv4-sdk-ts#readme",
"devDependencies": {
"@types/jest": "^29.5.14",
"@types/node": "^22.13.4",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
},
"dependencies": {
"@types/jest": "^29.5.14",
"dotenv": "^16.4.7",
"jest": "^29.7.0",
"ts-jest": "^29.2.5",
"tweetnacl": "^1.0.3"
}
}
53 changes: 53 additions & 0 deletions packages/registrar_client/src/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import axios, { AxiosInstance, AxiosRequestConfig } from "axios";
import { Accounts } from "../modules/account/service";
import { Config } from "../config";
import { Farms } from "../modules/farm/service";
import { Nodes } from "../modules/node/service";
import { Zos } from "../modules/zos/service";

export abstract class BaseClient {
private client: AxiosInstance;

constructor() {
this.client = axios.create({
baseURL: Config.getInstance().registrarUrl,
});
}

async get<T>(uri: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.get<T>(uri, config);
return response.data;
}

async post<T>(uri: string, data: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(uri, data, config);
return response.data;
}

async patch<T>(uri: string, data: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.patch<T>(uri, data, config);
return response.data;
}

async put<T>(uri: string, data: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(uri, data, config);
return response.data;
}
}

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

constructor(private_key: string) {
super();
this.private_key = private_key;
this.accounts = new Accounts(this);
this.farms = new Farms(this);
this.nodes = new Nodes(this);
this.zos = new Zos(this);
}
}
18 changes: 18 additions & 0 deletions packages/registrar_client/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export class Config {
private static _instance: Config;
public readonly registrarUrl: string;

private constructor() {
if (!process.env.REGISTRAR_URL) {
throw new Error("REGISTRAR_URL environment variable is not defined");
}
this.registrarUrl = process.env.REGISTRAR_URL;
}

public static getInstance(): Config {
if (!Config._instance) {
Config._instance = new Config();
}
return Config._instance;
}
}
95 changes: 95 additions & 0 deletions packages/registrar_client/src/modules/account/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RegistrarClient } from "../../client/client";
import { Account, CreateAccountRequest, UpdateAccountRequest } from "./types";
import * as tweetnacl from "tweetnacl";
import * as base64 from "base64-js";

export class Accounts {
private client: RegistrarClient;

private readonly accountUri = "/accounts";

constructor(client: RegistrarClient) {
this.client = client;
}

async createAccount(request: Partial<CreateAccountRequest>): Promise<Account | null> {
const timestamp = Math.floor(Date.now() / 1000);

const privateKey = this.client.private_key;
let publicKey;
try {
publicKey = base64.fromByteArray(tweetnacl.sign.keyPair.fromSecretKey(base64.toByteArray(privateKey)).publicKey);
} catch (e) {
console.error("Failed to generate public key: ", e);
return null;
}

const challenge = `${timestamp}:${publicKey}`;
const signature = base64.fromByteArray(
tweetnacl.sign.detached(Buffer.from(challenge, "utf-8"), base64.toByteArray(privateKey)),
);

request.public_key = publicKey;
request.signature = signature;
request.timestamp = timestamp;

try {
const data = await this.client.post<Account>(this.accountUri, request);
return data;
} catch (e) {
console.error("Failed to create account: ", e);
return null;
}
}

async getAccountByPublicKey(publicKey: string): Promise<Account | null> {
try {
const data = await this.client.get<Account>(this.accountUri, {
params: {
public_key: publicKey,
},
});
return data;
} catch (e) {
console.error("Failed to get account: ", e);
return null;
}
}

async getAccountByTwinId(twinId: number): Promise<Account | null> {
try {
const data = await this.client.get<Account>(this.accountUri, {
params: {
twin_id: twinId,
},
});

return data;
} catch (e) {
console.error("Failed to get account: ", e);
return null;
}
}

async updateAccount(twinID: number, body: UpdateAccountRequest): Promise<any> {
const timestamp = Math.floor(Date.now() / 1000);
const challenge = `${timestamp}:${twinID}`;
const privateKey = this.client.private_key;
if (!privateKey) {
throw new Error("Private key is not found");
}
const signature = tweetnacl.sign.detached(Buffer.from(challenge, "utf-8"), base64.toByteArray(privateKey));
const config = {
headers: {
"X-Auth": `${Buffer.from(challenge).toString("base64")}:${base64.fromByteArray(signature)}`,
},
};
try {
const data = await this.client.patch<any>(`${this.accountUri}/${twinID}`, body, config);
return data;
} catch (e) {
console.error("Failed to update account: ", e);
return null;
}
}
}
24 changes: 24 additions & 0 deletions packages/registrar_client/src/modules/account/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Farm } from "../farm/types";

export interface Account {
createdAt: string;
public_key: string;
twin_id: number;
relays: string[];
rmb_enc_key: string;
farms: Farm[];
updatedAt: string;
}

export interface CreateAccountRequest {
public_key: string;
relays: string[];
rmb_enc_key: string;
signature: string;
timestamp: number;
}

export interface UpdateAccountRequest {
relays: string[];
rmb_enc_key: string;
}
Loading