@circuit-llm/onchain 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Circuit LLM
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # @circuit-llm/onchain
2
+
3
+ > Read Circuit's on-chain state over **pure JSON-RPC** — StakePoint stake verification, CIRC balances, and the mesh registry — with no `@solana/web3.js` dependency.
4
+
5
+ Part of the **[Circuit SDK](https://github.com/Circuit-LLM/circuit-sdk)**. [Contribute a node →](https://github.com/Circuit-LLM/circuit-sdk/blob/main/docs/contributing-a-node.md)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @circuit-llm/onchain
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { verifyStake, circBalance } from '@circuit-llm/onchain';
17
+
18
+ const staked = await verifyStake(wallet, pool, 100_000, { rpcUrl }); // ≥ 100k CIRC staked?
19
+ const circ = await circBalance(address, { rpcUrl });
20
+ ```
21
+
22
+ Also reads the on-chain mesh registry (`getMeshConfig`, `getNode`, `getNodes`) and stake positions (`getStakePositions`). Thin, dependency-light, and browser-friendly.
@@ -0,0 +1,95 @@
1
+ interface RpcOptions {
2
+ rpcUrl: string;
3
+ fetchImpl?: typeof fetch;
4
+ timeoutMs?: number;
5
+ }
6
+ declare class RpcError extends Error {
7
+ readonly code: number;
8
+ constructor(code: number, message: string);
9
+ }
10
+ declare function rpcCall<T = unknown>(opts: RpcOptions, method: string, params: unknown[]): Promise<T>;
11
+
12
+ declare const STAKEPOINT_PROGRAM_ID = "gLHaGJsZ6G7AXZxoDL9EsSWkRbKAWhFHi73gVfNXuzK";
13
+ interface StakePosition {
14
+ positionAddress: string;
15
+ stakedRaw: bigint;
16
+ lockUntil: number;
17
+ lockActive: boolean;
18
+ }
19
+ /** Fetch all ACTIVE (non-zero) staker positions for a wallet in a pool. A wallet can
20
+ * hold multiple positions; they're summed by verifyStake. */
21
+ declare function getStakePositions(wallet: string, pool: string, opts: RpcOptions & {
22
+ now?: () => number;
23
+ }): Promise<StakePosition[]>;
24
+ interface StakeResult {
25
+ eligible: boolean;
26
+ stakedAmount: number;
27
+ stakedRaw: string;
28
+ positionCount: number;
29
+ lockUntil: number | null;
30
+ lockActive: boolean;
31
+ positions: Array<{
32
+ address: string;
33
+ stakedAmount: number;
34
+ stakedRaw: string;
35
+ lockUntil: number;
36
+ lockActive: boolean;
37
+ }>;
38
+ }
39
+ /** Verify a wallet has >= minAmount (human units) staked, summed across all positions.
40
+ * Eligibility is exact (BigInt); the float amounts are for display. */
41
+ declare function verifyStake(wallet: string, pool: string, minAmount: number, opts: RpcOptions & {
42
+ decimals?: number;
43
+ now?: () => number;
44
+ }): Promise<StakeResult>;
45
+
46
+ /** Total CIRC (human units) held by a wallet, summed across its token accounts. */
47
+ declare function circBalance(wallet: string, opts: RpcOptions & {
48
+ mint?: string;
49
+ }): Promise<number>;
50
+
51
+ declare const MESH_REGISTRY_PROGRAM_ID = "BC2sxffu498cB8gUp3P5V5HuBLDsx9XCtJdEmnnGUvfe";
52
+ type NodeRole = 'orchestrator' | 'holder';
53
+ type TrustLevel = 'probation' | 'trusted';
54
+ interface MeshNode {
55
+ address: string;
56
+ node: string;
57
+ role: NodeRole;
58
+ trust: TrustLevel;
59
+ banned: boolean;
60
+ payoutWallet: string;
61
+ stakePool: string;
62
+ joinedAt: number;
63
+ updatedAt: number;
64
+ }
65
+ interface SlotRange {
66
+ start: number;
67
+ end: number;
68
+ }
69
+ interface MeshConfig {
70
+ address: string;
71
+ authority: string;
72
+ auditor: string;
73
+ modelFp: string;
74
+ numLayers: number;
75
+ replication: number;
76
+ slots: SlotRange[];
77
+ version: number;
78
+ bump: number;
79
+ }
80
+ /** Decode a 124-byte Node account (pure). Throws if the discriminator/size is wrong. */
81
+ declare function decodeNode(address: string, data: Buffer): MeshNode;
82
+ /** Decode the singleton MeshConfig account (pure). Variable length (String + Vec), so read sequentially. */
83
+ declare function decodeMeshConfig(address: string, data: Buffer): MeshConfig;
84
+ /** All registered node membership records (each node's role/trust/ban/payout/stake-pool). */
85
+ declare function getNodes(opts: RpcOptions): Promise<MeshNode[]>;
86
+ /** A single node's membership record by its ed25519 identity. Looks up by the stored identity field
87
+ * (offset 8) so no PDA derivation (and thus no @solana/web3.js) is needed. Null if not registered. */
88
+ declare function getNode(nodePubkey: string, opts: RpcOptions): Promise<MeshNode | null>;
89
+ /** The singleton topology contract (authority, auditor, model, layer slots, version). Null if the mesh
90
+ * config has not been initialized on this cluster. */
91
+ declare function getMeshConfig(opts: RpcOptions): Promise<MeshConfig | null>;
92
+
93
+ declare function base58(buf: Uint8Array | Buffer): string;
94
+
95
+ export { MESH_REGISTRY_PROGRAM_ID, type MeshConfig, type MeshNode, type NodeRole, RpcError, type RpcOptions, STAKEPOINT_PROGRAM_ID, type SlotRange, type StakePosition, type StakeResult, type TrustLevel, base58, circBalance, decodeMeshConfig, decodeNode, getMeshConfig, getNode, getNodes, getStakePositions, rpcCall, verifyStake };
package/dist/index.js ADDED
@@ -0,0 +1,235 @@
1
+ // src/rpc.ts
2
+ var RpcError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(`RPC ${code}: ${message}`);
6
+ this.name = "RpcError";
7
+ this.code = code;
8
+ }
9
+ };
10
+ var _id = 0;
11
+ async function rpcCall(opts, method, params) {
12
+ const fetchImpl = opts.fetchImpl ?? fetch;
13
+ const res = await fetchImpl(opts.rpcUrl, {
14
+ method: "POST",
15
+ headers: { "Content-Type": "application/json" },
16
+ body: JSON.stringify({ jsonrpc: "2.0", id: ++_id, method, params }),
17
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 15e3)
18
+ });
19
+ if (!res.ok) throw new RpcError(res.status, `HTTP ${res.status}`);
20
+ const j = await res.json();
21
+ if (j.error) throw new RpcError(j.error.code, j.error.message);
22
+ return j.result;
23
+ }
24
+
25
+ // src/stakepoint.ts
26
+ var STAKEPOINT_PROGRAM_ID = "gLHaGJsZ6G7AXZxoDL9EsSWkRbKAWhFHi73gVfNXuzK";
27
+ var STAKER_ACCOUNT_SIZE = 185;
28
+ var DISCRIMINATOR = "96c5b01d37847095";
29
+ var OFFSET_WALLET = 8;
30
+ var OFFSET_POOL = 40;
31
+ var OFFSET_STAKED = 72;
32
+ var OFFSET_LOCK_UNTIL = 80;
33
+ async function getStakePositions(wallet, pool, opts) {
34
+ const accounts = await rpcCall(opts, "getProgramAccounts", [
35
+ STAKEPOINT_PROGRAM_ID,
36
+ {
37
+ encoding: "base64",
38
+ filters: [
39
+ { dataSize: STAKER_ACCOUNT_SIZE },
40
+ { memcmp: { offset: OFFSET_WALLET, bytes: wallet } },
41
+ { memcmp: { offset: OFFSET_POOL, bytes: pool } }
42
+ ]
43
+ }
44
+ ]);
45
+ const nowSec = Math.floor((opts.now ?? Date.now)() / 1e3);
46
+ const positions = [];
47
+ for (const acc of accounts ?? []) {
48
+ const buf = Buffer.from(acc.account.data[0], "base64");
49
+ if (buf.length < STAKER_ACCOUNT_SIZE) continue;
50
+ if (buf.subarray(0, 8).toString("hex") !== DISCRIMINATOR) continue;
51
+ const stakedRaw = buf.readBigUInt64LE(OFFSET_STAKED);
52
+ if (stakedRaw === 0n) continue;
53
+ const lockUntil = Number(buf.readBigUInt64LE(OFFSET_LOCK_UNTIL));
54
+ positions.push({
55
+ positionAddress: acc.pubkey,
56
+ stakedRaw,
57
+ lockUntil,
58
+ lockActive: lockUntil > 0 && lockUntil > nowSec
59
+ });
60
+ }
61
+ return positions;
62
+ }
63
+ async function verifyStake(wallet, pool, minAmount, opts) {
64
+ const positions = await getStakePositions(wallet, pool, opts);
65
+ const dec = opts.decimals ?? 6;
66
+ const div = 10 ** dec;
67
+ if (!positions.length) {
68
+ return { eligible: false, stakedAmount: 0, stakedRaw: "0", positionCount: 0, lockUntil: null, lockActive: false, positions: [] };
69
+ }
70
+ const totalRaw = positions.reduce((s, p) => s + p.stakedRaw, 0n);
71
+ const minRaw = BigInt(Math.round((minAmount ?? 0) * div));
72
+ const activeLocks = positions.filter((p) => p.lockActive);
73
+ return {
74
+ eligible: totalRaw >= minRaw,
75
+ stakedAmount: Number(totalRaw) / div,
76
+ stakedRaw: totalRaw.toString(),
77
+ positionCount: positions.length,
78
+ lockUntil: activeLocks.length ? Math.max(...activeLocks.map((p) => p.lockUntil)) : null,
79
+ lockActive: activeLocks.length > 0,
80
+ positions: positions.map((p) => ({
81
+ address: p.positionAddress,
82
+ stakedAmount: Number(p.stakedRaw) / div,
83
+ stakedRaw: p.stakedRaw.toString(),
84
+ lockUntil: p.lockUntil,
85
+ lockActive: p.lockActive
86
+ }))
87
+ };
88
+ }
89
+
90
+ // src/balance.ts
91
+ import { CIRC_MINT } from "@circuit-llm/core";
92
+ async function circBalance(wallet, opts) {
93
+ const mint = opts.mint ?? CIRC_MINT;
94
+ const r = await rpcCall(opts, "getTokenAccountsByOwner", [
95
+ wallet,
96
+ { mint },
97
+ { encoding: "jsonParsed" }
98
+ ]);
99
+ return (r.value ?? []).reduce(
100
+ (sum, a) => sum + (a.account.data.parsed.info.tokenAmount.uiAmount ?? 0),
101
+ 0
102
+ );
103
+ }
104
+
105
+ // src/bs58.ts
106
+ var B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
107
+ function base58(buf) {
108
+ const bytes = Uint8Array.from(buf);
109
+ let zeros = 0;
110
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
111
+ const digits = [];
112
+ for (let i = zeros; i < bytes.length; i++) {
113
+ let carry = bytes[i];
114
+ for (let j = 0; j < digits.length; j++) {
115
+ carry += digits[j] << 8;
116
+ digits[j] = carry % 58;
117
+ carry = carry / 58 | 0;
118
+ }
119
+ while (carry) {
120
+ digits.push(carry % 58);
121
+ carry = carry / 58 | 0;
122
+ }
123
+ }
124
+ let out = "1".repeat(zeros);
125
+ for (let k = digits.length - 1; k >= 0; k--) out += B58[digits[k]];
126
+ return out;
127
+ }
128
+
129
+ // src/mesh-registry.ts
130
+ var MESH_REGISTRY_PROGRAM_ID = "BC2sxffu498cB8gUp3P5V5HuBLDsx9XCtJdEmnnGUvfe";
131
+ var NODE_DISCRIMINATOR = "d0350103317ab431";
132
+ var NODE_DISCRIMINATOR_B58 = "bpsQsGpQe1N";
133
+ var MESH_CONFIG_DISCRIMINATOR = "eff3dba40bea7cf5";
134
+ var MESH_CONFIG_DISCRIMINATOR_B58 = "h8qkU9oSVXr";
135
+ var NODE_ACCOUNT_SIZE = 124;
136
+ var ROLES = ["orchestrator", "holder"];
137
+ var TRUST = ["probation", "trusted"];
138
+ function decodeNode(address, data) {
139
+ if (data.length < NODE_ACCOUNT_SIZE) throw new Error(`Node account too small: ${data.length} < ${NODE_ACCOUNT_SIZE}`);
140
+ if (data.subarray(0, 8).toString("hex") !== NODE_DISCRIMINATOR) throw new Error("not a Node account (bad discriminator)");
141
+ const roleByte = data.readUInt8(40);
142
+ const trustByte = data.readUInt8(41);
143
+ const role = ROLES[roleByte];
144
+ const trust = TRUST[trustByte];
145
+ if (!role) throw new Error(`unknown node role ${roleByte}`);
146
+ if (!trust) throw new Error(`unknown trust level ${trustByte}`);
147
+ return {
148
+ address,
149
+ node: base58(data.subarray(8, 40)),
150
+ role,
151
+ trust,
152
+ banned: data.readUInt8(42) !== 0,
153
+ // canonical Borsh bool: any non-zero byte is true
154
+ payoutWallet: base58(data.subarray(43, 75)),
155
+ stakePool: base58(data.subarray(75, 107)),
156
+ joinedAt: Number(data.readBigInt64LE(107)),
157
+ updatedAt: Number(data.readBigInt64LE(115))
158
+ };
159
+ }
160
+ function decodeMeshConfig(address, data) {
161
+ if (data.subarray(0, 8).toString("hex") !== MESH_CONFIG_DISCRIMINATOR) {
162
+ throw new Error("not a MeshConfig account (bad discriminator)");
163
+ }
164
+ let o = 8;
165
+ const authority = base58(data.subarray(o, o + 32));
166
+ o += 32;
167
+ const auditor = base58(data.subarray(o, o + 32));
168
+ o += 32;
169
+ const fpLen = data.readUInt32LE(o);
170
+ o += 4;
171
+ const modelFp = data.subarray(o, o + fpLen).toString("utf8");
172
+ o += fpLen;
173
+ const numLayers = data.readUInt16LE(o);
174
+ o += 2;
175
+ const replication = data.readUInt8(o);
176
+ o += 1;
177
+ const slotCount = data.readUInt32LE(o);
178
+ o += 4;
179
+ const slots = [];
180
+ for (let i = 0; i < slotCount; i++) {
181
+ slots.push({ start: data.readUInt16LE(o), end: data.readUInt16LE(o + 2) });
182
+ o += 4;
183
+ }
184
+ const version = data.readUInt32LE(o);
185
+ o += 4;
186
+ const bump = data.readUInt8(o);
187
+ return { address, authority, auditor, modelFp, numLayers, replication, slots, version, bump };
188
+ }
189
+ async function getNodes(opts) {
190
+ const accounts = await rpcCall(opts, "getProgramAccounts", [
191
+ MESH_REGISTRY_PROGRAM_ID,
192
+ { encoding: "base64", filters: [{ dataSize: NODE_ACCOUNT_SIZE }, { memcmp: { offset: 0, bytes: NODE_DISCRIMINATOR_B58 } }] }
193
+ ]);
194
+ const out = [];
195
+ for (const acc of accounts ?? []) {
196
+ try {
197
+ out.push(decodeNode(acc.pubkey, Buffer.from(acc.account.data[0], "base64")));
198
+ } catch {
199
+ }
200
+ }
201
+ return out;
202
+ }
203
+ async function getNode(nodePubkey, opts) {
204
+ const accounts = await rpcCall(opts, "getProgramAccounts", [
205
+ MESH_REGISTRY_PROGRAM_ID,
206
+ { encoding: "base64", filters: [{ dataSize: NODE_ACCOUNT_SIZE }, { memcmp: { offset: 8, bytes: nodePubkey } }] }
207
+ ]);
208
+ const acc = (accounts ?? [])[0];
209
+ if (!acc) return null;
210
+ return decodeNode(acc.pubkey, Buffer.from(acc.account.data[0], "base64"));
211
+ }
212
+ async function getMeshConfig(opts) {
213
+ const accounts = await rpcCall(opts, "getProgramAccounts", [
214
+ MESH_REGISTRY_PROGRAM_ID,
215
+ { encoding: "base64", filters: [{ memcmp: { offset: 0, bytes: MESH_CONFIG_DISCRIMINATOR_B58 } }] }
216
+ ]);
217
+ const acc = (accounts ?? [])[0];
218
+ if (!acc) return null;
219
+ return decodeMeshConfig(acc.pubkey, Buffer.from(acc.account.data[0], "base64"));
220
+ }
221
+ export {
222
+ MESH_REGISTRY_PROGRAM_ID,
223
+ RpcError,
224
+ STAKEPOINT_PROGRAM_ID,
225
+ base58,
226
+ circBalance,
227
+ decodeMeshConfig,
228
+ decodeNode,
229
+ getMeshConfig,
230
+ getNode,
231
+ getNodes,
232
+ getStakePositions,
233
+ rpcCall,
234
+ verifyStake
235
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@circuit-llm/onchain",
3
+ "version": "0.2.1",
4
+ "description": "Circuit SDK on-chain reads — StakePoint stake verification + CIRC balances via pure JSON-RPC (no @solana/web3.js).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "development": "./src/index.ts",
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "test": "node --experimental-strip-types --conditions=development --test test/*.test.ts",
16
+ "typecheck": "tsc -p tsconfig.json",
17
+ "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist",
18
+ "prepack": "tsup src/index.ts --format esm --dts --clean --out-dir dist"
19
+ },
20
+ "dependencies": {
21
+ "@circuit-llm/core": "0.2.1"
22
+ },
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Circuit-LLM/circuit-sdk.git",
34
+ "directory": "packages/onchain"
35
+ },
36
+ "homepage": "https://github.com/Circuit-LLM/circuit-sdk/tree/main/packages/onchain#readme",
37
+ "bugs": {
38
+ "url": "https://github.com/Circuit-LLM/circuit-sdk/issues"
39
+ },
40
+ "engines": {
41
+ "node": ">=18"
42
+ }
43
+ }