@vurto/sign-tx 0.1.0-stage.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -0
- package/bin/sign-tx.js +9 -0
- package/package.json +42 -0
- package/src/cli.js +238 -0
- package/src/contractAllowList.js +94 -0
- package/src/decodeAbi.js +198 -0
- package/src/openBrowser.js +33 -0
- package/src/server.js +139 -0
- package/src/signerHtml.js +636 -0
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @vurto/sign-tx
|
|
2
|
+
|
|
3
|
+
Local browser-signer CLI for the Vurto AnA MCP hard-wallet flow.
|
|
4
|
+
|
|
5
|
+
Your AI agent never sees your private key. When an MCP tool prepares a transaction (supply, repay, withdraw, borrow, swap, zap, …), it returns the calldata plus a short summary. Pipe that into `sign-tx`, and this CLI spins up a **single-use local page on `127.0.0.1`** that connects to your MetaMask / Rabby / Ledger / Trezor and asks **you** to sign. The transaction hash is printed to stdout so the agent can pick it back up and continue the workflow.
|
|
6
|
+
|
|
7
|
+
The page works fully offline from Vurto's network — every asset is inlined, the Content-Security-Policy denies all external loads.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @vurto/sign-tx
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
You can also run it ad hoc without installing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx @vurto/sign-tx --tx '<json>'
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
sign-tx --tx '{
|
|
25
|
+
"txData": {
|
|
26
|
+
"to": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5",
|
|
27
|
+
"data": "0x617ba037...",
|
|
28
|
+
"chainId": 8453,
|
|
29
|
+
"value": "0x0"
|
|
30
|
+
},
|
|
31
|
+
"summary": {
|
|
32
|
+
"action": "supply",
|
|
33
|
+
"humanDescription": "Supply 100 USDC on Base to Aave V3"
|
|
34
|
+
},
|
|
35
|
+
"walletAddress": "0xYourWalletHere",
|
|
36
|
+
"transactionId": 17
|
|
37
|
+
}'
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
What happens:
|
|
41
|
+
|
|
42
|
+
1. `sign-tx` binds an HTTP server on `127.0.0.1` (free port chosen by the OS).
|
|
43
|
+
2. The system browser opens the signer page.
|
|
44
|
+
3. You connect MetaMask / Rabby (the page validates wallet and chain).
|
|
45
|
+
4. You inspect the decoded calldata, gas estimate, and destination contract.
|
|
46
|
+
5. You click **Sign** (or **Reject**).
|
|
47
|
+
6. `sign-tx` prints the result JSON to stdout and exits.
|
|
48
|
+
|
|
49
|
+
### Result on stdout
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"txHash": "0xabc...",
|
|
54
|
+
"from": "0xYour...",
|
|
55
|
+
"chainId": 8453,
|
|
56
|
+
"signedAt": "2026-05-27T12:34:56.000Z"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Exit codes
|
|
61
|
+
|
|
62
|
+
| Code | Meaning |
|
|
63
|
+
|------|--------------------------------------------|
|
|
64
|
+
| 0 | Transaction signed and broadcast |
|
|
65
|
+
| 1 | User rejected the request |
|
|
66
|
+
| 2 | Timed out waiting for approval |
|
|
67
|
+
| 3 | Validation error (bad flags or bad JSON) |
|
|
68
|
+
|
|
69
|
+
## Flags
|
|
70
|
+
|
|
71
|
+
| Flag | Default | Description |
|
|
72
|
+
|-----------------------|---------|-------------|
|
|
73
|
+
| `--tx <json>` | — | Inline JSON payload. Required unless you pipe via stdin. |
|
|
74
|
+
| `--port <n>` | `0` | TCP port on `127.0.0.1`. `0` = OS picks a free port. |
|
|
75
|
+
| `--no-open` | off | Do not auto-open the browser; just print the URL on stderr. |
|
|
76
|
+
| `--timeout <seconds>` | `180` | Approval timeout. After this, the CLI exits with code 2. |
|
|
77
|
+
| `--report-to <url>` | — | Optional HTTPS endpoint to POST the result to (e.g. an MCP webhook). |
|
|
78
|
+
| `--help` | — | Print help. |
|
|
79
|
+
| `--version` | — | Print version. |
|
|
80
|
+
|
|
81
|
+
You can also pipe the JSON instead of using `--tx`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
cat tx.json | sign-tx --no-open
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Safety mitigations
|
|
88
|
+
|
|
89
|
+
- **127.0.0.1 only.** The HTTP server never binds to a public interface.
|
|
90
|
+
- **Single-use session.** A random session token is required for `POST /result`. After a result is delivered, every route returns 410 Gone.
|
|
91
|
+
- **Strict CSP.** `default-src 'none'`; scripts and styles whitelisted by nonce; no external loads possible. No cross-origin frames, no form submissions.
|
|
92
|
+
- **Allow-list of destinations.** Vurto Dual-Aave Adapter, Aave V3 Pool, ParaSwap Augustus V6.2 and TokenTransferProxy across 9 chains. Unknown destinations show a red banner; you can still sign, but only after acknowledging it.
|
|
93
|
+
- **Wallet match.** The connected `eth_accounts[0]` must equal the expected `walletAddress`. Sign is disabled otherwise.
|
|
94
|
+
- **Chain match.** The wallet's current `chainId` must equal the payload's `chainId`. `sign-tx` triggers `wallet_switchEthereumChain` automatically; if you reject the switch, Sign stays disabled.
|
|
95
|
+
- **Unlimited-approval guard.** `approve(spender, MAX_UINT256)` (or any amount > 1B raw units of a known stablecoin) shows a red card with the spender address and forces you to tick "I understand I'm approving an unlimited amount of {TOKEN}" before Sign enables.
|
|
96
|
+
- **Decoded calldata first, raw second.** The page leads with the human-readable summary and decoded named parameters. The raw `to`/`data`/`value` triplet lives behind a collapsible.
|
|
97
|
+
|
|
98
|
+
## Sigstore verification
|
|
99
|
+
|
|
100
|
+
Releases are signed with [sigstore/cosign](https://docs.sigstore.dev/). To verify a published tarball:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# Download the tarball + bundle from the GitHub release page, then:
|
|
104
|
+
cosign verify-blob \
|
|
105
|
+
--bundle vurto-sign-tx-0.1.0.tgz.sigstore \
|
|
106
|
+
--certificate-identity-regexp 'https://github\.com/vurto-cc/vurto-a/\.github/workflows/.*' \
|
|
107
|
+
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
|
|
108
|
+
vurto-sign-tx-0.1.0.tgz
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A successful verify means the tarball was built by the canonical GitHub Actions workflow in `vurto-cc/vurto-a` — no human can sneak in a malicious build.
|
|
112
|
+
|
|
113
|
+
## Dev usage from a checkout
|
|
114
|
+
|
|
115
|
+
If you're hacking on this CLI inside the `vurto-a` repo:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
cd tools/sign-cli
|
|
119
|
+
npm install
|
|
120
|
+
node bin/sign-tx.js --help
|
|
121
|
+
node --test tests/
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
A handy smoke test (will time out after 5s with exit code 2):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node bin/sign-tx.js --tx '{
|
|
128
|
+
"txData": {
|
|
129
|
+
"to": "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5",
|
|
130
|
+
"data": "0x617ba037000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e58310000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000001234567890123456789012345678901234567890000000000000000000000000000000000000000000000000000000000000000a",
|
|
131
|
+
"chainId": 42161,
|
|
132
|
+
"value": "0x0"
|
|
133
|
+
},
|
|
134
|
+
"summary": { "action": "supply", "humanDescription": "Supply 100 USDC on Arbitrum to Aave V3" },
|
|
135
|
+
"walletAddress": "0x1234567890123456789012345678901234567890",
|
|
136
|
+
"transactionId": 1
|
|
137
|
+
}' --no-open --timeout 5
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT.
|
package/bin/sign-tx.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { run } from '../src/cli.js';
|
|
3
|
+
|
|
4
|
+
run(process.argv.slice(2)).then((code) => {
|
|
5
|
+
process.exit(code);
|
|
6
|
+
}).catch((err) => {
|
|
7
|
+
process.stderr.write(`sign-tx: fatal: ${err?.stack || err?.message || String(err)}\n`);
|
|
8
|
+
process.exit(3);
|
|
9
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vurto/sign-tx",
|
|
3
|
+
"version": "0.1.0-stage.1",
|
|
4
|
+
"description": "Vurto AnA local browser-signer CLI: opens a single-use page on 127.0.0.1 that lets you sign a prepared transaction with MetaMask/Rabby and returns the tx hash to stdout.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sign-tx": "./bin/sign-tx.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"src/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node --test tests/",
|
|
20
|
+
"check": "node --check bin/sign-tx.js"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"ethers": "^6.13.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"vurto",
|
|
27
|
+
"aave",
|
|
28
|
+
"ana",
|
|
29
|
+
"mcp",
|
|
30
|
+
"signer",
|
|
31
|
+
"ethereum",
|
|
32
|
+
"metamask",
|
|
33
|
+
"rabby",
|
|
34
|
+
"hardware-wallet"
|
|
35
|
+
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/vurto-cc/vurto-a.git",
|
|
39
|
+
"directory": "tools/sign-cli"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://vurto.cc"
|
|
42
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { startSignerServer } from './server.js';
|
|
2
|
+
import { openBrowser } from './openBrowser.js';
|
|
3
|
+
|
|
4
|
+
const USAGE = `@vurto/sign-tx — sign a prepared transaction in your local browser with MetaMask/Rabby.
|
|
5
|
+
|
|
6
|
+
USAGE
|
|
7
|
+
sign-tx --tx '<json>' [options]
|
|
8
|
+
echo '<json>' | sign-tx [options]
|
|
9
|
+
|
|
10
|
+
OPTIONS
|
|
11
|
+
--tx <json> Inline transaction payload (JSON). If omitted, read from stdin.
|
|
12
|
+
--port <number> TCP port to bind on 127.0.0.1 (default: 0 = OS-assigned).
|
|
13
|
+
--no-open Do not auto-open the system browser; just print the URL.
|
|
14
|
+
--timeout <seconds> Approval timeout in seconds (default: 180).
|
|
15
|
+
--report-to <url> Optional HTTPS endpoint that receives a POST with the result.
|
|
16
|
+
--version Print version and exit.
|
|
17
|
+
--help Print this help and exit.
|
|
18
|
+
|
|
19
|
+
EXIT CODES
|
|
20
|
+
0 transaction signed and broadcast
|
|
21
|
+
1 user rejected
|
|
22
|
+
2 timeout
|
|
23
|
+
3 validation error
|
|
24
|
+
|
|
25
|
+
EXAMPLES
|
|
26
|
+
sign-tx --tx "$(cat tx.json)"
|
|
27
|
+
cat tx.json | sign-tx --no-open --timeout 60
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
export function parseArgs(argv) {
|
|
31
|
+
const out = {
|
|
32
|
+
tx: null,
|
|
33
|
+
port: 0,
|
|
34
|
+
open: true,
|
|
35
|
+
timeout: 180,
|
|
36
|
+
reportTo: null,
|
|
37
|
+
help: false,
|
|
38
|
+
version: false,
|
|
39
|
+
};
|
|
40
|
+
const errors = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 0; i < argv.length; i++) {
|
|
43
|
+
const a = argv[i];
|
|
44
|
+
switch (a) {
|
|
45
|
+
case '--help':
|
|
46
|
+
case '-h':
|
|
47
|
+
out.help = true;
|
|
48
|
+
break;
|
|
49
|
+
case '--version':
|
|
50
|
+
case '-v':
|
|
51
|
+
out.version = true;
|
|
52
|
+
break;
|
|
53
|
+
case '--no-open':
|
|
54
|
+
out.open = false;
|
|
55
|
+
break;
|
|
56
|
+
case '--tx': {
|
|
57
|
+
const v = argv[++i];
|
|
58
|
+
if (v === undefined) { errors.push('--tx requires a JSON string argument'); break; }
|
|
59
|
+
out.tx = v;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
case '--port': {
|
|
63
|
+
const v = argv[++i];
|
|
64
|
+
const n = Number(v);
|
|
65
|
+
if (!Number.isInteger(n) || n < 0 || n > 65535) {
|
|
66
|
+
errors.push(`--port must be an integer 0..65535 (got: ${v})`);
|
|
67
|
+
} else {
|
|
68
|
+
out.port = n;
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case '--timeout': {
|
|
73
|
+
const v = argv[++i];
|
|
74
|
+
const n = Number(v);
|
|
75
|
+
if (!Number.isFinite(n) || n <= 0 || n > 3600) {
|
|
76
|
+
errors.push(`--timeout must be a number 1..3600 seconds (got: ${v})`);
|
|
77
|
+
} else {
|
|
78
|
+
out.timeout = n;
|
|
79
|
+
}
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case '--report-to': {
|
|
83
|
+
const v = argv[++i];
|
|
84
|
+
if (v === undefined) { errors.push('--report-to requires a URL argument'); break; }
|
|
85
|
+
if (!/^https?:\/\//i.test(v)) {
|
|
86
|
+
errors.push(`--report-to must be an http(s) URL (got: ${v})`);
|
|
87
|
+
} else {
|
|
88
|
+
out.reportTo = v;
|
|
89
|
+
}
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
default:
|
|
93
|
+
if (a.startsWith('--')) errors.push(`unknown option: ${a}`);
|
|
94
|
+
else errors.push(`unexpected positional argument: ${a}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { opts: out, errors };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const TX_HASH_RE = /^0x[0-9a-f]{64}$/i;
|
|
102
|
+
const ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
103
|
+
const HEX_RE = /^0x[0-9a-fA-F]*$/;
|
|
104
|
+
|
|
105
|
+
export function validatePayload(payload) {
|
|
106
|
+
const errors = [];
|
|
107
|
+
if (!payload || typeof payload !== 'object') {
|
|
108
|
+
return ['payload must be a JSON object'];
|
|
109
|
+
}
|
|
110
|
+
const { txData, summary, walletAddress, transactionId } = payload;
|
|
111
|
+
if (!txData || typeof txData !== 'object') errors.push('payload.txData is required');
|
|
112
|
+
else {
|
|
113
|
+
if (!ADDR_RE.test(String(txData.to || ''))) errors.push('payload.txData.to must be a 0x-prefixed 40-hex address');
|
|
114
|
+
if (typeof txData.data !== 'string' || !HEX_RE.test(txData.data)) errors.push('payload.txData.data must be a hex string');
|
|
115
|
+
if (!Number.isInteger(txData.chainId) || txData.chainId <= 0) errors.push('payload.txData.chainId must be a positive integer');
|
|
116
|
+
if (txData.value !== undefined && typeof txData.value !== 'string') errors.push('payload.txData.value must be a hex string when present');
|
|
117
|
+
}
|
|
118
|
+
if (!summary || typeof summary !== 'object') errors.push('payload.summary is required');
|
|
119
|
+
else if (typeof summary.humanDescription !== 'string' || !summary.humanDescription.trim()) {
|
|
120
|
+
errors.push('payload.summary.humanDescription must be a non-empty string');
|
|
121
|
+
}
|
|
122
|
+
if (!ADDR_RE.test(String(walletAddress || ''))) errors.push('payload.walletAddress must be a 0x-prefixed 40-hex address');
|
|
123
|
+
if (transactionId !== undefined && transactionId !== null && !(typeof transactionId === 'string' || Number.isFinite(transactionId))) {
|
|
124
|
+
errors.push('payload.transactionId must be a string or number when present');
|
|
125
|
+
}
|
|
126
|
+
return errors;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function readStdin() {
|
|
130
|
+
if (process.stdin.isTTY) return '';
|
|
131
|
+
return await new Promise((resolve, reject) => {
|
|
132
|
+
let buf = '';
|
|
133
|
+
process.stdin.setEncoding('utf8');
|
|
134
|
+
process.stdin.on('data', (chunk) => { buf += chunk; });
|
|
135
|
+
process.stdin.on('end', () => resolve(buf));
|
|
136
|
+
process.stdin.on('error', reject);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function run(argv, deps = {}) {
|
|
141
|
+
const stdout = deps.stdout || process.stdout;
|
|
142
|
+
const stderr = deps.stderr || process.stderr;
|
|
143
|
+
const startServerFn = deps.startSignerServer || startSignerServer;
|
|
144
|
+
const openBrowserFn = deps.openBrowser || openBrowser;
|
|
145
|
+
const readStdinFn = deps.readStdin || readStdin;
|
|
146
|
+
|
|
147
|
+
const { opts, errors } = parseArgs(argv);
|
|
148
|
+
if (errors.length) {
|
|
149
|
+
for (const e of errors) stderr.write(`sign-tx: ${e}\n`);
|
|
150
|
+
stderr.write(`\n${USAGE}`);
|
|
151
|
+
return 3;
|
|
152
|
+
}
|
|
153
|
+
if (opts.help) { stdout.write(USAGE); return 0; }
|
|
154
|
+
if (opts.version) {
|
|
155
|
+
stdout.write(`@vurto/sign-tx 0.1.0-stage.1\n`);
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let txJson = opts.tx;
|
|
160
|
+
if (!txJson) txJson = (await readStdinFn()).trim();
|
|
161
|
+
if (!txJson) {
|
|
162
|
+
stderr.write('sign-tx: missing --tx and no stdin input\n');
|
|
163
|
+
stderr.write(`\n${USAGE}`);
|
|
164
|
+
return 3;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let payload;
|
|
168
|
+
try {
|
|
169
|
+
payload = JSON.parse(txJson);
|
|
170
|
+
} catch (e) {
|
|
171
|
+
stderr.write(`sign-tx: invalid JSON payload: ${e.message}\n`);
|
|
172
|
+
return 3;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const validation = validatePayload(payload);
|
|
176
|
+
if (validation.length) {
|
|
177
|
+
for (const v of validation) stderr.write(`sign-tx: ${v}\n`);
|
|
178
|
+
return 3;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const result = await startServerFn({
|
|
182
|
+
payload,
|
|
183
|
+
port: opts.port,
|
|
184
|
+
timeoutMs: opts.timeout * 1000,
|
|
185
|
+
onListen: ({ url }) => {
|
|
186
|
+
stderr.write(`sign-tx: signer ready at ${url}\n`);
|
|
187
|
+
stderr.write(`sign-tx: waiting up to ${opts.timeout}s for approval...\n`);
|
|
188
|
+
if (opts.open) {
|
|
189
|
+
openBrowserFn(url).catch((e) => {
|
|
190
|
+
stderr.write(`sign-tx: could not auto-open browser (${e.message}); paste the URL manually\n`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (opts.reportTo && (result.kind === 'signed' || result.kind === 'rejected')) {
|
|
197
|
+
try {
|
|
198
|
+
await fetch(opts.reportTo, {
|
|
199
|
+
method: 'POST',
|
|
200
|
+
headers: { 'content-type': 'application/json' },
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
transactionId: payload.transactionId ?? null,
|
|
203
|
+
result,
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
} catch (e) {
|
|
207
|
+
stderr.write(`sign-tx: --report-to delivery failed: ${e.message}\n`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
switch (result.kind) {
|
|
212
|
+
case 'signed': {
|
|
213
|
+
if (!TX_HASH_RE.test(result.txHash || '')) {
|
|
214
|
+
stderr.write('sign-tx: signer returned malformed txHash\n');
|
|
215
|
+
return 3;
|
|
216
|
+
}
|
|
217
|
+
stdout.write(JSON.stringify({
|
|
218
|
+
txHash: result.txHash,
|
|
219
|
+
from: result.from,
|
|
220
|
+
chainId: payload.txData.chainId,
|
|
221
|
+
signedAt: result.signedAt,
|
|
222
|
+
}) + '\n');
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
case 'rejected':
|
|
226
|
+
stderr.write('sign-tx: user rejected the transaction\n');
|
|
227
|
+
return 1;
|
|
228
|
+
case 'timeout':
|
|
229
|
+
stderr.write('sign-tx: timed out waiting for approval\n');
|
|
230
|
+
return 2;
|
|
231
|
+
case 'error':
|
|
232
|
+
stderr.write(`sign-tx: ${result.message}\n`);
|
|
233
|
+
return 3;
|
|
234
|
+
default:
|
|
235
|
+
stderr.write(`sign-tx: unexpected signer result\n`);
|
|
236
|
+
return 3;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const CHAINS = Object.freeze({
|
|
2
|
+
1: { name: 'Ethereum', explorer: 'https://etherscan.io', symbol: 'ETH' },
|
|
3
|
+
10: { name: 'Optimism', explorer: 'https://optimistic.etherscan.io', symbol: 'ETH' },
|
|
4
|
+
56: { name: 'BNB Chain', explorer: 'https://bscscan.com', symbol: 'BNB' },
|
|
5
|
+
100: { name: 'Gnosis', explorer: 'https://gnosisscan.io', symbol: 'xDAI' },
|
|
6
|
+
137: { name: 'Polygon', explorer: 'https://polygonscan.com', symbol: 'POL' },
|
|
7
|
+
146: { name: 'Sonic', explorer: 'https://sonicscan.org', symbol: 'S' },
|
|
8
|
+
8453: { name: 'Base', explorer: 'https://basescan.org', symbol: 'ETH' },
|
|
9
|
+
42161: { name: 'Arbitrum', explorer: 'https://arbiscan.io', symbol: 'ETH' },
|
|
10
|
+
43114: { name: 'Avalanche', explorer: 'https://snowtrace.io', symbol: 'AVAX' },
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const AAVE_V3_POOL = {
|
|
14
|
+
1: '0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2',
|
|
15
|
+
10: '0x794a61358D6845594F94dc1DB02A252b5b4814aD',
|
|
16
|
+
56: '0x6807dc923806fE8Fd134338EABCA509979a7e0cB',
|
|
17
|
+
100: '0xb50201558B00496A145fE76f7424749556E326D8',
|
|
18
|
+
137: '0x794a61358D6845594F94dc1DB02A252b5b4814aD',
|
|
19
|
+
146: '0x5362dBb1e601abF3a4c14c22ffEdA64042E5eAA3',
|
|
20
|
+
8453: '0xA238Dd80C259a72e81d7e4664a9801593F98d1c5',
|
|
21
|
+
42161: '0x794a61358D6845594F94dc1DB02A252b5b4814aD',
|
|
22
|
+
43114: '0x794a61358D6845594F94dc1DB02A252b5b4814aD',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const VURTO_DUAL_AAVE_ADAPTER = {
|
|
26
|
+
1: '0x7014309A43860dcEDdC0Ae2cDbDCE16F724e10Da',
|
|
27
|
+
56: '0x7014309A43860dcEDdC0Ae2cDbDCE16F724e10Da',
|
|
28
|
+
100: '0x7014309A43860dcEDdC0Ae2cDbDCE16F724e10Da',
|
|
29
|
+
137: '0x9cCc0B9e7F36c0ae5b95f6d9C522c74f789A0035',
|
|
30
|
+
8453: '0x009D3f5c5cee523D9005125E2e0EE51878eff21F',
|
|
31
|
+
42161: '0x9397a49Ee8b4E7B71d38f920fb9F1c8E1b1F4d11',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const PARASWAP_AUGUSTUS_V6_2 = {
|
|
35
|
+
1: '0x6A000F20005980200259B80c5102003040001068',
|
|
36
|
+
10: '0x6A000F20005980200259B80c5102003040001068',
|
|
37
|
+
56: '0x6A000F20005980200259B80c5102003040001068',
|
|
38
|
+
137: '0x6A000F20005980200259B80c5102003040001068',
|
|
39
|
+
8453: '0x6A000F20005980200259B80c5102003040001068',
|
|
40
|
+
42161: '0x6A000F20005980200259B80c5102003040001068',
|
|
41
|
+
43114: '0x6A000F20005980200259B80c5102003040001068',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const PARASWAP_TOKEN_TRANSFER_PROXY = {
|
|
45
|
+
1: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
46
|
+
10: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
47
|
+
56: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
48
|
+
137: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
49
|
+
8453: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
50
|
+
42161: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
51
|
+
43114: '0x6A0009d27Ee0d1A6c8aabb27DDE7e83C5d7C20fF',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function lower(addr) {
|
|
55
|
+
return String(addr || '').toLowerCase();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildAllowMap() {
|
|
59
|
+
const out = new Map();
|
|
60
|
+
const add = (chainId, addr, name, category) => {
|
|
61
|
+
if (!addr) return;
|
|
62
|
+
const key = `${chainId}:${lower(addr)}`;
|
|
63
|
+
out.set(key, { chainId, address: addr, name, category });
|
|
64
|
+
};
|
|
65
|
+
for (const [chainId, addr] of Object.entries(AAVE_V3_POOL)) add(Number(chainId), addr, `Aave V3 Pool (${CHAINS[chainId]?.name || `chain ${chainId}`})`, 'aave-pool');
|
|
66
|
+
for (const [chainId, addr] of Object.entries(VURTO_DUAL_AAVE_ADAPTER)) add(Number(chainId), addr, `Vurto Dual-Aave Adapter (${CHAINS[chainId]?.name || `chain ${chainId}`})`, 'vurto-adapter');
|
|
67
|
+
for (const [chainId, addr] of Object.entries(PARASWAP_AUGUSTUS_V6_2)) add(Number(chainId), addr, `ParaSwap Augustus V6.2 (${CHAINS[chainId]?.name || `chain ${chainId}`})`, 'paraswap-router');
|
|
68
|
+
for (const [chainId, addr] of Object.entries(PARASWAP_TOKEN_TRANSFER_PROXY)) add(Number(chainId), addr, `ParaSwap TokenTransferProxy (${CHAINS[chainId]?.name || `chain ${chainId}`})`, 'paraswap-proxy');
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ALLOW_MAP = buildAllowMap();
|
|
73
|
+
|
|
74
|
+
export function lookupContract(chainId, address) {
|
|
75
|
+
if (!Number.isInteger(Number(chainId)) || typeof address !== 'string') return null;
|
|
76
|
+
return ALLOW_MAP.get(`${Number(chainId)}:${lower(address)}`) || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getChainInfo(chainId) {
|
|
80
|
+
return CHAINS[Number(chainId)] || null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isErc20Selector(data) {
|
|
84
|
+
if (typeof data !== 'string' || data.length < 10) return null;
|
|
85
|
+
const sel = data.slice(0, 10).toLowerCase();
|
|
86
|
+
if (sel === '0x095ea7b3') return 'approve';
|
|
87
|
+
if (sel === '0xa9059cbb') return 'transfer';
|
|
88
|
+
if (sel === '0x23b872dd') return 'transferFrom';
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function snapshotAllowList() {
|
|
93
|
+
return Array.from(ALLOW_MAP.values());
|
|
94
|
+
}
|