@arxiumlabs/sdk 0.0.0-stage → 0.5.0
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/README.md +138 -2
- package/dist/actions.d.ts +185 -0
- package/dist/actions.js +241 -0
- package/dist/amounts.d.ts +3 -0
- package/dist/amounts.js +5 -0
- package/dist/bech32.d.ts +9 -0
- package/dist/bech32.js +39 -0
- package/dist/bincode.d.ts +31 -0
- package/dist/bincode.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +264 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.js +36 -0
- package/dist/policy.d.ts +53 -0
- package/dist/policy.js +116 -0
- package/dist/rpc.d.ts +65 -0
- package/dist/rpc.js +74 -0
- package/package.json +41 -4
package/dist/policy.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Writer, asBuffer, fromHex, toHex } from "./bincode.js";
|
|
2
|
+
import { multisigAddress, multisigSignature, signAction, verifyMultisig } from "./actions.js";
|
|
3
|
+
export const ACCOUNT_POLICY_ACTION = 40;
|
|
4
|
+
export const POLICY_VARIANT = { setAccountPolicy: 0, rotateAccountMembers: 1, addSessionKey: 2, revokeSessionKey: 3, startAccountRecovery: 4, cancelAccountRecovery: 5, executeAccountRecovery: 6 };
|
|
5
|
+
const MODE = { owner: 0, session: 1, guardian: 2 };
|
|
6
|
+
function key(hex) { const bytes = fromHex(hex); if (bytes.length !== 32)
|
|
7
|
+
throw new Error("public key must be 32 bytes"); return bytes; }
|
|
8
|
+
function bounded(value, bits) { const n = BigInt(value); if (n < 0n || n >= 1n << BigInt(bits))
|
|
9
|
+
throw new Error(`value outside u${bits}`); return n; }
|
|
10
|
+
function threshold(w, p) {
|
|
11
|
+
if (!Number.isInteger(p.threshold) || p.threshold < 1 || p.threshold > p.members.length || p.members.length > 16 || p.members.some((m, i) => i > 0 && p.members[i - 1] >= m))
|
|
12
|
+
throw new Error("threshold policy must have 1..16 sorted unique members");
|
|
13
|
+
w.u8(p.threshold).vec(p.members, (ww, m) => ww.raw(key(m)));
|
|
14
|
+
}
|
|
15
|
+
function asset(w, a) { if (a === "native")
|
|
16
|
+
w.varint(0);
|
|
17
|
+
else
|
|
18
|
+
w.varint(1).string(a.asset); }
|
|
19
|
+
function recipients(w, list) { if (list.length > 64 || list.some((a, i) => i > 0 && list[i - 1] >= a))
|
|
20
|
+
throw new Error("recipients must be bounded, unique and sorted"); w.vec(list, (ww, a) => ww.string(a)); }
|
|
21
|
+
function session(w, s) { w.raw(key(s.publicKey)).varint(bounded(s.expiresAt, 64)); asset(w, s.asset); w.varint(bounded(s.allowance, 128)); recipients(w, s.recipients); }
|
|
22
|
+
function policy(w, p) {
|
|
23
|
+
threshold(w, p.owners);
|
|
24
|
+
if (p.limits.length > 16 || p.sessions.length > 16)
|
|
25
|
+
throw new Error("policy exceeds bounded menu");
|
|
26
|
+
if (p.sessions.length && !p.limits.some(l => l.asset === "native"))
|
|
27
|
+
throw new Error("session keys require a native fee budget");
|
|
28
|
+
w.vec(p.limits, (ww, l) => { asset(ww, l.asset); if (BigInt(l.periodBlocks) === 0n)
|
|
29
|
+
throw new Error("period must be positive"); ww.varint(bounded(l.periodBlocks, 64)).varint(bounded(l.amount, 128)); });
|
|
30
|
+
w.option(p.recipients, recipients).vec(p.sessions, session).option(p.recovery, (ww, r) => { threshold(ww, r.guardians); if (BigInt(r.delayBlocks) === 0n)
|
|
31
|
+
throw new Error("recovery delay must be positive"); ww.varint(bounded(r.delayBlocks, 64)); });
|
|
32
|
+
}
|
|
33
|
+
export function encodeAccountPolicy(name, input) {
|
|
34
|
+
const w = new Writer().varint(ACCOUNT_POLICY_ACTION).varint(POLICY_VARIANT[name]);
|
|
35
|
+
switch (name) {
|
|
36
|
+
case "setAccountPolicy":
|
|
37
|
+
policy(w, input.policy);
|
|
38
|
+
break;
|
|
39
|
+
case "rotateAccountMembers":
|
|
40
|
+
case "startAccountRecovery":
|
|
41
|
+
threshold(w, input.owners);
|
|
42
|
+
break;
|
|
43
|
+
case "addSessionKey":
|
|
44
|
+
session(w, input.session);
|
|
45
|
+
break;
|
|
46
|
+
case "revokeSessionKey":
|
|
47
|
+
w.raw(key(input.publicKey));
|
|
48
|
+
break;
|
|
49
|
+
case "cancelAccountRecovery":
|
|
50
|
+
case "executeAccountRecovery": break;
|
|
51
|
+
}
|
|
52
|
+
return w.bytes();
|
|
53
|
+
}
|
|
54
|
+
function readKey(r) { return toHex(Uint8Array.from({ length: 32 }, () => r.u8())); }
|
|
55
|
+
function readThreshold(r) { return { threshold: r.u8(), members: r.vec(readKey) }; }
|
|
56
|
+
function readAsset(r) { const tag = r.varint(); if (tag === 0n)
|
|
57
|
+
return "native"; if (tag === 1n)
|
|
58
|
+
return { asset: r.string() }; throw new Error("unknown spend asset"); }
|
|
59
|
+
function readSession(r) { return { publicKey: readKey(r), expiresAt: r.varint().toString(), asset: readAsset(r), allowance: r.varint().toString(), recipients: r.vec(rr => rr.string()) }; }
|
|
60
|
+
export function readAccountPolicy(r) {
|
|
61
|
+
const variant = Number(r.varint()), name = Object.keys(POLICY_VARIANT).find(n => POLICY_VARIANT[n] === variant);
|
|
62
|
+
if (!name)
|
|
63
|
+
throw new Error("unknown account policy action");
|
|
64
|
+
let input;
|
|
65
|
+
switch (name) {
|
|
66
|
+
case "setAccountPolicy":
|
|
67
|
+
input = { policy: { owners: readThreshold(r), limits: r.vec(rr => ({ asset: readAsset(rr), periodBlocks: rr.varint().toString(), amount: rr.varint().toString() })), recipients: r.option(rr => rr.vec(v => v.string())), sessions: r.vec(readSession), recovery: r.option(rr => ({ guardians: readThreshold(rr), delayBlocks: rr.varint().toString() })) } };
|
|
68
|
+
break;
|
|
69
|
+
case "rotateAccountMembers":
|
|
70
|
+
case "startAccountRecovery":
|
|
71
|
+
input = { owners: readThreshold(r) };
|
|
72
|
+
break;
|
|
73
|
+
case "addSessionKey":
|
|
74
|
+
input = { session: readSession(r) };
|
|
75
|
+
break;
|
|
76
|
+
case "revokeSessionKey":
|
|
77
|
+
input = { publicKey: readKey(r) };
|
|
78
|
+
break;
|
|
79
|
+
default: input = {};
|
|
80
|
+
}
|
|
81
|
+
return { name, input };
|
|
82
|
+
}
|
|
83
|
+
/** Sign the stable account's envelope, not the signer's personal address. */
|
|
84
|
+
export async function signPolicyMember(privateKey, genesis, sender, nonce, payload) { return signAction(privateKey, genesis, sender, nonce, payload); }
|
|
85
|
+
export function assemblePolicySignature(mode, policy, signatures) {
|
|
86
|
+
if (signatures.length !== policy.threshold || new Set(signatures.map(([m]) => m)).size !== signatures.length || signatures.some(([, s]) => fromHex(s).length !== 64))
|
|
87
|
+
throw new Error("need exactly threshold distinct member signatures");
|
|
88
|
+
return `a7${MODE[mode].toString(16).padStart(2, "0")}${multisigSignature(policy.threshold, policy.members.map(key), signatures.map(([m, s]) => [key(m), s]))}`;
|
|
89
|
+
}
|
|
90
|
+
export function assembleSessionSignature(publicKey, signature) { if (fromHex(signature).length !== 64)
|
|
91
|
+
throw new Error("signature must be 64 bytes"); return `a701${toHex(key(publicKey))}${signature}`; }
|
|
92
|
+
/** Cryptographic verification only; use proven policy at the action's pre-state.
|
|
93
|
+
* The runtime also checks capability scope, expiry, nonce and spend counters. */
|
|
94
|
+
export async function verifyPolicySignature(policy, signature, message) {
|
|
95
|
+
try {
|
|
96
|
+
const bytes = fromHex(signature);
|
|
97
|
+
if (bytes[0] !== 0xa7)
|
|
98
|
+
return null;
|
|
99
|
+
if (bytes[1] === 1) {
|
|
100
|
+
if (bytes.length !== 98)
|
|
101
|
+
return null;
|
|
102
|
+
const pk = bytes.slice(2, 34);
|
|
103
|
+
if (!policy.sessions.some(s => s.publicKey === toHex(pk)))
|
|
104
|
+
return null;
|
|
105
|
+
const cryptoKey = await crypto.subtle.importKey("raw", asBuffer(pk), "Ed25519", false, ["verify"]);
|
|
106
|
+
return await crypto.subtle.verify("Ed25519", cryptoKey, asBuffer(bytes.slice(34)), asBuffer(message)) ? "session" : null;
|
|
107
|
+
}
|
|
108
|
+
const p = bytes[1] === 0 ? policy.owners : bytes[1] === 2 ? policy.recovery?.guardians : null;
|
|
109
|
+
if (!p)
|
|
110
|
+
return null;
|
|
111
|
+
return await verifyMultisig(await multisigAddress(p.threshold, p.members.map(key)), toHex(bytes.slice(2)), message) ? (bytes[1] === 0 ? "owner" : "guardian") : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type SignedAction } from "./actions.js";
|
|
2
|
+
import { type AccountPolicy } from "./policy.js";
|
|
3
|
+
export type RpcOptions = {
|
|
4
|
+
rpc: string;
|
|
5
|
+
token?: string;
|
|
6
|
+
fetch?: typeof globalThis.fetch;
|
|
7
|
+
};
|
|
8
|
+
export type ActionStatus = {
|
|
9
|
+
status: "pending";
|
|
10
|
+
} | {
|
|
11
|
+
status: "confirmed";
|
|
12
|
+
height: number;
|
|
13
|
+
block_hash: string;
|
|
14
|
+
sender: string;
|
|
15
|
+
nonce: number;
|
|
16
|
+
} | {
|
|
17
|
+
status: "dropped";
|
|
18
|
+
reason: string;
|
|
19
|
+
} | {
|
|
20
|
+
status: "unknown";
|
|
21
|
+
};
|
|
22
|
+
export type SendActionOptions = {
|
|
23
|
+
privateKey: CryptoKey;
|
|
24
|
+
sender: string;
|
|
25
|
+
payload: Uint8Array;
|
|
26
|
+
pollIntervalMs?: number;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
finalized?: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare class RpcError extends Error {
|
|
31
|
+
readonly status: number;
|
|
32
|
+
constructor(status: number, message: string);
|
|
33
|
+
}
|
|
34
|
+
export declare class ArxiumRpc {
|
|
35
|
+
readonly rpc: string;
|
|
36
|
+
private readonly token?;
|
|
37
|
+
private readonly requestFetch;
|
|
38
|
+
constructor(options: RpcOptions);
|
|
39
|
+
private request;
|
|
40
|
+
private genesis?;
|
|
41
|
+
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
42
|
+
genesisHash(): Promise<Uint8Array>;
|
|
43
|
+
status<T = Record<string, unknown>>(): Promise<T>;
|
|
44
|
+
account<T = Record<string, unknown>>(address: string): Promise<T>;
|
|
45
|
+
stake<T = Record<string, unknown>>(address: string): Promise<T>;
|
|
46
|
+
stakes<T = Record<string, unknown>[]>(address: string): Promise<T>;
|
|
47
|
+
blsKey<T = Record<string, unknown>>(address: string): Promise<T>;
|
|
48
|
+
submit(action: SignedAction): Promise<unknown>;
|
|
49
|
+
action(signature: string): Promise<ActionStatus>;
|
|
50
|
+
blocks<T = unknown>(from: number, to: number): Promise<T>;
|
|
51
|
+
block<T = unknown>(height: number): Promise<T>;
|
|
52
|
+
blockByHash<T = unknown>(hash: string): Promise<T>;
|
|
53
|
+
validators<T = unknown>(): Promise<T>;
|
|
54
|
+
finality<T = unknown>(): Promise<T>;
|
|
55
|
+
search<T = unknown>(query: string): Promise<T>;
|
|
56
|
+
minStake<T = unknown>(): Promise<T>;
|
|
57
|
+
actionFee<T = unknown>(): Promise<T>;
|
|
58
|
+
sendAction(options: SendActionOptions): Promise<{
|
|
59
|
+
action: SignedAction;
|
|
60
|
+
status: ActionStatus;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
/** Stateful witnesses require the policy at the action's pre-state, not today's
|
|
64
|
+
* policy. This checks cryptography; circuit execution proves limits and scope. */
|
|
65
|
+
export declare function verifySignedAction(action: SignedAction, genesis: Uint8Array, preStatePolicy?: AccountPolicy): Promise<boolean>;
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { signAction, signingBytes, submitBody, verifyMultisig } from "./actions.js";
|
|
2
|
+
import { decodeAddress, isMultisigAddress } from "./bech32.js";
|
|
3
|
+
import { asBuffer, fromHex } from "./bincode.js";
|
|
4
|
+
import { verifyPolicySignature } from "./policy.js";
|
|
5
|
+
export class RpcError extends Error {
|
|
6
|
+
status;
|
|
7
|
+
constructor(status, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.status = status;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class ArxiumRpc {
|
|
13
|
+
rpc;
|
|
14
|
+
token;
|
|
15
|
+
requestFetch;
|
|
16
|
+
constructor(options) { this.rpc = options.rpc.replace(/\/+$/, ""); this.token = options.token; this.requestFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init)); } // browsers and Workers throw "Illegal invocation" if fetch is called with `this` = ArxiumRpc
|
|
17
|
+
async request(path, init) { const response = await this.requestFetch(`${this.rpc}${path}`, { ...init, headers: { ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), ...init?.headers } }); const text = await response.text(); if (!response.ok)
|
|
18
|
+
throw new RpcError(response.status, text || `${response.status} ${response.statusText}`); return text ? JSON.parse(text) : undefined; }
|
|
19
|
+
genesis;
|
|
20
|
+
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
21
|
+
genesisHash() { return this.genesis ??= this.request("/genesis-hash").then((r) => fromHex(r.genesis_hash.replace(/^0x/, "")), (e) => { this.genesis = undefined; throw e; }); }
|
|
22
|
+
status() { return this.request("/status"); }
|
|
23
|
+
account(address) { return this.request(`/accounts/${encodeURIComponent(address)}`); }
|
|
24
|
+
stake(address) { return this.request(`/accounts/${encodeURIComponent(address)}/stake`); }
|
|
25
|
+
stakes(address) { return this.request(`/accounts/${encodeURIComponent(address)}/stakes`); }
|
|
26
|
+
blsKey(address) { return this.request(`/accounts/${encodeURIComponent(address)}/bls-key`); }
|
|
27
|
+
submit(action) { return this.request("/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) }); }
|
|
28
|
+
async action(signature) { try {
|
|
29
|
+
return await this.request(`/actions/${encodeURIComponent(signature)}`);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (error instanceof RpcError && error.status === 404)
|
|
33
|
+
return { status: "unknown" };
|
|
34
|
+
throw error;
|
|
35
|
+
} }
|
|
36
|
+
blocks(from, to) { return this.request(`/blocks?from=${from}&to=${to}`); }
|
|
37
|
+
block(height) { return this.request(`/blocks/${height}`); }
|
|
38
|
+
blockByHash(hash) { return this.request(`/blocks/by-hash/${encodeURIComponent(hash)}`); }
|
|
39
|
+
validators() { return this.request("/validators"); }
|
|
40
|
+
finality() { return this.request("/finality"); }
|
|
41
|
+
search(query) { return this.request(`/search?q=${encodeURIComponent(query)}`); }
|
|
42
|
+
minStake() { return this.request("/min-stake"); }
|
|
43
|
+
actionFee() { return this.request("/action-fee"); }
|
|
44
|
+
async sendAction(options) { const account = await this.account(options.sender).catch((error) => { if (error instanceof RpcError && error.status === 404)
|
|
45
|
+
return { nonce: 0 }; throw error; }); const signature = await signAction(options.privateKey, await this.genesisHash(), options.sender, account.nonce, options.payload); const action = submitBody(options.sender, account.nonce, signature, options.payload); await this.submit(action); const deadline = Date.now() + (options.timeoutMs ?? 120_000); while (Date.now() < deadline) {
|
|
46
|
+
const status = await this.action(signature);
|
|
47
|
+
if (status.status === "dropped")
|
|
48
|
+
throw new Error(`action dropped: ${status.reason}`);
|
|
49
|
+
if (status.status === "confirmed") {
|
|
50
|
+
if (!options.finalized)
|
|
51
|
+
return { action, status };
|
|
52
|
+
const block = await this.block(status.height);
|
|
53
|
+
if (block.finalized)
|
|
54
|
+
return { action, status };
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs ?? 1_000));
|
|
57
|
+
} throw new Error("timed out waiting for action"); }
|
|
58
|
+
}
|
|
59
|
+
/** Stateful witnesses require the policy at the action's pre-state, not today's
|
|
60
|
+
* policy. This checks cryptography; circuit execution proves limits and scope. */
|
|
61
|
+
export async function verifySignedAction(action, genesis, preStatePolicy) {
|
|
62
|
+
try {
|
|
63
|
+
const message = signingBytes(genesis, action.sender, action.nonce, Uint8Array.from(action.payload));
|
|
64
|
+
if (action.signature.length > 128 && /^a7(00|01|02)/.test(action.signature))
|
|
65
|
+
return !!preStatePolicy && await verifyPolicySignature(preStatePolicy, action.signature, message) !== null;
|
|
66
|
+
if (isMultisigAddress(action.sender))
|
|
67
|
+
return await verifyMultisig(action.sender, action.signature, message);
|
|
68
|
+
const key = await crypto.subtle.importKey("raw", asBuffer(decodeAddress(action.sender)), "Ed25519", false, ["verify"]);
|
|
69
|
+
return crypto.subtle.verify("Ed25519", key, asBuffer(fromHex(action.signature)), asBuffer(message));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arxiumlabs/sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Arxium TypeScript SDK and arx CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "Apache-2.0",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Arxium-Protocol/arxium.git",
|
|
11
|
+
"directory": "sdk/ts"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"import": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"arx": "dist/cli.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist",
|
|
27
|
+
"!dist/*.test.*",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"check:package": "node scripts/release-check.mjs",
|
|
36
|
+
"test:release": "node --test scripts/release-check.test.mjs",
|
|
37
|
+
"test": "npm run build && node dist/codec.test.js && node dist/policy.test.js && node dist/cli.test.js"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^20.0.0",
|
|
41
|
+
"typescript": "^5.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|