Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
36 changes: 35 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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 <address>
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 <url>`.

## 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
8 changes: 8 additions & 0 deletions bin/sol-priority-fee.js
Original file line number Diff line number Diff line change
@@ -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;
});
27 changes: 27 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
42 changes: 42 additions & 0 deletions src/args.js
Original file line number Diff line number Diff line change
@@ -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 };
50 changes: 50 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
@@ -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 <address> Only consider fees for transactions touching this account (repeatable)
--rpc <url> 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 };
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'use strict';

module.exports = {};
39 changes: 39 additions & 0 deletions tests/args.test.js
Original file line number Diff line number Diff line change
@@ -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']);
});
39 changes: 39 additions & 0 deletions tests/cli.test.js
Original file line number Diff line number Diff line change
@@ -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/);
});
Loading