From 98bf078a773f40f4b391618cb8483b862934d5bc Mon Sep 17 00:00:00 2001 From: Sybi Date: Sun, 11 Oct 2026 17:04:37 +0000 Subject: [PATCH] feat: scaffold CLI with argument parsing, tests and CI --- .github/workflows/test.yml | 19 +++++++++++++++ README.md | 36 ++++++++++++++++++++++++++- bin/sol-priority-fee.js | 8 ++++++ package.json | 27 ++++++++++++++++++++ src/args.js | 42 ++++++++++++++++++++++++++++++++ src/cli.js | 50 ++++++++++++++++++++++++++++++++++++++ src/index.js | 3 +++ tests/args.test.js | 39 +++++++++++++++++++++++++++++ tests/cli.test.js | 39 +++++++++++++++++++++++++++++ 9 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/test.yml create mode 100755 bin/sol-priority-fee.js create mode 100644 package.json create mode 100644 src/args.js create mode 100644 src/cli.js create mode 100644 src/index.js create mode 100644 tests/args.test.js create mode 100644 tests/cli.test.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..9fdf944 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - run: npm test \ No newline at end of file diff --git a/README.md b/README.md index ef52f64..fbc4da6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,36 @@ # sol-priority-fee -Recommended Solana priority fee from recent network data (zero-dependency CLI) + +A zero-dependency Node.js CLI that recommends a Solana priority fee based on recent network data. + +> Status: early development. The scaffold is in place; fee fetching is coming in the next releases. + +## Planned usage + +```bash +npx sol-priority-fee +npx sol-priority-fee --account
+npx sol-priority-fee --watch +npx sol-priority-fee --json +``` + +## Requirements + +- Node.js 18 or newer + +## RPC endpoint + +The public mainnet RPC rate-limits heavily. Set your own endpoint with the `SOL_PRIORITY_FEE_RPC` environment variable or pass `--rpc `. + +## Honest limits + +A recommendation is a statistical guess from recent blocks, not a guarantee of inclusion. Fees can spike faster than any sample can show. + +## Development + +```bash +npm test +``` + +## License + +MIT \ No newline at end of file diff --git a/bin/sol-priority-fee.js b/bin/sol-priority-fee.js new file mode 100755 index 0000000..ae4f488 --- /dev/null +++ b/bin/sol-priority-fee.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node +'use strict'; + +const { run } = require('../src/cli'); + +run(process.argv.slice(2)).then((code) => { + process.exitCode = code; +}); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7a95af --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "sol-priority-fee", + "version": "0.1.0", + "description": "Recommended Solana priority fee from recent network data (zero-dependency CLI)", + "bin": { + "sol-priority-fee": "bin/sol-priority-fee.js" + }, + "main": "src/index.js", + "files": [ + "bin", + "src" + ], + "engines": { + "node": ">=18" + }, + "scripts": { + "test": "node --test" + }, + "keywords": [ + "solana", + "priority-fee", + "compute-budget", + "cli" + ], + "author": "Sybi", + "license": "MIT" +} \ No newline at end of file diff --git a/src/args.js b/src/args.js new file mode 100644 index 0000000..a655f9b --- /dev/null +++ b/src/args.js @@ -0,0 +1,42 @@ +'use strict'; + +function parseArgs(argv) { + const opts = { + help: false, + version: false, + json: false, + watch: false, + rpc: null, + accounts: [], + errors: [], + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + + if (arg === '--help' || arg === '-h') { + opts.help = true; + } else if (arg === '--version' || arg === '-v') { + opts.version = true; + } else if (arg === '--json') { + opts.json = true; + } else if (arg === '--watch') { + opts.watch = true; + } else if (arg === '--rpc' || arg === '--account') { + const value = argv[i + 1]; + if (value === undefined || value.startsWith('--')) { + opts.errors.push(`${arg} needs a value`); + } else { + i++; + if (arg === '--rpc') opts.rpc = value; + else opts.accounts.push(value); + } + } else { + opts.errors.push(`unknown option: ${arg}`); + } + } + + return opts; +} + +module.exports = { parseArgs }; \ No newline at end of file diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..f66b238 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,50 @@ +'use strict'; + +const pkg = require('../package.json'); +const { parseArgs } = require('./args'); + +const HELP = `sol-priority-fee - recommended Solana priority fee from recent network data + +Usage: + sol-priority-fee [options] + +Options: + --account
Only consider fees for transactions touching this account (repeatable) + --rpc RPC endpoint (default: SOL_PRIORITY_FEE_RPC, then public mainnet RPC) + --watch Keep refreshing the recommendation + --json Print machine-readable JSON + -v, --version Print version + -h, --help Show this help +`; + +function defaultIo() { + return { + out: (text) => process.stdout.write(text + '\n'), + err: (text) => process.stderr.write(text + '\n'), + }; +} + +async function run(argv, io = defaultIo()) { + const opts = parseArgs(argv); + + if (opts.errors.length > 0) { + for (const message of opts.errors) io.err(`error: ${message}`); + io.err('Run sol-priority-fee --help for usage.'); + return 2; + } + + if (opts.help) { + io.out(HELP); + return 0; + } + + if (opts.version) { + io.out(pkg.version); + return 0; + } + + io.err('Fetching fees is not implemented yet.'); + return 1; +} + +module.exports = { run, HELP }; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..41952db --- /dev/null +++ b/src/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = {}; \ No newline at end of file diff --git a/tests/args.test.js b/tests/args.test.js new file mode 100644 index 0000000..49c5cfa --- /dev/null +++ b/tests/args.test.js @@ -0,0 +1,39 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { parseArgs } = require('../src/args'); + +test('no arguments gives defaults', () => { + const opts = parseArgs([]); + assert.equal(opts.help, false); + assert.equal(opts.json, false); + assert.equal(opts.watch, false); + assert.equal(opts.rpc, null); + assert.deepEqual(opts.accounts, []); + assert.deepEqual(opts.errors, []); +}); + +test('parses flags and values', () => { + const opts = parseArgs(['--json', '--watch', '--rpc', 'https://example.com', '--account', 'A', '--account', 'B']); + assert.equal(opts.json, true); + assert.equal(opts.watch, true); + assert.equal(opts.rpc, 'https://example.com'); + assert.deepEqual(opts.accounts, ['A', 'B']); + assert.deepEqual(opts.errors, []); +}); + +test('short flags work', () => { + assert.equal(parseArgs(['-h']).help, true); + assert.equal(parseArgs(['-v']).version, true); +}); + +test('unknown option is reported', () => { + const opts = parseArgs(['--nope']); + assert.deepEqual(opts.errors, ['unknown option: --nope']); +}); + +test('missing value is reported', () => { + assert.deepEqual(parseArgs(['--rpc']).errors, ['--rpc needs a value']); + assert.deepEqual(parseArgs(['--account', '--json']).errors, ['--account needs a value']); +}); \ No newline at end of file diff --git a/tests/cli.test.js b/tests/cli.test.js new file mode 100644 index 0000000..354f1cc --- /dev/null +++ b/tests/cli.test.js @@ -0,0 +1,39 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { run } = require('../src/cli'); +const pkg = require('../package.json'); + +function capture() { + const lines = { out: [], err: [] }; + return { + lines, + io: { + out: (text) => lines.out.push(text), + err: (text) => lines.err.push(text), + }, + }; +} + +test('--help prints usage and exits 0', async () => { + const { lines, io } = capture(); + const code = await run(['--help'], io); + assert.equal(code, 0); + assert.match(lines.out.join('\n'), /Usage:/); +}); + +test('--version prints the package version', async () => { + const { lines, io } = capture(); + const code = await run(['--version'], io); + assert.equal(code, 0); + assert.equal(lines.out[0], pkg.version); +}); + +test('unknown option exits 2 with a hint', async () => { + const { lines, io } = capture(); + const code = await run(['--nope'], io); + assert.equal(code, 2); + assert.match(lines.err.join('\n'), /unknown option: --nope/); + assert.match(lines.err.join('\n'), /--help/); +}); \ No newline at end of file