@arxiumlabs/sdk 0.9.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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), encoded = payload(action, rest.slice(1));
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,22 @@ 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 === "fee" ? await rpc.actionFee()
348
- : throwUsage("use: arx query status|account|block|action|validators|validator|finality|search|assets [address]|asset|holders|balance|attestors|attestor|stakes|fee");
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 === "fee" ? await rpc.actionFee()
383
+ : throwUsage("use: arx query status|account|block|action|validators|validator|finality|search|assets [address]|asset|holders|balance|attestors|attestor|stakes|module|module-state|fee");
349
384
  output(result, globals.json);
350
385
  return;
351
386
  }
387
+ // Dry-runs a module call as the key's address; no passphrase needed, nothing is signed or sent.
388
+ if (command === "simulate") {
389
+ const [module, gas, inputHex] = rest;
390
+ const caller = (await readKeyFile(globals.key)).address;
391
+ if (!module || !gas || !/^\d+$/.test(gas))
392
+ throwUsage("simulate needs <module-address> <gas-limit> [input-hex] and --key");
393
+ output(await rpc.simulateModule(module, { caller, input: inputHex === undefined ? new Uint8Array() : hexArg(inputHex), gasLimit: BigInt(gas) }), globals.json);
394
+ return;
395
+ }
352
396
  throwUsage(`unknown command: ${command}`);
353
397
  }
354
398
  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,6 +3,10 @@ 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 = 4, DEPLOY_WEIGHT_PER_CODE_BYTE = 7, COST_READ = 1000;
6
10
  /** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
7
11
  export declare const POLICY_AUTHORIZATION_WEIGHT = 5000;
8
12
  /** A single-key ed25519 signature: 64 bytes as hex. */
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,6 +25,10 @@ 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 = 4, DEPLOY_WEIGHT_PER_CODE_BYTE = 7, COST_READ = 1_000;
28
32
  /** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
29
33
  export const POLICY_AUTHORIZATION_WEIGHT = 5_000;
30
34
  /** A single-key ed25519 signature: 64 bytes as hex. */
@@ -37,6 +41,28 @@ function baseWeight(payload) {
37
41
  return Number(r.varint()) === TOKEN_VARIANT.createToken ? 150 : 100;
38
42
  if (variant === ACCOUNT_POLICY_ACTION)
39
43
  return 1_000;
44
+ if (variant === COLLATERAL_ACTION) {
45
+ const action = Number(r.varint());
46
+ return action === COLLATERAL_VARIANT.pledge || action === COLLATERAL_VARIANT.substitutePledge ? 350 : action === COLLATERAL_VARIANT.seizePledge ? 250 : 150;
47
+ }
48
+ if (variant === VENUE_ACTION) {
49
+ // `VENUE_SETTLE_BASE + trades × VENUE_TRADE_WEIGHT` in metering.rs; a cancel is flat.
50
+ return Number(r.varint()) === VENUE_VARIANT.settleVenue ? 100 + Number(r.varint()) * 800 : 100;
51
+ }
52
+ // `2_000 + 7 × code length` in metering.rs: the code rides in a dispute artifact about six times.
53
+ if (variant === ACTION_VARIANT.deployModule)
54
+ return 2_000 + r.vec((rr) => rr.u8()).length * DEPLOY_WEIGHT_PER_CODE_BYTE;
55
+ // The larger of `100 + ceil(gas_limit / FUEL_PER_WEIGHT)` and the proof bytes it could add (rows it could
56
+ // touch × PROOF_WEIGHT_PER_KEY) in metering.rs: priced by the limit, not by use. A tight limit is cheap.
57
+ if (variant === ACTION_VARIANT.callModule) {
58
+ r.string();
59
+ r.vec((rr) => rr.u8());
60
+ const gas = r.varint(), ceilDiv = (a, b) => (a + BigInt(b) - 1n) / BigInt(b);
61
+ const execution = 100n + ceilDiv(gas, FUEL_PER_WEIGHT);
62
+ const rows = ceilDiv(gas, COST_READ) + BigInt(MODULE_BASE_KEYS), keys = rows < BigInt(BLOCK_PROOF_KEYS) ? rows : BigInt(BLOCK_PROOF_KEYS);
63
+ const proofs = keys * BigInt(PROOF_WEIGHT_PER_KEY);
64
+ return Number(execution > proofs ? execution : proofs);
65
+ }
40
66
  const weight = BASE_WEIGHT[variant];
41
67
  if (weight === undefined)
42
68
  throw new Error(`unknown action variant: ${variant}`);
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export * from "./keys.js";
6
6
  export * from "./rpc.js";
7
7
  export * from "./policy.js";
8
8
  export * from "./fees.js";
9
+ export * from "./signer.js";
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@ export * from "./keys.js";
6
6
  export * from "./rpc.js";
7
7
  export * from "./policy.js";
8
8
  export * from "./fees.js";
9
+ export * from "./signer.js";
package/dist/rpc.d.ts CHANGED
@@ -164,6 +164,90 @@ 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
+ };
212
+ /** A collateral pledge as stored (`xc_primitives::Pledge`). `asset` is `"Native"` or `{ Asset: ref }`, the node's JSON for `SpendAsset`. */
213
+ export type Pledge = {
214
+ pledgor: string;
215
+ beneficiary: string;
216
+ valuation_agent: string;
217
+ asset: "Native" | {
218
+ Asset: string;
219
+ };
220
+ amount: bigint;
221
+ haircut_bps: number;
222
+ exposure: bigint;
223
+ unit_price: bigint;
224
+ marked_at: number;
225
+ cure_period_blocks: number;
226
+ maturity: number | null;
227
+ margin_call: {
228
+ opened_at: number;
229
+ deadline: number;
230
+ } | null;
231
+ };
232
+ /** `rpc.pledge(id)`: the record plus what it means at the tip. `collateral_value` and `shortfall` are `null` when the valuation overflows. */
233
+ export type PledgeStatus = Pledge & {
234
+ id: string;
235
+ collateral_value: bigint | null;
236
+ shortfall: bigint | null;
237
+ in_default: boolean;
238
+ };
239
+ /** A pledge a block opened or changed (`record` is its end-of-block state), or closed (`record: null`, released or seized). */
240
+ export type PledgeEffect = {
241
+ pledge: string;
242
+ record: Pledge | null;
243
+ };
244
+ /** `rpc.blockEffects(height)`. `corporate_actions` and `pledges` are typed; the other fields mirror the node's `BlockEffects`. */
245
+ export type BlockEffects = {
246
+ height: number;
247
+ corporate_actions: CorporateActionLine[];
248
+ pledges: PledgeEffect[];
249
+ [field: string]: unknown;
250
+ };
167
251
  export type ActionFeeInfo = {
168
252
  action_fee: bigint;
169
253
  weight_fee: bigint;
@@ -186,6 +270,8 @@ export declare class ArxiumRpc {
186
270
  private readonly senders;
187
271
  constructor(options: RpcOptions);
188
272
  private request;
273
+ /** Follows `next_cursor` through a paged list route (`{ items, next_cursor }`); a node that predates paging answers with the whole bare array. */
274
+ private allPages;
189
275
  private genesis?;
190
276
  /** This chain's genesis hash, fetched once per client; every signature binds it. */
191
277
  genesisHash(): Promise<Uint8Array>;
@@ -214,9 +300,36 @@ export declare class ArxiumRpc {
214
300
  blocks<T = unknown>(from: number, to: number): Promise<T>;
215
301
  block<T = unknown>(height: number): Promise<T>;
216
302
  blockByHash<T = unknown>(hash: string): Promise<T>;
217
- /** State changes a block made (balances, assets, stake), for indexers. */
218
- blockEffects<T = unknown>(height: number): Promise<T>;
219
- effects<T = unknown>(from: number, to: number): Promise<T>;
303
+ /** State changes a block made (balances, assets, stake, per-holder corporate-action lines), for indexers. */
304
+ blockEffects<T = BlockEffects>(height: number): Promise<T>;
305
+ effects<T = BlockEffects[]>(from: number, to: number): Promise<T>;
306
+ /** A deployed WASM module's record (`GET /modules/{address}`). */
307
+ module(address: string): Promise<ModuleInfo>;
308
+ /** The module's code as hex. */
309
+ moduleCode(address: string): Promise<{
310
+ code_hash: string;
311
+ code: string;
312
+ }>;
313
+ /** One row of a module's storage by hex key (at most 64 bytes); `value` is hex, or `null` when the row is absent. */
314
+ moduleState(address: string, keyHex: string): Promise<{
315
+ key: string;
316
+ value: string | null;
317
+ }>;
318
+ /**
319
+ * Dry-runs a module call at the tip and returns what it would do; nothing is written. A revert, trap,
320
+ * out-of-gas or refused rule comes back as `{ ok: false, error }`, not as an exception. It skips the action's own
321
+ * checks (signature, nonce, fee, account policy), so a simulation can succeed where the real action is refused.
322
+ * `caller` need not exist on chain. Runs at the next height with the wall clock as its time.
323
+ */
324
+ simulateModule(address: string, call: {
325
+ caller: string;
326
+ input?: Uint8Array;
327
+ gasLimit: number | bigint;
328
+ }): Promise<ModuleSimulation>;
329
+ /** One pledge by id (`pledgeId(pledgor, nonce)`). */
330
+ pledge(id: string): Promise<PledgeStatus>;
331
+ /** The pledges encumbering `address`'s balance of `asset`, oldest first. Native collateral is escrowed, so it is not listed. */
332
+ accountAssetPledges(address: string, asset: string): Promise<PledgeStatus[]>;
220
333
  /** Sorted validator addresses, at the tip or at `height`. */
221
334
  validators<T = string[]>(height?: number): Promise<T>;
222
335
  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"]);
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.request(issuer ? `/assets?issuer=${encodeURIComponent(issuer)}` : "/assets"); }
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.request(`/assets/${encodeURIComponent(assetRef)}/holders`); }
77
- attestors() { return this.request("/attestors"); }
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.request("/evidence"); }
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
- const run = previous.catch(() => undefined).then(() => this.sendNow(options));
117
- const tail = run.catch(() => undefined);
118
- this.senders.set(options.sender, tail);
119
- void tail.then(() => { if (this.senders.get(options.sender) === tail)
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) {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arxiumlabs/sdk",
3
- "version": "0.9.0",
3
+ "version": "0.13.1",
4
4
  "description": "Arxium TypeScript SDK and arx CLI",
5
5
  "type": "module",
6
6
  "private": false,
@@ -25,7 +25,8 @@
25
25
  "files": [
26
26
  "dist",
27
27
  "!dist/*.test.*",
28
- "README.md"
28
+ "README.md",
29
+ "SIGNERS.md"
29
30
  ],
30
31
  "engines": {
31
32
  "node": ">=20"
@@ -34,7 +35,7 @@
34
35
  "build": "tsc -p tsconfig.json",
35
36
  "check:package": "node scripts/release-check.mjs",
36
37
  "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
+ "test": "npm run build && node dist/signer.test.js && node dist/codec.test.js && node dist/policy.test.js && node dist/cli.test.js"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^20.0.0",