@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 +73 -3
- package/SIGNERS.md +132 -0
- package/dist/actions.d.ts +105 -2
- package/dist/actions.js +166 -6
- package/dist/cli.js +49 -5
- package/dist/fees.d.ts +4 -0
- package/dist/fees.js +28 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/rpc.d.ts +116 -3
- package/dist/rpc.js +49 -12
- package/dist/signer.d.ts +63 -0
- package/dist/signer.js +107 -0
- package/package.json +4 -3
package/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
|
|
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 `
|
|
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(
|
|
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(
|
|
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. */
|