strade-stx-scripts 1.0.0 → 1.0.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.
@@ -0,0 +1,9 @@
1
+ #!/bin/bash
2
+ REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
3
+ echo "Auto-activity started. Running every 3 minutes. Press Ctrl+C to stop."
4
+ while true; do
5
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] Running generate-activity..."
6
+ cd "$REPO_DIR" && npx tsx scripts/generate-activity.ts
7
+ echo "[$(date '+%Y-%m-%d %H:%M:%S')] Done. Sleeping 3 minutes..."
8
+ sleep 180
9
+ done
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // Cancels all pending mempool txs by replacing them with 0-value self-transfers at higher fee
3
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
4
+ import { getAddressFromPrivateKey, makeSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
5
+ import { StacksMainnet } from '@stacks/network';
6
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ const ACCOUNTS_TO_CANCEL = [24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49];
11
+ const CANCEL_FEE = 2000n; // higher than original to replace
12
+
13
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
14
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
15
+
16
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
17
+ for (let i = 1; i < 50; i++) wallet = generateNewAccount(wallet);
18
+
19
+ const network = new StacksMainnet();
20
+
21
+ for (const i of ACCOUNTS_TO_CANCEL) {
22
+ const pk = wallet.accounts[i].stxPrivateKey;
23
+ const address = getAddressFromPrivateKey(pk, network.version);
24
+
25
+ // Get pending txs from mempool
26
+ const mempool = JSON.parse(execSync(`curl -s 'https://api.hiro.so/extended/v1/address/${address}/mempool?limit=50'`).toString());
27
+ const pendingTxs = mempool.results ?? [];
28
+
29
+ if (pendingTxs.length === 0) {
30
+ console.log(`Account ${i}: ${address} — no pending txs`);
31
+ continue;
32
+ }
33
+
34
+ // Get unique nonces of pending txs
35
+ const nonces: Set<number> = new Set(pendingTxs.map((tx: any) => tx.nonce));
36
+ console.log(`Account ${i}: ${address} — cancelling nonces: ${[...nonces].join(', ')}`);
37
+
38
+ for (const nonce of nonces) {
39
+ const bal = BigInt(JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${address}?proof=0'`).toString()).balance);
40
+ if (bal < CANCEL_FEE) { console.log(` nonce ${nonce}: insufficient balance, skipping`); continue; }
41
+
42
+ const tx = await makeSTXTokenTransfer({
43
+ recipient: address, // send to self
44
+ amount: 1n,
45
+ senderKey: pk,
46
+ network,
47
+ anchorMode: AnchorMode.Any,
48
+ nonce,
49
+ fee: CANCEL_FEE,
50
+ });
51
+
52
+ const tmp = `/tmp/cancel_${i}_${nonce}.bin`;
53
+ writeFileSync(tmp, tx.serialize());
54
+ const result = execSync(`curl -s --max-time 30 -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmp}`, { timeout: 35000 }).toString();
55
+ unlinkSync(tmp);
56
+
57
+ const parsed = JSON.parse(result);
58
+ if (typeof parsed === 'string' && parsed.length === 64) {
59
+ console.log(` ✅ nonce ${nonce} replaced — txid: ${parsed}`);
60
+ } else {
61
+ console.error(` ❌ nonce ${nonce} — ${result}`);
62
+ }
63
+ await new Promise(r => setTimeout(r, 500));
64
+ }
65
+ }
66
+
67
+ console.log('\nDone! Run drain-accounts.ts after these confirm.');
@@ -0,0 +1,23 @@
1
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
2
+ import { getAddressFromPrivateKey } from '@stacks/transactions';
3
+ import { StacksMainnet } from '@stacks/network';
4
+ import { readFileSync } from 'fs';
5
+ import { resolve } from 'path';
6
+ import { execSync } from 'child_process';
7
+
8
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
9
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
10
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
11
+ for (let i = 1; i < 50; i++) wallet = generateNewAccount(wallet);
12
+ const network = new StacksMainnet();
13
+
14
+ let total = 0;
15
+ for (let i = 0; i < 50; i++) {
16
+ const address = getAddressFromPrivateKey(wallet.accounts[i].stxPrivateKey, network.version);
17
+ const d = JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${address}?proof=0'`).toString());
18
+ const bal = parseInt(d.balance, 16) / 1e6;
19
+ total += bal;
20
+ console.log(`Account ${i.toString().padStart(2)}: ${address} — ${bal.toFixed(6)} STX`);
21
+ await new Promise(r => setTimeout(r, 300));
22
+ }
23
+ console.log(`\nTotal: ${total.toFixed(6)} STX`);
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env node
2
+ // Distributes 15 STX evenly among all 50 accounts: 0.3 STX each. Account 0 retains its 0.3 STX share, sends 0.3 STX to accounts 1-49.
3
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
4
+ import { getAddressFromPrivateKey, makeSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
5
+ import { StacksMainnet } from '@stacks/network';
6
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ const curlFetch = async (url: string, opts: any = {}) => {
11
+ const method = opts.method || 'GET';
12
+ const headers = opts.headers ? Object.entries(opts.headers).map(([k,v]) => `-H '${k}: ${v}'`).join(' ') : '';
13
+ if (opts.body) {
14
+ const tmp = `/tmp/cf_${Date.now()}.bin`;
15
+ writeFileSync(tmp, opts.body);
16
+ const r = execSync(`curl -s --max-time 30 -X ${method} ${headers} --data-binary @${tmp} '${url}'`, { timeout: 35000 }).toString();
17
+ unlinkSync(tmp);
18
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
19
+ }
20
+ const r = execSync(`curl -s --max-time 15 '${url}'`, { timeout: 20000 }).toString();
21
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
22
+ };
23
+
24
+ const SEND_AMOUNT = 300_000n; // 0.3 STX each × 49 = 14.7 STX sent, account 0 retains 0.3 STX = 15 STX total
25
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
26
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
27
+
28
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
29
+ for (let i = 1; i < 50; i++) wallet = generateNewAccount(wallet);
30
+
31
+ const network = new StacksMainnet({ fetchFn: curlFetch as any });
32
+ const senderKey = wallet.accounts[0].stxPrivateKey;
33
+ const senderAddress = getAddressFromPrivateKey(senderKey, network.version);
34
+ const nonceData = JSON.parse(execSync(`curl -s 'https://api.hiro.so/extended/v1/address/${senderAddress}/nonces'`).toString());
35
+ let nonce = nonceData.possible_next_nonce;
36
+
37
+ console.log(`\nSender: ${senderAddress} | Nonce: ${nonce}`);
38
+ console.log(`Distributing ${Number(SEND_AMOUNT * 49n) / 1e6} STX total — account 0 retains ${Number(SEND_AMOUNT) / 1e6} STX, accounts 1-49 each receive ${Number(SEND_AMOUNT) / 1e6} STX\n`);
39
+
40
+ for (let i = 1; i < 50; i++) {
41
+ const address = getAddressFromPrivateKey(wallet.accounts[i].stxPrivateKey, network.version);
42
+ const tx = await makeSTXTokenTransfer({ recipient: address, amount: SEND_AMOUNT, senderKey, network, anchorMode: AnchorMode.Any, nonce });
43
+ const tmp = `/tmp/dist_${i}.bin`;
44
+ writeFileSync(tmp, tx.serialize());
45
+ const result = execSync(`curl -s --max-time 30 -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmp}`, { timeout: 35000 }).toString();
46
+ unlinkSync(tmp);
47
+ const parsed = JSON.parse(result);
48
+ if (typeof parsed === 'string' && parsed.length === 64) {
49
+ console.log(`✅ Account ${i}: ${address} — sent ${Number(SEND_AMOUNT) / 1e6} STX`);
50
+ } else {
51
+ console.error(`❌ Account ${i}: ${address} — ${result}`);
52
+ }
53
+ nonce++;
54
+ await new Promise(r => setTimeout(r, 500));
55
+ }
56
+ console.log(`\nDone! ${Number(SEND_AMOUNT * 49n) / 1e6} STX distributed across 49 accounts. Account 0 retains ${Number(SEND_AMOUNT) / 1e6} STX. Total: 15 STX evenly split among all 50 accounts.`);
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // Drains accounts 10-49 back to account 0
3
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
4
+ import { getAddressFromPrivateKey, makeSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
5
+ import { StacksMainnet } from '@stacks/network';
6
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ const NUM_ACCOUNTS = 41; // only drain accounts 35 and 40
11
+ const KEEP_ACCOUNTS = 35;
12
+ const FEE = 300n;
13
+
14
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
15
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
16
+
17
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
18
+ for (let i = 1; i < NUM_ACCOUNTS; i++) wallet = generateNewAccount(wallet);
19
+
20
+ const network = new StacksMainnet();
21
+ const recipient = getAddressFromPrivateKey(wallet.accounts[0].stxPrivateKey, network.version);
22
+ console.log(`Draining accounts ${KEEP_ACCOUNTS}-${NUM_ACCOUNTS - 1} → ${recipient}\n`);
23
+
24
+ for (let i = KEEP_ACCOUNTS; i < NUM_ACCOUNTS; i++) {
25
+ await new Promise(r => setTimeout(r, 4000)); // delay before each account to avoid rate limits
26
+ const pk = wallet.accounts[i].stxPrivateKey;
27
+ const address = getAddressFromPrivateKey(pk, network.version);
28
+ let d: any;
29
+ try {
30
+ d = JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${address}?proof=0'`).toString());
31
+ } catch { console.log(`Account ${i}: rate limited, skipping`); continue; }
32
+ const balance = BigInt(d.balance ?? '0x0');
33
+ const sendAmount = balance - FEE;
34
+
35
+ if (sendAmount <= 0n) {
36
+ console.log(`Account ${i}: ${address} — skipped (balance: ${Number(balance)/1e6} STX)`);
37
+ continue;
38
+ }
39
+
40
+ const nonceRaw = execSync(`curl -s 'https://api.hiro.so/extended/v1/address/${address}/nonces'`).toString();
41
+ if (nonceRaw.includes('rate limit')) { console.log(`Account ${i}: rate limited on nonce, skipping`); continue; }
42
+ const nonceData = JSON.parse(nonceRaw);
43
+ const nonce = nonceData.possible_next_nonce;
44
+
45
+ const tx = await makeSTXTokenTransfer({
46
+ recipient,
47
+ amount: sendAmount,
48
+ senderKey: pk,
49
+ network,
50
+ anchorMode: AnchorMode.Any,
51
+ nonce,
52
+ fee: FEE,
53
+ });
54
+
55
+ const tmp = `/tmp/drain_${i}.bin`;
56
+ writeFileSync(tmp, tx.serialize());
57
+ const result = execSync(`curl -s --max-time 30 -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmp}`, { timeout: 35000 }).toString();
58
+ unlinkSync(tmp);
59
+
60
+ if (result.includes('rate limit')) { console.log(`Account ${i}: rate limited on broadcast, skipping`); continue; }
61
+ const parsed = JSON.parse(result);
62
+ if (typeof parsed === 'string' && parsed.length === 64) {
63
+ console.log(`✅ Account ${i}: ${address} — drained ${Number(sendAmount)/1e6} STX — txid: ${parsed}`);
64
+ } else {
65
+ console.error(`❌ Account ${i}: ${address} — ${result}`);
66
+ }
67
+ await new Promise(r => setTimeout(r, 1000));
68
+ }
69
+
70
+ console.log('\nDone!');
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Funds derived accounts 1-49 from account 0.
4
+ * Run once before generate-activity.ts
5
+ */
6
+ import {
7
+ makeSTXTokenTransfer,
8
+ AnchorMode,
9
+ getAddressFromPrivateKey,
10
+ } from '@stacks/transactions';
11
+ import { StacksMainnet } from '@stacks/network';
12
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
13
+ import { readFileSync } from 'fs';
14
+ import { resolve } from 'path';
15
+ import { execSync } from 'child_process';
16
+
17
+ // Use curl-based fetch to bypass Node.js network restrictions
18
+ const curlFetch = async (url: string, opts: any = {}) => {
19
+ const method = opts.method || 'GET';
20
+ const body = opts.body ? `-d '${opts.body}'` : '';
21
+ const headers = opts.headers ? Object.entries(opts.headers).map(([k,v]) => `-H '${k}: ${v}'`).join(' ') : '';
22
+ const result = execSync(`curl -s -X ${method} ${headers} ${body} '${url}'`, { timeout: 15000 }).toString();
23
+ return { ok: true, json: async () => JSON.parse(result), text: async () => result };
24
+ };
25
+
26
+ const NUM_ACCOUNTS = 50;
27
+ const FUND_AMOUNT = 100_000n; // 0.1 STX in microSTX
28
+ const FEE = 300n;
29
+
30
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
31
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
32
+
33
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
34
+ for (let i = 1; i < NUM_ACCOUNTS; i++) wallet = generateNewAccount(wallet);
35
+
36
+ const network = new StacksMainnet({ fetchFn: curlFetch as any });
37
+ const senderKey = wallet.accounts[0].stxPrivateKey;
38
+ const senderAddress = getAddressFromPrivateKey(senderKey, network.version);
39
+
40
+ const res = await curlFetch(`https://api.hiro.so/v2/accounts/${senderAddress}?proof=0`);
41
+ const data = await res.json() as { nonce: number; balance: string };
42
+ let nonce = data.nonce;
43
+ const balance = BigInt(data.balance);
44
+
45
+ console.log(`Sender: ${senderAddress}`);
46
+ console.log(`Balance: ${Number(balance) / 1e6} STX`);
47
+ console.log(`Funding ${NUM_ACCOUNTS - 1} accounts with ${Number(FUND_AMOUNT) / 1e6} STX each\n`);
48
+
49
+ const totalNeeded = (FUND_AMOUNT + FEE) * BigInt(NUM_ACCOUNTS - 1) + FEE;
50
+ if (balance < totalNeeded) {
51
+ console.error(`Insufficient balance. Need ${Number(totalNeeded) / 1e6} STX, have ${Number(balance) / 1e6} STX`);
52
+ process.exit(1);
53
+ }
54
+
55
+ import { writeFileSync, unlinkSync } from 'fs';
56
+
57
+ for (let i = 1; i < NUM_ACCOUNTS; i++) {
58
+ const recipient = getAddressFromPrivateKey(wallet.accounts[i].stxPrivateKey, network.version);
59
+ const tx = await makeSTXTokenTransfer({
60
+ recipient,
61
+ amount: FUND_AMOUNT,
62
+ senderKey,
63
+ network,
64
+ anchorMode: AnchorMode.Any,
65
+ nonce,
66
+ fee: FEE,
67
+ });
68
+
69
+ // Write serialized tx to temp file and POST via curl
70
+ const tmpFile = `/tmp/stx_tx_${i}.bin`;
71
+ writeFileSync(tmpFile, tx.serialize());
72
+ const result = execSync(
73
+ `curl -s -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmpFile}`,
74
+ { timeout: 15000 }
75
+ ).toString();
76
+ unlinkSync(tmpFile);
77
+
78
+ const parsed = JSON.parse(result);
79
+ if (typeof parsed === 'string' && parsed.length === 64) {
80
+ console.log(`✅ Funded account ${i}: ${recipient} — txid: ${parsed}`);
81
+ } else {
82
+ console.error(`❌ Failed account ${i}: ${recipient} — ${result}`);
83
+ }
84
+ nonce++;
85
+ await new Promise(r => setTimeout(r, 500));
86
+ }
87
+
88
+ console.log('\nDone! Wait ~10 min for txs to confirm before running generate-activity.ts');
package/fund-active.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+ // Funds only accounts 1-9 (the 10 active accounts) if balance < 0.05 STX
3
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
4
+ import { getAddressFromPrivateKey, makeSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
5
+ import { StacksMainnet } from '@stacks/network';
6
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
7
+ import { resolve } from 'path';
8
+ import { execSync } from 'child_process';
9
+
10
+ const curlFetch = async (url: string, opts: any = {}) => {
11
+ const method = opts.method || 'GET';
12
+ const headers = opts.headers ? Object.entries(opts.headers).map(([k,v]) => `-H '${k}: ${v}'`).join(' ') : '';
13
+ if (opts.body) {
14
+ const tmp = `/tmp/cf_${Date.now()}.bin`;
15
+ writeFileSync(tmp, opts.body);
16
+ const r = execSync(`curl -s --max-time 30 -X ${method} ${headers} --data-binary @${tmp} '${url}'`, { timeout: 35000 }).toString();
17
+ unlinkSync(tmp);
18
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
19
+ }
20
+ const r = execSync(`curl -s --max-time 15 '${url}'`, { timeout: 20000 }).toString();
21
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
22
+ };
23
+
24
+ const FUND_AMOUNT = 300_000n; // 0.3 STX — evenly distributes ~15 STX across 50 accounts
25
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
26
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
27
+
28
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
29
+ for (let i = 1; i < 50; i++) wallet = generateNewAccount(wallet);
30
+
31
+ const network = new StacksMainnet({ fetchFn: curlFetch as any });
32
+ const senderKey = wallet.accounts[0].stxPrivateKey;
33
+ const senderAddress = getAddressFromPrivateKey(senderKey, network.version);
34
+ const nonceData = JSON.parse(execSync(`curl -s 'https://api.hiro.so/extended/v1/address/${senderAddress}/nonces'`).toString());
35
+ let nonce = nonceData.possible_next_nonce;
36
+
37
+ console.log(`Sender: ${senderAddress} | Nonce: ${nonce}\n`);
38
+
39
+ for (let i = 1; i < 50; i++) {
40
+ const address = getAddressFromPrivateKey(wallet.accounts[i].stxPrivateKey, network.version);
41
+ const d = JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${address}?proof=0'`).toString());
42
+ const bal = parseInt(d.balance, 16);
43
+ if (bal >= 50_000) { console.log(`Account ${i}: ${address} — ${(bal/1e6).toFixed(3)} STX OK`); continue; }
44
+
45
+ const tx = await makeSTXTokenTransfer({ recipient: address, amount: FUND_AMOUNT, senderKey, network, anchorMode: AnchorMode.Any, nonce });
46
+ const tmp = `/tmp/fund10_${i}.bin`;
47
+ writeFileSync(tmp, tx.serialize());
48
+ const result = execSync(`curl -s --max-time 30 -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmp}`, { timeout: 35000 }).toString();
49
+ unlinkSync(tmp);
50
+ const parsed = JSON.parse(result);
51
+ if (typeof parsed === 'string' && parsed.length === 64) {
52
+ console.log(`✅ Account ${i}: ${address} — funded 0.2 STX`);
53
+ } else {
54
+ console.error(`❌ Account ${i}: ${address} — ${result}`);
55
+ }
56
+ nonce++;
57
+ await new Promise(r => setTimeout(r, 500));
58
+ }
59
+ console.log('\nDone!');
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
3
+ import { getAddressFromPrivateKey, makeSTXTokenTransfer, AnchorMode } from '@stacks/transactions';
4
+ import { StacksMainnet } from '@stacks/network';
5
+ import { readFileSync, writeFileSync, unlinkSync } from 'fs';
6
+ import { resolve } from 'path';
7
+ import { execSync } from 'child_process';
8
+
9
+ const curlFetch = async (url: string, opts: any = {}) => {
10
+ const method = opts.method || 'GET';
11
+ const headers = opts.headers ? Object.entries(opts.headers).map(([k,v]) => `-H '${k}: ${v}'`).join(' ') : '';
12
+ if (opts.body) {
13
+ const tmp = `/tmp/stx_fund_${Date.now()}.bin`;
14
+ writeFileSync(tmp, opts.body);
15
+ const r = execSync(`curl -s -X ${method} ${headers} --data-binary @${tmp} '${url}'`, { timeout: 15000 }).toString();
16
+ unlinkSync(tmp);
17
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
18
+ }
19
+ const r = execSync(`curl -s '${url}'`, { timeout: 15000 }).toString();
20
+ return { ok: true, json: async () => JSON.parse(r), text: async () => r };
21
+ };
22
+
23
+ const FUND_AMOUNT = 200_000n; // 0.2 STX
24
+ const NUM_ACCOUNTS = 50;
25
+
26
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
27
+ const mnemonic = toml.match(/mnemonic\s*=\s*"([^"]+)"/)![1];
28
+
29
+ let wallet = await generateWallet({ secretKey: mnemonic, password: '' });
30
+ for (let i = 1; i < NUM_ACCOUNTS; i++) wallet = generateNewAccount(wallet);
31
+
32
+ const network = new StacksMainnet({ fetchFn: curlFetch as any });
33
+ const senderKey = wallet.accounts[0].stxPrivateKey;
34
+ const senderAddress = getAddressFromPrivateKey(senderKey, network.version);
35
+
36
+ // Get sender nonce (possible_next to include mempool)
37
+ const nonceData = JSON.parse(execSync(`curl -s 'https://api.hiro.so/extended/v1/address/${senderAddress}/nonces'`).toString());
38
+ let nonce = nonceData.possible_next_nonce;
39
+
40
+ const balData = JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${senderAddress}?proof=0'`).toString());
41
+ const balance = BigInt(balData.balance);
42
+ console.log(`Sender: ${senderAddress} | Balance: ${Number(balance)/1e6} STX | Nonce: ${nonce}\n`);
43
+
44
+ // Find accounts with low balance (< 0.05 STX)
45
+ const unfunded: { idx: number; address: string }[] = [];
46
+ for (let i = 1; i < NUM_ACCOUNTS; i++) {
47
+ const address = getAddressFromPrivateKey(wallet.accounts[i].stxPrivateKey, network.version);
48
+ const d = JSON.parse(execSync(`curl -s 'https://api.hiro.so/v2/accounts/${address}?proof=0'`).toString());
49
+ const bal = parseInt(d.balance, 16);
50
+ console.log(`Account ${i}: ${address} — ${(bal/1e6).toFixed(3)} STX`);
51
+ if (bal < 50_000) unfunded.push({ idx: i, address }); // top up if < 0.05 STX
52
+ await new Promise(r => setTimeout(r, 600));
53
+ }
54
+
55
+ console.log(`\nFound ${unfunded.length} accounts with low balance. Topping up each with 0.1 STX...\n`);
56
+
57
+ const totalNeeded = FUND_AMOUNT * BigInt(unfunded.length);
58
+ if (balance < totalNeeded) {
59
+ console.error(`Insufficient balance. Need ${Number(totalNeeded)/1e6} STX, have ${Number(balance)/1e6} STX`);
60
+ process.exit(1);
61
+ }
62
+
63
+ for (const { idx, address } of unfunded) {
64
+ const tx = await makeSTXTokenTransfer({
65
+ recipient: address,
66
+ amount: FUND_AMOUNT,
67
+ senderKey,
68
+ network,
69
+ anchorMode: AnchorMode.Any,
70
+ nonce,
71
+ });
72
+
73
+ const tmp = `/tmp/stx_fund_${idx}.bin`;
74
+ writeFileSync(tmp, tx.serialize());
75
+ const result = execSync(`curl -s --max-time 30 -X POST 'https://api.hiro.so/v2/transactions' -H 'Content-Type: application/octet-stream' --data-binary @${tmp}`, { timeout: 35000 }).toString();
76
+ unlinkSync(tmp);
77
+
78
+ const parsed = JSON.parse(result);
79
+ if (typeof parsed === 'string' && parsed.length === 64) {
80
+ console.log(`✅ Account ${idx}: ${address} — txid: ${parsed}`);
81
+ } else {
82
+ console.error(`❌ Account ${idx}: ${address} — ${result}`);
83
+ }
84
+ nonce++;
85
+ await new Promise(r => setTimeout(r, 300));
86
+ }
87
+
88
+ console.log('\nDone!');
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Strade Activity Generator
4
+ * Usage:
5
+ * 1. Set PRIVATE_KEY below (64-char hex)
6
+ * 2. Set DRY_RUN=false when ready
7
+ * 3. npx tsx scripts/generate-activity.ts
8
+ */
9
+
10
+ import {
11
+ broadcastTransaction,
12
+ makeContractCall,
13
+ noneCV,
14
+ principalCV,
15
+ stringUtf8CV,
16
+ uintCV,
17
+ AnchorMode,
18
+ } from '@stacks/transactions';
19
+ import { StacksMainnet } from '@stacks/network';
20
+ import { generateWallet, generateNewAccount } from '@stacks/wallet-sdk';
21
+ import { readFileSync } from 'fs';
22
+ import { resolve } from 'path';
23
+ import { execSync } from 'child_process';
24
+
25
+ const DRY_RUN = false; // set to false to broadcast
26
+
27
+ // curl-based fetch to bypass Node.js network restrictions
28
+ const curlFetch = async (url: string, opts: any = {}) => {
29
+ const method = opts.method || 'GET';
30
+ const headers = opts.headers ? Object.entries(opts.headers).map(([k,v]) => `-H '${k}: ${v}'`).join(' ') : '';
31
+ if (opts.body) {
32
+ const tmpFile = `/tmp/stx_req_${Date.now()}.bin`;
33
+ const { writeFileSync, unlinkSync } = await import('fs');
34
+ writeFileSync(tmpFile, opts.body);
35
+ const result = execSync(`curl -s --max-time 30 -X ${method} ${headers} --data-binary @${tmpFile} '${url}'`, { timeout: 35000 }).toString();
36
+ unlinkSync(tmpFile);
37
+ return { ok: true, json: async () => JSON.parse(result), text: async () => result };
38
+ }
39
+ const result = execSync(`curl -s -X ${method} ${headers} '${url}'`, { timeout: 15000 }).toString();
40
+ return { ok: true, json: async () => JSON.parse(result), text: async () => result };
41
+ };
42
+
43
+ // Read mnemonic from settings/Mainnet.toml
44
+ const toml = readFileSync(resolve('settings/Mainnet.toml'), 'utf8');
45
+ const mnemonicMatch = toml.match(/mnemonic\s*=\s*"([^"]+)"/);
46
+ if (!mnemonicMatch) { console.error('mnemonic not found in settings/Mainnet.toml'); process.exit(1); }
47
+ const MNEMONIC = mnemonicMatch[1];
48
+
49
+ // Derive 100 accounts from the same mnemonic (indices 0-99)
50
+ const NUM_ACCOUNTS = 50;
51
+ const baseWallet = await generateWallet({ secretKey: MNEMONIC, password: '' });
52
+ let wallet = baseWallet;
53
+ for (let i = 1; i < NUM_ACCOUNTS; i++) {
54
+ wallet = generateNewAccount(wallet);
55
+ }
56
+ const accounts = wallet.accounts.slice(0, NUM_ACCOUNTS).map(a => a.stxPrivateKey);
57
+ const CONTRACT_PRINCIPAL = 'SPB669EVRTKWYGY5GNQ7VEBZ7RF8A3K01EP6GN8N';
58
+ const DELAY_MS = 3000;
59
+
60
+ const network = new StacksMainnet({ fetchFn: curlFetch as any });
61
+ const rand = () => Math.random().toString(36).slice(2, 8);
62
+
63
+ async function getNonce(address: string): Promise<number> {
64
+ for (let attempt = 1; attempt <= 3; attempt++) {
65
+ try {
66
+ const result = execSync(`curl -s --max-time 15 'https://api.hiro.so/extended/v1/address/${address}/nonces'`, { timeout: 20000 }).toString();
67
+ if (result.includes('Per-minute') || result.includes('rate limit')) throw new Error('rate limited');
68
+ const data = JSON.parse(result) as { possible_next_nonce: number };
69
+ return data.possible_next_nonce;
70
+ } catch (e: any) {
71
+ console.error(` getNonce attempt ${attempt}/3 failed: ${e.message}`);
72
+ if (attempt < 3) await new Promise(r => setTimeout(r, 10000)); // 10s backoff on rate limit
73
+ }
74
+ }
75
+ throw new Error('Failed to get nonce after 3 attempts');
76
+ }
77
+
78
+ async function sendTx(contractName: string, fn: string, args: any[], nonce: number, privateKey: string) {
79
+ const tx = await makeContractCall({
80
+ contractAddress: CONTRACT_PRINCIPAL,
81
+ contractName,
82
+ functionName: fn,
83
+ functionArgs: args,
84
+ senderKey: privateKey,
85
+ validateWithAbi: false,
86
+ network,
87
+ anchorMode: AnchorMode.Any,
88
+ nonce,
89
+ fee: 4_000, // 0.2 STX total per cycle across 50 accounts
90
+ });
91
+
92
+ console.log(`[${nonce}] ${contractName}.${fn}`);
93
+
94
+ if (DRY_RUN) {
95
+ console.log(' DRY-RUN: skipped');
96
+ return;
97
+ }
98
+
99
+ try {
100
+ const res = await broadcastTransaction(tx, network);
101
+ if ('txid' in res) {
102
+ console.log(` ✅ https://explorer.hiro.so/txid/${res.txid}`);
103
+ } else {
104
+ console.error(` ❌ ${JSON.stringify(res)}`);
105
+ }
106
+ } catch (e: any) {
107
+ console.error(` ❌ ${e.message}`);
108
+ }
109
+
110
+ await new Promise(r => setTimeout(r, DELAY_MS));
111
+ }
112
+
113
+ async function main() {
114
+ const { getAddressFromPrivateKey } = await import('@stacks/transactions');
115
+
116
+ // Build per-account state sequentially to avoid rate limiting
117
+ const allStates = [];
118
+ for (let i = 0; i < accounts.length; i++) {
119
+ const pk = accounts[i];
120
+ const { getAddressFromPrivateKey } = await import('@stacks/transactions');
121
+ const address = getAddressFromPrivateKey(pk, network.version);
122
+ const nonce = await getNonce(address);
123
+ let pending = 0, balance = 0;
124
+ try {
125
+ const mempool = JSON.parse(execSync(`curl -s --max-time 15 'https://api.hiro.so/extended/v1/address/${address}/mempool?limit=1'`, { timeout: 20000 }).toString());
126
+ pending = mempool.total ?? 0;
127
+ const balData = JSON.parse(execSync(`curl -s --max-time 15 'https://api.hiro.so/v2/accounts/${address}?proof=0'`, { timeout: 20000 }).toString());
128
+ balance = parseInt(balData.balance, 16);
129
+ } catch { pending = 0; }
130
+ console.log(`Account ${i}: ${address} (nonce: ${nonce}, pending: ${pending}, balance: ${(balance/1e6).toFixed(4)} STX)`);
131
+ allStates.push({ pk, address, nonce, pending, balance });
132
+ await new Promise(r => setTimeout(r, 2000));
133
+ }
134
+
135
+ const accountStates = allStates.filter(s => s.pending < 3 && s.balance > 1000);
136
+ if (accountStates.length === 0) { console.log('All accounts have too many pending txs. Try again later.'); return; }
137
+ console.log(`\nUsing ${accountStates.length}/${allStates.length} accounts\nDRY_RUN=${DRY_RUN}\n`);
138
+
139
+ // 1 tx per account per run — rotate through different contract calls
140
+ // Rotate through different contract calls - 1 per account per run
141
+ const calls = [
142
+ (s: any) => sendTx('UserProfile', 'update-profile', [
143
+ stringUtf8CV(`Bio ${rand()}`), stringUtf8CV(`${rand()}@test.com`)
144
+ ], s.nonce++, s.pk),
145
+
146
+ (s: any) => sendTx('CoreMarketPlace', 'create-listing', [
147
+ stringUtf8CV(`Item ${rand()}`), stringUtf8CV(`Desc ${rand()}`), uintCV(500_000), uintCV(144)
148
+ ], s.nonce++, s.pk),
149
+
150
+ (s: any) => sendTx('CoreMarketPlace', 'update-listing', [
151
+ uintCV(Math.floor(Math.random() * 100) + 1),
152
+ uintCV(750_000),
153
+ stringUtf8CV(`Updated desc ${rand()}`)
154
+ ], s.nonce++, s.pk),
155
+
156
+ (s: any) => sendTx('UserProfile', 'rate-user', [
157
+ principalCV(accountStates[(accountStates.indexOf(s) + 1) % accountStates.length].address),
158
+ uintCV(Math.floor(Math.random() * 5) + 1)
159
+ ], s.nonce++, s.pk),
160
+
161
+ (s: any) => sendTx('UserProfile', 'calculate-reputation', [
162
+ principalCV(s.address)
163
+ ], s.nonce++, s.pk),
164
+
165
+ (s: any) => sendTx('CoreMarketPlace', 'create-listing', [
166
+ stringUtf8CV(`Product ${rand()}`), stringUtf8CV(`Details ${rand()}`), uintCV(1_000_000), uintCV(288)
167
+ ], s.nonce++, s.pk),
168
+ ];
169
+
170
+ for (let i = 0; i < accountStates.length; i++) {
171
+ const s = accountStates[i];
172
+ // Re-fetch nonce right before sending to avoid BadNonce
173
+ s.nonce = await getNonce(s.address);
174
+ await calls[i % calls.length](s);
175
+ await new Promise(r => setTimeout(r, 500));
176
+ }
177
+
178
+ console.log('\nDone!');
179
+ }
180
+
181
+ main().catch(console.error);
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Git Activity Generator for Strade
4
+ * Creates commits for GitHub activity. Supports resuming from existing commits.
5
+ *
6
+ * Usage:
7
+ * npx tsx scripts/git-activity-generator.ts --help
8
+ * npx tsx scripts/git-activity-generator.ts --dry-run
9
+ * npx tsx scripts/git-activity-generator.ts
10
+ * npx tsx scripts/git-activity-generator.ts --total 500
11
+ *
12
+ * WARNINGS:
13
+ * - Creates temp files in ./temp-commits/ and commits them.
14
+ * - Rate limited by GitHub (consider delays).
15
+ * - Check GitHub TOS; for testing only.
16
+ * - Runs `git push`, `gh pr create`. Ensure authenticated.
17
+ * - Cleanup: rm -rf temp-commits/ && git branch -D fake-activity-pr-*
18
+ */
19
+
20
+ import { execSync } from 'child_process';
21
+ import { mkdirSync, writeFileSync, existsSync, rmSync, readdirSync } from 'fs';
22
+ import { dirname, join } from 'path';
23
+ import { fileURLToPath } from 'url';
24
+
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const TEMP_DIR = join(__dirname, '../temp-commits');
27
+ const NUM_PRS = 5;
28
+ const TOTAL_COMMITS = process.argv.includes('--total') ? parseInt(process.argv[process.argv.indexOf('--total') + 1] || '100') : 100;
29
+ const COMMITS_PER_PR = Math.ceil(TOTAL_COMMITS / NUM_PRS);
30
+ const PR_START = 194;
31
+ const COUNTER_OFFSET = 35044;
32
+ const DRY_RUN = process.argv.includes('--dry-run');
33
+ const HELP = process.argv.includes('--help');
34
+
35
+ if (HELP) {
36
+ console.log(`Usage: npx tsx ${join('scripts/git-activity-generator.ts')} [--dry-run] [--help] [--total <num>]\n`);
37
+ console.log('Options:\n --dry-run Simulate without git/gh commands\n --help Show this help\n --total Number of commits to make (default: 500)\n');
38
+ process.exit(0);
39
+ }
40
+
41
+ function run(cmd: string, options: { cwd?: string; dryRun?: boolean } = {}) {
42
+ const { cwd, dryRun } = options;
43
+ if (dryRun || DRY_RUN) {
44
+ console.log(`[DRY-RUN] cd ${cwd || process.cwd()} && ${cmd}`);
45
+ return;
46
+ }
47
+ try {
48
+ execSync(cmd, { cwd, stdio: 'inherit' });
49
+ } catch (e: any) {
50
+ console.error(`Error: ${e.message}`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ function log(msg: string) {
56
+ console.log(`\n>>> ${msg}`);
57
+ }
58
+
59
+ async function main() {
60
+ // Get existing file count to continue from where we left off
61
+ let startCommit = 1;
62
+ if (existsSync(TEMP_DIR)) {
63
+ const existingFiles = readdirSync(TEMP_DIR).filter(f => f.startsWith('counter') && f.endsWith('.txt'));
64
+ if (existingFiles.length > 0) {
65
+ const maxCounter = Math.max(...existingFiles.map(f => parseInt(f.replace('counter', '').replace('.txt', ''))));
66
+ startCommit = maxCounter - COUNTER_OFFSET + 1;
67
+ console.log(`\n>>> Continuing from existing commit ${startCommit} (found ${existingFiles.length} existing files, max counter: ${maxCounter})`);
68
+ } else {
69
+ // Cleanup previous if empty
70
+ rmSync(TEMP_DIR, { recursive: true });
71
+ mkdirSync(TEMP_DIR, { recursive: true });
72
+ }
73
+ } else {
74
+ mkdirSync(TEMP_DIR, { recursive: true });
75
+ }
76
+
77
+ // Create new temp files only for commits that don't exist yet
78
+ for (let i = startCommit; i <= TOTAL_COMMITS; i++) {
79
+ const file = join(TEMP_DIR, `counter${COUNTER_OFFSET + i}.txt`);
80
+ if (!existsSync(file)) {
81
+ writeFileSync(file, `Commit counter #${COUNTER_OFFSET + i} - ${Date.now()}\nMinor change for activity.\n`);
82
+ }
83
+ }
84
+
85
+ run('git add temp-commits/');
86
+ try { execSync('git commit -m "chore: add temp-commits dir for activity tracking"', { stdio: 'inherit' }); } catch {}
87
+
88
+ for (let pr = 1; pr <= NUM_PRS; pr++) {
89
+ log(`=== PR ${pr}/${NUM_PRS} (commits ${((pr-1)*COMMITS_PER_PR + 1)}-${pr*COMMITS_PER_PR}) ===`);
90
+
91
+ const branch = `fake-activity-pr-${PR_START + pr - 1}-${COUNTER_OFFSET}`;
92
+
93
+ // Create & switch branch
94
+ run(`git checkout -b ${branch}`, { dryRun: DRY_RUN });
95
+
96
+ // Make commits for this PR
97
+ for (let c = (pr-1)*COMMITS_PER_PR + 1; c <= Math.min(pr*COMMITS_PER_PR, TOTAL_COMMITS); c++) {
98
+ if (c > TOTAL_COMMITS) break;
99
+ const file = join(TEMP_DIR, `counter${COUNTER_OFFSET + c}.txt`);
100
+ const content = `Commit counter #${COUNTER_OFFSET + c} - ${Date.now() + c}\nUpdated at ${new Date().toISOString()}\nMinor change for activity.\n`;
101
+ writeFileSync(file, content);
102
+
103
+ run(`git add temp-commits/counter${COUNTER_OFFSET + c}.txt`, { cwd: process.cwd(), dryRun: DRY_RUN });
104
+ if ((c - 1) % 50 === 0 || c === Math.min(pr*COMMITS_PER_PR, TOTAL_COMMITS)) {
105
+ console.log(` Progress: PR${pr} commit ${c}/${Math.min(pr*COMMITS_PER_PR, TOTAL_COMMITS)} (${Math.min(TOTAL_COMMITS, pr*COMMITS_PER_PR)}/${TOTAL_COMMITS} total)`);
106
+ }
107
+ run(`git commit -m "chore: bump counter ${COUNTER_OFFSET + c} for activity tracking"`, { dryRun: DRY_RUN });
108
+ }
109
+
110
+ // Push & create PR
111
+ run(`git push origin ${branch}`, { dryRun: DRY_RUN });
112
+ run(`gh pr create --title "chore: activity batch #${pr} - ${COMMITS_PER_PR} minor updates" --body "Batch of ${COMMITS_PER_PR} commits for activity tracking. Changes in temp-commits/. #automation" --base main`, { dryRun: DRY_RUN });
113
+
114
+ log(`PR ${pr} created! Merging to main...`);
115
+
116
+ // Switch back to main
117
+ run(`git checkout main`, { dryRun: DRY_RUN });
118
+
119
+ // Merge the PR and delete the remote branch
120
+ run(`gh pr merge ${branch} --merge --delete-branch --subject "chore: merge activity batch #${pr}"`, { dryRun: DRY_RUN });
121
+
122
+ // Delete local branch if it still exists
123
+ try { execSync(`git branch -D ${branch}`, { stdio: 'inherit' }); } catch {}
124
+
125
+ // Pull latest main
126
+ run(`git pull origin main`, { dryRun: DRY_RUN });
127
+
128
+ log(`PR ${pr} merged and deleted!`);
129
+ }
130
+
131
+ // Safety: stash if dirty, checkout main
132
+ try { execSync('git stash push -m "pre-activity auto-save"', { stdio: 'inherit' }); } catch {}
133
+ try { execSync('git checkout main', { stdio: 'inherit' }); } catch {
134
+ execSync('git checkout master', { stdio: 'inherit' });
135
+ }
136
+ try { execSync('git stash pop', { stdio: 'inherit' }); } catch {}
137
+
138
+ // Clean up temp-commits
139
+ if (existsSync(TEMP_DIR)) {
140
+ rmSync(TEMP_DIR, { recursive: true });
141
+ log('Temp files cleaned up');
142
+ }
143
+
144
+ // Update counter file
145
+ writeFileSync('.activity_counter', TOTAL_COMMITS.toString());
146
+ console.log(`Updated .activity_counter to ${TOTAL_COMMITS}`);
147
+
148
+ log('✅ Complete! All PRs merged and deleted.');
149
+ if (!DRY_RUN) {
150
+ console.log(`\nSummary: ${TOTAL_COMMITS} commits across ${NUM_PRS} PRs, all merged to main and PRs deleted.`);
151
+ }
152
+ }
153
+
154
+ main().catch(console.error);
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env tsx
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { join, dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { networkInterfaces } from "node:os";
7
+
8
+ const PORT = parseInt(process.env.PORT || "3456", 10);
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const REPO_DIR = join(__dirname, "..");
11
+
12
+ let childProcess: ReturnType<typeof spawn> | null = null;
13
+
14
+ const html = `<!DOCTYPE html>
15
+ <html lang="en">
16
+ <head>
17
+ <meta charset="UTF-8">
18
+ <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
19
+ <title>Strade Auto-Activity</title>
20
+ <style>
21
+ *{box-sizing:border-box;margin:0;padding:0}
22
+ body{font-family:system-ui,-apple-system,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100dvh;background:#0f172a;color:#e2e8f0}
23
+ .container{text-align:center;padding:2rem;width:100%;max-width:400px}
24
+ h1{font-size:1.5rem;margin-bottom:.5rem;color:#f8fafc}
25
+ p{font-size:.875rem;color:#94a3b8;margin-bottom:2rem}
26
+ .btn{display:block;width:100%;padding:1rem;font-size:1.25rem;font-weight:600;border:none;border-radius:12px;cursor:pointer;margin-bottom:1rem;transition:opacity .2s}
27
+ .btn:active{opacity:.7}
28
+ .btn:disabled{opacity:.4;cursor:not-allowed}
29
+ .btn-start{background:#22c55e;color:#052e16}
30
+ .btn-stop{background:#ef4444;color:#450a0a}
31
+ #status{margin-top:1rem;padding:.75rem 1rem;border-radius:8px;font-weight:600;font-size:1rem}
32
+ .running{background:#166534;color:#86efac}
33
+ .idle{background:#1e293b;color:#94a3b8}
34
+ .error{background:#7f1d1d;color:#fca5a5}
35
+ .log{background:#1e293b;border-radius:8px;padding:.75rem;margin-top:1rem;max-height:200px;overflow-y:auto;text-align:left;font-family:monospace;font-size:.75rem;line-height:1.4;color:#94a3b8}
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="container">
40
+ <h1>Strade Auto-Activity</h1>
41
+ <p>Start or stop the auto-activity loop</p>
42
+ <button class="btn btn-start" id="startBtn" onclick="run('/start')">▶ Start</button>
43
+ <button class="btn btn-stop" id="stopBtn" onclick="run('/stop')">■ Stop</button>
44
+ <div id="status" class="idle">Idle</div>
45
+ <div class="log" id="log">Ready.</div>
46
+ </div>
47
+ <script>
48
+ function log(msg){const el=document.getElementById('log');el.textContent+=msg;el.scrollTop=el.scrollHeight}
49
+ async function run(path){const btn=event.target;btn.disabled=true;try{const r=await fetch(path,{method:'POST'});log(await r.text()+'. ')}catch(e){log('Error: '+e.message+'. ')}finally{btn.disabled=false;poll()}}
50
+ async function poll(){try{const r=await fetch('/status');const d=await r.json();const el=document.getElementById('status');if(d.running){el.textContent='Running';el.className='running'}else if(d.error){el.textContent='Error';el.className='error';log('['+d.error+'] ')}else{el.textContent='Idle';el.className='idle'}}catch(e){const el=document.getElementById('status');el.textContent='Offline';el.className='error'}}
51
+ setInterval(poll,3000);poll()
52
+ </script>
53
+ </body>
54
+ </html>`;
55
+
56
+ const server = createServer((req, res) => {
57
+ res.setHeader("Access-Control-Allow-Origin", "*");
58
+
59
+ if (req.method === "GET" && req.url === "/") {
60
+ res.writeHead(200, { "Content-Type": "text/html" });
61
+ res.end(html);
62
+ return;
63
+ }
64
+
65
+ if (req.method === "POST" && req.url === "/start") {
66
+ if (childProcess) {
67
+ res.writeHead(200, { "Content-Type": "text/plain" });
68
+ res.end("Already running");
69
+ return;
70
+ }
71
+ const scriptPath = join(REPO_DIR, "scripts", "auto-activity.sh");
72
+ childProcess = spawn("bash", [scriptPath], {
73
+ cwd: REPO_DIR,
74
+ stdio: "inherit",
75
+ });
76
+ childProcess.on("exit", (code) => {
77
+ childProcess = null;
78
+ });
79
+ childProcess.on("error", () => {
80
+ childProcess = null;
81
+ });
82
+ res.writeHead(200, { "Content-Type": "text/plain" });
83
+ res.end("Started");
84
+ return;
85
+ }
86
+
87
+ if (req.method === "POST" && req.url === "/stop") {
88
+ if (childProcess) {
89
+ childProcess.kill("SIGTERM");
90
+ childProcess = null;
91
+ }
92
+ res.writeHead(200, { "Content-Type": "text/plain" });
93
+ res.end("Stopped");
94
+ return;
95
+ }
96
+
97
+ if (req.method === "GET" && req.url === "/status") {
98
+ res.writeHead(200, { "Content-Type": "application/json" });
99
+ res.end(JSON.stringify({ running: childProcess !== null }));
100
+ return;
101
+ }
102
+
103
+ res.writeHead(404);
104
+ res.end("Not found");
105
+ });
106
+
107
+ server.listen(PORT, "0.0.0.0", () => {
108
+ const ifaces = networkInterfaces();
109
+ const ips: string[] = [];
110
+ for (const name of Object.keys(ifaces)) {
111
+ for (const iface of ifaces[name] || []) {
112
+ if (iface.family === "IPv4" && !iface.internal) {
113
+ ips.push(iface.address);
114
+ }
115
+ }
116
+ }
117
+ console.log(`Mobile control server running:`);
118
+ console.log(` Local: http://localhost:${PORT}`);
119
+ for (const ip of ips) {
120
+ console.log(` Network: http://${ip}:${PORT}`);
121
+ }
122
+ console.log(`Open one of the Network URLs on your phone's browser.`);
123
+ });
package/package.json CHANGED
@@ -1,7 +1,15 @@
1
1
  {
2
2
  "name": "strade-stx-scripts",
3
- "version": "1.0.0",
4
- "description": "Utility scripts for Strade - git activity generation, STX distribution, account management on Stacks",
5
- "main": "index.js",
6
- "license": "ISC"
3
+ "version": "1.0.1",
4
+ "description": "Utility scripts for Strade",
5
+ "type": "module",
6
+ "license": "ISC",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Marvy247/Strade.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/Marvy247/Strade/issues"
13
+ },
14
+ "homepage": "https://github.com/Marvy247/Strade"
7
15
  }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = { name: "strade-stx-scripts", version: "1.0.0" };