@arxiumlabs/sdk 0.5.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -17
- package/dist/actions.d.ts +33 -1
- package/dist/actions.js +36 -4
- package/dist/cli.js +97 -7
- package/dist/fees.d.ts +26 -0
- package/dist/fees.js +63 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/policy.d.ts +5 -1
- package/dist/policy.js +2 -0
- package/dist/rpc.d.ts +191 -8
- package/dist/rpc.js +105 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install -g @arxiumlabs/sdk # arx CLI
|
|
|
13
13
|
|
|
14
14
|
## Releasing
|
|
15
15
|
|
|
16
|
-
Bump `version` here, run the release checks, then push a `sdk-vX.Y.Z` tag. `.github/workflows/sdk.yml` publishes to npm through trusted publishing (no token)
|
|
16
|
+
Bump `version` here, run the release checks, then push a `sdk-vX.Y.Z` tag. `.github/workflows/sdk.yml` publishes to npm through trusted publishing (no token). It publishes without a provenance attestation while the repository is private, because npm accepts provenance only from public repositories.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
cd Arxium && cargo test -p arxd-runtime writes_typescript_signed_action_fixtures && git diff --exit-code sdk/ts/fixtures
|
|
@@ -29,7 +29,28 @@ const rpc = new ArxiumRpc({ rpc: "https://node.example", token: process.env.ARX_
|
|
|
29
29
|
const payload = encodeTransfer("arx1...", arxToIum("1.5"));
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
All token amounts are `bigint`: `1 ARX = 1_000_000_000 IUM`.
|
|
32
|
+
All token amounts are `bigint`: `1 ARX = 1_000_000_000 IUM`. That includes amounts in RPC responses (`balance`, `total_supply`, `frozen_amount`, …): the node writes u128 amounts as plain JSON numbers, so the client parses them with `parseJson`, which never rounds them. `JSON.stringify` throws on a `bigint`, so pass `bigintReplacer` when you serialize a response.
|
|
33
|
+
|
|
34
|
+
### Fees
|
|
35
|
+
|
|
36
|
+
An action costs `action_fee + weight × weight_fee`. The weight is the action type's base weight plus the signed action's size in bytes, plus 5,000 when a stateful account policy signs it. `actionFee` matches the node's metering exactly, and the tests check it against weights the node itself produced:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const fee = actionFee(await rpc.feeParams(), sender, nonce, payload); // single-key signature
|
|
40
|
+
const fee2 = actionFee(params, sender, nonce, payload, { signatureLength: multisigSignatureLength(2, 3) }); // before a 2-of-3 multisig signs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`CreateToken` also pays `token_create_fee` (`rpc.actionFee()`). `ACTION_FEE` is deprecated, because it is only the flat floor.
|
|
44
|
+
|
|
45
|
+
### Reading the chain
|
|
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.
|
|
48
|
+
|
|
49
|
+
### Sending
|
|
50
|
+
|
|
51
|
+
`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
|
+
|
|
53
|
+
`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
|
|
33
54
|
|
|
34
55
|
### Chain binding
|
|
35
56
|
|
|
@@ -52,6 +73,22 @@ A co-signer should see what they're signing before they sign it. `decodePayload(
|
|
|
52
73
|
|
|
53
74
|
Set the resulting address as an admin role (genesis or `SetAdmin`) or as an asset issuer (`TransferIssuer`). To rotate members, move the role to a new multisig address.
|
|
54
75
|
|
|
76
|
+
### Atomic DvP (delivery versus payment)
|
|
77
|
+
|
|
78
|
+
`SettleDvp` swaps two assets between two parties in one action: both legs settle or neither does. Each leg is `"native"` (IUM) or `{ asset }` (an RWA asset, with its full compliance gates, or a token). The counterparty signs the order first, at its current nonce. The initiator then sends it and pays the fee:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const order: DvpOrder = { give: { asset: bondRef }, giveAmount: 10n, counterparty: buyer, take: { asset: cashRef }, takeAmount: 500n, expiresAt: height + 100n };
|
|
82
|
+
const consent = await signDvpConsent(buyerKey, genesis, seller, order, buyerNonce); // on the buyer's machine
|
|
83
|
+
await rpc.sendAction({ privateKey: sellerKey, sender: seller, payload: encodeSettleDvp(order, consent) });
|
|
84
|
+
```
|
|
85
|
+
|
|
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.
|
|
87
|
+
|
|
88
|
+
### Programmable accounts
|
|
89
|
+
|
|
90
|
+
The SDK exports bounded account-policy codecs and stateful owner/session/guardian witness helpers. `arx multisig build|sign|combine|link` lets each member sign separately, for both immutable and stateful account addresses. To verify a historical action, `verifySignedAction` needs the proven pre-action policy for stateful witnesses, and `arx verify SIGNATURE --policy FILE` accepts that policy. See [the programmable-account guide](../../docs/Programmable_Accounts.md).
|
|
91
|
+
|
|
55
92
|
## Key files
|
|
56
93
|
|
|
57
94
|
`arx` reads and writes Console's encrypted export JSON unchanged:
|
|
@@ -85,7 +122,7 @@ printf '%s' "$SEED_HEX" | arx keys import
|
|
|
85
122
|
arx keys show --key arxium-devnet-key-abc.json # no passphrase needed
|
|
86
123
|
|
|
87
124
|
arx sign transfer arx1recipient 1500000000 --key key.json # nonce fetched from --rpc
|
|
88
|
-
arx sign transfer arx1recipient 1500000000 --key key.json --nonce 7 #
|
|
125
|
+
arx sign transfer arx1recipient 1500000000 --key key.json --nonce 7 # skips the nonce lookup; still reads the genesis hash from --rpc
|
|
89
126
|
arx submit signed-action.json
|
|
90
127
|
arx send transfer arx1recipient 1500000000 --key key.json
|
|
91
128
|
|
|
@@ -100,30 +137,51 @@ arx query search arx1address
|
|
|
100
137
|
arx verify signature
|
|
101
138
|
```
|
|
102
139
|
|
|
103
|
-
|
|
140
|
+
`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
|
+
|
|
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).
|
|
104
143
|
|
|
105
144
|
```sh
|
|
106
145
|
arx send apply-attestor "Arxium Bank" <owner>,<owner>,<owner> 2 <evidence-hash> <evidence-uri> # sender is the 2-of-N multisig; validators vote
|
|
107
146
|
arx send propose-remove-attestor <attestor> "reason" # validators only; blocks that attestor's grants while the vote runs
|
|
108
147
|
arx send vote <proposal-id> yes # then, after the window: arx send execute-proposal <proposal-id>
|
|
109
148
|
arx send block-attestor # attestor emergency stop; only a governance vote lifts it
|
|
110
|
-
arx send register-asset fund-a FUNDA "Fund A" 0 kyc CH,LI # <asset-id> <symbol> <name> <decimals> [topics] [jurisdictions]
|
|
149
|
+
arx send register-asset fund-a FUNDA "Fund A" 0 kyc CH,LI # <asset-id> <symbol> <name> <decimals> [topics] [jurisdictions] [controls]
|
|
150
|
+
arx send register-asset fund-b FUNDB "Fund B" 0 - - freeze_asset,recover_holder # only these issuer controls, forever
|
|
111
151
|
arx send issue-asset arxasset1... 1000
|
|
112
152
|
arx send transfer-asset arxasset1... arx1recipient 100
|
|
153
|
+
arx send set-asset-limits arxasset1... - 150 - # <max-holders|-> [max-balance-per-holder|-] [max-attestation-age-blocks|-]; - = no limit
|
|
154
|
+
arx send freeze-holder arxasset1... arx1holder # unfreeze-holder to lift
|
|
155
|
+
arx send freeze-asset arxasset1... "reason" # unfreeze-asset arxasset1... "reason" to lift
|
|
156
|
+
arx send issue-asset-to arxasset1... arx1holder 100
|
|
157
|
+
arx send lock-holder-amount arxasset1... arx1holder 50 [until-height] # unlock-holder-amount to release
|
|
158
|
+
arx send forced-transfer arxasset1... arx1from arx1to 10 "court order"
|
|
159
|
+
arx send recover-holder arxasset1... arx1lost arx1replacement
|
|
160
|
+
arx send burn-asset arxasset1... 10
|
|
161
|
+
arx send lock-issuance arxasset1... # one-way
|
|
162
|
+
arx send transfer-issuer arxasset1... arx1newissuer
|
|
163
|
+
arx send set-metadata-uri arxasset1... ipfs://... # - clears it
|
|
164
|
+
arx send set-private-claims arxasset1... on|off
|
|
165
|
+
arx send snapshot-holders arxasset1... # then, by the snapshot height:
|
|
166
|
+
arx send distribute arxasset1... <height> arxasset1payout... 1000
|
|
167
|
+
arx send redeem-holders arxasset1... <height>
|
|
168
|
+
arx send split-asset arxasset1... <height> 2 1
|
|
169
|
+
arx send create-token ABC "Abc Token" 6 1000000 - mintable # <max-supply|-> mintable|fixed
|
|
170
|
+
arx send mint-token arxasset1... arx1to 100 # transfer-token, burn-token, renounce-mint
|
|
171
|
+
arx dvp-consent arx1seller arxasset1bond... 10 native 500 <expires-at-height> --key buyer.json # buyer signs; prints the "send" args
|
|
172
|
+
arx send settle-dvp arxasset1bond... 10 arx1buyer native 500 <expires-at-height> <buyer-signature> --key seller.json
|
|
173
|
+
|
|
174
|
+
arx query assets [arx1address] # all assets, or one account's balances
|
|
175
|
+
arx query asset arxasset1...
|
|
176
|
+
arx query holders arxasset1...
|
|
177
|
+
arx query balance arx1address arxasset1...
|
|
178
|
+
arx query attestors # attestor <address>
|
|
179
|
+
arx query validator arx1address
|
|
180
|
+
arx query stakes arx1address
|
|
181
|
+
arx query fee
|
|
113
182
|
```
|
|
114
183
|
|
|
115
184
|
Builder docs (devnet quickstart, RWA walkthrough, arx-verify): https://arxium.network/docs
|
|
116
|
-
# Programmable-account tooling (0.1.5)
|
|
117
|
-
|
|
118
|
-
The SDK exports bounded account-policy codecs and stateful owner/session/guardian
|
|
119
|
-
witness helpers. `arx multisig build|sign|combine|link` provides an individual
|
|
120
|
-
member signing workflow for both immutable and stateful account addresses.
|
|
121
|
-
Historical `verifySignedAction` calls require the proven pre-action policy for
|
|
122
|
-
stateful witnesses; `arx verify SIGNATURE --policy FILE` accepts that policy.
|
|
123
|
-
|
|
124
|
-
See [the programmable-account guide](../../docs/Programmable_Accounts.md) for
|
|
125
|
-
configuration shapes, fees, block-height windows, recovery and reset requirements.
|
|
126
|
-
|
|
127
185
|
## Release validation
|
|
128
186
|
|
|
129
187
|
CI builds and installs the packed SDK on pull requests. Run `npm ci`,
|
|
@@ -133,7 +191,7 @@ public entry point and checks the CLI version; it makes no chain calls.
|
|
|
133
191
|
|
|
134
192
|
Publication runs only for an approved `sdk-vX.Y.Z` tag matching package.json.
|
|
135
193
|
The publish job builds its own checkout, checks the packed files and smoke-tests
|
|
136
|
-
the tarball, then publishes that exact tarball
|
|
194
|
+
the tarball, then publishes that exact tarball (no provenance while the repo is private). Existing npm
|
|
137
195
|
versions cannot be replaced. npm trusted-publisher configuration and release
|
|
138
196
|
authorization are still required; a passing package check is not publication
|
|
139
197
|
or on-chain compatibility proof.
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { POLICY_VARIANT } from "./policy.js";
|
|
1
|
+
import { POLICY_VARIANT, type SpendAsset } from "./policy.js";
|
|
2
2
|
/** These indices are ActionPayload's positional bincode discriminants. Never derive them. */
|
|
3
3
|
export declare const ACTION_VARIANT: {
|
|
4
4
|
readonly transfer: 0;
|
|
@@ -39,6 +39,7 @@ export declare const ACTION_VARIANT: {
|
|
|
39
39
|
readonly executeProposal: 32;
|
|
40
40
|
readonly applyAttestor: 41;
|
|
41
41
|
readonly blockAttestor: 42;
|
|
42
|
+
readonly settleDvp: 43;
|
|
42
43
|
};
|
|
43
44
|
/** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
|
|
44
45
|
export declare const TOKEN_ACTION = 39;
|
|
@@ -66,6 +67,13 @@ declare const TOPIC: {
|
|
|
66
67
|
};
|
|
67
68
|
export type AssetClass = keyof typeof CLASS;
|
|
68
69
|
export type ClaimTopic = keyof typeof TOPIC;
|
|
70
|
+
/** `IssuerControls` field order — positional bincode, append only. Fixed for the asset's life once registered. */
|
|
71
|
+
export declare const ISSUER_CONTROLS: readonly ["freeze_asset", "freeze_holder", "lock_holder_amount", "forced_transfer", "recover_holder", "redeem", "split", "set_limits", "set_metadata_uri"];
|
|
72
|
+
export type IssuerControl = (typeof ISSUER_CONTROLS)[number];
|
|
73
|
+
export type IssuerControls = Record<IssuerControl, boolean>;
|
|
74
|
+
/** Every power on, the chain's default (what assets had before controls were selectable). */
|
|
75
|
+
export declare const ALL_ISSUER_CONTROLS: IssuerControls;
|
|
76
|
+
/** `controls` omitted means every power on, matching the node's default. */
|
|
69
77
|
export type AssetMetadata = {
|
|
70
78
|
asset_class: AssetClass;
|
|
71
79
|
decimals: number;
|
|
@@ -75,7 +83,9 @@ export type AssetMetadata = {
|
|
|
75
83
|
metadata_uri: string | null;
|
|
76
84
|
symbol: string;
|
|
77
85
|
name: string;
|
|
86
|
+
controls?: IssuerControls;
|
|
78
87
|
};
|
|
88
|
+
/** @deprecated The flat floor only. The node charges `action_fee + weight × weight_fee`: use `actionFee(await rpc.feeParams(), …)`. */
|
|
79
89
|
export declare const ACTION_FEE = 1000000n;
|
|
80
90
|
export declare function encodeTransfer(to: string, amount: bigint): Uint8Array;
|
|
81
91
|
export declare function encodeJoinValidator(validator: string, stake: bigint, blsPubkey: Uint8Array, blsPop: Uint8Array): Uint8Array;
|
|
@@ -128,6 +138,28 @@ export declare function encodeRenounceMint(tokenRef: string): Uint8Array;
|
|
|
128
138
|
export declare function encodeApplyAttestor(name: string, owners: string[], threshold: number, evidenceHash: string, evidenceUri: string): Uint8Array;
|
|
129
139
|
/** Emergency stop: the sending attestor can no longer grant new attestations. Only a governance vote lifts it. */
|
|
130
140
|
export declare function encodeBlockAttestor(): Uint8Array;
|
|
141
|
+
/**
|
|
142
|
+
* Atomic delivery-versus-payment. The sender (initiator) delivers `giveAmount` of `give` to `counterparty`, who delivers
|
|
143
|
+
* `takeAmount` of `take` back, both legs or neither. `"native"` is IUM; `{ asset }` is an RWA asset or a token ref.
|
|
144
|
+
*/
|
|
145
|
+
export type DvpOrder = {
|
|
146
|
+
give: SpendAsset;
|
|
147
|
+
giveAmount: bigint;
|
|
148
|
+
counterparty: string;
|
|
149
|
+
take: SpendAsset;
|
|
150
|
+
takeAmount: bigint;
|
|
151
|
+
expiresAt: bigint;
|
|
152
|
+
};
|
|
153
|
+
/** `DvpTerms.domain`: no `ActionPayload` can start with it, so a consent never doubles as an ordinary action signature. */
|
|
154
|
+
export declare const DVP_DOMAIN = 4294967295;
|
|
155
|
+
/**
|
|
156
|
+
* What the counterparty signs, mirroring `dvp_consent(…).signing_bytes`: the order, bound to the genesis hash and the
|
|
157
|
+
* 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`.
|
|
159
|
+
*/
|
|
160
|
+
export declare function dvpConsentBytes(genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Uint8Array;
|
|
161
|
+
export declare function signDvpConsent(privateKey: CryptoKey, genesis: Uint8Array, initiator: string, order: DvpOrder, counterpartyNonce: number | bigint): Promise<string>;
|
|
162
|
+
export declare function encodeSettleDvp(order: DvpOrder, counterpartySignature: string): Uint8Array;
|
|
131
163
|
/** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
|
|
132
164
|
export declare const GOVERNANCE_ACTION: {
|
|
133
165
|
readonly addAttestor: 4;
|
package/dist/actions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MULTISIG_TAG, decodeAddress, encodeAddress } from "./bech32.js";
|
|
2
2
|
import { Reader, Writer, asBuffer, fromHex, toHex } from "./bincode.js";
|
|
3
|
-
import { ACCOUNT_POLICY_ACTION, POLICY_VARIANT, encodeAccountPolicy, readAccountPolicy } from "./policy.js";
|
|
3
|
+
import { ACCOUNT_POLICY_ACTION, POLICY_VARIANT, encodeAccountPolicy, readAccountPolicy, readSpendAsset, writeSpendAsset } from "./policy.js";
|
|
4
4
|
/** These indices are ActionPayload's positional bincode discriminants. Never derive them. */
|
|
5
5
|
export const ACTION_VARIANT = {
|
|
6
6
|
transfer: 0, joinValidator: 1, leaveValidator: 2, stake: 3, unstake: 4, registerBlsKey: 6,
|
|
@@ -12,13 +12,18 @@ export const ACTION_VARIANT = {
|
|
|
12
12
|
snapshotHolders: 33, distributeToHolders: 34, redeemHolders: 35, splitAsset: 36,
|
|
13
13
|
verifyClaimProof: 37, setPrivateClaims: 38,
|
|
14
14
|
submitProposal: 30, voteProposal: 31, executeProposal: 32,
|
|
15
|
-
applyAttestor: 41, blockAttestor: 42,
|
|
15
|
+
applyAttestor: 41, blockAttestor: 42, settleDvp: 43,
|
|
16
16
|
};
|
|
17
17
|
/** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
|
|
18
18
|
export const TOKEN_ACTION = 39;
|
|
19
19
|
export const TOKEN_VARIANT = { createToken: 0, mintToken: 1, transferToken: 2, burnToken: 3, renounceMint: 4 };
|
|
20
20
|
const CLASS = { other: 0, real_estate: 1, equity: 2, bond: 3, stablecoin: 4, commodity: 5, token: 6 };
|
|
21
21
|
const TOPIC = { kyc: 0, aml: 1, accredited: 2, jurisdiction: 3 };
|
|
22
|
+
/** `IssuerControls` field order — positional bincode, append only. Fixed for the asset's life once registered. */
|
|
23
|
+
export const ISSUER_CONTROLS = ["freeze_asset", "freeze_holder", "lock_holder_amount", "forced_transfer", "recover_holder", "redeem", "split", "set_limits", "set_metadata_uri"];
|
|
24
|
+
/** Every power on, the chain's default (what assets had before controls were selectable). */
|
|
25
|
+
export const ALL_ISSUER_CONTROLS = Object.fromEntries(ISSUER_CONTROLS.map((c) => [c, true]));
|
|
26
|
+
/** @deprecated The flat floor only. The node charges `action_fee + weight × weight_fee`: use `actionFee(await rpc.feeParams(), …)`. */
|
|
22
27
|
export const ACTION_FEE = 1000000n;
|
|
23
28
|
export function encodeTransfer(to, amount) { return new Writer().varint(ACTION_VARIANT.transfer).string(to).varint(amount).bytes(); }
|
|
24
29
|
export function encodeJoinValidator(validator, stake, blsPubkey, blsPop) { return new Writer().varint(ACTION_VARIANT.joinValidator).string(validator).varint(stake).vec(Array.from(blsPubkey), (w, b) => w.u8(b)).vec(Array.from(blsPop), (w, b) => w.u8(b)).bytes(); }
|
|
@@ -30,7 +35,8 @@ export function encodeAuthorizeOperator(operator) { return new Writer().varint(A
|
|
|
30
35
|
export function encodeRevokeOperator() { return new Writer().varint(ACTION_VARIANT.revokeOperator).bytes(); }
|
|
31
36
|
export function encodeGrantAttestation(subject, hash, topics, jurisdiction) { return new Writer().varint(ACTION_VARIANT.grantAttestation).string(subject).string(hash).vec(topics, (w, topic) => w.varint(TOPIC[topic])).option(jurisdiction, (w, value) => w.string(value)).bytes(); }
|
|
32
37
|
export function encodeRevokeAttestation(subject) { return new Writer().varint(ACTION_VARIANT.revokeAttestation).string(subject).bytes(); }
|
|
33
|
-
export function encodeRegisterAsset(assetId, complianceRequired, metadata) {
|
|
38
|
+
export function encodeRegisterAsset(assetId, complianceRequired, metadata) { const w = new Writer().varint(ACTION_VARIANT.registerAsset).string(assetId).bool(complianceRequired).varint(CLASS[metadata.asset_class]).u8(metadata.decimals).vec(metadata.required_claims, (w, topic) => w.varint(TOPIC[topic])).option(metadata.allowed_jurisdictions, (w, codes) => w.vec(codes, (ww, code) => ww.string(code))).option(metadata.max_supply, (w, cap) => w.varint(BigInt(cap))).option(metadata.metadata_uri, (w, uri) => w.string(uri)).string(metadata.symbol).string(metadata.name); const controls = metadata.controls ?? ALL_ISSUER_CONTROLS; for (const c of ISSUER_CONTROLS)
|
|
39
|
+
w.bool(controls[c]); return w.bytes(); }
|
|
34
40
|
export function encodeIssueAsset(asset, amount) { return new Writer().varint(ACTION_VARIANT.issueAsset).string(asset).varint(amount).bytes(); }
|
|
35
41
|
export function encodeTransferAsset(asset, to, amount) { return new Writer().varint(ACTION_VARIANT.transferAsset).string(asset).string(to).varint(amount).bytes(); }
|
|
36
42
|
export function encodeFreezeAsset(asset, frozen, reason) { return new Writer().varint(frozen ? ACTION_VARIANT.freezeAsset : ACTION_VARIANT.unfreezeAsset).string(asset).string(reason).bytes(); }
|
|
@@ -73,6 +79,30 @@ export function encodeRenounceMint(tokenRef) { return token("renounceMint").stri
|
|
|
73
79
|
export function encodeApplyAttestor(name, owners, threshold, evidenceHash, evidenceUri) { return new Writer().varint(ACTION_VARIANT.applyAttestor).string(name).vec(owners, (w, o) => w.string(o)).u8(threshold).string(evidenceHash).string(evidenceUri).bytes(); }
|
|
74
80
|
/** Emergency stop: the sending attestor can no longer grant new attestations. Only a governance vote lifts it. */
|
|
75
81
|
export function encodeBlockAttestor() { return new Writer().varint(ACTION_VARIANT.blockAttestor).bytes(); }
|
|
82
|
+
/** `DvpTerms.domain`: no `ActionPayload` can start with it, so a consent never doubles as an ordinary action signature. */
|
|
83
|
+
export const DVP_DOMAIN = 0xffff_ffff;
|
|
84
|
+
/**
|
|
85
|
+
* What the counterparty signs, mirroring `dvp_consent(…).signing_bytes`: the order, bound to the genesis hash and the
|
|
86
|
+
* 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`.
|
|
88
|
+
*/
|
|
89
|
+
export function dvpConsentBytes(genesis, initiator, order, counterpartyNonce) {
|
|
90
|
+
const w = new Writer().varint(DVP_DOMAIN).string(initiator);
|
|
91
|
+
writeSpendAsset(w, order.give);
|
|
92
|
+
w.varint(order.giveAmount);
|
|
93
|
+
writeSpendAsset(w, order.take);
|
|
94
|
+
w.varint(order.takeAmount).varint(order.expiresAt);
|
|
95
|
+
return signingBytes(genesis, order.counterparty, counterpartyNonce, w.bytes());
|
|
96
|
+
}
|
|
97
|
+
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
|
+
export function encodeSettleDvp(order, counterpartySignature) {
|
|
99
|
+
const w = new Writer().varint(ACTION_VARIANT.settleDvp);
|
|
100
|
+
writeSpendAsset(w, order.give);
|
|
101
|
+
w.varint(order.giveAmount).string(order.counterparty);
|
|
102
|
+
writeSpendAsset(w, order.take);
|
|
103
|
+
w.varint(order.takeAmount).varint(order.expiresAt).string(counterpartySignature);
|
|
104
|
+
return w.bytes();
|
|
105
|
+
}
|
|
76
106
|
/** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
|
|
77
107
|
export const GOVERNANCE_ACTION = { addAttestor: 4, removeAttestor: 5, unblockAttestor: 6 };
|
|
78
108
|
/** 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. */
|
|
@@ -130,6 +160,7 @@ export function encodePayload({ name, input }) {
|
|
|
130
160
|
case "executeProposal": return encodeExecuteProposal(BigInt(input.proposal));
|
|
131
161
|
case "applyAttestor": return encodeApplyAttestor(input.name, input.owners, input.threshold, input.evidenceHash, input.evidenceUri);
|
|
132
162
|
case "blockAttestor": return encodeBlockAttestor();
|
|
163
|
+
case "settleDvp": return encodeSettleDvp({ give: input.give, giveAmount: BigInt(input.giveAmount), counterparty: input.counterparty, take: input.take, takeAmount: BigInt(input.takeAmount), expiresAt: BigInt(input.expiresAt) }, input.counterpartySignature);
|
|
133
164
|
case "createToken": return encodeCreateToken(input.symbol, input.name, input.decimals, BigInt(input.initialSupply), nullableBig(input.maxSupply), input.mintable);
|
|
134
165
|
case "mintToken": return encodeMintToken(input.token, input.to, BigInt(input.amount));
|
|
135
166
|
case "transferToken": return encodeTransferToken(input.token, input.to, BigInt(input.amount));
|
|
@@ -168,7 +199,7 @@ export function decodePayload(payload) {
|
|
|
168
199
|
revokeOperator: () => ({}),
|
|
169
200
|
grantAttestation: () => ({ subject: str(), hash: str(), topics: topics(), jurisdiction: r.option(str) }),
|
|
170
201
|
revokeAttestation: () => ({ subject: str() }),
|
|
171
|
-
registerAsset: () => ({ assetId: str(), complianceRequired: r.bool(), metadata: { asset_class: known(CLASS_NAME[Number(r.varint())], "asset class"), decimals: r.u8(), required_claims: topics(), allowed_jurisdictions: r.option((rr) => rr.vec(str)), max_supply: r.option(amount), metadata_uri: r.option(str), symbol: str(), name: str() } }),
|
|
202
|
+
registerAsset: () => ({ assetId: str(), complianceRequired: r.bool(), metadata: { asset_class: known(CLASS_NAME[Number(r.varint())], "asset class"), decimals: r.u8(), required_claims: topics(), allowed_jurisdictions: r.option((rr) => rr.vec(str)), max_supply: r.option(amount), metadata_uri: r.option(str), symbol: str(), name: str(), controls: Object.fromEntries(ISSUER_CONTROLS.map((c) => [c, r.bool()])) } }),
|
|
172
203
|
issueAsset: () => ({ asset: str(), amount: amount() }),
|
|
173
204
|
transferAsset: () => ({ asset: str(), to: str(), amount: amount() }),
|
|
174
205
|
freezeAsset: () => ({ asset: str(), frozen: true, reason: str() }),
|
|
@@ -196,6 +227,7 @@ export function decodePayload(payload) {
|
|
|
196
227
|
executeProposal: () => ({ proposal: amount() }),
|
|
197
228
|
applyAttestor: () => ({ name: str(), owners: r.vec((rr) => rr.string()), threshold: r.u8(), evidenceHash: str(), evidenceUri: str() }),
|
|
198
229
|
blockAttestor: () => ({}),
|
|
230
|
+
settleDvp: () => ({ give: readSpendAsset(r), giveAmount: amount(), counterparty: str(), take: readSpendAsset(r), takeAmount: amount(), expiresAt: amount(), counterpartySignature: str() }),
|
|
199
231
|
createToken: () => ({ symbol: str(), name: str(), decimals: r.u8(), initialSupply: amount(), maxSupply: r.option(amount), mintable: r.bool() }),
|
|
200
232
|
mintToken: () => ({ token: str(), to: str(), amount: amount() }),
|
|
201
233
|
transferToken: () => ({ token: str(), to: str(), amount: amount() }),
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,10 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { stdin as input, stderr, stdout } from "node:process";
|
|
5
5
|
import { createPrivateKey, createPublicKey } from "node:crypto";
|
|
6
|
-
import { ArxiumRpc, RpcError, encodeApplyAttestor, encodeBlockAttestor, encodeExecuteProposal, encodeSubmitProposal, encodeVoteProposal, encodeAuthorizeOperator, encodeGrantAttestation, encodeIssueAsset, encodeRegisterAsset, encodeTransferAsset, encodeJoinValidator, encodeLeaveValidator, encodeRegisterBlsKey, encodeRevokeAttestation, encodeRevokeOperator, encodeStake, encodeTransfer, encodeUnstake, generateKey, isKeyFile, signAction, submitBody, unlockKey, verifyKeyFile, verifySignedAction, wrapPkcs8, encodeAddress, toHex, toKeyFile, PKCS8_ED25519_PREFIX, assemblePolicySignature, decodePayload, encodePayload, fromHex, multisigAddress, multisigSignature, signingBytes, asBuffer, } from "./index.js";
|
|
6
|
+
import { ArxiumRpc, RpcError, encodeApplyAttestor, encodeBlockAttestor, encodeExecuteProposal, encodeSubmitProposal, encodeVoteProposal, encodeAuthorizeOperator, encodeGrantAttestation, encodeIssueAsset, encodeRegisterAsset, encodeFreezeAsset, encodeSetHolderFrozen, encodeSetAssetLimits, encodeTransferAsset, encodeJoinValidator, encodeLeaveValidator, encodeRegisterBlsKey, encodeRevokeAttestation, encodeRevokeOperator, encodeStake, encodeTransfer, encodeUnstake, generateKey, isKeyFile, signAction, submitBody, unlockKey, verifyKeyFile, verifySignedAction, wrapPkcs8, encodeAddress, toHex, toKeyFile, PKCS8_ED25519_PREFIX, assemblePolicySignature, decodePayload, encodePayload, fromHex, multisigAddress, multisigSignature, signingBytes, asBuffer, ALL_ISSUER_CONTROLS, ISSUER_CONTROLS, bigintReplacer, encodeBurnAsset, encodeIssuerForcedTransfer, encodeRecoverHolder, encodeIssueAssetTo, encodeLockHolderAmount, encodeLockHolderAmountUntil, encodeLockIssuance, encodeTransferIssuer, encodeSetAssetMetadataUri, encodeSetPrivateClaims, encodeSnapshotHolders, encodeDistributeToHolders, encodeRedeemHolders, encodeSplitAsset, encodeCreateToken, encodeMintToken, encodeTransferToken, encodeBurnToken, encodeRenounceMint, encodeSettleDvp, signDvpConsent, } from "./index.js";
|
|
7
7
|
class UsageError extends Error {
|
|
8
8
|
}
|
|
9
|
-
function output(value, json) { stdout.write(json
|
|
9
|
+
function output(value, json) { stdout.write(!json && typeof value === "string" ? `${value}\n` : `${JSON.stringify(value, bigintReplacer, 2)}\n`); }
|
|
10
10
|
function parseGlobals(args) { let rpc = process.env.ARX_RPC ?? "http://127.0.0.1:8080", token = process.env.ARX_TOKEN, key = process.env.ARX_KEY, json = false, nonce; const rest = []; for (let i = 0; i < args.length; i++) {
|
|
11
11
|
const arg = args[i];
|
|
12
12
|
if (arg === "--rpc")
|
|
@@ -95,13 +95,80 @@ function payload(action, args) { const topics = (list) => list ? list.split(",")
|
|
|
95
95
|
case "register-asset": {
|
|
96
96
|
const [assetId, symbol, name, decimals] = args;
|
|
97
97
|
if (!assetId || !symbol || !name || !/^\d+$/.test(decimals ?? ""))
|
|
98
|
-
throwUsage("register-asset needs <asset-id> <symbol> <name> <decimals> [topics] [jurisdictions]");
|
|
99
|
-
return encodeRegisterAsset(assetId, false, { asset_class: "other", decimals: Number(decimals), required_claims: topics(args[4]), allowed_jurisdictions: args[5] ? args[5].split(",") : null, max_supply: null, metadata_uri: null, symbol, name });
|
|
98
|
+
throwUsage("register-asset needs <asset-id> <symbol> <name> <decimals> [topics|-] [jurisdictions|-] [controls: all|none|freeze_asset,forced_transfer,...]");
|
|
99
|
+
return encodeRegisterAsset(assetId, false, { asset_class: "other", decimals: Number(decimals), required_claims: args[4] === "-" ? [] : topics(args[4]), allowed_jurisdictions: args[5] && args[5] !== "-" ? args[5].split(",") : null, max_supply: null, metadata_uri: null, symbol, name, controls: controlsArg(args[6]) });
|
|
100
100
|
}
|
|
101
101
|
case "issue-asset": return encodeIssueAsset(args[0] ?? throwUsage("issue-asset needs <asset-ref> <amount>"), amount(1));
|
|
102
102
|
case "transfer-asset": return encodeTransferAsset(args[0] ?? throwUsage("transfer-asset needs <asset-ref> <to> <amount>"), args[1] ?? throwUsage("transfer-asset needs <asset-ref> <to> <amount>"), amount(2));
|
|
103
|
-
|
|
103
|
+
case "set-asset-limits": {
|
|
104
|
+
const asset = args[0] ?? throwUsage("set-asset-limits needs <asset-ref> <max-holders|-> [max-balance-per-holder|-] [max-attestation-age-blocks|-]");
|
|
105
|
+
const opt = (index) => { const value = args[index]; if (value === undefined || value === "-")
|
|
106
|
+
return null; if (!/^\d+$/.test(value))
|
|
107
|
+
throwUsage("limits must be integers, or - for no limit"); return BigInt(value); };
|
|
108
|
+
const holders = opt(1);
|
|
109
|
+
return encodeSetAssetLimits(asset, holders === null ? null : Number(holders), opt(2), opt(3));
|
|
110
|
+
}
|
|
111
|
+
case "freeze-asset":
|
|
112
|
+
case "unfreeze-asset": return encodeFreezeAsset(args[0] ?? throwUsage(`${action} needs <asset-ref> <reason>`), action === "freeze-asset", args[1] ?? throwUsage(`${action} needs <asset-ref> <reason>`));
|
|
113
|
+
case "freeze-holder":
|
|
114
|
+
case "unfreeze-holder": return encodeSetHolderFrozen(args[0] ?? throwUsage(`${action} needs <asset-ref> <holder>`), args[1] ?? throwUsage(`${action} needs <asset-ref> <holder>`), action === "freeze-holder");
|
|
115
|
+
default: return morePayload(action, args);
|
|
104
116
|
} }
|
|
117
|
+
/** Issuer, corporate and token actions. `<…>` arguments are required; amounts and heights are raw integers. */
|
|
118
|
+
const USAGE = {
|
|
119
|
+
"burn-asset": "<asset-ref> <amount>", "forced-transfer": "<asset-ref> <from> <to> <amount> <reason>", "recover-holder": "<asset-ref> <lost> <replacement>",
|
|
120
|
+
"issue-asset-to": "<asset-ref> <to> <amount>", "lock-holder-amount": "<asset-ref> <holder> <amount> [until-height]", "unlock-holder-amount": "<asset-ref> <holder> <amount>",
|
|
121
|
+
"lock-issuance": "<asset-ref>", "transfer-issuer": "<asset-ref> <new-issuer>", "set-metadata-uri": "<asset-ref> <uri|->", "set-private-claims": "<asset-ref> on|off",
|
|
122
|
+
"snapshot-holders": "<asset-ref>", "distribute": "<asset-ref> <snapshot-height> <payout-asset-ref> <total>", "redeem-holders": "<asset-ref> <snapshot-height>",
|
|
123
|
+
"split-asset": "<asset-ref> <snapshot-height> <numerator> <denominator>",
|
|
124
|
+
"create-token": "<symbol> <name> <decimals> <initial-supply> <max-supply|-> mintable|fixed", "mint-token": "<token-ref> <to> <amount>",
|
|
125
|
+
"transfer-token": "<token-ref> <to> <amount>", "burn-token": "<token-ref> <amount>", "renounce-mint": "<token-ref>",
|
|
126
|
+
"settle-dvp": "<give-asset-ref|native> <give-amount> <counterparty> <take-asset-ref|native> <take-amount> <expires-at-height> <counterparty-signature>",
|
|
127
|
+
};
|
|
128
|
+
const spendArg = (value) => value === "native" ? "native" : { asset: value };
|
|
129
|
+
function morePayload(action, args) {
|
|
130
|
+
const usage = USAGE[action] ?? throwUsage(`unsupported action: ${action}`);
|
|
131
|
+
const arg = (i) => args[i] ?? throwUsage(`${action} needs ${usage}`);
|
|
132
|
+
const int = (i) => { const value = arg(i); if (!/^\d+$/.test(value))
|
|
133
|
+
throwUsage(`${action} needs ${usage} (integers in base units)`); return BigInt(value); };
|
|
134
|
+
switch (action) {
|
|
135
|
+
case "burn-asset": return encodeBurnAsset(arg(0), int(1));
|
|
136
|
+
case "forced-transfer": return encodeIssuerForcedTransfer(arg(0), arg(1), arg(2), int(3), arg(4));
|
|
137
|
+
case "recover-holder": return encodeRecoverHolder(arg(0), arg(1), arg(2));
|
|
138
|
+
case "issue-asset-to": return encodeIssueAssetTo(arg(0), arg(1), int(2));
|
|
139
|
+
case "lock-holder-amount": return args[3] === undefined ? encodeLockHolderAmount(arg(0), arg(1), int(2), true) : encodeLockHolderAmountUntil(arg(0), arg(1), int(2), int(3));
|
|
140
|
+
case "unlock-holder-amount": return encodeLockHolderAmount(arg(0), arg(1), int(2), false);
|
|
141
|
+
case "lock-issuance": return encodeLockIssuance(arg(0));
|
|
142
|
+
case "transfer-issuer": return encodeTransferIssuer(arg(0), arg(1));
|
|
143
|
+
case "set-metadata-uri": return encodeSetAssetMetadataUri(arg(0), arg(1) === "-" ? null : arg(1));
|
|
144
|
+
case "set-private-claims": {
|
|
145
|
+
const on = arg(1);
|
|
146
|
+
if (on !== "on" && on !== "off")
|
|
147
|
+
throwUsage(`${action} needs ${usage}`);
|
|
148
|
+
return encodeSetPrivateClaims(arg(0), on === "on");
|
|
149
|
+
}
|
|
150
|
+
case "snapshot-holders": return encodeSnapshotHolders(arg(0));
|
|
151
|
+
case "distribute": return encodeDistributeToHolders(arg(0), int(1), arg(2), int(3));
|
|
152
|
+
case "redeem-holders": return encodeRedeemHolders(arg(0), int(1));
|
|
153
|
+
case "split-asset": return encodeSplitAsset(arg(0), int(1), int(2), int(3));
|
|
154
|
+
case "create-token": {
|
|
155
|
+
const kind = arg(5);
|
|
156
|
+
if (kind !== "mintable" && kind !== "fixed")
|
|
157
|
+
throwUsage(`${action} needs ${usage}`);
|
|
158
|
+
return encodeCreateToken(arg(0), arg(1), Number(int(2)), int(3), arg(4) === "-" ? null : int(4), kind === "mintable");
|
|
159
|
+
}
|
|
160
|
+
case "mint-token": return encodeMintToken(arg(0), arg(1), int(2));
|
|
161
|
+
case "transfer-token": return encodeTransferToken(arg(0), arg(1), int(2));
|
|
162
|
+
case "burn-token": return encodeBurnToken(arg(0), int(1));
|
|
163
|
+
case "settle-dvp": return encodeSettleDvp({ give: spendArg(arg(0)), giveAmount: int(1), counterparty: arg(2), take: spendArg(arg(3)), takeAmount: int(4), expiresAt: int(5) }, arg(6));
|
|
164
|
+
default: return encodeRenounceMint(arg(0));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/** Enabled issuer controls, fixed for the asset's life. Omitted or "all" keeps every power (the node's default). */
|
|
168
|
+
function controlsArg(value) { if (!value || value === "all")
|
|
169
|
+
return ALL_ISSUER_CONTROLS; const on = value === "none" ? [] : value.split(","); for (const c of on)
|
|
170
|
+
if (!ISSUER_CONTROLS.includes(c))
|
|
171
|
+
throwUsage(`unknown control: ${c} (${ISSUER_CONTROLS.join(", ")})`); return Object.fromEntries(ISSUER_CONTROLS.map((c) => [c, on.includes(c)])); }
|
|
105
172
|
function hexArg(value) { if (!value || !/^[0-9a-f]*$/i.test(value) || value.length % 2)
|
|
106
173
|
throwUsage("expected even-length hex"); return Uint8Array.from(value.match(/../g)?.map((byte) => Number.parseInt(byte, 16)) ?? []); }
|
|
107
174
|
async function main() {
|
|
@@ -113,7 +180,7 @@ async function main() {
|
|
|
113
180
|
return;
|
|
114
181
|
}
|
|
115
182
|
if (!command)
|
|
116
|
-
throwUsage("use: arx keys|sign|submit|send|query|verify");
|
|
183
|
+
throwUsage("use: arx keys|sign|submit|send|dvp-consent|query|verify");
|
|
117
184
|
if (command === "keys") {
|
|
118
185
|
const [operation, ...keyArgs] = rest;
|
|
119
186
|
if (operation === "new") {
|
|
@@ -220,6 +287,19 @@ async function main() {
|
|
|
220
287
|
}
|
|
221
288
|
throwUsage("use: arx multisig address|build|sign|combine|link");
|
|
222
289
|
}
|
|
290
|
+
// The counterparty's half of a DvP: sign the order at its current nonce. The initiator sends it with `arx send settle-dvp`.
|
|
291
|
+
if (command === "dvp-consent") {
|
|
292
|
+
const use = "dvp-consent needs <initiator> <give-asset-ref|native> <give-amount> <take-asset-ref|native> <take-amount> <expires-at-height>", [initiator, give, giveAmount, take, takeAmount, expiresAt] = rest;
|
|
293
|
+
if (!initiator || !give || !take || ![giveAmount, takeAmount, expiresAt].every((v) => /^\d+$/.test(v ?? "")))
|
|
294
|
+
throwUsage(use);
|
|
295
|
+
const { file, privateKey } = await loadKey(globals.key);
|
|
296
|
+
const nonce = globals.nonce ?? await rpc.account(file.address).then((account) => account.nonce, (error) => { if (error instanceof RpcError && error.status === 404)
|
|
297
|
+
return 0; throw error; });
|
|
298
|
+
const order = { give: spendArg(give), giveAmount: BigInt(giveAmount), counterparty: file.address, take: spendArg(take), takeAmount: BigInt(takeAmount), expiresAt: BigInt(expiresAt) };
|
|
299
|
+
const signature = await signDvpConsent(privateKey, await rpc.genesisHash(), initiator, order, nonce);
|
|
300
|
+
output({ counterparty: file.address, counterpartyNonce: nonce, signature, send: ["settle-dvp", give, giveAmount, file.address, take, takeAmount, expiresAt, signature] }, true);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
223
303
|
if (command === "sign" || command === "send") {
|
|
224
304
|
const action = rest[0] ?? throwUsage(`${command} needs an action`), { file, privateKey } = await loadKey(globals.key), encoded = payload(action, rest.slice(1));
|
|
225
305
|
if (command === "sign") {
|
|
@@ -255,7 +335,17 @@ async function main() {
|
|
|
255
335
|
}
|
|
256
336
|
if (command === "query") {
|
|
257
337
|
const [operation, value] = rest;
|
|
258
|
-
const result = operation === "status" ? await rpc.status() : operation === "account" ? await rpc.account(value ?? throwUsage("account needs an address")) : operation === "block" ? (/^\d+$/.test(value ?? "") ? await rpc.block(Number(value)) : await rpc.blockByHash(value ?? throwUsage("block needs a height or hash"))) : operation === "action" ? await rpc.action(value ?? throwUsage("action needs a signature")) : operation === "validators" ? await rpc.validators() : operation === "finality" ? await rpc.finality() : operation === "search" ? await rpc.search(value ?? throwUsage("search needs a query"))
|
|
338
|
+
const result = operation === "status" ? await rpc.status() : operation === "account" ? await rpc.account(value ?? throwUsage("account needs an address")) : operation === "block" ? (/^\d+$/.test(value ?? "") ? await rpc.block(Number(value)) : await rpc.blockByHash(value ?? throwUsage("block needs a height or hash"))) : operation === "action" ? await rpc.action(value ?? throwUsage("action needs a signature")) : operation === "validators" ? await rpc.validators() : operation === "finality" ? await rpc.finality() : operation === "search" ? await rpc.search(value ?? throwUsage("search needs a query"))
|
|
339
|
+
: operation === "assets" ? (value ? await rpc.assets(value) : await rpc.listAssets())
|
|
340
|
+
: operation === "asset" ? await rpc.asset(value ?? throwUsage("asset needs an asset ref"))
|
|
341
|
+
: operation === "holders" ? await rpc.assetHolders(value ?? throwUsage("holders needs an asset ref"))
|
|
342
|
+
: operation === "balance" ? await rpc.assetBalance(value ?? throwUsage("balance needs <address> <asset-ref>"), rest[2] ?? throwUsage("balance needs <address> <asset-ref>"))
|
|
343
|
+
: operation === "attestors" ? await rpc.attestors()
|
|
344
|
+
: operation === "attestor" ? await rpc.attestor(value ?? throwUsage("attestor needs an address"))
|
|
345
|
+
: operation === "validator" ? await rpc.validator(value ?? throwUsage("validator needs an address"))
|
|
346
|
+
: operation === "stakes" ? await rpc.stakes(value ?? throwUsage("stakes needs an address"))
|
|
347
|
+
: operation === "fee" ? await rpc.actionFee()
|
|
348
|
+
: throwUsage("use: arx query status|account|block|action|validators|validator|finality|search|assets [address]|asset|holders|balance|attestors|attestor|stakes|fee");
|
|
259
349
|
output(result, globals.json);
|
|
260
350
|
return;
|
|
261
351
|
}
|
package/dist/fees.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** `action_fee` and `weight_fee` from `GET /action-fee` (`rpc.feeParams()`). */
|
|
2
|
+
export type FeeParams = {
|
|
3
|
+
actionFee: bigint;
|
|
4
|
+
weightFee: bigint;
|
|
5
|
+
};
|
|
6
|
+
/** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
|
|
7
|
+
export declare const POLICY_AUTHORIZATION_WEIGHT = 5000;
|
|
8
|
+
/** A single-key ed25519 signature: 64 bytes as hex. */
|
|
9
|
+
export declare const SINGLE_SIGNATURE_LENGTH = 128;
|
|
10
|
+
/** Hex length of a `threshold`-of-`members` multisig witness (`multisigSignature`), for estimating a fee before anyone signs. */
|
|
11
|
+
export declare const multisigSignatureLength: (threshold: number, members: number) => number;
|
|
12
|
+
/**
|
|
13
|
+
* Weight that the node charges for an action. Pass the real `signature` once it is signed. Before signing, pass
|
|
14
|
+
* `signatureLength` instead (128 for one key, longer for a multisig witness), and set `policy` when a stateful account policy will sign.
|
|
15
|
+
*/
|
|
16
|
+
export declare function actionWeight(sender: string, nonce: number | bigint, payload: Uint8Array, options?: {
|
|
17
|
+
signature?: string;
|
|
18
|
+
signatureLength?: number;
|
|
19
|
+
policy?: boolean;
|
|
20
|
+
}): number;
|
|
21
|
+
/** The fee the node charges, in IUM. `CreateToken` also pays `token_create_fee` on top (see `rpc.feeParams()`). */
|
|
22
|
+
export declare function actionFee(params: FeeParams, sender: string, nonce: number | bigint, payload: Uint8Array, options?: {
|
|
23
|
+
signature?: string;
|
|
24
|
+
signatureLength?: number;
|
|
25
|
+
policy?: boolean;
|
|
26
|
+
}): bigint;
|
package/dist/fees.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { ACTION_VARIANT, TOKEN_ACTION, TOKEN_VARIANT } from "./actions.js";
|
|
2
|
+
import { Reader } from "./bincode.js";
|
|
3
|
+
import { ACCOUNT_POLICY_ACTION } from "./policy.js";
|
|
4
|
+
/** `base_weight` in metering.rs, by `ActionPayload` discriminant. Token and account-policy actions are handled below. */
|
|
5
|
+
const BASE_WEIGHT = {
|
|
6
|
+
[ACTION_VARIANT.transfer]: 50,
|
|
7
|
+
[ACTION_VARIANT.stake]: 100, [ACTION_VARIANT.unstake]: 100,
|
|
8
|
+
[ACTION_VARIANT.joinValidator]: 1_500, [ACTION_VARIANT.registerBlsKey]: 1_500,
|
|
9
|
+
[ACTION_VARIANT.leaveValidator]: 150,
|
|
10
|
+
[ACTION_VARIANT.authorizeOperator]: 100, [ACTION_VARIANT.revokeOperator]: 100,
|
|
11
|
+
7 /* VerifyIdentityCredential */: 5_000, [ACTION_VARIANT.verifyClaimProof]: 5_000,
|
|
12
|
+
[ACTION_VARIANT.grantAttestation]: 100, [ACTION_VARIANT.revokeAttestation]: 100,
|
|
13
|
+
[ACTION_VARIANT.applyAttestor]: 150, [ACTION_VARIANT.blockAttestor]: 150,
|
|
14
|
+
[ACTION_VARIANT.registerAsset]: 150,
|
|
15
|
+
[ACTION_VARIANT.issueAsset]: 150, [ACTION_VARIANT.issueAssetTo]: 150, [ACTION_VARIANT.transferAsset]: 150,
|
|
16
|
+
[ACTION_VARIANT.issuerForcedTransfer]: 150, [ACTION_VARIANT.burnAsset]: 150, [ACTION_VARIANT.recoverHolder]: 150,
|
|
17
|
+
[ACTION_VARIANT.freezeAsset]: 100, [ACTION_VARIANT.unfreezeAsset]: 100, [ACTION_VARIANT.setHolderFrozen]: 100,
|
|
18
|
+
[ACTION_VARIANT.lockHolderAmount]: 100, [ACTION_VARIANT.lockHolderAmountUntil]: 100, [ACTION_VARIANT.unlockHolderAmount]: 100,
|
|
19
|
+
[ACTION_VARIANT.setAssetLimits]: 100, [ACTION_VARIANT.setPrivateClaims]: 100, [ACTION_VARIANT.lockIssuance]: 100,
|
|
20
|
+
[ACTION_VARIANT.transferIssuer]: 100, [ACTION_VARIANT.setAssetMetadataUri]: 100,
|
|
21
|
+
[ACTION_VARIANT.snapshotHolders]: 2_000, [ACTION_VARIANT.distributeToHolders]: 15_000,
|
|
22
|
+
[ACTION_VARIANT.redeemHolders]: 10_000, [ACTION_VARIANT.splitAsset]: 10_000,
|
|
23
|
+
[ACTION_VARIANT.settleDvp]: 350,
|
|
24
|
+
[ACTION_VARIANT.submitProposal]: 150, [ACTION_VARIANT.voteProposal]: 150, [ACTION_VARIANT.executeProposal]: 150,
|
|
25
|
+
5 /* SubmitEquivocationEvidence */: 2_000,
|
|
26
|
+
15 /* SubmitExecutionFault */: 500_000,
|
|
27
|
+
};
|
|
28
|
+
/** `POLICY_AUTHORIZATION_WEIGHT`: added when a stateful account policy authorizes the action. */
|
|
29
|
+
export const POLICY_AUTHORIZATION_WEIGHT = 5_000;
|
|
30
|
+
/** A single-key ed25519 signature: 64 bytes as hex. */
|
|
31
|
+
export const SINGLE_SIGNATURE_LENGTH = 128;
|
|
32
|
+
/** Hex length of a `threshold`-of-`members` multisig witness (`multisigSignature`), for estimating a fee before anyone signs. */
|
|
33
|
+
export const multisigSignatureLength = (threshold, members) => 2 * (2 + 32 * members + 65 * threshold);
|
|
34
|
+
function baseWeight(payload) {
|
|
35
|
+
const r = new Reader(payload), variant = Number(r.varint());
|
|
36
|
+
if (variant === TOKEN_ACTION)
|
|
37
|
+
return Number(r.varint()) === TOKEN_VARIANT.createToken ? 150 : 100;
|
|
38
|
+
if (variant === ACCOUNT_POLICY_ACTION)
|
|
39
|
+
return 1_000;
|
|
40
|
+
const weight = BASE_WEIGHT[variant];
|
|
41
|
+
if (weight === undefined)
|
|
42
|
+
throw new Error(`unknown action variant: ${variant}`);
|
|
43
|
+
return weight;
|
|
44
|
+
}
|
|
45
|
+
/** bincode `standard()` varint length. */
|
|
46
|
+
const varintLength = (n) => (n <= 250 ? 1 : n <= 0xffff ? 3 : n <= 0xffff_ffff ? 5 : 9);
|
|
47
|
+
const isPolicyWitness = (signature) => signature.length > 128 && /^a7(00|01|02)/i.test(signature);
|
|
48
|
+
/**
|
|
49
|
+
* Weight that the node charges for an action. Pass the real `signature` once it is signed. Before signing, pass
|
|
50
|
+
* `signatureLength` instead (128 for one key, longer for a multisig witness), and set `policy` when a stateful account policy will sign.
|
|
51
|
+
*/
|
|
52
|
+
export function actionWeight(sender, nonce, payload, options = {}) {
|
|
53
|
+
const sigLength = options.signature?.length ?? options.signatureLength ?? SINGLE_SIGNATURE_LENGTH;
|
|
54
|
+
const policy = options.signature !== undefined ? isPolicyWitness(options.signature) : !!options.policy;
|
|
55
|
+
const senderLength = new TextEncoder().encode(sender).length;
|
|
56
|
+
// `ActionWire`: sender string, nonce, `Some(signature)`, payload as a length-prefixed byte string.
|
|
57
|
+
const size = varintLength(senderLength) + senderLength + varintLength(Number(nonce)) + 1 + varintLength(sigLength) + sigLength + varintLength(payload.length) + payload.length;
|
|
58
|
+
return baseWeight(payload) + size + (policy ? POLICY_AUTHORIZATION_WEIGHT : 0);
|
|
59
|
+
}
|
|
60
|
+
/** The fee the node charges, in IUM. `CreateToken` also pays `token_create_fee` on top (see `rpc.feeParams()`). */
|
|
61
|
+
export function actionFee(params, sender, nonce, payload, options = {}) {
|
|
62
|
+
return params.actionFee + BigInt(actionWeight(sender, nonce, payload, options)) * params.weightFee;
|
|
63
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/policy.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Reader } from "./bincode.js";
|
|
1
|
+
import { Reader, Writer } from "./bincode.js";
|
|
2
2
|
export declare const ACCOUNT_POLICY_ACTION = 40;
|
|
3
3
|
export declare const POLICY_VARIANT: {
|
|
4
4
|
readonly setAccountPolicy: 0;
|
|
@@ -39,7 +39,11 @@ export type AccountPolicy = {
|
|
|
39
39
|
} | null;
|
|
40
40
|
};
|
|
41
41
|
export type PolicyMode = "owner" | "session" | "guardian";
|
|
42
|
+
declare function asset(w: Writer, a: SpendAsset): void;
|
|
43
|
+
export { asset as writeSpendAsset };
|
|
42
44
|
export declare function encodeAccountPolicy(name: keyof typeof POLICY_VARIANT, input: Record<string, any>): Uint8Array;
|
|
45
|
+
declare function readAsset(r: Reader): SpendAsset;
|
|
46
|
+
export { readAsset as readSpendAsset };
|
|
43
47
|
export declare function readAccountPolicy(r: Reader): {
|
|
44
48
|
name: keyof typeof POLICY_VARIANT;
|
|
45
49
|
input: Record<string, any>;
|
package/dist/policy.js
CHANGED
|
@@ -16,6 +16,7 @@ function asset(w, a) { if (a === "native")
|
|
|
16
16
|
w.varint(0);
|
|
17
17
|
else
|
|
18
18
|
w.varint(1).string(a.asset); }
|
|
19
|
+
export { asset as writeSpendAsset };
|
|
19
20
|
function recipients(w, list) { if (list.length > 64 || list.some((a, i) => i > 0 && list[i - 1] >= a))
|
|
20
21
|
throw new Error("recipients must be bounded, unique and sorted"); w.vec(list, (ww, a) => ww.string(a)); }
|
|
21
22
|
function session(w, s) { w.raw(key(s.publicKey)).varint(bounded(s.expiresAt, 64)); asset(w, s.asset); w.varint(bounded(s.allowance, 128)); recipients(w, s.recipients); }
|
|
@@ -56,6 +57,7 @@ function readThreshold(r) { return { threshold: r.u8(), members: r.vec(readKey)
|
|
|
56
57
|
function readAsset(r) { const tag = r.varint(); if (tag === 0n)
|
|
57
58
|
return "native"; if (tag === 1n)
|
|
58
59
|
return { asset: r.string() }; throw new Error("unknown spend asset"); }
|
|
60
|
+
export { readAsset as readSpendAsset };
|
|
59
61
|
function readSession(r) { return { publicKey: readKey(r), expiresAt: r.varint().toString(), asset: readAsset(r), allowance: r.varint().toString(), recipients: r.vec(rr => rr.string()) }; }
|
|
60
62
|
export function readAccountPolicy(r) {
|
|
61
63
|
const variant = Number(r.varint()), name = Object.keys(POLICY_VARIANT).find(n => POLICY_VARIANT[n] === variant);
|
package/dist/rpc.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type SignedAction } from "./actions.js";
|
|
1
|
+
import { type AssetClass, type IssuerControls, type SignedAction } from "./actions.js";
|
|
2
|
+
import type { FeeParams } from "./fees.js";
|
|
2
3
|
import { type AccountPolicy } from "./policy.js";
|
|
3
4
|
export type RpcOptions = {
|
|
4
5
|
rpc: string;
|
|
@@ -19,10 +20,15 @@ export type ActionStatus = {
|
|
|
19
20
|
} | {
|
|
20
21
|
status: "unknown";
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* `privateKey` signs as a single-key sender. For a multisig or programmable-account sender, pass `sign` instead: it gets the
|
|
25
|
+
* exact signing bytes and the nonce, and returns the action's `signature` field (e.g. from `multisigSignature`).
|
|
26
|
+
*/
|
|
22
27
|
export type SendActionOptions = {
|
|
23
|
-
privateKey: CryptoKey;
|
|
24
28
|
sender: string;
|
|
25
29
|
payload: Uint8Array;
|
|
30
|
+
privateKey?: CryptoKey;
|
|
31
|
+
sign?: (message: Uint8Array, nonce: number) => Promise<string>;
|
|
26
32
|
pollIntervalMs?: number;
|
|
27
33
|
timeoutMs?: number;
|
|
28
34
|
finalized?: boolean;
|
|
@@ -31,34 +37,211 @@ export declare class RpcError extends Error {
|
|
|
31
37
|
readonly status: number;
|
|
32
38
|
constructor(status: number, message: string);
|
|
33
39
|
}
|
|
40
|
+
/** The chain refused the action. `reason` is the node's exact text, e.g. `compliance check failed: arx1… is not KYC'd/allowlisted`. */
|
|
41
|
+
export declare class ActionDroppedError extends Error {
|
|
42
|
+
readonly reason: string;
|
|
43
|
+
readonly action: SignedAction;
|
|
44
|
+
constructor(reason: string, action: SignedAction);
|
|
45
|
+
}
|
|
46
|
+
export type ClaimTopicName = "Kyc" | "Aml" | "Accredited" | "Jurisdiction";
|
|
47
|
+
export type NodeStatus = {
|
|
48
|
+
chain_name: string;
|
|
49
|
+
version: string;
|
|
50
|
+
genesis_hash: string;
|
|
51
|
+
tip_height: number;
|
|
52
|
+
tip_hash: string;
|
|
53
|
+
finalized_height: number | null;
|
|
54
|
+
final_watermark: number;
|
|
55
|
+
settled_height: number;
|
|
56
|
+
block_interval_secs: number | null;
|
|
57
|
+
storage_schema: number;
|
|
58
|
+
min_migratable_schema: number;
|
|
59
|
+
open_dispute_height: number | null;
|
|
60
|
+
open_dispute_header: string | null;
|
|
61
|
+
open_dispute_cause: string | null;
|
|
62
|
+
};
|
|
63
|
+
export type Account = {
|
|
64
|
+
balance: bigint;
|
|
65
|
+
nonce: number;
|
|
66
|
+
identity_hash: string | null;
|
|
67
|
+
zk_identity_verified: boolean;
|
|
68
|
+
attested_by: string | null;
|
|
69
|
+
claims: ClaimTopicName[];
|
|
70
|
+
jurisdiction: string | null;
|
|
71
|
+
attested_at: number | null;
|
|
72
|
+
programmable?: unknown;
|
|
73
|
+
};
|
|
74
|
+
export type StakeAllocation = {
|
|
75
|
+
master: string;
|
|
76
|
+
validator: string;
|
|
77
|
+
active_amount: bigint;
|
|
78
|
+
unbonding: {
|
|
79
|
+
amount: bigint;
|
|
80
|
+
unlock_at_height: number;
|
|
81
|
+
} | null;
|
|
82
|
+
created_at: number;
|
|
83
|
+
updated_at: number;
|
|
84
|
+
};
|
|
85
|
+
/** One asset in `rpc.assets(address)`. `transfer_eligible`/`eligibility_reason` describe the sending side. */
|
|
86
|
+
export type AccountAssetBalance = {
|
|
87
|
+
ref: string;
|
|
88
|
+
asset_id: string;
|
|
89
|
+
symbol: string;
|
|
90
|
+
name: string;
|
|
91
|
+
issuer: string;
|
|
92
|
+
issuer_attested: boolean;
|
|
93
|
+
compliance_required: boolean;
|
|
94
|
+
frozen: boolean;
|
|
95
|
+
holder_frozen: boolean;
|
|
96
|
+
frozen_amount: bigint;
|
|
97
|
+
transfer_eligible: boolean;
|
|
98
|
+
eligibility_reason: string;
|
|
99
|
+
decimals: number;
|
|
100
|
+
balance: bigint;
|
|
101
|
+
};
|
|
102
|
+
export type Asset = {
|
|
103
|
+
ref: string;
|
|
104
|
+
asset_ref: string;
|
|
105
|
+
asset_id: string;
|
|
106
|
+
issuer: string;
|
|
107
|
+
issuer_attested: boolean;
|
|
108
|
+
holders: number;
|
|
109
|
+
compliance_required: boolean;
|
|
110
|
+
asset_class: string;
|
|
111
|
+
decimals: number;
|
|
112
|
+
required_claims: ClaimTopicName[];
|
|
113
|
+
allowed_jurisdictions: string[] | null;
|
|
114
|
+
max_supply: bigint | null;
|
|
115
|
+
total_supply: bigint;
|
|
116
|
+
frozen: boolean;
|
|
117
|
+
metadata_uri: string | null;
|
|
118
|
+
registered_at: number;
|
|
119
|
+
symbol: string;
|
|
120
|
+
name: string;
|
|
121
|
+
issuance_locked: boolean;
|
|
122
|
+
max_holders: number | null;
|
|
123
|
+
max_balance_per_holder: bigint | null;
|
|
124
|
+
max_attestation_age: number | null;
|
|
125
|
+
holder_count: number;
|
|
126
|
+
snapshot: {
|
|
127
|
+
height: number;
|
|
128
|
+
total: bigint;
|
|
129
|
+
holders: [string, number | bigint][];
|
|
130
|
+
} | null;
|
|
131
|
+
private_claims: boolean;
|
|
132
|
+
controls: IssuerControls;
|
|
133
|
+
};
|
|
134
|
+
export type AssetHolder = {
|
|
135
|
+
address: string;
|
|
136
|
+
balance: bigint;
|
|
137
|
+
frozen: boolean;
|
|
138
|
+
frozen_amount: bigint;
|
|
139
|
+
attested_at: number | null;
|
|
140
|
+
};
|
|
141
|
+
export type Attestor = {
|
|
142
|
+
attestor: string;
|
|
143
|
+
name: string;
|
|
144
|
+
registered_at: number;
|
|
145
|
+
owners: string[];
|
|
146
|
+
threshold: number;
|
|
147
|
+
blocked: boolean;
|
|
148
|
+
self_blocked: boolean;
|
|
149
|
+
removal_pending: boolean;
|
|
150
|
+
};
|
|
151
|
+
export type ValidatorStatus = "Active" | "Pending" | "Tombstoned" | {
|
|
152
|
+
Jailed: {
|
|
153
|
+
until_epoch: number;
|
|
154
|
+
};
|
|
155
|
+
} | {
|
|
156
|
+
Leaving: {
|
|
157
|
+
from_epoch: number;
|
|
158
|
+
};
|
|
159
|
+
};
|
|
160
|
+
export type Validator = {
|
|
161
|
+
address: string;
|
|
162
|
+
status: ValidatorStatus;
|
|
163
|
+
voting_power: number;
|
|
164
|
+
bls_registered: boolean;
|
|
165
|
+
operator: string | null;
|
|
166
|
+
};
|
|
167
|
+
export type ActionFeeInfo = {
|
|
168
|
+
action_fee: bigint;
|
|
169
|
+
weight_fee: bigint;
|
|
170
|
+
max_block_weight: number;
|
|
171
|
+
token_create_fee: bigint;
|
|
172
|
+
};
|
|
173
|
+
export type { AssetClass };
|
|
174
|
+
/**
|
|
175
|
+
* `JSON.parse` that never rounds an integer: the node writes u128 amounts as bare JSON numbers, and anything above 2^53
|
|
176
|
+
* (about 9M ARX in IUM) loses digits in a plain parse. Amount keys become `bigint`; other integers stay `number` unless unsafe.
|
|
177
|
+
*/
|
|
178
|
+
export declare function parseJson(text: string): unknown;
|
|
179
|
+
/** `JSON.stringify` replacer for `bigint` values (as decimal strings). */
|
|
180
|
+
export declare const bigintReplacer: (_key: string, value: unknown) => unknown;
|
|
34
181
|
export declare class ArxiumRpc {
|
|
35
182
|
readonly rpc: string;
|
|
36
183
|
private readonly token?;
|
|
37
184
|
private readonly requestFetch;
|
|
185
|
+
/** One in-flight `sendAction` per sender: the node's account nonce doesn't count the mempool, so two concurrent sends would sign the same nonce. */
|
|
186
|
+
private readonly senders;
|
|
38
187
|
constructor(options: RpcOptions);
|
|
39
188
|
private request;
|
|
40
189
|
private genesis?;
|
|
41
190
|
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
42
191
|
genesisHash(): Promise<Uint8Array>;
|
|
43
|
-
status<T =
|
|
44
|
-
account<T =
|
|
45
|
-
|
|
46
|
-
|
|
192
|
+
status<T = NodeStatus>(): Promise<T>;
|
|
193
|
+
account<T = Account>(address: string): Promise<T>;
|
|
194
|
+
/** The account's asset balances, each with `transfer_eligible` and `eligibility_reason` for the sending side. Includes rows that went to zero. */
|
|
195
|
+
assets<T = AccountAssetBalance[]>(address: string): Promise<T>;
|
|
196
|
+
/** One asset balance. 404 (`RpcError`) when the asset was never registered; a registered asset held at zero is `balance: 0n`. */
|
|
197
|
+
assetBalance<T = AccountAssetBalance>(address: string, assetRef: string): Promise<T>;
|
|
198
|
+
stake<T = StakeAllocation>(address: string): Promise<T>;
|
|
199
|
+
stakes<T = StakeAllocation[]>(address: string): Promise<T>;
|
|
200
|
+
/** A stake `master` placed on `validator`'s behalf (`stake(address)` is the self-stake case). */
|
|
201
|
+
delegatedStake<T = StakeAllocation>(master: string, validator: string): Promise<T>;
|
|
47
202
|
blsKey<T = Record<string, unknown>>(address: string): Promise<T>;
|
|
203
|
+
/** Every registered asset, or one issuer's. */
|
|
204
|
+
listAssets<T = Asset[]>(issuer?: string): Promise<T>;
|
|
205
|
+
asset<T = Asset>(assetRef: string): Promise<T>;
|
|
206
|
+
/** The asset `issuer` registered as `assetId`, or `null` when that id is still free for them. */
|
|
207
|
+
assetByAlias<T = Asset>(issuer: string, assetId: string): Promise<T | null>;
|
|
208
|
+
/** The cap table: every address with a non-zero balance, with its freeze state. */
|
|
209
|
+
assetHolders<T = AssetHolder[]>(assetRef: string): Promise<T>;
|
|
210
|
+
attestors<T = Attestor[]>(): Promise<T>;
|
|
211
|
+
attestor<T = Attestor>(address: string): Promise<T>;
|
|
48
212
|
submit(action: SignedAction): Promise<unknown>;
|
|
49
213
|
action(signature: string): Promise<ActionStatus>;
|
|
50
214
|
blocks<T = unknown>(from: number, to: number): Promise<T>;
|
|
51
215
|
block<T = unknown>(height: number): Promise<T>;
|
|
52
216
|
blockByHash<T = unknown>(hash: string): Promise<T>;
|
|
53
|
-
|
|
217
|
+
/** State changes a block made (balances, assets, stake), for indexers. */
|
|
218
|
+
blockEffects<T = unknown>(height: number): Promise<T>;
|
|
219
|
+
effects<T = unknown>(from: number, to: number): Promise<T>;
|
|
220
|
+
/** Sorted validator addresses, at the tip or at `height`. */
|
|
221
|
+
validators<T = string[]>(height?: number): Promise<T>;
|
|
222
|
+
validator<T = Validator>(address: string): Promise<T>;
|
|
223
|
+
/** Voting power by validator address, at the tip or at `height`. */
|
|
224
|
+
validatorPower<T = Record<string, number>>(height?: number): Promise<T>;
|
|
225
|
+
operatorValidators<T = string[]>(operator: string): Promise<T>;
|
|
54
226
|
finality<T = unknown>(): Promise<T>;
|
|
227
|
+
/** Ids of stored equivocation/fault evidence; `evidence(id)` returns one. */
|
|
228
|
+
evidenceList<T = string[]>(): Promise<T>;
|
|
229
|
+
evidence<T = unknown>(id: string): Promise<T>;
|
|
230
|
+
/** Merkle proofs against a finalized state root, for light clients. */
|
|
231
|
+
accountProof<T = unknown>(address: string): Promise<T>;
|
|
232
|
+
assetBalanceProof<T = unknown>(address: string, assetRef: string): Promise<T>;
|
|
233
|
+
validatorProof<T = unknown>(address: string): Promise<T>;
|
|
55
234
|
search<T = unknown>(query: string): Promise<T>;
|
|
56
235
|
minStake<T = unknown>(): Promise<T>;
|
|
57
|
-
actionFee<T =
|
|
236
|
+
actionFee<T = ActionFeeInfo>(): Promise<T>;
|
|
237
|
+
/** The chain's current fee parameters, for `actionFee(params, …)`. */
|
|
238
|
+
feeParams(): Promise<FeeParams>;
|
|
239
|
+
/** Signs at the sender's current nonce, submits and polls until confirmed (or finalized). Calls for one sender run one after another. */
|
|
58
240
|
sendAction(options: SendActionOptions): Promise<{
|
|
59
241
|
action: SignedAction;
|
|
60
242
|
status: ActionStatus;
|
|
61
243
|
}>;
|
|
244
|
+
private sendNow;
|
|
62
245
|
}
|
|
63
246
|
/** Stateful witnesses require the policy at the action's pre-state, not today's
|
|
64
247
|
* policy. This checks cryptography; circuit execution proves limits and scope. */
|
package/dist/rpc.js
CHANGED
|
@@ -9,21 +9,73 @@ export class RpcError extends Error {
|
|
|
9
9
|
this.status = status;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
/** The chain refused the action. `reason` is the node's exact text, e.g. `compliance check failed: arx1… is not KYC'd/allowlisted`. */
|
|
13
|
+
export class ActionDroppedError extends Error {
|
|
14
|
+
reason;
|
|
15
|
+
action;
|
|
16
|
+
constructor(reason, action) {
|
|
17
|
+
super(`action dropped: ${reason}`);
|
|
18
|
+
this.reason = reason;
|
|
19
|
+
this.action = action;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Keys whose integer values are token amounts (u128 on the node): always `bigint`, so callers never see a rounded balance. */
|
|
23
|
+
const AMOUNT_KEYS = new Set(["balance", "amount", "stake", "active_amount", "frozen_amount", "total_supply", "max_supply", "max_balance_per_holder", "total", "action_fee", "weight_fee", "token_create_fee", "min_stake"]);
|
|
24
|
+
const MARK = "\u0000int:";
|
|
25
|
+
/**
|
|
26
|
+
* `JSON.parse` that never rounds an integer: the node writes u128 amounts as bare JSON numbers, and anything above 2^53
|
|
27
|
+
* (about 9M ARX in IUM) loses digits in a plain parse. Amount keys become `bigint`; other integers stay `number` unless unsafe.
|
|
28
|
+
*/
|
|
29
|
+
export function parseJson(text) {
|
|
30
|
+
const marked = text.replace(/"(?:[^"\\]|\\.)*"|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g, (token) => token[0] === '"' || /[.eE]/.test(token) ? token : `"\\u0000int:${token}"`);
|
|
31
|
+
return JSON.parse(marked, (key, value) => {
|
|
32
|
+
if (typeof value !== "string" || !value.startsWith(MARK))
|
|
33
|
+
return value;
|
|
34
|
+
const digits = value.slice(MARK.length), number = Number(digits);
|
|
35
|
+
return AMOUNT_KEYS.has(key) || !Number.isSafeInteger(number) ? BigInt(digits) : number;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** `JSON.stringify` replacer for `bigint` values (as decimal strings). */
|
|
39
|
+
export const bigintReplacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
12
40
|
export class ArxiumRpc {
|
|
13
41
|
rpc;
|
|
14
42
|
token;
|
|
15
43
|
requestFetch;
|
|
44
|
+
/** One in-flight `sendAction` per sender: the node's account nonce doesn't count the mempool, so two concurrent sends would sign the same nonce. */
|
|
45
|
+
senders = new Map();
|
|
16
46
|
constructor(options) { this.rpc = options.rpc.replace(/\/+$/, ""); this.token = options.token; this.requestFetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init)); } // browsers and Workers throw "Illegal invocation" if fetch is called with `this` = ArxiumRpc
|
|
17
47
|
async request(path, init) { const response = await this.requestFetch(`${this.rpc}${path}`, { ...init, headers: { ...(this.token ? { Authorization: `Bearer ${this.token}` } : {}), ...init?.headers } }); const text = await response.text(); if (!response.ok)
|
|
18
|
-
throw new RpcError(response.status, text || `${response.status} ${response.statusText}`); return text ?
|
|
48
|
+
throw new RpcError(response.status, text || `${response.status} ${response.statusText}`); return text ? parseJson(text) : undefined; }
|
|
19
49
|
genesis;
|
|
20
50
|
/** This chain's genesis hash, fetched once per client; every signature binds it. */
|
|
21
51
|
genesisHash() { return this.genesis ??= this.request("/genesis-hash").then((r) => fromHex(r.genesis_hash.replace(/^0x/, "")), (e) => { this.genesis = undefined; throw e; }); }
|
|
22
52
|
status() { return this.request("/status"); }
|
|
23
53
|
account(address) { return this.request(`/accounts/${encodeURIComponent(address)}`); }
|
|
54
|
+
/** The account's asset balances, each with `transfer_eligible` and `eligibility_reason` for the sending side. Includes rows that went to zero. */
|
|
55
|
+
assets(address) { return this.request(`/accounts/${encodeURIComponent(address)}/assets`); }
|
|
56
|
+
/** One asset balance. 404 (`RpcError`) when the asset was never registered; a registered asset held at zero is `balance: 0n`. */
|
|
57
|
+
assetBalance(address, assetRef) { return this.request(`/accounts/${encodeURIComponent(address)}/assets/${encodeURIComponent(assetRef)}`); }
|
|
24
58
|
stake(address) { return this.request(`/accounts/${encodeURIComponent(address)}/stake`); }
|
|
25
59
|
stakes(address) { return this.request(`/accounts/${encodeURIComponent(address)}/stakes`); }
|
|
60
|
+
/** A stake `master` placed on `validator`'s behalf (`stake(address)` is the self-stake case). */
|
|
61
|
+
delegatedStake(master, validator) { return this.request(`/stake/${encodeURIComponent(master)}/${encodeURIComponent(validator)}`); }
|
|
26
62
|
blsKey(address) { return this.request(`/accounts/${encodeURIComponent(address)}/bls-key`); }
|
|
63
|
+
/** Every registered asset, or one issuer's. */
|
|
64
|
+
listAssets(issuer) { return this.request(issuer ? `/assets?issuer=${encodeURIComponent(issuer)}` : "/assets"); }
|
|
65
|
+
asset(assetRef) { return this.request(`/assets/${encodeURIComponent(assetRef)}`); }
|
|
66
|
+
/** The asset `issuer` registered as `assetId`, or `null` when that id is still free for them. */
|
|
67
|
+
async assetByAlias(issuer, assetId) { try {
|
|
68
|
+
return await this.request(`/assets/alias/${encodeURIComponent(issuer)}/${encodeURIComponent(assetId)}`);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof RpcError && error.status === 404)
|
|
72
|
+
return null;
|
|
73
|
+
throw error;
|
|
74
|
+
} }
|
|
75
|
+
/** The cap table: every address with a non-zero balance, with its freeze state. */
|
|
76
|
+
assetHolders(assetRef) { return this.request(`/assets/${encodeURIComponent(assetRef)}/holders`); }
|
|
77
|
+
attestors() { return this.request("/attestors"); }
|
|
78
|
+
attestor(address) { return this.request(`/attestors/${encodeURIComponent(address)}`); }
|
|
27
79
|
submit(action) { return this.request("/actions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(action) }); }
|
|
28
80
|
async action(signature) { try {
|
|
29
81
|
return await this.request(`/actions/${encodeURIComponent(signature)}`);
|
|
@@ -36,25 +88,63 @@ export class ArxiumRpc {
|
|
|
36
88
|
blocks(from, to) { return this.request(`/blocks?from=${from}&to=${to}`); }
|
|
37
89
|
block(height) { return this.request(`/blocks/${height}`); }
|
|
38
90
|
blockByHash(hash) { return this.request(`/blocks/by-hash/${encodeURIComponent(hash)}`); }
|
|
39
|
-
|
|
91
|
+
/** State changes a block made (balances, assets, stake), for indexers. */
|
|
92
|
+
blockEffects(height) { return this.request(`/blocks/${height}/effects`); }
|
|
93
|
+
effects(from, to) { return this.request(`/effects?from=${from}&to=${to}`); }
|
|
94
|
+
/** Sorted validator addresses, at the tip or at `height`. */
|
|
95
|
+
validators(height) { return this.request(height === undefined ? "/validators" : `/validators?height=${height}`); }
|
|
96
|
+
validator(address) { return this.request(`/validators/${encodeURIComponent(address)}`); }
|
|
97
|
+
/** Voting power by validator address, at the tip or at `height`. */
|
|
98
|
+
validatorPower(height) { return this.request(height === undefined ? "/validators/power" : `/validators/power?height=${height}`); }
|
|
99
|
+
operatorValidators(operator) { return this.request(`/operators/${encodeURIComponent(operator)}/validators`); }
|
|
40
100
|
finality() { return this.request("/finality"); }
|
|
101
|
+
/** Ids of stored equivocation/fault evidence; `evidence(id)` returns one. */
|
|
102
|
+
evidenceList() { return this.request("/evidence"); }
|
|
103
|
+
evidence(id) { return this.request(`/evidence/${encodeURIComponent(id)}`); }
|
|
104
|
+
/** Merkle proofs against a finalized state root, for light clients. */
|
|
105
|
+
accountProof(address) { return this.request(`/accounts/${encodeURIComponent(address)}/proof`); }
|
|
106
|
+
assetBalanceProof(address, assetRef) { return this.request(`/accounts/${encodeURIComponent(address)}/assets/${encodeURIComponent(assetRef)}/proof`); }
|
|
107
|
+
validatorProof(address) { return this.request(`/validators/${encodeURIComponent(address)}/proof`); }
|
|
41
108
|
search(query) { return this.request(`/search?q=${encodeURIComponent(query)}`); }
|
|
42
109
|
minStake() { return this.request("/min-stake"); }
|
|
43
110
|
actionFee() { return this.request("/action-fee"); }
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
111
|
+
/** The chain's current fee parameters, for `actionFee(params, …)`. */
|
|
112
|
+
async feeParams() { const fee = await this.actionFee(); return { actionFee: BigInt(fee.action_fee), weightFee: BigInt(fee.weight_fee) }; }
|
|
113
|
+
/** Signs at the sender's current nonce, submits and polls until confirmed (or finalized). Calls for one sender run one after another. */
|
|
114
|
+
sendAction(options) {
|
|
115
|
+
const previous = this.senders.get(options.sender) ?? Promise.resolve();
|
|
116
|
+
const run = previous.catch(() => undefined).then(() => this.sendNow(options));
|
|
117
|
+
const tail = run.catch(() => undefined);
|
|
118
|
+
this.senders.set(options.sender, tail);
|
|
119
|
+
void tail.then(() => { if (this.senders.get(options.sender) === tail)
|
|
120
|
+
this.senders.delete(options.sender); });
|
|
121
|
+
return run;
|
|
122
|
+
}
|
|
123
|
+
async sendNow(options) {
|
|
124
|
+
if (!options.privateKey === !options.sign)
|
|
125
|
+
throw new Error("sendAction needs exactly one of privateKey or sign");
|
|
126
|
+
const account = await this.account(options.sender).catch((error) => { if (error instanceof RpcError && error.status === 404)
|
|
127
|
+
return { nonce: 0 }; throw error; });
|
|
128
|
+
const genesis = await this.genesisHash(), nonce = account.nonce;
|
|
129
|
+
const signature = options.sign ? await options.sign(signingBytes(genesis, options.sender, nonce, options.payload), nonce) : await signAction(options.privateKey, genesis, options.sender, nonce, options.payload);
|
|
130
|
+
const action = submitBody(options.sender, nonce, signature, options.payload);
|
|
131
|
+
await this.submit(action);
|
|
132
|
+
const deadline = Date.now() + (options.timeoutMs ?? 120_000);
|
|
133
|
+
while (Date.now() < deadline) {
|
|
134
|
+
const status = await this.action(signature);
|
|
135
|
+
if (status.status === "dropped")
|
|
136
|
+
throw new ActionDroppedError(status.reason, action);
|
|
137
|
+
if (status.status === "confirmed") {
|
|
138
|
+
if (!options.finalized)
|
|
139
|
+
return { action, status };
|
|
140
|
+
const block = await this.block(status.height);
|
|
141
|
+
if (block.finalized)
|
|
142
|
+
return { action, status };
|
|
143
|
+
}
|
|
144
|
+
await new Promise((resolve) => setTimeout(resolve, options.pollIntervalMs ?? 1_000));
|
|
55
145
|
}
|
|
56
|
-
|
|
57
|
-
}
|
|
146
|
+
throw new Error("timed out waiting for action");
|
|
147
|
+
}
|
|
58
148
|
}
|
|
59
149
|
/** Stateful witnesses require the policy at the action's pre-state, not today's
|
|
60
150
|
* policy. This checks cryptography; circuit execution proves limits and scope. */
|