From 0cf1838212f510a821816c6e7e03156ba0dbad95 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 14:22:31 +0300 Subject: [PATCH 01/20] .gitignore --- .gitignore | 2 ++ e2e/.gitignore | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 e2e/.gitignore diff --git a/.gitignore b/.gitignore index 3f20eb58e2..136c151f53 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,5 @@ scripts/specs/local.json # Node modules node_modules + +.claude \ No newline at end of file diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 0000000000..661f94a6e0 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,3 @@ +node_modules +.papi +.env From 8684401aee71fd9ff7e68bcaa39aef4a13eba021 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 15:31:41 +0300 Subject: [PATCH 02/20] Add kill-nodes script --- e2e/kill-nodes.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 e2e/kill-nodes.sh diff --git a/e2e/kill-nodes.sh b/e2e/kill-nodes.sh new file mode 100755 index 0000000000..0cf855b2e4 --- /dev/null +++ b/e2e/kill-nodes.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +pkill -9 -f node-subtensor + +rm -r /tmp/one +rm -r /tmp/two From be4e244efe7cf15382c8bc5587d27e81613be4cd Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 15:32:03 +0300 Subject: [PATCH 03/20] Add metadata --- e2e/get-metadata.sh | 2 ++ e2e/package.json | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100755 e2e/get-metadata.sh create mode 100644 e2e/package.json diff --git a/e2e/get-metadata.sh b/e2e/get-metadata.sh new file mode 100755 index 0000000000..318468f314 --- /dev/null +++ b/e2e/get-metadata.sh @@ -0,0 +1,2 @@ +rm -rf .papi +npx papi add devnet -w ws://localhost:9944 diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 0000000000..973ca7eb99 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,10 @@ +{ + "name": "e2e", + "version": "1.0.0", + "private": true, + "type": "module", + "dependencies": { + "@polkadot-api/descriptors": "file:.papi/descriptors", + "polkadot-api": "^1.22.0" + } +} From dd7c6c4258c727c3321baf6c232e61ab28e46590 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 15:32:18 +0300 Subject: [PATCH 04/20] Add shared package --- e2e/package.json | 1 - e2e/shared/address.ts | 44 ++ e2e/shared/balance.ts | 24 + e2e/shared/client.ts | 30 + e2e/shared/index.ts | 7 + e2e/shared/logger.ts | 7 + e2e/shared/package-lock.json | 1437 ++++++++++++++++++++++++++++++++++ e2e/shared/package.json | 22 + e2e/shared/staking.ts | 30 + e2e/shared/subnet.ts | 72 ++ e2e/shared/transactions.ts | 68 ++ e2e/shared/tsconfig.json | 11 + 12 files changed, 1752 insertions(+), 1 deletion(-) create mode 100644 e2e/shared/address.ts create mode 100644 e2e/shared/balance.ts create mode 100644 e2e/shared/client.ts create mode 100644 e2e/shared/index.ts create mode 100644 e2e/shared/logger.ts create mode 100644 e2e/shared/package-lock.json create mode 100644 e2e/shared/package.json create mode 100644 e2e/shared/staking.ts create mode 100644 e2e/shared/subnet.ts create mode 100644 e2e/shared/transactions.ts create mode 100644 e2e/shared/tsconfig.json diff --git a/e2e/package.json b/e2e/package.json index 973ca7eb99..0cbfae1b70 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -4,7 +4,6 @@ "private": true, "type": "module", "dependencies": { - "@polkadot-api/descriptors": "file:.papi/descriptors", "polkadot-api": "^1.22.0" } } diff --git a/e2e/shared/address.ts b/e2e/shared/address.ts new file mode 100644 index 0000000000..5585c4cb68 --- /dev/null +++ b/e2e/shared/address.ts @@ -0,0 +1,44 @@ +import { sr25519CreateDerive } from "@polkadot-labs/hdkd"; +import { DEV_PHRASE, entropyToMiniSecret, mnemonicToEntropy, KeyPair } from "@polkadot-labs/hdkd-helpers"; +import { getPolkadotSigner } from "polkadot-api/signer"; +import { PolkadotSigner } from "polkadot-api"; +import { randomBytes } from "crypto"; +import { ss58Address } from "@polkadot-labs/hdkd-helpers"; + +export const SS58_PREFIX = 42; + +// ─── KEYPAIR UTILITIES ─────────────────────────────────────────────────────── + +export function getKeypairFromPath(path: string): KeyPair { + const entropy = mnemonicToEntropy(DEV_PHRASE); + const miniSecret = entropyToMiniSecret(entropy); + const derive = sr25519CreateDerive(miniSecret); + return derive(path); +} + +export const getAlice = () => getKeypairFromPath("//Alice"); + +export function getRandomSubstrateKeypair(): KeyPair { + const seed = randomBytes(32); + const miniSecret = entropyToMiniSecret(seed); + const derive = sr25519CreateDerive(miniSecret); + return derive(""); +} + +// ─── SIGNER UTILITIES ──────────────────────────────────────────────────────── + +export function getSignerFromKeypair(keypair: KeyPair): PolkadotSigner { + return getPolkadotSigner(keypair.publicKey, "Sr25519", keypair.sign); +} + +export function getSignerFromPath(path: string): PolkadotSigner { + return getSignerFromKeypair(getKeypairFromPath(path)); +} + +export const getAliceSigner = () => getSignerFromPath("//Alice"); + +// ─── ADDRESS UTILITIES ─────────────────────────────────────────────────────── + +export function convertPublicKeyToSs58(publicKey: Uint8Array): string { + return ss58Address(publicKey, SS58_PREFIX); +} diff --git a/e2e/shared/balance.ts b/e2e/shared/balance.ts new file mode 100644 index 0000000000..7d93815629 --- /dev/null +++ b/e2e/shared/balance.ts @@ -0,0 +1,24 @@ +import { devnet, MultiAddress } from "@polkadot-api/descriptors"; +import { TypedApi } from "polkadot-api"; +import { getAliceSigner } from "./address.js"; +import { waitForTransactionWithRetry } from "./transactions.js"; + +export const TAO = BigInt(1000000000); // 10^9 RAO per TAO + +export function tao(value: number): bigint { + return TAO * BigInt(value); +} + +export async function forceSetBalance( + api: TypedApi, + ss58Address: string, + amount: bigint = tao(1e10) +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.Balances.force_set_balance({ + who: MultiAddress.Id(ss58Address), + new_free: amount, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "force_set_balance"); +} diff --git a/e2e/shared/client.ts b/e2e/shared/client.ts new file mode 100644 index 0000000000..7053458d9a --- /dev/null +++ b/e2e/shared/client.ts @@ -0,0 +1,30 @@ +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi, PolkadotClient, createClient } from "polkadot-api"; +import { getWsProvider } from "polkadot-api/ws-provider/web"; + +export const SUB_LOCAL_URL = "ws://localhost:9944"; + +let client: PolkadotClient | undefined = undefined; +let api: TypedApi | undefined = undefined; + +export async function getClient(): Promise { + if (client === undefined) { + const provider = getWsProvider(SUB_LOCAL_URL); + client = createClient(provider); + } + return client; +} + +export async function getDevnetApi(): Promise> { + if (api === undefined) { + const c = await getClient(); + api = c.getTypedApi(devnet); + } + return api; +} + +export function destroyClient(): void { + client?.destroy(); + client = undefined; + api = undefined; +} diff --git a/e2e/shared/index.ts b/e2e/shared/index.ts new file mode 100644 index 0000000000..981273ae5c --- /dev/null +++ b/e2e/shared/index.ts @@ -0,0 +1,7 @@ +export * from "./logger.js"; +export * from "./client.js"; +export * from "./address.js"; +export * from "./transactions.js"; +export * from "./balance.js"; +export * from "./subnet.js"; +export * from "./staking.js"; diff --git a/e2e/shared/logger.ts b/e2e/shared/logger.ts new file mode 100644 index 0000000000..041443353a --- /dev/null +++ b/e2e/shared/logger.ts @@ -0,0 +1,7 @@ +const LOG_INDENT = " "; + +export const log = { + tx: (label: string, msg: string) => console.log(`${LOG_INDENT}[${label}] ${msg}`), + info: (msg: string) => console.log(`${LOG_INDENT}${msg}`), + error: (label: string, msg: string) => console.error(`${LOG_INDENT}[${label}] ${msg}`), +}; diff --git a/e2e/shared/package-lock.json b/e2e/shared/package-lock.json new file mode 100644 index 0000000000..aae37d9be3 --- /dev/null +++ b/e2e/shared/package-lock.json @@ -0,0 +1,1437 @@ +{ + "name": "node-compat", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node-compat", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@polkadot/api": "^16.5.4", + "tsx": "^4.21.0" + }, + "devDependencies": { + "@types/node": "^24" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polkadot-api/json-rpc-provider": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider/-/json-rpc-provider-0.0.1.tgz", + "integrity": "sha512-/SMC/l7foRjpykLTUTacIH05H3mr9ip8b5xxfwXlVezXrNVLp3Cv0GX6uItkKd+ZjzVPf3PFrDF2B2/HLSNESA==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/json-rpc-provider-proxy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/json-rpc-provider-proxy/-/json-rpc-provider-proxy-0.1.0.tgz", + "integrity": "sha512-8GSFE5+EF73MCuLQm8tjrbCqlgclcHBSRaswvXziJ0ZW7iw3UEMsKkkKvELayWyBuOPa2T5i1nj6gFOeIsqvrg==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot-api/metadata-builders": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/metadata-builders/-/metadata-builders-0.3.2.tgz", + "integrity": "sha512-TKpfoT6vTb+513KDzMBTfCb/ORdgRnsS3TDFpOhAhZ08ikvK+hjHMt5plPiAX/OWkm1Wc9I3+K6W0hX5Ab7MVg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + } + }, + "node_modules/@polkadot-api/observable-client": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@polkadot-api/observable-client/-/observable-client-0.3.2.tgz", + "integrity": "sha512-HGgqWgEutVyOBXoGOPp4+IAq6CNdK/3MfQJmhCJb8YaJiaK4W6aRGrdQuQSTPHfERHCARt9BrOmEvTXAT257Ug==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/metadata-builders": "0.3.2", + "@polkadot-api/substrate-bindings": "0.6.0", + "@polkadot-api/utils": "0.1.0" + }, + "peerDependencies": { + "@polkadot-api/substrate-client": "0.1.4", + "rxjs": ">=7.8.0" + } + }, + "node_modules/@polkadot-api/substrate-bindings": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/substrate-bindings/-/substrate-bindings-0.6.0.tgz", + "integrity": "sha512-lGuhE74NA1/PqdN7fKFdE5C1gNYX357j1tWzdlPXI0kQ7h3kN0zfxNOpPUN7dIrPcOFZ6C0tRRVrBylXkI6xPw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@noble/hashes": "^1.3.1", + "@polkadot-api/utils": "0.1.0", + "@scure/base": "^1.1.1", + "scale-ts": "^1.6.0" + } + }, + "node_modules/@polkadot-api/utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@polkadot-api/utils/-/utils-0.1.0.tgz", + "integrity": "sha512-MXzWZeuGxKizPx2Xf/47wx9sr/uxKw39bVJUptTJdsaQn/TGq+z310mHzf1RCGvC1diHM8f593KrnDgc9oNbJA==", + "license": "MIT", + "optional": true + }, + "node_modules/@polkadot/api": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-16.5.4.tgz", + "integrity": "sha512-mX1fwtXCBAHXEyZLSnSrMDGP+jfU2rr7GfDVQBz0cBY1nmY8N34RqPWGrZWj8o4DxVu1DQ91sGncOmlBwEl0Qg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-augment": "16.5.4", + "@polkadot/api-base": "16.5.4", + "@polkadot/api-derive": "16.5.4", + "@polkadot/keyring": "^14.0.1", + "@polkadot/rpc-augment": "16.5.4", + "@polkadot/rpc-core": "16.5.4", + "@polkadot/rpc-provider": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/types-augment": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/types-create": "16.5.4", + "@polkadot/types-known": "16.5.4", + "@polkadot/util": "^14.0.1", + "@polkadot/util-crypto": "^14.0.1", + "eventemitter3": "^5.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-augment": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-16.5.4.tgz", + "integrity": "sha512-9FTohz13ih458V2JBFjRACKHPqfM6j4bmmTbcSaE7hXcIOYzm4ABFo7xq5osLyvItganjsICErL2vRn2zULycw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-base": "16.5.4", + "@polkadot/rpc-augment": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/types-augment": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-base": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-16.5.4.tgz", + "integrity": "sha512-V69v3ieg5+91yRUCG1vFRSLr7V7MvHPvo/QrzleIUu8tPXWldJ0kyXbWKHVNZEpVBA9LpjGvII+MHUW7EaKMNg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/util": "^14.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/api-derive": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-16.5.4.tgz", + "integrity": "sha512-0JP2a6CaqTviacHsmnUKF4VLRsKdYOzQCqdL9JpwY/QBz/ZLqIKKPiSRg285EVLf8n/hWdTfxbWqQCsRa5NL+Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "16.5.4", + "@polkadot/api-augment": "16.5.4", + "@polkadot/api-base": "16.5.4", + "@polkadot/rpc-core": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/util": "^14.0.1", + "@polkadot/util-crypto": "^14.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/keyring": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-14.0.1.tgz", + "integrity": "sha512-kHydQPCeTvJrMC9VQO8LPhAhTUxzxfNF1HEknhZDBPPsxP/XpkYsEy/Ln1QzJmQqD5VsgwzLDE6cExbJ2CT9CA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "14.0.1", + "@polkadot/util-crypto": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.1", + "@polkadot/util-crypto": "14.0.1" + } + }, + "node_modules/@polkadot/networks": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-14.0.1.tgz", + "integrity": "sha512-wGlBtXDkusRAj4P7uxfPz80gLO1+j99MLBaQi3bEym2xrFrFhgIWVHOZlBit/1PfaBjhX2Z8XjRxaM2w1p7w2w==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "14.0.1", + "@substrate/ss58-registry": "^1.51.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-augment": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-16.5.4.tgz", + "integrity": "sha512-j9v3Ttqv/EYGezHtVksGJAFZhE/4F7LUWooOazh/53ATowMby3lZUdwInrK6bpYmG2whmYMw/Fo283fwDroBtQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-core": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-core": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-16.5.4.tgz", + "integrity": "sha512-92LOSTWujPjtmKOPvfCPs8rAaPFU+18wTtkIzwPwKxvxkN/SWsYSGIxmsoags9ramyHB6jp7Lr59TEuGMxIZzQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/rpc-augment": "16.5.4", + "@polkadot/rpc-provider": "16.5.4", + "@polkadot/types": "16.5.4", + "@polkadot/util": "^14.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/rpc-provider": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-16.5.4.tgz", + "integrity": "sha512-mNAIBRA3jMvpnHsuqAX4InHSIqBdgxFD6ayVUFFAzOX8Fh6Xpd4RdI1dqr6a1pCzjnPSby4nbg+VuadWwauVtg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^14.0.1", + "@polkadot/types": "16.5.4", + "@polkadot/types-support": "16.5.4", + "@polkadot/util": "^14.0.1", + "@polkadot/util-crypto": "^14.0.1", + "@polkadot/x-fetch": "^14.0.1", + "@polkadot/x-global": "^14.0.1", + "@polkadot/x-ws": "^14.0.1", + "eventemitter3": "^5.0.1", + "mock-socket": "^9.3.1", + "nock": "^13.5.5", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@substrate/connect": "0.8.11" + } + }, + "node_modules/@polkadot/types": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-16.5.4.tgz", + "integrity": "sha512-8Oo1QWaL0DkIc/n2wKBIozPWug/0b2dPVhL+XrXHxJX7rIqS0x8sXDRbM9r166sI0nTqJiUho7pRIkt2PR/DMQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^14.0.1", + "@polkadot/types-augment": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/types-create": "16.5.4", + "@polkadot/util": "^14.0.1", + "@polkadot/util-crypto": "^14.0.1", + "rxjs": "^7.8.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-augment": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-16.5.4.tgz", + "integrity": "sha512-AGjXR+Q9O9UtVkGw/HuOXlbRqVpvG6H8nr+taXP71wuC6RD9gznFBFBqoNkfWHD2w89esNVQLTvXHVxlLpTXqA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-codec": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-16.5.4.tgz", + "integrity": "sha512-OQtT1pmJu2F3/+Vh1OiXifKoeRy+CU1+Lu7dgTcdO705dnxU4447Zup5JVCJDnxBmMITts/38vbFN2pD225AnA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^14.0.1", + "@polkadot/x-bigint": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-create": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-16.5.4.tgz", + "integrity": "sha512-URQnvr/sgvgIRSxIW3lmml6HMSTRRj2hTZIm6nhMTlYSVT4rLWx0ZbYUAjoPBbaJ+BmoqZ6Bbs+tA+5cQViv5Q==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/types-codec": "16.5.4", + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-known": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-16.5.4.tgz", + "integrity": "sha512-Dd59y4e3AFCrH9xiqMU4xlG5+Zy0OTy7GQvqJVYXZFyAH+4HYDlxXjJGcSidGAmJcclSYfS3wyEkfw+j1EOVEw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/networks": "^14.0.1", + "@polkadot/types": "16.5.4", + "@polkadot/types-codec": "16.5.4", + "@polkadot/types-create": "16.5.4", + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/types-support": { + "version": "16.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-16.5.4.tgz", + "integrity": "sha512-Ra6keCaO73ibxN6MzA56jFq9EReje7jjE4JQfzV5IpyDZdXcmPyJiEfa2Yps/YSP13Gc2e38t9FFyVau0V+SFQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/util": "^14.0.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-14.0.1.tgz", + "integrity": "sha512-764HhxkPV3x5rM0/p6QdynC2dw26n+SaE+jisjx556ViCd4E28Ke4xSPef6C0Spy4aoXf2gt0PuLEcBvd6fVZg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@polkadot/x-bigint": "14.0.1", + "@polkadot/x-global": "14.0.1", + "@polkadot/x-textdecoder": "14.0.1", + "@polkadot/x-textencoder": "14.0.1", + "@types/bn.js": "^5.1.6", + "bn.js": "^5.2.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/util-crypto": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-14.0.1.tgz", + "integrity": "sha512-Cu7AKUzBTsUkbOtyuNzXcTpDjR9QW0fVR56o3gBmzfUCmvO1vlsuGzmmPzqpHymQQ3rrfqV78CPs62EGhw0R+A==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@polkadot/networks": "14.0.1", + "@polkadot/util": "14.0.1", + "@polkadot/wasm-crypto": "^7.5.3", + "@polkadot/wasm-util": "^7.5.3", + "@polkadot/x-bigint": "14.0.1", + "@polkadot/x-randomvalues": "14.0.1", + "@scure/base": "^1.1.7", + "@scure/sr25519": "^0.2.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.1" + } + }, + "node_modules/@polkadot/wasm-bridge": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-bridge/-/wasm-bridge-7.5.4.tgz", + "integrity": "sha512-6xaJVvoZbnbgpQYXNw9OHVNWjXmtcoPcWh7hlwx3NpfiLkkjljj99YS+XGZQlq7ks2fVCg7FbfknkNb8PldDaA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-7.5.4.tgz", + "integrity": "sha512-1seyClxa7Jd7kQjfnCzTTTfYhTa/KUTDUaD3DMHBk5Q4ZUN1D1unJgX+v1aUeXSPxmzocdZETPJJRZjhVOqg9g==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.4", + "@polkadot/wasm-crypto-asmjs": "7.5.4", + "@polkadot/wasm-crypto-init": "7.5.4", + "@polkadot/wasm-crypto-wasm": "7.5.4", + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-asmjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-asmjs/-/wasm-crypto-asmjs-7.5.4.tgz", + "integrity": "sha512-ZYwxQHAJ8pPt6kYk9XFmyuFuSS+yirJLonvP+DYbxOrARRUHfN4nzp4zcZNXUuaFhpbDobDSFn6gYzye6BUotA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-init": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-init/-/wasm-crypto-init-7.5.4.tgz", + "integrity": "sha512-U6s4Eo2rHs2n1iR01vTz/sOQ7eOnRPjaCsGWhPV+ZC/20hkVzwPAhiizu/IqMEol4tO2yiSheD4D6bn0KxUJhg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-bridge": "7.5.4", + "@polkadot/wasm-crypto-asmjs": "7.5.4", + "@polkadot/wasm-crypto-wasm": "7.5.4", + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*", + "@polkadot/x-randomvalues": "*" + } + }, + "node_modules/@polkadot/wasm-crypto-wasm": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto-wasm/-/wasm-crypto-wasm-7.5.4.tgz", + "integrity": "sha512-PsHgLsVTu43eprwSvUGnxybtOEuHPES6AbApcs7y5ZbM2PiDMzYbAjNul098xJK/CPtrxZ0ePDFnaQBmIJyTFw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/wasm-util": "7.5.4", + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/wasm-util": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/@polkadot/wasm-util/-/wasm-util-7.5.4.tgz", + "integrity": "sha512-hqPpfhCpRAqCIn/CYbBluhh0TXmwkJnDRjxrU9Bnqtw9nMNa97D8JuOjdd2pi0rxm+eeLQ/f1rQMp71RMM9t4w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "*" + } + }, + "node_modules/@polkadot/x-bigint": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-14.0.1.tgz", + "integrity": "sha512-gfozjGnebr2rqURs31KtaWumbW4rRZpbiluhlmai6luCNrf5u8pB+oLA35kPEntrsLk9PnIG9OsC/n4hEtx4OQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-fetch": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-14.0.1.tgz", + "integrity": "sha512-yFsnO0xfkp3bIcvH70ZvmeUINYH1YnjOIS1B430f3w6axkqKhAOWCgzzKGMSRgn4dtm3YgwMBKPQ4nyfIsGOJQ==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "node-fetch": "^3.3.2", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-global": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-14.0.1.tgz", + "integrity": "sha512-aCI44DJU4fU0XXqrrSGIpi7JrZXK2kpe0jaQ2p6oDVXOOYEnZYXnMhTTmBE1lF/xtxzX50MnZrrU87jziU0qbA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-randomvalues": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-14.0.1.tgz", + "integrity": "sha512-/XkQcvshzJLHITuPrN3zmQKuFIPdKWoaiHhhVLD6rQWV60lTXA3ajw3ocju8ZN7xRxnweMS9Ce0kMPYa0NhRMg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@polkadot/util": "14.0.1", + "@polkadot/wasm-util": "*" + } + }, + "node_modules/@polkadot/x-textdecoder": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-14.0.1.tgz", + "integrity": "sha512-CcWiPCuPVJsNk4Vq43lgFHqLRBQHb4r9RD7ZIYgmwoebES8TNm4g2ew9ToCzakFKSpzKu6I07Ne9wv/dt5zLuw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-textencoder": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-14.0.1.tgz", + "integrity": "sha512-VY51SpQmF1ccmAGLfxhYnAe95Spfz049WZ/+kK4NfsGF9WejxVdU53Im5C80l45r8qHuYQsCWU3+t0FNunh2Kg==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@polkadot/x-ws": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-14.0.1.tgz", + "integrity": "sha512-Q18hoSuOl7F4aENNGNt9XYxkrjwZlC6xye9OQrPDeHam1SrvflGv9mSZHyo+mwJs0z1PCz2STpPEN9PKfZvHng==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/x-global": "14.0.1", + "tslib": "^2.8.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/sr25519": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-0.2.0.tgz", + "integrity": "sha512-uUuLP7Z126XdSizKtrCGqYyR3b3hYtJ6Fg/XFUXmc2//k2aXHDLqZwFeXxL97gg4XydPROPVnuaHGF2+xriSKg==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.2", + "@noble/hashes": "~1.8.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@substrate/connect": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.11.tgz", + "integrity": "sha512-ofLs1PAO9AtDdPbdyTYj217Pe+lBfTLltdHDs3ds8no0BseoLeAGxpz1mHfi7zB4IxI3YyAiLjH6U8cw4pj4Nw==", + "deprecated": "versions below 1.x are no longer maintained", + "license": "GPL-3.0-only", + "optional": true, + "dependencies": { + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "@substrate/light-client-extension-helpers": "^1.0.0", + "smoldot": "2.0.26" + } + }, + "node_modules/@substrate/connect-extension-protocol": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@substrate/connect-extension-protocol/-/connect-extension-protocol-2.2.2.tgz", + "integrity": "sha512-t66jwrXA0s5Goq82ZtjagLNd7DPGCNjHeehRlE/gcJmJ+G56C0W+2plqOMRicJ8XGR1/YFnUSEqUFiSNbjGrAA==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/connect-known-chains": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@substrate/connect-known-chains/-/connect-known-chains-1.10.3.tgz", + "integrity": "sha512-OJEZO1Pagtb6bNE3wCikc2wrmvEU5x7GxFFLqqbz1AJYYxSlrPCGu4N2og5YTExo4IcloNMQYFRkBGue0BKZ4w==", + "license": "GPL-3.0-only", + "optional": true + }, + "node_modules/@substrate/light-client-extension-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@substrate/light-client-extension-helpers/-/light-client-extension-helpers-1.0.0.tgz", + "integrity": "sha512-TdKlni1mBBZptOaeVrKnusMg/UBpWUORNDv5fdCaJklP4RJiFOzBCrzC+CyVI5kQzsXBisZ+2pXm+rIjS38kHg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@polkadot-api/json-rpc-provider": "^0.0.1", + "@polkadot-api/json-rpc-provider-proxy": "^0.1.0", + "@polkadot-api/observable-client": "^0.3.0", + "@polkadot-api/substrate-client": "^0.1.2", + "@substrate/connect-extension-protocol": "^2.0.0", + "@substrate/connect-known-chains": "^1.1.5", + "rxjs": "^7.8.1" + }, + "peerDependencies": { + "smoldot": "2.x" + } + }, + "node_modules/@substrate/ss58-registry": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@substrate/ss58-registry/-/ss58-registry-1.51.0.tgz", + "integrity": "sha512-TWDurLiPxndFgKjVavCniytBIw+t4ViOi7TYp9h/D0NMmkEc9klFTo+827eyEJ0lELpqO207Ey7uGxUa+BS1jQ==", + "license": "Apache-2.0" + }, + "node_modules/@types/bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", + "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, + "node_modules/mock-socket": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.3.1.tgz", + "integrity": "sha512-qxBgB7Qa2sEQgHFjj0dSigq7fX4k6Saisd5Nelwp2q8mlbAFh5dHV9JTTlF8viYJLSSWgMCZFUom8PJcMNBoJw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nock": { + "version": "13.5.6", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.5.6.tgz", + "integrity": "sha512-o2zOYiCpzRqSzPj0Zt/dQ/DqZeYoaQ7TUonc/xUPjCGl9WeHpNbxgVvOquXYAaJzI0M9BXV3HTzG0p8IUAbBTQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/scale-ts": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/scale-ts/-/scale-ts-1.6.1.tgz", + "integrity": "sha512-PBMc2AWc6wSEqJYBDPcyCLUj9/tMKnLX70jLOSndMtcUoLQucP/DM0vnQo1wJAYjTrQiq8iG9rD0q6wFzgjH7g==", + "license": "MIT", + "optional": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/e2e/shared/package.json b/e2e/shared/package.json new file mode 100644 index 0000000000..78735fc530 --- /dev/null +++ b/e2e/shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "shared", + "version": "1.0.0", + "type": "module", + "license": "ISC", + "main": "index.ts", + "dependencies": { + "@polkadot-api/descriptors": "file:../.papi/descriptors", + "@polkadot-labs/hdkd": "^0.0.25", + "@polkadot-labs/hdkd-helpers": "^0.0.25", + "polkadot-api": "^1.22.0", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "@types/node": "^24" + }, + "prettier": { + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120 + } +} diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts new file mode 100644 index 0000000000..47fb48dd8a --- /dev/null +++ b/e2e/shared/staking.ts @@ -0,0 +1,30 @@ +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi } from "polkadot-api"; +import { KeyPair } from "@polkadot-labs/hdkd-helpers"; +import { getSignerFromKeypair } from "./address.js"; +import { waitForTransactionWithRetry } from "./transactions.js"; + +export async function addStake( + api: TypedApi, + netuid: number, + hotkeyAddress: string, + amount: bigint, + coldkey: KeyPair +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.add_stake({ + netuid: netuid, + hotkey: hotkeyAddress, + amount_staked: amount, + }); + await waitForTransactionWithRetry(api, tx, signer, "add_stake"); +} + +export async function getStake( + api: TypedApi, + hotkeyAddress: string, + coldkeyAddress: string, + netuid: number +): Promise { + return await api.query.SubtensorModule.Alpha.getValue(hotkeyAddress, coldkeyAddress, netuid); +} diff --git a/e2e/shared/subnet.ts b/e2e/shared/subnet.ts new file mode 100644 index 0000000000..6cab21957b --- /dev/null +++ b/e2e/shared/subnet.ts @@ -0,0 +1,72 @@ +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi } from "polkadot-api"; +import { KeyPair } from "@polkadot-labs/hdkd-helpers"; +import { getAliceSigner, getSignerFromKeypair, convertPublicKeyToSs58 } from "./address.js"; +import { waitForTransactionWithRetry } from "./transactions.js"; +import { log } from "./logger.js"; + +export async function addNewSubnetwork( + api: TypedApi, + hotkey: KeyPair, + coldkey: KeyPair +): Promise { + const alice = getAliceSigner(); + const totalNetworks = await api.query.SubtensorModule.TotalNetworks.getValue(); + + // Disable network rate limit for testing + const rateLimit = await api.query.SubtensorModule.NetworkRateLimit.getValue(); + if (rateLimit !== BigInt(0)) { + const internalCall = api.tx.AdminUtils.sudo_set_network_rate_limit({ rate_limit: BigInt(0) }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "set_network_rate_limit"); + } + + const signer = getSignerFromKeypair(coldkey); + const registerNetworkTx = api.tx.SubtensorModule.register_network({ + hotkey: convertPublicKeyToSs58(hotkey.publicKey), + }); + await waitForTransactionWithRetry(api, registerNetworkTx, signer, "register_network"); + + return totalNetworks; +} + +export async function burnedRegister( + api: TypedApi, + netuid: number, + hotkeyAddress: string, + coldkey: KeyPair +): Promise { + const registered = await api.query.SubtensorModule.Uids.getValue(netuid, hotkeyAddress); + if (registered !== undefined) { + log.tx("burned_register", `skipped: hotkey already registered on netuid ${netuid}`); + return; + } + + await new Promise((resolve) => setTimeout(resolve, 1000)); + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.burned_register({ hotkey: hotkeyAddress, netuid: netuid }); + await waitForTransactionWithRetry(api, tx, signer, "burned_register"); +} + +export async function startCall( + api: TypedApi, + netuid: number, + coldkey: KeyPair +): Promise { + const registerBlock = Number(await api.query.SubtensorModule.NetworkRegisteredAt.getValue(netuid)); + let currentBlock = await api.query.System.Number.getValue(); + const duration = Number(await api.constants.SubtensorModule.InitialStartCallDelay); + + while (currentBlock - registerBlock <= duration) { + await new Promise((resolve) => setTimeout(resolve, 2000)); + currentBlock = await api.query.System.Number.getValue(); + } + + await new Promise((resolve) => setTimeout(resolve, 2000)); + + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.start_call({ netuid: netuid }); + await waitForTransactionWithRetry(api, tx, signer, "start_call"); + + await new Promise((resolve) => setTimeout(resolve, 1000)); +} diff --git a/e2e/shared/transactions.ts b/e2e/shared/transactions.ts new file mode 100644 index 0000000000..cbf1884328 --- /dev/null +++ b/e2e/shared/transactions.ts @@ -0,0 +1,68 @@ +import { devnet } from "@polkadot-api/descriptors"; +import { TypedApi, Transaction, PolkadotSigner } from "polkadot-api"; +import { log } from "./logger.js"; + +export const TX_TIMEOUT = 5000; + +export async function waitForTransactionWithRetry( + api: TypedApi, + tx: Transaction<{}, string, string, void>, + signer: PolkadotSigner, + label: string, + maxRetries = 5 +): Promise { + let success = false; + let retries = 0; + + while (!success && retries < maxRetries) { + await waitForTransactionCompletion(tx, signer, label) + .then(() => { + success = true; + }) + .catch((error) => { + log.tx(label, `error: ${error}`); + }); + await new Promise((resolve) => setTimeout(resolve, 1000)); + retries += 1; + } + + if (!success) { + throw new Error(`[${label}] failed after ${maxRetries} retries`); + } +} + +async function waitForTransactionCompletion( + tx: Transaction<{}, string, string, void>, + signer: PolkadotSigner, + label: string +): Promise { + return new Promise((resolve, reject) => { + let txHash = ""; + const subscription = tx.signSubmitAndWatch(signer).subscribe({ + next(value) { + txHash = value.txHash; + if (value.type === "finalized") { + log.tx(label, `finalized: ${value.txHash}`); + subscription.unsubscribe(); + clearTimeout(timeoutId); + if (!value.ok) { + log.tx(label, `dispatch error: ${value.dispatchError}`); + } + resolve(); + } + }, + error(err) { + log.error(label, `failed: ${err}`); + subscription.unsubscribe(); + clearTimeout(timeoutId); + reject(err); + }, + }); + + const timeoutId = setTimeout(() => { + subscription.unsubscribe(); + log.tx(label, `timeout for tx: ${txHash}`); + reject(new Error(`[${label}] timeout`)); + }, TX_TIMEOUT); + }); +} diff --git a/e2e/shared/tsconfig.json b/e2e/shared/tsconfig.json new file mode 100644 index 0000000000..b4cbbb843b --- /dev/null +++ b/e2e/shared/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + } +} From 700140278a30118edc389543f67657bb154d83b5 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 15:32:32 +0300 Subject: [PATCH 05/20] Add start-nodes package --- e2e/start-nodes/lib.ts | 135 ++++++++++++++++++++++++++++++++++ e2e/start-nodes/main.ts | 52 +++++++++++++ e2e/start-nodes/package.json | 20 +++++ e2e/start-nodes/tsconfig.json | 11 +++ 4 files changed, 218 insertions(+) create mode 100644 e2e/start-nodes/lib.ts create mode 100644 e2e/start-nodes/main.ts create mode 100644 e2e/start-nodes/package.json create mode 100644 e2e/start-nodes/tsconfig.json diff --git a/e2e/start-nodes/lib.ts b/e2e/start-nodes/lib.ts new file mode 100644 index 0000000000..172888164d --- /dev/null +++ b/e2e/start-nodes/lib.ts @@ -0,0 +1,135 @@ +import { spawnSync, spawn, ChildProcess } from "node:child_process"; +import { writeFile } from "node:fs/promises"; + +export const SECOND = 1000; +export const MINUTE = 60 * SECOND; + +export type Node = { name: string; binaryPath: string; process: ChildProcess }; + +export type NodeOptions = { + binaryPath: string; + basePath: string; + name: string; + port: number; + rpcPort: number; + validator: boolean; +}; + +export type ChainSpecOptions = { + binaryPath: string; + outputPath: string; + chain?: string; +}; + +// Generate the chain spec for the local network +export const generateChainSpec = async (opts: ChainSpecOptions): Promise => { + const result = spawnSync( + opts.binaryPath, + ["build-spec", "--disable-default-bootnode", "--raw", "--chain", opts.chain ?? "local"], + { maxBuffer: 1024 * 1024 * 10 }, // 10MB + ); + + if (result.error) { + throw new Error(`Failed to spawn process: ${result.error.message}`); + } + + if (result.status !== 0) { + throw new Error(`Failed to generate chain spec: ${result.stderr?.toString() ?? "unknown error"}`); + } + + const stdout = result.stdout.toString(); + await writeFile(opts.outputPath, stdout, { encoding: "utf-8" }); +}; + +// Start a node with the given options +export const startNode = (opts: NodeOptions & { chainSpecPath: string }): Node => { + const process = spawn(opts.binaryPath, [ + `--${opts.name}`, + ...["--chain", opts.chainSpecPath], + ...["--base-path", opts.basePath], + ...["--port", opts.port.toString()], + ...["--rpc-port", opts.rpcPort.toString()], + ...(opts.validator ? ["--validator"] : []), + "--rpc-cors=all", + "--allow-private-ipv4", + "--discover-local", + "--unsafe-force-node-key-generation", + ]); + + process.on("error", (error) => console.error(`${opts.name} (error): ${error}`)); + process.on("close", (code) => log(`${opts.name}: process closed with code ${code}`)); + + return { name: opts.name, binaryPath: opts.binaryPath, process }; +}; + +// Detach the node process so Node.js can exit while it keeps running +export const detach = (node: Node): void => { + node.process.unref(); + node.process.stdout?.unref(); + node.process.stderr?.unref(); + node.process.stdin?.unref(); +}; + +// Ensure the node has correctly started +export const started = (node: Node, timeout = 30 * SECOND): Promise => { + const errorMessage = `Failed to start ${node.name} in time`; + + return innerEnsure(node, errorMessage, timeout, (data, ok) => { + if (data.includes("💤 Idle")) { + log(`${node.name}: started using ${node.binaryPath}`); + ok(); + } + }); +}; + +// Ensure the node has reached the expected number of peers +export const peerCount = (node: Node, expectedPeers: number, timeout = 30 * SECOND): Promise => { + const errorMessage = `Failed to reach ${expectedPeers} peers in time`; + + return innerEnsure(node, errorMessage, timeout, (data, ok) => { + const maybePeers = /Idle \((?\d+) peers\)/.exec(data)?.groups?.peers; + if (!maybePeers) return; + + const peers = parseInt(maybePeers); + if (peers >= expectedPeers) { + log(`${node.name}: reached ${expectedPeers} peers`); + ok(); + } + }); +}; + +// Ensure the node has reached the expected number of finalized blocks +export const finalizedBlocks = (node: Node, expectedFinalized: number, timeout = 10 * MINUTE): Promise => { + const errorMessage = `Failed to reach ${expectedFinalized} finalized blocks in time`; + + return innerEnsure(node, errorMessage, timeout, (data, ok) => { + const maybeFinalized = /finalized #(?\d+)/.exec(data)?.groups?.blocks; + if (!maybeFinalized) return; + + const finalized = parseInt(maybeFinalized); + if (finalized >= expectedFinalized) { + log(`${node.name}: reached ${expectedFinalized} finalized blocks`); + ok(); + } + }); +}; + +// Helper function to ensure a condition is met within a timeout +function innerEnsure(node: Node, errorMessage: string, timeout: number, f: (data: string, ok: () => void) => void) { + return new Promise((resolve, reject) => { + const id = setTimeout(() => reject(new Error(errorMessage)), timeout); + + const fn = (data: string) => + f(data, () => { + clearTimeout(id); + node.process.stderr?.off("data", fn); + resolve(); + }); + + node.process.stderr?.on("data", fn); + }); +} + +export const log = (message: string) => console.log(`[${new Date().toISOString()}] ${message}`); + +export const all = Promise.all.bind(Promise); diff --git a/e2e/start-nodes/main.ts b/e2e/start-nodes/main.ts new file mode 100644 index 0000000000..aa079f1060 --- /dev/null +++ b/e2e/start-nodes/main.ts @@ -0,0 +1,52 @@ +import { + generateChainSpec, + startNode, + detach, + started, + peerCount, + finalizedBlocks, + log, + all, + NodeOptions, +} from "./lib.js"; + +const OLD_BINARY_PATH = "../../target/release/node-subtensor"; +const CHAIN_SPEC_PATH = "/tmp/local.json"; + +const ONE_OPTIONS: NodeOptions = { + binaryPath: OLD_BINARY_PATH, + basePath: "/tmp/one", + name: "one", + port: 30333, + rpcPort: 9933, + validator: true, +}; + +const TWO_OPTIONS: NodeOptions = { + binaryPath: OLD_BINARY_PATH, + basePath: "/tmp/two", + name: "two", + port: 30334, + rpcPort: 9944, + validator: true, +}; + +async function main() { + await generateChainSpec({ binaryPath: OLD_BINARY_PATH, outputPath: CHAIN_SPEC_PATH }); + + const one = startNode({ ...ONE_OPTIONS, chainSpecPath: CHAIN_SPEC_PATH }); + await started(one); + + const two = startNode({ ...TWO_OPTIONS, chainSpecPath: CHAIN_SPEC_PATH }); + await started(two); + + await all([peerCount(one, 1), peerCount(two, 1)]); + await all([finalizedBlocks(one, 5), finalizedBlocks(two, 5)]); + + log("Validators started ✅"); + + detach(one); + detach(two); +} + +main(); diff --git a/e2e/start-nodes/package.json b/e2e/start-nodes/package.json new file mode 100644 index 0000000000..5b3a884c77 --- /dev/null +++ b/e2e/start-nodes/package.json @@ -0,0 +1,20 @@ +{ + "name": "start-nodes", + "version": "1.0.0", + "type": "module", + "license": "ISC", + "scripts": { + "start": "tsx main.ts" + }, + "dependencies": { + "tsx": "^4.21.0" + }, + "devDependencies": { + "@types/node": "^24" + }, + "prettier": { + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120 + } +} diff --git a/e2e/start-nodes/tsconfig.json b/e2e/start-nodes/tsconfig.json new file mode 100644 index 0000000000..b4cbbb843b --- /dev/null +++ b/e2e/start-nodes/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "types": ["node"] + } +} From 866ec36c83fd82b70a6c997c0d96ea5a2101c767 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 15:32:36 +0300 Subject: [PATCH 06/20] Add staking-tests package --- e2e/staking-tests/package.json | 23 ++++++++++ e2e/staking-tests/setup.ts | 5 +++ e2e/staking-tests/test/add-stake.test.ts | 55 ++++++++++++++++++++++++ e2e/staking-tests/tsconfig.json | 11 +++++ 4 files changed, 94 insertions(+) create mode 100644 e2e/staking-tests/package.json create mode 100644 e2e/staking-tests/setup.ts create mode 100644 e2e/staking-tests/test/add-stake.test.ts create mode 100644 e2e/staking-tests/tsconfig.json diff --git a/e2e/staking-tests/package.json b/e2e/staking-tests/package.json new file mode 100644 index 0000000000..d3b6d2ed9c --- /dev/null +++ b/e2e/staking-tests/package.json @@ -0,0 +1,23 @@ +{ + "name": "staking-tests", + "version": "1.0.0", + "type": "module", + "license": "ISC", + "scripts": { + "test": "tsx node_modules/mocha/bin/mocha.js --timeout 999999 --retries 3 --file setup.ts --extension ts \"test/**/*.ts\"" + }, + "dependencies": { + "shared": "../shared", + "mocha": "^11.1.0", + "tsx": "^4.21.0" + }, + "devDependencies": { + "@types/mocha": "^10.0.10", + "@types/node": "^24" + }, + "prettier": { + "singleQuote": false, + "trailingComma": "all", + "printWidth": 120 + } +} diff --git a/e2e/staking-tests/setup.ts b/e2e/staking-tests/setup.ts new file mode 100644 index 0000000000..3589e8a9aa --- /dev/null +++ b/e2e/staking-tests/setup.ts @@ -0,0 +1,5 @@ +import { destroyClient } from "shared"; + +after(() => { + destroyClient(); +}); diff --git a/e2e/staking-tests/test/add-stake.test.ts b/e2e/staking-tests/test/add-stake.test.ts new file mode 100644 index 0000000000..6b23cff70c --- /dev/null +++ b/e2e/staking-tests/test/add-stake.test.ts @@ -0,0 +1,55 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + getStake, + tao, + log, +} from "shared"; + +describe("▶ add_stake extrinsic", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let netuid: number; + + before(async () => { + const api = await getDevnetApi(); + + // Fund accounts + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + + // Create subnet and register hotkey + netuid = await addNewSubnetwork(api, hotkey, coldkey); + await burnedRegister(api, netuid, hotkeyAddress, coldkey); + await startCall(api, netuid, coldkey); + }); + + it("should add stake to a hotkey", async () => { + const api = await getDevnetApi(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + // Get initial stake + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + + // Add stake + const stakeAmount = tao(100); + await addStake(api, netuid, hotkeyAddress, stakeAmount, coldkey); + + // Verify stake increased + const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + assert.ok(stakeAfter > stakeBefore, `Stake should increase: before=${stakeBefore}, after=${stakeAfter}`); + + log.info("✅ Successfully added stake."); + }); +}); diff --git a/e2e/staking-tests/tsconfig.json b/e2e/staking-tests/tsconfig.json new file mode 100644 index 0000000000..a414aaf914 --- /dev/null +++ b/e2e/staking-tests/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "types": ["node", "mocha"] + } +} From 229fe368312842188cbddabf33df896c395d9491 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 17 Feb 2026 16:52:41 +0300 Subject: [PATCH 07/20] E2E workflow --- .github/workflows/e2e.yml | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .github/workflows/e2e.yml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000000..750cc9ab99 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,78 @@ +name: E2E Tests + +on: + workflow_dispatch: + +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + protobuf-compiler \ + libprotobuf-dev \ + libclang-dev \ + clang \ + cmake \ + build-essential + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Build node with release, fast-runtime, and metadata-hash + run: | + cargo build \ + --profile release \ + --features "fast-runtime metadata-hash pow-faucet" \ + -p node-subtensor + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + + - name: Install start-nodes dependencies + working-directory: e2e/start-nodes + run: npm install + + - name: Start validator nodes + working-directory: e2e/start-nodes + run: npx tsx main.ts + + - name: Install polkadot-api for papi CLI + working-directory: e2e + run: npm install + + - name: Generate PAPI descriptors from running node + working-directory: e2e + run: | + rm -rf .papi + npx papi add devnet -w ws://localhost:9944 + + - name: Install shared dependencies + working-directory: e2e/shared + run: npm install + + - name: Install staking-tests dependencies + working-directory: e2e/staking-tests + run: npm install + + - name: Run staking tests + working-directory: e2e/staking-tests + run: npm test From 5544301a22029aa4d08d48636268ed74980ead64 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Fri, 20 Feb 2026 14:51:37 +0300 Subject: [PATCH 08/20] Add stake limit test --- e2e/shared/staking.ts | 20 +++++ .../test/add-stake-limit.test.ts | 76 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 e2e/staking-tests/test/add-stake-limit.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 47fb48dd8a..212aa979fa 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -20,6 +20,26 @@ export async function addStake( await waitForTransactionWithRetry(api, tx, signer, "add_stake"); } +export async function addStakeLimit( + api: TypedApi, + netuid: number, + hotkeyAddress: string, + amount: bigint, + limitPrice: bigint, + allowPartial: boolean, + coldkey: KeyPair +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.add_stake_limit({ + netuid: netuid, + hotkey: hotkeyAddress, + amount_staked: amount, + limit_price: limitPrice, + allow_partial: allowPartial, + }); + await waitForTransactionWithRetry(api, tx, signer, "add_stake_limit"); +} + export async function getStake( api: TypedApi, hotkeyAddress: string, diff --git a/e2e/staking-tests/test/add-stake-limit.test.ts b/e2e/staking-tests/test/add-stake-limit.test.ts new file mode 100644 index 0000000000..508f56d45b --- /dev/null +++ b/e2e/staking-tests/test/add-stake-limit.test.ts @@ -0,0 +1,76 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStakeLimit, + getStake, + tao, + log, +} from "shared"; + +describe("▶ add_stake_limit extrinsic", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + let netuid: number; + + before(async () => { + const api = await getDevnetApi(); + + // Fund accounts + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + + // Create subnet and register hotkey + netuid = await addNewSubnetwork(api, hotkey, coldkey); + await burnedRegister(api, netuid, hotkeyAddress, coldkey); + await startCall(api, netuid, coldkey); + }); + + it("should add stake with price limit (allow partial)", async () => { + const api = await getDevnetApi(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + // Get initial stake + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + + // Add stake with limit price (1 TAO per Alpha) and allow partial fills + const stakeAmount = tao(100); + const limitPrice = tao(1); // 1 TAO per Alpha (1e9 RAO) + await addStakeLimit(api, netuid, hotkeyAddress, stakeAmount, limitPrice, true, coldkey); + + // Verify stake increased + const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + assert.ok(stakeAfter > stakeBefore, `Stake should increase: before=${stakeBefore}, after=${stakeAfter}`); + + log.info("✅ Successfully added stake with limit (allow partial)."); + }); + + it("should add stake with price limit (fill or kill)", async () => { + const api = await getDevnetApi(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + // Get initial stake + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + + // Add stake with high limit price (fill or kill mode) + const stakeAmount = tao(50); + const limitPrice = tao(10); // High limit price to ensure full fill + await addStakeLimit(api, netuid, hotkeyAddress, stakeAmount, limitPrice, false, coldkey); + + // Verify stake increased + const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + assert.ok(stakeAfter > stakeBefore, `Stake should increase: before=${stakeBefore}, after=${stakeAfter}`); + + log.info("✅ Successfully added stake with limit (fill or kill)."); + }); +}); From 08b994d5db08226535aa3a2b55621e00781aaa80 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Fri, 20 Feb 2026 15:01:02 +0300 Subject: [PATCH 09/20] Refactor params --- e2e/shared/staking.ts | 22 +++++++++---------- .../test/add-stake-limit.test.ts | 4 ++-- e2e/staking-tests/test/add-stake.test.ts | 2 +- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 212aa979fa..a2aad3c6a3 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -6,15 +6,15 @@ import { waitForTransactionWithRetry } from "./transactions.js"; export async function addStake( api: TypedApi, + coldkey: KeyPair, + hotkey: string, netuid: number, - hotkeyAddress: string, - amount: bigint, - coldkey: KeyPair + amount: bigint ): Promise { const signer = getSignerFromKeypair(coldkey); const tx = api.tx.SubtensorModule.add_stake({ + hotkey: hotkey, netuid: netuid, - hotkey: hotkeyAddress, amount_staked: amount, }); await waitForTransactionWithRetry(api, tx, signer, "add_stake"); @@ -22,17 +22,17 @@ export async function addStake( export async function addStakeLimit( api: TypedApi, + coldkey: KeyPair, + hotkey: string, netuid: number, - hotkeyAddress: string, amount: bigint, limitPrice: bigint, - allowPartial: boolean, - coldkey: KeyPair + allowPartial: boolean ): Promise { const signer = getSignerFromKeypair(coldkey); const tx = api.tx.SubtensorModule.add_stake_limit({ + hotkey: hotkey, netuid: netuid, - hotkey: hotkeyAddress, amount_staked: amount, limit_price: limitPrice, allow_partial: allowPartial, @@ -42,9 +42,9 @@ export async function addStakeLimit( export async function getStake( api: TypedApi, - hotkeyAddress: string, - coldkeyAddress: string, + hotkey: string, + coldkey: string, netuid: number ): Promise { - return await api.query.SubtensorModule.Alpha.getValue(hotkeyAddress, coldkeyAddress, netuid); + return await api.query.SubtensorModule.Alpha.getValue(hotkey, coldkey, netuid); } diff --git a/e2e/staking-tests/test/add-stake-limit.test.ts b/e2e/staking-tests/test/add-stake-limit.test.ts index 508f56d45b..6b64f7581c 100644 --- a/e2e/staking-tests/test/add-stake-limit.test.ts +++ b/e2e/staking-tests/test/add-stake-limit.test.ts @@ -45,7 +45,7 @@ describe("▶ add_stake_limit extrinsic", () => { // Add stake with limit price (1 TAO per Alpha) and allow partial fills const stakeAmount = tao(100); const limitPrice = tao(1); // 1 TAO per Alpha (1e9 RAO) - await addStakeLimit(api, netuid, hotkeyAddress, stakeAmount, limitPrice, true, coldkey); + await addStakeLimit(api, coldkey, hotkeyAddress, netuid, stakeAmount, limitPrice, true); // Verify stake increased const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); @@ -65,7 +65,7 @@ describe("▶ add_stake_limit extrinsic", () => { // Add stake with high limit price (fill or kill mode) const stakeAmount = tao(50); const limitPrice = tao(10); // High limit price to ensure full fill - await addStakeLimit(api, netuid, hotkeyAddress, stakeAmount, limitPrice, false, coldkey); + await addStakeLimit(api, coldkey, hotkeyAddress, netuid, stakeAmount, limitPrice, false); // Verify stake increased const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); diff --git a/e2e/staking-tests/test/add-stake.test.ts b/e2e/staking-tests/test/add-stake.test.ts index 6b23cff70c..3891ad69b3 100644 --- a/e2e/staking-tests/test/add-stake.test.ts +++ b/e2e/staking-tests/test/add-stake.test.ts @@ -44,7 +44,7 @@ describe("▶ add_stake extrinsic", () => { // Add stake const stakeAmount = tao(100); - await addStake(api, netuid, hotkeyAddress, stakeAmount, coldkey); + await addStake(api, coldkey, hotkeyAddress, netuid, stakeAmount); // Verify stake increased const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); From b495876c18993fccebedde68dd61d61e48198b78 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Fri, 20 Feb 2026 18:59:01 +0300 Subject: [PATCH 10/20] Refactor add_stake_* tests 2 3 4 7 --- e2e/staking-tests/package.json | 2 +- .../test/add-stake-limit.test.ts | 26 ++++++------------- e2e/staking-tests/test/add-stake.test.ts | 12 ++------- 3 files changed, 11 insertions(+), 29 deletions(-) diff --git a/e2e/staking-tests/package.json b/e2e/staking-tests/package.json index d3b6d2ed9c..d8652ce6e3 100644 --- a/e2e/staking-tests/package.json +++ b/e2e/staking-tests/package.json @@ -4,7 +4,7 @@ "type": "module", "license": "ISC", "scripts": { - "test": "tsx node_modules/mocha/bin/mocha.js --timeout 999999 --retries 3 --file setup.ts --extension ts \"test/**/*.ts\"" + "test": "tsx node_modules/mocha/bin/mocha.js --timeout 999999 --retries 1 --file setup.ts --extension ts \"test/**/*.ts\"" }, "dependencies": { "shared": "../shared", diff --git a/e2e/staking-tests/test/add-stake-limit.test.ts b/e2e/staking-tests/test/add-stake-limit.test.ts index 6b64f7581c..b0be3c714f 100644 --- a/e2e/staking-tests/test/add-stake-limit.test.ts +++ b/e2e/staking-tests/test/add-stake-limit.test.ts @@ -16,35 +16,27 @@ import { describe("▶ add_stake_limit extrinsic", () => { const hotkey = getRandomSubstrateKeypair(); const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); let netuid: number; before(async () => { const api = await getDevnetApi(); - - // Fund accounts - const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); - const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); - await forceSetBalance(api, hotkeyAddress); await forceSetBalance(api, coldkeyAddress); - - // Create subnet and register hotkey netuid = await addNewSubnetwork(api, hotkey, coldkey); - await burnedRegister(api, netuid, hotkeyAddress, coldkey); await startCall(api, netuid, coldkey); }); it("should add stake with price limit (allow partial)", async () => { const api = await getDevnetApi(); - const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); - const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); // Get initial stake const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); - // Add stake with limit price (1 TAO per Alpha) and allow partial fills - const stakeAmount = tao(100); - const limitPrice = tao(1); // 1 TAO per Alpha (1e9 RAO) + // Add stake with limit price and allow partial fills, limit_price is MAX TAO per Alpha willing to pay. + const stakeAmount = tao(44); + const limitPrice = tao(6); await addStakeLimit(api, coldkey, hotkeyAddress, netuid, stakeAmount, limitPrice, true); // Verify stake increased @@ -56,15 +48,13 @@ describe("▶ add_stake_limit extrinsic", () => { it("should add stake with price limit (fill or kill)", async () => { const api = await getDevnetApi(); - const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); - const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); // Get initial stake const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); - // Add stake with high limit price (fill or kill mode) - const stakeAmount = tao(50); - const limitPrice = tao(10); // High limit price to ensure full fill + // Add stake with limit price (fill or kill mode), limit_price is MAX TAO per Alpha willing to pay + const stakeAmount = tao(44); + const limitPrice = tao(6); await addStakeLimit(api, coldkey, hotkeyAddress, netuid, stakeAmount, limitPrice, false); // Verify stake increased diff --git a/e2e/staking-tests/test/add-stake.test.ts b/e2e/staking-tests/test/add-stake.test.ts index 3891ad69b3..c3d0e368ba 100644 --- a/e2e/staking-tests/test/add-stake.test.ts +++ b/e2e/staking-tests/test/add-stake.test.ts @@ -16,28 +16,20 @@ import { describe("▶ add_stake extrinsic", () => { const hotkey = getRandomSubstrateKeypair(); const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); let netuid: number; before(async () => { const api = await getDevnetApi(); - - // Fund accounts - const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); - const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); - await forceSetBalance(api, hotkeyAddress); await forceSetBalance(api, coldkeyAddress); - - // Create subnet and register hotkey netuid = await addNewSubnetwork(api, hotkey, coldkey); - await burnedRegister(api, netuid, hotkeyAddress, coldkey); await startCall(api, netuid, coldkey); }); it("should add stake to a hotkey", async () => { const api = await getDevnetApi(); - const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); - const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); // Get initial stake const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); From af7b9bf8f11bb5f54b1ad0600142a76626b07574 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Fri, 20 Feb 2026 18:59:29 +0300 Subject: [PATCH 11/20] Add remove_stake_* tests --- e2e/shared/balance.ts | 5 ++ e2e/shared/staking.ts | 36 +++++++++ e2e/shared/transactions.ts | 9 ++- .../test/remove-stake-limit.test.ts | 80 +++++++++++++++++++ e2e/staking-tests/test/remove-stake.test.ts | 54 +++++++++++++ 5 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 e2e/staking-tests/test/remove-stake-limit.test.ts create mode 100644 e2e/staking-tests/test/remove-stake.test.ts diff --git a/e2e/shared/balance.ts b/e2e/shared/balance.ts index 7d93815629..e0a435a00f 100644 --- a/e2e/shared/balance.ts +++ b/e2e/shared/balance.ts @@ -9,6 +9,11 @@ export function tao(value: number): bigint { return TAO * BigInt(value); } +export async function getBalance(api: TypedApi, ss58Address: string): Promise { + const account = await api.query.System.Account.getValue(ss58Address); + return account.data.free; +} + export async function forceSetBalance( api: TypedApi, ss58Address: string, diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index a2aad3c6a3..5d8a512de5 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -40,6 +40,42 @@ export async function addStakeLimit( await waitForTransactionWithRetry(api, tx, signer, "add_stake_limit"); } +export async function removeStake( + api: TypedApi, + coldkey: KeyPair, + hotkey: string, + netuid: number, + amount: bigint +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.remove_stake({ + hotkey: hotkey, + netuid: netuid, + amount_unstaked: amount, + }); + await waitForTransactionWithRetry(api, tx, signer, "remove_stake"); +} + +export async function removeStakeLimit( + api: TypedApi, + coldkey: KeyPair, + hotkey: string, + netuid: number, + amount: bigint, + limitPrice: bigint, + allowPartial: boolean +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.remove_stake_limit({ + hotkey: hotkey, + netuid: netuid, + amount_unstaked: amount, + limit_price: limitPrice, + allow_partial: allowPartial, + }); + await waitForTransactionWithRetry(api, tx, signer, "remove_stake_limit"); +} + export async function getStake( api: TypedApi, hotkey: string, diff --git a/e2e/shared/transactions.ts b/e2e/shared/transactions.ts index cbf1884328..4b052682e5 100644 --- a/e2e/shared/transactions.ts +++ b/e2e/shared/transactions.ts @@ -9,7 +9,7 @@ export async function waitForTransactionWithRetry( tx: Transaction<{}, string, string, void>, signer: PolkadotSigner, label: string, - maxRetries = 5 + maxRetries = 1 ): Promise { let success = false; let retries = 0; @@ -46,9 +46,12 @@ async function waitForTransactionCompletion( subscription.unsubscribe(); clearTimeout(timeoutId); if (!value.ok) { - log.tx(label, `dispatch error: ${value.dispatchError}`); + const errorStr = JSON.stringify(value.dispatchError, null, 2); + log.tx(label, `dispatch error: ${errorStr}`); + reject(new Error(`[${label}] dispatch error: ${errorStr}`)); + } else { + resolve(); } - resolve(); } }, error(err) { diff --git a/e2e/staking-tests/test/remove-stake-limit.test.ts b/e2e/staking-tests/test/remove-stake-limit.test.ts new file mode 100644 index 0000000000..d13462d2bb --- /dev/null +++ b/e2e/staking-tests/test/remove-stake-limit.test.ts @@ -0,0 +1,80 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + getBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + removeStakeLimit, + getStake, + tao, + log, +} from "shared"; + +describe("▶ remove_stake_limit extrinsic", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + let netuid: number; + + before(async () => { + const api = await getDevnetApi(); + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + netuid = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid, coldkey); + }); + + it("should remove stake with price limit (allow partial)", async () => { + const api = await getDevnetApi(); + + // Add stake first (100 TAO like benchmark) + await addStake(api, coldkey, hotkeyAddress, netuid, tao(100)); + + // Get initial stake and balance + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceBefore = await getBalance(api, coldkeyAddress); + log.info(`Stake before: ${stakeBefore}, Balance before: ${balanceBefore}`); + assert.ok(stakeBefore > 0n, "Should have stake before removal"); + + // Remove stake with limit price and allow partial fills + const unstakeAmount = tao(30); + const limitPrice = tao(1); + await removeStakeLimit(api, coldkey, hotkeyAddress, netuid, unstakeAmount, limitPrice, true); + + // Verify balance increased (received TAO from unstaking) + const balanceAfter = await getBalance(api, coldkeyAddress); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully removed stake with limit (allow partial)."); + }); + + it("should remove stake with price limit (fill or kill)", async () => { + const api = await getDevnetApi(); + + // Add stake first (100 TAO like benchmark) + await addStake(api, coldkey, hotkeyAddress, netuid, tao(100)); + + // Get initial stake and balance + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceBefore = await getBalance(api, coldkeyAddress); + log.info(`Stake before: ${stakeBefore}, Balance before: ${balanceBefore}`); + assert.ok(stakeBefore > 0n, "Should have stake before removal"); + + // Remove stake with limit price (fill or kill mode) + const unstakeAmount = tao(30); + const limitPrice = tao(1); + await removeStakeLimit(api, coldkey, hotkeyAddress, netuid, unstakeAmount, limitPrice, false); + + // Verify balance increased (received TAO from unstaking) + const balanceAfter = await getBalance(api, coldkeyAddress); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully removed stake with limit (fill or kill)."); + }); +}); diff --git a/e2e/staking-tests/test/remove-stake.test.ts b/e2e/staking-tests/test/remove-stake.test.ts new file mode 100644 index 0000000000..1ff90f2e2e --- /dev/null +++ b/e2e/staking-tests/test/remove-stake.test.ts @@ -0,0 +1,54 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + getBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + removeStake, + getStake, + tao, + log, +} from "shared"; + +describe("▶ remove_stake extrinsic", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + let netuid: number; + + before(async () => { + const api = await getDevnetApi(); + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + netuid = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid, coldkey); + }); + + it("should remove stake from a hotkey", async () => { + const api = await getDevnetApi(); + + // Add stake first + await addStake(api, coldkey, hotkeyAddress, netuid, tao(200)); + + // Get initial stake and balance + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceBefore = await getBalance(api, coldkeyAddress); + assert.ok(stakeBefore > 0n, "Should have stake before removal"); + + // Remove stake (amount is in alpha units) + const unstakeAmount = stakeBefore / 2n; + await removeStake(api, coldkey, hotkeyAddress, netuid, unstakeAmount); + + // Verify balance increased (received TAO from unstaking) + const balanceAfter = await getBalance(api, coldkeyAddress); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully removed stake."); + }); +}); From 10bcff28f0d85a92373bff35eb1e64697d8c3677 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 14:44:13 +0300 Subject: [PATCH 12/20] Add unstake_all test. --- e2e/shared/staking.ts | 12 ++++ e2e/staking-tests/test/unstake-all.test.ts | 70 ++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 e2e/staking-tests/test/unstake-all.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 5d8a512de5..e9ea3ed56b 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -76,6 +76,18 @@ export async function removeStakeLimit( await waitForTransactionWithRetry(api, tx, signer, "remove_stake_limit"); } +export async function unstakeAll( + api: TypedApi, + coldkey: KeyPair, + hotkey: string +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.unstake_all({ + hotkey: hotkey, + }); + await waitForTransactionWithRetry(api, tx, signer, "unstake_all"); +} + export async function getStake( api: TypedApi, hotkey: string, diff --git a/e2e/staking-tests/test/unstake-all.test.ts b/e2e/staking-tests/test/unstake-all.test.ts new file mode 100644 index 0000000000..c360581ab1 --- /dev/null +++ b/e2e/staking-tests/test/unstake-all.test.ts @@ -0,0 +1,70 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + getBalance, + addNewSubnetwork, + startCall, + addStake, + unstakeAll, + getStake, + tao, + log, +} from "shared"; + +describe("▶ unstake_all extrinsic", () => { + it("should unstake all from a hotkey across all subnets", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + const hotkey2 = getRandomSubstrateKeypair(); + const hotkeyAddress2 = convertPublicKeyToSs58(hotkey2.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + await forceSetBalance(api, hotkeyAddress2); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, coldkey); + await startCall(api, netuid2, coldkey); + + // Add stake to both subnets + await addStake(api, coldkey, hotkeyAddress, netuid1, tao(100)); + await addStake(api, coldkey, hotkeyAddress, netuid2, tao(50)); + + // Verify stake was added to both subnets + const stake1Before = await getStake(api, hotkeyAddress, coldkeyAddress, netuid1); + const stake2Before = await getStake(api, hotkeyAddress, coldkeyAddress, netuid2); + const balanceBefore = await getBalance(api, coldkeyAddress); + + assert.ok(stake1Before > 0n, "Should have stake in subnet 1 before unstake_all"); + assert.ok(stake2Before > 0n, "Should have stake in subnet 2 before unstake_all"); + log.info(`Stake1 before: ${stake1Before}, Stake2 before: ${stake2Before}, Balance before: ${balanceBefore}`); + + // Unstake all + await unstakeAll(api, coldkey, hotkeyAddress); + + // Verify stakes are removed from both subnets and balance increased + const stake1After = await getStake(api, hotkeyAddress, coldkeyAddress, netuid1); + const stake2After = await getStake(api, hotkeyAddress, coldkeyAddress, netuid2); + const balanceAfter = await getBalance(api, coldkeyAddress); + + log.info(`Stake1 after: ${stake1After}, Stake2 after: ${stake2After}, Balance after: ${balanceAfter}`); + + assert.strictEqual(stake1After, 0n, `Stake1 should be zero after unstake_all, got ${stake1After}`); + assert.strictEqual(stake2After, 0n, `Stake2 should be zero after unstake_all, got ${stake2After}`); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully unstaked all from multiple subnets."); + }); +}); From 43f299801e5deeaa442678ed32e0c215e6d91157 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 14:44:32 +0300 Subject: [PATCH 13/20] Add transfer_stake tests --- e2e/shared/staking.ts | 20 +++ e2e/staking-tests/test/transfer-stake.test.ts | 129 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 e2e/staking-tests/test/transfer-stake.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index e9ea3ed56b..ca5a623bc6 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -96,3 +96,23 @@ export async function getStake( ): Promise { return await api.query.SubtensorModule.Alpha.getValue(hotkey, coldkey, netuid); } + +export async function transferStake( + api: TypedApi, + originColdkey: KeyPair, + destinationColdkey: string, + hotkey: string, + originNetuid: number, + destinationNetuid: number, + amount: bigint +): Promise { + const signer = getSignerFromKeypair(originColdkey); + const tx = api.tx.SubtensorModule.transfer_stake({ + destination_coldkey: destinationColdkey, + hotkey: hotkey, + origin_netuid: originNetuid, + destination_netuid: destinationNetuid, + alpha_amount: amount, + }); + await waitForTransactionWithRetry(api, tx, signer, "transfer_stake"); +} diff --git a/e2e/staking-tests/test/transfer-stake.test.ts b/e2e/staking-tests/test/transfer-stake.test.ts new file mode 100644 index 0000000000..4585fe21ae --- /dev/null +++ b/e2e/staking-tests/test/transfer-stake.test.ts @@ -0,0 +1,129 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + startCall, + addStake, + transferStake, + getStake, + tao, + log, +} from "shared"; + +describe("▶ transfer_stake extrinsic", () => { + it("should transfer stake to another coldkey across subnets", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const originColdkey = getRandomSubstrateKeypair(); + const destinationColdkey = getRandomSubstrateKeypair(); + const hotkey1Address = convertPublicKeyToSs58(hotkey1.publicKey); + const hotkey2Address = convertPublicKeyToSs58(hotkey2.publicKey); + const originColdkeyAddress = convertPublicKeyToSs58(originColdkey.publicKey); + const destinationColdkeyAddress = convertPublicKeyToSs58(destinationColdkey.publicKey); + + await forceSetBalance(api, hotkey1Address); + await forceSetBalance(api, hotkey2Address); + await forceSetBalance(api, originColdkeyAddress); + await forceSetBalance(api, destinationColdkeyAddress); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey1, originColdkey); + await startCall(api, netuid1, originColdkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, originColdkey); + await startCall(api, netuid2, originColdkey); + + // Add stake from origin coldkey on first subnet + await addStake(api, originColdkey, hotkey1Address, netuid1, tao(200)); + + // Get initial stakes + const originStakeBefore = await getStake(api, hotkey1Address, originColdkeyAddress, netuid1); + const destStakeBefore = await getStake(api, hotkey1Address, destinationColdkeyAddress, netuid2); + assert.ok(originStakeBefore > 0n, "Origin should have stake before transfer"); + + log.info(`Origin stake (netuid1) before: ${originStakeBefore}, Destination stake (netuid2) before: ${destStakeBefore}`); + + // Transfer stake to destination coldkey on a different subnet + const transferAmount = originStakeBefore / 2n; + await transferStake( + api, + originColdkey, + destinationColdkeyAddress, + hotkey1Address, + netuid1, + netuid2, + transferAmount + ); + + // Verify stakes changed + const originStakeAfter = await getStake(api, hotkey1Address, originColdkeyAddress, netuid1); + const destStakeAfter = await getStake(api, hotkey1Address, destinationColdkeyAddress, netuid2); + + log.info(`Origin stake (netuid1) after: ${originStakeAfter}, Destination stake (netuid2) after: ${destStakeAfter}`); + + assert.ok(originStakeAfter < originStakeBefore, `Origin stake should decrease: before=${originStakeBefore}, after=${originStakeAfter}`); + assert.ok(destStakeAfter > destStakeBefore, `Destination stake should increase: before=${destStakeBefore}, after=${destStakeAfter}`); + + log.info("✅ Successfully transferred stake to another coldkey across subnets."); + }); + + it("should transfer stake to another coldkey", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey = getRandomSubstrateKeypair(); + const originColdkey = getRandomSubstrateKeypair(); + const destinationColdkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const originColdkeyAddress = convertPublicKeyToSs58(originColdkey.publicKey); + const destinationColdkeyAddress = convertPublicKeyToSs58(destinationColdkey.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, originColdkeyAddress); + await forceSetBalance(api, destinationColdkeyAddress); + + // Create subnet + const netuid = await addNewSubnetwork(api, hotkey, originColdkey); + await startCall(api, netuid, originColdkey); + + // Add stake from origin coldkey + const stakeAmount = tao(100); + await addStake(api, originColdkey, hotkeyAddress, netuid, stakeAmount); + + // Get initial stake + const originStakeBefore = await getStake(api, hotkeyAddress, originColdkeyAddress, netuid); + assert.ok(originStakeBefore > 0n, "Origin should have stake before transfer"); + + log.info(`Origin stake before: ${originStakeBefore}`); + + // Transfer stake to destination coldkey + // Use the known staked amount instead of queried value (Alpha storage returns inflated values) + await transferStake( + api, + originColdkey, + destinationColdkeyAddress, + hotkeyAddress, + netuid, + netuid, + stakeAmount + ); + + // Verify destination received stake + const originStakeAfter = await getStake(api, hotkeyAddress, originColdkeyAddress, netuid); + const destStakeAfter = await getStake(api, hotkeyAddress, destinationColdkeyAddress, netuid); + + log.info(`Origin stake after: ${originStakeAfter}, Destination stake after: ${destStakeAfter}`); + + assert.ok(originStakeAfter < originStakeBefore, `Origin stake should decrease after transfer`); + assert.ok(destStakeAfter > 0n, `Destination stake should be non-zero after transfer`); + + log.info("✅ Successfully transferred stake to another coldkey."); + }); +}); From b7e0972d1315c680fa7c4521fe65fc62c99c2654 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 14:44:58 +0300 Subject: [PATCH 14/20] Add move-stake tests --- e2e/shared/staking.ts | 20 ++++ e2e/staking-tests/test/move-stake.test.ts | 130 ++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 e2e/staking-tests/test/move-stake.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index ca5a623bc6..8e188a92d7 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -116,3 +116,23 @@ export async function transferStake( }); await waitForTransactionWithRetry(api, tx, signer, "transfer_stake"); } + +export async function moveStake( + api: TypedApi, + coldkey: KeyPair, + originHotkey: string, + destinationHotkey: string, + originNetuid: number, + destinationNetuid: number, + amount: bigint +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.move_stake({ + origin_hotkey: originHotkey, + destination_hotkey: destinationHotkey, + origin_netuid: originNetuid, + destination_netuid: destinationNetuid, + alpha_amount: amount, + }); + await waitForTransactionWithRetry(api, tx, signer, "move_stake"); +} diff --git a/e2e/staking-tests/test/move-stake.test.ts b/e2e/staking-tests/test/move-stake.test.ts new file mode 100644 index 0000000000..178748dfb9 --- /dev/null +++ b/e2e/staking-tests/test/move-stake.test.ts @@ -0,0 +1,130 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + moveStake, + getStake, + tao, + log, +} from "shared"; + +describe("▶ move_stake extrinsic", () => { + it("should move stake to another hotkey across subnets", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const originHotkey = getRandomSubstrateKeypair(); + const destinationHotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const originHotkeyAddress = convertPublicKeyToSs58(originHotkey.publicKey); + const destinationHotkeyAddress = convertPublicKeyToSs58(destinationHotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, originHotkeyAddress); + await forceSetBalance(api, destinationHotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + + // Create first subnet with origin hotkey + const netuid1 = await addNewSubnetwork(api, originHotkey, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet with destination hotkey + const netuid2 = await addNewSubnetwork(api, destinationHotkey, coldkey); + await startCall(api, netuid2, coldkey); + + // Add stake to origin hotkey on first subnet + await addStake(api, coldkey, originHotkeyAddress, netuid1, tao(200)); + + // Get initial stakes + const originStakeBefore = await getStake(api, originHotkeyAddress, coldkeyAddress, netuid1); + const destStakeBefore = await getStake(api, destinationHotkeyAddress, coldkeyAddress, netuid2); + assert.ok(originStakeBefore > 0n, "Origin hotkey should have stake before move"); + + log.info(`Origin stake (netuid1) before: ${originStakeBefore}, Destination stake (netuid2) before: ${destStakeBefore}`); + + // Move stake to destination hotkey on different subnet + const moveAmount = originStakeBefore / 2n; + await moveStake( + api, + coldkey, + originHotkeyAddress, + destinationHotkeyAddress, + netuid1, + netuid2, + moveAmount + ); + + // Verify stakes changed + const originStakeAfter = await getStake(api, originHotkeyAddress, coldkeyAddress, netuid1); + const destStakeAfter = await getStake(api, destinationHotkeyAddress, coldkeyAddress, netuid2); + + log.info(`Origin stake (netuid1) after: ${originStakeAfter}, Destination stake (netuid2) after: ${destStakeAfter}`); + + assert.ok(originStakeAfter < originStakeBefore, `Origin stake should decrease: before=${originStakeBefore}, after=${originStakeAfter}`); + assert.ok(destStakeAfter > destStakeBefore, `Destination stake should increase: before=${destStakeBefore}, after=${destStakeAfter}`); + + log.info("✅ Successfully moved stake to another hotkey across subnets."); + }); + + it("should move stake to another hotkey on the same subnet", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const originHotkey = getRandomSubstrateKeypair(); + const destinationHotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const originHotkeyAddress = convertPublicKeyToSs58(originHotkey.publicKey); + const destinationHotkeyAddress = convertPublicKeyToSs58(destinationHotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, originHotkeyAddress); + await forceSetBalance(api, destinationHotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + + // Create subnet with origin hotkey + const netuid = await addNewSubnetwork(api, originHotkey, coldkey); + await startCall(api, netuid, coldkey); + + // Register destination hotkey on the same subnet + await burnedRegister(api, netuid, destinationHotkeyAddress, coldkey); + + // Add stake to origin hotkey + await addStake(api, coldkey, originHotkeyAddress, netuid, tao(200)); + + // Get initial stakes + const originStakeBefore = await getStake(api, originHotkeyAddress, coldkeyAddress, netuid); + const destStakeBefore = await getStake(api, destinationHotkeyAddress, coldkeyAddress, netuid); + assert.ok(originStakeBefore > 0n, "Origin hotkey should have stake before move"); + + log.info(`Origin stake before: ${originStakeBefore}, Destination stake before: ${destStakeBefore}`); + + // Move stake to destination hotkey on the same subnet + const moveAmount = originStakeBefore / 2n; + await moveStake( + api, + coldkey, + originHotkeyAddress, + destinationHotkeyAddress, + netuid, + netuid, + moveAmount + ); + + // Verify stakes changed + const originStakeAfter = await getStake(api, originHotkeyAddress, coldkeyAddress, netuid); + const destStakeAfter = await getStake(api, destinationHotkeyAddress, coldkeyAddress, netuid); + + log.info(`Origin stake after: ${originStakeAfter}, Destination stake after: ${destStakeAfter}`); + + assert.ok(originStakeAfter < originStakeBefore, `Origin stake should decrease: before=${originStakeBefore}, after=${originStakeAfter}`); + assert.ok(destStakeAfter > destStakeBefore, `Destination stake should increase: before=${destStakeBefore}, after=${destStakeAfter}`); + + log.info("✅ Successfully moved stake to another hotkey on the same subnet."); + }); +}); From 03b74dbb1b68498a65f45c0edabe6aad0fbc76c2 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 14:45:13 +0300 Subject: [PATCH 15/20] Add remove_stake_full_limit stake --- e2e/shared/staking.ts | 16 ++++ .../test/remove-stake-full-limit.test.ts | 85 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 e2e/staking-tests/test/remove-stake-full-limit.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 8e188a92d7..2a4e0bda12 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -76,6 +76,22 @@ export async function removeStakeLimit( await waitForTransactionWithRetry(api, tx, signer, "remove_stake_limit"); } +export async function removeStakeFullLimit( + api: TypedApi, + coldkey: KeyPair, + hotkey: string, + netuid: number, + limitPrice: bigint | undefined +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.remove_stake_full_limit({ + hotkey: hotkey, + netuid: netuid, + limit_price: limitPrice, + }); + await waitForTransactionWithRetry(api, tx, signer, "remove_stake_full_limit"); +} + export async function unstakeAll( api: TypedApi, coldkey: KeyPair, diff --git a/e2e/staking-tests/test/remove-stake-full-limit.test.ts b/e2e/staking-tests/test/remove-stake-full-limit.test.ts new file mode 100644 index 0000000000..ccf2b59342 --- /dev/null +++ b/e2e/staking-tests/test/remove-stake-full-limit.test.ts @@ -0,0 +1,85 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + getBalance, + addNewSubnetwork, + startCall, + addStake, + removeStakeFullLimit, + getStake, + tao, + log, +} from "shared"; + +describe("▶ remove_stake_full_limit extrinsic", () => { + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + let netuid: number; + + before(async () => { + const api = await getDevnetApi(); + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + netuid = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid, coldkey); + }); + + it("should remove all stake with price limit", async () => { + const api = await getDevnetApi(); + + // Add stake first + await addStake(api, coldkey, hotkeyAddress, netuid, tao(100)); + + // Get initial stake and balance + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceBefore = await getBalance(api, coldkeyAddress); + log.info(`Stake before: ${stakeBefore}, Balance before: ${balanceBefore}`); + assert.ok(stakeBefore > 0n, "Should have stake before removal"); + + // Remove all stake with a reasonable limit price (low limit to avoid slippage rejection) + // Using a low limit price (0.09 TAO per alpha) allows the transaction to succeed + const limitPrice = tao(1) / 10n; // 0.1 TAO + await removeStakeFullLimit(api, coldkey, hotkeyAddress, netuid, limitPrice); + + // Verify stake is zero + const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceAfter = await getBalance(api, coldkeyAddress); + log.info(`Stake after: ${stakeAfter}, Balance after: ${balanceAfter}`); + + assert.strictEqual(stakeAfter, 0n, `Stake should be zero after full removal, got ${stakeAfter}`); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully removed all stake with price limit."); + }); + + it("should remove all stake without price limit", async () => { + const api = await getDevnetApi(); + + // Add stake first + await addStake(api, coldkey, hotkeyAddress, netuid, tao(100)); + + // Get initial stake and balance + const stakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceBefore = await getBalance(api, coldkeyAddress); + log.info(`Stake before: ${stakeBefore}, Balance before: ${balanceBefore}`); + assert.ok(stakeBefore > 0n, "Should have stake before removal"); + + // Remove all stake without limit price (undefined = no slippage protection) + await removeStakeFullLimit(api, coldkey, hotkeyAddress, netuid, undefined); + + // Verify stake is zero + const stakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, netuid); + const balanceAfter = await getBalance(api, coldkeyAddress); + log.info(`Stake after: ${stakeAfter}, Balance after: ${balanceAfter}`); + + assert.strictEqual(stakeAfter, 0n, `Stake should be zero after full removal, got ${stakeAfter}`); + assert.ok(balanceAfter > balanceBefore, `Balance should increase: before=${balanceBefore}, after=${balanceAfter}`); + + log.info("✅ Successfully removed all stake without price limit."); + }); +}); From 18ba4a99f0408fdf82c9a1450f9e500e878de39c Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 15:10:16 +0300 Subject: [PATCH 16/20] Add swap_stake tests. --- e2e/shared/staking.ts | 18 ++++++ e2e/staking-tests/test/swap-stake.test.ts | 67 +++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 e2e/staking-tests/test/swap-stake.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 2a4e0bda12..3dad767352 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -152,3 +152,21 @@ export async function moveStake( }); await waitForTransactionWithRetry(api, tx, signer, "move_stake"); } + +export async function swapStake( + api: TypedApi, + coldkey: KeyPair, + hotkey: string, + originNetuid: number, + destinationNetuid: number, + amount: bigint +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.swap_stake({ + hotkey: hotkey, + origin_netuid: originNetuid, + destination_netuid: destinationNetuid, + alpha_amount: amount, + }); + await waitForTransactionWithRetry(api, tx, signer, "swap_stake"); +} diff --git a/e2e/staking-tests/test/swap-stake.test.ts b/e2e/staking-tests/test/swap-stake.test.ts new file mode 100644 index 0000000000..b5d57e3856 --- /dev/null +++ b/e2e/staking-tests/test/swap-stake.test.ts @@ -0,0 +1,67 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + swapStake, + getStake, + tao, + log, +} from "shared"; + +describe("▶ swap_stake extrinsic", () => { + it("should swap full stake from one subnet to another", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkey1Address = convertPublicKeyToSs58(hotkey1.publicKey); + const hotkey2Address = convertPublicKeyToSs58(hotkey2.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkey1Address); + await forceSetBalance(api, hotkey2Address); + await forceSetBalance(api, coldkeyAddress); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey1, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, coldkey); + await startCall(api, netuid2, coldkey); + + // Register hotkey1 on subnet2 so we can swap stake there + await burnedRegister(api, netuid2, hotkey1Address, coldkey); + + // Add stake to hotkey1 on subnet1 + await addStake(api, coldkey, hotkey1Address, netuid1, tao(100)); + + // Get initial stake + const stake1Before = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + assert.ok(stake1Before > 0n, "Should have stake on subnet1 before swap"); + + log.info(`Stake on netuid1 before: ${stake1Before}`); + + // Swap full stake from subnet1 to subnet2 + await swapStake(api, coldkey, hotkey1Address, netuid1, netuid2, stake1Before); + + // Verify stakes changed + const stake1After = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + const stake2After = await getStake(api, hotkey1Address, coldkeyAddress, netuid2); + + log.info(`Stake on netuid1 after: ${stake1After}, Stake on netuid2 after: ${stake2After}`); + + assert.strictEqual(stake1After, 0n, `Stake on subnet1 should be zero after full swap, got ${stake1After}`); + assert.ok(stake2After > 0n, `Stake on subnet2 should be non-zero after swap`); + + log.info("✅ Successfully swapped full stake from one subnet to another."); + }); +}); From 13248f01eff6ffa4cde61995f7b4247ef02d6ba0 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 16:10:41 +0300 Subject: [PATCH 17/20] Add swap_stake_limit tests. --- e2e/shared/staking.ts | 22 +++ .../test/swap-stake-limit.test.ts | 126 ++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 e2e/staking-tests/test/swap-stake-limit.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 3dad767352..86db870d65 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -170,3 +170,25 @@ export async function swapStake( }); await waitForTransactionWithRetry(api, tx, signer, "swap_stake"); } + +export async function swapStakeLimit( + api: TypedApi, + coldkey: KeyPair, + hotkey: string, + originNetuid: number, + destinationNetuid: number, + amount: bigint, + limitPrice: bigint, + allowPartial: boolean +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.swap_stake_limit({ + hotkey: hotkey, + origin_netuid: originNetuid, + destination_netuid: destinationNetuid, + alpha_amount: amount, + limit_price: limitPrice, + allow_partial: allowPartial, + }); + await waitForTransactionWithRetry(api, tx, signer, "swap_stake_limit"); +} diff --git a/e2e/staking-tests/test/swap-stake-limit.test.ts b/e2e/staking-tests/test/swap-stake-limit.test.ts new file mode 100644 index 0000000000..ebd73ee7d3 --- /dev/null +++ b/e2e/staking-tests/test/swap-stake-limit.test.ts @@ -0,0 +1,126 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + burnedRegister, + startCall, + addStake, + swapStakeLimit, + getStake, + tao, + log, +} from "shared"; + +describe("▶ swap_stake_limit extrinsic", () => { + it("should swap stake with price limit (allow partial)", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkey1Address = convertPublicKeyToSs58(hotkey1.publicKey); + const hotkey2Address = convertPublicKeyToSs58(hotkey2.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkey1Address); + await forceSetBalance(api, hotkey2Address); + await forceSetBalance(api, coldkeyAddress); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey1, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, coldkey); + await startCall(api, netuid2, coldkey); + + // Register hotkey1 on subnet2 so we can swap stake there + await burnedRegister(api, netuid2, hotkey1Address, coldkey); + + // Add stake to hotkey1 on subnet1 + await addStake(api, coldkey, hotkey1Address, netuid1, tao(100)); + + // Get initial stakes + const stake1Before = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + const stake2Before = await getStake(api, hotkey1Address, coldkeyAddress, netuid2); + assert.ok(stake1Before > 0n, "Should have stake on subnet1 before swap"); + + log.info(`Stake on netuid1 before: ${stake1Before}, Stake on netuid2 before: ${stake2Before}`); + + // Swap stake with limit price (0.99 TAO relative price limit, allow partial fills) + // This limit is based on the Rust test: limit_price = 990_000_000 + const swapAmount = stake1Before / 2n; + const limitPrice = tao(1) * 99n / 100n; // 0.99 TAO + await swapStakeLimit(api, coldkey, hotkey1Address, netuid1, netuid2, swapAmount, limitPrice, true); + + // Verify stakes changed + const stake1After = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + const stake2After = await getStake(api, hotkey1Address, coldkeyAddress, netuid2); + + log.info(`Stake on netuid1 after: ${stake1After}, Stake on netuid2 after: ${stake2After}`); + + assert.ok(stake1After < stake1Before, `Stake on subnet1 should decrease: before=${stake1Before}, after=${stake1After}`); + assert.ok(stake2After > stake2Before, `Stake on subnet2 should increase: before=${stake2Before}, after=${stake2After}`); + + log.info("✅ Successfully swapped stake with price limit (allow partial)."); + }); + + it("should swap stake with price limit (fill or kill)", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey1 = getRandomSubstrateKeypair(); + const hotkey2 = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkey1Address = convertPublicKeyToSs58(hotkey1.publicKey); + const hotkey2Address = convertPublicKeyToSs58(hotkey2.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkey1Address); + await forceSetBalance(api, hotkey2Address); + await forceSetBalance(api, coldkeyAddress); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey1, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, coldkey); + await startCall(api, netuid2, coldkey); + + // Register hotkey1 on subnet2 so we can swap stake there + await burnedRegister(api, netuid2, hotkey1Address, coldkey); + + // Add stake to hotkey1 on subnet1 + await addStake(api, coldkey, hotkey1Address, netuid1, tao(100)); + + // Get initial stakes + const stake1Before = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + const stake2Before = await getStake(api, hotkey1Address, coldkeyAddress, netuid2); + assert.ok(stake1Before > 0n, "Should have stake on subnet1 before swap"); + + log.info(`Stake on netuid1 before: ${stake1Before}, Stake on netuid2 before: ${stake2Before}`); + + // Swap stake with limit price (fill or kill mode - allow_partial = false) + // Using a low limit price to allow more slippage and ensure the swap succeeds + // The limit_price is the minimum acceptable price ratio - lower = more permissive + const swapAmount = stake1Before / 2n; + const limitPrice = tao(1) / 10n; // 0.1 TAO - permissive limit to allow slippage + await swapStakeLimit(api, coldkey, hotkey1Address, netuid1, netuid2, swapAmount, limitPrice, false); + + // Verify stakes changed + const stake1After = await getStake(api, hotkey1Address, coldkeyAddress, netuid1); + const stake2After = await getStake(api, hotkey1Address, coldkeyAddress, netuid2); + + log.info(`Stake on netuid1 after: ${stake1After}, Stake on netuid2 after: ${stake2After}`); + + assert.ok(stake1After < stake1Before, `Stake on subnet1 should decrease: before=${stake1Before}, after=${stake1After}`); + assert.ok(stake2After > stake2Before, `Stake on subnet2 should increase: before=${stake2Before}, after=${stake2After}`); + + log.info("✅ Successfully swapped stake with price limit (fill or kill)."); + }); +}); From cec5b281d3f67324defcae8c5f185d183c19c7c8 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Mon, 23 Feb 2026 18:21:18 +0300 Subject: [PATCH 18/20] Add unstake_all_alpha tests. --- e2e/shared/staking.ts | 12 ++++ .../test/unstake-all-alpha.test.ts | 72 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 e2e/staking-tests/test/unstake-all-alpha.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 86db870d65..344665dbe6 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -104,6 +104,18 @@ export async function unstakeAll( await waitForTransactionWithRetry(api, tx, signer, "unstake_all"); } +export async function unstakeAllAlpha( + api: TypedApi, + coldkey: KeyPair, + hotkey: string +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.unstake_all_alpha({ + hotkey: hotkey, + }); + await waitForTransactionWithRetry(api, tx, signer, "unstake_all_alpha"); +} + export async function getStake( api: TypedApi, hotkey: string, diff --git a/e2e/staking-tests/test/unstake-all-alpha.test.ts b/e2e/staking-tests/test/unstake-all-alpha.test.ts new file mode 100644 index 0000000000..8b9d3535d5 --- /dev/null +++ b/e2e/staking-tests/test/unstake-all-alpha.test.ts @@ -0,0 +1,72 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + addNewSubnetwork, + startCall, + addStake, + unstakeAllAlpha, + getStake, + tao, + log, +} from "shared"; + +// Root subnet netuid is 0 +const ROOT_NETUID = 0; + +describe("▶ unstake_all_alpha extrinsic", () => { + it("should unstake all alpha from multiple subnets and restake to root", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + const hotkey2 = getRandomSubstrateKeypair(); + const hotkeyAddress2 = convertPublicKeyToSs58(hotkey2.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + await forceSetBalance(api, hotkeyAddress2); + + // Create first subnet + const netuid1 = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid1, coldkey); + + // Create second subnet + const netuid2 = await addNewSubnetwork(api, hotkey2, coldkey); + await startCall(api, netuid2, coldkey); + + // Add stake to both subnets (using same hotkey as in unstake_all test) + await addStake(api, coldkey, hotkeyAddress, netuid1, tao(100)); + await addStake(api, coldkey, hotkeyAddress, netuid2, tao(50)); + + // Verify stake was added to both subnets + const stake1Before = await getStake(api, hotkeyAddress, coldkeyAddress, netuid1); + const stake2Before = await getStake(api, hotkeyAddress, coldkeyAddress, netuid2); + const rootStakeBefore = await getStake(api, hotkeyAddress, coldkeyAddress, ROOT_NETUID); + + assert.ok(stake1Before > 0n, "Should have stake in subnet 1 before unstake_all_alpha"); + assert.ok(stake2Before > 0n, "Should have stake in subnet 2 before unstake_all_alpha"); + log.info(`Stake1 before: ${stake1Before}, Stake2 before: ${stake2Before}, Root stake before: ${rootStakeBefore}`); + + // Unstake all alpha - this removes stake from dynamic subnets and restakes to root + await unstakeAllAlpha(api, coldkey, hotkeyAddress); + + // Verify stakes are removed from both dynamic subnets + const stake1After = await getStake(api, hotkeyAddress, coldkeyAddress, netuid1); + const stake2After = await getStake(api, hotkeyAddress, coldkeyAddress, netuid2); + const rootStakeAfter = await getStake(api, hotkeyAddress, coldkeyAddress, ROOT_NETUID); + + log.info(`Stake1 after: ${stake1After}, Stake2 after: ${stake2After}, Root stake after: ${rootStakeAfter}`); + + assert.strictEqual(stake1After, 0n, `Stake1 should be zero after unstake_all_alpha, got ${stake1After}`); + assert.strictEqual(stake2After, 0n, `Stake2 should be zero after unstake_all_alpha, got ${stake2After}`); + assert.ok(rootStakeAfter > rootStakeBefore, `Root stake should increase: before=${rootStakeBefore}, after=${rootStakeAfter}`); + + log.info("✅ Successfully unstaked all alpha from multiple subnets to root."); + }); +}); From a648c851d0cc08191d9c4ed18c87bb53aedfd8f9 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 24 Feb 2026 15:27:05 +0300 Subject: [PATCH 19/20] Add root claim type tests --- e2e/shared/staking.ts | 43 +++++++++++ e2e/staking-tests/test/claim-root.test.ts | 89 +++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 e2e/staking-tests/test/claim-root.test.ts diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 344665dbe6..9333c94f94 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -204,3 +204,46 @@ export async function swapStakeLimit( }); await waitForTransactionWithRetry(api, tx, signer, "swap_stake_limit"); } + +export type RootClaimType = "Swap" | "Keep" | { type: "KeepSubnets"; subnets: number[] }; + +export async function getRootClaimType( + api: TypedApi, + coldkey: string +): Promise { + const result = await api.query.SubtensorModule.RootClaimType.getValue(coldkey); + if (result.type === "KeepSubnets") { + return { type: "KeepSubnets", subnets: result.value.subnets as number[] }; + } + return result.type as "Swap" | "Keep"; +} + +export async function setRootClaimType( + api: TypedApi, + coldkey: KeyPair, + claimType: RootClaimType +): Promise { + const signer = getSignerFromKeypair(coldkey); + let newRootClaimType; + if (typeof claimType === "string") { + newRootClaimType = { type: claimType, value: undefined }; + } else { + newRootClaimType = { type: "KeepSubnets", value: { subnets: claimType.subnets } }; + } + const tx = api.tx.SubtensorModule.set_root_claim_type({ + new_root_claim_type: newRootClaimType, + }); + await waitForTransactionWithRetry(api, tx, signer, "set_root_claim_type"); +} + +export async function claimRoot( + api: TypedApi, + coldkey: KeyPair, + subnets: number[] +): Promise { + const signer = getSignerFromKeypair(coldkey); + const tx = api.tx.SubtensorModule.claim_root({ + subnets: subnets, + }); + await waitForTransactionWithRetry(api, tx, signer, "claim_root"); +} diff --git a/e2e/staking-tests/test/claim-root.test.ts b/e2e/staking-tests/test/claim-root.test.ts new file mode 100644 index 0000000000..20e04fa9ff --- /dev/null +++ b/e2e/staking-tests/test/claim-root.test.ts @@ -0,0 +1,89 @@ +import * as assert from "assert"; +import { + getDevnetApi, + getRandomSubstrateKeypair, + convertPublicKeyToSs58, + forceSetBalance, + getRootClaimType, + setRootClaimType, + log, +} from "shared"; + +describe("▶ set_root_claim_type extrinsic", () => { + it("should set root claim type to Keep", async () => { + const api = await getDevnetApi(); + + const coldkey = getRandomSubstrateKeypair(); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, coldkeyAddress); + + // Check initial claim type (default is "Swap") + const claimTypeBefore = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type before: ${claimTypeBefore}`); + + // Set root claim type to Keep + await setRootClaimType(api, coldkey, "Keep"); + + // Verify claim type changed + const claimTypeAfter = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type after: ${claimTypeAfter}`); + + assert.strictEqual(claimTypeAfter, "Keep", `Expected claim type to be Keep, got ${claimTypeAfter}`); + + log.info("✅ Successfully set root claim type to Keep."); + }); + + it("should set root claim type to Swap", async () => { + const api = await getDevnetApi(); + + const coldkey = getRandomSubstrateKeypair(); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, coldkeyAddress); + + // First set to Keep so we can verify the change to Swap + await setRootClaimType(api, coldkey, "Keep"); + const claimTypeBefore = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type before: ${claimTypeBefore}`); + assert.strictEqual(claimTypeBefore, "Keep", "Should be Keep before changing to Swap"); + + // Set root claim type to Swap + await setRootClaimType(api, coldkey, "Swap"); + + // Verify claim type changed + const claimTypeAfter = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type after: ${claimTypeAfter}`); + + assert.strictEqual(claimTypeAfter, "Swap", `Expected claim type to be Swap, got ${claimTypeAfter}`); + + log.info("✅ Successfully set root claim type to Swap."); + }); + + it("should set root claim type to KeepSubnets", async () => { + const api = await getDevnetApi(); + + const coldkey = getRandomSubstrateKeypair(); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, coldkeyAddress); + + // Check initial claim type (default is "Swap") + const claimTypeBefore = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type before: ${JSON.stringify(claimTypeBefore)}`); + + // Set root claim type to KeepSubnets with specific subnets + const subnetsToKeep = [1, 2]; + await setRootClaimType(api, coldkey, { type: "KeepSubnets", subnets: subnetsToKeep }); + + // Verify claim type changed + const claimTypeAfter = await getRootClaimType(api, coldkeyAddress); + log.info(`Root claim type after: ${JSON.stringify(claimTypeAfter)}`); + + assert.strictEqual(typeof claimTypeAfter, "object", "Expected claim type to be an object"); + assert.strictEqual((claimTypeAfter as { type: string }).type, "KeepSubnets", "Expected type to be KeepSubnets"); + assert.deepStrictEqual((claimTypeAfter as { subnets: number[] }).subnets, subnetsToKeep, "Expected subnets to match"); + + log.info("✅ Successfully set root claim type to KeepSubnets."); + }); +}); From e4be0ac57de2bb553d1e37c8f2a996c3ea3461f6 Mon Sep 17 00:00:00 2001 From: Shamil Gadelshin Date: Tue, 24 Feb 2026 16:43:52 +0300 Subject: [PATCH 20/20] More root claim test. --- e2e/shared/staking.ts | 226 ++++++++++++- e2e/staking-tests/setup.ts | 12 +- e2e/staking-tests/test/claim-root.test.ts | 388 ++++++++++++++++++++++ 3 files changed, 624 insertions(+), 2 deletions(-) diff --git a/e2e/shared/staking.ts b/e2e/shared/staking.ts index 9333c94f94..2c5250ec01 100644 --- a/e2e/shared/staking.ts +++ b/e2e/shared/staking.ts @@ -1,7 +1,7 @@ import { devnet } from "@polkadot-api/descriptors"; import { TypedApi } from "polkadot-api"; import { KeyPair } from "@polkadot-labs/hdkd-helpers"; -import { getSignerFromKeypair } from "./address.js"; +import { getSignerFromKeypair, getAliceSigner } from "./address.js"; import { waitForTransactionWithRetry } from "./transactions.js"; export async function addStake( @@ -247,3 +247,227 @@ export async function claimRoot( }); await waitForTransactionWithRetry(api, tx, signer, "claim_root"); } + +export async function getNumRootClaims( + api: TypedApi +): Promise { + return await api.query.SubtensorModule.NumRootClaim.getValue(); +} + +export async function sudoSetNumRootClaims( + api: TypedApi, + newValue: bigint +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.SubtensorModule.sudo_set_num_root_claims({ + new_value: newValue, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_num_root_claims"); +} + +export async function getRootClaimThreshold( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.RootClaimableThreshold.getValue(netuid); +} + +export async function sudoSetRootClaimThreshold( + api: TypedApi, + netuid: number, + newValue: bigint +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.SubtensorModule.sudo_set_root_claim_threshold({ + netuid: netuid, + new_value: newValue, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_root_claim_threshold"); +} + +export async function getTempo( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.Tempo.getValue(netuid); +} + +export async function sudoSetTempo( + api: TypedApi, + netuid: number, + tempo: number +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_tempo({ + netuid: netuid, + tempo: tempo, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_tempo"); +} + +export async function waitForBlocks( + api: TypedApi, + numBlocks: number +): Promise { + const startBlock = await api.query.System.Number.getValue(); + const targetBlock = startBlock + numBlocks; + + while (true) { + const currentBlock = await api.query.System.Number.getValue(); + if (currentBlock >= targetBlock) { + break; + } + await new Promise((resolve) => setTimeout(resolve, 1000)); + } +} + +export async function getRootClaimable( + api: TypedApi, + hotkey: string +): Promise> { + const result = await api.query.SubtensorModule.RootClaimable.getValue(hotkey); + const claimableMap = new Map(); + for (const [netuid, amount] of result) { + claimableMap.set(netuid, amount); + } + return claimableMap; +} + +export async function getRootClaimed( + api: TypedApi, + netuid: number, + hotkey: string, + coldkey: string +): Promise { + return await api.query.SubtensorModule.RootClaimed.getValue(netuid, hotkey, coldkey); +} + +export async function isSubtokenEnabled( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.SubtokenEnabled.getValue(netuid); +} + +export async function sudoSetSubtokenEnabled( + api: TypedApi, + netuid: number, + enabled: boolean +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_subtoken_enabled({ + netuid: netuid, + subtoken_enabled: enabled, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_subtoken_enabled"); +} + +export async function isNetworkAdded( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.NetworksAdded.getValue(netuid); +} + +export async function getAdminFreezeWindow( + api: TypedApi +): Promise { + return await api.query.SubtensorModule.AdminFreezeWindow.getValue(); +} + +export async function sudoSetAdminFreezeWindow( + api: TypedApi, + window: number +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_admin_freeze_window({ + window: window, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_admin_freeze_window"); +} + +export async function sudoSetEmaPriceHalvingPeriod( + api: TypedApi, + netuid: number, + emaPriceHalvingPeriod: number +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_ema_price_halving_period({ + netuid: netuid, + ema_halving: BigInt(emaPriceHalvingPeriod), + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_ema_price_halving_period"); +} + +export async function sudoSetLockReductionInterval( + api: TypedApi, + interval: number +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_lock_reduction_interval({ + interval: BigInt(interval), + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_lock_reduction_interval"); +} + +export async function sudoSetSubnetMovingAlpha( + api: TypedApi, + alpha: bigint +): Promise { + const alice = getAliceSigner(); + const internalCall = api.tx.AdminUtils.sudo_set_subnet_moving_alpha({ + alpha: alpha, + }); + const tx = api.tx.Sudo.sudo({ call: internalCall.decodedCall }); + await waitForTransactionWithRetry(api, tx, alice, "sudo_set_subnet_moving_alpha"); +} + +// Debug helpers for claim_root investigation +export async function getSubnetTAO( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.SubnetTAO.getValue(netuid); +} + +export async function getSubnetMovingPrice( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.SubnetMovingPrice.getValue(netuid); +} + +export async function getPendingRootAlphaDivs( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.PendingRootAlphaDivs.getValue(netuid); +} + +export async function getTaoWeight( + api: TypedApi +): Promise { + return await api.query.SubtensorModule.TaoWeight.getValue(); +} + +export async function getSubnetAlphaIn( + api: TypedApi, + netuid: number +): Promise { + return await api.query.SubtensorModule.SubnetAlphaIn.getValue(netuid); +} + +export async function getTotalHotkeyAlpha( + api: TypedApi, + hotkey: string, + netuid: number +): Promise { + return await api.query.SubtensorModule.TotalHotkeyAlpha.getValue(hotkey, netuid); +} diff --git a/e2e/staking-tests/setup.ts b/e2e/staking-tests/setup.ts index 3589e8a9aa..4fdd3a2b98 100644 --- a/e2e/staking-tests/setup.ts +++ b/e2e/staking-tests/setup.ts @@ -1,4 +1,14 @@ -import { destroyClient } from "shared"; +import { destroyClient, getDevnetApi, sudoSetLockReductionInterval, log } from "shared"; + +before(async () => { + const api = await getDevnetApi(); + log.info("Setup: set lock reduction interval to 1 for instant lock cost decay"); + + // Set lock reduction interval to 1 block to make network registration lock cost decay instantly. + // By default, the lock cost doubles with each subnet registration and decays over 14 days (100,800 blocks). + // Without this, tests creating multiple subnets would fail with CannotAffordLockCost. + await sudoSetLockReductionInterval(api, 1); +}); after(() => { destroyClient(); diff --git a/e2e/staking-tests/test/claim-root.test.ts b/e2e/staking-tests/test/claim-root.test.ts index 20e04fa9ff..32c1586220 100644 --- a/e2e/staking-tests/test/claim-root.test.ts +++ b/e2e/staking-tests/test/claim-root.test.ts @@ -4,8 +4,35 @@ import { getRandomSubstrateKeypair, convertPublicKeyToSs58, forceSetBalance, + addNewSubnetwork, + startCall, getRootClaimType, setRootClaimType, + getNumRootClaims, + sudoSetNumRootClaims, + getRootClaimThreshold, + sudoSetRootClaimThreshold, + addStake, + getStake, + claimRoot, + getTempo, + sudoSetTempo, + waitForBlocks, + getRootClaimable, + getRootClaimed, + isSubtokenEnabled, + sudoSetSubtokenEnabled, + isNetworkAdded, + sudoSetAdminFreezeWindow, + sudoSetEmaPriceHalvingPeriod, + getSubnetTAO, + getSubnetMovingPrice, + getPendingRootAlphaDivs, + getTaoWeight, + getSubnetAlphaIn, + getTotalHotkeyAlpha, + sudoSetSubnetMovingAlpha, + tao, log, } from "shared"; @@ -87,3 +114,364 @@ describe("▶ set_root_claim_type extrinsic", () => { log.info("✅ Successfully set root claim type to KeepSubnets."); }); }); + +describe("▶ sudo_set_num_root_claims extrinsic", () => { + it("should set num root claims", async () => { + const api = await getDevnetApi(); + + // Get initial value + const numClaimsBefore = await getNumRootClaims(api); + log.info(`Num root claims before: ${numClaimsBefore}`); + + // Set new value (different from current) + const newValue = numClaimsBefore + 5n; + await sudoSetNumRootClaims(api, newValue); + + // Verify value changed + const numClaimsAfter = await getNumRootClaims(api); + log.info(`Num root claims after: ${numClaimsAfter}`); + + assert.strictEqual(numClaimsAfter, newValue, `Expected num root claims to be ${newValue}, got ${numClaimsAfter}`); + + log.info("✅ Successfully set num root claims."); + }); +}); + +describe("▶ sudo_set_root_claim_threshold extrinsic", () => { + it("should set root claim threshold for subnet", async () => { + const api = await getDevnetApi(); + + // Create a subnet to test with + const hotkey = getRandomSubstrateKeypair(); + const coldkey = getRandomSubstrateKeypair(); + const hotkeyAddress = convertPublicKeyToSs58(hotkey.publicKey); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, hotkeyAddress); + await forceSetBalance(api, coldkeyAddress); + + const netuid = await addNewSubnetwork(api, hotkey, coldkey); + await startCall(api, netuid, coldkey); + + // Get initial threshold + const thresholdBefore = await getRootClaimThreshold(api, netuid); + log.info(`Root claim threshold before: ${thresholdBefore}`); + + // Set new threshold value (MAX_ROOT_CLAIM_THRESHOLD is 10_000_000) + // The value is stored as I96F32 fixed-point with 32 fractional bits + const newThreshold = 1_000_000n; + await sudoSetRootClaimThreshold(api, netuid, newThreshold); + + // Verify threshold changed + // I96F32 encoding: newThreshold * 2^32 = 1_000_000 * 4294967296 = 4294967296000000 + const thresholdAfter = await getRootClaimThreshold(api, netuid); + log.info(`Root claim threshold after: ${thresholdAfter}`); + + const expectedStoredValue = newThreshold * (1n << 32n); // I96F32 encoding + assert.strictEqual(thresholdAfter, expectedStoredValue, `Expected threshold to be ${expectedStoredValue}, got ${thresholdAfter}`); + + log.info("✅ Successfully set root claim threshold."); + }); +}); + +// Root subnet netuid is 0 +const ROOT_NETUID = 0; + +describe("▶ claim_root extrinsic", () => { + it("should claim root dividends with Keep type (stake to dynamic subnet)", async () => { + const api = await getDevnetApi(); + + // Setup accounts + // - owner1Hotkey/owner1Coldkey: subnet 1 owner + // - owner2Hotkey/owner2Coldkey: subnet 2 owner (needed for root_sell_flag) + // - stakerColdkey: the coldkey that will stake on root and claim dividends + const owner1Hotkey = getRandomSubstrateKeypair(); + const owner1Coldkey = getRandomSubstrateKeypair(); + const owner2Hotkey = getRandomSubstrateKeypair(); + const owner2Coldkey = getRandomSubstrateKeypair(); + const stakerColdkey = getRandomSubstrateKeypair(); + const owner1HotkeyAddress = convertPublicKeyToSs58(owner1Hotkey.publicKey); + const owner1ColdkeyAddress = convertPublicKeyToSs58(owner1Coldkey.publicKey); + const owner2HotkeyAddress = convertPublicKeyToSs58(owner2Hotkey.publicKey); + const owner2ColdkeyAddress = convertPublicKeyToSs58(owner2Coldkey.publicKey); + const stakerColdkeyAddress = convertPublicKeyToSs58(stakerColdkey.publicKey); + + // Fund all accounts + await forceSetBalance(api, owner1HotkeyAddress); + await forceSetBalance(api, owner1ColdkeyAddress); + await forceSetBalance(api, owner2HotkeyAddress); + await forceSetBalance(api, owner2ColdkeyAddress); + await forceSetBalance(api, stakerColdkeyAddress); + + // Disable admin freeze window to allow enabling subtoken for ROOT + await sudoSetAdminFreezeWindow(api, 0); + log.info("Admin freeze window set to 0"); + + // Enable subtoken for ROOT subnet (required for staking on root) + const subtokenEnabledBefore = await isSubtokenEnabled(api, ROOT_NETUID); + if (!subtokenEnabledBefore) { + await sudoSetSubtokenEnabled(api, ROOT_NETUID, true); + const subtokenEnabledAfter = await isSubtokenEnabled(api, ROOT_NETUID); + log.info(`ROOT subtoken enabled: ${subtokenEnabledAfter}`); + assert.strictEqual(subtokenEnabledAfter, true, "ROOT subtoken should be enabled"); + } + + // Create TWO dynamic subnets - needed for root_sell_flag to become true + // root_sell_flag = sum(moving_prices) > 1.0 + // Each subnet's moving price approaches 1.0 via EMA, so 2 subnets can exceed threshold + const netuid1 = await addNewSubnetwork(api, owner1Hotkey, owner1Coldkey); + await startCall(api, netuid1, owner1Coldkey); + log.info(`Created subnet 1 with netuid: ${netuid1}`); + + const netuid2 = await addNewSubnetwork(api, owner2Hotkey, owner2Coldkey); + await startCall(api, netuid2, owner2Coldkey); + log.info(`Created subnet 2 with netuid: ${netuid2}`); + + // Set short tempo for faster emission distribution + await sudoSetTempo(api, netuid1, 1); + await sudoSetTempo(api, netuid2, 1); + log.info("Set tempo to 1 for both subnets"); + + // Set EMA price halving period to 1 for fast moving price convergence + // Formula: alpha = SubnetMovingAlpha * blocks/(blocks + halving_time) + // With halving_time=1: after 10 blocks, alpha ≈ 0.91, moving price ≈ 0.91 + // With 2 subnets at ~0.9 each, total > 1.0 enabling root_sell_flag + await sudoSetEmaPriceHalvingPeriod(api, netuid1, 1); + await sudoSetEmaPriceHalvingPeriod(api, netuid2, 1); + log.info("Set EMA halving period to 1 for fast price convergence"); + + // Set SubnetMovingAlpha to 1.0 (default is 0.000003 which is way too slow) + // I96F32 encoding: 1.0 * 2^32 = 4294967296 + const movingAlpha = BigInt(4294967296); // 1.0 in I96F32 + await sudoSetSubnetMovingAlpha(api, movingAlpha); + log.info("Set SubnetMovingAlpha to 1.0 for fast EMA convergence"); + + // Set threshold to 0 to allow claiming any amount + await sudoSetRootClaimThreshold(api, netuid1, 0n); + await sudoSetRootClaimThreshold(api, netuid2, 0n); + + // Add stake to ROOT subnet for the staker (makes them eligible for root dividends) + const rootStakeAmount = tao(100); + await addStake(api, stakerColdkey, owner1HotkeyAddress, ROOT_NETUID, rootStakeAmount); + log.info(`Added ${rootStakeAmount} stake to root subnet for staker`); + + // Verify root stake was added + const rootStake = await getStake(api, owner1HotkeyAddress, stakerColdkeyAddress, ROOT_NETUID); + log.info(`Root stake: ${rootStake}`); + assert.ok(rootStake > 0n, "Should have stake on root subnet"); + + // Add stake to both dynamic subnets (owner stake to enable emissions flow) + const subnetStakeAmount = tao(50); + await addStake(api, owner1Coldkey, owner1HotkeyAddress, netuid1, subnetStakeAmount); + await addStake(api, owner2Coldkey, owner2HotkeyAddress, netuid2, subnetStakeAmount); + log.info(`Added ${subnetStakeAmount} owner stake to subnets ${netuid1} and ${netuid2}`); + + // Get initial stake on subnet 1 for the staker (should be 0) + const stakerSubnetStakeBefore = await getStake(api, owner1HotkeyAddress, stakerColdkeyAddress, netuid1); + log.info(`Staker subnet stake before claim: ${stakerSubnetStakeBefore}`); + + // Set root claim type to Keep (keep alpha on subnet instead of swapping to TAO) + await setRootClaimType(api, stakerColdkey, "Keep"); + const claimType = await getRootClaimType(api, stakerColdkeyAddress); + log.info(`Root claim type: ${claimType}`); + assert.strictEqual(claimType, "Keep", "Should have Keep claim type"); + + // Wait for blocks to: + // 1. Allow moving prices to converge (need sum > 1.0 for root_sell_flag) + // 2. Accumulate PendingRootAlphaDivs + // 3. Distribute emissions at tempo boundary + const blocksToWait = 25; + log.info(`Waiting for ${blocksToWait} blocks for moving prices to converge and emissions to accumulate...`); + await waitForBlocks(api, blocksToWait); + + // Debug: Check key storage values + const subnetTaoRoot = await getSubnetTAO(api, ROOT_NETUID); + const subnetTao1 = await getSubnetTAO(api, netuid1); + const subnetTao2 = await getSubnetTAO(api, netuid2); + log.info(`SubnetTAO - ROOT: ${subnetTaoRoot}, netuid1: ${subnetTao1}, netuid2: ${subnetTao2}`); + + const movingPrice1 = await getSubnetMovingPrice(api, netuid1); + const movingPrice2 = await getSubnetMovingPrice(api, netuid2); + log.info(`SubnetMovingPrice - netuid1: ${movingPrice1}, netuid2: ${movingPrice2}`); + // Note: Moving price is I96F32, so divide by 2^32 to get actual value + const mp1Float = Number(movingPrice1) / 2**32; + const mp2Float = Number(movingPrice2) / 2**32; + log.info(`SubnetMovingPrice (float) - netuid1: ${mp1Float}, netuid2: ${mp2Float}, sum: ${mp1Float + mp2Float}`); + + const pendingDivs1 = await getPendingRootAlphaDivs(api, netuid1); + const pendingDivs2 = await getPendingRootAlphaDivs(api, netuid2); + log.info(`PendingRootAlphaDivs - netuid1: ${pendingDivs1}, netuid2: ${pendingDivs2}`); + + const taoWeight = await getTaoWeight(api); + log.info(`TaoWeight: ${taoWeight}`); + + const alphaIn1 = await getSubnetAlphaIn(api, netuid1); + const alphaIn2 = await getSubnetAlphaIn(api, netuid2); + log.info(`SubnetAlphaIn - netuid1: ${alphaIn1}, netuid2: ${alphaIn2}`); + + const totalHotkeyAlpha1 = await getTotalHotkeyAlpha(api, owner1HotkeyAddress, netuid1); + log.info(`TotalHotkeyAlpha for hotkey1 on netuid1: ${totalHotkeyAlpha1}`); + + // Check if there are any claimable dividends + const claimable = await getRootClaimable(api, owner1HotkeyAddress); + const claimableStr = [...claimable.entries()].map(([k, v]) => `[${k}: ${v.toString()}]`).join(", "); + log.info(`RootClaimable entries for hotkey1: ${claimableStr || "(none)"}`); + + // Call claim_root to claim dividends for subnet 1 + await claimRoot(api, stakerColdkey, [netuid1]); + log.info("Called claim_root"); + + // Get stake on subnet 1 after claim + const stakerSubnetStakeAfter = await getStake(api, owner1HotkeyAddress, stakerColdkeyAddress, netuid1); + log.info(`Staker subnet stake after claim: ${stakerSubnetStakeAfter}`); + + // Check RootClaimed value + const rootClaimed = await getRootClaimed(api, netuid1, owner1HotkeyAddress, stakerColdkeyAddress); + log.info(`RootClaimed value: ${rootClaimed}`); + + // Verify dividends were claimed + assert.ok( + stakerSubnetStakeAfter > stakerSubnetStakeBefore, + `Stake should increase after claiming root dividends: before=${stakerSubnetStakeBefore}, after=${stakerSubnetStakeAfter}` + ); + log.info(`✅ Root claim successful: stake increased from ${stakerSubnetStakeBefore} to ${stakerSubnetStakeAfter}`); + }); + + it("should claim root dividends with Swap type (swap to TAO on ROOT)", async () => { + const api = await getDevnetApi(); + + // Setup accounts + // - owner1Hotkey/owner1Coldkey: subnet 1 owner + // - owner2Hotkey/owner2Coldkey: subnet 2 owner (needed for root_sell_flag) + // - stakerColdkey: the coldkey that will stake on root and claim dividends + const owner1Hotkey = getRandomSubstrateKeypair(); + const owner1Coldkey = getRandomSubstrateKeypair(); + const owner2Hotkey = getRandomSubstrateKeypair(); + const owner2Coldkey = getRandomSubstrateKeypair(); + const stakerColdkey = getRandomSubstrateKeypair(); + const owner1HotkeyAddress = convertPublicKeyToSs58(owner1Hotkey.publicKey); + const owner1ColdkeyAddress = convertPublicKeyToSs58(owner1Coldkey.publicKey); + const owner2HotkeyAddress = convertPublicKeyToSs58(owner2Hotkey.publicKey); + const owner2ColdkeyAddress = convertPublicKeyToSs58(owner2Coldkey.publicKey); + const stakerColdkeyAddress = convertPublicKeyToSs58(stakerColdkey.publicKey); + + // Fund all accounts + await forceSetBalance(api, owner1HotkeyAddress); + await forceSetBalance(api, owner1ColdkeyAddress); + await forceSetBalance(api, owner2HotkeyAddress); + await forceSetBalance(api, owner2ColdkeyAddress); + await forceSetBalance(api, stakerColdkeyAddress); + + // Disable admin freeze window to allow enabling subtoken for ROOT + await sudoSetAdminFreezeWindow(api, 0); + log.info("Admin freeze window set to 0"); + + // Create TWO dynamic subnets + const netuid1 = await addNewSubnetwork(api, owner1Hotkey, owner1Coldkey); + await startCall(api, netuid1, owner1Coldkey); + log.info(`Created subnet 1 with netuid: ${netuid1}`); + + const netuid2 = await addNewSubnetwork(api, owner2Hotkey, owner2Coldkey); + await startCall(api, netuid2, owner2Coldkey); + log.info(`Created subnet 2 with netuid: ${netuid2}`); + + // Set short tempo for faster emission distribution + await sudoSetTempo(api, netuid1, 1); + await sudoSetTempo(api, netuid2, 1); + log.info("Set tempo to 1 for both subnets"); + + // Set EMA price halving period to 1 for fast moving price convergence + await sudoSetEmaPriceHalvingPeriod(api, netuid1, 1); + await sudoSetEmaPriceHalvingPeriod(api, netuid2, 1); + log.info("Set EMA halving period to 1 for fast price convergence"); + + // Set SubnetMovingAlpha to 1.0 (default is 0.000003 which is way too slow) + // I96F32 encoding: 1.0 * 2^32 = 4294967296 + const movingAlpha = BigInt(4294967296); // 1.0 in I96F32 + await sudoSetSubnetMovingAlpha(api, movingAlpha); + log.info("Set SubnetMovingAlpha to 1.0 for fast EMA convergence"); + + // Set threshold to 0 to allow claiming any amount + await sudoSetRootClaimThreshold(api, netuid1, 0n); + await sudoSetRootClaimThreshold(api, netuid2, 0n); + + // Add stake to ROOT subnet for the staker + const rootStakeAmount = tao(100); + await addStake(api, stakerColdkey, owner1HotkeyAddress, ROOT_NETUID, rootStakeAmount); + log.info(`Added ${rootStakeAmount} stake to root subnet for staker`); + + // Get initial ROOT stake + const rootStakeBefore = await getStake(api, owner1HotkeyAddress, stakerColdkeyAddress, ROOT_NETUID); + log.info(`Root stake before: ${rootStakeBefore}`); + + // Add stake to both dynamic subnets (owner stake to enable emissions flow) + const subnetStakeAmount = tao(50); + await addStake(api, owner1Coldkey, owner1HotkeyAddress, netuid1, subnetStakeAmount); + await addStake(api, owner2Coldkey, owner2HotkeyAddress, netuid2, subnetStakeAmount); + log.info(`Added ${subnetStakeAmount} owner stake to subnets ${netuid1} and ${netuid2}`); + + // Set root claim type to Swap (swap alpha to TAO and add to ROOT stake) + await setRootClaimType(api, stakerColdkey, "Swap"); + const claimType = await getRootClaimType(api, stakerColdkeyAddress); + log.info(`Root claim type: ${claimType}`); + assert.strictEqual(claimType, "Swap", "Should have Swap claim type"); + + // Wait for blocks + const blocksToWait = 25; + log.info(`Waiting for ${blocksToWait} blocks for emissions to accumulate...`); + await waitForBlocks(api, blocksToWait); + + // Debug: Check moving prices + const movingPrice1 = await getSubnetMovingPrice(api, netuid1); + const movingPrice2 = await getSubnetMovingPrice(api, netuid2); + const mp1Float = Number(movingPrice1) / 2**32; + const mp2Float = Number(movingPrice2) / 2**32; + log.info(`SubnetMovingPrice (float) - netuid1: ${mp1Float}, netuid2: ${mp2Float}, sum: ${mp1Float + mp2Float}`); + + const pendingDivs1 = await getPendingRootAlphaDivs(api, netuid1); + log.info(`PendingRootAlphaDivs netuid1: ${pendingDivs1}`); + + // Check claimable + const claimable = await getRootClaimable(api, owner1HotkeyAddress); + const claimableStr = [...claimable.entries()].map(([k, v]) => `[${k}: ${v.toString()}]`).join(", "); + log.info(`RootClaimable entries for hotkey1: ${claimableStr || "(none)"}`); + + // Call claim_root - with Swap type, dividends are swapped to TAO and added to ROOT stake + await claimRoot(api, stakerColdkey, [netuid1]); + log.info("Called claim_root with Swap type"); + + // Get ROOT stake after claim + const rootStakeAfter = await getStake(api, owner1HotkeyAddress, stakerColdkeyAddress, ROOT_NETUID); + log.info(`Root stake after claim: ${rootStakeAfter}`); + + // Check RootClaimed value + const rootClaimed = await getRootClaimed(api, netuid1, owner1HotkeyAddress, stakerColdkeyAddress); + log.info(`RootClaimed value: ${rootClaimed}`); + + // With Swap type, ROOT stake should increase (not dynamic subnet stake) + assert.ok( + rootStakeAfter > rootStakeBefore, + `ROOT stake should increase after claiming with Swap type: before=${rootStakeBefore}, after=${rootStakeAfter}` + ); + log.info(`✅ Root claim with Swap successful: ROOT stake increased from ${rootStakeBefore} to ${rootStakeAfter}`); + }); + + it("should handle claim_root when no dividends are available", async () => { + const api = await getDevnetApi(); + + // Setup accounts + const coldkey = getRandomSubstrateKeypair(); + const coldkeyAddress = convertPublicKeyToSs58(coldkey.publicKey); + + await forceSetBalance(api, coldkeyAddress); + + // Set root claim type to Keep + await setRootClaimType(api, coldkey, "Keep"); + + // Try to claim on a non-existent subnet (should succeed but be a no-op) + // According to Rust tests, claiming on unrelated subnets returns Ok but does nothing + await claimRoot(api, coldkey, [1]); + + log.info("✅ claim_root with no dividends executed successfully (no-op)."); + }); +});