clanker-wallet 0.4.1 → 0.5.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 +60 -10
- package/SKILL.md +37 -6
- package/bin/cli.js +156 -45
- package/dist/bech32.d.ts +27 -0
- package/dist/bech32.js +111 -0
- package/dist/client.d.ts +10 -1
- package/dist/client.js +68 -17
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/types.d.ts +44 -0
- package/dist/validate.d.ts +13 -2
- package/dist/validate.js +63 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# clanker-wallet
|
|
2
2
|
|
|
3
|
-
Human-approved blockchain transactions for AI agents. Agent proposes a tx, human reviews and signs it from the [web app](https://clanker-wallet.xyz). All communication is end-to-end encrypted.
|
|
3
|
+
Human-approved blockchain transactions for AI agents. Agent proposes a tx, human reviews and signs it from the [web app](https://clanker-wallet.xyz). All communication is end-to-end encrypted. EVM chains take raw transactions; bitcoin and chia take transfers (network, recipient, amount) that the human's wallet builds.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -14,6 +14,8 @@ npx clanker-wallet
|
|
|
14
14
|
bunx clanker-wallet
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Running in a sandbox (e.g. Claude Code)? Allow a WebSocket to the relay, `wss://relay.clanker-wallet.xyz/ws` (the `relay` URL in the pairing JSON), or `tx` fails with exit code 3. If the sandbox only lets traffic out through `HTTPS_PROXY`, run the CLI as `NODE_USE_ENV_PROXY=1 clanker-wallet ...` (Node 24+); Node ignores the proxy otherwise. `pair` writes `~/.clanker-wallet/` and the other commands write there too, so allow writes (see [State](#state)).
|
|
18
|
+
|
|
17
19
|
## Quick Start
|
|
18
20
|
|
|
19
21
|
```bash
|
|
@@ -34,7 +36,9 @@ clanker-wallet status
|
|
|
34
36
|
|
|
35
37
|
### `pair`
|
|
36
38
|
|
|
37
|
-
Pair with a human's web app. Run once — identity and pairing
|
|
39
|
+
Pair with a human's web app. Run once per chain family — `pair` writes identity and pairing to `~/.clanker-wallet/`. The web app's bitcoin and chia tabs give pairing JSON with `"chain_family":"bitcoin"` / `"chain_family":"chia"`; each family is kept in its own file, so pairing one never replaces another.
|
|
40
|
+
|
|
41
|
+
If the pairing JSON contains `"mode":"managed"`, the relay simulates each transaction for the human's review screen, so `tx` also sends the plain transaction fields to it. Otherwise the relay only sees ciphertext. Pair with the JSON as given.
|
|
38
42
|
|
|
39
43
|
```bash
|
|
40
44
|
clanker-wallet pair '<pairing JSON from web app>'
|
|
@@ -43,7 +47,7 @@ clanker-wallet pair '<pairing JSON from web app>'
|
|
|
43
47
|
|
|
44
48
|
### `tx`
|
|
45
49
|
|
|
46
|
-
Send a transaction request. Blocks until the human approves or rejects, for up to 1 hour by default; `--timeout <ms>` changes that.
|
|
50
|
+
Send a transaction request. Blocks until the human approves or rejects, for up to 1 hour by default; `--timeout <ms>` changes that. If your shell tool kills long commands, run `tx` in the background and read its output when it exits.
|
|
47
51
|
|
|
48
52
|
```bash
|
|
49
53
|
clanker-wallet tx \
|
|
@@ -74,13 +78,33 @@ clanker-wallet tx \
|
|
|
74
78
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
75
79
|
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
76
80
|
|
|
81
|
+
### `send`
|
|
82
|
+
|
|
83
|
+
Send bitcoin or chia. The human's wallet builds, signs and broadcasts the transaction; you give the network, recipient and amount. Needs a pairing from the web app's bitcoin or chia tab. Same blocking, `--reason`/`--urgency`/`--expected-outcome`/`--timeout`, output and exit codes as `tx`; exit code 4 means that chain family isn't paired.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
clanker-wallet send --btc testnet --to tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx --sats 1000 --reason "send 1000 sats on bitcoin testnet"
|
|
87
|
+
clanker-wallet send --xch testnet11 --to txch1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sw0amhg --mojos 1000 --fee-mojos 0 --reason "send 1000 mojos on chia testnet11"
|
|
88
|
+
# approved → {"tx_hash":"<bitcoin txid or chia transaction id>","status":"approved"}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Flag | Required | Default | Description |
|
|
92
|
+
|------|----------|---------|-------------|
|
|
93
|
+
| `--btc <network>` | one of `--btc`/`--xch` | — | `mainnet`, `testnet` or `signet` |
|
|
94
|
+
| `--xch <network>` | one of `--btc`/`--xch` | — | `mainnet` or `testnet11` |
|
|
95
|
+
| `--to <address>` | yes | — | On that network: segwit `bc1...`/`tb1...` (no legacy `1...`/`3...`), or `xch1...`/`txch1...` |
|
|
96
|
+
| `--sats <n>` | with `--btc` | — | Whole sats (1 BTC = `100000000`); the wallet sets the fee |
|
|
97
|
+
| `--mojos <n>` | with `--xch` | — | Whole mojos (1 XCH = `1000000000000`) |
|
|
98
|
+
| `--fee-mojos <n>` | no (chia only) | `0` | Chia fee, shown to the human |
|
|
99
|
+
|
|
77
100
|
### `status`
|
|
78
101
|
|
|
79
|
-
Check current pairing status.
|
|
102
|
+
Check current pairing status. `wallet` and `relay` are the evm pairing's; `pairings` lists every paired chain family.
|
|
80
103
|
|
|
81
104
|
```bash
|
|
82
105
|
clanker-wallet status
|
|
83
|
-
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."
|
|
106
|
+
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://...",
|
|
107
|
+
# "pairings":{"evm":{"wallet":"0x...","relay":"wss://...","mode":null,"chain_network":null}}}
|
|
84
108
|
```
|
|
85
109
|
|
|
86
110
|
### `whoami`
|
|
@@ -100,7 +124,7 @@ clanker-wallet whoami
|
|
|
100
124
|
| 1 | Rejected by human |
|
|
101
125
|
| 2 | Timeout |
|
|
102
126
|
| 3 | Connection/relay error |
|
|
103
|
-
| 4 | Not paired |
|
|
127
|
+
| 4 | Not paired (`send`: not paired for that chain family) |
|
|
104
128
|
| 5 | Invalid arguments (bad or missing flag; nothing sent) |
|
|
105
129
|
|
|
106
130
|
## State
|
|
@@ -109,10 +133,15 @@ Identity and pairing are persisted in `~/.clanker-wallet/`:
|
|
|
109
133
|
|
|
110
134
|
```
|
|
111
135
|
~/.clanker-wallet/
|
|
112
|
-
identity.json
|
|
113
|
-
|
|
136
|
+
identity.json # agent keypair (auto-generated on first use)
|
|
137
|
+
pairings/evm.json # per chain family: human's public key + relay URL
|
|
138
|
+
pairings/bitcoin.json
|
|
139
|
+
pairings/chia.json
|
|
140
|
+
pairing.json # written by 0.4.x; still read as the evm pairing, never changed
|
|
114
141
|
```
|
|
115
142
|
|
|
143
|
+
`~/.clanker-wallet/pairings/<family>.json` takes precedence over a 0.4.x `pairing.json`, which is never moved, rewritten or deleted.
|
|
144
|
+
|
|
116
145
|
## Common Chain IDs
|
|
117
146
|
|
|
118
147
|
| Chain | ID |
|
|
@@ -151,6 +180,19 @@ const txHash = await wallet.requestTx({
|
|
|
151
180
|
|
|
152
181
|
`requestTx` throws `InvalidTxError` (with `field` and `reason`) before anything is sent if `chain_id`, `to`, `value` or `data` is malformed.
|
|
153
182
|
|
|
183
|
+
With a bitcoin or chia pairing, use `requestTransfer`; the family comes from the pairing:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// 1000 mojos on chia testnet11; amount and fee are integer strings or bigints
|
|
187
|
+
const txId = await wallet.requestTransfer({
|
|
188
|
+
chain_network: 'testnet11',
|
|
189
|
+
transfer: { asset: 'xch', to: 'txch1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sw0amhg', amount: '1000', fee: '0' },
|
|
190
|
+
context: { reason: 'send 1000 mojos on chia testnet11' },
|
|
191
|
+
})
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
It throws `InvalidTxError` before sending for an evm pairing, an unknown network, a recipient from another network, a non-integer amount, or a bitcoin `fee` (the wallet sets bitcoin fees). Chia `fee` defaults to `"0"`.
|
|
195
|
+
|
|
154
196
|
See full API: [`src/`](./src/)
|
|
155
197
|
|
|
156
198
|
## Python SDK
|
|
@@ -172,14 +214,22 @@ tx_hash = await agent.request_tx(RequestTxOptions(
|
|
|
172
214
|
transaction=Transaction(to="0x000000000000000000000000000000000000dEaD", value="1000000000000000", data="0x"),
|
|
173
215
|
context=TxContext(reason="send 0.001 ETH on base sepolia"),
|
|
174
216
|
))
|
|
217
|
+
|
|
218
|
+
# with a bitcoin pairing: 1000 sats on bitcoin testnet
|
|
219
|
+
from clanker_wallet import RequestTransferOptions, Transfer
|
|
220
|
+
|
|
221
|
+
txid = await agent.request_transfer(RequestTransferOptions(
|
|
222
|
+
chain_network="testnet",
|
|
223
|
+
transfer=Transfer(asset="btc", to="tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx", amount=1000),
|
|
224
|
+
))
|
|
175
225
|
```
|
|
176
226
|
|
|
177
227
|
## How It Works
|
|
178
228
|
|
|
179
229
|
1. Human opens [clanker-wallet.xyz](https://clanker-wallet.xyz), connects wallet, clicks "copy prompt for agent" — gets a paste-ready prompt that bundles intent, the setup guide URL, and the pairing JSON
|
|
180
230
|
2. Human pastes the prompt to their AI agent. Agent extracts the JSON and runs `clanker-wallet pair` — establishes E2E encrypted channel
|
|
181
|
-
3. Agent runs `clanker-wallet tx` — encrypts
|
|
182
|
-
4. Human reviews tx details, simulation, risk flags in the web app. A new agent shows up as unverified with its fingerprint; auto-approve rules only apply after the human trusts it. Contract rules (address + methods) don't limit token amounts or recipients
|
|
231
|
+
3. Agent runs `clanker-wallet tx` (or `send` for bitcoin/chia) — encrypts the request with nacl.box, sends via WebSocket relay
|
|
232
|
+
4. Human reviews tx details, simulation, risk flags in the web app. A new agent shows up as unverified with its fingerprint; auto-approve rules only apply after the human trusts it. Trust applies on every chain family; each chain family has its own policy (rules and remote URL), edited while that chain is selected. Value and daily limits are wei for evm and sats for bitcoin (the sent amount; the wallet's fee isn't counted); a send on another family never matches a rule that sets one, and chia has none. On bitcoin the wallet still confirms each auto-approved send. Contract rules (address + methods) don't limit token amounts or recipients
|
|
183
233
|
5. Human approves → wallet signs + broadcasts → agent gets the tx hash
|
|
184
234
|
|
|
185
235
|
Request and response payloads are encrypted end-to-end; the relay only forwards them. Exception: when paired in managed mode, the agent also sends the transaction fields (from, to, value, data, chainId) to the relay for simulation.
|
package/SKILL.md
CHANGED
|
@@ -16,6 +16,16 @@ npm install -g clanker-wallet
|
|
|
16
16
|
|
|
17
17
|
Or use without installing: `npx clanker-wallet` / `bunx clanker-wallet`
|
|
18
18
|
|
|
19
|
+
### Running in a sandbox
|
|
20
|
+
|
|
21
|
+
If your commands run in a sandbox (e.g. Claude Code), the CLI needs:
|
|
22
|
+
|
|
23
|
+
- **network:** a WebSocket to the relay, `wss://relay.clanker-wallet.xyz/ws` (the `relay` URL in the pairing JSON). Without it `tx` fails with exit code 3.
|
|
24
|
+
- **the sandbox proxy:** if the sandbox only lets traffic out through `HTTPS_PROXY` (Claude Code does), Node ignores it unless you run the CLI as `NODE_USE_ENV_PROXY=1 clanker-wallet ...` (Node 24+). The exit-3 error says so when this applies.
|
|
25
|
+
- **files:** `pair` writes your identity and the pairing to `~/.clanker-wallet/`, and the other commands write there too (they fix file permissions and create the identity if it is missing). Allow writes, not just reads.
|
|
26
|
+
|
|
27
|
+
Ask the human to allow the host and the directory before you pair.
|
|
28
|
+
|
|
19
29
|
## Pairing (one-time)
|
|
20
30
|
|
|
21
31
|
Before you can send transactions, check if you're already paired:
|
|
@@ -41,11 +51,15 @@ clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet
|
|
|
41
51
|
|
|
42
52
|
If the human only pastes raw JSON (no prompt prose), pair with it directly. If they only paste the URL, ask them to click "copy prompt for agent" on the pairing screen.
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
`pair` writes your identity and the pairing to `~/.clanker-wallet/`. You only need to do this once per chain family.
|
|
55
|
+
|
|
56
|
+
The web app has an evm, a bitcoin and a chia tab, and each tab gives its own pairing JSON (bitcoin and chia add `"chain_family":"bitcoin"` / `"chain_family":"chia"`). Pair each one you need: every family is kept in its own file, `~/.clanker-wallet/pairings/<family>.json` (`evm`, `bitcoin`, `chia`), so pairing one never replaces another. A `~/.clanker-wallet/pairing.json` written by clanker-wallet 0.4.x is still read as the evm pairing and is left untouched.
|
|
57
|
+
|
|
58
|
+
If the JSON contains `"mode":"managed"`, the human chose managed mode: the relay simulates each transaction for the review screen, so `tx` also sends the plain transaction fields (from, to, value, data, chainId) to the relay, which shares them with its simulation provider. Without it (or with `"mode":"encrypted"`) the relay only sees ciphertext. Pair with the JSON exactly as given either way.
|
|
45
59
|
|
|
46
60
|
### Trust (first request)
|
|
47
61
|
|
|
48
|
-
The web app lists a new agent as unverified and shows its fingerprint (e.g. `ab12 cd34 ef56 7890`). Until the human compares it with the `fingerprint` from `clanker-wallet whoami` and clicks "trust this agent", none of their auto-approve rules apply: every tx waits for manual approval. Contract rules (contract address + allowed methods) do not limit token amounts or recipients: a rule allowing `transfer` on a token auto-approves any amount to any recipient. If asked, run `clanker-wallet whoami` and share the fingerprint.
|
|
62
|
+
The web app lists a new agent as unverified and shows its fingerprint (e.g. `ab12 cd34 ef56 7890`). Until the human compares it with the `fingerprint` from `clanker-wallet whoami` and clicks "trust this agent", none of their auto-approve rules apply: every tx waits for manual approval. Trust applies on every chain family; each chain family has its own policy (rules and remote URL), edited while that chain is selected. Value and daily limits come in two units: wei limits apply to evm only, sats limits to bitcoin only (they count the sent amount, not the fee the wallet adds); a `send` on any other family never matches a rule that sets one and waits for the human. Chia has no value limits. On bitcoin the wallet still asks the human to confirm each auto-approved `send`. Contract rules (contract address + allowed methods) do not limit token amounts or recipients: a rule allowing `transfer` on a token auto-approves any amount to any recipient. If asked, run `clanker-wallet whoami` and share the fingerprint.
|
|
49
63
|
|
|
50
64
|
### Transaction notifications
|
|
51
65
|
|
|
@@ -79,7 +93,7 @@ clanker-wallet tx \
|
|
|
79
93
|
|
|
80
94
|
This sends 0.001 ETH (`1000000000000000` wei) on Base Sepolia (chain `84532`) to the burn address `0x000000000000000000000000000000000000dEaD`.
|
|
81
95
|
|
|
82
|
-
The command **blocks until the human approves or rejects**, for up to 1 hour by default. Pass `--timeout <ms>` to wait a different time (`600000` = 10 minutes). Output is JSON:
|
|
96
|
+
The command **blocks until the human approves or rejects**, for up to 1 hour by default. Pass `--timeout <ms>` to wait a different time (`600000` = 10 minutes). If your shell tool kills commands after a few minutes, run `tx` in the background and read its output when it exits. Output is JSON:
|
|
83
97
|
- Approved: `{"tx_hash":"0x...","status":"approved"}` (exit code 0)
|
|
84
98
|
- Rejected: `{"status":"rejected","error":"..."}` (exit code 1)
|
|
85
99
|
- Timeout: `{"status":"timeout","error":"..."}` (exit code 2)
|
|
@@ -105,6 +119,21 @@ The command **blocks until the human approves or rejects**, for up to 1 hour by
|
|
|
105
119
|
| `--expected-outcome <text>` | no | — | What will happen |
|
|
106
120
|
| `--timeout <ms>` | no | `3600000` (1 h) | How long `tx` blocks waiting for the human |
|
|
107
121
|
|
|
122
|
+
## Sending Bitcoin or Chia
|
|
123
|
+
|
|
124
|
+
Bitcoin and chia sends are transfers: you name the network, recipient and amount, and the human's wallet builds, signs and broadcasts the transaction. Pair the web app's bitcoin or chia tab first.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
clanker-wallet send --btc testnet --to tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx --sats 1000 --reason "Send 1000 sats on bitcoin testnet"
|
|
128
|
+
clanker-wallet send --xch testnet11 --to txch1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sw0amhg --mojos 1000 --fee-mojos 0 --reason "Send 1000 mojos on chia testnet11"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
- **network:** `--btc mainnet|testnet|signet` or `--xch mainnet|testnet11`. There is no default network; the recipient must belong to it (`bc1...` on bitcoin mainnet, `tb1...` on testnet/signet, `xch1...` / `txch1...` on chia).
|
|
132
|
+
- **bitcoin recipients are segwit addresses only** (`bc1q...`, `bc1p...`, `tb1...`); legacy `1...`/`3...` addresses are refused.
|
|
133
|
+
- **amounts are whole base units**: `--sats` (1 BTC = `100000000`), `--mojos` (1 XCH = `1000000000000`). No decimals.
|
|
134
|
+
- **fees:** bitcoin fees are set in the human's wallet. Chia takes `--fee-mojos` (default `0`), which the human sees before approving.
|
|
135
|
+
- `--reason`, `--urgency`, `--expected-outcome` and `--timeout` work as for `tx`, and so do the output and exit codes. On approval `tx_hash` is the bitcoin txid or the chia transaction id. Exit code 4 means that chain family isn't paired.
|
|
136
|
+
|
|
108
137
|
## Common Chain IDs
|
|
109
138
|
|
|
110
139
|
| Chain | ID |
|
|
@@ -153,7 +182,9 @@ clanker-wallet tx \
|
|
|
153
182
|
```bash
|
|
154
183
|
# Check pairing status
|
|
155
184
|
clanker-wallet status
|
|
156
|
-
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://..."
|
|
185
|
+
# → {"agent_id":"agent_abc123","paired":true,"wallet":"0x...","relay":"wss://...",
|
|
186
|
+
# "pairings":{"evm":{"wallet":"0x...","relay":"wss://...","mode":null,"chain_network":null},"chia":{"wallet":"xch1...",...}}}
|
|
187
|
+
# wallet and relay are the evm pairing's; pairings lists every paired chain family
|
|
157
188
|
|
|
158
189
|
# Show agent identity
|
|
159
190
|
clanker-wallet whoami
|
|
@@ -163,7 +194,7 @@ clanker-wallet whoami
|
|
|
163
194
|
## Troubleshooting
|
|
164
195
|
|
|
165
196
|
- **exit code 5 / "invalid --flag: ..."** — fix the named flag; nothing was sent. Usually a placeholder `--to`, a `--value` in ETH instead of wei, or `--data` that isn't even-length hex
|
|
166
|
-
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first
|
|
197
|
+
- **exit code 4 / "not paired"** — run `clanker-wallet pair '<JSON>'` first (for `send`, with the JSON from the web app's bitcoin or chia tab)
|
|
167
198
|
- **exit code 2 / timeout** — human didn't respond in time, increase `--timeout` or ask them to check the web app
|
|
168
199
|
- **exit code 1 / "rejected"** — human clicked reject. Explain why the tx is needed and try again if appropriate.
|
|
169
|
-
- **exit code 3 / connection error** — relay may be down, try again in a few seconds
|
|
200
|
+
- **exit code 3 / "relay connection error: could not reach wss://..."** — if you run in a sandbox or behind a firewall, allow that host (`relay.clanker-wallet.xyz` by default) and, if the error mentions it, rerun with `NODE_USE_ENV_PROXY=1` (see "Running in a sandbox"); otherwise the relay may be down or rate limiting, try again in a few seconds
|
package/bin/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { ClankerWallet, InvalidTxError, validateTx } from '../dist/index.js'
|
|
2
|
+
import { ClankerWallet, InvalidTxError, validateTransfer, validateTx } from '../dist/index.js'
|
|
3
3
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'fs'
|
|
4
4
|
import { join } from 'path'
|
|
5
5
|
import { homedir } from 'os'
|
|
@@ -7,12 +7,21 @@ import { homedir } from 'os'
|
|
|
7
7
|
// ── constants ────────────────────────────────────────────────────────
|
|
8
8
|
const STATE_DIR = join(homedir(), '.clanker-wallet')
|
|
9
9
|
const IDENTITY_FILE = join(STATE_DIR, 'identity.json')
|
|
10
|
-
|
|
10
|
+
// one pairing per chain family: pairings/<family>.json. 0.4.x wrote pairing.json (evm only); it is still read as the
|
|
11
|
+
// evm pairing and never moved, rewritten or deleted
|
|
12
|
+
const PAIRINGS_DIR = join(STATE_DIR, 'pairings')
|
|
13
|
+
const LEGACY_PAIRING_FILE = join(STATE_DIR, 'pairing.json')
|
|
14
|
+
const FAMILIES = ['evm', 'bitcoin', 'chia']
|
|
11
15
|
|
|
12
16
|
const EXIT = { OK: 0, REJECTED: 1, TIMEOUT: 2, CONNECTION: 3, NOT_PAIRED: 4, BAD_ARGS: 5 }
|
|
13
17
|
const DEFAULT_TIMEOUT_MS = 3_600_000
|
|
14
18
|
/** validateTx field -> the tx flag that sets it */
|
|
15
19
|
const FIELD_FLAGS = { chain_id: '--chain', to: '--to', value: '--value', data: '--data' }
|
|
20
|
+
/** per send network flag (named after the asset it sends): the pairing family it uses and its amount/fee flags */
|
|
21
|
+
const SEND_FAMILIES = {
|
|
22
|
+
btc: { family: 'bitcoin', amountFlag: 'sats', networks: 'mainnet, testnet or signet' },
|
|
23
|
+
xch: { family: 'chia', amountFlag: 'mojos', feeFlag: 'fee-mojos', networks: 'mainnet or testnet11' },
|
|
24
|
+
}
|
|
16
25
|
|
|
17
26
|
// ── helpers ──────────────────────────────────────────────────────────
|
|
18
27
|
const hex = (bytes) => Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
|
@@ -50,15 +59,20 @@ function saveIdentity(data) {
|
|
|
50
59
|
chmodSync(IDENTITY_FILE, 0o600)
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
function
|
|
54
|
-
if (!existsSync(
|
|
55
|
-
try { return JSON.parse(readFileSync(
|
|
62
|
+
function readJson(path) {
|
|
63
|
+
if (!existsSync(path)) return null
|
|
64
|
+
try { return JSON.parse(readFileSync(path, 'utf8')) }
|
|
56
65
|
catch { return null }
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
function
|
|
68
|
+
function loadPairing(family) {
|
|
69
|
+
return readJson(join(PAIRINGS_DIR, `${family}.json`)) ?? (family === 'evm' ? readJson(LEGACY_PAIRING_FILE) : null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function savePairing(family, data) {
|
|
60
73
|
ensureDir()
|
|
61
|
-
|
|
74
|
+
if (!existsSync(PAIRINGS_DIR)) mkdirSync(PAIRINGS_DIR, { mode: 0o700 })
|
|
75
|
+
writeFileSync(join(PAIRINGS_DIR, `${family}.json`), JSON.stringify(data, null, 2) + '\n')
|
|
62
76
|
}
|
|
63
77
|
|
|
64
78
|
/** restore or create a ClankerWallet with persisted identity */
|
|
@@ -114,11 +128,19 @@ function cmdPair(positional) {
|
|
|
114
128
|
die(`pair failed: ${err.message}`, EXIT.BAD_ARGS)
|
|
115
129
|
}
|
|
116
130
|
|
|
117
|
-
|
|
131
|
+
// the family names the pairing file, so only the known ones are accepted
|
|
132
|
+
const family = pairingData.chain_family || 'evm'
|
|
133
|
+
if (!FAMILIES.includes(family)) {
|
|
134
|
+
die(`pair failed: unsupported chain_family ${JSON.stringify(family)} (expected ${FAMILIES.join(', ')})`, EXIT.BAD_ARGS)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
savePairing(family, {
|
|
118
138
|
pubkey: pairingData.pubkey,
|
|
119
139
|
relay: pairingData.relay,
|
|
120
140
|
wallet: pairingData.wallet || null,
|
|
121
141
|
mode: pairingData.mode || null,
|
|
142
|
+
chain_family: pairingData.chain_family,
|
|
143
|
+
chain_network: pairingData.chain_network,
|
|
122
144
|
})
|
|
123
145
|
|
|
124
146
|
out({
|
|
@@ -126,55 +148,44 @@ function cmdPair(positional) {
|
|
|
126
148
|
status: 'paired',
|
|
127
149
|
wallet: pairingData.wallet || null,
|
|
128
150
|
relay: pairingData.relay,
|
|
151
|
+
...(family !== 'evm' && { chain_family: family }),
|
|
129
152
|
})
|
|
130
153
|
}
|
|
131
154
|
|
|
132
|
-
|
|
133
|
-
const pairing = loadPairing()
|
|
134
|
-
if (!pairing) die('not paired — run: clanker-wallet pair \'<JSON>\'', EXIT.NOT_PAIRED)
|
|
135
|
-
|
|
136
|
-
const chain = flags.chain
|
|
137
|
-
const to = flags.to
|
|
138
|
-
if (!chain) die('missing --chain', EXIT.BAD_ARGS)
|
|
139
|
-
if (!to) die('missing --to', EXIT.BAD_ARGS)
|
|
140
|
-
|
|
141
|
-
if (!/^[0-9]+$/.test(chain)) die(`invalid --chain: must be a positive integer chain id, got ${JSON.stringify(chain)}`, EXIT.BAD_ARGS)
|
|
155
|
+
function parseTimeout(flags) {
|
|
142
156
|
if (flags.timeout !== undefined && !/^[1-9][0-9]*$/.test(flags.timeout)) {
|
|
143
157
|
die(`invalid --timeout: must be a positive number of milliseconds, got ${JSON.stringify(flags.timeout)}`, EXIT.BAD_ARGS)
|
|
144
158
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// same checks requestTx runs, done here so a bad flag exits 5 instead of surfacing as a connection error
|
|
148
|
-
let request
|
|
149
|
-
try {
|
|
150
|
-
request = validateTx(Number(chain), { to, value: flags.value ?? '0', data: flags.data ?? '0x' })
|
|
151
|
-
} catch (err) {
|
|
152
|
-
if (!(err instanceof InvalidTxError)) throw err
|
|
153
|
-
die(`invalid ${FIELD_FLAGS[err.field]}: ${err.reason}`, EXIT.BAD_ARGS)
|
|
154
|
-
}
|
|
159
|
+
return flags.timeout ? Number(flags.timeout) : DEFAULT_TIMEOUT_MS
|
|
160
|
+
}
|
|
155
161
|
|
|
162
|
+
function contextFrom(flags) {
|
|
156
163
|
const context = {}
|
|
157
164
|
if (flags.reason) context.reason = flags.reason
|
|
158
165
|
if (flags.urgency) context.urgency = flags.urgency
|
|
159
166
|
if (flags['expected-outcome']) context.expected_outcome = flags['expected-outcome']
|
|
167
|
+
return Object.keys(context).length > 0 ? context : undefined
|
|
168
|
+
}
|
|
160
169
|
|
|
170
|
+
/** a ClankerWallet paired from a saved pairing */
|
|
171
|
+
function pairedWallet(pairing) {
|
|
161
172
|
const wallet = getWallet()
|
|
162
|
-
|
|
173
|
+
wallet.pair(JSON.stringify({
|
|
163
174
|
version: 1,
|
|
164
175
|
pubkey: pairing.pubkey,
|
|
165
176
|
relay: pairing.relay,
|
|
166
177
|
wallet: pairing.wallet,
|
|
167
178
|
mode: pairing.mode || undefined,
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
chain_family: pairing.chain_family,
|
|
180
|
+
chain_network: pairing.chain_network,
|
|
181
|
+
}))
|
|
182
|
+
return wallet
|
|
183
|
+
}
|
|
170
184
|
|
|
185
|
+
/** wait for the human's answer and exit with its code */
|
|
186
|
+
async function exitWithResult(request) {
|
|
171
187
|
try {
|
|
172
|
-
const txHash = await
|
|
173
|
-
chain_id: request.chain_id,
|
|
174
|
-
transaction: request.transaction,
|
|
175
|
-
context: Object.keys(context).length > 0 ? context : undefined,
|
|
176
|
-
timeout_ms: timeout,
|
|
177
|
-
})
|
|
188
|
+
const txHash = await request
|
|
178
189
|
out({ tx_hash: txHash, status: 'approved' })
|
|
179
190
|
process.exit(EXIT.OK)
|
|
180
191
|
} catch (err) {
|
|
@@ -192,15 +203,99 @@ async function cmdTx(flags) {
|
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
205
|
|
|
206
|
+
async function cmdTx(flags) {
|
|
207
|
+
const pairing = loadPairing('evm')
|
|
208
|
+
if (!pairing) die('not paired — run: clanker-wallet pair \'<JSON>\'', EXIT.NOT_PAIRED)
|
|
209
|
+
|
|
210
|
+
const chain = flags.chain
|
|
211
|
+
const to = flags.to
|
|
212
|
+
if (!chain) die('missing --chain', EXIT.BAD_ARGS)
|
|
213
|
+
if (!to) die('missing --to', EXIT.BAD_ARGS)
|
|
214
|
+
|
|
215
|
+
if (!/^[0-9]+$/.test(chain)) die(`invalid --chain: must be a positive integer chain id, got ${JSON.stringify(chain)}`, EXIT.BAD_ARGS)
|
|
216
|
+
const timeout = parseTimeout(flags)
|
|
217
|
+
|
|
218
|
+
// same checks requestTx runs, done here so a bad flag exits 5 instead of surfacing as a connection error
|
|
219
|
+
let request
|
|
220
|
+
try {
|
|
221
|
+
request = validateTx(Number(chain), { to, value: flags.value ?? '0', data: flags.data ?? '0x' })
|
|
222
|
+
} catch (err) {
|
|
223
|
+
if (!(err instanceof InvalidTxError)) throw err
|
|
224
|
+
die(`invalid ${FIELD_FLAGS[err.field]}: ${err.reason}`, EXIT.BAD_ARGS)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const wallet = pairedWallet(pairing)
|
|
228
|
+
await exitWithResult(wallet.requestTx({
|
|
229
|
+
chain_id: request.chain_id,
|
|
230
|
+
transaction: request.transaction,
|
|
231
|
+
context: contextFrom(flags),
|
|
232
|
+
timeout_ms: timeout,
|
|
233
|
+
}))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function cmdSend(flags) {
|
|
237
|
+
const picked = Object.keys(SEND_FAMILIES).filter((flag) => flags[flag] !== undefined)
|
|
238
|
+
if (picked.length !== 1) die('invalid --btc/--xch: give exactly one of --btc <network> or --xch <network>', EXIT.BAD_ARGS)
|
|
239
|
+
const networkFlag = picked[0]
|
|
240
|
+
const { family, amountFlag, feeFlag, networks } = SEND_FAMILIES[networkFlag]
|
|
241
|
+
const network = flags[networkFlag]
|
|
242
|
+
if (network === true) die(`invalid --${networkFlag}: give the network, ${networks}`, EXIT.BAD_ARGS)
|
|
243
|
+
|
|
244
|
+
// another family's amount/fee flag is a mistake, not something to ignore
|
|
245
|
+
for (const flag of ['sats', 'mojos', 'fee-mojos']) {
|
|
246
|
+
if (flag !== amountFlag && flag !== feeFlag && flags[flag] !== undefined) {
|
|
247
|
+
die(`invalid --${flag}: not a --${networkFlag} flag (${family} amounts are --${amountFlag}${feeFlag ? `, fee --${feeFlag}` : ', and the wallet sets the fee'})`, EXIT.BAD_ARGS)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const flag of ['to', amountFlag, feeFlag]) {
|
|
251
|
+
if (flag && flags[flag] === true) die(`invalid --${flag}: missing value`, EXIT.BAD_ARGS)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const pairing = loadPairing(family)
|
|
255
|
+
if (!pairing) die(`not paired with a ${family} wallet — open the web app's ${family} tab and run: clanker-wallet pair '<JSON>'`, EXIT.NOT_PAIRED)
|
|
256
|
+
|
|
257
|
+
if (!flags.to) die('missing --to', EXIT.BAD_ARGS)
|
|
258
|
+
if (flags[amountFlag] === undefined) die(`missing --${amountFlag}`, EXIT.BAD_ARGS)
|
|
259
|
+
const timeout = parseTimeout(flags)
|
|
260
|
+
|
|
261
|
+
const transfer = { asset: networkFlag, to: flags.to, amount: flags[amountFlag] }
|
|
262
|
+
if (feeFlag && flags[feeFlag] !== undefined) transfer.fee = flags[feeFlag]
|
|
263
|
+
const flagFor = { chain_family: networkFlag, chain_network: networkFlag, asset: networkFlag, to: 'to', amount: amountFlag, fee: feeFlag }
|
|
264
|
+
const badArg = (err) => die(`invalid --${flagFor[err.field]}: ${err.reason}`, EXIT.BAD_ARGS)
|
|
265
|
+
|
|
266
|
+
// same checks requestTransfer runs, done here so a bad flag exits 5 instead of surfacing as a connection error
|
|
267
|
+
try {
|
|
268
|
+
validateTransfer(family, network, transfer)
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (!(err instanceof InvalidTxError)) throw err
|
|
271
|
+
badArg(err)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const wallet = pairedWallet(pairing)
|
|
275
|
+
// requestTransfer also refuses a network the pairing isn't for; that is a bad flag too
|
|
276
|
+
const request = wallet.requestTransfer({ chain_network: network, transfer, context: contextFrom(flags), timeout_ms: timeout })
|
|
277
|
+
await request.catch((err) => { if (err instanceof InvalidTxError) badArg(err) })
|
|
278
|
+
await exitWithResult(request)
|
|
279
|
+
}
|
|
280
|
+
|
|
195
281
|
function cmdStatus() {
|
|
196
282
|
const identity = loadIdentity()
|
|
197
|
-
const
|
|
283
|
+
const pairings = {}
|
|
284
|
+
for (const family of FAMILIES) {
|
|
285
|
+
const pairing = loadPairing(family)
|
|
286
|
+
if (pairing) {
|
|
287
|
+
pairings[family] = { wallet: pairing.wallet || null, relay: pairing.relay || null, mode: pairing.mode || null, chain_network: pairing.chain_network || null }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const evm = pairings.evm
|
|
198
291
|
|
|
292
|
+
// wallet/relay stay the evm pairing's, as in 0.4.x
|
|
199
293
|
out({
|
|
200
294
|
agent_id: identity?.agent_id || null,
|
|
201
|
-
paired:
|
|
202
|
-
wallet:
|
|
203
|
-
relay:
|
|
295
|
+
paired: Object.keys(pairings).length > 0,
|
|
296
|
+
wallet: evm?.wallet || null,
|
|
297
|
+
relay: evm?.relay || null,
|
|
298
|
+
pairings,
|
|
204
299
|
})
|
|
205
300
|
}
|
|
206
301
|
|
|
@@ -223,8 +318,9 @@ function showHelp() {
|
|
|
223
318
|
|
|
224
319
|
commands:
|
|
225
320
|
pair '<JSON>' pair with human's web app (one-time setup)
|
|
226
|
-
tx send
|
|
227
|
-
|
|
321
|
+
tx send an evm transaction (blocks until approved/rejected)
|
|
322
|
+
send send btc or xch; the human's wallet builds it (blocks until approved/rejected)
|
|
323
|
+
status check pairing status (one pairing per chain family: evm, bitcoin, chia)
|
|
228
324
|
whoami show agent identity
|
|
229
325
|
|
|
230
326
|
tx flags:
|
|
@@ -237,17 +333,29 @@ tx flags:
|
|
|
237
333
|
--expected-outcome <text> what will happen
|
|
238
334
|
--timeout <ms> how long tx blocks waiting for the human (default: 3600000 = 1 h)
|
|
239
335
|
|
|
336
|
+
send flags (pair the web app's bitcoin or chia tab first):
|
|
337
|
+
send --btc <mainnet|testnet|signet> --to <address> --sats <n>
|
|
338
|
+
send --xch <mainnet|testnet11> --to <address> --mojos <n> [--fee-mojos <n>]
|
|
339
|
+
--to <address> recipient: segwit bc1/tb1 for bitcoin, xch1/txch1 for chia (required)
|
|
340
|
+
--sats <n> bitcoin amount in sats (1 BTC = 100000000); the wallet sets the fee
|
|
341
|
+
--mojos <n> chia amount in mojos (1 XCH = 1000000000000)
|
|
342
|
+
--fee-mojos <n> chia fee in mojos (default: 0), shown to the human
|
|
343
|
+
--reason, --urgency, --expected-outcome, --timeout as for tx
|
|
344
|
+
|
|
240
345
|
exit codes:
|
|
241
346
|
0 success
|
|
242
347
|
1 rejected by human
|
|
243
348
|
2 timeout
|
|
244
|
-
3 connection error
|
|
245
|
-
|
|
349
|
+
3 connection error (in a sandbox or behind a firewall, allow the relay host, wss://relay.clanker-wallet.xyz/ws by default;
|
|
350
|
+
behind HTTPS_PROXY run with NODE_USE_ENV_PROXY=1)
|
|
351
|
+
4 not paired (send: not paired for that chain family)
|
|
246
352
|
5 invalid arguments
|
|
247
353
|
|
|
248
354
|
examples:
|
|
249
355
|
clanker-wallet pair '{"version":1,"pubkey":"ab12...","relay":"wss://...","wallet":"0x..."}'
|
|
250
356
|
clanker-wallet tx --chain 84532 --to 0x000000000000000000000000000000000000dEaD --value 1000000000000000 --reason "send 0.001 ETH on base sepolia"
|
|
357
|
+
clanker-wallet send --btc testnet --to tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx --sats 1000 --reason "test payment"
|
|
358
|
+
clanker-wallet send --xch testnet11 --to txch1qqqsyqcyq5rqwzqfpg9scrgwpugpzysnzs23v9ccrydpk8qarc0sw0amhg --mojos 1000 --fee-mojos 0
|
|
251
359
|
clanker-wallet status
|
|
252
360
|
clanker-wallet whoami
|
|
253
361
|
|
|
@@ -266,6 +374,9 @@ switch (cmd) {
|
|
|
266
374
|
case 'tx':
|
|
267
375
|
await cmdTx(flags)
|
|
268
376
|
break
|
|
377
|
+
case 'send':
|
|
378
|
+
await cmdSend(flags)
|
|
379
|
+
break
|
|
269
380
|
case 'status':
|
|
270
381
|
cmdStatus()
|
|
271
382
|
break
|
package/dist/bech32.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bech32 / Bech32m decoding — a port of the BIP-173 / BIP-350 reference
|
|
3
|
+
* implementation (github.com/sipa/bech32 ref/python/segwit_addr.py).
|
|
4
|
+
* Used to check transfer recipients: bitcoin segwit addresses and chia
|
|
5
|
+
* puzzle-hash addresses (bech32m, hrp xch / txch).
|
|
6
|
+
*/
|
|
7
|
+
export type Bech32Encoding = 'bech32' | 'bech32m';
|
|
8
|
+
/** validate a bech32/bech32m string; returns its hrp, data (checksum stripped) and encoding */
|
|
9
|
+
export declare function bech32Decode(bech: string): {
|
|
10
|
+
hrp: string;
|
|
11
|
+
data: number[];
|
|
12
|
+
encoding: Bech32Encoding;
|
|
13
|
+
} | null;
|
|
14
|
+
/** general power-of-2 base conversion */
|
|
15
|
+
export declare function convertBits(data: number[], fromBits: number, toBits: number, pad: boolean): number[] | null;
|
|
16
|
+
/** decode a segwit address for the given hrp ('bc' mainnet, 'tb' testnet/signet) */
|
|
17
|
+
export declare function decodeSegwitAddress(hrp: string, addr: string): {
|
|
18
|
+
version: number;
|
|
19
|
+
program: number[];
|
|
20
|
+
} | null;
|
|
21
|
+
/**
|
|
22
|
+
* a segwit address a wallet can pay today: v0 p2wpkh/p2wsh (20/32 bytes) or v1 p2tr (32 bytes).
|
|
23
|
+
* other versions and v1 lengths decode but are unspendable or anyone-can-spend, so transfers refuse them
|
|
24
|
+
*/
|
|
25
|
+
export declare function isStandardSegwitAddress(hrp: string, addr: string): boolean;
|
|
26
|
+
/** decode a chia address (bech32m of a 32-byte puzzle hash) for the given hrp ('xch' / 'txch') */
|
|
27
|
+
export declare function decodeChiaAddress(hrp: string, addr: string): number[] | null;
|
package/dist/bech32.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bech32 / Bech32m decoding — a port of the BIP-173 / BIP-350 reference
|
|
3
|
+
* implementation (github.com/sipa/bech32 ref/python/segwit_addr.py).
|
|
4
|
+
* Used to check transfer recipients: bitcoin segwit addresses and chia
|
|
5
|
+
* puzzle-hash addresses (bech32m, hrp xch / txch).
|
|
6
|
+
*/
|
|
7
|
+
const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
|
|
8
|
+
const BECH32M_CONST = 0x2bc830a3;
|
|
9
|
+
const GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
|
|
10
|
+
function polymod(values) {
|
|
11
|
+
let chk = 1;
|
|
12
|
+
for (const value of values) {
|
|
13
|
+
const top = chk >>> 25;
|
|
14
|
+
chk = (((chk & 0x1ffffff) << 5) ^ value) >>> 0;
|
|
15
|
+
for (let i = 0; i < 5; i++) {
|
|
16
|
+
if ((top >>> i) & 1)
|
|
17
|
+
chk = (chk ^ GENERATOR[i]) >>> 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return chk;
|
|
21
|
+
}
|
|
22
|
+
function hrpExpand(hrp) {
|
|
23
|
+
const codes = Array.from(hrp, (c) => c.charCodeAt(0));
|
|
24
|
+
return [...codes.map((c) => c >> 5), 0, ...codes.map((c) => c & 31)];
|
|
25
|
+
}
|
|
26
|
+
/** validate a bech32/bech32m string; returns its hrp, data (checksum stripped) and encoding */
|
|
27
|
+
export function bech32Decode(bech) {
|
|
28
|
+
for (const c of bech) {
|
|
29
|
+
const code = c.charCodeAt(0);
|
|
30
|
+
if (code < 33 || code > 126)
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (bech.toLowerCase() !== bech && bech.toUpperCase() !== bech)
|
|
34
|
+
return null;
|
|
35
|
+
const lower = bech.toLowerCase();
|
|
36
|
+
const pos = lower.lastIndexOf('1');
|
|
37
|
+
if (pos < 1 || pos + 7 > lower.length || lower.length > 90)
|
|
38
|
+
return null;
|
|
39
|
+
const data = [];
|
|
40
|
+
for (const c of lower.slice(pos + 1)) {
|
|
41
|
+
const d = CHARSET.indexOf(c);
|
|
42
|
+
if (d === -1)
|
|
43
|
+
return null;
|
|
44
|
+
data.push(d);
|
|
45
|
+
}
|
|
46
|
+
const hrp = lower.slice(0, pos);
|
|
47
|
+
const check = polymod([...hrpExpand(hrp), ...data]);
|
|
48
|
+
const encoding = check === 1 ? 'bech32' : check === BECH32M_CONST ? 'bech32m' : null;
|
|
49
|
+
if (!encoding)
|
|
50
|
+
return null;
|
|
51
|
+
return { hrp, data: data.slice(0, -6), encoding };
|
|
52
|
+
}
|
|
53
|
+
/** general power-of-2 base conversion */
|
|
54
|
+
export function convertBits(data, fromBits, toBits, pad) {
|
|
55
|
+
let acc = 0;
|
|
56
|
+
let bits = 0;
|
|
57
|
+
const ret = [];
|
|
58
|
+
const maxv = (1 << toBits) - 1;
|
|
59
|
+
const maxAcc = (1 << (fromBits + toBits - 1)) - 1;
|
|
60
|
+
for (const value of data) {
|
|
61
|
+
if (value < 0 || value >> fromBits)
|
|
62
|
+
return null;
|
|
63
|
+
acc = ((acc << fromBits) | value) & maxAcc;
|
|
64
|
+
bits += fromBits;
|
|
65
|
+
while (bits >= toBits) {
|
|
66
|
+
bits -= toBits;
|
|
67
|
+
ret.push((acc >> bits) & maxv);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (pad) {
|
|
71
|
+
if (bits)
|
|
72
|
+
ret.push((acc << (toBits - bits)) & maxv);
|
|
73
|
+
}
|
|
74
|
+
else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return ret;
|
|
78
|
+
}
|
|
79
|
+
/** decode a segwit address for the given hrp ('bc' mainnet, 'tb' testnet/signet) */
|
|
80
|
+
export function decodeSegwitAddress(hrp, addr) {
|
|
81
|
+
const decoded = bech32Decode(addr);
|
|
82
|
+
if (!decoded || decoded.hrp !== hrp || decoded.data.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const [version, ...rest] = decoded.data;
|
|
85
|
+
const program = convertBits(rest, 5, 8, false);
|
|
86
|
+
if (!program || program.length < 2 || program.length > 40)
|
|
87
|
+
return null;
|
|
88
|
+
if (version > 16)
|
|
89
|
+
return null;
|
|
90
|
+
if (version === 0 && program.length !== 20 && program.length !== 32)
|
|
91
|
+
return null;
|
|
92
|
+
if ((version === 0) !== (decoded.encoding === 'bech32'))
|
|
93
|
+
return null;
|
|
94
|
+
return { version, program };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* a segwit address a wallet can pay today: v0 p2wpkh/p2wsh (20/32 bytes) or v1 p2tr (32 bytes).
|
|
98
|
+
* other versions and v1 lengths decode but are unspendable or anyone-can-spend, so transfers refuse them
|
|
99
|
+
*/
|
|
100
|
+
export function isStandardSegwitAddress(hrp, addr) {
|
|
101
|
+
const decoded = decodeSegwitAddress(hrp, addr);
|
|
102
|
+
return !!decoded && (decoded.version === 0 || (decoded.version === 1 && decoded.program.length === 32));
|
|
103
|
+
}
|
|
104
|
+
/** decode a chia address (bech32m of a 32-byte puzzle hash) for the given hrp ('xch' / 'txch') */
|
|
105
|
+
export function decodeChiaAddress(hrp, addr) {
|
|
106
|
+
const decoded = bech32Decode(addr);
|
|
107
|
+
if (!decoded || decoded.hrp !== hrp || decoded.encoding !== 'bech32m')
|
|
108
|
+
return null;
|
|
109
|
+
const puzzleHash = convertBits(decoded.data, 5, 8, false);
|
|
110
|
+
return puzzleHash && puzzleHash.length === 32 ? puzzleHash : null;
|
|
111
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TypedEmitter } from './events.js';
|
|
2
|
-
import type { PairingData, RequestTxOptions } from './types.js';
|
|
2
|
+
import type { PairingData, RequestTransferOptions, RequestTxOptions } from './types.js';
|
|
3
3
|
export interface ClankerWalletOptions {
|
|
4
4
|
/** agent display name (informational) */
|
|
5
5
|
agentName?: string;
|
|
@@ -27,4 +27,13 @@ export declare class ClankerWallet extends TypedEmitter {
|
|
|
27
27
|
* throws on rejection, timeout, or connection error.
|
|
28
28
|
*/
|
|
29
29
|
requestTx(options: RequestTxOptions): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* send a bitcoin / chia transfer to the paired human and wait for their response.
|
|
32
|
+
* the family comes from the pairing; the human's wallet builds, signs and broadcasts
|
|
33
|
+
* the send. resolves with the txid on approval.
|
|
34
|
+
* throws InvalidTxError (before anything is sent), or on rejection, timeout, or connection error.
|
|
35
|
+
*/
|
|
36
|
+
requestTransfer(options: RequestTransferOptions): Promise<string>;
|
|
37
|
+
/** encrypt a signed request to the paired human, send it through the relay, and wait for the answer */
|
|
38
|
+
private send;
|
|
30
39
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex } from './crypto.js';
|
|
2
2
|
import { TypedEmitter } from './events.js';
|
|
3
|
-
import { validateTx } from './validate.js';
|
|
3
|
+
import { InvalidTxError, validateTransfer, validateTx } from './validate.js';
|
|
4
4
|
const DEFAULT_TIMEOUT_MS = 3_600_000; // 1 hour
|
|
5
|
+
const UNREACHABLE_HINT = 'if you run in a sandbox or behind a firewall, allow this host; otherwise the relay may be down or rate limiting, retry in a few seconds';
|
|
6
|
+
/** node's WebSocket ignores HTTPS_PROXY unless NODE_USE_ENV_PROXY is set at startup, and sandboxes (claude code) only let traffic out through that proxy */
|
|
7
|
+
function proxyHint() {
|
|
8
|
+
const env = globalThis.process?.env;
|
|
9
|
+
if (!env || env.NODE_USE_ENV_PROXY)
|
|
10
|
+
return '';
|
|
11
|
+
return env.HTTPS_PROXY || env.https_proxy || env.HTTP_PROXY || env.http_proxy
|
|
12
|
+
? '; HTTPS_PROXY is set but node ignores it, rerun with NODE_USE_ENV_PROXY=1 (node 24+)'
|
|
13
|
+
: '';
|
|
14
|
+
}
|
|
5
15
|
export class ClankerWallet extends TypedEmitter {
|
|
6
16
|
publicKey;
|
|
7
17
|
secretKey;
|
|
@@ -45,6 +55,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
45
55
|
relayUrl: data.relay,
|
|
46
56
|
humanWallet: data.wallet || null,
|
|
47
57
|
mode: data.mode || 'encrypted',
|
|
58
|
+
chainFamily: data.chain_family || null,
|
|
59
|
+
chainNetwork: data.chain_network || null,
|
|
48
60
|
};
|
|
49
61
|
this.emit('paired', { relay: data.relay, wallet: data.wallet || '' });
|
|
50
62
|
return data;
|
|
@@ -58,13 +70,16 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
58
70
|
if (!this.pairing) {
|
|
59
71
|
throw new Error('not paired — call pair() first');
|
|
60
72
|
}
|
|
73
|
+
// a bitcoin or chia pairing takes transfers only: its wallet can't sign an evm tx
|
|
74
|
+
const { chainFamily } = this.pairing;
|
|
75
|
+
if (chainFamily && chainFamily !== 'evm') {
|
|
76
|
+
throw new InvalidTxError('chain_family', `this pairing is for ${chainFamily}: use requestTransfer for bitcoin or chia`);
|
|
77
|
+
}
|
|
61
78
|
// reject malformed requests before anything reaches the relay
|
|
62
79
|
const { chain_id, transaction } = validateTx(options.chain_id, options.transaction);
|
|
63
|
-
const {
|
|
64
|
-
const timeoutMs = options.timeout_ms ?? DEFAULT_TIMEOUT_MS;
|
|
65
|
-
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
80
|
+
const { humanWallet, mode } = this.pairing;
|
|
66
81
|
const txRequest = {
|
|
67
|
-
request_id:
|
|
82
|
+
request_id: newRequestId(),
|
|
68
83
|
agent_id: this.agentId,
|
|
69
84
|
timestamp: new Date().toISOString(),
|
|
70
85
|
chain_id,
|
|
@@ -73,6 +88,46 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
73
88
|
signature: '', // set below after signing
|
|
74
89
|
};
|
|
75
90
|
txRequest.signature = signMessage(JSON.stringify(txRequest), this.signingSecretKey);
|
|
91
|
+
// managed mode: relay gets plaintext tx params so it can simulate
|
|
92
|
+
const simulate = mode === 'managed'
|
|
93
|
+
? { from: humanWallet || '', to: transaction.to, value: transaction.value, data: transaction.data, chainId: chain_id }
|
|
94
|
+
: undefined;
|
|
95
|
+
return this.send(txRequest, options.timeout_ms ?? DEFAULT_TIMEOUT_MS, simulate);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* send a bitcoin / chia transfer to the paired human and wait for their response.
|
|
99
|
+
* the family comes from the pairing; the human's wallet builds, signs and broadcasts
|
|
100
|
+
* the send. resolves with the txid on approval.
|
|
101
|
+
* throws InvalidTxError (before anything is sent), or on rejection, timeout, or connection error.
|
|
102
|
+
*/
|
|
103
|
+
async requestTransfer(options) {
|
|
104
|
+
if (!this.pairing) {
|
|
105
|
+
throw new Error('not paired — call pair() first');
|
|
106
|
+
}
|
|
107
|
+
const { chainFamily, chainNetwork } = this.pairing;
|
|
108
|
+
// reject malformed requests before anything reaches the relay
|
|
109
|
+
const { chain_family, chain_network, transfer } = validateTransfer(chainFamily, options.chain_network, options.transfer);
|
|
110
|
+
if (chainNetwork && chainNetwork !== chain_network) {
|
|
111
|
+
throw new InvalidTxError('chain_network', `this pairing is for ${chain_family} ${chainNetwork}, got "${chain_network}"`);
|
|
112
|
+
}
|
|
113
|
+
const request = {
|
|
114
|
+
request_id: newRequestId(),
|
|
115
|
+
agent_id: this.agentId,
|
|
116
|
+
timestamp: new Date().toISOString(),
|
|
117
|
+
chain_family,
|
|
118
|
+
chain_network,
|
|
119
|
+
transfer,
|
|
120
|
+
context: options.context,
|
|
121
|
+
signature: '', // set below after signing
|
|
122
|
+
};
|
|
123
|
+
request.signature = signMessage(JSON.stringify(request), this.signingSecretKey);
|
|
124
|
+
// the relay simulator is evm-only: no simulate object for transfers, even on a managed pairing
|
|
125
|
+
return this.send(request, options.timeout_ms ?? DEFAULT_TIMEOUT_MS);
|
|
126
|
+
}
|
|
127
|
+
/** encrypt a signed request to the paired human, send it through the relay, and wait for the answer */
|
|
128
|
+
send(request, timeoutMs, simulate) {
|
|
129
|
+
const { humanPubkey, humanPubkeyHex, relayUrl } = this.pairing;
|
|
130
|
+
const requestId = request.request_id;
|
|
76
131
|
return new Promise((resolve, reject) => {
|
|
77
132
|
let ws;
|
|
78
133
|
let timer;
|
|
@@ -103,8 +158,9 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
103
158
|
return;
|
|
104
159
|
}
|
|
105
160
|
ws.addEventListener('error', () => {
|
|
161
|
+
// node's WebSocket error event carries no cause, so name the url: a sandbox or firewall blocking it looks the same as an outage
|
|
106
162
|
if (!settled)
|
|
107
|
-
fail(
|
|
163
|
+
fail(`relay connection error: could not reach ${relayUrl} (${UNREACHABLE_HINT}${proxyHint()})`);
|
|
108
164
|
});
|
|
109
165
|
ws.addEventListener('close', () => {
|
|
110
166
|
this.emit('disconnected', {});
|
|
@@ -128,7 +184,7 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
128
184
|
}
|
|
129
185
|
if (msg.type === 'joined') {
|
|
130
186
|
this.emit('connected', { room: humanPubkeyHex });
|
|
131
|
-
const ciphertext = encryptMessage(JSON.stringify(
|
|
187
|
+
const ciphertext = encryptMessage(JSON.stringify(request), humanPubkey, this.secretKey);
|
|
132
188
|
const envelope = {
|
|
133
189
|
sender_pubkey: this.agentPubkeyHex,
|
|
134
190
|
ciphertext,
|
|
@@ -139,16 +195,8 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
139
195
|
room: humanPubkeyHex,
|
|
140
196
|
payload: JSON.stringify(envelope),
|
|
141
197
|
};
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
wsMsg.simulate = {
|
|
145
|
-
from: humanWallet || '',
|
|
146
|
-
to: transaction.to,
|
|
147
|
-
value: transaction.value,
|
|
148
|
-
data: transaction.data,
|
|
149
|
-
chainId: chain_id,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
198
|
+
if (simulate)
|
|
199
|
+
wsMsg.simulate = simulate;
|
|
152
200
|
ws.send(JSON.stringify(wsMsg));
|
|
153
201
|
this.emit('request_sent', { request_id: requestId });
|
|
154
202
|
return;
|
|
@@ -196,3 +244,6 @@ export class ClankerWallet extends TypedEmitter {
|
|
|
196
244
|
});
|
|
197
245
|
}
|
|
198
246
|
}
|
|
247
|
+
function newRequestId() {
|
|
248
|
+
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
249
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export type { ClankerWalletOptions } from './client.js';
|
|
|
3
3
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
4
4
|
export { TypedEmitter } from './events.js';
|
|
5
5
|
export type { EventMap } from './events.js';
|
|
6
|
-
export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
|
|
6
|
+
export { InvalidTxError, isChecksumAddress, validateTransfer, validateTx } from './validate.js';
|
|
7
7
|
export type { TxField } from './validate.js';
|
|
8
|
-
export type { AgentEnvelope, PairingData, RequestTxOptions, Transaction, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
|
|
8
|
+
export type { AgentEnvelope, PairingData, RequestTransferOptions, RequestTxOptions, Transaction, Transfer, TransferFamily, TransferInput, TransferRequest, TxContext, TxInput, TxRequest, TxResponse, } from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { ClankerWallet } from './client.js';
|
|
2
2
|
export { base64ToBytes, bytesToBase64, decryptMessage, encryptMessage, fromHex, generateKeypair, keypairFromSecret, signMessage, signingKeypairFromSeed, toHex, verifySignature } from './crypto.js';
|
|
3
3
|
export { TypedEmitter } from './events.js';
|
|
4
|
-
export { InvalidTxError, isChecksumAddress, validateTx } from './validate.js';
|
|
4
|
+
export { InvalidTxError, isChecksumAddress, validateTransfer, validateTx } from './validate.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -48,6 +48,10 @@ export interface PairingData {
|
|
|
48
48
|
wallet: string;
|
|
49
49
|
/** "encrypted" (default): relay sees only ciphertext. "managed": relay also receives tx params for simulation. */
|
|
50
50
|
mode?: 'encrypted' | 'managed';
|
|
51
|
+
/** "bitcoin" or "chia" for a non-evm wallet; absent for evm */
|
|
52
|
+
chain_family?: string;
|
|
53
|
+
/** the wallet's network, when the pairing names one */
|
|
54
|
+
chain_network?: string;
|
|
51
55
|
}
|
|
52
56
|
/** options for requestTx */
|
|
53
57
|
export interface RequestTxOptions {
|
|
@@ -58,3 +62,43 @@ export interface RequestTxOptions {
|
|
|
58
62
|
/** timeout in ms (default: 3_600_000 = 1hr) */
|
|
59
63
|
timeout_ms?: number;
|
|
60
64
|
}
|
|
65
|
+
/** wallet families a transfer can go to */
|
|
66
|
+
export type TransferFamily = 'bitcoin' | 'chia';
|
|
67
|
+
/** an intent-level send: the human's wallet builds and signs it */
|
|
68
|
+
export interface Transfer {
|
|
69
|
+
asset: 'btc' | 'xch';
|
|
70
|
+
/** bech32 / bech32m address on the request's network */
|
|
71
|
+
to: string;
|
|
72
|
+
/** integer string: sats for btc, mojos for xch */
|
|
73
|
+
amount: string;
|
|
74
|
+
/** chia only, in mojos (defaults to "0"); bitcoin wallets set their own fee */
|
|
75
|
+
fee?: string;
|
|
76
|
+
}
|
|
77
|
+
/** transfer as passed to requestTransfer: amounts as decimal strings or bigints */
|
|
78
|
+
export interface TransferInput {
|
|
79
|
+
asset: 'btc' | 'xch';
|
|
80
|
+
to: string;
|
|
81
|
+
amount: string | bigint;
|
|
82
|
+
fee?: string | bigint;
|
|
83
|
+
}
|
|
84
|
+
/** bitcoin / chia request sent to the human: no chain_id, no transaction */
|
|
85
|
+
export interface TransferRequest {
|
|
86
|
+
request_id: string;
|
|
87
|
+
agent_id: string;
|
|
88
|
+
timestamp: string;
|
|
89
|
+
chain_family: TransferFamily;
|
|
90
|
+
chain_network: string;
|
|
91
|
+
transfer: Transfer;
|
|
92
|
+
context?: TxContext;
|
|
93
|
+
signature: string;
|
|
94
|
+
}
|
|
95
|
+
/** options for requestTransfer; the family comes from the pairing */
|
|
96
|
+
export interface RequestTransferOptions {
|
|
97
|
+
/** bitcoin: mainnet | testnet | signet; chia: mainnet | testnet11 */
|
|
98
|
+
chain_network: string;
|
|
99
|
+
/** checked before sending; an invalid field throws InvalidTxError */
|
|
100
|
+
transfer: TransferInput;
|
|
101
|
+
context?: TxContext;
|
|
102
|
+
/** timeout in ms (default: 3_600_000 = 1hr) */
|
|
103
|
+
timeout_ms?: number;
|
|
104
|
+
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Transaction, TxInput } from './types.js';
|
|
2
|
-
export type TxField = 'chain_id' | 'to' | 'value' | 'data';
|
|
1
|
+
import type { Transaction, Transfer, TransferFamily, TransferInput, TxInput } from './types.js';
|
|
2
|
+
export type TxField = 'chain_id' | 'to' | 'value' | 'data' | 'chain_family' | 'chain_network' | 'asset' | 'amount' | 'fee';
|
|
3
3
|
/** a tx request field failed validation; nothing was sent */
|
|
4
4
|
export declare class InvalidTxError extends Error {
|
|
5
5
|
readonly field: TxField;
|
|
@@ -17,3 +17,14 @@ export declare function validateTx(chainId: unknown, tx: TxInput | Record<string
|
|
|
17
17
|
chain_id: number;
|
|
18
18
|
transaction: Transaction;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* check a bitcoin / chia transfer before anything is signed or sent, and normalize it
|
|
22
|
+
* to the wire format (amounts as decimal strings, chia fee defaulting to "0").
|
|
23
|
+
* the recipient must decode as an address on `chainNetwork`. as strict as the web app,
|
|
24
|
+
* so a request it would refuse never leaves the agent. throws InvalidTxError.
|
|
25
|
+
*/
|
|
26
|
+
export declare function validateTransfer(chainFamily: unknown, chainNetwork: unknown, transfer: TransferInput | Record<string, unknown>): {
|
|
27
|
+
chain_family: TransferFamily;
|
|
28
|
+
chain_network: string;
|
|
29
|
+
transfer: Transfer;
|
|
30
|
+
};
|
package/dist/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decodeChiaAddress, isStandardSegwitAddress } from './bech32.js';
|
|
1
2
|
import { keccak256 } from './keccak.js';
|
|
2
3
|
/** a tx request field failed validation; nothing was sent */
|
|
3
4
|
export class InvalidTxError extends Error {
|
|
@@ -63,3 +64,65 @@ export function validateTx(chainId, tx) {
|
|
|
63
64
|
}
|
|
64
65
|
return { chain_id: chainId, transaction: { to, value: wei, data } };
|
|
65
66
|
}
|
|
67
|
+
const TRANSFER_ASSET = { bitcoin: 'btc', chia: 'xch' };
|
|
68
|
+
/** address hrp per network; the web app checks the same table */
|
|
69
|
+
const ADDRESS_HRP = {
|
|
70
|
+
bitcoin: { mainnet: 'bc', testnet: 'tb', signet: 'tb' },
|
|
71
|
+
chia: { mainnet: 'xch', testnet11: 'txch' },
|
|
72
|
+
};
|
|
73
|
+
const NETWORK_NAMES = {
|
|
74
|
+
bitcoin: 'mainnet, testnet or signet',
|
|
75
|
+
chia: 'mainnet or testnet11',
|
|
76
|
+
};
|
|
77
|
+
const POSITIVE_INT_RE = /^[1-9][0-9]*$/;
|
|
78
|
+
/** a decimal string or bigint amount as a decimal string, or null */
|
|
79
|
+
function units(v, allowZero) {
|
|
80
|
+
if (typeof v === 'bigint')
|
|
81
|
+
return v > 0n || (allowZero && v === 0n) ? v.toString() : null;
|
|
82
|
+
if (typeof v !== 'string')
|
|
83
|
+
return null;
|
|
84
|
+
return POSITIVE_INT_RE.test(v) || (allowZero && v === '0') ? v : null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* check a bitcoin / chia transfer before anything is signed or sent, and normalize it
|
|
88
|
+
* to the wire format (amounts as decimal strings, chia fee defaulting to "0").
|
|
89
|
+
* the recipient must decode as an address on `chainNetwork`. as strict as the web app,
|
|
90
|
+
* so a request it would refuse never leaves the agent. throws InvalidTxError.
|
|
91
|
+
*/
|
|
92
|
+
export function validateTransfer(chainFamily, chainNetwork, transfer) {
|
|
93
|
+
if (chainFamily !== 'bitcoin' && chainFamily !== 'chia') {
|
|
94
|
+
throw new InvalidTxError('chain_family', `transfers need a bitcoin or chia pairing, got ${shown(chainFamily ?? 'evm')}`);
|
|
95
|
+
}
|
|
96
|
+
const family = chainFamily;
|
|
97
|
+
const hrp = typeof chainNetwork === 'string' ? ADDRESS_HRP[family][chainNetwork] : undefined;
|
|
98
|
+
if (!hrp)
|
|
99
|
+
throw new InvalidTxError('chain_network', `must be ${NETWORK_NAMES[family]}, got ${shown(chainNetwork)}`);
|
|
100
|
+
const network = chainNetwork;
|
|
101
|
+
const asset = TRANSFER_ASSET[family];
|
|
102
|
+
if (transfer.asset !== asset)
|
|
103
|
+
throw new InvalidTxError('asset', `${family} transfers must be ${shown(asset)}, got ${shown(transfer.asset)}`);
|
|
104
|
+
const unit = family === 'bitcoin' ? 'sats' : 'mojos';
|
|
105
|
+
const amount = units(transfer.amount, false);
|
|
106
|
+
if (!amount)
|
|
107
|
+
throw new InvalidTxError('amount', `must be a positive integer amount of ${unit} (decimal string or bigint), got ${shown(transfer.amount)}`);
|
|
108
|
+
const { to } = transfer;
|
|
109
|
+
if (family === 'bitcoin') {
|
|
110
|
+
if (transfer.fee !== undefined)
|
|
111
|
+
throw new InvalidTxError('fee', 'bitcoin transfers take no fee; the wallet sets it');
|
|
112
|
+
// the wallet api takes sats as a js number
|
|
113
|
+
if (BigInt(amount) > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
114
|
+
throw new InvalidTxError('amount', `must be at most ${Number.MAX_SAFE_INTEGER} sats, got ${shown(amount)}`);
|
|
115
|
+
}
|
|
116
|
+
if (typeof to !== 'string' || !isStandardSegwitAddress(hrp, to)) {
|
|
117
|
+
throw new InvalidTxError('to', `not a bitcoin ${network} address (segwit, ${hrp}1...), got ${shown(to)}`);
|
|
118
|
+
}
|
|
119
|
+
return { chain_family: family, chain_network: network, transfer: { asset, to, amount } };
|
|
120
|
+
}
|
|
121
|
+
const fee = transfer.fee === undefined ? '0' : units(transfer.fee, true);
|
|
122
|
+
if (!fee)
|
|
123
|
+
throw new InvalidTxError('fee', `must be a non-negative integer amount of mojos (decimal string or bigint), got ${shown(transfer.fee)}`);
|
|
124
|
+
if (typeof to !== 'string' || !decodeChiaAddress(hrp, to)) {
|
|
125
|
+
throw new InvalidTxError('to', `not a chia ${network} address (${hrp}1...), got ${shown(to)}`);
|
|
126
|
+
}
|
|
127
|
+
return { chain_family: family, chain_network: network, transfer: { asset, to, amount, fee } };
|
|
128
|
+
}
|