From 1e4a6f508dc2f8791743d61c45597331a1d1f997 Mon Sep 17 00:00:00 2001 From: Josh Scanlan Date: Fri, 15 May 2026 17:03:21 -0700 Subject: [PATCH 1/3] add @magic-ext/subtensor New SDK extension that emits the subtensor_* RPC methods consumed by embedded-wallet's subtensor_handler: getAccount, sendTransaction, addProxy, addStake, removeStake, moveStake, signRaw. chainType is 'SUBTENSOR' to match WalletType in embedded-wallet. Package scaffolding (jest, babel, eslint, tsconfig) mirrors @magic-ext/sui. 8 unit tests at 100% coverage. Co-Authored-By: Claude Opus 4.7 --- .../@magic-ext/subtensor/.lintstagedrc.yml | 2 + packages/@magic-ext/subtensor/.prettierrc.js | 1 + .../@magic-ext/subtensor/babel.config.json | 7 ++ .../@magic-ext/subtensor/eslint.config.mjs | 21 ++++ packages/@magic-ext/subtensor/jest.config.ts | 13 +++ packages/@magic-ext/subtensor/package.json | 33 +++++++ .../@magic-ext/subtensor/src/index.cdn.ts | 3 + .../@magic-ext/subtensor/src/index.native.ts | 1 + packages/@magic-ext/subtensor/src/index.ts | 97 +++++++++++++++++++ packages/@magic-ext/subtensor/src/type.ts | 47 +++++++++ packages/@magic-ext/subtensor/test/setup.ts | 6 ++ .../subtensor/test/spec/subtensor.spec.ts | 85 ++++++++++++++++ .../@magic-ext/subtensor/test/tsconfig.json | 3 + packages/@magic-ext/subtensor/tsconfig.json | 3 + yarn.lock | 8 ++ 15 files changed, 330 insertions(+) create mode 100644 packages/@magic-ext/subtensor/.lintstagedrc.yml create mode 100644 packages/@magic-ext/subtensor/.prettierrc.js create mode 100644 packages/@magic-ext/subtensor/babel.config.json create mode 100644 packages/@magic-ext/subtensor/eslint.config.mjs create mode 100644 packages/@magic-ext/subtensor/jest.config.ts create mode 100644 packages/@magic-ext/subtensor/package.json create mode 100644 packages/@magic-ext/subtensor/src/index.cdn.ts create mode 100644 packages/@magic-ext/subtensor/src/index.native.ts create mode 100644 packages/@magic-ext/subtensor/src/index.ts create mode 100644 packages/@magic-ext/subtensor/src/type.ts create mode 100644 packages/@magic-ext/subtensor/test/setup.ts create mode 100644 packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts create mode 100644 packages/@magic-ext/subtensor/test/tsconfig.json create mode 100644 packages/@magic-ext/subtensor/tsconfig.json diff --git a/packages/@magic-ext/subtensor/.lintstagedrc.yml b/packages/@magic-ext/subtensor/.lintstagedrc.yml new file mode 100644 index 000000000..1c250ad65 --- /dev/null +++ b/packages/@magic-ext/subtensor/.lintstagedrc.yml @@ -0,0 +1,2 @@ +'*.{ts,tsx}': + - eslint --fix diff --git a/packages/@magic-ext/subtensor/.prettierrc.js b/packages/@magic-ext/subtensor/.prettierrc.js new file mode 100644 index 000000000..6177cac66 --- /dev/null +++ b/packages/@magic-ext/subtensor/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require('../../../.prettierrc.js'); diff --git a/packages/@magic-ext/subtensor/babel.config.json b/packages/@magic-ext/subtensor/babel.config.json new file mode 100644 index 000000000..7521eb073 --- /dev/null +++ b/packages/@magic-ext/subtensor/babel.config.json @@ -0,0 +1,7 @@ +{ + "env": { + "test": { + "plugins": ["@babel/plugin-transform-modules-commonjs"] + } + } +} diff --git a/packages/@magic-ext/subtensor/eslint.config.mjs b/packages/@magic-ext/subtensor/eslint.config.mjs new file mode 100644 index 000000000..a94768e85 --- /dev/null +++ b/packages/@magic-ext/subtensor/eslint.config.mjs @@ -0,0 +1,21 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import rootEslintConfig from '../../../eslint.config.mjs'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export default [ + ...rootEslintConfig, + { + ignores: ['node_modules', 'coverage', 'dist', 'eslint.config.mjs', 'jest.config.ts'], + }, + { + languageOptions: { + parserOptions: { + project: ['./tsconfig.json'], + tsconfigRootDir: __dirname, + }, + }, + }, +]; diff --git a/packages/@magic-ext/subtensor/jest.config.ts b/packages/@magic-ext/subtensor/jest.config.ts new file mode 100644 index 000000000..433182c4b --- /dev/null +++ b/packages/@magic-ext/subtensor/jest.config.ts @@ -0,0 +1,13 @@ +import baseJestConfig from '../../../jest.config'; +import type { Config } from '@jest/types'; + +const config: Config.InitialOptions = { + ...baseJestConfig, + transform: { + '^.+\\.(js|jsx)$': 'babel-jest', + '\\.(ts|tsx)$': 'ts-jest', + }, + coveragePathIgnorePatterns: ['index.cdn.ts', 'index.native.ts'], +}; + +export default config; diff --git a/packages/@magic-ext/subtensor/package.json b/packages/@magic-ext/subtensor/package.json new file mode 100644 index 000000000..74b97c69a --- /dev/null +++ b/packages/@magic-ext/subtensor/package.json @@ -0,0 +1,33 @@ +{ + "name": "@magic-ext/subtensor", + "version": "1.0.0", + "description": "magic subtensor extension", + "author": "Magic (https://magic.link/)", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/magiclabs/magic-js" + }, + "files": [ + "dist" + ], + "target": "neutral", + "cdnGlobalName": "MagicSubtensorExtension", + "main": "./dist/cjs/index.js", + "module": "./dist/es/index.js", + "types": "./dist/types/index.d.ts", + "jsdelivr": "./dist/extension.js", + "exports": { + "import": "./dist/es/index.mjs", + "types": "./dist/types/index.d.ts", + "require": "./dist/cjs/index.js" + }, + "externals": { + "include": [ + "@magic-sdk/provider" + ] + }, + "devDependencies": { + "@magic-sdk/provider": "^33.7.1" + } +} diff --git a/packages/@magic-ext/subtensor/src/index.cdn.ts b/packages/@magic-ext/subtensor/src/index.cdn.ts new file mode 100644 index 000000000..ecc6d1899 --- /dev/null +++ b/packages/@magic-ext/subtensor/src/index.cdn.ts @@ -0,0 +1,3 @@ +import { SubtensorExtension } from './index'; + +export default SubtensorExtension; diff --git a/packages/@magic-ext/subtensor/src/index.native.ts b/packages/@magic-ext/subtensor/src/index.native.ts new file mode 100644 index 000000000..ea465c2a3 --- /dev/null +++ b/packages/@magic-ext/subtensor/src/index.native.ts @@ -0,0 +1 @@ +export * from './index'; diff --git a/packages/@magic-ext/subtensor/src/index.ts b/packages/@magic-ext/subtensor/src/index.ts new file mode 100644 index 000000000..56dd08c8c --- /dev/null +++ b/packages/@magic-ext/subtensor/src/index.ts @@ -0,0 +1,97 @@ +import { MultichainExtension } from '@magic-sdk/provider'; + +import type { + AddProxyParams, + AddStakeParams, + MoveStakeParams, + RemoveStakeParams, + SignedRawResult, + SignedTransactionResult, + SubtensorConfig, + TransferParams, +} from './type'; + +export class SubtensorExtension extends MultichainExtension<'subtensor'> { + name = 'subtensor' as const; + + constructor(public subtensorConfig: SubtensorConfig) { + super({ + rpcUrl: subtensorConfig.rpcUrl, + chainType: 'SUBTENSOR', + }); + } + + public getAccount = (): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_getAccount', + params: [], + }); + }; + + public sendTransaction = (params: TransferParams): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_sendTransaction', + params, + }); + }; + + public addProxy = (params: AddProxyParams): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_addProxy', + params, + }); + }; + + public addStake = (params: AddStakeParams): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_addStake', + params, + }); + }; + + public removeStake = (params: RemoveStakeParams): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_removeStake', + params, + }); + }; + + public moveStake = (params: MoveStakeParams): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_moveStake', + params, + }); + }; + + public signRaw = (payload: Record): Promise => { + return this.request({ + id: 42, + jsonrpc: '2.0', + method: 'subtensor_signRaw', + params: payload, + }); + }; +} + +export type { + AddProxyParams, + AddStakeParams, + MoveStakeParams, + RemoveStakeParams, + SignedRawResult, + SignedTransactionResult, + SubtensorConfig, + TransferParams, +} from './type'; diff --git a/packages/@magic-ext/subtensor/src/type.ts b/packages/@magic-ext/subtensor/src/type.ts new file mode 100644 index 000000000..a880e8325 --- /dev/null +++ b/packages/@magic-ext/subtensor/src/type.ts @@ -0,0 +1,47 @@ +export interface SubtensorConfig { + rpcUrl: string; +} + +export interface ConfigType { + rpcUrl: string; + chainType: string; +} + +export interface AddProxyParams { + delegate: string; + proxyType?: string; + delay?: number; +} + +export interface AddStakeParams { + hotkey: string; + amount: string | bigint; + netuid?: number; +} + +export interface RemoveStakeParams { + hotkey: string; + amount: string | bigint; + netuid?: number; +} + +export interface MoveStakeParams { + originHotkey: string; + destHotkey: string; + amount: string | bigint; + originNetuid?: number; + destNetuid?: number; +} + +export interface TransferParams { + to: string; + amount: string | bigint; +} + +export interface SignedTransactionResult { + txHash: string; +} + +export interface SignedRawResult { + signature: string; +} diff --git a/packages/@magic-ext/subtensor/test/setup.ts b/packages/@magic-ext/subtensor/test/setup.ts new file mode 100644 index 000000000..6430bda21 --- /dev/null +++ b/packages/@magic-ext/subtensor/test/setup.ts @@ -0,0 +1,6 @@ +// NOTE: This module is automatically included at the top of each test file. +import { mockConsole } from '../../../../scripts/utils/mock-console'; + +beforeEach(() => { + mockConsole(); +}); diff --git a/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts b/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts new file mode 100644 index 000000000..d8aed08bc --- /dev/null +++ b/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts @@ -0,0 +1,85 @@ +import { SubtensorExtension } from '../../src/index'; +import { createMagicSDKWithExtension } from '../../../../@magic-sdk/provider/test/factories'; + +beforeEach(() => { + jest.resetAllMocks(); +}); + +function makeMagic() { + const magic = createMagicSDKWithExtension({}, [new SubtensorExtension({ rpcUrl: 'wss://entrypoint-finney.opentensor.ai:443' })]); + magic.subtensor.request = jest.fn(); + return magic; +} + +test('extension reports name and chain type', () => { + const ext = new SubtensorExtension({ rpcUrl: 'wss://example' }); + expect(ext.name).toBe('subtensor'); +}); + +test('getAccount sends subtensor_getAccount with empty params', () => { + const magic = makeMagic(); + magic.subtensor.getAccount(); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_getAccount'); + expect(payload.params).toEqual([]); +}); + +test('sendTransaction forwards { to, amount }', () => { + const magic = makeMagic(); + magic.subtensor.sendTransaction({ to: '5Dest', amount: '1000000000' }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_sendTransaction'); + expect(payload.params).toEqual({ to: '5Dest', amount: '1000000000' }); +}); + +test('addProxy forwards delegate + optional proxyType/delay', () => { + const magic = makeMagic(); + magic.subtensor.addProxy({ delegate: '5Del', proxyType: 'Staking', delay: 5 }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_addProxy'); + expect(payload.params).toEqual({ delegate: '5Del', proxyType: 'Staking', delay: 5 }); +}); + +test('addStake forwards hotkey, amount, and optional netuid', () => { + const magic = makeMagic(); + magic.subtensor.addStake({ hotkey: '5Hot', amount: 100n, netuid: 7 }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_addStake'); + expect(payload.params).toEqual({ hotkey: '5Hot', amount: 100n, netuid: 7 }); +}); + +test('removeStake forwards hotkey + amount', () => { + const magic = makeMagic(); + magic.subtensor.removeStake({ hotkey: '5Hot', amount: '50' }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_removeStake'); + expect(payload.params).toEqual({ hotkey: '5Hot', amount: '50' }); +}); + +test('moveStake forwards origin/dest hotkeys and amount', () => { + const magic = makeMagic(); + magic.subtensor.moveStake({ + originHotkey: '5From', + destHotkey: '5To', + amount: 200n, + originNetuid: 1, + destNetuid: 2, + }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_moveStake'); + expect(payload.params).toEqual({ + originHotkey: '5From', + destHotkey: '5To', + amount: 200n, + originNetuid: 1, + destNetuid: 2, + }); +}); + +test('signRaw forwards arbitrary payload as params', () => { + const magic = makeMagic(); + magic.subtensor.signRaw({ method: '0xdeadbeef', version: 4 }); + const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; + expect(payload.method).toBe('subtensor_signRaw'); + expect(payload.params).toEqual({ method: '0xdeadbeef', version: 4 }); +}); diff --git a/packages/@magic-ext/subtensor/test/tsconfig.json b/packages/@magic-ext/subtensor/test/tsconfig.json new file mode 100644 index 000000000..ef2de78d3 --- /dev/null +++ b/packages/@magic-ext/subtensor/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../../tsconfig.settings.test.json", +} diff --git a/packages/@magic-ext/subtensor/tsconfig.json b/packages/@magic-ext/subtensor/tsconfig.json new file mode 100644 index 000000000..d426fac03 --- /dev/null +++ b/packages/@magic-ext/subtensor/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.settings.json", +} diff --git a/yarn.lock b/yarn.lock index a2c31dff7..c19f400f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3339,6 +3339,14 @@ __metadata: languageName: unknown linkType: soft +"@magic-ext/subtensor@workspace:packages/@magic-ext/subtensor": + version: 0.0.0-use.local + resolution: "@magic-ext/subtensor@workspace:packages/@magic-ext/subtensor" + dependencies: + "@magic-sdk/provider": ^33.7.1 + languageName: unknown + linkType: soft + "@magic-ext/sui@workspace:packages/@magic-ext/sui": version: 0.0.0-use.local resolution: "@magic-ext/sui@workspace:packages/@magic-ext/sui" From f97a809c56b714166598fa617e28dd7dd6f622ab Mon Sep 17 00:00:00 2001 From: Josh Scanlan Date: Mon, 18 May 2026 12:27:53 -0700 Subject: [PATCH 2/3] fix type errors --- .../@magic-ext/subtensor/eslint.config.mjs | 2 +- packages/@magic-ext/subtensor/src/index.ts | 28 +++++++++---------- .../subtensor/test/spec/subtensor.spec.ts | 12 ++++---- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/@magic-ext/subtensor/eslint.config.mjs b/packages/@magic-ext/subtensor/eslint.config.mjs index a94768e85..c68f8e69b 100644 --- a/packages/@magic-ext/subtensor/eslint.config.mjs +++ b/packages/@magic-ext/subtensor/eslint.config.mjs @@ -13,7 +13,7 @@ export default [ { languageOptions: { parserOptions: { - project: ['./tsconfig.json'], + project: ['./tsconfig.json', './test/tsconfig.json'], tsconfigRootDir: __dirname, }, }, diff --git a/packages/@magic-ext/subtensor/src/index.ts b/packages/@magic-ext/subtensor/src/index.ts index 56dd08c8c..04cacbd17 100644 --- a/packages/@magic-ext/subtensor/src/index.ts +++ b/packages/@magic-ext/subtensor/src/index.ts @@ -21,8 +21,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); } - public getAccount = (): Promise => { - return this.request({ + public getAccount = () => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_getAccount', @@ -30,8 +30,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public sendTransaction = (params: TransferParams): Promise => { - return this.request({ + public sendTransaction = (params: TransferParams) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_sendTransaction', @@ -39,8 +39,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public addProxy = (params: AddProxyParams): Promise => { - return this.request({ + public addProxy = (params: AddProxyParams) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_addProxy', @@ -48,8 +48,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public addStake = (params: AddStakeParams): Promise => { - return this.request({ + public addStake = (params: AddStakeParams) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_addStake', @@ -57,8 +57,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public removeStake = (params: RemoveStakeParams): Promise => { - return this.request({ + public removeStake = (params: RemoveStakeParams) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_removeStake', @@ -66,8 +66,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public moveStake = (params: MoveStakeParams): Promise => { - return this.request({ + public moveStake = (params: MoveStakeParams) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_moveStake', @@ -75,8 +75,8 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public signRaw = (payload: Record): Promise => { - return this.request({ + public signRaw = (payload: Record) => { + return this.request({ id: 42, jsonrpc: '2.0', method: 'subtensor_signRaw', diff --git a/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts b/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts index d8aed08bc..6328cf675 100644 --- a/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts +++ b/packages/@magic-ext/subtensor/test/spec/subtensor.spec.ts @@ -6,7 +6,9 @@ beforeEach(() => { }); function makeMagic() { - const magic = createMagicSDKWithExtension({}, [new SubtensorExtension({ rpcUrl: 'wss://entrypoint-finney.opentensor.ai:443' })]); + const magic = createMagicSDKWithExtension({}, [ + new SubtensorExtension({ rpcUrl: 'wss://entrypoint-finney.opentensor.ai:443' }), + ]); magic.subtensor.request = jest.fn(); return magic; } @@ -42,10 +44,10 @@ test('addProxy forwards delegate + optional proxyType/delay', () => { test('addStake forwards hotkey, amount, and optional netuid', () => { const magic = makeMagic(); - magic.subtensor.addStake({ hotkey: '5Hot', amount: 100n, netuid: 7 }); + magic.subtensor.addStake({ hotkey: '5Hot', amount: BigInt(100), netuid: 7 }); const payload = (magic.subtensor.request as jest.Mock).mock.calls[0][0]; expect(payload.method).toBe('subtensor_addStake'); - expect(payload.params).toEqual({ hotkey: '5Hot', amount: 100n, netuid: 7 }); + expect(payload.params).toEqual({ hotkey: '5Hot', amount: BigInt(100), netuid: 7 }); }); test('removeStake forwards hotkey + amount', () => { @@ -61,7 +63,7 @@ test('moveStake forwards origin/dest hotkeys and amount', () => { magic.subtensor.moveStake({ originHotkey: '5From', destHotkey: '5To', - amount: 200n, + amount: BigInt(200), originNetuid: 1, destNetuid: 2, }); @@ -70,7 +72,7 @@ test('moveStake forwards origin/dest hotkeys and amount', () => { expect(payload.params).toEqual({ originHotkey: '5From', destHotkey: '5To', - amount: 200n, + amount: BigInt(200), originNetuid: 1, destNetuid: 2, }); From 00a0c32716fd7f1f5dfeb8fd6d99fff247636b34 Mon Sep 17 00:00:00 2001 From: Josh Scanlan Date: Wed, 20 May 2026 16:02:00 -0700 Subject: [PATCH 3/3] update params typing --- packages/@magic-ext/subtensor/src/index.ts | 4 +++- packages/@magic-ext/subtensor/src/type.ts | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/@magic-ext/subtensor/src/index.ts b/packages/@magic-ext/subtensor/src/index.ts index 04cacbd17..ac8d55356 100644 --- a/packages/@magic-ext/subtensor/src/index.ts +++ b/packages/@magic-ext/subtensor/src/index.ts @@ -5,6 +5,7 @@ import type { AddStakeParams, MoveStakeParams, RemoveStakeParams, + SignRawParams, SignedRawResult, SignedTransactionResult, SubtensorConfig, @@ -75,7 +76,7 @@ export class SubtensorExtension extends MultichainExtension<'subtensor'> { }); }; - public signRaw = (payload: Record) => { + public signRaw = (payload: SignRawParams) => { return this.request({ id: 42, jsonrpc: '2.0', @@ -90,6 +91,7 @@ export type { AddStakeParams, MoveStakeParams, RemoveStakeParams, + SignRawParams, SignedRawResult, SignedTransactionResult, SubtensorConfig, diff --git a/packages/@magic-ext/subtensor/src/type.ts b/packages/@magic-ext/subtensor/src/type.ts index a880e8325..da6e184d9 100644 --- a/packages/@magic-ext/subtensor/src/type.ts +++ b/packages/@magic-ext/subtensor/src/type.ts @@ -42,6 +42,13 @@ export interface SignedTransactionResult { txHash: string; } +export interface SignRawParams { + /** Hex-encoded data to sign (e.g. '0x...'). */ + data: string; + /** 'bytes' signs raw data; 'payload' treats data as an extrinsic payload. Defaults to 'bytes'. */ + type?: 'bytes' | 'payload'; +} + export interface SignedRawResult { signature: string; }