@arxiumlabs/sdk 0.0.0-stage → 0.4.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 +124 -2
- package/dist/actions.d.ts +161 -0
- package/dist/actions.js +207 -0
- package/dist/amounts.d.ts +3 -0
- package/dist/amounts.js +5 -0
- package/dist/bech32.d.ts +9 -0
- package/dist/bech32.js +39 -0
- package/dist/bincode.d.ts +31 -0
- package/dist/bincode.js +77 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +264 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/keys.d.ts +37 -0
- package/dist/keys.js +36 -0
- package/dist/policy.d.ts +53 -0
- package/dist/policy.js +116 -0
- package/dist/rpc.d.ts +65 -0
- package/dist/rpc.js +74 -0
- package/package.json +39 -4
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @arxiumlabs/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript SDK and `arx` CLI for the Arxium network. It has zero runtime dependencies and requires Node 20 or later. The SDK uses WebCrypto Ed25519 and also runs in browsers and Workers.
|
|
4
|
+
|
|
5
|
+
Builder docs (endpoints, API keys, faucet, quickstart, RWA walkthrough): https://arxium.network/docs
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @arxiumlabs/sdk # library
|
|
11
|
+
npm install -g @arxiumlabs/sdk # arx CLI
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Releasing
|
|
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) with provenance.
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
cd Arxium && cargo test -p arxd-runtime writes_typescript_signed_action_fixtures && git diff --exit-code sdk/ts/fixtures
|
|
20
|
+
cd sdk/ts && npm test
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## SDK quickstart
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { ArxiumRpc, encodeTransfer, arxToIum } from "@arxiumlabs/sdk";
|
|
27
|
+
|
|
28
|
+
const rpc = new ArxiumRpc({ rpc: "https://node.example", token: process.env.ARX_TOKEN });
|
|
29
|
+
const payload = encodeTransfer("arx1...", arxToIum("1.5"));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
All token amounts are `bigint`: `1 ARX = 1_000_000_000 IUM`.
|
|
33
|
+
|
|
34
|
+
### Chain binding
|
|
35
|
+
|
|
36
|
+
Every action signature covers the chain's 32-byte genesis hash (`GET /genesis-hash`, `rpc.genesisHash()`) ahead of sender, nonce and payload, so an action signed for one chain is rejected on any other (devnet vs. mainnet, or after a reset). `signingBytes`, `signAction` and `verifySignedAction` take it as an argument; `rpc.sendAction` fetches it for you.
|
|
37
|
+
|
|
38
|
+
### Multisig senders
|
|
39
|
+
|
|
40
|
+
An M-of-N address (up to 16 members) is derived from its policy. Each member signs the action exactly as a single-key sender would. You then combine exactly `threshold` of those signatures:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
const sender = await multisigAddress(2, [pkA, pkB, pkC]); // member order doesn't matter
|
|
44
|
+
const genesis = await rpc.genesisHash(); // every signature binds the chain's genesis hash
|
|
45
|
+
const sigA = await signAction(keyA, genesis, sender, nonce, payload); // on A's machine
|
|
46
|
+
const sigC = await signAction(keyC, genesis, sender, nonce, payload); // on C's machine
|
|
47
|
+
const signature = multisigSignature(2, [pkA, pkB, pkC], [[pkA, sigA], [pkC, sigC]]);
|
|
48
|
+
await rpc.submit(submitBody(sender, nonce, signature, payload));
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A co-signer should see what they're signing before they sign it. `decodePayload(bytes)` returns `{ name, input }` for every action the SDK can encode. It re-encodes the result and throws unless the bytes match exactly.
|
|
52
|
+
|
|
53
|
+
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
|
+
|
|
55
|
+
## Key files
|
|
56
|
+
|
|
57
|
+
`arx` reads and writes Console's encrypted export JSON unchanged:
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"address": "arx1...",
|
|
62
|
+
"publicKey": "...",
|
|
63
|
+
"version": 1,
|
|
64
|
+
"kdf": "PBKDF2-SHA256",
|
|
65
|
+
"iterations": 600000,
|
|
66
|
+
"salt": "...",
|
|
67
|
+
"iv": "...",
|
|
68
|
+
"ciphertext": "...",
|
|
69
|
+
"network": "arx-devnet"
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`network` is a display label. It is not a signing domain. The CLI verifies that a decrypted private key matches `address` and `publicKey`.
|
|
74
|
+
|
|
75
|
+
Set `ARX_PASSPHRASE` for non-interactive use. Otherwise `arx` prompts on a terminal. A non-terminal invocation without it exits with status 2.
|
|
76
|
+
|
|
77
|
+
## CLI
|
|
78
|
+
|
|
79
|
+
Global configuration: `--rpc`, `--token`, `--key`, and `--json` have `ARX_RPC`, `ARX_TOKEN`, and `ARX_KEY` environment equivalents.
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
arx keys new
|
|
83
|
+
arx keys import --file arxium-devnet-key-abc.json
|
|
84
|
+
printf '%s' "$SEED_HEX" | arx keys import
|
|
85
|
+
arx keys show --key arxium-devnet-key-abc.json # no passphrase needed
|
|
86
|
+
|
|
87
|
+
arx sign transfer arx1recipient 1500000000 --key key.json # nonce fetched from --rpc
|
|
88
|
+
arx sign transfer arx1recipient 1500000000 --key key.json --nonce 7 # fully offline
|
|
89
|
+
arx submit signed-action.json
|
|
90
|
+
arx send transfer arx1recipient 1500000000 --key key.json
|
|
91
|
+
|
|
92
|
+
arx query status
|
|
93
|
+
arx query account arx1address
|
|
94
|
+
arx query block 42
|
|
95
|
+
arx query block 0xblockhash
|
|
96
|
+
arx query action signature
|
|
97
|
+
arx query validators
|
|
98
|
+
arx query finality
|
|
99
|
+
arx query search arx1address
|
|
100
|
+
arx verify signature
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
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` and `execute-proposal`. Amount arguments are raw base-unit integers (IUM for ARX, the asset's smallest unit for an asset).
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
arx send apply-attestor "Arxium Bank" <owner>,<owner>,<owner> 2 <evidence-hash> <evidence-uri> # sender is the 2-of-N multisig; validators vote
|
|
107
|
+
arx send propose-remove-attestor <attestor> "reason" # validators only; blocks that attestor's grants while the vote runs
|
|
108
|
+
arx send vote <proposal-id> yes # then, after the window: arx send execute-proposal <proposal-id>
|
|
109
|
+
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]
|
|
111
|
+
arx send issue-asset arxasset1... 1000
|
|
112
|
+
arx send transfer-asset arxasset1... arx1recipient 100
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
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.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { POLICY_VARIANT } from "./policy.js";
|
|
2
|
+
/** These indices are ActionPayload's positional bincode discriminants. Never derive them. */
|
|
3
|
+
export declare const ACTION_VARIANT: {
|
|
4
|
+
readonly transfer: 0;
|
|
5
|
+
readonly joinValidator: 1;
|
|
6
|
+
readonly leaveValidator: 2;
|
|
7
|
+
readonly stake: 3;
|
|
8
|
+
readonly unstake: 4;
|
|
9
|
+
readonly registerBlsKey: 6;
|
|
10
|
+
readonly authorizeOperator: 8;
|
|
11
|
+
readonly revokeOperator: 9;
|
|
12
|
+
readonly grantAttestation: 10;
|
|
13
|
+
readonly revokeAttestation: 11;
|
|
14
|
+
readonly registerAsset: 12;
|
|
15
|
+
readonly issueAsset: 13;
|
|
16
|
+
readonly transferAsset: 14;
|
|
17
|
+
readonly freezeAsset: 16;
|
|
18
|
+
readonly unfreezeAsset: 17;
|
|
19
|
+
readonly burnAsset: 18;
|
|
20
|
+
readonly setHolderFrozen: 19;
|
|
21
|
+
readonly lockHolderAmount: 20;
|
|
22
|
+
readonly unlockHolderAmount: 21;
|
|
23
|
+
readonly issuerForcedTransfer: 22;
|
|
24
|
+
readonly recoverHolder: 23;
|
|
25
|
+
readonly issueAssetTo: 24;
|
|
26
|
+
readonly setAssetLimits: 28;
|
|
27
|
+
readonly verifyClaimProof: 37;
|
|
28
|
+
readonly setPrivateClaims: 38;
|
|
29
|
+
readonly submitProposal: 30;
|
|
30
|
+
readonly voteProposal: 31;
|
|
31
|
+
readonly executeProposal: 32;
|
|
32
|
+
readonly applyAttestor: 41;
|
|
33
|
+
readonly blockAttestor: 42;
|
|
34
|
+
};
|
|
35
|
+
/** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
|
|
36
|
+
export declare const TOKEN_ACTION = 39;
|
|
37
|
+
export declare const TOKEN_VARIANT: {
|
|
38
|
+
readonly createToken: 0;
|
|
39
|
+
readonly mintToken: 1;
|
|
40
|
+
readonly transferToken: 2;
|
|
41
|
+
readonly burnToken: 3;
|
|
42
|
+
readonly renounceMint: 4;
|
|
43
|
+
};
|
|
44
|
+
declare const CLASS: {
|
|
45
|
+
readonly other: 0;
|
|
46
|
+
readonly real_estate: 1;
|
|
47
|
+
readonly equity: 2;
|
|
48
|
+
readonly bond: 3;
|
|
49
|
+
readonly stablecoin: 4;
|
|
50
|
+
readonly commodity: 5;
|
|
51
|
+
readonly token: 6;
|
|
52
|
+
};
|
|
53
|
+
declare const TOPIC: {
|
|
54
|
+
readonly kyc: 0;
|
|
55
|
+
readonly aml: 1;
|
|
56
|
+
readonly accredited: 2;
|
|
57
|
+
readonly jurisdiction: 3;
|
|
58
|
+
};
|
|
59
|
+
export type AssetClass = keyof typeof CLASS;
|
|
60
|
+
export type ClaimTopic = keyof typeof TOPIC;
|
|
61
|
+
export type AssetMetadata = {
|
|
62
|
+
asset_class: AssetClass;
|
|
63
|
+
decimals: number;
|
|
64
|
+
required_claims: ClaimTopic[];
|
|
65
|
+
allowed_jurisdictions: string[] | null;
|
|
66
|
+
max_supply: string | bigint | null;
|
|
67
|
+
metadata_uri: string | null;
|
|
68
|
+
symbol: string;
|
|
69
|
+
name: string;
|
|
70
|
+
};
|
|
71
|
+
export declare const ACTION_FEE = 1000000n;
|
|
72
|
+
export declare function encodeTransfer(to: string, amount: bigint): Uint8Array;
|
|
73
|
+
export declare function encodeJoinValidator(validator: string, stake: bigint, blsPubkey: Uint8Array, blsPop: Uint8Array): Uint8Array;
|
|
74
|
+
export declare function encodeLeaveValidator(validator: string): Uint8Array;
|
|
75
|
+
export declare function encodeStake(validator: string, amount: bigint): Uint8Array;
|
|
76
|
+
export declare function encodeUnstake(validator: string, amount: bigint): Uint8Array;
|
|
77
|
+
export declare function encodeRegisterBlsKey(validator: string, pubkey: Uint8Array, pop: Uint8Array): Uint8Array;
|
|
78
|
+
export declare function encodeAuthorizeOperator(operator: string): Uint8Array;
|
|
79
|
+
export declare function encodeRevokeOperator(): Uint8Array;
|
|
80
|
+
export declare function encodeGrantAttestation(subject: string, hash: string, topics: ClaimTopic[], jurisdiction: string | null): Uint8Array;
|
|
81
|
+
export declare function encodeRevokeAttestation(subject: string): Uint8Array;
|
|
82
|
+
export declare function encodeRegisterAsset(assetId: string, complianceRequired: boolean, metadata: AssetMetadata): Uint8Array;
|
|
83
|
+
export declare function encodeIssueAsset(asset: string, amount: bigint): Uint8Array;
|
|
84
|
+
export declare function encodeTransferAsset(asset: string, to: string, amount: bigint): Uint8Array;
|
|
85
|
+
export declare function encodeFreezeAsset(asset: string, frozen: boolean, reason: string): Uint8Array;
|
|
86
|
+
export declare function encodeBurnAsset(asset: string, amount: bigint): Uint8Array;
|
|
87
|
+
export declare function encodeSetHolderFrozen(asset: string, holder: string, frozen: boolean): Uint8Array;
|
|
88
|
+
export declare function encodeLockHolderAmount(asset: string, holder: string, amount: bigint, lock: boolean): Uint8Array;
|
|
89
|
+
export declare function encodeIssuerForcedTransfer(asset: string, from: string, to: string, amount: bigint, reason: string): Uint8Array;
|
|
90
|
+
export declare function encodeRecoverHolder(asset: string, lost: string, replacement: string): Uint8Array;
|
|
91
|
+
export declare function encodeIssueAssetTo(asset: string, to: string, amount: bigint): Uint8Array;
|
|
92
|
+
/** Sets issuer-controlled investor, concentration, and attestation-age limits. `null` clears each limit. */
|
|
93
|
+
/** zk-KYC: a holder's private eligibility proof. `sub` is a fixed 32-byte array on the node, so it rides raw (no length prefix); `proof` is length-prefixed. */
|
|
94
|
+
export declare function encodeVerifyClaimProof(asset: string, sub: Uint8Array, todayDays: number, proof: Uint8Array): Uint8Array;
|
|
95
|
+
/** Issuer opt-in: whether holders may clear `asset`'s gate with a claim proof instead of clear-text claims. */
|
|
96
|
+
export declare function encodeSetPrivateClaims(asset: string, enabled: boolean): Uint8Array;
|
|
97
|
+
/** Permissionless token (`circuits/token`); its ref is `deriveAssetRef(sender, symbol.toLowerCase())`. `mintable: false` fixes supply at creation. Costs `token_create_fee` on top of the action fee. */
|
|
98
|
+
export declare function encodeCreateToken(symbol: string, name: string, decimals: number, initialSupply: bigint, maxSupply: bigint | null, mintable: boolean): Uint8Array;
|
|
99
|
+
export declare function encodeMintToken(tokenRef: string, to: string, amount: bigint): Uint8Array;
|
|
100
|
+
export declare function encodeTransferToken(tokenRef: string, to: string, amount: bigint): Uint8Array;
|
|
101
|
+
export declare function encodeBurnToken(tokenRef: string, amount: bigint): Uint8Array;
|
|
102
|
+
export declare function encodeRenounceMint(tokenRef: string): Uint8Array;
|
|
103
|
+
/** Applies to become an attestor. The sender is the attestor: its address must be the `threshold`-of-`owners` multisig (threshold >= 2). Pays the application bond and opens a validator vote. */
|
|
104
|
+
export declare function encodeApplyAttestor(name: string, owners: string[], threshold: number, evidenceHash: string, evidenceUri: string): Uint8Array;
|
|
105
|
+
/** Emergency stop: the sending attestor can no longer grant new attestations. Only a governance vote lifts it. */
|
|
106
|
+
export declare function encodeBlockAttestor(): Uint8Array;
|
|
107
|
+
/** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
|
|
108
|
+
export declare const GOVERNANCE_ACTION: {
|
|
109
|
+
readonly addAttestor: 4;
|
|
110
|
+
readonly removeAttestor: 5;
|
|
111
|
+
readonly unblockAttestor: 6;
|
|
112
|
+
};
|
|
113
|
+
export type GovernanceProposal = {
|
|
114
|
+
kind: "addAttestor";
|
|
115
|
+
attestor: string;
|
|
116
|
+
name: string;
|
|
117
|
+
owners: string[];
|
|
118
|
+
threshold: number;
|
|
119
|
+
evidenceHash: string;
|
|
120
|
+
evidenceUri: string;
|
|
121
|
+
} | {
|
|
122
|
+
kind: "removeAttestor" | "unblockAttestor";
|
|
123
|
+
attestor: string;
|
|
124
|
+
};
|
|
125
|
+
/** 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. */
|
|
126
|
+
export declare function encodeSubmitProposal(proposal: GovernanceProposal, description: string): Uint8Array;
|
|
127
|
+
/** Stake-weighted vote. One per validator per proposal. */
|
|
128
|
+
export declare function encodeVoteProposal(proposal: number | bigint, approve: boolean): Uint8Array;
|
|
129
|
+
/** Anyone may close a proposal once its window has ended; this applies it if it passed and always lifts a removal block. */
|
|
130
|
+
export declare function encodeExecuteProposal(proposal: number | bigint): Uint8Array;
|
|
131
|
+
export declare function encodeSetAssetLimits(asset: string, maxHolders: number | null, maxBalancePerHolder?: bigint | null, maxAttestationAge?: bigint | null): Uint8Array;
|
|
132
|
+
/** `{ name, input }` names and shapes match `fixtures/signed-actions.json`: amounts are decimal strings, byte fields are number arrays. */
|
|
133
|
+
export type ActionName = keyof typeof ACTION_VARIANT | keyof typeof TOKEN_VARIANT | keyof typeof POLICY_VARIANT;
|
|
134
|
+
export type DecodedPayload = {
|
|
135
|
+
name: ActionName;
|
|
136
|
+
input: Record<string, any>;
|
|
137
|
+
};
|
|
138
|
+
export declare function encodePayload({ name, input }: DecodedPayload): Uint8Array;
|
|
139
|
+
/**
|
|
140
|
+
* Decodes a payload the SDK can encode, then re-encodes it and throws unless the bytes match, so what
|
|
141
|
+
* a co-signer is shown is exactly what they sign. Unknown variants throw rather than display partially.
|
|
142
|
+
*/
|
|
143
|
+
export declare function decodePayload(payload: Uint8Array): DecodedPayload;
|
|
144
|
+
/** 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. */
|
|
145
|
+
export declare function signingBytes(genesis: Uint8Array, sender: string, nonce: number | bigint, payload: Uint8Array): Uint8Array;
|
|
146
|
+
export declare function signAction(privateKey: CryptoKey, genesis: Uint8Array, sender: string, nonce: number, payload: Uint8Array): Promise<string>;
|
|
147
|
+
export type SignedAction = {
|
|
148
|
+
sender: string;
|
|
149
|
+
nonce: number;
|
|
150
|
+
signature: string;
|
|
151
|
+
payload: number[];
|
|
152
|
+
};
|
|
153
|
+
export declare function submitBody(sender: string, nonce: number, signature: string, payload: Uint8Array): SignedAction;
|
|
154
|
+
/** 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. */
|
|
155
|
+
export declare const MAX_MULTISIG_MEMBERS = 16;
|
|
156
|
+
export declare function multisigAddress(threshold: number, members: Uint8Array[]): Promise<string>;
|
|
157
|
+
/** `signatures`: [member public key, that member's hex signature]. */
|
|
158
|
+
export declare function multisigSignature(threshold: number, members: Uint8Array[], signatures: [Uint8Array, string][]): string;
|
|
159
|
+
/** Same checks as `Action::verify_signature` for a multisig sender. */
|
|
160
|
+
export declare function verifyMultisig(sender: string, signature: string, message: Uint8Array): Promise<boolean>;
|
|
161
|
+
export {};
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { MULTISIG_TAG, decodeAddress, encodeAddress } from "./bech32.js";
|
|
2
|
+
import { Reader, Writer, asBuffer, fromHex, toHex } from "./bincode.js";
|
|
3
|
+
import { ACCOUNT_POLICY_ACTION, POLICY_VARIANT, encodeAccountPolicy, readAccountPolicy } from "./policy.js";
|
|
4
|
+
/** These indices are ActionPayload's positional bincode discriminants. Never derive them. */
|
|
5
|
+
export const ACTION_VARIANT = {
|
|
6
|
+
transfer: 0, joinValidator: 1, leaveValidator: 2, stake: 3, unstake: 4, registerBlsKey: 6,
|
|
7
|
+
authorizeOperator: 8, revokeOperator: 9, grantAttestation: 10, revokeAttestation: 11,
|
|
8
|
+
registerAsset: 12, issueAsset: 13, transferAsset: 14, freezeAsset: 16, unfreezeAsset: 17,
|
|
9
|
+
burnAsset: 18, setHolderFrozen: 19, lockHolderAmount: 20, unlockHolderAmount: 21,
|
|
10
|
+
issuerForcedTransfer: 22, recoverHolder: 23, issueAssetTo: 24,
|
|
11
|
+
setAssetLimits: 28, verifyClaimProof: 37, setPrivateClaims: 38,
|
|
12
|
+
submitProposal: 30, voteProposal: 31, executeProposal: 32,
|
|
13
|
+
applyAttestor: 41, blockAttestor: 42,
|
|
14
|
+
};
|
|
15
|
+
/** `ActionPayload::Token(TokenAction)` is variant 39; these are `TokenAction`'s own discriminants, written after it. */
|
|
16
|
+
export const TOKEN_ACTION = 39;
|
|
17
|
+
export const TOKEN_VARIANT = { createToken: 0, mintToken: 1, transferToken: 2, burnToken: 3, renounceMint: 4 };
|
|
18
|
+
const CLASS = { other: 0, real_estate: 1, equity: 2, bond: 3, stablecoin: 4, commodity: 5, token: 6 };
|
|
19
|
+
const TOPIC = { kyc: 0, aml: 1, accredited: 2, jurisdiction: 3 };
|
|
20
|
+
export const ACTION_FEE = 1000000n;
|
|
21
|
+
export function encodeTransfer(to, amount) { return new Writer().varint(ACTION_VARIANT.transfer).string(to).varint(amount).bytes(); }
|
|
22
|
+
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(); }
|
|
23
|
+
export function encodeLeaveValidator(validator) { return new Writer().varint(ACTION_VARIANT.leaveValidator).string(validator).bytes(); }
|
|
24
|
+
export function encodeStake(validator, amount) { return new Writer().varint(ACTION_VARIANT.stake).string(validator).varint(amount).bytes(); }
|
|
25
|
+
export function encodeUnstake(validator, amount) { return new Writer().varint(ACTION_VARIANT.unstake).string(validator).varint(amount).bytes(); }
|
|
26
|
+
export function encodeRegisterBlsKey(validator, pubkey, pop) { return new Writer().varint(ACTION_VARIANT.registerBlsKey).string(validator).vec(Array.from(pubkey), (w, b) => w.u8(b)).vec(Array.from(pop), (w, b) => w.u8(b)).bytes(); }
|
|
27
|
+
export function encodeAuthorizeOperator(operator) { return new Writer().varint(ACTION_VARIANT.authorizeOperator).string(operator).bytes(); }
|
|
28
|
+
export function encodeRevokeOperator() { return new Writer().varint(ACTION_VARIANT.revokeOperator).bytes(); }
|
|
29
|
+
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(); }
|
|
30
|
+
export function encodeRevokeAttestation(subject) { return new Writer().varint(ACTION_VARIANT.revokeAttestation).string(subject).bytes(); }
|
|
31
|
+
export function encodeRegisterAsset(assetId, complianceRequired, metadata) { return 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).bytes(); }
|
|
32
|
+
export function encodeIssueAsset(asset, amount) { return new Writer().varint(ACTION_VARIANT.issueAsset).string(asset).varint(amount).bytes(); }
|
|
33
|
+
export function encodeTransferAsset(asset, to, amount) { return new Writer().varint(ACTION_VARIANT.transferAsset).string(asset).string(to).varint(amount).bytes(); }
|
|
34
|
+
export function encodeFreezeAsset(asset, frozen, reason) { return new Writer().varint(frozen ? ACTION_VARIANT.freezeAsset : ACTION_VARIANT.unfreezeAsset).string(asset).string(reason).bytes(); }
|
|
35
|
+
export function encodeBurnAsset(asset, amount) { return new Writer().varint(ACTION_VARIANT.burnAsset).string(asset).varint(amount).bytes(); }
|
|
36
|
+
export function encodeSetHolderFrozen(asset, holder, frozen) { return new Writer().varint(ACTION_VARIANT.setHolderFrozen).string(asset).string(holder).bool(frozen).bytes(); }
|
|
37
|
+
export function encodeLockHolderAmount(asset, holder, amount, lock) { return new Writer().varint(lock ? ACTION_VARIANT.lockHolderAmount : ACTION_VARIANT.unlockHolderAmount).string(asset).string(holder).varint(amount).bytes(); }
|
|
38
|
+
export function encodeIssuerForcedTransfer(asset, from, to, amount, reason) { return new Writer().varint(ACTION_VARIANT.issuerForcedTransfer).string(asset).string(from).string(to).varint(amount).string(reason).bytes(); }
|
|
39
|
+
export function encodeRecoverHolder(asset, lost, replacement) { return new Writer().varint(ACTION_VARIANT.recoverHolder).string(asset).string(lost).string(replacement).bytes(); }
|
|
40
|
+
export function encodeIssueAssetTo(asset, to, amount) { return new Writer().varint(ACTION_VARIANT.issueAssetTo).string(asset).string(to).varint(amount).bytes(); }
|
|
41
|
+
/** Sets issuer-controlled investor, concentration, and attestation-age limits. `null` clears each limit. */
|
|
42
|
+
/** zk-KYC: a holder's private eligibility proof. `sub` is a fixed 32-byte array on the node, so it rides raw (no length prefix); `proof` is length-prefixed. */
|
|
43
|
+
export function encodeVerifyClaimProof(asset, sub, todayDays, proof) { if (sub.length !== 32)
|
|
44
|
+
throw new Error("sub must be 32 bytes"); return new Writer().varint(ACTION_VARIANT.verifyClaimProof).string(asset).raw(sub).varint(todayDays).vec(Array.from(proof), (w, b) => w.u8(b)).bytes(); }
|
|
45
|
+
/** Issuer opt-in: whether holders may clear `asset`'s gate with a claim proof instead of clear-text claims. */
|
|
46
|
+
export function encodeSetPrivateClaims(asset, enabled) { return new Writer().varint(ACTION_VARIANT.setPrivateClaims).string(asset).bool(enabled).bytes(); }
|
|
47
|
+
const token = (name) => new Writer().varint(TOKEN_ACTION).varint(TOKEN_VARIANT[name]);
|
|
48
|
+
/** Permissionless token (`circuits/token`); its ref is `deriveAssetRef(sender, symbol.toLowerCase())`. `mintable: false` fixes supply at creation. Costs `token_create_fee` on top of the action fee. */
|
|
49
|
+
export function encodeCreateToken(symbol, name, decimals, initialSupply, maxSupply, mintable) { return token("createToken").string(symbol).string(name).u8(decimals).varint(initialSupply).option(maxSupply, (w, cap) => w.varint(cap)).bool(mintable).bytes(); }
|
|
50
|
+
export function encodeMintToken(tokenRef, to, amount) { return token("mintToken").string(tokenRef).string(to).varint(amount).bytes(); }
|
|
51
|
+
export function encodeTransferToken(tokenRef, to, amount) { return token("transferToken").string(tokenRef).string(to).varint(amount).bytes(); }
|
|
52
|
+
export function encodeBurnToken(tokenRef, amount) { return token("burnToken").string(tokenRef).varint(amount).bytes(); }
|
|
53
|
+
export function encodeRenounceMint(tokenRef) { return token("renounceMint").string(tokenRef).bytes(); }
|
|
54
|
+
/** Applies to become an attestor. The sender is the attestor: its address must be the `threshold`-of-`owners` multisig (threshold >= 2). Pays the application bond and opens a validator vote. */
|
|
55
|
+
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(); }
|
|
56
|
+
/** Emergency stop: the sending attestor can no longer grant new attestations. Only a governance vote lifts it. */
|
|
57
|
+
export function encodeBlockAttestor() { return new Writer().varint(ACTION_VARIANT.blockAttestor).bytes(); }
|
|
58
|
+
/** `GovernanceAction`'s own discriminants (after `SetChainParams` 0, `TreasurySpend` 1, `ResolveDispute` 2, `ReinstateValidator` 3). Only the attestor actions are encodable here. */
|
|
59
|
+
export const GOVERNANCE_ACTION = { addAttestor: 4, removeAttestor: 5, unblockAttestor: 6 };
|
|
60
|
+
/** 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. */
|
|
61
|
+
export function encodeSubmitProposal(proposal, description) {
|
|
62
|
+
const w = new Writer().varint(ACTION_VARIANT.submitProposal).varint(GOVERNANCE_ACTION[proposal.kind]).string(proposal.attestor);
|
|
63
|
+
if (proposal.kind === "addAttestor")
|
|
64
|
+
w.string(proposal.name).vec(proposal.owners, (x, o) => x.string(o)).u8(proposal.threshold).string(proposal.evidenceHash).string(proposal.evidenceUri);
|
|
65
|
+
return w.string(description).bytes();
|
|
66
|
+
}
|
|
67
|
+
/** Stake-weighted vote. One per validator per proposal. */
|
|
68
|
+
export function encodeVoteProposal(proposal, approve) { return new Writer().varint(ACTION_VARIANT.voteProposal).varint(BigInt(proposal)).bool(approve).bytes(); }
|
|
69
|
+
/** Anyone may close a proposal once its window has ended; this applies it if it passed and always lifts a removal block. */
|
|
70
|
+
export function encodeExecuteProposal(proposal) { return new Writer().varint(ACTION_VARIANT.executeProposal).varint(BigInt(proposal)).bytes(); }
|
|
71
|
+
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(); }
|
|
72
|
+
const nullableBig = (value) => value == null ? null : BigInt(value);
|
|
73
|
+
export function encodePayload({ name, input }) {
|
|
74
|
+
if (name in POLICY_VARIANT)
|
|
75
|
+
return encodeAccountPolicy(name, input);
|
|
76
|
+
switch (name) {
|
|
77
|
+
case "transfer": return encodeTransfer(input.to, BigInt(input.amount));
|
|
78
|
+
case "joinValidator": return encodeJoinValidator(input.validator, BigInt(input.stake), Uint8Array.from(input.blsPubkey), Uint8Array.from(input.blsPop));
|
|
79
|
+
case "leaveValidator": return encodeLeaveValidator(input.validator);
|
|
80
|
+
case "stake": return encodeStake(input.validator, BigInt(input.amount));
|
|
81
|
+
case "unstake": return encodeUnstake(input.validator, BigInt(input.amount));
|
|
82
|
+
case "registerBlsKey": return encodeRegisterBlsKey(input.validator, Uint8Array.from(input.pubkey), Uint8Array.from(input.pop));
|
|
83
|
+
case "authorizeOperator": return encodeAuthorizeOperator(input.operator);
|
|
84
|
+
case "revokeOperator": return encodeRevokeOperator();
|
|
85
|
+
case "grantAttestation": return encodeGrantAttestation(input.subject, input.hash, input.topics, input.jurisdiction);
|
|
86
|
+
case "revokeAttestation": return encodeRevokeAttestation(input.subject);
|
|
87
|
+
case "registerAsset": return encodeRegisterAsset(input.assetId, input.complianceRequired, input.metadata);
|
|
88
|
+
case "issueAsset": return encodeIssueAsset(input.asset, BigInt(input.amount));
|
|
89
|
+
case "transferAsset": return encodeTransferAsset(input.asset, input.to, BigInt(input.amount));
|
|
90
|
+
case "freezeAsset":
|
|
91
|
+
case "unfreezeAsset": return encodeFreezeAsset(input.asset, input.frozen, input.reason);
|
|
92
|
+
case "burnAsset": return encodeBurnAsset(input.asset, BigInt(input.amount));
|
|
93
|
+
case "setHolderFrozen": return encodeSetHolderFrozen(input.asset, input.holder, input.frozen);
|
|
94
|
+
case "lockHolderAmount":
|
|
95
|
+
case "unlockHolderAmount": return encodeLockHolderAmount(input.asset, input.holder, BigInt(input.amount), input.lock);
|
|
96
|
+
case "issuerForcedTransfer": return encodeIssuerForcedTransfer(input.asset, input.from, input.to, BigInt(input.amount), input.reason);
|
|
97
|
+
case "recoverHolder": return encodeRecoverHolder(input.asset, input.lost, input.replacement);
|
|
98
|
+
case "issueAssetTo": return encodeIssueAssetTo(input.asset, input.to, BigInt(input.amount));
|
|
99
|
+
case "setAssetLimits": return encodeSetAssetLimits(input.asset, input.maxHolders, nullableBig(input.maxBalancePerHolder), nullableBig(input.maxAttestationAge));
|
|
100
|
+
case "verifyClaimProof": return encodeVerifyClaimProof(input.asset, Uint8Array.from(input.sub), input.todayDays, Uint8Array.from(input.proof));
|
|
101
|
+
case "setPrivateClaims": return encodeSetPrivateClaims(input.asset, input.enabled);
|
|
102
|
+
case "submitProposal": return encodeSubmitProposal(input.action, input.description);
|
|
103
|
+
case "voteProposal": return encodeVoteProposal(BigInt(input.proposal), input.approve);
|
|
104
|
+
case "executeProposal": return encodeExecuteProposal(BigInt(input.proposal));
|
|
105
|
+
case "applyAttestor": return encodeApplyAttestor(input.name, input.owners, input.threshold, input.evidenceHash, input.evidenceUri);
|
|
106
|
+
case "blockAttestor": return encodeBlockAttestor();
|
|
107
|
+
case "createToken": return encodeCreateToken(input.symbol, input.name, input.decimals, BigInt(input.initialSupply), nullableBig(input.maxSupply), input.mintable);
|
|
108
|
+
case "mintToken": return encodeMintToken(input.token, input.to, BigInt(input.amount));
|
|
109
|
+
case "transferToken": return encodeTransferToken(input.token, input.to, BigInt(input.amount));
|
|
110
|
+
case "burnToken": return encodeBurnToken(input.token, BigInt(input.amount));
|
|
111
|
+
case "renounceMint": return encodeRenounceMint(input.token);
|
|
112
|
+
default: throw new Error(`unsupported action: ${name}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const invert = (table) => Object.fromEntries(Object.entries(table).map(([key, value]) => [value, key]));
|
|
116
|
+
const VARIANT_NAME = invert(ACTION_VARIANT), TOKEN_NAME = invert(TOKEN_VARIANT), CLASS_NAME = invert(CLASS), TOPIC_NAME = invert(TOPIC), GOVERNANCE_NAME = invert(GOVERNANCE_ACTION);
|
|
117
|
+
const known = (value, what) => { if (value === undefined)
|
|
118
|
+
throw new Error(`unknown ${what}`); return value; };
|
|
119
|
+
/**
|
|
120
|
+
* Decodes a payload the SDK can encode, then re-encodes it and throws unless the bytes match, so what
|
|
121
|
+
* a co-signer is shown is exactly what they sign. Unknown variants throw rather than display partially.
|
|
122
|
+
*/
|
|
123
|
+
export function decodePayload(payload) {
|
|
124
|
+
const r = new Reader(payload), outer = Number(r.varint());
|
|
125
|
+
if (outer === ACCOUNT_POLICY_ACTION) {
|
|
126
|
+
const decoded = readAccountPolicy(r);
|
|
127
|
+
r.done();
|
|
128
|
+
if (toHex(encodePayload(decoded)) !== toHex(payload))
|
|
129
|
+
throw new Error("payload is not in canonical form");
|
|
130
|
+
return decoded;
|
|
131
|
+
}
|
|
132
|
+
const name = outer === TOKEN_ACTION ? known(TOKEN_NAME[Number(r.varint())], "token action") : known(VARIANT_NAME[outer], "action variant");
|
|
133
|
+
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"));
|
|
134
|
+
const read = {
|
|
135
|
+
transfer: () => ({ to: str(), amount: amount() }),
|
|
136
|
+
joinValidator: () => ({ validator: str(), stake: amount(), blsPubkey: bytes(), blsPop: bytes() }),
|
|
137
|
+
leaveValidator: () => ({ validator: str() }),
|
|
138
|
+
stake: () => ({ validator: str(), amount: amount() }),
|
|
139
|
+
unstake: () => ({ validator: str(), amount: amount() }),
|
|
140
|
+
registerBlsKey: () => ({ validator: str(), pubkey: bytes(), pop: bytes() }),
|
|
141
|
+
authorizeOperator: () => ({ operator: str() }),
|
|
142
|
+
revokeOperator: () => ({}),
|
|
143
|
+
grantAttestation: () => ({ subject: str(), hash: str(), topics: topics(), jurisdiction: r.option(str) }),
|
|
144
|
+
revokeAttestation: () => ({ subject: str() }),
|
|
145
|
+
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() } }),
|
|
146
|
+
issueAsset: () => ({ asset: str(), amount: amount() }),
|
|
147
|
+
transferAsset: () => ({ asset: str(), to: str(), amount: amount() }),
|
|
148
|
+
freezeAsset: () => ({ asset: str(), frozen: true, reason: str() }),
|
|
149
|
+
unfreezeAsset: () => ({ asset: str(), frozen: false, reason: str() }),
|
|
150
|
+
burnAsset: () => ({ asset: str(), amount: amount() }),
|
|
151
|
+
setHolderFrozen: () => ({ asset: str(), holder: str(), frozen: r.bool() }),
|
|
152
|
+
lockHolderAmount: () => ({ asset: str(), holder: str(), amount: amount(), lock: true }),
|
|
153
|
+
unlockHolderAmount: () => ({ asset: str(), holder: str(), amount: amount(), lock: false }),
|
|
154
|
+
issuerForcedTransfer: () => ({ asset: str(), from: str(), to: str(), amount: amount(), reason: str() }),
|
|
155
|
+
recoverHolder: () => ({ asset: str(), lost: str(), replacement: str() }),
|
|
156
|
+
issueAssetTo: () => ({ asset: str(), to: str(), amount: amount() }),
|
|
157
|
+
setAssetLimits: () => ({ asset: str(), maxHolders: r.option((rr) => Number(rr.varint())), maxBalancePerHolder: r.option(amount), maxAttestationAge: r.option(amount) }),
|
|
158
|
+
verifyClaimProof: () => ({ asset: str(), sub: Array.from({ length: 32 }, () => r.u8()), todayDays: Number(r.varint()), proof: bytes() }),
|
|
159
|
+
setPrivateClaims: () => ({ asset: str(), enabled: r.bool() }),
|
|
160
|
+
submitProposal: () => { const kind = known(GOVERNANCE_NAME[Number(r.varint())], "governance action"), attestor = str(); const action = kind === "addAttestor" ? { kind, attestor, name: str(), owners: r.vec((rr) => rr.string()), threshold: r.u8(), evidenceHash: str(), evidenceUri: str() } : { kind, attestor }; return { action, description: str() }; },
|
|
161
|
+
voteProposal: () => ({ proposal: amount(), approve: r.bool() }),
|
|
162
|
+
executeProposal: () => ({ proposal: amount() }),
|
|
163
|
+
applyAttestor: () => ({ name: str(), owners: r.vec((rr) => rr.string()), threshold: r.u8(), evidenceHash: str(), evidenceUri: str() }),
|
|
164
|
+
blockAttestor: () => ({}),
|
|
165
|
+
createToken: () => ({ symbol: str(), name: str(), decimals: r.u8(), initialSupply: amount(), maxSupply: r.option(amount), mintable: r.bool() }),
|
|
166
|
+
mintToken: () => ({ token: str(), to: str(), amount: amount() }),
|
|
167
|
+
transferToken: () => ({ token: str(), to: str(), amount: amount() }),
|
|
168
|
+
burnToken: () => ({ token: str(), amount: amount() }),
|
|
169
|
+
renounceMint: () => ({ token: str() }),
|
|
170
|
+
};
|
|
171
|
+
const decoded = { name, input: read[name]() };
|
|
172
|
+
r.done();
|
|
173
|
+
if (toHex(encodePayload(decoded)) !== toHex(payload))
|
|
174
|
+
throw new Error("payload is not in canonical form");
|
|
175
|
+
return decoded;
|
|
176
|
+
}
|
|
177
|
+
/** 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. */
|
|
178
|
+
export function signingBytes(genesis, sender, nonce, payload) { if (genesis.length !== 32)
|
|
179
|
+
throw new Error("genesis hash must be 32 bytes"); return new Writer().raw(genesis).string(sender).varint(nonce).raw(payload).bytes(); }
|
|
180
|
+
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))))); }
|
|
181
|
+
export function submitBody(sender, nonce, signature, payload) { return { sender, nonce, signature, payload: Array.from(payload) }; }
|
|
182
|
+
/** 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. */
|
|
183
|
+
export const MAX_MULTISIG_MEMBERS = 16;
|
|
184
|
+
const compareBytes = (a, b) => { for (let i = 0; i < a.length; i++)
|
|
185
|
+
if (a[i] !== b[i])
|
|
186
|
+
return a[i] - b[i]; return 0; };
|
|
187
|
+
function multisigPolicy(threshold, members) { const sorted = [...members].sort(compareBytes); if (!sorted.length || sorted.length > MAX_MULTISIG_MEMBERS)
|
|
188
|
+
throw new Error("member count must be 1..=16"); if (!Number.isInteger(threshold) || threshold < 1 || threshold > sorted.length)
|
|
189
|
+
throw new Error("threshold must be 1..=members"); if (sorted.some((m, i) => m.length !== 32 || (i > 0 && compareBytes(sorted[i - 1], m) >= 0)))
|
|
190
|
+
throw new Error("members must be unique 32-byte keys"); return sorted; }
|
|
191
|
+
async function policyHash(threshold, sorted) { const domain = new TextEncoder().encode("arxium-multisig-v1"), input = new Uint8Array(domain.length + 2 + 32 * sorted.length); input.set(domain); input.set([threshold, sorted.length], domain.length); sorted.forEach((m, i) => input.set(m, domain.length + 2 + 32 * i)); return new Uint8Array(await crypto.subtle.digest("SHA-256", input)); }
|
|
192
|
+
export async function multisigAddress(threshold, members) { const hash = await policyHash(threshold, multisigPolicy(threshold, members)); return encodeAddress(Uint8Array.from([MULTISIG_TAG, ...hash])); }
|
|
193
|
+
/** `signatures`: [member public key, that member's hex signature]. */
|
|
194
|
+
export function multisigSignature(threshold, members, signatures) { const sorted = multisigPolicy(threshold, members); const indexed = signatures.map(([key, sig]) => { const index = sorted.findIndex((m) => compareBytes(m, key) === 0); if (index < 0)
|
|
195
|
+
throw new Error("signer is not a member"); return [index, fromHex(sig)]; }).sort((a, b) => a[0] - b[0]); const out = new Writer().raw(Uint8Array.from([threshold, sorted.length])); sorted.forEach((m) => out.raw(m)); indexed.forEach(([index, sig]) => out.raw(Uint8Array.from([index])).raw(sig)); return toHex(out.bytes()); }
|
|
196
|
+
/** Same checks as `Action::verify_signature` for a multisig sender. */
|
|
197
|
+
export async function verifyMultisig(sender, signature, message) { const sent = decodeAddress(sender), witness = fromHex(signature), [threshold, n] = witness; if (sent.length !== 33 || witness.length !== 2 + 32 * n + 65 * threshold)
|
|
198
|
+
return false; const members = Array.from({ length: n }, (_, i) => witness.slice(2 + 32 * i, 34 + 32 * i)); const hash = await policyHash(threshold, multisigPolicy(threshold, members)); if (compareBytes(hash, sent.slice(1)) !== 0)
|
|
199
|
+
return false; let last = -1; for (let at = 2 + 32 * n; at < witness.length; at += 65) {
|
|
200
|
+
const index = witness[at];
|
|
201
|
+
if (index >= n || index <= last)
|
|
202
|
+
return false;
|
|
203
|
+
last = index;
|
|
204
|
+
const key = await crypto.subtle.importKey("raw", asBuffer(members[index]), "Ed25519", false, ["verify"]);
|
|
205
|
+
if (!await crypto.subtle.verify("Ed25519", key, asBuffer(witness.slice(at + 1, at + 65)), asBuffer(message)))
|
|
206
|
+
return false;
|
|
207
|
+
} return true; }
|
package/dist/amounts.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const IUM_PER_ARX = 1000000000n;
|
|
2
|
+
export function arxToIum(value) { if (!/^(0|[1-9]\d*)(\.\d{1,9})?$/.test(value))
|
|
3
|
+
throw new Error("amount must be a non-negative decimal with at most 9 places"); const [whole, fraction = ""] = value.split("."); return BigInt(whole) * IUM_PER_ARX + BigInt((fraction + "000000000").slice(0, 9)); }
|
|
4
|
+
export function iumToArx(value) { if (value < 0n)
|
|
5
|
+
throw new RangeError("amount cannot be negative"); const whole = value / IUM_PER_ARX, fraction = (value % IUM_PER_ARX).toString().padStart(9, "0").replace(/0+$/, ""); return fraction ? `${whole}.${fraction}` : whole.toString(); }
|
package/dist/bech32.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const HRP = "arx";
|
|
2
|
+
export declare const ASSET_HRP = "arxasset";
|
|
3
|
+
/** A plain address is a 32-byte ed25519 key; a multisig one is 0x01 ‖ sha256(policy), see multisig.ts. */
|
|
4
|
+
export declare const MULTISIG_TAG = 1;
|
|
5
|
+
export declare function encodeAddress(publicKey: Uint8Array): string;
|
|
6
|
+
export declare function decodeAddress(address: string): Uint8Array;
|
|
7
|
+
export declare function isMultisigAddress(address: string): boolean;
|
|
8
|
+
export declare function decodeAssetRef(ref: string): Uint8Array;
|
|
9
|
+
export declare function deriveAssetRef(issuer: string, assetId: string): Promise<string>;
|
package/dist/bech32.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
|
2
|
+
const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
3
|
+
export const HRP = "arx";
|
|
4
|
+
export const ASSET_HRP = "arxasset";
|
|
5
|
+
function polymod(values) { let chk = 1; for (const value of values) {
|
|
6
|
+
const top = chk >>> 25;
|
|
7
|
+
chk = ((chk & 0x1ffffff) << 5) ^ value;
|
|
8
|
+
for (let i = 0; i < 5; i++)
|
|
9
|
+
if ((top >>> i) & 1)
|
|
10
|
+
chk ^= GENERATORS[i];
|
|
11
|
+
} return chk >>> 0; }
|
|
12
|
+
function expand(hrp) { return [...hrp].map((char) => char.charCodeAt(0) >>> 5).concat(0, [...hrp].map((char) => char.charCodeAt(0) & 31)); }
|
|
13
|
+
function convertBits(data, from, to, pad) { let acc = 0, bits = 0; const out = [], max = (1 << to) - 1; for (const byte of Array.from(data)) {
|
|
14
|
+
acc = (acc << from) | byte;
|
|
15
|
+
bits += from;
|
|
16
|
+
while (bits >= to) {
|
|
17
|
+
bits -= to;
|
|
18
|
+
out.push((acc >>> bits) & max);
|
|
19
|
+
}
|
|
20
|
+
} if (pad && bits)
|
|
21
|
+
out.push((acc << (to - bits)) & max);
|
|
22
|
+
else if (!pad && (bits >= from || ((acc << (to - bits)) & max)))
|
|
23
|
+
throw new Error("invalid padding"); return out; }
|
|
24
|
+
function encode32(hrp, bytes) { const data = convertBits(bytes, 8, 5, true); const checksumValue = polymod([...expand(hrp), ...data, 0, 0, 0, 0, 0, 0]) ^ 1; const checksum = Array.from({ length: 6 }, (_, i) => (checksumValue >>> (5 * (5 - i))) & 31); return `${hrp}1${[...data, ...checksum].map((value) => CHARSET[value]).join("")}`; }
|
|
25
|
+
function decode32(hrp, encoded, name) { if (encoded !== encoded.toLowerCase() || !encoded.startsWith(`${hrp}1`))
|
|
26
|
+
throw new Error(`invalid ${name}`); const values = [...encoded.slice(hrp.length + 1)].map((char) => { const value = CHARSET.indexOf(char); if (value < 0)
|
|
27
|
+
throw new Error(`invalid ${name}`); return value; }); if (polymod([...expand(hrp), ...values]) !== 1)
|
|
28
|
+
throw new Error("invalid checksum"); const bytes = Uint8Array.from(convertBits(values.slice(0, -6), 5, 8, false)); return bytes; }
|
|
29
|
+
/** A plain address is a 32-byte ed25519 key; a multisig one is 0x01 ‖ sha256(policy), see multisig.ts. */
|
|
30
|
+
export const MULTISIG_TAG = 0x01;
|
|
31
|
+
function checkAddressBytes(bytes) { if (bytes.length !== 32 && !(bytes.length === 33 && bytes[0] === MULTISIG_TAG))
|
|
32
|
+
throw new Error("invalid address length"); return bytes; }
|
|
33
|
+
function check32(bytes, name) { if (bytes.length !== 32)
|
|
34
|
+
throw new Error(`invalid ${name} length`); return bytes; }
|
|
35
|
+
export function encodeAddress(publicKey) { return encode32(HRP, checkAddressBytes(publicKey)); }
|
|
36
|
+
export function decodeAddress(address) { return checkAddressBytes(decode32(HRP, address, "address")); }
|
|
37
|
+
export function isMultisigAddress(address) { return decodeAddress(address).length === 33; }
|
|
38
|
+
export function decodeAssetRef(ref) { return check32(decode32(ASSET_HRP, ref, "asset ref"), "asset ref"); }
|
|
39
|
+
export async function deriveAssetRef(issuer, assetId) { const pubkey = decodeAddress(issuer), id = new TextEncoder().encode(assetId), domain = new TextEncoder().encode("arxium/asset/v1"), input = new Uint8Array(domain.length + pubkey.length + 1 + id.length); input.set(domain); input.set(pubkey, domain.length); input.set(id, domain.length + pubkey.length + 1); return encode32(ASSET_HRP, new Uint8Array(await crypto.subtle.digest("SHA-256", input))); }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Canonical bincode 2.x `config::standard()` writer for Arxium action payloads. */
|
|
2
|
+
export declare class Writer {
|
|
3
|
+
private chunks;
|
|
4
|
+
bytes(): Uint8Array;
|
|
5
|
+
raw(bytes: Uint8Array | number[]): this;
|
|
6
|
+
u8(value: number): this;
|
|
7
|
+
bool(value: boolean): this;
|
|
8
|
+
varint(value: bigint | number): this;
|
|
9
|
+
private le;
|
|
10
|
+
string(value: string): this;
|
|
11
|
+
option<T>(value: T | null | undefined, write: (writer: Writer, value: T) => void): this;
|
|
12
|
+
vec<T>(values: readonly T[], write: (writer: Writer, value: T) => void): this;
|
|
13
|
+
}
|
|
14
|
+
/** A fresh ArrayBuffer holding exactly `bytes`. Not `bytes.slice().buffer`: Node's Buffer.slice() is a view, so `.buffer` would be the whole shared pool. */
|
|
15
|
+
export declare const asBuffer: (bytes: Uint8Array) => ArrayBuffer;
|
|
16
|
+
export declare function toHex(bytes: Uint8Array): string;
|
|
17
|
+
export declare function fromHex(hex: string): Uint8Array;
|
|
18
|
+
/** Reads what `Writer` writes. Pair every decode with a re-encode check (see `decodePayload`) rather than trusting it to reject non-canonical input. */
|
|
19
|
+
export declare class Reader {
|
|
20
|
+
private readonly data;
|
|
21
|
+
private at;
|
|
22
|
+
constructor(data: Uint8Array);
|
|
23
|
+
u8(): number;
|
|
24
|
+
bool(): boolean;
|
|
25
|
+
varint(): bigint;
|
|
26
|
+
length(): number;
|
|
27
|
+
string(): string;
|
|
28
|
+
option<T>(read: (reader: Reader) => T): T | null;
|
|
29
|
+
vec<T>(read: (reader: Reader) => T): T[];
|
|
30
|
+
done(): void;
|
|
31
|
+
}
|