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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type Address, type Instruction, type TransactionSigner } from '@solana/kit';
|
|
2
|
+
import type { NosanaClient } from '@nosana/kit';
|
|
3
|
+
export interface SweepItem {
|
|
4
|
+
/** 'SOL' or the token mint */
|
|
5
|
+
mint: Address | 'SOL';
|
|
6
|
+
amount: bigint;
|
|
7
|
+
decimals: number;
|
|
8
|
+
/** human description for logs */
|
|
9
|
+
label: string;
|
|
10
|
+
/** atomic instruction group that moves this asset to the safe wallet */
|
|
11
|
+
instructions: Instruction[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Build instruction groups that move everything of value (SOL + all SPL /
|
|
15
|
+
* Token-2022 balances) from the compromised wallet to the safe wallet.
|
|
16
|
+
* Destination ATAs are created idempotently with rent paid by `payer`,
|
|
17
|
+
* so the compromised wallet never needs a single lamport.
|
|
18
|
+
*
|
|
19
|
+
* Token accounts are also CLOSED, with their ~0.002 SOL rent sent straight to
|
|
20
|
+
* the safe wallet — that reclaims the rent and denies it to the attacker:
|
|
21
|
+
* - empty token accounts: close only (pure rent reclaim)
|
|
22
|
+
* - wrapped SOL: close only (unwraps balance + rent as native SOL to safe)
|
|
23
|
+
* - classic SPL with a balance: transfer + close in the same transaction
|
|
24
|
+
* - Token-2022 with a balance: transfer, then close as a SEPARATE group, so
|
|
25
|
+
* an extension that blocks closing (e.g. withheld transfer fees) can never
|
|
26
|
+
* block the transfer itself
|
|
27
|
+
*/
|
|
28
|
+
export declare function buildSweepInstructions(client: NosanaClient, params: {
|
|
29
|
+
hacked: TransactionSigner;
|
|
30
|
+
safe: Address;
|
|
31
|
+
payer: TransactionSigner;
|
|
32
|
+
/** ignore SOL balances below this (default: LAMPORTS_PER_SIGNATURE, the minimum useful to an attacker) */
|
|
33
|
+
minLamports?: bigint;
|
|
34
|
+
}): Promise<SweepItem[]>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { address, } from '@solana/kit';
|
|
2
|
+
import { getTransferSolInstruction } from '@solana-program/system';
|
|
3
|
+
import { findAssociatedTokenPda, getCloseAccountInstruction, getCreateAssociatedTokenIdempotentInstruction, getTransferInstruction, } from '@solana-program/token';
|
|
4
|
+
import { LAMPORTS_PER_SIGNATURE, TOKEN_2022_PROGRAM, TOKEN_PROGRAM, WRAPPED_SOL_MINT, } from './constants.js';
|
|
5
|
+
/**
|
|
6
|
+
* Build instruction groups that move everything of value (SOL + all SPL /
|
|
7
|
+
* Token-2022 balances) from the compromised wallet to the safe wallet.
|
|
8
|
+
* Destination ATAs are created idempotently with rent paid by `payer`,
|
|
9
|
+
* so the compromised wallet never needs a single lamport.
|
|
10
|
+
*
|
|
11
|
+
* Token accounts are also CLOSED, with their ~0.002 SOL rent sent straight to
|
|
12
|
+
* the safe wallet — that reclaims the rent and denies it to the attacker:
|
|
13
|
+
* - empty token accounts: close only (pure rent reclaim)
|
|
14
|
+
* - wrapped SOL: close only (unwraps balance + rent as native SOL to safe)
|
|
15
|
+
* - classic SPL with a balance: transfer + close in the same transaction
|
|
16
|
+
* - Token-2022 with a balance: transfer, then close as a SEPARATE group, so
|
|
17
|
+
* an extension that blocks closing (e.g. withheld transfer fees) can never
|
|
18
|
+
* block the transfer itself
|
|
19
|
+
*/
|
|
20
|
+
export async function buildSweepInstructions(client, params) {
|
|
21
|
+
const items = [];
|
|
22
|
+
const owner = params.hacked.address;
|
|
23
|
+
for (const tokenProgram of [TOKEN_PROGRAM, TOKEN_2022_PROGRAM]) {
|
|
24
|
+
const res = await client.solana.rpc
|
|
25
|
+
.getTokenAccountsByOwner(owner, { programId: tokenProgram }, { encoding: 'jsonParsed' })
|
|
26
|
+
.send();
|
|
27
|
+
for (const { pubkey, account } of res.value) {
|
|
28
|
+
const info = account.data.parsed?.info;
|
|
29
|
+
if (!info)
|
|
30
|
+
continue;
|
|
31
|
+
if (info.state === 'frozen')
|
|
32
|
+
continue; // can neither transfer nor close
|
|
33
|
+
const amount = BigInt(info.tokenAmount?.amount ?? '0');
|
|
34
|
+
const mint = address(info.mint);
|
|
35
|
+
const decimals = Number(info.tokenAmount?.decimals ?? 0);
|
|
36
|
+
const closeIx = getCloseAccountInstruction({ account: pubkey, destination: params.safe, owner: params.hacked }, { programAddress: tokenProgram });
|
|
37
|
+
if (amount === 0n) {
|
|
38
|
+
items.push({
|
|
39
|
+
mint,
|
|
40
|
+
amount: 0n,
|
|
41
|
+
decimals,
|
|
42
|
+
label: `rent of empty token account ${pubkey}`,
|
|
43
|
+
instructions: [closeIx],
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (mint === WRAPPED_SOL_MINT) {
|
|
48
|
+
items.push({
|
|
49
|
+
mint,
|
|
50
|
+
amount,
|
|
51
|
+
decimals,
|
|
52
|
+
label: `${amount} wrapped SOL (unwrapped) + rent of ${pubkey}`,
|
|
53
|
+
instructions: [closeIx],
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const [destAta] = await findAssociatedTokenPda({
|
|
58
|
+
mint,
|
|
59
|
+
owner: params.safe,
|
|
60
|
+
tokenProgram,
|
|
61
|
+
});
|
|
62
|
+
const moveInstructions = [
|
|
63
|
+
getCreateAssociatedTokenIdempotentInstruction({
|
|
64
|
+
payer: params.payer,
|
|
65
|
+
owner: params.safe,
|
|
66
|
+
mint,
|
|
67
|
+
ata: destAta,
|
|
68
|
+
tokenProgram,
|
|
69
|
+
}),
|
|
70
|
+
getTransferInstruction({
|
|
71
|
+
source: pubkey,
|
|
72
|
+
destination: destAta,
|
|
73
|
+
authority: params.hacked,
|
|
74
|
+
amount,
|
|
75
|
+
}, { programAddress: tokenProgram }),
|
|
76
|
+
];
|
|
77
|
+
if (tokenProgram === TOKEN_PROGRAM) {
|
|
78
|
+
// closing a drained classic SPL account can't fail; keep it atomic
|
|
79
|
+
moveInstructions.push(closeIx);
|
|
80
|
+
items.push({
|
|
81
|
+
mint,
|
|
82
|
+
amount,
|
|
83
|
+
decimals,
|
|
84
|
+
label: `${amount} of ${mint} + account rent`,
|
|
85
|
+
instructions: moveInstructions,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
items.push({
|
|
90
|
+
mint,
|
|
91
|
+
amount,
|
|
92
|
+
decimals,
|
|
93
|
+
label: `${amount} of ${mint}`,
|
|
94
|
+
instructions: moveInstructions,
|
|
95
|
+
});
|
|
96
|
+
items.push({
|
|
97
|
+
mint,
|
|
98
|
+
amount: 0n,
|
|
99
|
+
decimals,
|
|
100
|
+
label: `rent of drained token account ${pubkey}`,
|
|
101
|
+
instructions: [closeIx],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// SOL last: fees are paid by `payer`, so the FULL lamport balance can leave.
|
|
107
|
+
const minLamports = params.minLamports ?? LAMPORTS_PER_SIGNATURE;
|
|
108
|
+
const lamports = await client.solana.getBalance(owner);
|
|
109
|
+
if (lamports >= minLamports && lamports > 0n) {
|
|
110
|
+
items.push({
|
|
111
|
+
mint: 'SOL',
|
|
112
|
+
amount: lamports,
|
|
113
|
+
decimals: 9,
|
|
114
|
+
label: formatLamports(lamports),
|
|
115
|
+
instructions: [
|
|
116
|
+
getTransferSolInstruction({
|
|
117
|
+
source: params.hacked,
|
|
118
|
+
destination: params.safe,
|
|
119
|
+
amount: lamports,
|
|
120
|
+
}),
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return items;
|
|
125
|
+
}
|
|
126
|
+
function formatLamports(lamports) {
|
|
127
|
+
return `${Number(lamports) / 1e9} SOL`;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=sweep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sweep.js","sourceRoot":"","sources":["../../src/lib/sweep.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,GAIR,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,EACL,sBAAsB,EACtB,0BAA0B,EAC1B,6CAA6C,EAC7C,sBAAsB,GACvB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,aAAa,EACb,gBAAgB,GACjB,MAAM,gBAAgB,CAAC;AAaxB;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAoB,EACpB,MAMC;IAED,MAAM,KAAK,GAAgB,EAAE,CAAC;IAC9B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;IAEpC,KAAK,MAAM,YAAY,IAAI,CAAC,aAAa,EAAE,kBAAkB,CAAC,EAAE,CAAC;QAC/D,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,GAAG;aAChC,uBAAuB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;aACvF,IAAI,EAAE,CAAC;QACV,KAAK,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YAC5C,MAAM,IAAI,GAAI,OAAO,CAAC,IAAY,CAAC,MAAM,EAAE,IAAI,CAAC;YAChD,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ;gBAAE,SAAS,CAAC,iCAAiC;YACxE,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,IAAI,GAAG,CAAC,CAAC;YACvD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,QAAQ,IAAI,CAAC,CAAC,CAAC;YACzD,MAAM,OAAO,GAAG,0BAA0B,CACxC,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,EACnE,EAAE,cAAc,EAAE,YAAY,EAAE,CACjC,CAAC;YAEF,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI;oBACJ,MAAM,EAAE,EAAE;oBACV,QAAQ;oBACR,KAAK,EAAE,+BAA+B,MAAM,EAAE;oBAC9C,YAAY,EAAE,CAAC,OAAO,CAAC;iBACxB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;gBAC9B,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI;oBACJ,MAAM;oBACN,QAAQ;oBACR,KAAK,EAAE,GAAG,MAAM,sCAAsC,MAAM,EAAE;oBAC9D,YAAY,EAAE,CAAC,OAAO,CAAC;iBACxB,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,sBAAsB,CAAC;gBAC7C,IAAI;gBACJ,KAAK,EAAE,MAAM,CAAC,IAAI;gBAClB,YAAY;aACb,CAAC,CAAC;YACH,MAAM,gBAAgB,GAAkB;gBACtC,6CAA6C,CAAC;oBAC5C,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,KAAK,EAAE,MAAM,CAAC,IAAI;oBAClB,IAAI;oBACJ,GAAG,EAAE,OAAO;oBACZ,YAAY;iBACb,CAAC;gBACF,sBAAsB,CACpB;oBACE,MAAM,EAAE,MAAM;oBACd,WAAW,EAAE,OAAO;oBACpB,SAAS,EAAE,MAAM,CAAC,MAAM;oBACxB,MAAM;iBACP,EACD,EAAE,cAAc,EAAE,YAAY,EAAE,CACjC;aACF,CAAC;YACF,IAAI,YAAY,KAAK,aAAa,EAAE,CAAC;gBACnC,mEAAmE;gBACnE,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC/B,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI;oBACJ,MAAM;oBACN,QAAQ;oBACR,KAAK,EAAE,GAAG,MAAM,OAAO,IAAI,iBAAiB;oBAC5C,YAAY,EAAE,gBAAgB;iBAC/B,CAAC,CAAC;YACL,CAAC;iBAAM,CAAC;gBACN,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI;oBACJ,MAAM;oBACN,QAAQ;oBACR,KAAK,EAAE,GAAG,MAAM,OAAO,IAAI,EAAE;oBAC7B,YAAY,EAAE,gBAAgB;iBAC/B,CAAC,CAAC;gBACH,KAAK,CAAC,IAAI,CAAC;oBACT,IAAI;oBACJ,MAAM,EAAE,EAAE;oBACV,QAAQ;oBACR,KAAK,EAAE,iCAAiC,MAAM,EAAE;oBAChD,YAAY,EAAE,CAAC,OAAO,CAAC;iBACxB,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,IAAI,sBAAsB,CAAC;IACjE,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;IACvD,IAAI,QAAQ,IAAI,WAAW,IAAI,QAAQ,GAAG,EAAE,EAAE,CAAC;QAC7C,KAAK,CAAC,IAAI,CAAC;YACT,IAAI,EAAE,KAAK;YACX,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,CAAC;YACX,KAAK,EAAE,cAAc,CAAC,QAAQ,CAAC;YAC/B,YAAY,EAAE;gBACZ,yBAAyB,CAAC;oBACxB,MAAM,EAAE,MAAM,CAAC,MAAM;oBACrB,WAAW,EAAE,MAAM,CAAC,IAAI;oBACxB,MAAM,EAAE,QAAQ;iBACjB,CAAC;aACH;SACF,CAAC,CAAC;IACL,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,cAAc,CAAC,QAAgB;IACtC,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,GAAG,MAAM,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Wallet } from '@nosana/kit';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve a wallet from (in order of precedence):
|
|
4
|
+
* 1. an explicit value (base58 private key, or path to a keypair.json)
|
|
5
|
+
* 2. an environment variable holding the same
|
|
6
|
+
* 3. an interactive hidden prompt
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveWallet(opts: {
|
|
9
|
+
value?: string;
|
|
10
|
+
envVar: string;
|
|
11
|
+
promptLabel: string;
|
|
12
|
+
}): Promise<Wallet>;
|
|
13
|
+
/** Prompt on the terminal without echoing the typed characters. */
|
|
14
|
+
export declare function promptHidden(question: string): Promise<string>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { createWalletFromBase58, loadWalletFromFile } from '@nosana/kit';
|
|
4
|
+
/**
|
|
5
|
+
* Resolve a wallet from (in order of precedence):
|
|
6
|
+
* 1. an explicit value (base58 private key, or path to a keypair.json)
|
|
7
|
+
* 2. an environment variable holding the same
|
|
8
|
+
* 3. an interactive hidden prompt
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveWallet(opts) {
|
|
11
|
+
const raw = opts.value ?? process.env[opts.envVar];
|
|
12
|
+
if (raw)
|
|
13
|
+
return walletFromString(raw);
|
|
14
|
+
const entered = await promptHidden(`Enter ${opts.promptLabel} (base58 private key or path to keypair.json): `);
|
|
15
|
+
if (!entered)
|
|
16
|
+
throw new Error(`No ${opts.promptLabel} provided`);
|
|
17
|
+
return walletFromString(entered.trim());
|
|
18
|
+
}
|
|
19
|
+
async function walletFromString(raw) {
|
|
20
|
+
if (raw.endsWith('.json') || existsSync(raw)) {
|
|
21
|
+
return loadWalletFromFile(raw);
|
|
22
|
+
}
|
|
23
|
+
return createWalletFromBase58(raw);
|
|
24
|
+
}
|
|
25
|
+
/** Prompt on the terminal without echoing the typed characters. */
|
|
26
|
+
export function promptHidden(question) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
if (!stdin.isTTY) {
|
|
29
|
+
reject(new Error('stdin is not a TTY; provide keys via flags or environment variables'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
stdout.write(question);
|
|
33
|
+
stdin.setRawMode(true);
|
|
34
|
+
stdin.resume();
|
|
35
|
+
stdin.setEncoding('utf8');
|
|
36
|
+
let input = '';
|
|
37
|
+
const onData = (char) => {
|
|
38
|
+
switch (char) {
|
|
39
|
+
case '\n':
|
|
40
|
+
case '\r':
|
|
41
|
+
case '': // Ctrl-D
|
|
42
|
+
stdin.setRawMode(false);
|
|
43
|
+
stdin.pause();
|
|
44
|
+
stdin.off('data', onData);
|
|
45
|
+
stdout.write('\n');
|
|
46
|
+
resolve(input);
|
|
47
|
+
break;
|
|
48
|
+
case '': // Ctrl-C
|
|
49
|
+
stdin.setRawMode(false);
|
|
50
|
+
stdin.pause();
|
|
51
|
+
stdout.write('\n');
|
|
52
|
+
process.exit(130);
|
|
53
|
+
break;
|
|
54
|
+
case '': // Backspace
|
|
55
|
+
case '\b':
|
|
56
|
+
input = input.slice(0, -1);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
input += char;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
stdin.on('data', onData);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=wallets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wallets.js","sourceRoot":"","sources":["../../src/lib/wallets.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAe,MAAM,aAAa,CAAC;AAEtF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAInC;IACC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,GAAG;QAAE,OAAO,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAEtC,MAAM,OAAO,GAAG,MAAM,YAAY,CAChC,SAAS,IAAI,CAAC,WAAW,iDAAiD,CAC3E,CAAC;IACF,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,WAAW,WAAW,CAAC,CAAC;IACjE,OAAO,gBAAgB,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,GAAW;IACzC,IAAI,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC7C,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACjC,CAAC;IACD,OAAO,sBAAsB,CAAC,GAAG,CAAC,CAAC;AACrC,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;YACjB,MAAM,CAAC,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC,CAAC;YACzF,OAAO;QACT,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACvB,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;QACvB,KAAK,CAAC,MAAM,EAAE,CAAC;QACf,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC1B,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,EAAE;YAC9B,QAAQ,IAAI,EAAE,CAAC;gBACb,KAAK,IAAI,CAAC;gBACV,KAAK,IAAI,CAAC;gBACV,KAAK,GAAG,EAAE,SAAS;oBACjB,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;oBACxB,KAAK,CAAC,KAAK,EAAE,CAAC;oBACd,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;oBAC1B,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACnB,OAAO,CAAC,KAAK,CAAC,CAAC;oBACf,MAAM;gBACR,KAAK,GAAG,EAAE,SAAS;oBACjB,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;oBACxB,KAAK,CAAC,KAAK,EAAE,CAAC;oBACd,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;oBACnB,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBAClB,MAAM;gBACR,KAAK,GAAG,CAAC,CAAC,YAAY;gBACtB,KAAK,IAAI;oBACP,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;oBAC3B,MAAM;gBACR;oBACE,KAAK,IAAI,IAAI,CAAC;YAClB,CAAC;QACH,CAAC,CAAC;QACF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Address } from '@solana/kit';
|
|
2
|
+
import type { NosanaClient } from '@nosana/kit';
|
|
3
|
+
/**
|
|
4
|
+
* A latch that lets a loop sleep until either its polling interval elapses or
|
|
5
|
+
* a websocket notification wakes it early. A wake() that arrives while the
|
|
6
|
+
* loop is busy (not waiting) is remembered, so the next wait() returns
|
|
7
|
+
* immediately instead of losing the event.
|
|
8
|
+
*/
|
|
9
|
+
export interface Waker {
|
|
10
|
+
wake(): void;
|
|
11
|
+
/** wait for ms, a wake() call, or signal abort — whichever comes first */
|
|
12
|
+
wait(ms: number, signal?: AbortSignal): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createWaker(): Waker;
|
|
15
|
+
export interface WatchParams {
|
|
16
|
+
client: NosanaClient;
|
|
17
|
+
owner: Address;
|
|
18
|
+
/**
|
|
19
|
+
* Called whenever the websocket reports value present in the wallet:
|
|
20
|
+
* lamports on the wallet account, or a positive balance on any token
|
|
21
|
+
* account owned by it (existing or freshly created). Notifications that
|
|
22
|
+
* show the account empty (our own sweep landing) are filtered out.
|
|
23
|
+
*/
|
|
24
|
+
onActivity: (source: string) => void;
|
|
25
|
+
log?: (msg: string) => void;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Watch a wallet over websocket so incoming funds are detected in well under a
|
|
30
|
+
* second instead of on the polling interval:
|
|
31
|
+
* - accountSubscribe on the wallet itself (SOL arriving)
|
|
32
|
+
* - programSubscribe on both token programs, filtered by owner (offset 32),
|
|
33
|
+
* which fires for existing AND newly-created token accounts
|
|
34
|
+
*
|
|
35
|
+
* Subscriptions are made at 'processed' commitment for the earliest possible
|
|
36
|
+
* signal. Each subscription auto-reconnects with backoff; the caller's
|
|
37
|
+
* interval polling remains the backup while the websocket is down.
|
|
38
|
+
*/
|
|
39
|
+
export declare function watchWalletActivity(params: WatchParams): void;
|
|
40
|
+
/** Token account balance (u64 LE at offset 64), or 0n when absent/closed. */
|
|
41
|
+
export declare function tokenAmountFromBase64(data: string | undefined): bigint;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from './constants.js';
|
|
2
|
+
export function createWaker() {
|
|
3
|
+
let pending = false;
|
|
4
|
+
let release = null;
|
|
5
|
+
return {
|
|
6
|
+
wake() {
|
|
7
|
+
if (release) {
|
|
8
|
+
const r = release;
|
|
9
|
+
release = null;
|
|
10
|
+
r();
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
pending = true;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
wait(ms, signal) {
|
|
17
|
+
if (pending || signal?.aborted) {
|
|
18
|
+
pending = false;
|
|
19
|
+
return Promise.resolve();
|
|
20
|
+
}
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const t = setTimeout(done, ms);
|
|
23
|
+
function done() {
|
|
24
|
+
signal?.removeEventListener('abort', done);
|
|
25
|
+
clearTimeout(t);
|
|
26
|
+
if (release === done)
|
|
27
|
+
release = null;
|
|
28
|
+
resolve();
|
|
29
|
+
}
|
|
30
|
+
release = done;
|
|
31
|
+
signal?.addEventListener('abort', done);
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Watch a wallet over websocket so incoming funds are detected in well under a
|
|
38
|
+
* second instead of on the polling interval:
|
|
39
|
+
* - accountSubscribe on the wallet itself (SOL arriving)
|
|
40
|
+
* - programSubscribe on both token programs, filtered by owner (offset 32),
|
|
41
|
+
* which fires for existing AND newly-created token accounts
|
|
42
|
+
*
|
|
43
|
+
* Subscriptions are made at 'processed' commitment for the earliest possible
|
|
44
|
+
* signal. Each subscription auto-reconnects with backoff; the caller's
|
|
45
|
+
* interval polling remains the backup while the websocket is down.
|
|
46
|
+
*/
|
|
47
|
+
export function watchWalletActivity(params) {
|
|
48
|
+
const { client, owner, onActivity, log, signal } = params;
|
|
49
|
+
const subs = client.solana.rpcSubscriptions;
|
|
50
|
+
const ownerBytes = owner.toString();
|
|
51
|
+
void runSubscription({
|
|
52
|
+
label: 'wallet SOL balance',
|
|
53
|
+
log,
|
|
54
|
+
signal,
|
|
55
|
+
subscribe: (abortSignal) => subs.accountNotifications(owner, { commitment: 'processed' }).subscribe({ abortSignal }),
|
|
56
|
+
onNotification: (notif) => {
|
|
57
|
+
const lamports = BigInt(notif?.value?.lamports ?? 0);
|
|
58
|
+
if (lamports > 0n)
|
|
59
|
+
onActivity('wallet SOL balance');
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
for (const [name, program] of [
|
|
63
|
+
['token accounts', TOKEN_PROGRAM],
|
|
64
|
+
['token-2022 accounts', TOKEN_2022_PROGRAM],
|
|
65
|
+
]) {
|
|
66
|
+
void runSubscription({
|
|
67
|
+
label: name,
|
|
68
|
+
log,
|
|
69
|
+
signal,
|
|
70
|
+
subscribe: (abortSignal) => subs
|
|
71
|
+
.programNotifications(program, {
|
|
72
|
+
commitment: 'processed',
|
|
73
|
+
encoding: 'base64',
|
|
74
|
+
// token account owner sits at data offset 32 for both programs
|
|
75
|
+
filters: [{ memcmp: { offset: 32n, bytes: ownerBytes, encoding: 'base58' } }],
|
|
76
|
+
})
|
|
77
|
+
.subscribe({ abortSignal }),
|
|
78
|
+
onNotification: (notif) => {
|
|
79
|
+
const data = notif?.value?.account?.data;
|
|
80
|
+
const amount = tokenAmountFromBase64(Array.isArray(data) ? data[0] : undefined);
|
|
81
|
+
if (amount > 0n)
|
|
82
|
+
onActivity(name);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Token account balance (u64 LE at offset 64), or 0n when absent/closed. */
|
|
88
|
+
export function tokenAmountFromBase64(data) {
|
|
89
|
+
if (!data)
|
|
90
|
+
return 0n;
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = atob(data);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return 0n;
|
|
97
|
+
}
|
|
98
|
+
if (raw.length < 72)
|
|
99
|
+
return 0n;
|
|
100
|
+
let amount = 0n;
|
|
101
|
+
for (let i = 71; i >= 64; i--) {
|
|
102
|
+
amount = (amount << 8n) | BigInt(raw.charCodeAt(i));
|
|
103
|
+
}
|
|
104
|
+
return amount;
|
|
105
|
+
}
|
|
106
|
+
async function runSubscription(params) {
|
|
107
|
+
const { label, subscribe, onNotification, log, signal } = params;
|
|
108
|
+
let backoffMs = 1000;
|
|
109
|
+
while (!signal?.aborted) {
|
|
110
|
+
// tie the subscription's lifetime to the caller's signal
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
const onAbort = () => controller.abort();
|
|
113
|
+
signal?.addEventListener('abort', onAbort);
|
|
114
|
+
try {
|
|
115
|
+
const notifications = await subscribe(controller.signal);
|
|
116
|
+
log?.(`Websocket: subscribed to ${label}`);
|
|
117
|
+
backoffMs = 1000;
|
|
118
|
+
for await (const notification of notifications) {
|
|
119
|
+
onNotification(notification);
|
|
120
|
+
}
|
|
121
|
+
if (!signal?.aborted)
|
|
122
|
+
log?.(`Websocket: ${label} subscription closed; reconnecting...`);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
if (!signal?.aborted) {
|
|
126
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
127
|
+
log?.(`Websocket: ${label} subscription failed (${msg}); retrying in ${Math.round(backoffMs / 1000)}s (polling continues meanwhile)`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
signal?.removeEventListener('abort', onAbort);
|
|
132
|
+
controller.abort();
|
|
133
|
+
}
|
|
134
|
+
if (signal?.aborted)
|
|
135
|
+
return;
|
|
136
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
137
|
+
backoffMs = Math.min(backoffMs * 2, 30_000);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sourceRoot":"","sources":["../../src/lib/watch.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nos-rescue",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rescue staked NOS (and all SOL/SPL tokens) from a compromised Solana wallet — a separate fee payer covers every fee and rent, so the hacked wallet never needs SOL",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Laurens Verspeek",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/laurensV/nos-rescue.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://laurensv.github.io/nos-rescue/",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/laurensV/nos-rescue/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"solana",
|
|
18
|
+
"nosana",
|
|
19
|
+
"nos",
|
|
20
|
+
"wallet",
|
|
21
|
+
"rescue",
|
|
22
|
+
"recovery",
|
|
23
|
+
"compromised-wallet",
|
|
24
|
+
"hacked-wallet",
|
|
25
|
+
"fee-payer",
|
|
26
|
+
"stake",
|
|
27
|
+
"unstake",
|
|
28
|
+
"security"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.18.0"
|
|
32
|
+
},
|
|
33
|
+
"bin": {
|
|
34
|
+
"nos-rescue": "dist/cli.js"
|
|
35
|
+
},
|
|
36
|
+
"main": "dist/lib/index.js",
|
|
37
|
+
"types": "dist/lib/index.d.ts",
|
|
38
|
+
"files": [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc",
|
|
43
|
+
"prepublishOnly": "npm run build && npm test",
|
|
44
|
+
"dev": "tsx src/cli.ts",
|
|
45
|
+
"typecheck": "tsc --noEmit",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:watch": "vitest",
|
|
48
|
+
"web": "vite",
|
|
49
|
+
"web:build": "vite build",
|
|
50
|
+
"web:build:single": "vite build --config vite.single.config.ts",
|
|
51
|
+
"web:preview": "vite preview"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@nosana/kit": "2.7.0",
|
|
55
|
+
"@solana-program/system": "0.10.0",
|
|
56
|
+
"@solana-program/token": "0.9.0",
|
|
57
|
+
"@solana/kit": "5.4.0",
|
|
58
|
+
"commander": "^12.1.0"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@types/node": "^20.19.27",
|
|
62
|
+
"tsx": "^4.19.0",
|
|
63
|
+
"typescript": "^5.7.0",
|
|
64
|
+
"vite": "^6.0.0",
|
|
65
|
+
"vite-plugin-singlefile": "^2.3.3",
|
|
66
|
+
"vitest": "^4.1.10"
|
|
67
|
+
}
|
|
68
|
+
}
|