@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/README.md CHANGED
@@ -44,10 +44,20 @@ const fee2 = actionFee(params, sender, nonce, payload, { signatureLength: multis
44
44
 
45
45
  ### Reading the chain
46
46
 
47
- `ArxiumRpc` returns typed responses for status, accounts, stake, assets (`listAssets`, `asset`, `assetByAlias`, `assetHolders`, `assetBalance`), attestors, validators (`validator`, `validatorPower`, `operatorValidators`), block effects, evidence and Merkle proofs. Each method takes a type parameter if you need a different shape.
47
+ The list methods (`listAssets`, `assetHolders`, `attestors`, `evidenceList`) follow the node's `next_cursor` pages (`?limit=&cursor=`) and return the whole list. `ArxiumRpc` returns typed responses for status, accounts, stake, assets (`listAssets`, `asset`, `assetByAlias`, `assetHolders`, `assetBalance`), attestors, validators (`validator`, `validatorPower`, `operatorValidators`), block effects, evidence and Merkle proofs. Each method takes a type parameter if you need a different shape.
48
+
49
+ `rpc.blockEffects(height)` and `rpc.effects(from, to)` carry `corporate_actions`: one `CorporateActionLine` per snapshot holder of every `DistributeToHolders`, `RedeemHolders` and `SplitAsset` in the block, with the action's `action_index` and `signature`, `asset`, `payout_asset` (distributions only), `holder`, `amount` (`bigint`) and `outcome` (`paid`, `withheld`, `minted`, `forced_back` or `redeemed`). Use these for per-holder payouts, not `asset_balances` diffs: those are end-of-block balances, so they blur a holder that moved twice in the block and show nothing for a withheld holder. Blocks a node wrote before it recorded these lines return an empty list.
50
+
51
+ They also carry `pledges`: one `PledgeEffect` per collateral pledge the block opened, changed or closed, with its `pledge` id and `record`, the end-of-block `Pledge`, or `null` once it was released or seized. Blocks from before nodes recorded pledges return an empty list.
48
52
 
49
53
  ### Sending
50
54
 
55
+ For pluggable asynchronous Ed25519 signing, use the verified signer/local
56
+ adapter with the existing RPC callback. [Contract, examples and limits](SIGNERS.md)
57
+ describe exact bytes, key matching, mutation protection and uncertain outcomes.
58
+ Keys held in an HSM or AWS KMS sign through `pkcs11Ed25519Signer` or
59
+ `awsKmsEd25519Signer`; see [HSM and KMS backends](SIGNERS.md#hsm-and-kms-backends).
60
+
51
61
  `sendAction` signs at the account's current nonce. The node's nonce doesn't count actions still in the mempool, so one client runs sends for the same sender one after another. Concurrent calls each wait their turn and never collide on a nonce. Two separate processes sending for one sender still need their own coordination. For a multisig or programmable-account sender, pass `sign(message, nonce)` instead of `privateKey` and return the combined witness.
52
62
 
53
63
  `rpc.sendAction({ privateKey, sender, payload })` signs, submits and waits for the block. If the chain refuses the action it throws `ActionDroppedError`, whose `reason` is the node's exact text. `rpc.assets(address)` returns the account's asset balances with `transfer_eligible` and `eligibility_reason`, so an app can check the sending side before the user signs. Walkthrough: https://arxium.network/docs/sdk-quickstart
@@ -83,7 +93,59 @@ const consent = await signDvpConsent(buyerKey, genesis, seller, order, buyerNonc
83
93
  await rpc.sendAction({ privateKey: sellerKey, sender: seller, payload: encodeSettleDvp(order, consent) });
84
94
  ```
85
95
 
86
- Settling consumes the counterparty's nonce, so the consent can't be replayed, and any other action the counterparty sends first cancels it. The node refuses the order after height `expiresAt`. A multisig counterparty's members each sign `dvpConsentBytes(…)` and combine them with `multisigSignature`. A programmable-account counterparty is not supported yet.
96
+ Settling consumes the counterparty's nonce, so the consent can't be replayed, and any other action the counterparty sends first cancels it. The node refuses the order after height `expiresAt`. A multisig counterparty's members each sign `dvpConsentBytes(…)` and combine them with `multisigSignature`. A programmable-account counterparty's owners each sign `dvpConsentBytes(…)` and combine them with `assemblePolicySignature("owner", policy, sigs)`; its leg is checked against its own spending limits and recipient allowlist (nodes after v0.15.0).
97
+
98
+ ### Venue settlement
99
+
100
+ A venue (an off-chain order book) settles trades between parties who each signed an order. Each order names the venue, so only that venue can settle it. The venue sends the action and pays the fee. One `encodeSettleVenue` carries 1 to `MAX_VENUE_TRADES` (32) trades, settled in order, all or nothing. In each trade the maker's give must equal the taker's take, same asset and amount, and the other way round:
101
+
102
+ ```ts
103
+ const sellTerms: VenueOrderTerms = { venue, give: { asset: bondRef }, giveAmount: 10n, take: { asset: cashRef }, takeAmount: 500n, expiresAt: height + 100n };
104
+ const buyTerms: VenueOrderTerms = { venue, give: { asset: cashRef }, giveAmount: 500n, take: { asset: bondRef }, takeAmount: 10n, expiresAt: height + 100n };
105
+ const maker: SignedVenueOrder = { owner: seller, orderId: sellId, terms: sellTerms, signature: await signVenueOrder(sellerKey, genesis, seller, sellId, sellTerms) }; // on the seller's machine
106
+ const taker: SignedVenueOrder = { owner: buyer, orderId: buyId, terms: buyTerms, signature: await signVenueOrder(buyerKey, genesis, buyer, buyId, buyTerms) }; // on the buyer's machine
107
+ await rpc.sendAction({ privateKey: venueKey, sender: venue, payload: encodeSettleVenue([{ maker, taker }]) });
108
+ ```
109
+
110
+ `orderId` is any u64 the owner hasn't used on a live order; pick a random one. Each `(owner, orderId)` settles at most once, there are no partial fills, and the owner's own actions don't touch its orders, so a party can have many live orders. An order may expire at most `MAX_ORDER_LIFETIME` (43,200 blocks, one day) past the height it settles at. The owner kills an order with `encodeCancelVenueOrder(orderId, expiresAt)`, passing the order's own expiry. A multisig or programmable-account owner's members each sign `venueOrderBytes(…)` and combine them as for `dvpConsentBytes(…)`. A programmable owner's leg is checked against its own spending limits and recipient allowlist.
111
+
112
+ ### Collateral pledges
113
+
114
+ A pledgor puts up `amount` of an asset for a beneficiary against an `exposure`, with a haircut, a valuation agent and a cure period. RWA and token units stay in the pledgor's balance, encumbered: the pledgor can't transfer, burn or deliver them. Native IUM moves into escrow. The beneficiary signs the terms first, at its current nonce, then the pledgor sends them:
115
+
116
+ ```ts
117
+ const terms: PledgeTerms = { beneficiary: bank, valuationAgent: null, asset: { asset: bondRef }, amount: 95n, haircutBps: 1_000, exposure: 50n, unitPrice: PRICE_SCALE, curePeriodBlocks: 10n, maturity: null };
118
+ const consent = await signPledgeConsent(bankKey, genesis, holder, terms, bankNonce); // on the bank's machine
119
+ await rpc.sendAction({ privateKey: holderKey, sender: holder, payload: encodePledge(terms, consent) });
120
+ const id = await pledgeId(holder, holderNonce); // the nonce the Pledge was sent at
121
+ ```
122
+
123
+ `unitPrice` is a price times `PRICE_SCALE`, so `PRICE_SCALE` is 1.00. `haircutBps: 1_000` values the collateral at 90%. `valuationAgent: null` makes the beneficiary the agent.
124
+
125
+ | Action | Sent by | Encoder |
126
+ |---|---|---|
127
+ | Mark a new price and exposure | the valuation agent | `encodeMarkPledge(id, unitPrice, exposure)` |
128
+ | Add collateral | the pledgor | `encodeTopUpPledge(id, amount)` |
129
+ | Swap the collateral | the pledgor, with the agent's `signSubstituteConsent(…)` | `encodeSubstitutePledge(id, asset, amount, unitPrice, agentSignature)` |
130
+ | Return collateral | the beneficiary any time; the pledgor in full once exposure is marked `0` | `encodeReleasePledge(id, amount)` |
131
+ | Seize in default | the beneficiary | `encodeSeizePledge(id, full)` |
132
+
133
+ A mark that values the collateral below the exposure opens a margin call. The pledgor has `curePeriodBlocks` to top up or substitute. After that, or after `maturity + curePeriodBlocks` with exposure still outstanding, the beneficiary may seize everything (`full: true`) or only the units worth the exposure, with the rest returned. `rpc.pledge(id)` returns the record with its `collateral_value`, `shortfall` and `in_default` at the tip. `rpc.accountAssetPledges(address, asset)` lists the pledges on one balance. Native collateral is escrowed, so it isn't listed there.
134
+
135
+ ### WASM modules
136
+
137
+ Asset modules (`docs/wasm-contracts-plan.md`) run inside the chain's compliance rules. They are off until a governed `SetChainParams` sets `modules_enabled`, so on a chain where it is off both actions are refused.
138
+
139
+ ```ts
140
+ const address = await moduleAddress(deployer, code, salt); // where it will live
141
+ await rpc.submit(submitBody(deployer, nonce, sig, encodeDeployModule(code, salt)));
142
+ const gas = 100_000n; // the fee follows this limit, not the gas used; set it from simulateModule's gas_used
143
+ await rpc.submit(submitBody(caller, nonce, sig, encodeCallModule(address, input, gas)));
144
+ await rpc.module(address); await rpc.moduleCode(address); await rpc.moduleState(address, keyHex);
145
+ const dry = await rpc.simulateModule(address, { caller, input, gasLimit: gas }); // { ok, gas_used, return, writes, events, balances } or { ok: false, error }
146
+ ```
147
+
148
+ Only an attested account can deploy. A module holds a regulated asset only if it is attested like any holder, and it pays out only from its own account through the same checks as a transfer (freeze, claims, jurisdiction, caps, locks). Limits per module: 32 KiB of code, 1 KiB per stored value, 16 reads, 16 writes, 16 lookups, 4 transfers per call. A call is priced by its gas limit twice over: execution (one weight per 50 fuel) and the dispute-artifact bytes it could add (15,000 weight per 1,000 fuel, up to a whole block at 64 rows), so a tight limit is cheap and a 1,000,000 limit fills a block. Deploying costs 8 weight per code byte. There is no way to list a module's storage; keep your own index and read it by key.
87
149
 
88
150
  ### Programmable accounts
89
151
 
@@ -139,7 +201,7 @@ arx verify signature
139
201
 
140
202
  `arx sign` always needs a reachable node. Every signature covers the chain's genesis hash (see Chain binding above), and `sign` reads it from `--rpc` (`GET /genesis-hash`) on every run. `--nonce` only skips the account-nonce lookup. There is no fully offline signing mode: with no node the command exits 1 with the fetch error.
141
203
 
142
- Supported signed actions are `transfer`, `stake`, `unstake`, `join-validator`, `leave-validator`, `register-bls-key`, `authorize-operator`, `revoke-operator`, `grant-attestation`, `revoke-attestation`, `register-asset`, `issue-asset`, `transfer-asset`, `apply-attestor`, `block-attestor`, `propose-add-attestor`, `propose-remove-attestor`, `propose-unblock-attestor`, `vote`, `execute-proposal` and `settle-dvp`, plus the issuer, corporate-action and token commands below. Amount arguments are raw base-unit integers (IUM for ARX, the asset's smallest unit for an asset).
204
+ Supported signed actions are `transfer`, `stake`, `unstake`, `join-validator`, `leave-validator`, `register-bls-key`, `authorize-operator`, `revoke-operator`, `grant-attestation`, `revoke-attestation`, `register-asset`, `issue-asset`, `transfer-asset`, `apply-attestor`, `block-attestor`, `propose-add-attestor`, `propose-remove-attestor`, `propose-unblock-attestor`, `vote`, `execute-proposal`, `settle-dvp`, `venue-settle`, `venue-cancel`, `module-deploy` and `module-call`, plus the issuer, corporate-action and token commands below. Amount arguments are raw base-unit integers (IUM for ARX, the asset's smallest unit for an asset).
143
205
 
144
206
  ```sh
145
207
  arx send apply-attestor "Arxium Bank" <owner>,<owner>,<owner> 2 <evidence-hash> <evidence-uri> # sender is the 2-of-N multisig; validators vote
@@ -170,6 +232,14 @@ arx send create-token ABC "Abc Token" 6 1000000 - mintable # <max-supply|-> mi
170
232
  arx send mint-token arxasset1... arx1to 100 # transfer-token, burn-token, renounce-mint
171
233
  arx dvp-consent arx1seller arxasset1bond... 10 native 500 <expires-at-height> --key buyer.json # buyer signs; prints the "send" args
172
234
  arx send settle-dvp arxasset1bond... 10 arx1buyer native 500 <expires-at-height> <buyer-signature> --key seller.json
235
+ arx venue-order arx1venue arxasset1bond... 10 arxasset1cash... 500 <expires-at-height> --key seller.json > sell.json # random order id, or --order-id n
236
+ arx send venue-settle trades.json --key venue.json # [{ "maker": <sell.json>, "taker": <buy.json> }, ...]
237
+ arx send venue-cancel <order-id> <expires-at-height> --key seller.json
238
+ arx send module-deploy <module.wasm> [salt] --key deployer.json
239
+ arx send module-call <module-address> <gas-limit> [input-hex] --key caller.json
240
+ arx query module <address>
241
+ arx query module-state <address> <key-hex>
242
+ arx simulate <module-address> <gas-limit> [input-hex] --key caller.json # dry run, nothing is sent
173
243
 
174
244
  arx query assets [arx1address] # all assets, or one account's balances
175
245
  arx query asset arxasset1...
package/SIGNERS.md ADDED
@@ -0,0 +1,132 @@
1
+ # Asynchronous signing contract
2
+
3
+ The SDK already accepts `ArxiumRpc.sendAction({ sign })` for custom witnesses.
4
+ The verified Ed25519 interface plugs into that callback and also signs offline
5
+ actions and DvP consents. It adds no provider, dependency, key storage or network
6
+ request. Existing CryptoKey functions and the generic witness callback remain
7
+ available. RPC now captures the caller's options and payload at invocation,
8
+ including requests waiting in the per-sender queue.
9
+
10
+ ```ts
11
+ import {
12
+ localEd25519Signer, signActionWithSigner, verifiedSignerCallback,
13
+ encodeAddress, type ArxiumRpc,
14
+ } from "@arxiumlabs/sdk";
15
+
16
+ // The public key is from your already verified key file/account configuration.
17
+ async function example(privateKey: CryptoKey, publicKey: Uint8Array,
18
+ genesis: Uint8Array, nonce: bigint,
19
+ payload: Uint8Array, rpc: ArxiumRpc) {
20
+ const signer = localEd25519Signer(privateKey, publicKey);
21
+ const sender = encodeAddress(publicKey);
22
+ const signature = await signActionWithSigner(signer, publicKey, genesis, sender, nonce, payload);
23
+ // An explicit send, only when the caller intends a transaction:
24
+ // await rpc.sendAction({ sender, payload, sign: verifiedSignerCallback(signer, publicKey) });
25
+ return signature;
26
+ }
27
+ ```
28
+
29
+ `Ed25519Signer` advertises `algorithm: "Ed25519"`, a raw32-byte compressed
30
+ public key and `sign(message): Promise<Uint8Array>` returning a raw64-byte
31
+ signature. The mode is **pure Ed25519 over the exact supplied message**.
32
+ No SDK prehash, double hash, DER/PEM wrapper, Ed25519ph/ctx or provider digest
33
+ mode is implied. A future backend must establish its actual mode/encoding;
34
+ different HSM/KMS products cannot be assumed to support this same operation.
35
+ The local adapter uses the existing WebCrypto Ed25519 primitive and never
36
+ exports private key material. It does not sign a probe at construction.
37
+
38
+ `signBytesWithSigner` snapshots the message and caller-selected expected key,
39
+ checks the advertised key/algorithm, invokes the provider once with a separate
40
+ buffer, snapshots the returned signature, then verifies it against the original
41
+ message and expected key before returning lowercase hex. The expected key must
42
+ come from trusted account/member configuration, not an unchecked provider claim.
43
+ The callback factory pins key bytes and the method at creation. Provider-side
44
+ mutation cannot change the trusted message; a signature of changed or hashed
45
+ bytes is refused. Each concurrent call owns separate state and buffers.
46
+
47
+ `signActionWithSigner` uses existing signingBytes: raw32-byte genesis, string
48
+ sender, u64 nonce, existing payload bytes. `signDvpConsentWithSigner` uses
49
+ existing dvpConsentBytes and its separate DvP domain/counterparty nonce. Amounts
50
+ remain bigint/u128 in their existing encoders; these helpers do not reinterpret
51
+ payload fields. Nonces accept exact bigint through u64 max, and safe integer
52
+ Numbers; unsafe values fail under A120. RPC's existing numeric nonce API still
53
+ rejects unsafe Numbers; this does not add full-u64 JSON RPC support.
54
+
55
+ For multisig/programmable accounts, sender is the stable account, while the
56
+ expected key is the chosen member key. These helpers return one member's raw
57
+ signature. Use existing multisigSignature/assemblePolicySignature tooling to
58
+ assemble a witness. Signature verification does not establish current policy,
59
+ membership, nonce freshness, balances, expiry, authorization or finality. The
60
+ verified callback directly returns a single-key signature; a custom callback
61
+ is still needed to assemble multi-member/policy witnesses before submission.
62
+
63
+ Provider errors/refusal/uncertain outcomes propagate unchanged. Invalid key,
64
+ mode, response shape or signature raises SignerVerificationError. There is no
65
+ fallback, retry, second signature, provider failover or implicit submission in
66
+ these helpers. They do not offer signing timeout/cancellation. A pending signer
67
+ may finish late; a caller's Promise.race or abandoned wait cannot cancel remote
68
+ work; abandoning an entire RPC send also does not cancel that still-pending
69
+ send or prevent its one originally requested submission. Once a provider promise rejects, a later resolve is ignored and does not
70
+ trigger submission. An explicit RPC send signs/submits once; submission errors
71
+ and the existing post-submit polling timeout propagate without automatic resend.
72
+ RPC timeoutMs begins after submission and does not interrupt signing or cancel a
73
+ transaction. A failed send rejects requests already queued for that sender
74
+ with the same error; they do not sign or submit after the failure. This changes
75
+ the old queue behavior that continued automatically after any error. Once the
76
+ queue unwinds, a new explicit request is possible. Reconcile an ambiguous remote
77
+ signing/submission outcome before any such separately authorized request. Per-sender queuing is local to one RPC
78
+ client, not a chain lock or cross-client nonce reservation.
79
+
80
+ ## HSM and KMS backends
81
+
82
+ Arxium signs with pure Ed25519. The SDK adds no dependency for either backend:
83
+ you pass in a client you have already configured, and the private key never leaves the device.
84
+
85
+ | Backend | Key | Mode | Status |
86
+ |---|---|---|---|
87
+ | PKCS#11 v3 (`pkcs11Ed25519Signer`) | `CKK_EC_EDWARDS`, Ed25519 | `CKM_EDDSA`, no parameters | Devnet E2E on SoftHSM 2.7 |
88
+ | AWS KMS (`awsKmsEd25519Signer`) | `ECC_NIST_EDWARDS25519` | `ED25519_SHA_512`, `MessageType: RAW` | Request shape unit-tested, no live KMS run yet |
89
+ | GCP Cloud KMS | `EC_SIGN_ED25519` | PureEdDSA | Software protection level only, not HSM: no adapter |
90
+ | Azure Key Vault / Managed HSM | no Ed25519 | | Not possible |
91
+
92
+ PKCS#11 tokens with Ed25519 include YubiHSM 2, Thales Luna 7.7+, Utimaco and
93
+ SoftHSM 2.6+. Check your firmware: `CKM_EDDSA` arrived with PKCS#11 3.0.
94
+
95
+ ```ts
96
+ import { KMS } from "@aws-sdk/client-kms";
97
+ const signer = await awsKmsEd25519Signer(new KMS({ region: "eu-west-1" }), "alias/treasury");
98
+ await rpc.sendAction({ sender: encodeAddress(trustedKey), payload, sign: verifiedSignerCallback(signer, trustedKey) });
99
+ ```
100
+
101
+ ```ts
102
+ import pkcs11js from "pkcs11js"; // logged-in session; key handles from C_FindObjects
103
+ const signer = pkcs11Ed25519Signer(p11, session, privateKeyHandle, publicKeyHandle);
104
+ ```
105
+
106
+ The adapters work with the verified helpers above. Pin `trustedKey` from your own
107
+ configuration, not from what the backend reports. For multisig, give each member its own
108
+ signer (one HSM, another KMS) and assemble with `multisigSignature`.
109
+
110
+ Limits:
111
+ - AWS KMS signs RAW messages of at most 4096 bytes (`AWS_KMS_MAX_MESSAGE`). Pure Ed25519
112
+ cannot be prehashed, so the adapter refuses a larger action before calling KMS.
113
+ `ED25519_PH_SHA_512` produces signatures the chain does not accept.
114
+ - A PKCS#11 session runs one operation at a time. The adapter's init and sign run
115
+ back to back synchronously, so calls on one session never interleave.
116
+
117
+ `scripts/pkcs11-e2e.mjs` generates a non-extractable key on a PKCS#11 token, funds it
118
+ from a devnet key and sends a transfer signed on the token. To run it on SoftHSM:
119
+
120
+ ```sh
121
+ brew install softhsm && softhsm2-util --init-token --free --label arx --pin 1234 --so-pin 5678
122
+ npm run build && npm i --no-save pkcs11js
123
+ PKCS11_LIB=/opt/homebrew/lib/softhsm/libsofthsm2.so PKCS11_PIN=1234 ARX_RPC=… ARX_TOKEN=… ARX_FUND_SEED=… node scripts/pkcs11-e2e.mjs
124
+ ```
125
+
126
+ Evidence: real WebCrypto signatures checked against Rust-generated action and
127
+ DvP vectors; adversarial buffers, wrong key/mode/signature, prehashed input,
128
+ refusal, ambiguous/late completion and concurrent requests; actual RPC/legacy/
129
+ multisig paths use injected synthetic fetch. These are source and synthetic
130
+ integration tests, **not HSM/KMS or production confirmation evidence**. The
131
+ PKCS#11 adapter has run end to end on devnet against SoftHSM. AWS KMS has not
132
+ run against a live key yet.
package/dist/actions.d.ts CHANGED
@@ -40,6 +40,8 @@ export declare const ACTION_VARIANT: {
40
40
  readonly applyAttestor: 41;
41
41
  readonly blockAttestor: 42;
42
42
  readonly settleDvp: 43;
43
+ readonly deployModule: 46;
44
+ readonly callModule: 47;
43
45
  };
44
46
  /** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
45
47
  export declare const TOKEN_ACTION = 39;
@@ -155,11 +157,112 @@ export declare const DVP_DOMAIN = 4294967295;
155
157
  /**
156
158
  * What the counterparty signs, mirroring `dvp_consent(…).signing_bytes`: the order, bound to the genesis hash and the
157
159
  * counterparty's current nonce, which settling consumes. Sending any other action first cancels the consent. A multisig
158
- * counterparty's members each sign these bytes and combine them with `multisigSignature`.
160
+ * counterparty's members each sign these bytes and combine them with `multisigSignature`; a programmable account's owners
161
+ * combine theirs with `assemblePolicySignature("owner", …)`.
159
162
  */
160
163
  export declare function dvpConsentBytes(genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Uint8Array;
161
164
  export declare function signDvpConsent(privateKey: CryptoKey, genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Promise<string>;
162
165
  export declare function encodeSettleDvp(order: DvpOrder, counterpartySignature: string): Uint8Array;
166
+ /** `ActionPayload::Collateral(CollateralAction)` is variant 44; these are `CollateralAction`'s own discriminants, written after it. */
167
+ export declare const COLLATERAL_ACTION = 44;
168
+ export declare const COLLATERAL_VARIANT: {
169
+ readonly pledge: 0;
170
+ readonly markPledge: 1;
171
+ readonly topUpPledge: 2;
172
+ readonly substitutePledge: 3;
173
+ readonly releasePledge: 4;
174
+ readonly seizePledge: 5;
175
+ };
176
+ /** `unitPrice` is quote units per base unit of the pledged asset, times this: `PRICE_SCALE` is a price of 1.00. */
177
+ export declare const PRICE_SCALE = 1000000000n;
178
+ /** `PledgeConsent.domain` and `SubstituteConsent.domain`: like `DVP_DOMAIN`, no consent doubles as an action signature. */
179
+ export declare const PLEDGE_DOMAIN = 4294967294;
180
+ export declare const SUBSTITUTE_DOMAIN = 4294967293;
181
+ /**
182
+ * The sender pledges `amount` of `asset` to `beneficiary` against `exposure`. `valuationAgent: null` makes the beneficiary the
183
+ * agent, the only account that may `markPledge`. A mark below the exposure opens a margin call the pledgor has
184
+ * `curePeriodBlocks` to cure; past it, or past `maturity + curePeriodBlocks` while exposure is outstanding, the beneficiary may seize.
185
+ */
186
+ export type PledgeTerms = {
187
+ beneficiary: string;
188
+ valuationAgent: string | null;
189
+ asset: SpendAsset;
190
+ amount: bigint;
191
+ haircutBps: number;
192
+ exposure: bigint;
193
+ unitPrice: bigint;
194
+ curePeriodBlocks: bigint;
195
+ maturity: bigint | null;
196
+ };
197
+ /** Mirrors `pledge_id`: `sha256("xc-pledge:" ‖ pledgor ‖ ":" ‖ nonce as 8 big-endian bytes)`, where `nonce` is the opening `Pledge`'s. */
198
+ export declare function pledgeId(pledgor: string, nonce: number | bigint): Promise<string>;
199
+ /**
200
+ * What the beneficiary signs, mirroring `pledge_consent(…).signing_bytes`: the terms, bound to the genesis hash and the
201
+ * beneficiary's current nonce, which the pledge consumes. Multisig and programmable beneficiaries combine signatures as for `dvpConsentBytes`.
202
+ */
203
+ export declare function pledgeConsentBytes(genesis: Uint8Array, pledgor: string, terms: PledgeTerms, beneficiaryNonce: number | bigint): Uint8Array;
204
+ export declare function signPledgeConsent(privateKey: CryptoKey, genesis: Uint8Array, pledgor: string, terms: PledgeTerms, beneficiaryNonce: number | bigint): Promise<string>;
205
+ export declare function encodePledge(terms: PledgeTerms, beneficiarySignature: string): Uint8Array;
206
+ /** The valuation agent's new price and exposure: opens a margin call on a shortfall, closes one once covered. Exposure `0` marks the debt repaid. */
207
+ export declare function encodeMarkPledge(pledge: string, unitPrice: bigint, exposure: bigint): Uint8Array;
208
+ /** The pledgor adds `amount` of the pledged asset. */
209
+ export declare function encodeTopUpPledge(pledge: string, amount: bigint): Uint8Array;
210
+ /** What the valuation agent signs for a substitution, mirroring `substitute_consent(…).signing_bytes`, at the agent's nonce (consumed). */
211
+ export declare function substituteConsentBytes(genesis: Uint8Array, agent: string, agentNonce: number | bigint, pledge: string, asset: SpendAsset, amount: bigint, unitPrice: bigint): Uint8Array;
212
+ export declare function signSubstituteConsent(privateKey: CryptoKey, genesis: Uint8Array, agent: string, agentNonce: number | bigint, pledge: string, asset: SpendAsset, amount: bigint, unitPrice: bigint): Promise<string>;
213
+ /** The pledgor swaps the collateral for `amount` of `asset` at `unitPrice`, with the agent's `signSubstituteConsent`. */
214
+ export declare function encodeSubstitutePledge(pledge: string, asset: SpendAsset, amount: bigint, unitPrice: bigint, agentSignature: string): Uint8Array;
215
+ /** `amount` back to the pledgor: the beneficiary at any time, the pledgor in full once the exposure is marked repaid. */
216
+ export declare function encodeReleasePledge(pledge: string, amount: bigint): Uint8Array;
217
+ /** In default only, by the beneficiary: everything (`full`) or the units worth the exposure, the rest returned. Closes the pledge. */
218
+ export declare function encodeSeizePledge(pledge: string, full: boolean): Uint8Array;
219
+ /** `ActionPayload::Venue(VenueAction)` is variant 45; these are `VenueAction`'s own discriminants, written after it. */
220
+ export declare const VENUE_ACTION = 45;
221
+ export declare const VENUE_VARIANT: {
222
+ readonly settleVenue: 0;
223
+ readonly cancelVenueOrder: 1;
224
+ };
225
+ /** `VenueOrder.domain`: like `DVP_DOMAIN`, an order signature never doubles as an action or a consent. */
226
+ export declare const VENUE_ORDER_DOMAIN = 4294967292;
227
+ /** Blocks an order may expire ahead of the height it settles at (one day at 2 s blocks). `signVenueOrder` callers should stay within it. */
228
+ export declare const MAX_ORDER_LIFETIME = 43200n;
229
+ /** Trades per `settleVenue`. */
230
+ export declare const MAX_VENUE_TRADES = 32;
231
+ /** One side of a trade: give `giveAmount` of `give` for `takeAmount` of `take`, settled only by `venue`, until `expiresAt`. */
232
+ export type VenueOrderTerms = {
233
+ venue: string;
234
+ give: SpendAsset;
235
+ giveAmount: bigint;
236
+ take: SpendAsset;
237
+ takeAmount: bigint;
238
+ expiresAt: bigint;
239
+ };
240
+ export type SignedVenueOrder = {
241
+ owner: string;
242
+ orderId: bigint;
243
+ terms: VenueOrderTerms;
244
+ signature: string;
245
+ };
246
+ /**
247
+ * What an order's owner signs, mirroring `venue_order(…).signing_bytes`: the terms as an action from the owner with
248
+ * `nonce = orderId`. Each `(owner, orderId)` settles at most once; pick a random u64. Multisig and programmable owners
249
+ * combine signatures as for `dvpConsentBytes`.
250
+ */
251
+ export declare function venueOrderBytes(genesis: Uint8Array, owner: string, orderId: number | bigint, terms: VenueOrderTerms): Uint8Array;
252
+ export declare function signVenueOrder(privateKey: CryptoKey, genesis: Uint8Array, owner: string, orderId: number | bigint, terms: VenueOrderTerms): Promise<string>;
253
+ /** The venue (sender, named in every order) settles 1..`MAX_VENUE_TRADES` mirrored trades, all or nothing, and pays the fee. */
254
+ export declare function encodeSettleVenue(trades: {
255
+ maker: SignedVenueOrder;
256
+ taker: SignedVenueOrder;
257
+ }[]): Uint8Array;
258
+ /** The owner kills its order. `expiresAt` must be the order's own expiry: the chain forgets the cancel after it. */
259
+ export declare function encodeCancelVenueOrder(orderId: number | bigint, expiresAt: number | bigint): Uint8Array;
260
+ /** Deploys a WASM asset module. Refused unless the chain has `modules_enabled`, and then only for an attested sender. Its address is `moduleAddress(sender, sha256(code), salt)` on the node; read it back from `GET /modules/{address}`. */
261
+ export declare function encodeDeployModule(code: Uint8Array, salt?: number | bigint): Uint8Array;
262
+ /** Calls a module's `call` export. The fee is set by `gasLimit`, not by the gas used, so keep it close to what the call needs. */
263
+ export declare function encodeCallModule(module: string, input: Uint8Array, gasLimit: number | bigint): Uint8Array;
264
+ /** Mirrors `module_address` in `arxd/runtime/src/module.rs`: where a module deployed by `deployer` with this `code` and `salt` lives. */
265
+ export declare function moduleAddress(deployer: string, code: Uint8Array, salt?: number | bigint): Promise<string>;
163
266
  /** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
164
267
  export declare const GOVERNANCE_ACTION: {
165
268
  readonly addAttestor: 4;
@@ -186,7 +289,7 @@ export declare function encodeVoteProposal(proposal: number | bigint, approve: b
186
289
  export declare function encodeExecuteProposal(proposal: number | bigint): Uint8Array;
187
290
  export declare function encodeSetAssetLimits(asset: string, maxHolders: number | null, maxBalancePerHolder?: bigint | null, maxAttestationAge?: bigint | null): Uint8Array;
188
291
  /** `{ name, input }` names and shapes match `fixtures/signed-actions.json`: amounts are decimal strings, byte fields are number arrays. */
189
- export type ActionName = keyof typeof ACTION_VARIANT | keyof typeof TOKEN_VARIANT | keyof typeof POLICY_VARIANT;
292
+ export type ActionName = keyof typeof ACTION_VARIANT | keyof typeof TOKEN_VARIANT | keyof typeof COLLATERAL_VARIANT | keyof typeof VENUE_VARIANT | keyof typeof POLICY_VARIANT;
190
293
  export type DecodedPayload = {
191
294
  name: ActionName;
192
295
  input: Record<string, any>;
package/dist/actions.js CHANGED
@@ -13,6 +13,7 @@ export const ACTION_VARIANT = {
13
13
  verifyClaimProof: 37, setPrivateClaims: 38,
14
14
  submitProposal: 30, voteProposal: 31, executeProposal: 32,
15
15
  applyAttestor: 41, blockAttestor: 42, settleDvp: 43,
16
+ deployModule: 46, callModule: 47,
16
17
  };
17
18
  /** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
18
19
  export const TOKEN_ACTION = 39;
@@ -81,28 +82,157 @@ export function encodeApplyAttestor(name, owners, threshold, evidenceHash, evide
81
82
  export function encodeBlockAttestor() { return new Writer().varint(ACTION_VARIANT.blockAttestor).bytes(); }
82
83
  /** `DvpTerms.domain`: no `ActionPayload` can start with it, so a consent never doubles as an ordinary action signature. */
83
84
  export const DVP_DOMAIN = 0xffff_ffff;
85
+ /** Protocol u64 fields must not inherit the generic writer's wider u128 range. */
86
+ function exactU64(value, field) {
87
+ if (typeof value === "number" && !Number.isSafeInteger(value))
88
+ throw new RangeError(`${field} must be an exact u64; use bigint above Number.MAX_SAFE_INTEGER`);
89
+ const integer = BigInt(value);
90
+ if (integer < 0n || integer > 0xffffffffffffffffn)
91
+ throw new RangeError(`${field} out of u64 range`);
92
+ return integer;
93
+ }
84
94
  /**
85
95
  * What the counterparty signs, mirroring `dvp_consent(…).signing_bytes`: the order, bound to the genesis hash and the
86
96
  * counterparty's current nonce, which settling consumes. Sending any other action first cancels the consent. A multisig
87
- * counterparty's members each sign these bytes and combine them with `multisigSignature`.
97
+ * counterparty's members each sign these bytes and combine them with `multisigSignature`; a programmable account's owners
98
+ * combine theirs with `assemblePolicySignature("owner", …)`.
88
99
  */
89
100
  export function dvpConsentBytes(genesis, initiator, order, counterpartyNonce) {
101
+ const expiresAt = exactU64(order.expiresAt, "expiresAt");
90
102
  const w = new Writer().varint(DVP_DOMAIN).string(initiator);
91
103
  writeSpendAsset(w, order.give);
92
104
  w.varint(order.giveAmount);
93
105
  writeSpendAsset(w, order.take);
94
- w.varint(order.takeAmount).varint(order.expiresAt);
106
+ w.varint(order.takeAmount).varint(expiresAt);
95
107
  return signingBytes(genesis, order.counterparty, counterpartyNonce, w.bytes());
96
108
  }
97
109
  export async function signDvpConsent(privateKey, genesis, initiator, order, counterpartyNonce) { return toHex(new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(dvpConsentBytes(genesis, initiator, order, counterpartyNonce))))); }
98
110
  export function encodeSettleDvp(order, counterpartySignature) {
111
+ const expiresAt = exactU64(order.expiresAt, "expiresAt");
99
112
  const w = new Writer().varint(ACTION_VARIANT.settleDvp);
100
113
  writeSpendAsset(w, order.give);
101
114
  w.varint(order.giveAmount).string(order.counterparty);
102
115
  writeSpendAsset(w, order.take);
103
- w.varint(order.takeAmount).varint(order.expiresAt).string(counterpartySignature);
116
+ w.varint(order.takeAmount).varint(expiresAt).string(counterpartySignature);
104
117
  return w.bytes();
105
118
  }
119
+ /** `ActionPayload::Collateral(CollateralAction)` is variant 44; these are `CollateralAction`'s own discriminants, written after it. */
120
+ export const COLLATERAL_ACTION = 44;
121
+ export const COLLATERAL_VARIANT = { pledge: 0, markPledge: 1, topUpPledge: 2, substitutePledge: 3, releasePledge: 4, seizePledge: 5 };
122
+ /** `unitPrice` is quote units per base unit of the pledged asset, times this: `PRICE_SCALE` is a price of 1.00. */
123
+ export const PRICE_SCALE = 1000000000n;
124
+ /** `PledgeConsent.domain` and `SubstituteConsent.domain`: like `DVP_DOMAIN`, no consent doubles as an action signature. */
125
+ export const PLEDGE_DOMAIN = 0xffff_fffe;
126
+ export const SUBSTITUTE_DOMAIN = 0xffff_fffd;
127
+ function exactU16(value, field) {
128
+ if (!Number.isInteger(value) || value < 0 || value > 0xffff)
129
+ throw new RangeError(`${field} out of u16 range`);
130
+ return value;
131
+ }
132
+ /** Mirrors `pledge_id`: `sha256("xc-pledge:" ‖ pledgor ‖ ":" ‖ nonce as 8 big-endian bytes)`, where `nonce` is the opening `Pledge`'s. */
133
+ export async function pledgeId(pledgor, nonce) {
134
+ const prefix = new TextEncoder().encode(`xc-pledge:${pledgor}:`), n = exactU64(nonce, "nonce"), input = new Uint8Array(prefix.length + 8);
135
+ input.set(prefix);
136
+ new DataView(input.buffer).setBigUint64(prefix.length, n);
137
+ return `0x${toHex(new Uint8Array(await crypto.subtle.digest("SHA-256", input)))}`;
138
+ }
139
+ /**
140
+ * What the beneficiary signs, mirroring `pledge_consent(…).signing_bytes`: the terms, bound to the genesis hash and the
141
+ * beneficiary's current nonce, which the pledge consumes. Multisig and programmable beneficiaries combine signatures as for `dvpConsentBytes`.
142
+ */
143
+ export function pledgeConsentBytes(genesis, pledgor, terms, beneficiaryNonce) {
144
+ const w = new Writer().varint(PLEDGE_DOMAIN).string(pledgor).string(terms.valuationAgent ?? terms.beneficiary);
145
+ writeSpendAsset(w, terms.asset);
146
+ w.varint(terms.amount).varint(exactU16(terms.haircutBps, "haircutBps")).varint(terms.exposure).varint(terms.unitPrice).varint(exactU64(terms.curePeriodBlocks, "curePeriodBlocks"));
147
+ w.option(terms.maturity, (ww, m) => ww.varint(exactU64(m, "maturity")));
148
+ return signingBytes(genesis, terms.beneficiary, beneficiaryNonce, w.bytes());
149
+ }
150
+ export async function signPledgeConsent(privateKey, genesis, pledgor, terms, beneficiaryNonce) { return toHex(new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(pledgeConsentBytes(genesis, pledgor, terms, beneficiaryNonce))))); }
151
+ const collateral = (name) => new Writer().varint(COLLATERAL_ACTION).varint(COLLATERAL_VARIANT[name]);
152
+ export function encodePledge(terms, beneficiarySignature) {
153
+ const w = collateral("pledge").string(terms.beneficiary).option(terms.valuationAgent, (ww, agent) => ww.string(agent));
154
+ writeSpendAsset(w, terms.asset);
155
+ w.varint(terms.amount).varint(exactU16(terms.haircutBps, "haircutBps")).varint(terms.exposure).varint(terms.unitPrice).varint(exactU64(terms.curePeriodBlocks, "curePeriodBlocks"));
156
+ return w.option(terms.maturity, (ww, m) => ww.varint(exactU64(m, "maturity"))).string(beneficiarySignature).bytes();
157
+ }
158
+ /** The valuation agent's new price and exposure: opens a margin call on a shortfall, closes one once covered. Exposure `0` marks the debt repaid. */
159
+ export function encodeMarkPledge(pledge, unitPrice, exposure) { return collateral("markPledge").string(pledge).varint(unitPrice).varint(exposure).bytes(); }
160
+ /** The pledgor adds `amount` of the pledged asset. */
161
+ export function encodeTopUpPledge(pledge, amount) { return collateral("topUpPledge").string(pledge).varint(amount).bytes(); }
162
+ /** What the valuation agent signs for a substitution, mirroring `substitute_consent(…).signing_bytes`, at the agent's nonce (consumed). */
163
+ export function substituteConsentBytes(genesis, agent, agentNonce, pledge, asset, amount, unitPrice) {
164
+ const w = new Writer().varint(SUBSTITUTE_DOMAIN).string(pledge);
165
+ writeSpendAsset(w, asset);
166
+ return signingBytes(genesis, agent, agentNonce, w.varint(amount).varint(unitPrice).bytes());
167
+ }
168
+ export async function signSubstituteConsent(privateKey, genesis, agent, agentNonce, pledge, asset, amount, unitPrice) { return toHex(new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(substituteConsentBytes(genesis, agent, agentNonce, pledge, asset, amount, unitPrice))))); }
169
+ /** The pledgor swaps the collateral for `amount` of `asset` at `unitPrice`, with the agent's `signSubstituteConsent`. */
170
+ export function encodeSubstitutePledge(pledge, asset, amount, unitPrice, agentSignature) {
171
+ const w = collateral("substitutePledge").string(pledge);
172
+ writeSpendAsset(w, asset);
173
+ return w.varint(amount).varint(unitPrice).string(agentSignature).bytes();
174
+ }
175
+ /** `amount` back to the pledgor: the beneficiary at any time, the pledgor in full once the exposure is marked repaid. */
176
+ export function encodeReleasePledge(pledge, amount) { return collateral("releasePledge").string(pledge).varint(amount).bytes(); }
177
+ /** In default only, by the beneficiary: everything (`full`) or the units worth the exposure, the rest returned. Closes the pledge. */
178
+ export function encodeSeizePledge(pledge, full) { return collateral("seizePledge").string(pledge).bool(full).bytes(); }
179
+ /** `ActionPayload::Venue(VenueAction)` is variant 45; these are `VenueAction`'s own discriminants, written after it. */
180
+ export const VENUE_ACTION = 45;
181
+ export const VENUE_VARIANT = { settleVenue: 0, cancelVenueOrder: 1 };
182
+ /** `VenueOrder.domain`: like `DVP_DOMAIN`, an order signature never doubles as an action or a consent. */
183
+ export const VENUE_ORDER_DOMAIN = 0xffff_fffc;
184
+ /** Blocks an order may expire ahead of the height it settles at (one day at 2 s blocks). `signVenueOrder` callers should stay within it. */
185
+ export const MAX_ORDER_LIFETIME = 43200n;
186
+ /** Trades per `settleVenue`. */
187
+ export const MAX_VENUE_TRADES = 32;
188
+ function writeVenueTerms(w, terms) {
189
+ const expiresAt = exactU64(terms.expiresAt, "expiresAt");
190
+ w.varint(VENUE_ORDER_DOMAIN).string(terms.venue);
191
+ writeSpendAsset(w, terms.give);
192
+ w.varint(terms.giveAmount);
193
+ writeSpendAsset(w, terms.take);
194
+ return w.varint(terms.takeAmount).varint(expiresAt);
195
+ }
196
+ /**
197
+ * What an order's owner signs, mirroring `venue_order(…).signing_bytes`: the terms as an action from the owner with
198
+ * `nonce = orderId`. Each `(owner, orderId)` settles at most once; pick a random u64. Multisig and programmable owners
199
+ * combine signatures as for `dvpConsentBytes`.
200
+ */
201
+ export function venueOrderBytes(genesis, owner, orderId, terms) {
202
+ return signingBytes(genesis, owner, exactU64(orderId, "orderId"), writeVenueTerms(new Writer(), terms).bytes());
203
+ }
204
+ export async function signVenueOrder(privateKey, genesis, owner, orderId, terms) { return toHex(new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(venueOrderBytes(genesis, owner, orderId, terms))))); }
205
+ const venue = (name) => new Writer().varint(VENUE_ACTION).varint(VENUE_VARIANT[name]);
206
+ function writeSignedOrder(w, order) {
207
+ w.string(order.owner).varint(exactU64(order.orderId, "orderId"));
208
+ return writeVenueTerms(w, order.terms).string(order.signature);
209
+ }
210
+ /** The venue (sender, named in every order) settles 1..`MAX_VENUE_TRADES` mirrored trades, all or nothing, and pays the fee. */
211
+ export function encodeSettleVenue(trades) {
212
+ if (trades.length < 1 || trades.length > MAX_VENUE_TRADES)
213
+ throw new RangeError(`a venue settlement carries 1 to ${MAX_VENUE_TRADES} trades`);
214
+ return venue("settleVenue").vec(trades, (w, t) => { writeSignedOrder(w, t.maker); writeSignedOrder(w, t.taker); }).bytes();
215
+ }
216
+ /** The owner kills its order. `expiresAt` must be the order's own expiry: the chain forgets the cancel after it. */
217
+ export function encodeCancelVenueOrder(orderId, expiresAt) { return venue("cancelVenueOrder").varint(exactU64(orderId, "orderId")).varint(exactU64(expiresAt, "expiresAt")).bytes(); }
218
+ /** Deploys a WASM asset module. Refused unless the chain has `modules_enabled`, and then only for an attested sender. Its address is `moduleAddress(sender, sha256(code), salt)` on the node; read it back from `GET /modules/{address}`. */
219
+ export function encodeDeployModule(code, salt = 0) { return new Writer().varint(ACTION_VARIANT.deployModule).vec(Array.from(code), (w, b) => w.u8(b)).varint(exactU64(salt, "salt")).bytes(); }
220
+ /** Calls a module's `call` export. The fee is set by `gasLimit`, not by the gas used, so keep it close to what the call needs. */
221
+ export function encodeCallModule(module, input, gasLimit) { return new Writer().varint(ACTION_VARIANT.callModule).string(module).vec(Array.from(input), (w, b) => w.u8(b)).varint(exactU64(gasLimit, "gasLimit")).bytes(); }
222
+ /** Mirrors `module_address` in `arxd/runtime/src/module.rs`: where a module deployed by `deployer` with this `code` and `salt` lives. */
223
+ export async function moduleAddress(deployer, code, salt = 0) {
224
+ const sha = async (bytes) => new Uint8Array(await crypto.subtle.digest("SHA-256", asBuffer(bytes))), text = new TextEncoder();
225
+ const tail = new Uint8Array(8);
226
+ new DataView(tail.buffer).setBigUint64(0, exactU64(salt, "salt"));
227
+ const parts = [text.encode("xc-module:"), text.encode(deployer), text.encode(":"), await sha(code), tail];
228
+ const preimage = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
229
+ let at = 0;
230
+ for (const p of parts) {
231
+ preimage.set(p, at);
232
+ at += p.length;
233
+ }
234
+ return encodeAddress(await sha(preimage));
235
+ }
106
236
  /** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
107
237
  export const GOVERNANCE_ACTION = { addAttestor: 4, removeAttestor: 5, unblockAttestor: 6 };
108
238
  /** An active validator opens a vote. `addAttestor` needs the attestor's multisig address and its owners; applicants who are not validators use `encodeApplyAttestor`. A removal blocks that attestor's new grants while it runs. */
@@ -118,6 +248,7 @@ export function encodeVoteProposal(proposal, approve) { return new Writer().vari
118
248
  export function encodeExecuteProposal(proposal) { return new Writer().varint(ACTION_VARIANT.executeProposal).varint(BigInt(proposal)).bytes(); }
119
249
  export function encodeSetAssetLimits(asset, maxHolders, maxBalancePerHolder = null, maxAttestationAge = null) { return new Writer().varint(ACTION_VARIANT.setAssetLimits).string(asset).option(maxHolders, (w, value) => w.varint(value)).option(maxBalancePerHolder, (w, value) => w.varint(value)).option(maxAttestationAge, (w, value) => w.varint(value)).bytes(); }
120
250
  const nullableBig = (value) => value == null ? null : BigInt(value);
251
+ const orderInput = (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 });
121
252
  export function encodePayload({ name, input }) {
122
253
  if (name in POLICY_VARIANT)
123
254
  return encodeAccountPolicy(name, input);
@@ -166,11 +297,21 @@ export function encodePayload({ name, input }) {
166
297
  case "transferToken": return encodeTransferToken(input.token, input.to, BigInt(input.amount));
167
298
  case "burnToken": return encodeBurnToken(input.token, BigInt(input.amount));
168
299
  case "renounceMint": return encodeRenounceMint(input.token);
300
+ case "pledge": return encodePledge({ beneficiary: input.beneficiary, valuationAgent: input.valuationAgent, asset: input.asset, amount: BigInt(input.amount), haircutBps: input.haircutBps, exposure: BigInt(input.exposure), unitPrice: BigInt(input.unitPrice), curePeriodBlocks: BigInt(input.curePeriodBlocks), maturity: nullableBig(input.maturity) }, input.beneficiarySignature);
301
+ case "markPledge": return encodeMarkPledge(input.pledge, BigInt(input.unitPrice), BigInt(input.exposure));
302
+ case "topUpPledge": return encodeTopUpPledge(input.pledge, BigInt(input.amount));
303
+ case "substitutePledge": return encodeSubstitutePledge(input.pledge, input.asset, BigInt(input.amount), BigInt(input.unitPrice), input.agentSignature);
304
+ case "releasePledge": return encodeReleasePledge(input.pledge, BigInt(input.amount));
305
+ case "seizePledge": return encodeSeizePledge(input.pledge, input.full);
306
+ case "settleVenue": return encodeSettleVenue(input.trades.map((t) => ({ maker: orderInput(t.maker), taker: orderInput(t.taker) })));
307
+ case "cancelVenueOrder": return encodeCancelVenueOrder(BigInt(input.orderId), BigInt(input.expiresAt));
308
+ case "deployModule": return encodeDeployModule(Uint8Array.from(input.code), BigInt(input.salt));
309
+ case "callModule": return encodeCallModule(input.module, Uint8Array.from(input.input), BigInt(input.gasLimit));
169
310
  default: throw new Error(`unsupported action: ${name}`);
170
311
  }
171
312
  }
172
313
  const invert = (table) => Object.fromEntries(Object.entries(table).map(([key, value]) => [value, key]));
173
- const VARIANT_NAME = invert(ACTION_VARIANT), TOKEN_NAME = invert(TOKEN_VARIANT), CLASS_NAME = invert(CLASS), TOPIC_NAME = invert(TOPIC), GOVERNANCE_NAME = invert(GOVERNANCE_ACTION);
314
+ const VARIANT_NAME = invert(ACTION_VARIANT), TOKEN_NAME = invert(TOKEN_VARIANT), COLLATERAL_NAME = invert(COLLATERAL_VARIANT), VENUE_NAME = invert(VENUE_VARIANT), CLASS_NAME = invert(CLASS), TOPIC_NAME = invert(TOPIC), GOVERNANCE_NAME = invert(GOVERNANCE_ACTION);
174
315
  const known = (value, what) => { if (value === undefined)
175
316
  throw new Error(`unknown ${what}`); return value; };
176
317
  /**
@@ -186,7 +327,7 @@ export function decodePayload(payload) {
186
327
  throw new Error("payload is not in canonical form");
187
328
  return decoded;
188
329
  }
189
- const name = outer === TOKEN_ACTION ? known(TOKEN_NAME[Number(r.varint())], "token action") : known(VARIANT_NAME[outer], "action variant");
330
+ const name = outer === TOKEN_ACTION ? known(TOKEN_NAME[Number(r.varint())], "token action") : outer === COLLATERAL_ACTION ? known(COLLATERAL_NAME[Number(r.varint())], "collateral action") : outer === VENUE_ACTION ? known(VENUE_NAME[Number(r.varint())], "venue action") : known(VARIANT_NAME[outer], "action variant");
190
331
  const str = () => r.string(), amount = () => r.varint().toString(), bytes = () => r.vec((rr) => rr.u8()), topics = () => r.vec((rr) => known(TOPIC_NAME[Number(rr.varint())], "claim topic"));
191
332
  const read = {
192
333
  transfer: () => ({ to: str(), amount: amount() }),
@@ -233,6 +374,25 @@ export function decodePayload(payload) {
233
374
  transferToken: () => ({ token: str(), to: str(), amount: amount() }),
234
375
  burnToken: () => ({ token: str(), amount: amount() }),
235
376
  renounceMint: () => ({ token: str() }),
377
+ pledge: () => ({ beneficiary: str(), valuationAgent: r.option(str), asset: readSpendAsset(r), amount: amount(), haircutBps: Number(r.varint()), exposure: amount(), unitPrice: amount(), curePeriodBlocks: amount(), maturity: r.option(amount), beneficiarySignature: str() }),
378
+ markPledge: () => ({ pledge: str(), unitPrice: amount(), exposure: amount() }),
379
+ topUpPledge: () => ({ pledge: str(), amount: amount() }),
380
+ substitutePledge: () => ({ pledge: str(), asset: readSpendAsset(r), amount: amount(), unitPrice: amount(), agentSignature: str() }),
381
+ releasePledge: () => ({ pledge: str(), amount: amount() }),
382
+ seizePledge: () => ({ pledge: str(), full: r.bool() }),
383
+ settleVenue: () => {
384
+ const order = () => {
385
+ const owner = str(), orderId = amount();
386
+ if (Number(r.varint()) !== VENUE_ORDER_DOMAIN)
387
+ throw new Error("not a venue order");
388
+ const terms = { venue: str(), give: readSpendAsset(r), giveAmount: amount(), take: readSpendAsset(r), takeAmount: amount(), expiresAt: amount() };
389
+ return { owner, orderId, terms, signature: str() };
390
+ };
391
+ return { trades: r.vec(() => ({ maker: order(), taker: order() })) };
392
+ },
393
+ cancelVenueOrder: () => ({ orderId: amount(), expiresAt: amount() }),
394
+ deployModule: () => ({ code: bytes(), salt: amount() }),
395
+ callModule: () => ({ module: str(), input: bytes(), gasLimit: amount() }),
236
396
  };
237
397
  const decoded = { name, input: read[name]() };
238
398
  r.done();
@@ -242,7 +402,7 @@ export function decodePayload(payload) {
242
402
  }
243
403
  /** Mirrors `Action::signing_bytes`: the chain's 32-byte genesis hash (`GET /genesis-hash`) binds the signature to one chain, so an action signed for chain A is rejected on chain B. */
244
404
  export function signingBytes(genesis, sender, nonce, payload) { if (genesis.length !== 32)
245
- throw new Error("genesis hash must be 32 bytes"); return new Writer().raw(genesis).string(sender).varint(nonce).raw(payload).bytes(); }
405
+ throw new Error("genesis hash must be 32 bytes"); return new Writer().raw(genesis).string(sender).varint(exactU64(nonce, "nonce")).raw(payload).bytes(); }
246
406
  export async function signAction(privateKey, genesis, sender, nonce, payload) { return toHex(new Uint8Array(await crypto.subtle.sign("Ed25519", privateKey, asBuffer(signingBytes(genesis, sender, nonce, payload))))); }
247
407
  export function submitBody(sender, nonce, signature, payload) { return { sender, nonce, signature, payload: Array.from(payload) }; }
248
408
  /** M-of-N sender, mirrors `xc_primitives::multisig_address`. Every member signs with plain `signAction(key, genesis, multisigAddress, nonce, payload)`; `multisigSignature` combines exactly `threshold` of them into the action's `signature` field. */