@arxiumlabs/sdk 0.9.0 → 0.14.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 +77 -3
- package/SIGNERS.md +132 -0
- package/dist/actions.d.ts +105 -2
- package/dist/actions.js +166 -6
- package/dist/cli.js +50 -5
- package/dist/fees.d.ts +11 -0
- package/dist/fees.js +35 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rpc.d.ts +128 -3
- package/dist/rpc.js +49 -12
- package/dist/signer.d.ts +63 -0
- package/dist/signer.js +107 -0
- package/package.json +4 -3
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { stdin as input, stderr, stdout } from "node:process";
|
|
5
5
|
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
6
|
-
import { ArxiumRpc, RpcError, encodeApplyAttestor, encodeBlockAttestor, encodeExecuteProposal, encodeSubmitProposal, encodeVoteProposal, encodeAuthorizeOperator, encodeGrantAttestation, encodeIssueAsset, encodeRegisterAsset, encodeFreezeAsset, encodeSetHolderFrozen, encodeSetAssetLimits, 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, ALL_ISSUER_CONTROLS, ISSUER_CONTROLS, bigintReplacer, encodeBurnAsset, encodeIssuerForcedTransfer, encodeRecoverHolder, encodeIssueAssetTo, encodeLockHolderAmount, encodeLockHolderAmountUntil, encodeLockIssuance, encodeTransferIssuer, encodeSetAssetMetadataUri, encodeSetPrivateClaims, encodeSnapshotHolders, encodeDistributeToHolders, encodeRedeemHolders, encodeSplitAsset, encodeCreateToken, encodeMintToken, encodeTransferToken, encodeBurnToken, encodeRenounceMint, encodeSettleDvp, signDvpConsent, } from "./index.js";
|
|
6
|
+
import { ArxiumRpc, RpcError, encodeApplyAttestor, encodeBlockAttestor, encodeExecuteProposal, encodeSubmitProposal, encodeVoteProposal, encodeAuthorizeOperator, encodeGrantAttestation, encodeIssueAsset, encodeRegisterAsset, encodeFreezeAsset, encodeSetHolderFrozen, encodeSetAssetLimits, 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, ALL_ISSUER_CONTROLS, ISSUER_CONTROLS, bigintReplacer, encodeBurnAsset, encodeIssuerForcedTransfer, encodeRecoverHolder, encodeIssueAssetTo, encodeLockHolderAmount, encodeLockHolderAmountUntil, encodeLockIssuance, encodeTransferIssuer, encodeSetAssetMetadataUri, encodeSetPrivateClaims, encodeSnapshotHolders, encodeDistributeToHolders, encodeRedeemHolders, encodeSplitAsset, encodeCreateToken, encodeMintToken, encodeTransferToken, encodeBurnToken, encodeRenounceMint, encodeSettleDvp, signDvpConsent, encodeDeployModule, encodeCallModule, encodeSettleVenue, encodeCancelVenueOrder, signVenueOrder, MAX_ORDER_LIFETIME, } from "./index.js";
|
|
7
7
|
class UsageError extends Error {
|
|
8
8
|
}
|
|
9
9
|
function output(value, json) { stdout.write(!json && typeof value === "string" ? `${value}\n` : `${JSON.stringify(value, bigintReplacer, 2)}\n`); }
|
|
@@ -124,6 +124,8 @@ const USAGE = {
|
|
|
124
124
|
"create-token": "<symbol> <name> <decimals> <initial-supply> <max-supply|-> mintable|fixed", "mint-token": "<token-ref> <to> <amount>",
|
|
125
125
|
"transfer-token": "<token-ref> <to> <amount>", "burn-token": "<token-ref> <amount>", "renounce-mint": "<token-ref>",
|
|
126
126
|
"settle-dvp": "<give-asset-ref|native> <give-amount> <counterparty> <take-asset-ref|native> <take-amount> <expires-at-height> <counterparty-signature>",
|
|
127
|
+
"venue-settle": "<trades.json>", "venue-cancel": "<order-id> <expires-at-height>",
|
|
128
|
+
"module-deploy": "<module.wasm> [salt]", "module-call": "<module-address> <gas-limit> [input-hex]",
|
|
127
129
|
};
|
|
128
130
|
const spendArg = (value) => value === "native" ? "native" : { asset: value };
|
|
129
131
|
function morePayload(action, args) {
|
|
@@ -161,6 +163,14 @@ function morePayload(action, args) {
|
|
|
161
163
|
case "transfer-token": return encodeTransferToken(arg(0), arg(1), int(2));
|
|
162
164
|
case "burn-token": return encodeBurnToken(arg(0), int(1));
|
|
163
165
|
case "settle-dvp": return encodeSettleDvp({ give: spendArg(arg(0)), giveAmount: int(1), counterparty: arg(2), take: spendArg(arg(3)), takeAmount: int(4), expiresAt: int(5) }, arg(6));
|
|
166
|
+
case "venue-settle": {
|
|
167
|
+
const raw = JSON.parse(arg(0));
|
|
168
|
+
const order = (o) => ({ owner: o.owner, orderId: BigInt(o.orderId), terms: { venue: o.terms.venue, give: o.terms.give, giveAmount: BigInt(o.terms.giveAmount), take: o.terms.take, takeAmount: BigInt(o.terms.takeAmount), expiresAt: BigInt(o.terms.expiresAt) }, signature: o.signature });
|
|
169
|
+
return encodeSettleVenue(raw.map((t) => ({ maker: order(t.maker), taker: order(t.taker) })));
|
|
170
|
+
}
|
|
171
|
+
case "venue-cancel": return encodeCancelVenueOrder(int(0), int(1));
|
|
172
|
+
case "module-deploy": return encodeDeployModule(hexArg(arg(0)), args[1] === undefined ? 0n : int(1));
|
|
173
|
+
case "module-call": return encodeCallModule(arg(0), args[2] === undefined ? new Uint8Array() : hexArg(args[2]), int(1));
|
|
164
174
|
default: return encodeRenounceMint(arg(0));
|
|
165
175
|
}
|
|
166
176
|
}
|
|
@@ -180,7 +190,7 @@ async function main() {
|
|
|
180
190
|
return;
|
|
181
191
|
}
|
|
182
192
|
if (!command)
|
|
183
|
-
throwUsage("use: arx keys|sign|submit|send|dvp-consent|query|verify");
|
|
193
|
+
throwUsage("use: arx keys|sign|submit|send|dvp-consent|venue-order|query|verify");
|
|
184
194
|
if (command === "keys") {
|
|
185
195
|
const [operation, ...keyArgs] = rest;
|
|
186
196
|
if (operation === "new") {
|
|
@@ -300,8 +310,31 @@ async function main() {
|
|
|
300
310
|
output({ counterparty: file.address, counterpartyNonce: nonce, signature, send: ["settle-dvp", give, giveAmount, file.address, take, takeAmount, expiresAt, signature] }, true);
|
|
301
311
|
return;
|
|
302
312
|
}
|
|
313
|
+
// One party's order for a venue: sign it with a fresh random id. The venue pairs two of these into `arx send venue-settle`.
|
|
314
|
+
if (command === "venue-order") {
|
|
315
|
+
const use = "venue-order needs <venue> <give-asset-ref|native> <give-amount> <take-asset-ref|native> <take-amount> <expires-at-height> [--order-id n]";
|
|
316
|
+
const idFlag = rest.indexOf("--order-id"), idText = idFlag >= 0 ? rest.splice(idFlag, 2)[1] : undefined;
|
|
317
|
+
if (idFlag >= 0 && !/^\d+$/.test(idText ?? ""))
|
|
318
|
+
throwUsage(use);
|
|
319
|
+
const orderId = idText !== undefined ? BigInt(idText) : new DataView(crypto.getRandomValues(new Uint8Array(8)).buffer).getBigUint64(0);
|
|
320
|
+
const [venueAddress, give, giveAmount, take, takeAmount, expiresAt] = rest;
|
|
321
|
+
if (!venueAddress || !give || !take || !/^\d+$/.test(giveAmount ?? "") || !/^\d+$/.test(takeAmount ?? "") || !/^\d+$/.test(expiresAt ?? ""))
|
|
322
|
+
throwUsage(use);
|
|
323
|
+
const tip = BigInt((await rpc.status()).tip_height);
|
|
324
|
+
if (BigInt(expiresAt) > tip + MAX_ORDER_LIFETIME)
|
|
325
|
+
throwUsage(`expires-at must be within ${MAX_ORDER_LIFETIME} blocks of the tip (${tip})`);
|
|
326
|
+
const { file, privateKey } = await loadKey(globals.key), genesis = await rpc.genesisHash();
|
|
327
|
+
const terms = { venue: venueAddress, give: spendArg(give), giveAmount: BigInt(giveAmount), take: spendArg(take), takeAmount: BigInt(takeAmount), expiresAt: BigInt(expiresAt) };
|
|
328
|
+
output({ owner: file.address, orderId, terms, signature: await signVenueOrder(privateKey, genesis, file.address, orderId, terms) }, true);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
303
331
|
if (command === "sign" || command === "send") {
|
|
304
|
-
const action = rest[0] ?? throwUsage(`${command} needs an action`), { file, privateKey } = await loadKey(globals.key),
|
|
332
|
+
const action = rest[0] ?? throwUsage(`${command} needs an action`), { file, privateKey } = await loadKey(globals.key), actionArgs = rest.slice(1);
|
|
333
|
+
if (action === "venue-settle")
|
|
334
|
+
actionArgs[0] = await readFile(actionArgs[0] ?? throwUsage("venue-settle needs <trades.json>"), "utf8");
|
|
335
|
+
if (action === "module-deploy")
|
|
336
|
+
actionArgs[0] = toHex(new Uint8Array(await readFile(actionArgs[0] ?? throwUsage("module-deploy needs <module.wasm>"))));
|
|
337
|
+
const encoded = payload(action, actionArgs);
|
|
305
338
|
if (command === "sign") {
|
|
306
339
|
const nonce = globals.nonce ?? await rpc.account(file.address).then((account) => account.nonce, (error) => { if (error instanceof RpcError && error.status === 404)
|
|
307
340
|
return 0; throw error; });
|
|
@@ -344,11 +377,23 @@ async function main() {
|
|
|
344
377
|
: operation === "attestor" ? await rpc.attestor(value ?? throwUsage("attestor needs an address"))
|
|
345
378
|
: operation === "validator" ? await rpc.validator(value ?? throwUsage("validator needs an address"))
|
|
346
379
|
: operation === "stakes" ? await rpc.stakes(value ?? throwUsage("stakes needs an address"))
|
|
347
|
-
: operation === "
|
|
348
|
-
: throwUsage("
|
|
380
|
+
: operation === "module" ? await rpc.module(value ?? throwUsage("module needs an address"))
|
|
381
|
+
: operation === "module-state" ? await rpc.moduleState(value ?? throwUsage("module-state needs <address> <key-hex>"), rest[2] ?? throwUsage("module-state needs <address> <key-hex>"))
|
|
382
|
+
: operation === "events" ? (await rpc.blockEffects(/^\d+$/.test(value ?? "") ? Number(value) : throwUsage("events needs <height> [module]"))).module_events.filter((event) => !rest[2] || event.module === rest[2])
|
|
383
|
+
: operation === "fee" ? await rpc.actionFee()
|
|
384
|
+
: throwUsage("use: arx query status|account|block|action|validators|validator|finality|search|assets [address]|asset|holders|balance|attestors|attestor|stakes|module|module-state|events|fee");
|
|
349
385
|
output(result, globals.json);
|
|
350
386
|
return;
|
|
351
387
|
}
|
|
388
|
+
// Dry-runs a module call as the key's address; no passphrase needed, nothing is signed or sent.
|
|
389
|
+
if (command === "simulate") {
|
|
390
|
+
const [module, gas, inputHex] = rest;
|
|
391
|
+
const caller = (await readKeyFile(globals.key)).address;
|
|
392
|
+
if (!module || !gas || !/^\d+$/.test(gas))
|
|
393
|
+
throwUsage("simulate needs <module-address> <gas-limit> [input-hex] and --key");
|
|
394
|
+
output(await rpc.simulateModule(module, { caller, input: inputHex === undefined ? new Uint8Array() : hexArg(inputHex), gasLimit: BigInt(gas) }), globals.json);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
352
397
|
throwUsage(`unknown command: ${command}`);
|
|
353
398
|
}
|
|
354
399
|
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/fees.d.ts
CHANGED
|
@@ -3,7 +3,18 @@ export type FeeParams = {
|
|
|
3
3
|
actionFee: bigint;
|
|
4
4
|
weightFee: bigint;
|
|
5
5
|
};
|
|
6
|
+
/** Fuel one weight unit pays for (`FUEL_PER_WEIGHT` in `arxd/runtime/src/module.rs`). */
|
|
7
|
+
export declare const FUEL_PER_WEIGHT = 50;
|
|
8
|
+
/** Dispute-artifact pricing for module actions (`BLOCK_PROOF_KEYS`, `PROOF_WEIGHT_PER_KEY`, `MODULE_BASE_KEYS`, `DEPLOY_WEIGHT_PER_CODE_BYTE`, `COST_READ` in metering.rs and `xc_wasm`). */
|
|
9
|
+
export declare const BLOCK_PROOF_KEYS = 64, PROOF_WEIGHT_PER_KEY = 15000, MODULE_BASE_KEYS = 5, DEPLOY_WEIGHT_PER_CODE_BYTE = 7, COST_READ = 1000;
|
|
6
10
|
/** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
|
|
11
|
+
/** Bytes a module state row counts for the storage deposit beyond its key and value (`STATE_ROW_OVERHEAD` in module.rs). */
|
|
12
|
+
export declare const STATE_ROW_OVERHEAD = 64;
|
|
13
|
+
/**
|
|
14
|
+
* IUM a module locks from its own balance to store `value` under `key` (or frees, for a row it deletes), at the chain's
|
|
15
|
+
* `module_storage_deposit_per_byte` (`rpc.actionFee()`). A refund is at the average price the module paid, so it can differ.
|
|
16
|
+
*/
|
|
17
|
+
export declare const moduleStorageDeposit: (keyBytes: number, valueBytes: number, perByte: bigint) => bigint;
|
|
7
18
|
export declare const POLICY_AUTHORIZATION_WEIGHT = 5000;
|
|
8
19
|
/** A single-key ed25519 signature: 64 bytes as hex. */
|
|
9
20
|
export declare const SINGLE_SIGNATURE_LENGTH = 128;
|
package/dist/fees.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ACTION_VARIANT, TOKEN_ACTION, TOKEN_VARIANT } from "./actions.js";
|
|
1
|
+
import { ACTION_VARIANT, COLLATERAL_ACTION, COLLATERAL_VARIANT, TOKEN_ACTION, TOKEN_VARIANT, VENUE_ACTION, VENUE_VARIANT } from "./actions.js";
|
|
2
2
|
import { Reader } from "./bincode.js";
|
|
3
3
|
import { ACCOUNT_POLICY_ACTION } from "./policy.js";
|
|
4
|
-
/** `base_weight` in metering.rs, by `ActionPayload` discriminant. Token and account-policy actions are handled below. */
|
|
4
|
+
/** `base_weight` in metering.rs, by `ActionPayload` discriminant. Token, collateral, venue and account-policy actions are handled below. */
|
|
5
5
|
const BASE_WEIGHT = {
|
|
6
6
|
[ACTION_VARIANT.transfer]: 50,
|
|
7
7
|
[ACTION_VARIANT.stake]: 100, [ACTION_VARIANT.unstake]: 100,
|
|
@@ -25,7 +25,18 @@ const BASE_WEIGHT = {
|
|
|
25
25
|
5 /* SubmitEquivocationEvidence */: 2_000,
|
|
26
26
|
15 /* SubmitExecutionFault */: 500_000,
|
|
27
27
|
};
|
|
28
|
+
/** Fuel one weight unit pays for (`FUEL_PER_WEIGHT` in `arxd/runtime/src/module.rs`). */
|
|
29
|
+
export const FUEL_PER_WEIGHT = 50;
|
|
30
|
+
/** Dispute-artifact pricing for module actions (`BLOCK_PROOF_KEYS`, `PROOF_WEIGHT_PER_KEY`, `MODULE_BASE_KEYS`, `DEPLOY_WEIGHT_PER_CODE_BYTE`, `COST_READ` in metering.rs and `xc_wasm`). */
|
|
31
|
+
export const BLOCK_PROOF_KEYS = 64, PROOF_WEIGHT_PER_KEY = 15_000, MODULE_BASE_KEYS = 5, DEPLOY_WEIGHT_PER_CODE_BYTE = 7, COST_READ = 1_000;
|
|
28
32
|
/** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
|
|
33
|
+
/** Bytes a module state row counts for the storage deposit beyond its key and value (`STATE_ROW_OVERHEAD` in module.rs). */
|
|
34
|
+
export const STATE_ROW_OVERHEAD = 64;
|
|
35
|
+
/**
|
|
36
|
+
* IUM a module locks from its own balance to store `value` under `key` (or frees, for a row it deletes), at the chain's
|
|
37
|
+
* `module_storage_deposit_per_byte` (`rpc.actionFee()`). A refund is at the average price the module paid, so it can differ.
|
|
38
|
+
*/
|
|
39
|
+
export const moduleStorageDeposit = (keyBytes, valueBytes, perByte) => BigInt(keyBytes + valueBytes + STATE_ROW_OVERHEAD) * perByte;
|
|
29
40
|
export const POLICY_AUTHORIZATION_WEIGHT = 5_000;
|
|
30
41
|
/** A single-key ed25519 signature: 64 bytes as hex. */
|
|
31
42
|
export const SINGLE_SIGNATURE_LENGTH = 128;
|
|
@@ -37,6 +48,28 @@ function baseWeight(payload) {
|
|
|
37
48
|
return Number(r.varint()) === TOKEN_VARIANT.createToken ? 150 : 100;
|
|
38
49
|
if (variant === ACCOUNT_POLICY_ACTION)
|
|
39
50
|
return 1_000;
|
|
51
|
+
if (variant === COLLATERAL_ACTION) {
|
|
52
|
+
const action = Number(r.varint());
|
|
53
|
+
return action === COLLATERAL_VARIANT.pledge || action === COLLATERAL_VARIANT.substitutePledge ? 350 : action === COLLATERAL_VARIANT.seizePledge ? 250 : 150;
|
|
54
|
+
}
|
|
55
|
+
if (variant === VENUE_ACTION) {
|
|
56
|
+
// `VENUE_SETTLE_BASE + trades × VENUE_TRADE_WEIGHT` in metering.rs; a cancel is flat.
|
|
57
|
+
return Number(r.varint()) === VENUE_VARIANT.settleVenue ? 100 + Number(r.varint()) * 800 : 100;
|
|
58
|
+
}
|
|
59
|
+
// `2_000 + 7 × code length` in metering.rs: the code rides in a dispute artifact about six times.
|
|
60
|
+
if (variant === ACTION_VARIANT.deployModule)
|
|
61
|
+
return 2_000 + r.vec((rr) => rr.u8()).length * DEPLOY_WEIGHT_PER_CODE_BYTE;
|
|
62
|
+
// The larger of `100 + ceil(gas_limit / FUEL_PER_WEIGHT)` and the proof bytes it could add (rows it could
|
|
63
|
+
// touch × PROOF_WEIGHT_PER_KEY) in metering.rs: priced by the limit, not by use. A tight limit is cheap.
|
|
64
|
+
if (variant === ACTION_VARIANT.callModule) {
|
|
65
|
+
r.string();
|
|
66
|
+
r.vec((rr) => rr.u8());
|
|
67
|
+
const gas = r.varint(), ceilDiv = (a, b) => (a + BigInt(b) - 1n) / BigInt(b);
|
|
68
|
+
const execution = 100n + ceilDiv(gas, FUEL_PER_WEIGHT);
|
|
69
|
+
const rows = ceilDiv(gas, COST_READ) + BigInt(MODULE_BASE_KEYS), keys = rows < BigInt(BLOCK_PROOF_KEYS) ? rows : BigInt(BLOCK_PROOF_KEYS);
|
|
70
|
+
const proofs = keys * BigInt(PROOF_WEIGHT_PER_KEY);
|
|
71
|
+
return Number(execution > proofs ? execution : proofs);
|
|
72
|
+
}
|
|
40
73
|
const weight = BASE_WEIGHT[variant];
|
|
41
74
|
if (weight === undefined)
|
|
42
75
|
throw new Error(`unknown action variant: ${variant}`);
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/rpc.d.ts
CHANGED
|
@@ -164,11 +164,107 @@ export type Validator = {
|
|
|
164
164
|
bls_registered: boolean;
|
|
165
165
|
operator: string | null;
|
|
166
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* One snapshot holder's part in a corporate action (`DistributeToHolders`, `RedeemHolders`, `SplitAsset`). `amount` is in
|
|
169
|
+
* `payout_asset` for a distribution, else in `asset`; for `withheld` it is the share the issuer kept because the holder failed
|
|
170
|
+
* the payout asset's compliance. Floor-rounding dust never appears on a line.
|
|
171
|
+
*/
|
|
172
|
+
export type CorporateActionLine = {
|
|
173
|
+
action_index: number;
|
|
174
|
+
signature: string;
|
|
175
|
+
asset: string;
|
|
176
|
+
payout_asset: string | null;
|
|
177
|
+
holder: string;
|
|
178
|
+
amount: bigint;
|
|
179
|
+
outcome: "paid" | "withheld" | "minted" | "forced_back" | "redeemed";
|
|
180
|
+
};
|
|
181
|
+
/** `rpc.simulateModule`: hex strings throughout; `balances` are what the call leaves (`asset` is empty for the native token). */
|
|
182
|
+
export type ModuleSimulation = {
|
|
183
|
+
ok: true;
|
|
184
|
+
gas_used: number;
|
|
185
|
+
return: string;
|
|
186
|
+
writes: {
|
|
187
|
+
key: string;
|
|
188
|
+
value: string | null;
|
|
189
|
+
}[];
|
|
190
|
+
events: {
|
|
191
|
+
topic: string;
|
|
192
|
+
data: string;
|
|
193
|
+
}[];
|
|
194
|
+
balances: {
|
|
195
|
+
asset: string;
|
|
196
|
+
holder: string;
|
|
197
|
+
balance: string;
|
|
198
|
+
}[];
|
|
199
|
+
} | {
|
|
200
|
+
ok: false;
|
|
201
|
+
error: string;
|
|
202
|
+
};
|
|
203
|
+
/** `rpc.module(address)`: `code_hash` is hex, `code_size` in bytes. */
|
|
204
|
+
export type ModuleInfo = {
|
|
205
|
+
address: string;
|
|
206
|
+
code_hash: string;
|
|
207
|
+
deployer: string;
|
|
208
|
+
deployed_at: number;
|
|
209
|
+
version: number;
|
|
210
|
+
code_size: number;
|
|
211
|
+
storage_bytes: number;
|
|
212
|
+
storage_deposit: bigint;
|
|
213
|
+
};
|
|
214
|
+
/** A collateral pledge as stored (`xc_primitives::Pledge`). `asset` is `"Native"` or `{ Asset: ref }`, the node's JSON for `SpendAsset`. */
|
|
215
|
+
export type Pledge = {
|
|
216
|
+
pledgor: string;
|
|
217
|
+
beneficiary: string;
|
|
218
|
+
valuation_agent: string;
|
|
219
|
+
asset: "Native" | {
|
|
220
|
+
Asset: string;
|
|
221
|
+
};
|
|
222
|
+
amount: bigint;
|
|
223
|
+
haircut_bps: number;
|
|
224
|
+
exposure: bigint;
|
|
225
|
+
unit_price: bigint;
|
|
226
|
+
marked_at: number;
|
|
227
|
+
cure_period_blocks: number;
|
|
228
|
+
maturity: number | null;
|
|
229
|
+
margin_call: {
|
|
230
|
+
opened_at: number;
|
|
231
|
+
deadline: number;
|
|
232
|
+
} | null;
|
|
233
|
+
};
|
|
234
|
+
/** `rpc.pledge(id)`: the record plus what it means at the tip. `collateral_value` and `shortfall` are `null` when the valuation overflows. */
|
|
235
|
+
export type PledgeStatus = Pledge & {
|
|
236
|
+
id: string;
|
|
237
|
+
collateral_value: bigint | null;
|
|
238
|
+
shortfall: bigint | null;
|
|
239
|
+
in_default: boolean;
|
|
240
|
+
};
|
|
241
|
+
/** A pledge a block opened or changed (`record` is its end-of-block state), or closed (`record: null`, released or seized). */
|
|
242
|
+
export type PledgeEffect = {
|
|
243
|
+
pledge: string;
|
|
244
|
+
record: Pledge | null;
|
|
245
|
+
};
|
|
246
|
+
/** One `emit_event` of a module call the block applied (`topic` and `data` are hex). A call that traps or is refused leaves none. */
|
|
247
|
+
export type ModuleEventLine = {
|
|
248
|
+
action_index: number;
|
|
249
|
+
signature: string;
|
|
250
|
+
module: string;
|
|
251
|
+
topic: string;
|
|
252
|
+
data: string;
|
|
253
|
+
};
|
|
254
|
+
/** `rpc.blockEffects(height)`. `corporate_actions`, `pledges` and `module_events` are typed; the other fields mirror the node's `BlockEffects`. */
|
|
255
|
+
export type BlockEffects = {
|
|
256
|
+
height: number;
|
|
257
|
+
corporate_actions: CorporateActionLine[];
|
|
258
|
+
pledges: PledgeEffect[];
|
|
259
|
+
module_events: ModuleEventLine[];
|
|
260
|
+
[field: string]: unknown;
|
|
261
|
+
};
|
|
167
262
|
export type ActionFeeInfo = {
|
|
168
263
|
action_fee: bigint;
|
|
169
264
|
weight_fee: bigint;
|
|
170
265
|
max_block_weight: number;
|
|
171
266
|
token_create_fee: bigint;
|
|
267
|
+
module_storage_deposit_per_byte: bigint;
|
|
172
268
|
};
|
|
173
269
|
export type { AssetClass };
|
|
174
270
|
/**
|
|
@@ -186,6 +282,8 @@ export declare class ArxiumRpc {
|
|
|
186
282
|
private readonly senders;
|
|
187
283
|
constructor(options: RpcOptions);
|
|
188
284
|
private request;
|
|
285
|
+
/** Follows `next_cursor` through a paged list route (`{ items, next_cursor }`); a node that predates paging answers with the whole bare array. */
|
|
286
|
+
private allPages;
|
|
189
287
|
private genesis?;
|
|
190
288
|
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
191
289
|
genesisHash(): Promise<Uint8Array>;
|
|
@@ -214,9 +312,36 @@ export declare class ArxiumRpc {
|
|
|
214
312
|
blocks<T = unknown>(from: number, to: number): Promise<T>;
|
|
215
313
|
block<T = unknown>(height: number): Promise<T>;
|
|
216
314
|
blockByHash<T = unknown>(hash: string): Promise<T>;
|
|
217
|
-
/** State changes a block made (balances, assets, stake), for indexers. */
|
|
218
|
-
blockEffects<T =
|
|
219
|
-
effects<T =
|
|
315
|
+
/** State changes a block made (balances, assets, stake, per-holder corporate-action lines), for indexers. */
|
|
316
|
+
blockEffects<T = BlockEffects>(height: number): Promise<T>;
|
|
317
|
+
effects<T = BlockEffects[]>(from: number, to: number): Promise<T>;
|
|
318
|
+
/** A deployed WASM module's record (`GET /modules/{address}`). */
|
|
319
|
+
module(address: string): Promise<ModuleInfo>;
|
|
320
|
+
/** The module's code as hex. */
|
|
321
|
+
moduleCode(address: string): Promise<{
|
|
322
|
+
code_hash: string;
|
|
323
|
+
code: string;
|
|
324
|
+
}>;
|
|
325
|
+
/** One row of a module's storage by hex key (at most 64 bytes); `value` is hex, or `null` when the row is absent. */
|
|
326
|
+
moduleState(address: string, keyHex: string): Promise<{
|
|
327
|
+
key: string;
|
|
328
|
+
value: string | null;
|
|
329
|
+
}>;
|
|
330
|
+
/**
|
|
331
|
+
* Dry-runs a module call at the tip and returns what it would do; nothing is written. A revert, trap,
|
|
332
|
+
* out-of-gas or refused rule comes back as `{ ok: false, error }`, not as an exception. It skips the action's own
|
|
333
|
+
* checks (signature, nonce, fee, account policy), so a simulation can succeed where the real action is refused.
|
|
334
|
+
* `caller` need not exist on chain. Runs at the next height with the wall clock as its time.
|
|
335
|
+
*/
|
|
336
|
+
simulateModule(address: string, call: {
|
|
337
|
+
caller: string;
|
|
338
|
+
input?: Uint8Array;
|
|
339
|
+
gasLimit: number | bigint;
|
|
340
|
+
}): Promise<ModuleSimulation>;
|
|
341
|
+
/** One pledge by id (`pledgeId(pledgor, nonce)`). */
|
|
342
|
+
pledge(id: string): Promise<PledgeStatus>;
|
|
343
|
+
/** The pledges encumbering `address`'s balance of `asset`, oldest first. Native collateral is escrowed, so it is not listed. */
|
|
344
|
+
accountAssetPledges(address: string, asset: string): Promise<PledgeStatus[]>;
|
|
220
345
|
/** Sorted validator addresses, at the tip or at `height`. */
|
|
221
346
|
validators<T = string[]>(height?: number): Promise<T>;
|
|
222
347
|
validator<T = Validator>(address: string): Promise<T>;
|
package/dist/rpc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { signAction, signingBytes, submitBody, verifyMultisig } from "./actions.js";
|
|
2
2
|
import { decodeAddress, isMultisigAddress } from "./bech32.js";
|
|
3
|
-
import { asBuffer, fromHex } from "./bincode.js";
|
|
3
|
+
import { asBuffer, fromHex, toHex } from "./bincode.js";
|
|
4
4
|
import { verifyPolicySignature } from "./policy.js";
|
|
5
5
|
export class RpcError extends Error {
|
|
6
6
|
status;
|
|
@@ -20,7 +20,7 @@ export class ActionDroppedError extends Error {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
/** Keys whose integer values are token amounts (u128 on the node): always `bigint`, so callers never see a rounded balance. */
|
|
23
|
-
const AMOUNT_KEYS = new Set(["balance", "amount", "stake", "active_amount", "frozen_amount", "total_supply", "max_supply", "max_balance_per_holder", "total", "action_fee", "weight_fee", "token_create_fee", "min_stake"]);
|
|
23
|
+
const AMOUNT_KEYS = new Set(["balance", "amount", "stake", "active_amount", "frozen_amount", "total_supply", "max_supply", "max_balance_per_holder", "total", "action_fee", "weight_fee", "token_create_fee", "min_stake", "exposure", "unit_price", "collateral_value", "shortfall", "storage_deposit", "module_storage_deposit_per_byte"]);
|
|
24
24
|
const MARK = "\u0000int:";
|
|
25
25
|
/**
|
|
26
26
|
* `JSON.parse` that never rounds an integer: the node writes u128 amounts as bare JSON numbers, and anything above 2^53
|
|
@@ -46,6 +46,18 @@ export class ArxiumRpc {
|
|
|
46
46
|
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
|
|
47
47
|
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)
|
|
48
48
|
throw new RpcError(response.status, text || `${response.status} ${response.statusText}`); return text ? parseJson(text) : undefined; }
|
|
49
|
+
/** Follows `next_cursor` through a paged list route (`{ items, next_cursor }`); a node that predates paging answers with the whole bare array. */
|
|
50
|
+
async allPages(path) {
|
|
51
|
+
const items = [];
|
|
52
|
+
for (let cursor = 0; cursor !== null;) {
|
|
53
|
+
const page = await this.request(`${path}${path.includes("?") ? "&" : "?"}limit=100&cursor=${cursor}`);
|
|
54
|
+
if (Array.isArray(page))
|
|
55
|
+
return page;
|
|
56
|
+
items.push(...page.items);
|
|
57
|
+
cursor = page.next_cursor;
|
|
58
|
+
}
|
|
59
|
+
return items;
|
|
60
|
+
}
|
|
49
61
|
genesis;
|
|
50
62
|
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
51
63
|
genesisHash() { return this.genesis ??= this.request("/genesis-hash").then((r) => fromHex(r.genesis_hash.replace(/^0x/, "")), (e) => { this.genesis = undefined; throw e; }); }
|
|
@@ -61,7 +73,7 @@ export class ArxiumRpc {
|
|
|
61
73
|
delegatedStake(master, validator) { return this.request(`/stake/${encodeURIComponent(master)}/${encodeURIComponent(validator)}`); }
|
|
62
74
|
blsKey(address) { return this.request(`/accounts/${encodeURIComponent(address)}/bls-key`); }
|
|
63
75
|
/** Every registered asset, or one issuer's. */
|
|
64
|
-
listAssets(issuer) { return this.
|
|
76
|
+
listAssets(issuer) { return this.allPages(issuer ? `/assets?issuer=${encodeURIComponent(issuer)}` : "/assets"); }
|
|
65
77
|
asset(assetRef) { return this.request(`/assets/${encodeURIComponent(assetRef)}`); }
|
|
66
78
|
/** The asset `issuer` registered as `assetId`, or `null` when that id is still free for them. */
|
|
67
79
|
async assetByAlias(issuer, assetId) { try {
|
|
@@ -73,8 +85,8 @@ export class ArxiumRpc {
|
|
|
73
85
|
throw error;
|
|
74
86
|
} }
|
|
75
87
|
/** The cap table: every address with a non-zero balance, with its freeze state. */
|
|
76
|
-
assetHolders(assetRef) { return this.
|
|
77
|
-
attestors() { return this.
|
|
88
|
+
assetHolders(assetRef) { return this.allPages(`/assets/${encodeURIComponent(assetRef)}/holders`); }
|
|
89
|
+
attestors() { return this.allPages("/attestors"); }
|
|
78
90
|
attestor(address) { return this.request(`/attestors/${encodeURIComponent(address)}`); }
|
|
79
91
|
submit(action) { return this.request("/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) }); }
|
|
80
92
|
async action(signature) { try {
|
|
@@ -88,9 +100,31 @@ export class ArxiumRpc {
|
|
|
88
100
|
blocks(from, to) { return this.request(`/blocks?from=${from}&to=${to}`); }
|
|
89
101
|
block(height) { return this.request(`/blocks/${height}`); }
|
|
90
102
|
blockByHash(hash) { return this.request(`/blocks/by-hash/${encodeURIComponent(hash)}`); }
|
|
91
|
-
/** State changes a block made (balances, assets, stake), for indexers. */
|
|
103
|
+
/** State changes a block made (balances, assets, stake, per-holder corporate-action lines), for indexers. */
|
|
92
104
|
blockEffects(height) { return this.request(`/blocks/${height}/effects`); }
|
|
93
105
|
effects(from, to) { return this.request(`/effects?from=${from}&to=${to}`); }
|
|
106
|
+
/** A deployed WASM module's record (`GET /modules/{address}`). */
|
|
107
|
+
module(address) { return this.request(`/modules/${encodeURIComponent(address)}`); }
|
|
108
|
+
/** The module's code as hex. */
|
|
109
|
+
moduleCode(address) { return this.request(`/modules/${encodeURIComponent(address)}/code`); }
|
|
110
|
+
/** One row of a module's storage by hex key (at most 64 bytes); `value` is hex, or `null` when the row is absent. */
|
|
111
|
+
moduleState(address, keyHex) { return this.request(`/modules/${encodeURIComponent(address)}/state/${encodeURIComponent(keyHex)}`); }
|
|
112
|
+
/**
|
|
113
|
+
* Dry-runs a module call at the tip and returns what it would do; nothing is written. A revert, trap,
|
|
114
|
+
* out-of-gas or refused rule comes back as `{ ok: false, error }`, not as an exception. It skips the action's own
|
|
115
|
+
* checks (signature, nonce, fee, account policy), so a simulation can succeed where the real action is refused.
|
|
116
|
+
* `caller` need not exist on chain. Runs at the next height with the wall clock as its time.
|
|
117
|
+
*/
|
|
118
|
+
simulateModule(address, call) {
|
|
119
|
+
const gas = Number(call.gasLimit);
|
|
120
|
+
if (!Number.isSafeInteger(gas) || gas < 0)
|
|
121
|
+
throw new RangeError("gasLimit must be a non-negative safe integer");
|
|
122
|
+
return this.request(`/modules/${encodeURIComponent(address)}/simulate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ caller: call.caller, input: toHex(call.input ?? new Uint8Array()), gas_limit: gas }) });
|
|
123
|
+
}
|
|
124
|
+
/** One pledge by id (`pledgeId(pledgor, nonce)`). */
|
|
125
|
+
pledge(id) { return this.request(`/pledges/${encodeURIComponent(id)}`); }
|
|
126
|
+
/** The pledges encumbering `address`'s balance of `asset`, oldest first. Native collateral is escrowed, so it is not listed. */
|
|
127
|
+
accountAssetPledges(address, asset) { return this.request(`/accounts/${encodeURIComponent(address)}/assets/${encodeURIComponent(asset)}/pledges`); }
|
|
94
128
|
/** Sorted validator addresses, at the tip or at `height`. */
|
|
95
129
|
validators(height) { return this.request(height === undefined ? "/validators" : `/validators?height=${height}`); }
|
|
96
130
|
validator(address) { return this.request(`/validators/${encodeURIComponent(address)}`); }
|
|
@@ -99,7 +133,7 @@ export class ArxiumRpc {
|
|
|
99
133
|
operatorValidators(operator) { return this.request(`/operators/${encodeURIComponent(operator)}/validators`); }
|
|
100
134
|
finality() { return this.request("/finality"); }
|
|
101
135
|
/** Ids of stored equivocation/fault evidence; `evidence(id)` returns one. */
|
|
102
|
-
evidenceList() { return this.
|
|
136
|
+
evidenceList() { return this.allPages("/evidence"); }
|
|
103
137
|
evidence(id) { return this.request(`/evidence/${encodeURIComponent(id)}`); }
|
|
104
138
|
/** Merkle proofs against a finalized state root, for light clients. */
|
|
105
139
|
accountProof(address) { return this.request(`/accounts/${encodeURIComponent(address)}/proof`); }
|
|
@@ -112,12 +146,15 @@ export class ArxiumRpc {
|
|
|
112
146
|
async feeParams() { const fee = await this.actionFee(); return { actionFee: BigInt(fee.action_fee), weightFee: BigInt(fee.weight_fee) }; }
|
|
113
147
|
/** Signs at the sender's current nonce, submits and polls until confirmed (or finalized). Calls for one sender run one after another. */
|
|
114
148
|
sendAction(options) {
|
|
149
|
+
// Capture at invocation, including time spent waiting in this sender's queue.
|
|
150
|
+
options = { ...options, payload: Uint8Array.from(options.payload) };
|
|
115
151
|
const previous = this.senders.get(options.sender) ?? Promise.resolve();
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
this.senders.set(options.sender,
|
|
119
|
-
|
|
120
|
-
this.senders.delete(options.sender); }
|
|
152
|
+
// A failed/uncertain send rejects already queued requests instead of signing again.
|
|
153
|
+
const run = previous.then(() => this.sendNow(options));
|
|
154
|
+
this.senders.set(options.sender, run);
|
|
155
|
+
const cleanup = () => { if (this.senders.get(options.sender) === run)
|
|
156
|
+
this.senders.delete(options.sender); };
|
|
157
|
+
void run.then(cleanup, cleanup);
|
|
121
158
|
return run;
|
|
122
159
|
}
|
|
123
160
|
async sendNow(options) {
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type DvpOrder } from "./actions.js";
|
|
2
|
+
/** Pure Ed25519 over the supplied message bytes, not Ed25519ph or a provider digest mode. */
|
|
3
|
+
export interface Ed25519Signer {
|
|
4
|
+
readonly algorithm: "Ed25519";
|
|
5
|
+
/** Raw32-byte compressed Ed25519 public key, not DER, PEM or an address. */
|
|
6
|
+
readonly publicKey: Uint8Array;
|
|
7
|
+
/** One attempt. Return the raw64-byte signature; reject on refusal or an uncertain outcome. */
|
|
8
|
+
sign(message: Uint8Array): Promise<Uint8Array>;
|
|
9
|
+
}
|
|
10
|
+
export declare class SignerVerificationError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
13
|
+
/** Local developer adapter. Does not export private key material or perform a probe signature. */
|
|
14
|
+
export declare function localEd25519Signer(privateKey: CryptoKey, publicKey: Uint8Array): Ed25519Signer;
|
|
15
|
+
/** Sign exactly these bytes, verify against the caller-selected key, return lowercase signature hex. */
|
|
16
|
+
export declare function signBytesWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, message: Uint8Array): Promise<string>;
|
|
17
|
+
/** Pins the key and callback at creation; plugs into existing ArxiumRpc.sendAction({ sign }). */
|
|
18
|
+
export declare function verifiedSignerCallback(signer: Ed25519Signer, expectedPublicKey: Uint8Array): (message: Uint8Array) => Promise<string>;
|
|
19
|
+
/** The expected key may be a member of a multisig/policy sender; authorization is a separate check. */
|
|
20
|
+
export declare function signActionWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, genesis: Uint8Array, sender: string, nonce: number | bigint, payload: Uint8Array): Promise<string>;
|
|
21
|
+
export declare function signDvpConsentWithSigner(signer: Ed25519Signer, expectedPublicKey: Uint8Array, genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* The two methods of `KMS` from `@aws-sdk/client-kms` (the aggregated client, not `KMSClient`) this adapter calls.
|
|
24
|
+
* The SDK ships no AWS dependency: pass your own configured client.
|
|
25
|
+
*/
|
|
26
|
+
export type AwsKmsClient = {
|
|
27
|
+
getPublicKey(input: {
|
|
28
|
+
KeyId: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
PublicKey?: Uint8Array;
|
|
31
|
+
KeySpec?: string;
|
|
32
|
+
}>;
|
|
33
|
+
sign(input: {
|
|
34
|
+
KeyId: string;
|
|
35
|
+
Message: Uint8Array;
|
|
36
|
+
MessageType: "RAW";
|
|
37
|
+
SigningAlgorithm: "ED25519_SHA_512";
|
|
38
|
+
}): Promise<{
|
|
39
|
+
Signature?: Uint8Array;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
/** AWS KMS caps a RAW message at 4096 bytes; pure Ed25519 cannot be prehashed, so a larger action cannot be KMS-signed. */
|
|
43
|
+
export declare const AWS_KMS_MAX_MESSAGE = 4096;
|
|
44
|
+
/** An `ECC_NIST_EDWARDS25519` key with `ED25519_SHA_512` (pure Ed25519, `MessageType: RAW`). Reads the key's public key once. */
|
|
45
|
+
export declare function awsKmsEd25519Signer(kms: AwsKmsClient, keyId: string): Promise<Ed25519Signer>;
|
|
46
|
+
/** The subset of a `pkcs11js` `PKCS11` instance this adapter calls. The SDK ships no native module: pass your own, logged in. */
|
|
47
|
+
export type Pkcs11Module = {
|
|
48
|
+
C_SignInit(session: Buffer, mechanism: {
|
|
49
|
+
mechanism: number;
|
|
50
|
+
}, key: Buffer): void;
|
|
51
|
+
C_Sign(session: Buffer, data: Buffer, signature: Buffer): Buffer;
|
|
52
|
+
C_GetAttributeValue(session: Buffer, object: Buffer, template: {
|
|
53
|
+
type: number;
|
|
54
|
+
}[]): {
|
|
55
|
+
type: number;
|
|
56
|
+
value?: Buffer;
|
|
57
|
+
}[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* A `CKK_EC_EDWARDS` key pair on a PKCS#11 v3 token (YubiHSM 2, Luna 7.7+, SoftHSM 2.6+), signed with `CKM_EDDSA` and no
|
|
61
|
+
* parameters: pure Ed25519. The public key comes from the public object's `CKA_EC_POINT`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function pkcs11Ed25519Signer(pkcs11: Pkcs11Module, session: Buffer, privateKey: Buffer, publicKeyObject: Buffer): Ed25519Signer;
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { dvpConsentBytes, signingBytes } from "./actions.js";
|
|
2
|
+
import { asBuffer, toHex } from "./bincode.js";
|
|
3
|
+
export class SignerVerificationError extends Error {
|
|
4
|
+
constructor(message) { super(message); this.name = "SignerVerificationError"; }
|
|
5
|
+
}
|
|
6
|
+
function rawKey(value) {
|
|
7
|
+
if (!(value instanceof Uint8Array) || value.length !== 32)
|
|
8
|
+
throw new SignerVerificationError("expected a raw32-byte Ed25519 public key");
|
|
9
|
+
return Uint8Array.from(value);
|
|
10
|
+
}
|
|
11
|
+
function pinSigner(signer, expectedPublicKey) {
|
|
12
|
+
const expected = rawKey(expectedPublicKey), advertised = rawKey(signer.publicKey);
|
|
13
|
+
const sign = signer.sign;
|
|
14
|
+
if (signer.algorithm !== "Ed25519" || typeof sign !== "function")
|
|
15
|
+
throw new SignerVerificationError("signer must implement pure Ed25519");
|
|
16
|
+
if (!expected.every((byte, i) => byte === advertised[i]))
|
|
17
|
+
throw new SignerVerificationError("signer public key does not match the expected key");
|
|
18
|
+
return { publicKey: expected, sign: sign.bind(signer) };
|
|
19
|
+
}
|
|
20
|
+
async function verifiedSign(signer, message) {
|
|
21
|
+
// Keep the trusted message private. A provider may retain/mutate its own input buffer.
|
|
22
|
+
if (!(message instanceof Uint8Array))
|
|
23
|
+
throw new SignerVerificationError("message must be Uint8Array bytes");
|
|
24
|
+
const snapshot = Uint8Array.from(message);
|
|
25
|
+
const response = await signer.sign(Uint8Array.from(snapshot));
|
|
26
|
+
if (!(response instanceof Uint8Array) || response.length !== 64)
|
|
27
|
+
throw new SignerVerificationError("signer must return a raw64-byte Ed25519 signature");
|
|
28
|
+
const signature = Uint8Array.from(response);
|
|
29
|
+
const key = await crypto.subtle.importKey("raw", asBuffer(signer.publicKey), "Ed25519", false, ["verify"]);
|
|
30
|
+
if (!await crypto.subtle.verify("Ed25519", key, asBuffer(signature), asBuffer(snapshot)))
|
|
31
|
+
throw new SignerVerificationError("signature does not verify for the expected key and original message");
|
|
32
|
+
return toHex(signature);
|
|
33
|
+
}
|
|
34
|
+
/** Local developer adapter. Does not export private key material or perform a probe signature. */
|
|
35
|
+
export function localEd25519Signer(privateKey, publicKey) {
|
|
36
|
+
const raw = rawKey(publicKey);
|
|
37
|
+
if (privateKey.type !== "private" || privateKey.algorithm.name !== "Ed25519" || !privateKey.usages.includes("sign"))
|
|
38
|
+
throw new SignerVerificationError("local signer needs an Ed25519 private signing key");
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
algorithm: "Ed25519",
|
|
41
|
+
get publicKey() { return Uint8Array.from(raw); },
|
|
42
|
+
async sign(message) { return new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(message))); },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Sign exactly these bytes, verify against the caller-selected key, return lowercase signature hex. */
|
|
46
|
+
export async function signBytesWithSigner(signer, expectedPublicKey, message) {
|
|
47
|
+
return verifiedSign(pinSigner(signer, expectedPublicKey), message);
|
|
48
|
+
}
|
|
49
|
+
/** Pins the key and callback at creation; plugs into existing ArxiumRpc.sendAction({ sign }). */
|
|
50
|
+
export function verifiedSignerCallback(signer, expectedPublicKey) {
|
|
51
|
+
const pinned = pinSigner(signer, expectedPublicKey);
|
|
52
|
+
return (message) => verifiedSign(pinned, message);
|
|
53
|
+
}
|
|
54
|
+
/** The expected key may be a member of a multisig/policy sender; authorization is a separate check. */
|
|
55
|
+
export async function signActionWithSigner(signer, expectedPublicKey, genesis, sender, nonce, payload) {
|
|
56
|
+
return signBytesWithSigner(signer, expectedPublicKey, signingBytes(genesis, sender, nonce, payload));
|
|
57
|
+
}
|
|
58
|
+
export async function signDvpConsentWithSigner(signer, expectedPublicKey, genesis, initiator, order, counterpartyNonce) {
|
|
59
|
+
return signBytesWithSigner(signer, expectedPublicKey, dvpConsentBytes(genesis, initiator, order, counterpartyNonce));
|
|
60
|
+
}
|
|
61
|
+
const SPKI_ED25519_PREFIX = "302a300506032b6570032100";
|
|
62
|
+
/** Raw 32-byte key from an Ed25519 SubjectPublicKeyInfo (what AWS KMS GetPublicKey returns). */
|
|
63
|
+
function spkiEd25519(der) {
|
|
64
|
+
if (!(der instanceof Uint8Array) || der.length !== 44 || toHex(der.subarray(0, 12)) !== SPKI_ED25519_PREFIX)
|
|
65
|
+
throw new SignerVerificationError("expected an Ed25519 SubjectPublicKeyInfo public key");
|
|
66
|
+
return der.slice(12);
|
|
67
|
+
}
|
|
68
|
+
/** AWS KMS caps a RAW message at 4096 bytes; pure Ed25519 cannot be prehashed, so a larger action cannot be KMS-signed. */
|
|
69
|
+
export const AWS_KMS_MAX_MESSAGE = 4096;
|
|
70
|
+
/** An `ECC_NIST_EDWARDS25519` key with `ED25519_SHA_512` (pure Ed25519, `MessageType: RAW`). Reads the key's public key once. */
|
|
71
|
+
export async function awsKmsEd25519Signer(kms, keyId) {
|
|
72
|
+
const info = await kms.getPublicKey({ KeyId: keyId });
|
|
73
|
+
if (info.KeySpec !== "ECC_NIST_EDWARDS25519")
|
|
74
|
+
throw new SignerVerificationError(`KMS key ${keyId} is ${info.KeySpec}, not ECC_NIST_EDWARDS25519`);
|
|
75
|
+
const raw = spkiEd25519(info.PublicKey);
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
algorithm: "Ed25519",
|
|
78
|
+
get publicKey() { return Uint8Array.from(raw); },
|
|
79
|
+
async sign(message) {
|
|
80
|
+
if (message.length > AWS_KMS_MAX_MESSAGE)
|
|
81
|
+
throw new SignerVerificationError(`message is ${message.length} bytes; AWS KMS signs at most ${AWS_KMS_MAX_MESSAGE}`);
|
|
82
|
+
const { Signature } = await kms.sign({ KeyId: keyId, Message: message, MessageType: "RAW", SigningAlgorithm: "ED25519_SHA_512" });
|
|
83
|
+
if (!(Signature instanceof Uint8Array))
|
|
84
|
+
throw new SignerVerificationError("KMS returned no signature");
|
|
85
|
+
return Signature;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const CKM_EDDSA = 0x1057, CKA_EC_POINT = 0x181;
|
|
90
|
+
/**
|
|
91
|
+
* A `CKK_EC_EDWARDS` key pair on a PKCS#11 v3 token (YubiHSM 2, Luna 7.7+, SoftHSM 2.6+), signed with `CKM_EDDSA` and no
|
|
92
|
+
* parameters: pure Ed25519. The public key comes from the public object's `CKA_EC_POINT`.
|
|
93
|
+
*/
|
|
94
|
+
export function pkcs11Ed25519Signer(pkcs11, session, privateKey, publicKeyObject) {
|
|
95
|
+
const point = pkcs11.C_GetAttributeValue(session, publicKeyObject, [{ type: CKA_EC_POINT }])[0]?.value;
|
|
96
|
+
// PKCS#11 3.0 wraps the point in a DER OCTET STRING; some tokens return it bare.
|
|
97
|
+
const raw = rawKey(point?.length === 34 && point[0] === 0x04 && point[1] === 0x20 ? point.subarray(2) : point);
|
|
98
|
+
return Object.freeze({
|
|
99
|
+
algorithm: "Ed25519",
|
|
100
|
+
get publicKey() { return Uint8Array.from(raw); },
|
|
101
|
+
async sign(message) {
|
|
102
|
+
// ponytail: init+sign run synchronously back to back, so concurrent calls never interleave on one session.
|
|
103
|
+
pkcs11.C_SignInit(session, { mechanism: CKM_EDDSA }, privateKey);
|
|
104
|
+
return Uint8Array.from(pkcs11.C_Sign(session, Buffer.from(message), Buffer.alloc(64)));
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|