From 2e8df9502838c6993a69054e97f76c998d0d1b0b Mon Sep 17 00:00:00 2001 From: Salma Elsoly Date: Thu, 13 Mar 2025 15:20:18 +0200 Subject: [PATCH 1/4] use mnemonic or seed instead of privatekey --- packages/registrar_client/README.md | 14 ++--- packages/registrar_client/package.json | 1 + packages/registrar_client/scripts/README.md | 2 +- packages/registrar_client/scripts/config.json | 2 +- .../scripts/create_account.ts | 2 +- .../registrar_client/scripts/create_farm.ts | 6 +-- .../registrar_client/scripts/create_node.ts | 6 +-- .../registrar_client/scripts/list_farms.ts | 2 +- .../registrar_client/scripts/list_nodes.ts | 2 +- .../scripts/update_account.ts | 2 +- .../registrar_client/scripts/update_farm.ts | 2 +- .../registrar_client/scripts/update_node.ts | 2 +- packages/registrar_client/scripts/utils.ts | 15 ++++++ .../registrar_client/src/client/client.ts | 31 ++++++++--- .../registrar_client/src/modules/accounts.ts | 10 +--- .../registrar_client/src/modules/farms.ts | 4 +- .../registrar_client/src/modules/nodes.ts | 6 +-- packages/registrar_client/src/utils.ts | 28 ++++++++-- .../tests/integration_tests/account.spec.ts | 19 +++++-- .../tests/integration_tests/farm.spec.ts | 6 +-- .../tests/integration_tests/node.spec.ts | 6 +-- .../tests/integration_tests/zos.spec.ts | 7 ++- .../tests/unit_tests/utils.spec.ts | 53 +++++++++++-------- packages/registrar_client/tests/utils.ts | 27 +++++++--- yarn.lock | 12 +++++ 25 files changed, 174 insertions(+), 93 deletions(-) create mode 100644 packages/registrar_client/scripts/utils.ts diff --git a/packages/registrar_client/README.md b/packages/registrar_client/README.md index 8373579..0b34ea0 100644 --- a/packages/registrar_client/README.md +++ b/packages/registrar_client/README.md @@ -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: diff --git a/packages/registrar_client/package.json b/packages/registrar_client/package.json index c8e45cd..2ac2746 100644 --- a/packages/registrar_client/package.json +++ b/packages/registrar_client/package.json @@ -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", diff --git a/packages/registrar_client/scripts/README.md b/packages/registrar_client/scripts/README.md index 040ca20..fb2206d 100644 --- a/packages/registrar_client/scripts/README.md +++ b/packages/registrar_client/scripts/README.md @@ -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 diff --git a/packages/registrar_client/scripts/config.json b/packages/registrar_client/scripts/config.json index 532cb39..0fbbeaf 100644 --- a/packages/registrar_client/scripts/config.json +++ b/packages/registrar_client/scripts/config.json @@ -1 +1 @@ -{"baseUrl": "http://localhost:8080/v1", "privateKey": "", "stellarAddress": ""} \ No newline at end of file +{"baseUrl": "https://registrar.dev4.grid.tf/v1", "mnemonicOrSeed": "", "stellarAddress": ""} \ No newline at end of file diff --git a/packages/registrar_client/scripts/create_account.ts b/packages/registrar_client/scripts/create_account.ts index b44519f..771cd36 100644 --- a/packages/registrar_client/scripts/create_account.ts +++ b/packages/registrar_client/scripts/create_account.ts @@ -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); } diff --git a/packages/registrar_client/scripts/create_farm.ts b/packages/registrar_client/scripts/create_farm.ts index 8464953..c6351b4 100644 --- a/packages/registrar_client/scripts/create_farm.ts +++ b/packages/registrar_client/scripts/create_farm.ts @@ -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 { - 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); @@ -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); diff --git a/packages/registrar_client/scripts/create_node.ts b/packages/registrar_client/scripts/create_node.ts index 912dfeb..ec3cf20 100644 --- a/packages/registrar_client/scripts/create_node.ts +++ b/packages/registrar_client/scripts/create_node.ts @@ -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 { - 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); @@ -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 = { diff --git a/packages/registrar_client/scripts/list_farms.ts b/packages/registrar_client/scripts/list_farms.ts index 2b8450b..137a417 100644 --- a/packages/registrar_client/scripts/list_farms.ts +++ b/packages/registrar_client/scripts/list_farms.ts @@ -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, {}); } diff --git a/packages/registrar_client/scripts/list_nodes.ts b/packages/registrar_client/scripts/list_nodes.ts index e5e0a25..534ea77 100644 --- a/packages/registrar_client/scripts/list_nodes.ts +++ b/packages/registrar_client/scripts/list_nodes.ts @@ -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, }; diff --git a/packages/registrar_client/scripts/update_account.ts b/packages/registrar_client/scripts/update_account.ts index 0aa7ba7..e5141a6 100644 --- a/packages/registrar_client/scripts/update_account.ts +++ b/packages/registrar_client/scripts/update_account.ts @@ -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", diff --git a/packages/registrar_client/scripts/update_farm.ts b/packages/registrar_client/scripts/update_farm.ts index 4ae7be4..60430f5 100644 --- a/packages/registrar_client/scripts/update_farm.ts +++ b/packages/registrar_client/scripts/update_farm.ts @@ -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"; diff --git a/packages/registrar_client/scripts/update_node.ts b/packages/registrar_client/scripts/update_node.ts index 73da673..a82438b 100644 --- a/packages/registrar_client/scripts/update_node.ts +++ b/packages/registrar_client/scripts/update_node.ts @@ -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: [ diff --git a/packages/registrar_client/scripts/utils.ts b/packages/registrar_client/scripts/utils.ts new file mode 100644 index 0000000..b728f77 --- /dev/null +++ b/packages/registrar_client/scripts/utils.ts @@ -0,0 +1,15 @@ +import { validateMnemonic, mnemonicToSeed } from "bip39"; +import { sign } from "tweetnacl"; + +export async function derivePublicKey(mnemonicOrSeed: string): Promise { + 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 +} diff --git a/packages/registrar_client/src/client/client.ts b/packages/registrar_client/src/client/client.ts index 8c85cc7..6a1306e 100644 --- a/packages/registrar_client/src/client/client.ts +++ b/packages/registrar_client/src/client/client.ts @@ -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; @@ -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); diff --git a/packages/registrar_client/src/modules/accounts.ts b/packages/registrar_client/src/modules/accounts.ts index d50ceb9..6ffa039 100644 --- a/packages/registrar_client/src/modules/accounts.ts +++ b/packages/registrar_client/src/modules/accounts.ts @@ -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 { @@ -14,11 +12,7 @@ export class Accounts { } async createAccount(request: Partial): Promise { - 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; @@ -59,7 +53,7 @@ export class Accounts { async updateAccount(twinID: number, body: UpdateAccountRequest): Promise { try { - const headers = createAuthHeader(twinID, this.client.privateKey); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); const data = await this.client.patch(`${this.accountUri}/${twinID}`, body, { headers }); return data; diff --git a/packages/registrar_client/src/modules/farms.ts b/packages/registrar_client/src/modules/farms.ts index efeda65..ac6e13c 100644 --- a/packages/registrar_client/src/modules/farms.ts +++ b/packages/registrar_client/src/modules/farms.ts @@ -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(`${this.farmUri}/`, farm, { headers }); return data; @@ -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(`${this.farmUri}/${farmID}`,farm , { headers }); diff --git a/packages/registrar_client/src/modules/nodes.ts b/packages/registrar_client/src/modules/nodes.ts index 016a8ac..e60587b 100644 --- a/packages/registrar_client/src/modules/nodes.ts +++ b/packages/registrar_client/src/modules/nodes.ts @@ -19,7 +19,7 @@ export class Nodes { async registerNode(node: NodeRegistrationRequest): Promise { 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(`${this.nodeUri}/`, node, { headers }); return data; @@ -50,7 +50,7 @@ export class Nodes { async updateNode(nodeID: number, twinID: number, node: UpdateNodeRequest): Promise { 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(`${this.nodeUri}/${nodeID}`, node, { headers }); return data; @@ -60,7 +60,7 @@ export class Nodes { } async reportNodeUptime(nodeID: number, twinID: number, uptime: UptimeReportRequest): Promise { - const headers = createAuthHeader(twinID, this.client.privateKey); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); try { const data = await this.client.post(`${this.nodeUri}/${nodeID}/uptime`, uptime, { headers }); return data; diff --git a/packages/registrar_client/src/utils.ts b/packages/registrar_client/src/utils.ts index dfd30b7..8a3ef94 100644 --- a/packages/registrar_client/src/utils.ts +++ b/packages/registrar_client/src/utils.ts @@ -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 { + const { privateKey } = await deriveKeyPair(mnemonicOrSeed); const timestamp = Math.floor(Date.now() / 1000); const challenge = `${timestamp}:${twinID}`; const signature = createSignatureForChallenge(challenge, privateKey); @@ -27,3 +44,4 @@ export function createAuthHeader(twinID: number, privateKey: string): AxiosReque }; return header; } + diff --git a/packages/registrar_client/tests/integration_tests/account.spec.ts b/packages/registrar_client/tests/integration_tests/account.spec.ts index e96b101..57b6972 100644 --- a/packages/registrar_client/tests/integration_tests/account.spec.ts +++ b/packages/registrar_client/tests/integration_tests/account.spec.ts @@ -1,15 +1,17 @@ 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) { @@ -17,11 +19,20 @@ describe("test account module", () => { } }); + 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(); }); diff --git a/packages/registrar_client/tests/integration_tests/farm.spec.ts b/packages/registrar_client/tests/integration_tests/farm.spec.ts index 7f95fc1..17bb1e0 100644 --- a/packages/registrar_client/tests/integration_tests/farm.spec.ts +++ b/packages/registrar_client/tests/integration_tests/farm.spec.ts @@ -1,12 +1,12 @@ import { describe, test, expect } from "@jest/globals"; import { RegistrarClient } from "../../src/client/client"; -import { generateKeypair } from "../utils"; import config from "../config.json"; import{Keypair} from "@stellar/stellar-base"; +import { generateMnemonic } from "bip39"; describe("test farm module", () => { - const { privateKey } = generateKeypair(); - const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: privateKey }); + const mnemonic = generateMnemonic(); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: mnemonic }); let twinID = 1; let farmID = 1; diff --git a/packages/registrar_client/tests/integration_tests/node.spec.ts b/packages/registrar_client/tests/integration_tests/node.spec.ts index 9e212bb..6f2e7d9 100644 --- a/packages/registrar_client/tests/integration_tests/node.spec.ts +++ b/packages/registrar_client/tests/integration_tests/node.spec.ts @@ -1,14 +1,14 @@ import { describe, test, expect } from "@jest/globals"; import { NodeRegistrationRequest, UptimeReportRequest, NodesFilter } from "../../src/types/node"; import { RegistrarClient } from "../../src/client/client"; -import { generateKeypair } from "../utils"; import config from "../config.json"; import{Keypair} from "@stellar/stellar-base"; +import { generateMnemonic } from "bip39"; describe("test node module", () => { - const { 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; let nodeID = 1; diff --git a/packages/registrar_client/tests/integration_tests/zos.spec.ts b/packages/registrar_client/tests/integration_tests/zos.spec.ts index 3a251e8..63529f2 100644 --- a/packages/registrar_client/tests/integration_tests/zos.spec.ts +++ b/packages/registrar_client/tests/integration_tests/zos.spec.ts @@ -1,12 +1,11 @@ import { describe, test, expect } from "@jest/globals"; import { RegistrarClient } from "../../src/client/client"; -import { generateKeypair } from "../utils"; import config from "../config.json"; +import { generateMnemonic } from "bip39"; describe("test zos module", () => { - const { privateKey } = generateKeypair(); - - const client = new RegistrarClient({ baseURL: config.baseUrl, privateKey: privateKey }); + const mnemonic = generateMnemonic(); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: mnemonic }); test("get zos version", async () => { const zos = await client.zos.getZosVersion(); diff --git a/packages/registrar_client/tests/unit_tests/utils.spec.ts b/packages/registrar_client/tests/unit_tests/utils.spec.ts index ba89399..98c4409 100644 --- a/packages/registrar_client/tests/unit_tests/utils.spec.ts +++ b/packages/registrar_client/tests/unit_tests/utils.spec.ts @@ -2,59 +2,70 @@ import * as base64 from "base64-js"; import * as tweetnacl from "tweetnacl"; import { describe, test, expect, jest, beforeEach } from "@jest/globals"; import { createSignatureWithPublicKey, createAuthHeader } from "../../src/utils"; +import { generateMnemonic} from "bip39"; jest.mock("tweetnacl", () => ({ sign: { detached: jest.fn(() => new Uint8Array([1, 2, 3, 4])), + keyPair: { + fromSeed: jest.fn(() => ({ + publicKey: new Uint8Array([1, 2, 3, 4]), + secretKey: new Uint8Array([1, 2, 3, 4]), + })), + }, }, })); describe("Util Functions", () => { - const privateKey = base64.fromByteArray(new Uint8Array(32)); - const publicKey = base64.fromByteArray(new Uint8Array(32)); + const mnemonic = generateMnemonic(); + const seed = "0xbd34054dca27b3fa2c84e50251293f58db494450d68f30addcdaca9b7fc6e7ef"; const twinID = 123456; beforeEach(() => { jest.clearAllMocks(); }); - test("createSignatureWithPublicKey generates a valid signature", () => { - const { signature, timestamp } = createSignatureWithPublicKey(publicKey, - privateKey); - + test("createSignatureWithPublicKey generates a valid signature from mnemonics", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(mnemonic); expect(tweetnacl.sign.detached).toHaveBeenCalledWith( Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), - base64.toByteArray(privateKey), + new Uint8Array([1, 2, 3, 4]), ); expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); - test("createAuthHeader generates correct headers", () => { + test("createSignatureWithPublicKey generates a valid signature from seed", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(seed); + expect(tweetnacl.sign.detached).toHaveBeenCalledWith( + Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), + new Uint8Array([1, 2, 3, 4]), + ); + + expect(signature).toBe(base64.fromByteArray(new Uint8Array + ([1, 2, 3, 4]))); + }); + + test("createAuthHeader generates correct headers", async () => { jest.spyOn(global.Date, "now").mockImplementation(() => 1700000000000); const timestamp = 1700000000; - const headers = createAuthHeader(twinID, privateKey)!; + const headers = await createAuthHeader(twinID, mnemonic); expect(headers).toHaveProperty("X-Auth"); - const [encodedChallenge, signature] = headers["X-Auth"].split(":"); + const [encodedChallenge, signature] = headers!["X-Auth"].split(":"); expect(Buffer.from(encodedChallenge, "base64").toString("utf-8")).toBe(`${timestamp}:${twinID}`); expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); - test("createSignatureWithPublicKey handles invalid base64 privateKey", () => { - const invalidPrivateKey = "invalid_base64_string"; - expect(() => createSignatureWithPublicKey(publicKey, invalidPrivateKey)).toThrow(); - }); - - test("createSignatureWithPublicKey handles empty publicKey", () => { - const emptyPublicKey = ""; - expect(() => createSignatureWithPublicKey(emptyPublicKey, privateKey)).toThrow(); + test("createSignatureWithPublicKey handles invalid mnemonic", async () => { + const invalidMnemonic = "invalid mnemonic"; + await expect(createSignatureWithPublicKey(invalidMnemonic)).rejects.toThrow("Invalid seed or mnemonic"); }); - test("createAuthHeader handles invalid base64 privateKey", () => { - const invalidPrivateKey = "invalid_base64_string"; - expect(() => createAuthHeader(twinID, invalidPrivateKey)).toThrow(); + test("createAuthHeader handles invalid mnemonic", async () => { + const invalidMnemonic = "invalid mnemonic"; + await expect(createAuthHeader(twinID, invalidMnemonic)).rejects.toThrow("Invalid seed or mnemonic"); }); }); diff --git a/packages/registrar_client/tests/utils.ts b/packages/registrar_client/tests/utils.ts index f0d8608..e78ba37 100644 --- a/packages/registrar_client/tests/utils.ts +++ b/packages/registrar_client/tests/utils.ts @@ -1,10 +1,21 @@ -import tweetnacl from 'tweetnacl'; -import base64 from 'base64-js'; +import {randomBytes, sign} from 'tweetnacl'; +import { validateMnemonic, mnemonicToSeed } from 'bip39'; -export function generateKeypair() { - const keyPair = tweetnacl.sign.keyPair(); - const publicKey = base64.fromByteArray(keyPair.publicKey); - const privateKey = base64.fromByteArray(keyPair.secretKey); - return { publicKey, privateKey }; - + +export async function derivePublicKey(mnemonicOrSeed: string) : Promise { + 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 Buffer.from(keyPair.publicKey).toString('base64'); +} + +export function generateRandomSeed(): string { + return Buffer.from(randomBytes(32)).toString('hex'); } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d136cdb..27fae28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -751,6 +751,11 @@ "@emnapi/runtime" "^1.1.0" "@tybys/wasm-util" "^0.9.0" +"@noble/hashes@^1.2.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" + integrity sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1765,6 +1770,13 @@ bin-links@^4.0.4: read-cmd-shim "^4.0.0" write-file-atomic "^5.0.0" +bip39@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bip39/-/bip39-3.1.0.tgz#c55a418deaf48826a6ceb34ac55b3ee1577e18a3" + integrity sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A== + dependencies: + "@noble/hashes" "^1.2.0" + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" From 35f428adc1cf942fe57da6fe61c632dd6a284f46 Mon Sep 17 00:00:00 2001 From: Salma Elsoly Date: Mon, 28 Apr 2025 11:00:07 +0300 Subject: [PATCH 2/4] refactor: use polkadot keyring for deriving keys and signing challenges --- packages/registrar_client/README.md | 4 +- packages/registrar_client/package.json | 1 + .../registrar_client/scripts/create_farm.ts | 6 +- .../registrar_client/scripts/create_node.ts | 6 +- packages/registrar_client/scripts/utils.ts | 15 -- packages/registrar_client/src/utils.ts | 53 +++--- .../tests/unit_tests/utils.spec.ts | 48 ++--- yarn.lock | 175 +++++++++++++++++- 8 files changed, 239 insertions(+), 69 deletions(-) delete mode 100644 packages/registrar_client/scripts/utils.ts diff --git a/packages/registrar_client/README.md b/packages/registrar_client/README.md index 0b34ea0..309d9cf 100644 --- a/packages/registrar_client/README.md +++ b/packages/registrar_client/README.md @@ -46,7 +46,7 @@ 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/). @@ -56,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 = { diff --git a/packages/registrar_client/package.json b/packages/registrar_client/package.json index 2ac2746..62e68dd 100644 --- a/packages/registrar_client/package.json +++ b/packages/registrar_client/package.json @@ -54,6 +54,7 @@ "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", diff --git a/packages/registrar_client/scripts/create_farm.ts b/packages/registrar_client/scripts/create_farm.ts index c6351b4..42acdbc 100644 --- a/packages/registrar_client/scripts/create_farm.ts +++ b/packages/registrar_client/scripts/create_farm.ts @@ -2,11 +2,11 @@ import { log } from "console"; import { Account, RegistrarClient } from "../src/"; import config from "./config.json"; import * as base64 from "base64-js"; -import { derivePublicKey } from "./utils"; +import { deriveKeyPair } from "../src/utils"; async function getAccount(client: RegistrarClient): Promise { - const publicKey = await derivePublicKey(config.mnemonicOrSeed); - 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 ================="); diff --git a/packages/registrar_client/scripts/create_node.ts b/packages/registrar_client/scripts/create_node.ts index ec3cf20..b32410b 100644 --- a/packages/registrar_client/scripts/create_node.ts +++ b/packages/registrar_client/scripts/create_node.ts @@ -2,11 +2,11 @@ import { log } from "console"; import { Account, RegistrarClient, NodeRegistrationRequest } from "../src/"; import config from "./config.json"; import * as base64 from "base64-js"; -import { derivePublicKey } from "./utils"; +import { deriveKeyPair } from "../src/utils"; async function getAccount(client: RegistrarClient): Promise { - const publicKey = await derivePublicKey(config.mnemonicOrSeed); - 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 ================="); diff --git a/packages/registrar_client/scripts/utils.ts b/packages/registrar_client/scripts/utils.ts deleted file mode 100644 index b728f77..0000000 --- a/packages/registrar_client/scripts/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { validateMnemonic, mnemonicToSeed } from "bip39"; -import { sign } from "tweetnacl"; - -export async function derivePublicKey(mnemonicOrSeed: string): Promise { - 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 -} diff --git a/packages/registrar_client/src/utils.ts b/packages/registrar_client/src/utils.ts index 8a3ef94..526e26f 100644 --- a/packages/registrar_client/src/utils.ts +++ b/packages/registrar_client/src/utils.ts @@ -1,47 +1,56 @@ import * as base64 from "base64-js"; -import * as tweetnacl from "tweetnacl"; import { AxiosRequestConfig } from "axios"; import { Buffer } from "buffer"; -import { mnemonicToSeed, validateMnemonic} from "bip39"; +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); } -async function deriveKeyPair(mnemonicOrSeed: string): Promise<{ publicKey: string; privateKey: string; }> { - let seed : Buffer; - if (validateMnemonic(mnemonicOrSeed)) { - seed = (await mnemonicToSeed(mnemonicOrSeed)).subarray(0, 32); +export async function deriveKeyPair( + mnemonicOrSeed: string, + keypairType: KeypairType = SUPPORTED_KEYPAIR_TYPES[0], +): Promise { + if (!SUPPORTED_KEYPAIR_TYPES.includes(keypairType)) { + throw new Error(`Unsupported keypair type: ${keypairType}`); + } - } else if(mnemonicOrSeed.startsWith("0x")) { - seed = Buffer.from(mnemonicOrSeed.slice(2), "hex"); - } else { + if (!validateMnemonic(mnemonicOrSeed) && !mnemonicOrSeed.startsWith("0x")) { 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), - }; + 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 { publicKey, privateKey } = await deriveKeyPair(mnemonicOrSeed); +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); + const signature = createSignatureForChallenge(challenge, keypair); return { signature, publicKey, timestamp }; } export async function createAuthHeader(twinID: number, mnemonicOrSeed: string): Promise { - const { privateKey } = await deriveKeyPair(mnemonicOrSeed); + 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}`, }; return header; } - diff --git a/packages/registrar_client/tests/unit_tests/utils.spec.ts b/packages/registrar_client/tests/unit_tests/utils.spec.ts index 98c4409..f097390 100644 --- a/packages/registrar_client/tests/unit_tests/utils.spec.ts +++ b/packages/registrar_client/tests/unit_tests/utils.spec.ts @@ -1,20 +1,22 @@ import * as base64 from "base64-js"; -import * as tweetnacl from "tweetnacl"; import { describe, test, expect, jest, beforeEach } from "@jest/globals"; import { createSignatureWithPublicKey, createAuthHeader } from "../../src/utils"; import { generateMnemonic} from "bip39"; -jest.mock("tweetnacl", () => ({ - sign: { - detached: jest.fn(() => new Uint8Array([1, 2, 3, 4])), - keyPair: { - fromSeed: jest.fn(() => ({ - publicKey: new Uint8Array([1, 2, 3, 4]), - secretKey: new Uint8Array([1, 2, 3, 4]), - })), - }, - }, -})); + + +const mockPublicKey = new Uint8Array([1, 2, 3, 4]); +const mockSignature = jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])); +jest.mock("@polkadot/keyring", () => { + return { + Keyring: jest.fn().mockImplementation(() => ({ + addFromUri: jest.fn().mockReturnValue({ + publicKey: mockPublicKey, + sign: mockSignature, + }), + })), + }; +}); describe("Util Functions", () => { const mnemonic = generateMnemonic(); @@ -23,39 +25,39 @@ describe("Util Functions", () => { beforeEach(() => { jest.clearAllMocks(); + + jest.spyOn(global.Date, "now").mockImplementation(() => 1700000000000); }); test("createSignatureWithPublicKey generates a valid signature from mnemonics", async () => { const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(mnemonic); - expect(tweetnacl.sign.detached).toHaveBeenCalledWith( + expect(timestamp).toBe(1700000000); + expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); + + expect(mockSignature).toHaveBeenCalledWith( Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), - new Uint8Array([1, 2, 3, 4]), ); - expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); test("createSignatureWithPublicKey generates a valid signature from seed", async () => { const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(seed); - expect(tweetnacl.sign.detached).toHaveBeenCalledWith( + expect(timestamp).toBe(1700000000); + expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); + expect(mockSignature).toHaveBeenCalledWith( Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), - new Uint8Array([1, 2, 3, 4]), ); - - expect(signature).toBe(base64.fromByteArray(new Uint8Array - ([1, 2, 3, 4]))); + expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); test("createAuthHeader generates correct headers", async () => { - jest.spyOn(global.Date, "now").mockImplementation(() => 1700000000000); - const timestamp = 1700000000; const headers = await createAuthHeader(twinID, mnemonic); expect(headers).toHaveProperty("X-Auth"); const [encodedChallenge, signature] = headers!["X-Auth"].split(":"); - expect(Buffer.from(encodedChallenge, "base64").toString("utf-8")).toBe(`${timestamp}:${twinID}`); + expect(encodedChallenge).toBe(base64.fromByteArray(Buffer.from(`${1700000000}:${twinID}`))); expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); diff --git a/yarn.lock b/yarn.lock index 27fae28..755a9d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -751,6 +751,18 @@ "@emnapi/runtime" "^1.1.0" "@tybys/wasm-util" "^0.9.0" +"@noble/curves@^1.3.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.0.tgz#13e0ca8be4a0ce66c113693a94514e5599f40cfc" + integrity sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg== + dependencies: + "@noble/hashes" "1.8.0" + +"@noble/hashes@1.8.0", "@noble/hashes@^1.3.3": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.8.0.tgz#cee43d801fcef9644b11b8194857695acd5f815a" + integrity sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== + "@noble/hashes@^1.2.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.7.1.tgz#5738f6d765710921e7a751e00c20ae091ed8db0f" @@ -1121,6 +1133,150 @@ resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== +"@polkadot/keyring@^13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/keyring/-/keyring-13.4.4.tgz#9a24ea3f2b5b554567d251ab734bf9db67baceaf" + integrity sha512-pIm+u1lat+mGUABsCZyTm1/qgwL3NS94SC7TPtSzhzFZq6faUFNMyq1gBfTicrb7MjGFc8FlrKlGxFtudnQC9Q== + dependencies: + "@polkadot/util" "13.4.4" + "@polkadot/util-crypto" "13.4.4" + tslib "^2.8.0" + +"@polkadot/networks@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/networks/-/networks-13.4.4.tgz#aeab1dc16b3a3f216db2366c267111b804cef404" + integrity sha512-I3g9OX3CpZbFKa1C4xQPYOJ2Y209oEb5x9lzgHuAzg64m5vr2y4azWDSQnoOrlFK5ztSrijkWe1gSME1lzy8iA== + dependencies: + "@polkadot/util" "13.4.4" + "@substrate/ss58-registry" "^1.51.0" + tslib "^2.8.0" + +"@polkadot/util-crypto@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/util-crypto/-/util-crypto-13.4.4.tgz#cd6f64481100cdea39ba6cf9ef0a2c0a058f7b4d" + integrity sha512-xuXBNdK3Axlj1ItR6n1kH9zgaDNWri9pb/w1HDFx89bHvw6Bl79wA/oHtVGLbHZ1y/bunpsuapznyswhnsaVog== + dependencies: + "@noble/curves" "^1.3.0" + "@noble/hashes" "^1.3.3" + "@polkadot/networks" "13.4.4" + "@polkadot/util" "13.4.4" + "@polkadot/wasm-crypto" "^7.4.1" + "@polkadot/wasm-util" "^7.4.1" + "@polkadot/x-bigint" "13.4.4" + "@polkadot/x-randomvalues" "13.4.4" + "@scure/base" "^1.1.7" + tslib "^2.8.0" + +"@polkadot/util@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/util/-/util-13.4.4.tgz#9bae6610838f538ead8995bbfb7c4d1b1e2de92b" + integrity sha512-Lveu+8pZLBoAyyv+8X7FI4aTOwLaNXRc9gz1wVaUoGzk5VgmKp/qbPg0a+mfJD8usjf748OVbShhjXvhV3MLiA== + dependencies: + "@polkadot/x-bigint" "13.4.4" + "@polkadot/x-global" "13.4.4" + "@polkadot/x-textdecoder" "13.4.4" + "@polkadot/x-textencoder" "13.4.4" + "@types/bn.js" "^5.1.6" + bn.js "^5.2.1" + tslib "^2.8.0" + +"@polkadot/wasm-bridge@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-bridge/-/wasm-bridge-7.4.1.tgz#dd59ebb7c425657aad64b1430e8455d14d935059" + integrity sha512-tdkJaV453tezBxhF39r4oeG0A39sPKGDJmN81LYLf+Fihb7astzwju+u75BRmDrHZjZIv00un3razJEWCxze6g== + dependencies: + "@polkadot/wasm-util" "7.4.1" + tslib "^2.7.0" + +"@polkadot/wasm-crypto-asmjs@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.4.1.tgz#5d36f3f498077f826f2bbe742070574606e673e9" + integrity sha512-pwU8QXhUW7IberyHJIQr37IhbB6DPkCG5FhozCiNTq4vFBsFPjm9q8aZh7oX1QHQaiAZa2m2/VjIVE+FHGbvHQ== + dependencies: + tslib "^2.7.0" + +"@polkadot/wasm-crypto-init@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.4.1.tgz#88bc61c9473a7c39d9fafb27cd631215b053b836" + integrity sha512-AVka33+f7MvXEEIGq5U0dhaA2SaXMXnxVCQyhJTaCnJ5bRDj0Xlm3ijwDEQUiaDql7EikbkkRtmlvs95eSUWYQ== + dependencies: + "@polkadot/wasm-bridge" "7.4.1" + "@polkadot/wasm-crypto-asmjs" "7.4.1" + "@polkadot/wasm-crypto-wasm" "7.4.1" + "@polkadot/wasm-util" "7.4.1" + tslib "^2.7.0" + +"@polkadot/wasm-crypto-wasm@7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.4.1.tgz#73d08f59aaf51ed70563c0099e7852fdeda03649" + integrity sha512-PE1OAoupFR0ZOV2O8tr7D1FEUAwaggzxtfs3Aa5gr+yxlSOaWUKeqsOYe1KdrcjmZVV3iINEAXxgrbzCmiuONg== + dependencies: + "@polkadot/wasm-util" "7.4.1" + tslib "^2.7.0" + +"@polkadot/wasm-crypto@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-crypto/-/wasm-crypto-7.4.1.tgz#6d5f94d28bf92ef234b94d55b0d1f4299cbbb7b7" + integrity sha512-kHN/kF7hYxm1y0WeFLWeWir6oTzvcFmR4N8fJJokR+ajYbdmrafPN+6iLgQVbhZnDdxyv9jWDuRRsDnBx8tPMQ== + dependencies: + "@polkadot/wasm-bridge" "7.4.1" + "@polkadot/wasm-crypto-asmjs" "7.4.1" + "@polkadot/wasm-crypto-init" "7.4.1" + "@polkadot/wasm-crypto-wasm" "7.4.1" + "@polkadot/wasm-util" "7.4.1" + tslib "^2.7.0" + +"@polkadot/wasm-util@7.4.1", "@polkadot/wasm-util@^7.4.1": + version "7.4.1" + resolved "https://registry.yarnpkg.com/@polkadot/wasm-util/-/wasm-util-7.4.1.tgz#e8cea38a3b752efdef55080bb1da795ac71c5136" + integrity sha512-RAcxNFf3zzpkr+LX/ItAsvj+QyM56TomJ0xjUMo4wKkHjwsxkz4dWJtx5knIgQz/OthqSDMR59VNEycQeNuXzA== + dependencies: + tslib "^2.7.0" + +"@polkadot/x-bigint@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/x-bigint/-/x-bigint-13.4.4.tgz#57fa68c90ce9729dcee57632dc4f94040c335827" + integrity sha512-XjChwagc8TbIoWx9N1JwCMOyte417ngobin6vTmLQsgaOlK84LU0/3uc0ea9qUurPopc/Spf1mSMOFp7lRBrIA== + dependencies: + "@polkadot/x-global" "13.4.4" + tslib "^2.8.0" + +"@polkadot/x-global@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/x-global/-/x-global-13.4.4.tgz#8d2a7cb7f11cc5c934b63064cd12a8b6ada671f0" + integrity sha512-kwXpzTXOgL2GdMWMzynj9ZpZdjfNvDlpQdlWzDNqTBeIeuklhmhDCA7ZFj3p5OkNUnZoTxNj4zgArYO3VKmQ1g== + dependencies: + tslib "^2.8.0" + +"@polkadot/x-randomvalues@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/x-randomvalues/-/x-randomvalues-13.4.4.tgz#98ff15b0222d837098c260ec35987158df342dd3" + integrity sha512-y6sMx2VrXi+V6SLTsd21DJ2Un9s2S7/G1MLDu6IiDBSKcnmOHPV6X43+Y8g+r8B7d9+1+ee/hn1JV+VY+SKSdg== + dependencies: + "@polkadot/x-global" "13.4.4" + tslib "^2.8.0" + +"@polkadot/x-textdecoder@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/x-textdecoder/-/x-textdecoder-13.4.4.tgz#7737438a19fae4949efbf5a0259165168856ad46" + integrity sha512-fXvM2ts0IUx6F/Fd1Yg4ypHCffmUtTSwOtFpqBPvWghKYnmvTbGVrSidGizu6as7KAp4dsum9mYKdRmScBHHzg== + dependencies: + "@polkadot/x-global" "13.4.4" + tslib "^2.8.0" + +"@polkadot/x-textencoder@13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@polkadot/x-textencoder/-/x-textencoder-13.4.4.tgz#857b67e9e4696d191a0be528b08e201b99323f10" + integrity sha512-WpPR2vMAiXS2AN8MfolzWbo5WPEmy5FVZFgwZraJZP8RKfzEuntbY5Nb25p/PIjsPJ1zKU0DE8uVvDcGjxIo7A== + dependencies: + "@polkadot/x-global" "13.4.4" + tslib "^2.8.0" + +"@scure/base@^1.1.7": + version "1.2.5" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.5.tgz#f9d1b232425b367d0dcb81c96611dcc651d58671" + integrity sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw== + "@sigstore/bundle@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@sigstore/bundle/-/bundle-2.3.2.tgz#ad4dbb95d665405fd4a7a02c8a073dbd01e4e95e" @@ -1205,6 +1361,11 @@ optionalDependencies: sodium-native "^4.1.1" +"@substrate/ss58-registry@^1.51.0": + version "1.51.0" + resolved "https://registry.yarnpkg.com/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz#39e0341eb4069c2d3e684b93f0d8cb0bec572383" + integrity sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ== + "@tsconfig/node10@^1.0.7": version "1.0.11" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" @@ -1278,6 +1439,13 @@ dependencies: "@babel/types" "^7.20.7" +"@types/bn.js@^5.1.6": + version "5.1.6" + resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.6.tgz#9ba818eec0c85e4d3c679518428afdf611d03203" + integrity sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w== + dependencies: + "@types/node" "*" + "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" @@ -1786,6 +1954,11 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bn.js@^5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.2.tgz#82c09f9ebbb17107cd72cb7fd39bd1f9d0aaa566" + integrity sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -5777,7 +5950,7 @@ tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2: +tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.6.2, tslib@^2.7.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== From 9a3e2c21f7c59fb46ff5ffc781baae6428da83be Mon Sep 17 00:00:00 2001 From: Salma Elsoly Date: Mon, 5 May 2025 11:25:36 +0300 Subject: [PATCH 3/4] refactor: intialize registrar client with keypair type --- packages/registrar_client/README.md | 25 ++++++- packages/registrar_client/scripts/README.md | 4 +- packages/registrar_client/scripts/config.json | 2 +- .../scripts/create_account.ts | 3 +- .../registrar_client/scripts/create_farm.ts | 3 +- .../registrar_client/scripts/create_node.ts | 5 +- .../scripts/update_account.ts | 3 +- .../registrar_client/scripts/update_farm.ts | 3 +- .../registrar_client/scripts/update_node.ts | 3 +- .../registrar_client/src/client/client.ts | 12 ++- .../registrar_client/src/modules/accounts.ts | 4 +- .../registrar_client/src/modules/farms.ts | 4 +- .../registrar_client/src/modules/nodes.ts | 6 +- packages/registrar_client/src/utils.ts | 11 +-- .../tests/unit_tests/utils.spec.ts | 74 ++++++++++++++----- 15 files changed, 116 insertions(+), 46 deletions(-) diff --git a/packages/registrar_client/README.md b/packages/registrar_client/README.md index 309d9cf..1341bb4 100644 --- a/packages/registrar_client/README.md +++ b/packages/registrar_client/README.md @@ -35,7 +35,22 @@ 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 your mnemonics or 64 character hex seed +To initialize the Registrar Client, you need to provide the base URL of the registrar and either your mnemonic phrase or a 64-character hex seed. You also need to specify the keypair type. + +The supported keypair types are: + +- `ed25519` +- `sr25519` (default) + +Here's how to initialize the client: + +```typescript +const client = new RegistrarClient({ + baseUrl: "https://registrar.dev4.grid.tf/v1", + mnemonicOrSeed: "your_mnemonic_or_seed", + keypairType: "sr25519", // Optional, defaults to "sr25519" +}); +``` To generate 64 character hex seed: @@ -46,7 +61,11 @@ openssl rand -hex 32 Here is an example: ```typescript -const client = new RegistrarClient({ baseURl: "https://registrar.dev4.grid.tf/v1", mnemonicOrSeed: "your_mnemonic_or_seed" }); +const client = new RegistrarClient({ + baseURl: "https://registrar.dev4.grid.tf/v1", + mnemonicOrSeed: "your_mnemonic_or_seed", + keypairType: "ed21559" +}); ``` 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/). @@ -56,7 +75,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, mnemonicOrSeed: your_mnemonic_or_seed }); +const client = new RegistrarClient({ baseUrl: URl, mnemonicOrSeed: your_mnemonic_or_seed, keypairType: "ed21559" }); // Example: Create an account const accountRequest: CreateAccountRequest = { diff --git a/packages/registrar_client/scripts/README.md b/packages/registrar_client/scripts/README.md index fb2206d..677d6d1 100644 --- a/packages/registrar_client/scripts/README.md +++ b/packages/registrar_client/scripts/README.md @@ -16,12 +16,12 @@ yarn install ## Getting Started -- Set base URL, your mnemonic or seed, and Stellar address (if needed) in `scripts/config.json`. +- Set base URL, your mnemonic or seed, keypair type and Stellar address (if needed) in `scripts/config.json`. ## Usage To run any of the scripts, use the following command format: ```bash -yarn ts-node --project tsconfig-node.json scripts/.ts +yarn ts-node --project tsconfig-node.json scripts/.ts ``` diff --git a/packages/registrar_client/scripts/config.json b/packages/registrar_client/scripts/config.json index 0fbbeaf..bf72fda 100644 --- a/packages/registrar_client/scripts/config.json +++ b/packages/registrar_client/scripts/config.json @@ -1 +1 @@ -{"baseUrl": "https://registrar.dev4.grid.tf/v1", "mnemonicOrSeed": "", "stellarAddress": ""} \ No newline at end of file +{"baseUrl": "https://registrar.dev4.grid.tf/v1", "mnemonicOrSeed": "", "stellarAddress": "", "keypairType": "ed25519"} \ No newline at end of file diff --git a/packages/registrar_client/scripts/create_account.ts b/packages/registrar_client/scripts/create_account.ts index 771cd36..5e3ff1f 100644 --- a/packages/registrar_client/scripts/create_account.ts +++ b/packages/registrar_client/scripts/create_account.ts @@ -1,6 +1,7 @@ import { log } from "console"; import { RegistrarClient } from "../src/"; import config from "./config.json"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function createAccount(client: RegistrarClient) { const account = await client.accounts.createAccount({}); @@ -10,7 +11,7 @@ async function createAccount(client: RegistrarClient) { } async function main() { - const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed }); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed, keypairType: config.keypairType as KeypairType }); await createAccount(client); } diff --git a/packages/registrar_client/scripts/create_farm.ts b/packages/registrar_client/scripts/create_farm.ts index 42acdbc..7fd0904 100644 --- a/packages/registrar_client/scripts/create_farm.ts +++ b/packages/registrar_client/scripts/create_farm.ts @@ -3,9 +3,10 @@ import { Account, RegistrarClient } from "../src/"; import config from "./config.json"; import * as base64 from "base64-js"; import { deriveKeyPair } from "../src/utils"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function getAccount(client: RegistrarClient): Promise { - const keyPair = await deriveKeyPair(config.mnemonicOrSeed); + const keyPair = await deriveKeyPair(config.mnemonicOrSeed, config.keypairType as KeypairType); const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(keyPair.publicKey)); log("================= Getting Account ================="); log(account); diff --git a/packages/registrar_client/scripts/create_node.ts b/packages/registrar_client/scripts/create_node.ts index b32410b..384dedf 100644 --- a/packages/registrar_client/scripts/create_node.ts +++ b/packages/registrar_client/scripts/create_node.ts @@ -3,9 +3,10 @@ import { Account, RegistrarClient, NodeRegistrationRequest } from "../src/"; import config from "./config.json"; import * as base64 from "base64-js"; import { deriveKeyPair } from "../src/utils"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function getAccount(client: RegistrarClient): Promise { - const keyPair = await deriveKeyPair(config.mnemonicOrSeed); + const keyPair = await deriveKeyPair(config.mnemonicOrSeed, config.keypairType as KeypairType); const account = await client.accounts.getAccountByPublicKey(base64.fromByteArray(keyPair.publicKey)); log("================= Getting Account ================="); log(account); @@ -29,7 +30,7 @@ async function getNode(client: RegistrarClient, nodeID: number) { } async function main() { - const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed }); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed, keypairType: config.keypairType as KeypairType }); const account = await getAccount(client); const twinID = account.twin_id; const node: NodeRegistrationRequest = { diff --git a/packages/registrar_client/scripts/update_account.ts b/packages/registrar_client/scripts/update_account.ts index e5141a6..047d87b 100644 --- a/packages/registrar_client/scripts/update_account.ts +++ b/packages/registrar_client/scripts/update_account.ts @@ -1,6 +1,7 @@ import { log } from "console"; import { RegistrarClient, UpdateAccountRequest } from "../src/"; import config from "./config.json"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function updateAccount(client: RegistrarClient, twinID: number, update: UpdateAccountRequest) { const account = await client.accounts.updateAccount(twinID, update); @@ -17,7 +18,7 @@ async function getAccount(client: RegistrarClient, twinID: number) { } async function main() { - const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed }); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed, keypairType: config.keypairType as KeypairType }); const update: UpdateAccountRequest = { relays: ["relay1", "relay2"], rmb_enc_key: "rmb_enc_key", diff --git a/packages/registrar_client/scripts/update_farm.ts b/packages/registrar_client/scripts/update_farm.ts index 60430f5..5b289de 100644 --- a/packages/registrar_client/scripts/update_farm.ts +++ b/packages/registrar_client/scripts/update_farm.ts @@ -1,6 +1,7 @@ import { log } from "console"; import { RegistrarClient } from "../src/"; import config from "./config.json"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function updateFarm(client: RegistrarClient, twinID: number, farmID: number, farmName: string) { const farm = await client.farms.updateFarm(farmID, twinID, farmName, config.stellarAddress); @@ -17,7 +18,7 @@ async function getFarm(client: RegistrarClient, farmID: number) { } async function main() { - const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed }); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed, keypairType: config.keypairType as KeypairType }); const twinID = 143; const farmID = 46; const farmName = "testfarm"; diff --git a/packages/registrar_client/scripts/update_node.ts b/packages/registrar_client/scripts/update_node.ts index a82438b..eff5a28 100644 --- a/packages/registrar_client/scripts/update_node.ts +++ b/packages/registrar_client/scripts/update_node.ts @@ -1,6 +1,7 @@ import { log } from "console"; import { UpdateNodeRequest, RegistrarClient } from "../src/"; import config from "./config.json"; +import { KeypairType } from "@polkadot/util-crypto/types"; async function updateNode(client: RegistrarClient, twinID: number, nodeID: number, update: UpdateNodeRequest) { const account = await client.nodes.updateNode(nodeID, twinID, update); @@ -17,7 +18,7 @@ async function getNode(client: RegistrarClient, nodeID: number) { } async function main() { - const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed }); + const client = new RegistrarClient({ baseURL: config.baseUrl, mnemonicOrSeed: config.mnemonicOrSeed, keypairType: config.keypairType as KeypairType }); const update: UpdateNodeRequest = { farm_id: 46, interfaces: [ diff --git a/packages/registrar_client/src/client/client.ts b/packages/registrar_client/src/client/client.ts index 6a1306e..7bedbd8 100644 --- a/packages/registrar_client/src/client/client.ts +++ b/packages/registrar_client/src/client/client.ts @@ -3,7 +3,9 @@ import { Accounts } from "../modules/accounts"; import { Farms } from "../modules/farms"; import { Nodes } from "../modules/nodes"; import { Zos } from "../modules/zos"; -import {validateMnemonic} from "bip39"; +import { validateMnemonic } from "bip39"; +import { KeypairType } from "@polkadot/util-crypto/types"; +import { SUPPORTED_KEYPAIR_TYPES } from "../utils"; export abstract class BaseClient { private client: AxiosInstance; @@ -36,10 +38,12 @@ export abstract class BaseClient { interface Config { baseURL: string; mnemonicOrSeed: string; + keypairType?: KeypairType; } export class RegistrarClient extends BaseClient { public readonly mnemonicOrSeed: string; + public readonly keypairType: KeypairType; accounts: Accounts; farms: Farms; nodes: Nodes; @@ -55,7 +59,10 @@ export class RegistrarClient extends BaseClient { return seed; } - constructor({ baseURL, mnemonicOrSeed }: Config) { + constructor({ baseURL, mnemonicOrSeed, keypairType = "sr25519" }: Config) { + if (!SUPPORTED_KEYPAIR_TYPES.includes(keypairType)) { + throw new Error(`Unsupported keypair type: ${keypairType}`); + } if (!baseURL) { throw new Error("Base URL is required"); } @@ -69,6 +76,7 @@ export class RegistrarClient extends BaseClient { } this.mnemonicOrSeed = mnemonicOrSeed; + this.keypairType = keypairType; this.accounts = new Accounts(this); this.farms = new Farms(this); this.nodes = new Nodes(this); diff --git a/packages/registrar_client/src/modules/accounts.ts b/packages/registrar_client/src/modules/accounts.ts index 6ffa039..30808ed 100644 --- a/packages/registrar_client/src/modules/accounts.ts +++ b/packages/registrar_client/src/modules/accounts.ts @@ -12,7 +12,7 @@ export class Accounts { } async createAccount(request: Partial): Promise { - const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(this.client.mnemonicOrSeed); + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(this.client.mnemonicOrSeed, this.client.keypairType); request.public_key = publicKey; request.signature = signature; @@ -53,7 +53,7 @@ export class Accounts { async updateAccount(twinID: number, body: UpdateAccountRequest): Promise { try { - const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed, this.client.keypairType); const data = await this.client.patch(`${this.accountUri}/${twinID}`, body, { headers }); return data; diff --git a/packages/registrar_client/src/modules/farms.ts b/packages/registrar_client/src/modules/farms.ts index ac6e13c..7a636d9 100644 --- a/packages/registrar_client/src/modules/farms.ts +++ b/packages/registrar_client/src/modules/farms.ts @@ -52,7 +52,7 @@ export class Farms { } const farm = { farm_name: farmName, dedicated, twin_id: twinID, stellar_address: stellarAddress }; - const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed, this.client.keypairType); try { const data = await this.client.post(`${this.farmUri}/`, farm, { headers }); return data; @@ -94,7 +94,7 @@ export class Farms { throw new Error("Invalid stellar address"); } - const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed, this.client.keypairType); const farm : FarmUpdateRequest = { farm_name: name, stellar_address: stellarAddress }; try { const data = await this.client.patch(`${this.farmUri}/${farmID}`,farm , { headers }); diff --git a/packages/registrar_client/src/modules/nodes.ts b/packages/registrar_client/src/modules/nodes.ts index e60587b..abb46e9 100644 --- a/packages/registrar_client/src/modules/nodes.ts +++ b/packages/registrar_client/src/modules/nodes.ts @@ -19,7 +19,7 @@ export class Nodes { async registerNode(node: NodeRegistrationRequest): Promise { this._validateNodeData(node); - const headers = await createAuthHeader(node.twin_id, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(node.twin_id, this.client.mnemonicOrSeed, this.client.keypairType); try { const data = await this.client.post(`${this.nodeUri}/`, node, { headers }); return data; @@ -50,7 +50,7 @@ export class Nodes { async updateNode(nodeID: number, twinID: number, node: UpdateNodeRequest): Promise { this._validateNodeData(node); - const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed, this.client.keypairType); try { const data = await this.client.patch(`${this.nodeUri}/${nodeID}`, node, { headers }); return data; @@ -60,7 +60,7 @@ export class Nodes { } async reportNodeUptime(nodeID: number, twinID: number, uptime: UptimeReportRequest): Promise { - const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed); + const headers = await createAuthHeader(twinID, this.client.mnemonicOrSeed, this.client.keypairType); try { const data = await this.client.post(`${this.nodeUri}/${nodeID}/uptime`, uptime, { headers }); return data; diff --git a/packages/registrar_client/src/utils.ts b/packages/registrar_client/src/utils.ts index 526e26f..328409b 100644 --- a/packages/registrar_client/src/utils.ts +++ b/packages/registrar_client/src/utils.ts @@ -7,7 +7,7 @@ import { KeyringPair } from "@polkadot/keyring/types"; import { validateMnemonic } from "bip39"; import { cryptoWaitReady } from "@polkadot/util-crypto"; -const SUPPORTED_KEYPAIR_TYPES: KeypairType[] = ["sr25519", "ed25519"]; +export const SUPPORTED_KEYPAIR_TYPES: KeypairType[] = ["sr25519", "ed25519"]; @@ -18,7 +18,7 @@ function createSignatureForChallenge(challenge: string, keypair: KeyringPair): s export async function deriveKeyPair( mnemonicOrSeed: string, - keypairType: KeypairType = SUPPORTED_KEYPAIR_TYPES[0], + keypairType: KeypairType ): Promise { if (!SUPPORTED_KEYPAIR_TYPES.includes(keypairType)) { throw new Error(`Unsupported keypair type: ${keypairType}`); @@ -35,8 +35,9 @@ export async function deriveKeyPair( export async function createSignatureWithPublicKey( mnemonicOrSeed: string, + keypairType: KeypairType , ): Promise<{ signature: string; publicKey: string; timestamp: number }> { - const keypair = await deriveKeyPair(mnemonicOrSeed); + const keypair = await deriveKeyPair(mnemonicOrSeed, keypairType); const publicKey = base64.fromByteArray(keypair.publicKey); const timestamp = Math.floor(Date.now() / 1000); const challenge = `${timestamp}:${publicKey}`; @@ -44,8 +45,8 @@ export async function createSignatureWithPublicKey( return { signature, publicKey, timestamp }; } -export async function createAuthHeader(twinID: number, mnemonicOrSeed: string): Promise { - const keypair = await deriveKeyPair(mnemonicOrSeed); +export async function createAuthHeader(twinID: number, mnemonicOrSeed: string, keypairType: KeypairType): Promise { + const keypair = await deriveKeyPair(mnemonicOrSeed, keypairType); const timestamp = Math.floor(Date.now() / 1000); const challenge = `${timestamp}:${twinID}`; const signature = createSignatureForChallenge(challenge, keypair); diff --git a/packages/registrar_client/tests/unit_tests/utils.spec.ts b/packages/registrar_client/tests/unit_tests/utils.spec.ts index f097390..6f16d72 100644 --- a/packages/registrar_client/tests/unit_tests/utils.spec.ts +++ b/packages/registrar_client/tests/unit_tests/utils.spec.ts @@ -1,20 +1,25 @@ import * as base64 from "base64-js"; import { describe, test, expect, jest, beforeEach } from "@jest/globals"; import { createSignatureWithPublicKey, createAuthHeader } from "../../src/utils"; -import { generateMnemonic} from "bip39"; +import { generateMnemonic } from "bip39"; -const mockPublicKey = new Uint8Array([1, 2, 3, 4]); -const mockSignature = jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])); +const mockPublicKeySr = new Uint8Array([1, 2, 3, 4]); +const mockPublicKeyEd = new Uint8Array([5, 6, 7, 8]); +const mockSignatureSr = jest.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])); +const mockSignatureEd = jest.fn().mockReturnValue(new Uint8Array([5, 6, 7, 8])); + jest.mock("@polkadot/keyring", () => { return { - Keyring: jest.fn().mockImplementation(() => ({ - addFromUri: jest.fn().mockReturnValue({ - publicKey: mockPublicKey, - sign: mockSignature, - }), - })), + Keyring: jest.fn().mockImplementation(function (options: any) { + return { + addFromUri: jest.fn().mockReturnValue({ + publicKey: options?.type === "sr25519" ? mockPublicKeySr : mockPublicKeyEd, + sign: options?.type === "sr25519" ? mockSignatureSr : mockSignatureEd, + }), + }; + }), }; }); @@ -29,30 +34,51 @@ describe("Util Functions", () => { jest.spyOn(global.Date, "now").mockImplementation(() => 1700000000000); }); - test("createSignatureWithPublicKey generates a valid signature from mnemonics", async () => { - const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(mnemonic); + test("createSignatureWithPublicKey generates a valid signature from mnemonics with sr25519 keypair", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(mnemonic, "sr25519"); expect(timestamp).toBe(1700000000); expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); - expect(mockSignature).toHaveBeenCalledWith( + expect(mockSignatureSr).toHaveBeenCalledWith( Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), ); expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); - test("createSignatureWithPublicKey generates a valid signature from seed", async () => { - const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(seed); + test("createSignatureWithPublicKey generates a valid signature from seed with sr25519", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(seed, "sr25519"); expect(timestamp).toBe(1700000000); expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); - expect(mockSignature).toHaveBeenCalledWith( + expect(mockSignatureSr).toHaveBeenCalledWith( Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), ); expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); - test("createAuthHeader generates correct headers", async () => { + test("createSignatureWithPublicKey generates a valid signature from mnemonics with ed25519 keypair", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(mnemonic, "ed25519"); + expect(timestamp).toBe(1700000000); + expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([5, 6, 7, 8]))); + + expect(mockSignatureEd).toHaveBeenCalledWith( + Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), + ); + expect(signature).toBe(base64.fromByteArray(new Uint8Array([5, 6, 7, 8]))); + }); - const headers = await createAuthHeader(twinID, mnemonic); + test("createSignatureWithPublicKey generates a valid signature from seed with ed25519", async () => { + const { signature, publicKey, timestamp } = await createSignatureWithPublicKey(seed,"ed25519"); + expect(timestamp).toBe(1700000000); + expect(publicKey).toBe(base64.fromByteArray(new Uint8Array([5, 6, 7, 8]))); + expect(mockSignatureEd).toHaveBeenCalledWith( + Buffer.from(`${timestamp}:${publicKey}`, "utf-8"), + ); + expect(signature).toBe(base64.fromByteArray(new Uint8Array([5, 6, 7, 8]))); + }); + + test("createAuthHeader generates correct headers with sr25519", async () => { + + const headers = await createAuthHeader(twinID, mnemonic, "sr25519"); expect(headers).toHaveProperty("X-Auth"); const [encodedChallenge, signature] = headers!["X-Auth"].split(":"); @@ -61,13 +87,23 @@ describe("Util Functions", () => { expect(signature).toBe(base64.fromByteArray(new Uint8Array([1, 2, 3, 4]))); }); + test("createAuthHeader generates correct headers with ed25519", async () => { + const headers = await createAuthHeader(twinID, mnemonic, "ed25519"); + + expect(headers).toHaveProperty("X-Auth"); + const [encodedChallenge, signature] = headers!["X-Auth"].split(":"); + + expect(encodedChallenge).toBe(base64.fromByteArray(Buffer.from(`${1700000000}:${twinID}`))); + expect(signature).toBe(base64.fromByteArray(new Uint8Array([5, 6, 7, 8]))); + }); + test("createSignatureWithPublicKey handles invalid mnemonic", async () => { const invalidMnemonic = "invalid mnemonic"; - await expect(createSignatureWithPublicKey(invalidMnemonic)).rejects.toThrow("Invalid seed or mnemonic"); + await expect(createSignatureWithPublicKey(invalidMnemonic, "sr25519")).rejects.toThrow("Invalid seed or mnemonic"); }); test("createAuthHeader handles invalid mnemonic", async () => { const invalidMnemonic = "invalid mnemonic"; - await expect(createAuthHeader(twinID, invalidMnemonic)).rejects.toThrow("Invalid seed or mnemonic"); + await expect(createAuthHeader(twinID, invalidMnemonic, "sr25519")).rejects.toThrow("Invalid seed or mnemonic"); }); }); From 94ac63d3bb6f6a1b515c84aed916c72c60541a18 Mon Sep 17 00:00:00 2001 From: Salma Elsoly Date: Mon, 5 May 2025 16:06:18 +0300 Subject: [PATCH 4/4] fix: tests to use new deriveKeypair function --- .../tests/integration_tests/account.spec.ts | 6 ++++-- .../tests/integration_tests/node.spec.ts | 2 +- packages/registrar_client/tests/utils.ts | 17 +---------------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/packages/registrar_client/tests/integration_tests/account.spec.ts b/packages/registrar_client/tests/integration_tests/account.spec.ts index 57b6972..27c6e39 100644 --- a/packages/registrar_client/tests/integration_tests/account.spec.ts +++ b/packages/registrar_client/tests/integration_tests/account.spec.ts @@ -2,9 +2,10 @@ import { describe, test, expect } from "@jest/globals"; import { RegistrarClient } from "../../src/client/client"; import { UpdateAccountRequest } from "../../src/types/account"; import {generateMnemonic} from "bip39"; +import { deriveKeyPair } from "../../src/utils"; import config from "../config.json"; -import { derivePublicKey, generateRandomSeed } from "../utils"; +import { generateRandomSeed } from "../utils"; describe("test account module", () => { const mnemonic = generateMnemonic(); @@ -32,7 +33,8 @@ describe("test account module", () => { }); test("get account by public key", async () => { - const publicKey = await derivePublicKey(mnemonic); + const keyPair = await deriveKeyPair(mnemonic, "sr25519"); + const publicKey = Buffer.from(keyPair.publicKey).toString("base64"); const account = await client.accounts.getAccountByPublicKey(publicKey); expect(account).not.toBeNull(); }); diff --git a/packages/registrar_client/tests/integration_tests/node.spec.ts b/packages/registrar_client/tests/integration_tests/node.spec.ts index 6f2e7d9..ffda48f 100644 --- a/packages/registrar_client/tests/integration_tests/node.spec.ts +++ b/packages/registrar_client/tests/integration_tests/node.spec.ts @@ -72,7 +72,7 @@ describe("test node module", () => { dummyNode.twin_id = twinID; dummyNode.farm_id = 9999999999999; await expect(client.nodes.registerNode(dummyNode as NodeRegistrationRequest)).rejects.toThrowError( - "Failed to register node: 400 Bad Request", + "Failed to register node: 500 Internal Server Error", ); }); diff --git a/packages/registrar_client/tests/utils.ts b/packages/registrar_client/tests/utils.ts index e78ba37..54e5df6 100644 --- a/packages/registrar_client/tests/utils.ts +++ b/packages/registrar_client/tests/utils.ts @@ -1,21 +1,6 @@ -import {randomBytes, sign} from 'tweetnacl'; -import { validateMnemonic, mnemonicToSeed } from 'bip39'; +import {randomBytes} from 'tweetnacl'; -export async function derivePublicKey(mnemonicOrSeed: string) : Promise { - 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 Buffer.from(keyPair.publicKey).toString('base64'); -} - export function generateRandomSeed(): string { return Buffer.from(randomBytes(32)).toString('hex'); } \ No newline at end of file