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.
- package/auto-activity.sh +9 -0
- package/cancel-pending.ts +67 -0
- package/check-balances.ts +23 -0
- package/distribute-evenly.ts +56 -0
- package/drain-accounts.ts +70 -0
- package/fund-accounts.ts +88 -0
- package/fund-active.ts +59 -0
- package/fund-unfunded.ts +88 -0
- package/generate-activity.ts +181 -0
- package/git-activity-generator.ts +154 -0
- package/mobile-server.ts +123 -0
- package/package.json +12 -4
- package/index.js +0 -1
package/auto-activity.sh
ADDED
|
@@ -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!');
|
package/fund-accounts.ts
ADDED
|
@@ -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!');
|
package/fund-unfunded.ts
ADDED
|
@@ -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);
|
package/mobile-server.ts
ADDED
|
@@ -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.
|
|
4
|
-
"description": "Utility scripts for Strade
|
|
5
|
-
"
|
|
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" };
|