@arxiumlabs/sdk 0.0.0-stage → 0.4.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 +124 -2
- package/dist/actions.d.ts +161 -0
- package/dist/actions.js +207 -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 +39 -4
package/dist/bincode.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const U8_MAX = 250n;
|
|
2
|
+
const U16_MAX = 0xffffn;
|
|
3
|
+
const U32_MAX = 0xffffffffn;
|
|
4
|
+
const U64_MAX = 0xffffffffffffffffn;
|
|
5
|
+
const U128_MAX = (1n << 128n) - 1n;
|
|
6
|
+
/** Canonical bincode 2.x `config::standard()` writer for Arxium action payloads. */
|
|
7
|
+
export class Writer {
|
|
8
|
+
chunks = [];
|
|
9
|
+
bytes() { return Uint8Array.from(this.chunks); }
|
|
10
|
+
raw(bytes) { for (const byte of bytes)
|
|
11
|
+
this.chunks.push(byte); return this; } // not push(...bytes): spreading overflows the call stack past ~100k bytes
|
|
12
|
+
u8(value) {
|
|
13
|
+
if (!Number.isInteger(value) || value < 0 || value > 255)
|
|
14
|
+
throw new RangeError(`u8 out of range: ${value}`);
|
|
15
|
+
this.chunks.push(value);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
bool(value) { return this.u8(value ? 1 : 0); }
|
|
19
|
+
varint(value) {
|
|
20
|
+
const v = BigInt(value);
|
|
21
|
+
if (v < 0n || v > U128_MAX)
|
|
22
|
+
throw new RangeError(`varint out of range: ${v}`);
|
|
23
|
+
if (v <= U8_MAX)
|
|
24
|
+
return this.u8(Number(v));
|
|
25
|
+
if (v <= U16_MAX)
|
|
26
|
+
return this.u8(0xfb).le(v, 2);
|
|
27
|
+
if (v <= U32_MAX)
|
|
28
|
+
return this.u8(0xfc).le(v, 4);
|
|
29
|
+
if (v <= U64_MAX)
|
|
30
|
+
return this.u8(0xfd).le(v, 8);
|
|
31
|
+
return this.u8(0xfe).le(v, 16);
|
|
32
|
+
}
|
|
33
|
+
le(value, width) { for (let i = 0; i < width; i++) {
|
|
34
|
+
this.chunks.push(Number(value >> BigInt(i * 8) & 0xffn));
|
|
35
|
+
} return this; }
|
|
36
|
+
string(value) { const bytes = new TextEncoder().encode(value); return this.varint(bytes.length).raw(bytes); }
|
|
37
|
+
option(value, write) { if (value == null)
|
|
38
|
+
return this.u8(0); this.u8(1); write(this, value); return this; }
|
|
39
|
+
vec(values, write) { this.varint(values.length); for (const value of values)
|
|
40
|
+
write(this, value); return this; }
|
|
41
|
+
}
|
|
42
|
+
/** A fresh ArrayBuffer holding exactly `bytes`. Not `bytes.slice().buffer`: Node's Buffer.slice() is a view, so `.buffer` would be the whole shared pool. */
|
|
43
|
+
export const asBuffer = (bytes) => new Uint8Array(bytes).buffer;
|
|
44
|
+
export function toHex(bytes) { return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join(""); }
|
|
45
|
+
export function fromHex(hex) { if (hex.length % 2 || !/^[0-9a-f]*$/i.test(hex))
|
|
46
|
+
throw new Error("invalid hex"); return Uint8Array.from(hex.match(/../g)?.map((byte) => Number.parseInt(byte, 16)) ?? []); }
|
|
47
|
+
/** Reads what `Writer` writes. Pair every decode with a re-encode check (see `decodePayload`) rather than trusting it to reject non-canonical input. */
|
|
48
|
+
export class Reader {
|
|
49
|
+
data;
|
|
50
|
+
at = 0;
|
|
51
|
+
constructor(data) {
|
|
52
|
+
this.data = data;
|
|
53
|
+
}
|
|
54
|
+
u8() { if (this.at >= this.data.length)
|
|
55
|
+
throw new RangeError("unexpected end of payload"); return this.data[this.at++]; }
|
|
56
|
+
bool() { const value = this.u8(); if (value > 1)
|
|
57
|
+
throw new RangeError(`invalid bool: ${value}`); return value === 1; }
|
|
58
|
+
varint() {
|
|
59
|
+
const tag = this.u8();
|
|
60
|
+
if (tag <= 250)
|
|
61
|
+
return BigInt(tag);
|
|
62
|
+
const width = { 0xfb: 2, 0xfc: 4, 0xfd: 8, 0xfe: 16 }[tag];
|
|
63
|
+
if (!width)
|
|
64
|
+
throw new RangeError(`invalid varint tag: ${tag}`);
|
|
65
|
+
let value = 0n;
|
|
66
|
+
for (let i = 0; i < width; i++)
|
|
67
|
+
value |= BigInt(this.u8()) << BigInt(i * 8);
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
length() { const len = this.varint(); if (len > BigInt(this.data.length - this.at))
|
|
71
|
+
throw new RangeError("length past end of payload"); return Number(len); }
|
|
72
|
+
string() { const len = this.length(), text = new TextDecoder("utf-8", { fatal: true }).decode(this.data.subarray(this.at, this.at + len)); this.at += len; return text; }
|
|
73
|
+
option(read) { return this.bool() ? read(this) : null; }
|
|
74
|
+
vec(read) { return Array.from({ length: this.length() }, () => read(this)); }
|
|
75
|
+
done() { if (this.at !== this.data.length)
|
|
76
|
+
throw new RangeError("trailing bytes after payload"); }
|
|
77
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
import { stdin as input, stderr, stdout } from "node:process";
|
|
5
|
+
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
6
|
+
import { ArxiumRpc, RpcError, encodeApplyAttestor, encodeBlockAttestor, encodeExecuteProposal, encodeSubmitProposal, encodeVoteProposal, encodeAuthorizeOperator, encodeGrantAttestation, encodeIssueAsset, encodeRegisterAsset, encodeTransferAsset, encodeJoinValidator, encodeLeaveValidator, encodeRegisterBlsKey, encodeRevokeAttestation, encodeRevokeOperator, encodeStake, encodeTransfer, encodeUnstake, generateKey, isKeyFile, signAction, submitBody, unlockKey, verifyKeyFile, verifySignedAction, wrapPkcs8, encodeAddress, toHex, toKeyFile, PKCS8_ED25519_PREFIX, assemblePolicySignature, decodePayload, encodePayload, fromHex, multisigAddress, multisigSignature, signingBytes, asBuffer, } from "./index.js";
|
|
7
|
+
class UsageError extends Error {
|
|
8
|
+
}
|
|
9
|
+
function output(value, json) { stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : typeof value === "string" ? `${value}\n` : `${JSON.stringify(value, null, 2)}\n`); }
|
|
10
|
+
function parseGlobals(args) { let rpc = process.env.ARX_RPC ?? "http://127.0.0.1:8080", token = process.env.ARX_TOKEN, key = process.env.ARX_KEY, json = false, nonce; const rest = []; for (let i = 0; i < args.length; i++) {
|
|
11
|
+
const arg = args[i];
|
|
12
|
+
if (arg === "--rpc")
|
|
13
|
+
rpc = args[++i] ?? throwUsage("--rpc needs a value");
|
|
14
|
+
else if (arg === "--token")
|
|
15
|
+
token = args[++i] ?? throwUsage("--token needs a value");
|
|
16
|
+
else if (arg === "--key")
|
|
17
|
+
key = args[++i] ?? throwUsage("--key needs a value");
|
|
18
|
+
else if (arg === "--json")
|
|
19
|
+
json = true;
|
|
20
|
+
else if (arg === "--nonce") {
|
|
21
|
+
const value = args[++i] ?? "";
|
|
22
|
+
if (!/^\d+$/.test(value))
|
|
23
|
+
throwUsage("--nonce needs a non-negative integer");
|
|
24
|
+
nonce = Number(value);
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
rest.push(arg);
|
|
28
|
+
} return { globals: { rpc, token, key, json, nonce }, args: rest }; }
|
|
29
|
+
function throwUsage(message) { throw new UsageError(message); }
|
|
30
|
+
// Only stdin must be a terminal: the prompt goes to stderr, so `arx send … | jq` still prompts.
|
|
31
|
+
function prompt(label) { return new Promise((resolve) => { stderr.write(label); input.setRawMode(true); input.resume(); let value = ""; const onData = (chunk) => { for (const char of chunk.toString("utf8")) {
|
|
32
|
+
if (char === "\r" || char === "\n") {
|
|
33
|
+
input.off("data", onData);
|
|
34
|
+
input.setRawMode(false);
|
|
35
|
+
input.pause();
|
|
36
|
+
stderr.write("\n");
|
|
37
|
+
resolve(value);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (char === "\u0003")
|
|
41
|
+
process.exit(130);
|
|
42
|
+
if (char === "\u007f" || char === "\b")
|
|
43
|
+
value = value.slice(0, -1);
|
|
44
|
+
else
|
|
45
|
+
value += char;
|
|
46
|
+
} }; input.on("data", onData); }); }
|
|
47
|
+
async function passphrase(confirm = false) { if (process.env.ARX_PASSPHRASE)
|
|
48
|
+
return process.env.ARX_PASSPHRASE; if (!input.isTTY)
|
|
49
|
+
throw new UsageError("no passphrase: set ARX_PASSPHRASE or run interactively"); const value = await prompt("Passphrase: "); if (confirm && await prompt("Confirm passphrase: ") !== value)
|
|
50
|
+
throw new UsageError("passphrases do not match"); return value; }
|
|
51
|
+
async function readKeyFile(path) { if (!path)
|
|
52
|
+
throw new UsageError("--key or ARX_KEY is required"); const parsed = JSON.parse(await readFile(path, "utf8")); if (!isKeyFile(parsed))
|
|
53
|
+
throw new UsageError("invalid key file (or its address does not match its publicKey)"); return parsed; }
|
|
54
|
+
async function loadKey(path) { const file = await readKeyFile(path); try {
|
|
55
|
+
const privateKey = await unlockKey(file, await passphrase());
|
|
56
|
+
await verifyKeyFile(file, privateKey);
|
|
57
|
+
return { file, privateKey };
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (error instanceof UsageError)
|
|
61
|
+
throw error;
|
|
62
|
+
throw new UsageError(error instanceof Error ? error.message : String(error));
|
|
63
|
+
} }
|
|
64
|
+
async function readStdin() { const chunks = []; for await (const chunk of input)
|
|
65
|
+
chunks.push(Buffer.from(chunk)); return Buffer.concat(chunks).toString("utf8"); }
|
|
66
|
+
function payload(action, args) { const topics = (list) => list ? list.split(",").map((topic) => ["kyc", "aml", "accredited", "jurisdiction"].includes(topic) ? topic : throwUsage(`unknown topic: ${topic} (kyc, aml, accredited, jurisdiction)`)) : []; const amount = (index) => { const value = args[index]; if (!value || !/^\d+$/.test(value))
|
|
67
|
+
throwUsage("amount must be an integer IUM value"); return BigInt(value); }; switch (action) {
|
|
68
|
+
case "apply-attestor": return encodeApplyAttestor(args[0] ?? throwUsage("apply-attestor needs <name> <owner,owner,...> <threshold> <evidence-hash> <evidence-uri>"), (args[1] ?? throwUsage("missing owners")).split(","), Number(args[2] ?? throwUsage("missing threshold")), args[3] ?? throwUsage("missing evidence hash"), args[4] ?? throwUsage("missing evidence uri"));
|
|
69
|
+
case "block-attestor": return encodeBlockAttestor();
|
|
70
|
+
case "propose-add-attestor": return encodeSubmitProposal({ kind: "addAttestor", attestor: args[0] ?? throwUsage("propose-add-attestor needs <attestor> <name> <owner,owner,...> <threshold> <evidence-hash> <evidence-uri> <description>"), name: args[1] ?? throwUsage("missing name"), owners: (args[2] ?? throwUsage("missing owners")).split(","), threshold: Number(args[3] ?? throwUsage("missing threshold")), evidenceHash: args[4] ?? throwUsage("missing evidence hash"), evidenceUri: args[5] ?? throwUsage("missing evidence uri") }, args[6] ?? throwUsage("missing description"));
|
|
71
|
+
case "propose-remove-attestor":
|
|
72
|
+
case "propose-unblock-attestor": return encodeSubmitProposal({ kind: action === "propose-remove-attestor" ? "removeAttestor" : "unblockAttestor", attestor: args[0] ?? throwUsage(`${action} needs <attestor> <description>`) }, args[1] ?? throwUsage("missing description"));
|
|
73
|
+
case "vote": {
|
|
74
|
+
const id = args[0], choice = args[1];
|
|
75
|
+
if (!id || !/^\d+$/.test(id) || (choice !== "yes" && choice !== "no"))
|
|
76
|
+
throwUsage("vote needs <proposal-id> yes|no");
|
|
77
|
+
return encodeVoteProposal(BigInt(id), choice === "yes");
|
|
78
|
+
}
|
|
79
|
+
case "execute-proposal": {
|
|
80
|
+
const id = args[0];
|
|
81
|
+
if (!id || !/^\d+$/.test(id))
|
|
82
|
+
throwUsage("execute-proposal needs <proposal-id>");
|
|
83
|
+
return encodeExecuteProposal(BigInt(id));
|
|
84
|
+
}
|
|
85
|
+
case "transfer": return encodeTransfer(args[0] ?? throwUsage("transfer needs <to> <ium>"), amount(1));
|
|
86
|
+
case "stake": return encodeStake(args[0] ?? throwUsage("stake needs <validator> <ium>"), amount(1));
|
|
87
|
+
case "unstake": return encodeUnstake(args[0] ?? throwUsage("unstake needs <validator> <ium>"), amount(1));
|
|
88
|
+
case "join-validator": return encodeJoinValidator(args[0] ?? throwUsage("join-validator needs <validator> <ium> <bls-pubkey-hex> <bls-pop-hex>"), amount(1), hexArg(args[2]), hexArg(args[3]));
|
|
89
|
+
case "leave-validator": return encodeLeaveValidator(args[0] ?? throwUsage("leave-validator needs <validator>"));
|
|
90
|
+
case "register-bls-key": return encodeRegisterBlsKey(args[0] ?? throwUsage("register-bls-key needs <validator> <pubkey-hex> <pop-hex>"), hexArg(args[1]), hexArg(args[2]));
|
|
91
|
+
case "authorize-operator": return encodeAuthorizeOperator(args[0] ?? throwUsage("authorize-operator needs <address>"));
|
|
92
|
+
case "revoke-operator": return encodeRevokeOperator();
|
|
93
|
+
case "grant-attestation": return encodeGrantAttestation(args[0] ?? throwUsage("grant-attestation needs <subject> <hash> [topics] [jurisdiction]"), args[1] ?? throwUsage("grant-attestation needs <subject> <hash>"), topics(args[2]), args[3] ?? null);
|
|
94
|
+
case "revoke-attestation": return encodeRevokeAttestation(args[0] ?? throwUsage("revoke-attestation needs <subject>"));
|
|
95
|
+
case "register-asset": {
|
|
96
|
+
const [assetId, symbol, name, decimals] = args;
|
|
97
|
+
if (!assetId || !symbol || !name || !/^\d+$/.test(decimals ?? ""))
|
|
98
|
+
throwUsage("register-asset needs <asset-id> <symbol> <name> <decimals> [topics] [jurisdictions]");
|
|
99
|
+
return encodeRegisterAsset(assetId, false, { asset_class: "other", decimals: Number(decimals), required_claims: topics(args[4]), allowed_jurisdictions: args[5] ? args[5].split(",") : null, max_supply: null, metadata_uri: null, symbol, name });
|
|
100
|
+
}
|
|
101
|
+
case "issue-asset": return encodeIssueAsset(args[0] ?? throwUsage("issue-asset needs <asset-ref> <amount>"), amount(1));
|
|
102
|
+
case "transfer-asset": return encodeTransferAsset(args[0] ?? throwUsage("transfer-asset needs <asset-ref> <to> <amount>"), args[1] ?? throwUsage("transfer-asset needs <asset-ref> <to> <amount>"), amount(2));
|
|
103
|
+
default: throwUsage(`unsupported action: ${action}`);
|
|
104
|
+
} }
|
|
105
|
+
function hexArg(value) { if (!value || !/^[0-9a-f]*$/i.test(value) || value.length % 2)
|
|
106
|
+
throwUsage("expected even-length hex"); return Uint8Array.from(value.match(/../g)?.map((byte) => Number.parseInt(byte, 16)) ?? []); }
|
|
107
|
+
async function main() {
|
|
108
|
+
const { globals, args } = parseGlobals(process.argv.slice(2));
|
|
109
|
+
const [command, ...rest] = args;
|
|
110
|
+
if (command === "--version") {
|
|
111
|
+
const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
112
|
+
output(`arx ${pkg.version}`, false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!command)
|
|
116
|
+
throwUsage("use: arx keys|sign|submit|send|query|verify");
|
|
117
|
+
if (command === "keys") {
|
|
118
|
+
const [operation, ...keyArgs] = rest;
|
|
119
|
+
if (operation === "new") {
|
|
120
|
+
const pass = await passphrase(true), generated = await generateKey(pass), file = toKeyFile(generated), destination = keyArgs[0] ?? `arxium-devnet-key-${generated.address.slice(4, 12)}.json`;
|
|
121
|
+
await writeFile(destination, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
122
|
+
output({ path: destination, address: file.address, publicKey: file.publicKey, network: file.network }, globals.json);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (operation === "import" && keyArgs[0] === "--file") {
|
|
126
|
+
const source = keyArgs[1] ?? throwUsage("keys import --file needs a path"), parsed = await readKeyFile(source);
|
|
127
|
+
const destination = keyArgs[2] ?? basename(source);
|
|
128
|
+
await writeFile(destination, `${JSON.stringify(parsed, null, 2)}\n`, { mode: 0o600 });
|
|
129
|
+
output({ path: destination, address: parsed.address }, globals.json);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (operation === "import") {
|
|
133
|
+
if (input.isTTY)
|
|
134
|
+
throwUsage("pipe a 32-byte hex seed to `arx keys import`; seeds are never command arguments");
|
|
135
|
+
const seed = (await readStdin()).trim();
|
|
136
|
+
if (!/^[0-9a-f]{64}$/i.test(seed))
|
|
137
|
+
throwUsage("seed must be 32 bytes of hex");
|
|
138
|
+
const pkcs8 = Buffer.from(PKCS8_ED25519_PREFIX + seed, "hex"), publicJwk = createPublicKey(createPrivateKey({ key: pkcs8, format: "der", type: "pkcs8" })).export({ format: "jwk" });
|
|
139
|
+
if (!publicJwk.x)
|
|
140
|
+
throw new Error("could not derive public key from seed");
|
|
141
|
+
const publicKey = Buffer.from(publicJwk.x, "base64url"), address = encodeAddress(publicKey), file = toKeyFile({ address, publicKey: toHex(publicKey), blob: await wrapPkcs8(pkcs8, await passphrase(true)) }), destination = keyArgs[0] ?? `arxium-devnet-key-${address.slice(4, 12)}.json`;
|
|
142
|
+
await writeFile(destination, `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
|
|
143
|
+
output({ path: destination, address }, globals.json);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (operation === "show") {
|
|
147
|
+
const file = await readKeyFile(globals.key);
|
|
148
|
+
output({ address: file.address, publicKey: file.publicKey, network: file.network }, globals.json);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throwUsage("use: arx keys new|import [destination]|import --file <file>|show");
|
|
152
|
+
}
|
|
153
|
+
const rpc = new ArxiumRpc(globals);
|
|
154
|
+
if (command === "multisig") {
|
|
155
|
+
const [operation, source, ...extra] = rest;
|
|
156
|
+
if (operation === "address") {
|
|
157
|
+
output(await multisigAddress(Number(source), extra.map(fromHex)), globals.json);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!source)
|
|
161
|
+
throwUsage("multisig build|sign|combine|link needs a JSON file");
|
|
162
|
+
const value = JSON.parse(await readFile(source, "utf8"));
|
|
163
|
+
if (operation === "build") {
|
|
164
|
+
const members = value.members.map(m => m.toLowerCase()).sort();
|
|
165
|
+
const derived = await multisigAddress(value.threshold, members.map(fromHex));
|
|
166
|
+
const sender = value.sender ?? derived;
|
|
167
|
+
if (!value.mode && sender !== derived)
|
|
168
|
+
throwUsage("legacy policy does not derive sender");
|
|
169
|
+
if (value.mode && !["owner", "guardian"].includes(value.mode))
|
|
170
|
+
throwUsage("invalid policy mode");
|
|
171
|
+
const nonce = globals.nonce ?? value.nonce ?? await rpc.account(sender).then(a => a.nonce);
|
|
172
|
+
if (!Number.isSafeInteger(nonce) || nonce < 0)
|
|
173
|
+
throwUsage("nonce must be a safe non-negative integer");
|
|
174
|
+
output({ genesis: toHex(await rpc.genesisHash()), sender, nonce, threshold: value.threshold, members, payload: toHex(encodePayload(value.payload)), sigs: [], ...(value.mode ? { mode: value.mode } : {}) }, true);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const r = value;
|
|
178
|
+
if (!Number.isSafeInteger(r.nonce) || r.nonce < 0 || (r.mode !== undefined && !["owner", "guardian"].includes(r.mode)))
|
|
179
|
+
throwUsage("invalid co-sign request");
|
|
180
|
+
const encoded = fromHex(r.payload), decoded = decodePayload(encoded), derived = await multisigAddress(r.threshold, r.members.map(fromHex));
|
|
181
|
+
if (r.mode) {
|
|
182
|
+
const current = await rpc.account(r.sender);
|
|
183
|
+
const p = r.mode === "owner" ? current.programmable?.policy.owners : current.programmable?.policy.recovery?.guardians;
|
|
184
|
+
if (!p || await multisigAddress(p.threshold, p.members.map(m => Uint8Array.from(m))) !== derived)
|
|
185
|
+
throwUsage("request is not authorized by the current account policy");
|
|
186
|
+
}
|
|
187
|
+
else if (r.sender !== derived)
|
|
188
|
+
throwUsage("policy does not derive sender");
|
|
189
|
+
const genesis = fromHex(r.genesis);
|
|
190
|
+
if (toHex(genesis) !== toHex(await rpc.genesisHash()))
|
|
191
|
+
throwUsage("request was built for a different chain than --rpc");
|
|
192
|
+
const message = signingBytes(genesis, r.sender, r.nonce, encoded);
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
for (const [member, sig] of r.sigs) {
|
|
195
|
+
if (!r.members.includes(member) || seen.has(member))
|
|
196
|
+
throwUsage("duplicate or non-member signature");
|
|
197
|
+
seen.add(member);
|
|
198
|
+
const key = await crypto.subtle.importKey("raw", asBuffer(fromHex(member)), "Ed25519", false, ["verify"]);
|
|
199
|
+
if (!await crypto.subtle.verify("Ed25519", key, asBuffer(fromHex(sig)), asBuffer(message)))
|
|
200
|
+
throwUsage("invalid member signature");
|
|
201
|
+
}
|
|
202
|
+
if (operation === "sign") {
|
|
203
|
+
const { file, privateKey } = await loadKey(globals.key);
|
|
204
|
+
if (!r.members.includes(file.publicKey) || seen.has(file.publicKey))
|
|
205
|
+
throwUsage("key is not an unsigned member");
|
|
206
|
+
stderr.write(`Signing ${JSON.stringify(decoded)} for ${r.sender} at nonce ${r.nonce}\n`);
|
|
207
|
+
output({ ...r, sigs: [...r.sigs, [file.publicKey, await signAction(privateKey, genesis, r.sender, r.nonce, encoded)]] }, true);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (operation === "combine") {
|
|
211
|
+
if (r.sigs.length !== r.threshold)
|
|
212
|
+
throwUsage("need exactly threshold signatures");
|
|
213
|
+
const sig = r.mode ? assemblePolicySignature(r.mode, { threshold: r.threshold, members: r.members }, r.sigs) : multisigSignature(r.threshold, r.members.map(fromHex), r.sigs.map(([m, s]) => [fromHex(m), s]));
|
|
214
|
+
output(submitBody(r.sender, r.nonce, sig, encoded), true);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (operation === "link") {
|
|
218
|
+
output(`${extra[0] ?? throwUsage("link needs the Console co-sign page URL")}#${Buffer.from(JSON.stringify(r)).toString("base64url")}`, globals.json);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
throwUsage("use: arx multisig address|build|sign|combine|link");
|
|
222
|
+
}
|
|
223
|
+
if (command === "sign" || command === "send") {
|
|
224
|
+
const action = rest[0] ?? throwUsage(`${command} needs an action`), { file, privateKey } = await loadKey(globals.key), encoded = payload(action, rest.slice(1));
|
|
225
|
+
if (command === "sign") {
|
|
226
|
+
const nonce = globals.nonce ?? await rpc.account(file.address).then((account) => account.nonce, (error) => { if (error instanceof RpcError && error.status === 404)
|
|
227
|
+
return 0; throw error; });
|
|
228
|
+
output(submitBody(file.address, nonce, await signAction(privateKey, await rpc.genesisHash(), file.address, nonce, encoded), encoded), true);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
output(await rpc.sendAction({ privateKey, sender: file.address, payload: encoded, finalized: true }), globals.json);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (command === "submit") {
|
|
235
|
+
const source = rest[0] ?? throwUsage("submit needs <file|->"), body = JSON.parse(source === "-" ? await readStdin() : await readFile(source, "utf8"));
|
|
236
|
+
output(await rpc.submit(body) ?? { submitted: body.signature }, globals.json);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (command === "verify") {
|
|
240
|
+
const signature = rest[0] ?? throwUsage("verify needs <signature>"), status = await rpc.action(signature);
|
|
241
|
+
if (status.status !== "confirmed")
|
|
242
|
+
throw new Error(`action is ${status.status}`);
|
|
243
|
+
const block = await rpc.block(status.height);
|
|
244
|
+
const action = block.actions?.find((item) => item.signature === signature);
|
|
245
|
+
let preStatePolicy;
|
|
246
|
+
if (signature.length > 128 && /^a7(00|01|02)/.test(signature)) {
|
|
247
|
+
if (rest[1] !== "--policy" || !rest[2])
|
|
248
|
+
throwUsage("stateful verification needs --policy <JSON file> containing the proven pre-action policy");
|
|
249
|
+
preStatePolicy = JSON.parse(await readFile(rest[2], "utf8"));
|
|
250
|
+
}
|
|
251
|
+
if (!block.finalized || !action || !await verifySignedAction(action, await rpc.genesisHash(), preStatePolicy))
|
|
252
|
+
throw new Error("action is not finalized with a valid signature");
|
|
253
|
+
output({ signature, finalized: true, signatureValid: true, height: status.height }, globals.json);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (command === "query") {
|
|
257
|
+
const [operation, value] = rest;
|
|
258
|
+
const result = operation === "status" ? await rpc.status() : operation === "account" ? await rpc.account(value ?? throwUsage("account needs an address")) : operation === "block" ? (/^\d+$/.test(value ?? "") ? await rpc.block(Number(value)) : await rpc.blockByHash(value ?? throwUsage("block needs a height or hash"))) : operation === "action" ? await rpc.action(value ?? throwUsage("action needs a signature")) : operation === "validators" ? await rpc.validators() : operation === "finality" ? await rpc.finality() : operation === "search" ? await rpc.search(value ?? throwUsage("search needs a query")) : throwUsage("use: arx query status|account|block|action|validators|finality|search");
|
|
259
|
+
output(result, globals.json);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
throwUsage(`unknown command: ${command}`);
|
|
263
|
+
}
|
|
264
|
+
main().catch((error) => { const usage = error instanceof UsageError; stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = usage ? 2 : 1; });
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const PBKDF2_ITERATIONS = 600000;
|
|
2
|
+
export declare const PKCS8_ED25519_PREFIX = "302e020100300506032b657004220420";
|
|
3
|
+
export type KeyBlob = {
|
|
4
|
+
version: 1;
|
|
5
|
+
kdf: "PBKDF2-SHA256";
|
|
6
|
+
iterations: number;
|
|
7
|
+
salt: string;
|
|
8
|
+
iv: string;
|
|
9
|
+
ciphertext: string;
|
|
10
|
+
};
|
|
11
|
+
/** The exact JSON document emitted by Console's `exportKey`. `network` is display metadata, not a signing domain. */
|
|
12
|
+
export type KeyFile = KeyBlob & {
|
|
13
|
+
address: string;
|
|
14
|
+
publicKey: string;
|
|
15
|
+
network: string;
|
|
16
|
+
};
|
|
17
|
+
export type GeneratedKey = {
|
|
18
|
+
address: string;
|
|
19
|
+
publicKey: string;
|
|
20
|
+
privateKey: CryptoKey;
|
|
21
|
+
blob: KeyBlob;
|
|
22
|
+
};
|
|
23
|
+
export declare function generateKey(passphrase: string): Promise<GeneratedKey>;
|
|
24
|
+
export declare function wrapPkcs8(pkcs8: Uint8Array, passphrase: string): Promise<KeyBlob>;
|
|
25
|
+
/** Also checks `address` is the bech32 of `publicKey`, so a tampered file is rejected before any passphrase is asked for. */
|
|
26
|
+
export declare function isKeyFile(value: unknown): value is KeyFile;
|
|
27
|
+
/** The Console "Export key" / `arx keys new` document. Field order matches Console's historical export. */
|
|
28
|
+
export declare function toKeyFile(key: {
|
|
29
|
+
address: string;
|
|
30
|
+
publicKey: string;
|
|
31
|
+
blob: KeyBlob;
|
|
32
|
+
}, network?: string): KeyFile;
|
|
33
|
+
export declare function unlockKey(blob: KeyBlob, passphrase: string): Promise<CryptoKey>;
|
|
34
|
+
export declare function rewrapKey(blob: KeyBlob, oldPassphrase: string, newPassphrase: string): Promise<KeyBlob>;
|
|
35
|
+
/** Confirms the decrypted private key belongs to the public address declared by the export. */
|
|
36
|
+
export declare function verifyKeyFile(file: KeyFile, privateKey: CryptoKey): Promise<void>;
|
|
37
|
+
export declare function importSeed(seedHex: string): Promise<CryptoKey>;
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { encodeAddress } from "./bech32.js";
|
|
2
|
+
import { asBuffer, fromHex, toHex } from "./bincode.js";
|
|
3
|
+
export const PBKDF2_ITERATIONS = 600_000;
|
|
4
|
+
export const PKCS8_ED25519_PREFIX = "302e020100300506032b657004220420";
|
|
5
|
+
const encodeBase64 = (bytes) => typeof btoa === "function" ? btoa(String.fromCharCode(...bytes)) : Buffer.from(bytes).toString("base64");
|
|
6
|
+
const decodeBase64 = (value) => typeof atob === "function" ? Uint8Array.from(atob(value), (char) => char.charCodeAt(0)) : new Uint8Array(Buffer.from(value, "base64"));
|
|
7
|
+
async function wrappingKey(passphrase, salt, iterations) { const material = await crypto.subtle.importKey("raw", asBuffer(new TextEncoder().encode(passphrase)), "PBKDF2", false, ["deriveKey"]); return crypto.subtle.deriveKey({ name: "PBKDF2", hash: "SHA-256", salt: asBuffer(salt), iterations }, material, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); }
|
|
8
|
+
export async function generateKey(passphrase) { const pair = await crypto.subtle.generateKey("Ed25519", true, ["sign", "verify"]), publicRaw = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey)), pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", pair.privateKey)), salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await wrappingKey(passphrase, salt, PBKDF2_ITERATIONS), pkcs8)); const privateKey = await crypto.subtle.importKey("pkcs8", pkcs8, "Ed25519", false, ["sign"]); pkcs8.fill(0); return { address: encodeAddress(publicRaw), publicKey: toHex(publicRaw), privateKey, blob: { version: 1, kdf: "PBKDF2-SHA256", iterations: PBKDF2_ITERATIONS, salt: encodeBase64(salt), iv: encodeBase64(iv), ciphertext: encodeBase64(ciphertext) } }; }
|
|
9
|
+
export async function wrapPkcs8(pkcs8, passphrase) { const salt = crypto.getRandomValues(new Uint8Array(16)), iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: "AES-GCM", iv }, await wrappingKey(passphrase, salt, PBKDF2_ITERATIONS), asBuffer(pkcs8))); return { version: 1, kdf: "PBKDF2-SHA256", iterations: PBKDF2_ITERATIONS, salt: encodeBase64(salt), iv: encodeBase64(iv), ciphertext: encodeBase64(ciphertext) }; }
|
|
10
|
+
/** Also checks `address` is the bech32 of `publicKey`, so a tampered file is rejected before any passphrase is asked for. */
|
|
11
|
+
export function isKeyFile(value) { const key = value; return !!key && key.version === 1 && key.kdf === "PBKDF2-SHA256" && typeof key.address === "string" && typeof key.publicKey === "string" && /^[0-9a-f]{64}$/i.test(key.publicKey) && encodeAddress(fromHex(key.publicKey)) === key.address && typeof key.network === "string" && typeof key.iterations === "number" && Number.isInteger(key.iterations) && key.iterations >= PBKDF2_ITERATIONS && [key.salt, key.iv, key.ciphertext].every((field) => typeof field === "string" && field.length > 0); }
|
|
12
|
+
/** The Console "Export key" / `arx keys new` document. Field order matches Console's historical export. */
|
|
13
|
+
export function toKeyFile(key, network = "arx-devnet") { return { address: key.address, publicKey: key.publicKey, ...key.blob, network }; }
|
|
14
|
+
export async function unlockKey(blob, passphrase) { try {
|
|
15
|
+
const pkcs8 = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: asBuffer(decodeBase64(blob.iv)) }, await wrappingKey(passphrase, decodeBase64(blob.salt), blob.iterations), asBuffer(decodeBase64(blob.ciphertext))));
|
|
16
|
+
const key = await crypto.subtle.importKey("pkcs8", asBuffer(pkcs8), "Ed25519", false, ["sign"]);
|
|
17
|
+
pkcs8.fill(0);
|
|
18
|
+
return key;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
throw new Error("wrong passphrase");
|
|
22
|
+
} }
|
|
23
|
+
export async function rewrapKey(blob, oldPassphrase, newPassphrase) { try {
|
|
24
|
+
const pkcs8 = new Uint8Array(await crypto.subtle.decrypt({ name: "AES-GCM", iv: asBuffer(decodeBase64(blob.iv)) }, await wrappingKey(oldPassphrase, decodeBase64(blob.salt), blob.iterations), asBuffer(decodeBase64(blob.ciphertext))));
|
|
25
|
+
const wrapped = await wrapPkcs8(pkcs8, newPassphrase);
|
|
26
|
+
pkcs8.fill(0);
|
|
27
|
+
return wrapped;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error("wrong passphrase");
|
|
31
|
+
} }
|
|
32
|
+
/** Confirms the decrypted private key belongs to the public address declared by the export. */
|
|
33
|
+
export async function verifyKeyFile(file, privateKey) { const message = new TextEncoder().encode("arxium key file verification"); const signature = await crypto.subtle.sign("Ed25519", privateKey, asBuffer(message)); const publicKey = await crypto.subtle.importKey("raw", asBuffer(fromHex(file.publicKey)), "Ed25519", false, ["verify"]); if (encodeAddress(fromHex(file.publicKey)) !== file.address || !await crypto.subtle.verify("Ed25519", publicKey, signature, asBuffer(message)))
|
|
34
|
+
throw new Error("key file address does not match decrypted key"); }
|
|
35
|
+
export async function importSeed(seedHex) { if (!/^[0-9a-f]{64}$/i.test(seedHex))
|
|
36
|
+
throw new Error("seed must be 32 bytes of hex"); return crypto.subtle.importKey("pkcs8", asBuffer(fromHex(PKCS8_ED25519_PREFIX + seedHex)), "Ed25519", false, ["sign"]); }
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Reader } from "./bincode.js";
|
|
2
|
+
export declare const ACCOUNT_POLICY_ACTION = 40;
|
|
3
|
+
export declare const POLICY_VARIANT: {
|
|
4
|
+
readonly setAccountPolicy: 0;
|
|
5
|
+
readonly rotateAccountMembers: 1;
|
|
6
|
+
readonly addSessionKey: 2;
|
|
7
|
+
readonly revokeSessionKey: 3;
|
|
8
|
+
readonly startAccountRecovery: 4;
|
|
9
|
+
readonly cancelAccountRecovery: 5;
|
|
10
|
+
readonly executeAccountRecovery: 6;
|
|
11
|
+
};
|
|
12
|
+
export type ThresholdPolicy = {
|
|
13
|
+
threshold: number;
|
|
14
|
+
members: string[];
|
|
15
|
+
};
|
|
16
|
+
export type SpendAsset = "native" | {
|
|
17
|
+
asset: string;
|
|
18
|
+
};
|
|
19
|
+
export type SpendingLimit = {
|
|
20
|
+
asset: SpendAsset;
|
|
21
|
+
periodBlocks: string;
|
|
22
|
+
amount: string;
|
|
23
|
+
};
|
|
24
|
+
export type SessionKey = {
|
|
25
|
+
publicKey: string;
|
|
26
|
+
expiresAt: string;
|
|
27
|
+
asset: SpendAsset;
|
|
28
|
+
allowance: string;
|
|
29
|
+
recipients: string[];
|
|
30
|
+
};
|
|
31
|
+
export type AccountPolicy = {
|
|
32
|
+
owners: ThresholdPolicy;
|
|
33
|
+
limits: SpendingLimit[];
|
|
34
|
+
recipients: string[] | null;
|
|
35
|
+
sessions: SessionKey[];
|
|
36
|
+
recovery: {
|
|
37
|
+
guardians: ThresholdPolicy;
|
|
38
|
+
delayBlocks: string;
|
|
39
|
+
} | null;
|
|
40
|
+
};
|
|
41
|
+
export type PolicyMode = "owner" | "session" | "guardian";
|
|
42
|
+
export declare function encodeAccountPolicy(name: keyof typeof POLICY_VARIANT, input: Record<string, any>): Uint8Array;
|
|
43
|
+
export declare function readAccountPolicy(r: Reader): {
|
|
44
|
+
name: keyof typeof POLICY_VARIANT;
|
|
45
|
+
input: Record<string, any>;
|
|
46
|
+
};
|
|
47
|
+
/** Sign the stable account's envelope, not the signer's personal address. */
|
|
48
|
+
export declare function signPolicyMember(privateKey: CryptoKey, genesis: Uint8Array, sender: string, nonce: number, payload: Uint8Array): Promise<string>;
|
|
49
|
+
export declare function assemblePolicySignature(mode: Exclude<PolicyMode, "session">, policy: ThresholdPolicy, signatures: [string, string][]): string;
|
|
50
|
+
export declare function assembleSessionSignature(publicKey: string, signature: string): string;
|
|
51
|
+
/** Cryptographic verification only; use proven policy at the action's pre-state.
|
|
52
|
+
* The runtime also checks capability scope, expiry, nonce and spend counters. */
|
|
53
|
+
export declare function verifyPolicySignature(policy: AccountPolicy, signature: string, message: Uint8Array): Promise<PolicyMode | null>;
|
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
|
+
}
|