nos-rescue 0.1.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/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +196 -0
- package/dist/cli.js.map +1 -0
- package/dist/lib/browserWallet.d.ts +8 -0
- package/dist/lib/browserWallet.js +19 -0
- package/dist/lib/browserWallet.js.map +1 -0
- package/dist/lib/constants.d.ts +20 -0
- package/dist/lib/constants.js +24 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/index.d.ts +7 -0
- package/dist/lib/index.js +8 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/rescue.d.ts +85 -0
- package/dist/lib/rescue.js +293 -0
- package/dist/lib/rescue.js.map +1 -0
- package/dist/lib/rewards.d.ts +48 -0
- package/dist/lib/rewards.js +105 -0
- package/dist/lib/rewards.js.map +1 -0
- package/dist/lib/stake.d.ts +61 -0
- package/dist/lib/stake.js +111 -0
- package/dist/lib/stake.js.map +1 -0
- package/dist/lib/sweep.d.ts +34 -0
- package/dist/lib/sweep.js +129 -0
- package/dist/lib/sweep.js.map +1 -0
- package/dist/lib/wallets.d.ts +14 -0
- package/dist/lib/wallets.js +65 -0
- package/dist/lib/wallets.js.map +1 -0
- package/dist/lib/watch.d.ts +41 -0
- package/dist/lib/watch.js +140 -0
- package/dist/lib/watch.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Laurens Verspeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# nos-rescue
|
|
2
|
+
|
|
3
|
+
Rescue staked **NOS** (and any other SOL / SPL tokens) from a **compromised Solana wallet**, when the attacker holds the same private key.
|
|
4
|
+
|
|
5
|
+
The trick: **a separate, uncompromised wallet pays every transaction fee and every rent deposit.** The compromised wallet therefore never needs — and never keeps — any SOL. That alone stops most attackers, because without SOL they cannot broadcast a single transaction. This tool then races them: it unstakes, and withdraws each released slice of NOS straight to a safe wallet as it vests, while continuously sweeping out anything (SOL or tokens) that lands in the compromised wallet.
|
|
6
|
+
|
|
7
|
+
> ⚠️ This is an emergency-response tool for **your own** compromised wallet. You need the compromised wallet's private key, a funded fee-payer wallet, and a safe destination address.
|
|
8
|
+
|
|
9
|
+
## For victims: run it right now, no clone needed
|
|
10
|
+
|
|
11
|
+
Your wallet was just hacked and you found this page? Three ways to run the tool, easiest first:
|
|
12
|
+
|
|
13
|
+
1. **One HTML file (recommended):** [download `nos-rescue.html` from the latest release](https://github.com/laurensV/nos-rescue/releases/latest/download/nos-rescue.html), double-click it, fill in the wallets, click **Start rescue**. Everything runs locally in your browser tab; keys never leave it. Verify the file's SHA-256 against the release notes before pasting keys in. (If your browser refuses `file://`, run `npx serve` in the download folder and open the printed URL.)
|
|
14
|
+
2. **npx (terminal):** `npx nos-rescue status <your-wallet>` to look, `npx nos-rescue rescue …` to act. Requires Node.js ≥ 20.18. See [Usage](#usage).
|
|
15
|
+
3. **Hosted app:** the docs site at **<https://laurensv.github.io/nos-rescue/>** explains everything and hosts [a copy of the app](https://laurensv.github.io/nos-rescue/app.html) — convenient for a first look, but download the file and run it locally for the real rescue.
|
|
16
|
+
|
|
17
|
+
Whichever you pick: create a **fresh fee-payer wallet** with a little SOL (~0.1), choose a **safe destination** the attacker can't touch, and try **dry run** first if you have the nerve to spare.
|
|
18
|
+
|
|
19
|
+
## How Nosana unstaking works (why this is a race)
|
|
20
|
+
|
|
21
|
+
- NOS is staked with a lock **duration** (14–365 days).
|
|
22
|
+
- `unstake` starts a linear release: after unstaking, NOS vests linearly over `duration`, and any released amount can be `withdraw`n.
|
|
23
|
+
- **Whoever holds the key can unstake and withdraw.** If the attacker also unstakes, the vesting clock is shared — every second, a bit more becomes withdrawable, and it's first-come-first-served on each slice.
|
|
24
|
+
|
|
25
|
+
So the strategy is:
|
|
26
|
+
|
|
27
|
+
1. **Unstake immediately** (the attacker will too; the clock is the same for both).
|
|
28
|
+
2. **Keep sweeping** all SOL out of the compromised wallet. This is the real defense: with no SOL, the attacker can't broadcast a single transaction, so they can't withdraw anything. Sweeping is **event-driven**: a websocket watcher on the wallet and its token accounts triggers a sweep in well under a second when anything lands; interval polling is only the backup.
|
|
29
|
+
3. **Withdraw the released NOS to the safe wallet on a sensible cadence** (see below), and do a final withdraw + close at full vest.
|
|
30
|
+
|
|
31
|
+
The on-chain `withdraw` and rewards `claim` both let you name the **destination token account**, so NOS goes **directly to the safe wallet**, never through the compromised one.
|
|
32
|
+
|
|
33
|
+
### How often should you withdraw?
|
|
34
|
+
|
|
35
|
+
A slice of NOS is released *every second*, so "withdraw every slice" would mean a transaction every second — pointless fee-burn. What actually matters:
|
|
36
|
+
|
|
37
|
+
- **The sweep is the defense.** As long as the compromised wallet has no SOL, the attacker cannot withdraw at all, so you don't need to race on withdrawals — you can safely batch them.
|
|
38
|
+
- **Withdrawing is a safety net** in case the attacker manages to get a withdraw through (e.g. they bring their *own* fee payer). Smaller intervals shrink the amount exposed if that happens, at the cost of more fees.
|
|
39
|
+
|
|
40
|
+
So the cadences are decoupled:
|
|
41
|
+
|
|
42
|
+
- **CLI**: sweeps instantly on websocket events (polling every `--sweep-interval`, default 15s, as backup) and withdraws at most every `--withdraw-interval` (default `3600` = hourly; set `0` to only withdraw once fully vested; `--no-auto-withdraw` to never auto-withdraw). A final withdraw + stake-account close always fires at full vest.
|
|
43
|
+
- **Web UI**: the loop sweeps continuously but does **not** auto-withdraw (a browser tab isn't a reliable long-running daemon). Click **Withdraw released NOS** whenever you want to bank what's vested so far; the final withdraw + close still happen automatically at full vest.
|
|
44
|
+
|
|
45
|
+
## Two ways to run it
|
|
46
|
+
|
|
47
|
+
- **Web UI** (local browser page) — easiest for a non-technical victim. Paste the two keys and the safe address, watch a live activity log. `npm run web`.
|
|
48
|
+
- **CLI** — best for unattended/server use near a fast RPC. `node dist/cli.js …`.
|
|
49
|
+
|
|
50
|
+
Both drive the exact same rescue engine (`src/lib/`).
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install
|
|
56
|
+
npm run build # builds the CLI (dist/)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requires Node.js ≥ 20.18.
|
|
60
|
+
|
|
61
|
+
## Web UI
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm run web # dev server at http://localhost:5173
|
|
65
|
+
# or produce a static bundle you can open/host locally:
|
|
66
|
+
npm run web:build # outputs to dist-web/
|
|
67
|
+
npm run web:preview
|
|
68
|
+
# or build the single self-contained file (what releases ship):
|
|
69
|
+
npm run web:build:single # outputs dist-single/nos-rescue.html
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Open the page, fill in **Network**, an optional fast **RPC**, the **compromised wallet key**, the **fee payer key**, and the **safe destination address**, then:
|
|
73
|
+
|
|
74
|
+
- **Check status** — read-only view of stake, vesting timeline, pending rewards, balances.
|
|
75
|
+
- **Start rescue** — runs the daemon in the tab: sweeps continuously and unstakes, but does **not** auto-withdraw. **Keep the tab open**; the loop lives there.
|
|
76
|
+
- **Withdraw released NOS** — banks whatever has vested so far to the safe wallet. Click it whenever; the final withdraw + close still fire automatically at full vest.
|
|
77
|
+
- **Stop** — halts after the current iteration.
|
|
78
|
+
|
|
79
|
+
Keys are pasted directly into the page (base58 or a `keypair.json` array), held only in the tab's memory, and never sent anywhere except as signed transactions to your RPC. Run it on a machine you trust.
|
|
80
|
+
|
|
81
|
+
## The three wallets
|
|
82
|
+
|
|
83
|
+
| Role | What it is | Needs SOL? |
|
|
84
|
+
|------|-----------|-----------|
|
|
85
|
+
| **Compromised** | The hacked wallet holding the stake. You provide its key. | No |
|
|
86
|
+
| **Fee payer** | A separate, clean wallet you control. Pays all fees + rent. | **Yes** — fund it with some SOL |
|
|
87
|
+
| **Safe** | Destination address. Receives all rescued NOS/SOL/tokens. | No |
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
Provide keys via flags, environment variables, or interactive hidden prompt. Every `node dist/cli.js` below can be replaced with `npx nos-rescue` if you installed nothing.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Inspect a wallet's stake / rewards / balances (read-only, no keys needed)
|
|
95
|
+
node dist/cli.js status <WALLET_ADDRESS>
|
|
96
|
+
|
|
97
|
+
# Full rescue daemon (recommended): sweep, unstake, withdraw-as-it-vests, close, keep sweeping
|
|
98
|
+
node dist/cli.js rescue \
|
|
99
|
+
--rpc "https://your-fast-rpc" \
|
|
100
|
+
--safe <SAFE_ADDRESS> \
|
|
101
|
+
--hacked-key ./hacked.json \
|
|
102
|
+
--payer-key ./payer.json
|
|
103
|
+
|
|
104
|
+
# Or with environment variables (keeps keys out of shell history)
|
|
105
|
+
export NOS_RESCUE_RPC="https://your-fast-rpc"
|
|
106
|
+
export NOS_RESCUE_SAFE="<SAFE_ADDRESS>"
|
|
107
|
+
export NOS_RESCUE_HACKED_KEY="<base58 or path>"
|
|
108
|
+
export NOS_RESCUE_PAYER_KEY="<base58 or path>"
|
|
109
|
+
node dist/cli.js rescue
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Individual one-shot commands are also available:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
node dist/cli.js unstake # claim rewards to safe wallet, close reward acct, unstake (atomic)
|
|
116
|
+
node dist/cli.js withdraw # withdraw currently-released NOS to the safe wallet
|
|
117
|
+
node dist/cli.js sweep # move all SOL + SPL tokens out (add --watch to loop)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Keys accepted as **base58 private key** or a path to a **`keypair.json`** (the Solana CLI array format).
|
|
121
|
+
|
|
122
|
+
### Dry run
|
|
123
|
+
|
|
124
|
+
Add the global `--dry-run` flag to any command to **simulate every transaction instead of sending it** — a sanity check before anything moves. Each transaction is built and signed exactly as it would be for real, run through `simulateTransaction`, and logged with its outcome. `rescue --dry-run` does one full pass and exits.
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node dist/cli.js --dry-run rescue # shows exactly what would happen; nothing is sent
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### RPC endpoint
|
|
131
|
+
|
|
132
|
+
Works out of the box on a public RPC, but a public RPC is slow and rate-limited — in a race against an attacker that can cost you slices. **Strongly recommended:** a fast private RPC (Helius / QuickNode / Triton free tiers all work) via `--rpc` or `NOS_RESCUE_RPC`.
|
|
133
|
+
|
|
134
|
+
The websocket endpoint defaults to the RPC URL with `ws(s)://`. If your provider uses a different websocket URL (or you run against a local validator, where it's port 8900), pass `--ws <url>` or `NOS_RESCUE_WS`.
|
|
135
|
+
|
|
136
|
+
## What the `rescue` daemon does each loop
|
|
137
|
+
|
|
138
|
+
1. Ensures the safe wallet has a NOS token account (rent paid by the fee payer).
|
|
139
|
+
2. **Checks the fee payer's balance** — if it can't pay for a transaction anymore, the rescue pauses and alerts every pass until you fund it (and warns early when it's merely low).
|
|
140
|
+
3. Sweeps all SOL + SPL/Token-2022 balances from the compromised wallet to the safe wallet, and **closes each drained token account so its ~0.002 SOL rent also goes to the safe wallet** (empty accounts are closed for their rent too; wrapped SOL is unwrapped by closing). A pass runs instantly whenever the websocket sees value land.
|
|
141
|
+
4. If still staked: **claims pending rewards to the safe wallet, closes the reward account, and unstakes — all in one atomic transaction.**
|
|
142
|
+
5. If unstaked and auto-withdraw is on: **withdraws the released NOS to the safe wallet on the configured cadence** (default hourly).
|
|
143
|
+
6. If the attacker re-stakes to grief you: detects it and unstakes again.
|
|
144
|
+
7. When fully vested: **withdraws the remainder and closes the stake account** (rent released, then swept).
|
|
145
|
+
8. Keeps sweeping incoming SOL/tokens forever (until you Ctrl-C).
|
|
146
|
+
|
|
147
|
+
Every transaction is signed by both the compromised wallet (as authority) and the fee payer (who pays), so the compromised wallet spends nothing.
|
|
148
|
+
|
|
149
|
+
## Architecture
|
|
150
|
+
|
|
151
|
+
The core is a reusable, browser-safe library under [`src/lib/`](src/lib/) (exported from `src/lib/index.ts`). Both front-ends are thin wrappers over it: the CLI in [`src/cli.ts`](src/cli.ts) and the local web UI in [`web/`](web/) (Vite + vanilla TS).
|
|
152
|
+
|
|
153
|
+
- `stake.ts` — stake account decoding, live vesting math (ported from the on-chain `StakeAccount::withdraw`), unstake/withdraw/close instruction builders.
|
|
154
|
+
- `rewards.ts` — `nosana-rewards` `claim` / `close` instructions (hand-built, since `@nosana/kit` doesn't wrap them) so rewards route to the safe wallet.
|
|
155
|
+
- `sweep.ts` — enumerate and move all SOL + SPL/Token-2022 balances, creating destination ATAs with fee-payer rent and closing drained source accounts (rent → safe wallet).
|
|
156
|
+
- `watch.ts` — websocket subscriptions (wallet account + token accounts by owner, at `processed` commitment) that wake the sweep loop instantly, with auto-reconnect.
|
|
157
|
+
- `rescue.ts` — the orchestration daemon.
|
|
158
|
+
|
|
159
|
+
Built on [`@nosana/kit`](https://www.npmjs.com/package/@nosana/kit), which natively supports a distinct fee payer (`client.solana.feePayer`) and per-ATA rent payer.
|
|
160
|
+
|
|
161
|
+
## Verification
|
|
162
|
+
|
|
163
|
+
- `npm test` runs the unit suite: the vesting math (`withdrawableAmount`) pinned against the on-chain formula across edge cases, sweep instruction building (transfer + close, rent reclaim, frozen accounts, wSOL unwrap), and the websocket notification parsing.
|
|
164
|
+
- The sweep engine, websocket-triggered sweeping, ATA rent reclaim, dry-run, and fee-payer pause/resume have been **executed end-to-end against a local `solana-test-validator`** (real transactions; sub-second sweep reaction measured).
|
|
165
|
+
- The unstake-with-rewards-claim and the withdraw-via-separate-fee-payer transactions have been **simulated successfully against live mainnet stake accounts** (see `status` on any real staker to inspect state). No mainnet transactions are broadcast during simulation.
|
|
166
|
+
|
|
167
|
+
## Security notes
|
|
168
|
+
|
|
169
|
+
- Private keys are only ever held in memory; nothing is written to disk. `.gitignore` excludes `*.key` and `keypair*.json`.
|
|
170
|
+
- Prefer the interactive prompt or env vars over shell flags (flags land in shell history).
|
|
171
|
+
- This tool moves assets **out** of a wallet you say is compromised. Double-check the `--safe` address — funds sent there are the whole point, and irreversible.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { address } from '@solana/kit';
|
|
4
|
+
import { createNosanaClient, NosanaNetwork } from '@nosana/kit';
|
|
5
|
+
import { resolveWallet } from './lib/wallets.js';
|
|
6
|
+
import { fetchStakeInfo } from './lib/stake.js';
|
|
7
|
+
import { fetchRewardAccount, fetchReflectionAccount, pendingRewards } from './lib/rewards.js';
|
|
8
|
+
import { ensureSafeNosAta, formatDuration, formatNos, formatSol, runRescue, sweepOnce, unstakeNow, withdrawReleased, } from './lib/rescue.js';
|
|
9
|
+
import { createWaker, watchWalletActivity } from './lib/watch.js';
|
|
10
|
+
import { NOS_DECIMALS } from './lib/constants.js';
|
|
11
|
+
const program = new Command()
|
|
12
|
+
.name('nos-rescue')
|
|
13
|
+
.description('Rescue staked NOS (and everything else) from a compromised wallet.\n' +
|
|
14
|
+
'All transaction fees and rent are paid by a separate, uncompromised fee payer,\n' +
|
|
15
|
+
'so the compromised wallet never needs — and never keeps — any SOL.')
|
|
16
|
+
.option('--rpc <url>', 'Solana RPC endpoint (env: NOS_RESCUE_RPC). A fast private RPC strongly recommended')
|
|
17
|
+
.option('--ws <url>', 'websocket endpoint (env: NOS_RESCUE_WS); default: the RPC endpoint with ws(s)://')
|
|
18
|
+
.option('--network <net>', 'mainnet or devnet', 'mainnet')
|
|
19
|
+
.option('--hacked-key <key>', 'compromised wallet: base58 key or keypair.json path (env: NOS_RESCUE_HACKED_KEY)')
|
|
20
|
+
.option('--payer-key <key>', 'fee payer wallet: base58 key or keypair.json path (env: NOS_RESCUE_PAYER_KEY)')
|
|
21
|
+
.option('--safe <address>', 'SAFE destination wallet address (env: NOS_RESCUE_SAFE)')
|
|
22
|
+
.option('--dry-run', 'simulate every transaction instead of sending it (nothing moves on-chain)');
|
|
23
|
+
function log(msg) {
|
|
24
|
+
console.log(`[${new Date().toISOString()}] ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
function makeClient(opts) {
|
|
27
|
+
const network = opts.network === 'devnet' ? NosanaNetwork.DEVNET : NosanaNetwork.MAINNET;
|
|
28
|
+
const rpcEndpoint = opts.rpc ?? process.env.NOS_RESCUE_RPC;
|
|
29
|
+
const wsEndpoint = opts.ws ?? process.env.NOS_RESCUE_WS;
|
|
30
|
+
const solana = {
|
|
31
|
+
...(rpcEndpoint ? { rpcEndpoint } : {}),
|
|
32
|
+
...(wsEndpoint ? { wsEndpoint } : {}),
|
|
33
|
+
};
|
|
34
|
+
const client = createNosanaClient(network, Object.keys(solana).length ? { solana } : undefined);
|
|
35
|
+
if (!rpcEndpoint) {
|
|
36
|
+
console.warn('WARNING: using the default public RPC endpoint. In a race against an attacker,\n' +
|
|
37
|
+
'a fast private RPC (Helius/QuickNode/Triton free tier works) can make the difference.\n' +
|
|
38
|
+
'Pass one with --rpc <url> or NOS_RESCUE_RPC.\n');
|
|
39
|
+
}
|
|
40
|
+
return client;
|
|
41
|
+
}
|
|
42
|
+
async function makeContext(opts) {
|
|
43
|
+
const client = makeClient(opts);
|
|
44
|
+
const hacked = await resolveWallet({
|
|
45
|
+
value: opts.hackedKey,
|
|
46
|
+
envVar: 'NOS_RESCUE_HACKED_KEY',
|
|
47
|
+
promptLabel: 'COMPROMISED wallet key',
|
|
48
|
+
});
|
|
49
|
+
const payer = await resolveWallet({
|
|
50
|
+
value: opts.payerKey,
|
|
51
|
+
envVar: 'NOS_RESCUE_PAYER_KEY',
|
|
52
|
+
promptLabel: 'FEE PAYER wallet key',
|
|
53
|
+
});
|
|
54
|
+
const safeRaw = opts.safe ?? process.env.NOS_RESCUE_SAFE;
|
|
55
|
+
if (!safeRaw)
|
|
56
|
+
throw new Error('Missing safe destination address (--safe or NOS_RESCUE_SAFE)');
|
|
57
|
+
const safe = address(safeRaw);
|
|
58
|
+
if (safe === hacked.address)
|
|
59
|
+
throw new Error('Safe address must differ from the compromised wallet');
|
|
60
|
+
if (payer.address === hacked.address)
|
|
61
|
+
throw new Error('Fee payer must be a different (uncompromised) wallet');
|
|
62
|
+
client.wallet = hacked;
|
|
63
|
+
client.solana.feePayer = payer;
|
|
64
|
+
if (opts.dryRun)
|
|
65
|
+
log('DRY RUN enabled: transactions are simulated, nothing is sent.');
|
|
66
|
+
return { client, network: opts.network, hacked, payer, safe, log, dryRun: opts.dryRun };
|
|
67
|
+
}
|
|
68
|
+
function abortSignalOnSigint() {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
process.on('SIGINT', () => {
|
|
71
|
+
log('Stopping...');
|
|
72
|
+
controller.abort();
|
|
73
|
+
});
|
|
74
|
+
return controller.signal;
|
|
75
|
+
}
|
|
76
|
+
program
|
|
77
|
+
.command('status')
|
|
78
|
+
.description('show stake, rewards and balances of a wallet (no keys needed)')
|
|
79
|
+
.argument('<owner>', 'wallet address to inspect')
|
|
80
|
+
.action(async (owner) => {
|
|
81
|
+
const opts = program.opts();
|
|
82
|
+
const client = makeClient(opts);
|
|
83
|
+
const ownerAddr = address(owner);
|
|
84
|
+
const sol = await client.solana.getBalance(ownerAddr);
|
|
85
|
+
const nos = await client.nos.getBalance(ownerAddr).catch(() => 0n);
|
|
86
|
+
console.log(`Wallet: ${ownerAddr}`);
|
|
87
|
+
console.log(`SOL balance: ${formatSol(sol)}`);
|
|
88
|
+
console.log(`NOS balance: ${formatNos(nos)}`);
|
|
89
|
+
const stake = await fetchStakeInfo(client, ownerAddr);
|
|
90
|
+
if (!stake) {
|
|
91
|
+
console.log('Stake: none');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`Stake account: ${stake.address}`);
|
|
95
|
+
console.log(` staked: ${formatNos(stake.amount)}`);
|
|
96
|
+
console.log(` duration: ${formatDuration(Number(stake.duration))}`);
|
|
97
|
+
if (stake.timeUnstake === 0n) {
|
|
98
|
+
console.log(' state: STAKED (not unstaked yet)');
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
const now = Math.floor(Date.now() / 1000);
|
|
102
|
+
console.log(` state: UNSTAKED at ${new Date(Number(stake.timeUnstake) * 1000).toISOString()}`);
|
|
103
|
+
console.log(` vault: ${formatNos(stake.vaultBalance)}`);
|
|
104
|
+
console.log(` withdrawable now: ${formatNos(stake.withdrawable)}`);
|
|
105
|
+
console.log(` fully vested: ${new Date(Number(stake.fullyVestedAt) * 1000).toISOString()} (${formatDuration(Number(stake.fullyVestedAt) - now)} left)`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const reward = await fetchRewardAccount(client, ownerAddr);
|
|
109
|
+
if (!reward) {
|
|
110
|
+
console.log('Rewards: no reward account (already closed or never opened)');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
const reflection = await fetchReflectionAccount(client);
|
|
114
|
+
console.log(`Rewards: ~${formatNos(pendingRewards(reward, reflection))} pending (account ${reward.address})`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
program
|
|
118
|
+
.command('rescue')
|
|
119
|
+
.description('full rescue daemon: sweep continuously, unstake, withdraw on a cadence, close')
|
|
120
|
+
.option('--sweep-interval <seconds>', 'seconds between backup sweep/status polls (websocket events trigger sweeps instantly)', '15')
|
|
121
|
+
.option('--withdraw-interval <seconds>', 'min seconds between partial withdrawals; 0 = only at full vest', '3600')
|
|
122
|
+
.option('--no-auto-withdraw', 'do not withdraw automatically; sweep only (withdraw with the `withdraw` command)')
|
|
123
|
+
.option('--min-withdraw <nos>', 'minimum withdrawable NOS before sending a partial withdraw tx', '0')
|
|
124
|
+
.action(async (cmdOpts) => {
|
|
125
|
+
const ctx = await makeContext(program.opts());
|
|
126
|
+
const minWithdraw = BigInt(Math.round(parseFloat(cmdOpts.minWithdraw) * 10 ** NOS_DECIMALS));
|
|
127
|
+
await runRescue(ctx, {
|
|
128
|
+
sweepIntervalSeconds: parseInt(cmdOpts.sweepInterval, 10),
|
|
129
|
+
withdrawIntervalSeconds: parseInt(cmdOpts.withdrawInterval, 10),
|
|
130
|
+
autoWithdraw: cmdOpts.autoWithdraw,
|
|
131
|
+
minWithdraw,
|
|
132
|
+
signal: abortSignalOnSigint(),
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
program
|
|
136
|
+
.command('unstake')
|
|
137
|
+
.description('one-shot: claim rewards to safe wallet, close reward account, unstake (atomic)')
|
|
138
|
+
.action(async () => {
|
|
139
|
+
const ctx = await makeContext(program.opts());
|
|
140
|
+
const stake = await fetchStakeInfo(ctx.client, ctx.hacked.address);
|
|
141
|
+
if (!stake)
|
|
142
|
+
throw new Error('No stake account found for this wallet');
|
|
143
|
+
if (stake.timeUnstake !== 0n) {
|
|
144
|
+
log('Already unstaked; nothing to do.');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const safeNosAta = await ensureSafeNosAta(ctx);
|
|
148
|
+
await unstakeNow(ctx, stake, safeNosAta);
|
|
149
|
+
});
|
|
150
|
+
program
|
|
151
|
+
.command('withdraw')
|
|
152
|
+
.description('one-shot: withdraw currently released NOS to the safe wallet (and close if fully vested)')
|
|
153
|
+
.action(async () => {
|
|
154
|
+
const ctx = await makeContext(program.opts());
|
|
155
|
+
const safeNosAta = await ensureSafeNosAta(ctx);
|
|
156
|
+
const res = await withdrawReleased(ctx, safeNosAta);
|
|
157
|
+
if (!res.stake)
|
|
158
|
+
throw new Error('No stake account found for this wallet');
|
|
159
|
+
if (res.withdrawn === 0n && !res.closed)
|
|
160
|
+
log('Nothing withdrawable right now.');
|
|
161
|
+
});
|
|
162
|
+
program
|
|
163
|
+
.command('sweep')
|
|
164
|
+
.description('move all SOL and SPL tokens out of the compromised wallet')
|
|
165
|
+
.option('--watch', 'keep watching (websocket-triggered, interval polling as backup) and sweeping', false)
|
|
166
|
+
.option('--interval <seconds>', 'seconds between backup sweeps in watch mode', '15')
|
|
167
|
+
.action(async (cmdOpts) => {
|
|
168
|
+
const ctx = await makeContext(program.opts());
|
|
169
|
+
const signal = abortSignalOnSigint();
|
|
170
|
+
const interval = parseInt(cmdOpts.interval, 10) * 1000;
|
|
171
|
+
const waker = createWaker();
|
|
172
|
+
if (cmdOpts.watch) {
|
|
173
|
+
watchWalletActivity({
|
|
174
|
+
client: ctx.client,
|
|
175
|
+
owner: ctx.hacked.address,
|
|
176
|
+
log,
|
|
177
|
+
signal,
|
|
178
|
+
onActivity: (source) => {
|
|
179
|
+
log(`Websocket: incoming value detected (${source}); sweeping immediately`);
|
|
180
|
+
waker.wake();
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
do {
|
|
185
|
+
const moved = await sweepOnce(ctx);
|
|
186
|
+
if (moved === 0)
|
|
187
|
+
log('Nothing to sweep.');
|
|
188
|
+
if (cmdOpts.watch)
|
|
189
|
+
await waker.wait(interval, signal);
|
|
190
|
+
} while (cmdOpts.watch && !signal.aborted);
|
|
191
|
+
});
|
|
192
|
+
program.parseAsync().catch((err) => {
|
|
193
|
+
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Wallet } from '@nosana/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Browser-safe wallet parsing. Accepts either:
|
|
4
|
+
* - a base58 private key string, or
|
|
5
|
+
* - the contents of a Solana `keypair.json` (a JSON array of 64 bytes).
|
|
6
|
+
* (No filesystem access — the user pastes the key material directly.)
|
|
7
|
+
*/
|
|
8
|
+
export declare function walletFromInput(raw: string): Promise<Wallet>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createWalletFromBase58, createWalletFromBytes } from '@nosana/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Browser-safe wallet parsing. Accepts either:
|
|
4
|
+
* - a base58 private key string, or
|
|
5
|
+
* - the contents of a Solana `keypair.json` (a JSON array of 64 bytes).
|
|
6
|
+
* (No filesystem access — the user pastes the key material directly.)
|
|
7
|
+
*/
|
|
8
|
+
export async function walletFromInput(raw) {
|
|
9
|
+
const trimmed = raw.trim();
|
|
10
|
+
if (trimmed.startsWith('[')) {
|
|
11
|
+
const bytes = Uint8Array.from(JSON.parse(trimmed));
|
|
12
|
+
if (bytes.length !== 64) {
|
|
13
|
+
throw new Error(`Expected a 64-byte keypair array, got ${bytes.length} bytes`);
|
|
14
|
+
}
|
|
15
|
+
return createWalletFromBytes(bytes);
|
|
16
|
+
}
|
|
17
|
+
return createWalletFromBase58(trimmed);
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=browserWallet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browserWallet.js","sourceRoot":"","sources":["../../src/lib/browserWallet.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,qBAAqB,EAAe,MAAM,aAAa,CAAC;AAEzF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,GAAW;IAC/C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAa,CAAC,CAAC;QAC/D,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,yCAAyC,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,qBAAqB,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,sBAAsB,CAAC,OAAO,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type Address } from '@solana/kit';
|
|
2
|
+
export declare const NOS_DECIMALS = 6;
|
|
3
|
+
/** nosana-rewards program (not wrapped by @nosana/kit, so we build its instructions manually) */
|
|
4
|
+
export declare const REWARDS_PROGRAM: Address<"nosRB8DUV67oLNrL45bo2pFLrmsWPiewe2Lk2DRNYCp">;
|
|
5
|
+
export declare const TOKEN_PROGRAM: Address<"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA">;
|
|
6
|
+
export declare const TOKEN_2022_PROGRAM: Address<"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb">;
|
|
7
|
+
/** Native (wrapped SOL) mint; closing a wSOL account unwraps it. */
|
|
8
|
+
export declare const WRAPPED_SOL_MINT: Address<"So11111111111111111111111111111111111111112">;
|
|
9
|
+
export declare const NOS_MINT: Record<'mainnet' | 'devnet', Address>;
|
|
10
|
+
/**
|
|
11
|
+
* Anchor instruction discriminators: sha256("global:<name>")[0..8], precomputed
|
|
12
|
+
* so this module stays browser-safe (no node:crypto). Verified against the
|
|
13
|
+
* on-chain nosana-rewards program via mainnet simulation.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ANCHOR_DISCRIMINATORS: {
|
|
16
|
+
readonly claim: Uint8Array<ArrayBuffer>;
|
|
17
|
+
readonly close: Uint8Array<ArrayBuffer>;
|
|
18
|
+
};
|
|
19
|
+
/** Base transaction fee per signature; a wallet below this can't pay for any transaction itself. */
|
|
20
|
+
export declare const LAMPORTS_PER_SIGNATURE = 5000n;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { address } from '@solana/kit';
|
|
2
|
+
export const NOS_DECIMALS = 6;
|
|
3
|
+
/** nosana-rewards program (not wrapped by @nosana/kit, so we build its instructions manually) */
|
|
4
|
+
export const REWARDS_PROGRAM = address('nosRB8DUV67oLNrL45bo2pFLrmsWPiewe2Lk2DRNYCp');
|
|
5
|
+
export const TOKEN_PROGRAM = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
|
|
6
|
+
export const TOKEN_2022_PROGRAM = address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');
|
|
7
|
+
/** Native (wrapped SOL) mint; closing a wSOL account unwraps it. */
|
|
8
|
+
export const WRAPPED_SOL_MINT = address('So11111111111111111111111111111111111111112');
|
|
9
|
+
export const NOS_MINT = {
|
|
10
|
+
mainnet: address('nosXBVoaCTtYdLvKY6Csb4AC8JCdQKKAaWYtx2ZMoo7'),
|
|
11
|
+
devnet: address('devr1BGQndEW5k5zfvG5FsLyZv1Ap73vNgAHcQ9sUVP'),
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Anchor instruction discriminators: sha256("global:<name>")[0..8], precomputed
|
|
15
|
+
* so this module stays browser-safe (no node:crypto). Verified against the
|
|
16
|
+
* on-chain nosana-rewards program via mainnet simulation.
|
|
17
|
+
*/
|
|
18
|
+
export const ANCHOR_DISCRIMINATORS = {
|
|
19
|
+
claim: Uint8Array.from([62, 198, 214, 193, 213, 159, 108, 210]),
|
|
20
|
+
close: Uint8Array.from([98, 165, 201, 177, 108, 65, 206, 96]),
|
|
21
|
+
};
|
|
22
|
+
/** Base transaction fee per signature; a wallet below this can't pay for any transaction itself. */
|
|
23
|
+
export const LAMPORTS_PER_SIGNATURE = 5000n;
|
|
24
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/lib/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAgB,MAAM,aAAa,CAAC;AAEpD,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAE9B,iGAAiG;AACjG,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC,6CAA6C,CAAC,CAAC;AAEtF,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,CAAC,6CAA6C,CAAC,CAAC;AACpF,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC,6CAA6C,CAAC,CAAC;AAEzF,oEAAoE;AACpE,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAC,6CAA6C,CAAC,CAAC;AAEvF,MAAM,CAAC,MAAM,QAAQ,GAA0C;IAC7D,OAAO,EAAE,OAAO,CAAC,6CAA6C,CAAC;IAC/D,MAAM,EAAE,OAAO,CAAC,6CAA6C,CAAC;CAC/D,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;IAC/D,KAAK,EAAE,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;CACrD,CAAC;AAEX,oGAAoG;AACpG,MAAM,CAAC,MAAM,sBAAsB,GAAG,KAAK,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Address } from '@solana/kit';
|
|
2
|
+
import type { NosanaClient, Wallet } from '@nosana/kit';
|
|
3
|
+
import { type StakeInfo } from './stake.js';
|
|
4
|
+
export interface RescueContext {
|
|
5
|
+
client: NosanaClient;
|
|
6
|
+
network: 'mainnet' | 'devnet';
|
|
7
|
+
hacked: Wallet;
|
|
8
|
+
payer: Wallet;
|
|
9
|
+
safe: Address;
|
|
10
|
+
log: (msg: string) => void;
|
|
11
|
+
/** simulate every transaction instead of sending it — nothing moves on-chain */
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface RescueOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Seconds between sweep/status iterations (default 15). This polling is the
|
|
17
|
+
* BACKUP: the primary defense is the websocket watcher, which wakes the loop
|
|
18
|
+
* the moment SOL or tokens land in the compromised wallet. Polling covers
|
|
19
|
+
* websocket downtime and anything the subscriptions miss.
|
|
20
|
+
*/
|
|
21
|
+
sweepIntervalSeconds?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Whether the loop withdraws released NOS on its own (default true). Set false
|
|
24
|
+
* for a manual model (e.g. the web UI's "Withdraw" button), where the loop only
|
|
25
|
+
* sweeps and reports.
|
|
26
|
+
*/
|
|
27
|
+
autoWithdraw?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* When auto-withdrawing, the minimum seconds between partial withdrawals
|
|
30
|
+
* (default 3600 = hourly). A released "slice" accrues every second, so
|
|
31
|
+
* withdrawing constantly would be pure fee waste; this batches it. `0` means
|
|
32
|
+
* only withdraw once the stake is fully vested. A final withdraw + close always
|
|
33
|
+
* fires at full vest regardless of this value.
|
|
34
|
+
*/
|
|
35
|
+
withdrawIntervalSeconds?: number;
|
|
36
|
+
/** Minimum withdrawable NOS (base units) before a partial withdraw is worth a tx (default 0). */
|
|
37
|
+
minWithdraw?: bigint;
|
|
38
|
+
/** stop signal */
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
export interface WithdrawResult {
|
|
42
|
+
/** NOS moved to the safe wallet on this call (base units) */
|
|
43
|
+
withdrawn: bigint;
|
|
44
|
+
/** whether the stake account was closed (fully rescued) */
|
|
45
|
+
closed: boolean;
|
|
46
|
+
/** the stake as observed, or null if there is no stake account */
|
|
47
|
+
stake: StakeInfo | null;
|
|
48
|
+
}
|
|
49
|
+
export declare function formatNos(amount: bigint): string;
|
|
50
|
+
export declare function formatSol(lamports: bigint): string;
|
|
51
|
+
/** Derive (and if needed create, rent paid by the rescue payer) the safe wallet's NOS ATA. */
|
|
52
|
+
export declare function ensureSafeNosAta(ctx: RescueContext): Promise<Address>;
|
|
53
|
+
/** Sweep all SOL + SPL balances out of the hacked wallet. Returns number of assets moved. */
|
|
54
|
+
export declare function sweepOnce(ctx: RescueContext): Promise<number>;
|
|
55
|
+
/** Claim rewards, close the rewards account and unstake — atomically. */
|
|
56
|
+
export declare function unstakeNow(ctx: RescueContext, stake: StakeInfo, safeNosAta: Address): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Withdraw all currently-released NOS from the (unstaked) vault straight to the
|
|
59
|
+
* safe wallet, and — if the stake is fully vested — close the stake account to
|
|
60
|
+
* release its rent (which the sweep then collects). Safe to call any time; it
|
|
61
|
+
* fetches fresh stake state and no-ops when there's nothing to do. This is the
|
|
62
|
+
* single path shared by the loop, the CLI `withdraw` command, and the web button.
|
|
63
|
+
*/
|
|
64
|
+
export declare function withdrawReleased(ctx: RescueContext, safeNosAta: Address, opts?: {
|
|
65
|
+
minWithdraw?: bigint;
|
|
66
|
+
}): Promise<WithdrawResult>;
|
|
67
|
+
/**
|
|
68
|
+
* The full rescue daemon:
|
|
69
|
+
* 1. sweep whatever is in the hacked wallet right now
|
|
70
|
+
* 2. unstake immediately (claim + close rewards + unstake, atomic)
|
|
71
|
+
* 3. loop: keep sweeping incoming SOL/tokens (the defense — this starves the
|
|
72
|
+
* attacker of the SOL they'd need to move anything), and, if auto-withdraw
|
|
73
|
+
* is on, withdraw released NOS to the safe wallet on the configured cadence
|
|
74
|
+
* (not every slice — a slice accrues every second)
|
|
75
|
+
* 4. re-unstake if the attacker restakes to grief the vest
|
|
76
|
+
* 5. at full vest: withdraw the remainder, close the stake account, sweep the rent
|
|
77
|
+
*
|
|
78
|
+
* Sweeping is event-driven: a websocket watcher on the wallet and its token
|
|
79
|
+
* accounts wakes the loop the moment value lands (sub-second reaction); the
|
|
80
|
+
* sweepInterval polling is the backup when the websocket is down.
|
|
81
|
+
*
|
|
82
|
+
* Runs until aborted; after the stake is fully rescued it keeps sweeping.
|
|
83
|
+
*/
|
|
84
|
+
export declare function runRescue(ctx: RescueContext, options?: RescueOptions): Promise<void>;
|
|
85
|
+
export declare function formatDuration(seconds: number): string;
|