bsv-pay-cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +435 -0
  4. package/dist/address.d.ts +6 -0
  5. package/dist/address.js +35 -0
  6. package/dist/chain/provider.d.ts +35 -0
  7. package/dist/chain/provider.js +1 -0
  8. package/dist/chain/whatsonchain.d.ts +23 -0
  9. package/dist/chain/whatsonchain.js +98 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +169 -0
  12. package/dist/commands/approvals.d.ts +19 -0
  13. package/dist/commands/approvals.js +112 -0
  14. package/dist/commands/balance.d.ts +3 -0
  15. package/dist/commands/balance.js +28 -0
  16. package/dist/commands/donate.d.ts +8 -0
  17. package/dist/commands/donate.js +16 -0
  18. package/dist/commands/fetch.d.ts +13 -0
  19. package/dist/commands/fetch.js +49 -0
  20. package/dist/commands/init.d.ts +11 -0
  21. package/dist/commands/init.js +188 -0
  22. package/dist/commands/mcp.d.ts +13 -0
  23. package/dist/commands/mcp.js +32 -0
  24. package/dist/commands/policy.d.ts +8 -0
  25. package/dist/commands/policy.js +101 -0
  26. package/dist/commands/request.d.ts +9 -0
  27. package/dist/commands/request.js +85 -0
  28. package/dist/commands/send.d.ts +11 -0
  29. package/dist/commands/send.js +125 -0
  30. package/dist/commands/serve.d.ts +16 -0
  31. package/dist/commands/serve.js +59 -0
  32. package/dist/commands/watch.d.ts +10 -0
  33. package/dist/commands/watch.js +163 -0
  34. package/dist/config.d.ts +16 -0
  35. package/dist/config.js +51 -0
  36. package/dist/context.d.ts +13 -0
  37. package/dist/context.js +12 -0
  38. package/dist/core/balance.d.ts +24 -0
  39. package/dist/core/balance.js +34 -0
  40. package/dist/core/context.d.ts +27 -0
  41. package/dist/core/context.js +9 -0
  42. package/dist/core/history.d.ts +18 -0
  43. package/dist/core/history.js +15 -0
  44. package/dist/core/index.d.ts +22 -0
  45. package/dist/core/index.js +17 -0
  46. package/dist/core/internal.d.ts +22 -0
  47. package/dist/core/internal.js +19 -0
  48. package/dist/core/policy-status.d.ts +55 -0
  49. package/dist/core/policy-status.js +49 -0
  50. package/dist/core/request.d.ts +43 -0
  51. package/dist/core/request.js +77 -0
  52. package/dist/core/send.d.ts +108 -0
  53. package/dist/core/send.js +277 -0
  54. package/dist/core/spend-lock.d.ts +2 -0
  55. package/dist/core/spend-lock.js +25 -0
  56. package/dist/core/wallet.d.ts +53 -0
  57. package/dist/core/wallet.js +77 -0
  58. package/dist/errors.d.ts +30 -0
  59. package/dist/errors.js +39 -0
  60. package/dist/http402/client.d.ts +32 -0
  61. package/dist/http402/client.js +85 -0
  62. package/dist/http402/middleware.d.ts +37 -0
  63. package/dist/http402/middleware.js +96 -0
  64. package/dist/http402/protocol.d.ts +50 -0
  65. package/dist/http402/protocol.js +114 -0
  66. package/dist/ledger.d.ts +51 -0
  67. package/dist/ledger.js +27 -0
  68. package/dist/mcp/server.d.ts +32 -0
  69. package/dist/mcp/server.js +484 -0
  70. package/dist/output.d.ts +17 -0
  71. package/dist/output.js +44 -0
  72. package/dist/paths.d.ts +11 -0
  73. package/dist/paths.js +24 -0
  74. package/dist/policy/approvals.d.ts +24 -0
  75. package/dist/policy/approvals.js +89 -0
  76. package/dist/policy/budget.d.ts +16 -0
  77. package/dist/policy/budget.js +47 -0
  78. package/dist/policy/engine.d.ts +76 -0
  79. package/dist/policy/engine.js +199 -0
  80. package/dist/policy/policy.d.ts +30 -0
  81. package/dist/policy/policy.js +126 -0
  82. package/dist/prompt.d.ts +11 -0
  83. package/dist/prompt.js +57 -0
  84. package/dist/tx.d.ts +29 -0
  85. package/dist/tx.js +68 -0
  86. package/dist/units.d.ts +13 -0
  87. package/dist/units.js +53 -0
  88. package/dist/wallet/brc100.d.ts +105 -0
  89. package/dist/wallet/brc100.js +217 -0
  90. package/dist/wallet/crypto.d.ts +25 -0
  91. package/dist/wallet/crypto.js +46 -0
  92. package/dist/wallet/wallet.d.ts +86 -0
  93. package/dist/wallet/wallet.js +186 -0
  94. package/docs/AGENTIC-PAYMENTS.md +218 -0
  95. package/docs/BRC100.md +151 -0
  96. package/package.json +82 -0
@@ -0,0 +1,98 @@
1
+ import { networkError } from '../errors.js';
2
+ const REQUEST_TIMEOUT_MS = 15_000;
3
+ const RETRY_DELAY_MS = 1_500;
4
+ const RATE_LIMIT_DELAY_MS = 3_000;
5
+ function sleep(ms) {
6
+ return new Promise((r) => setTimeout(r, ms));
7
+ }
8
+ export class WhatsOnChainProvider {
9
+ network;
10
+ baseUrl;
11
+ constructor(network) {
12
+ this.network = network;
13
+ // BSV_PAY_API_URL points at any WhatsOnChain-compatible API root
14
+ // (self-hosted instance, or the local e2e mock server).
15
+ const root = process.env.BSV_PAY_API_URL ?? 'https://api.whatsonchain.com/v1/bsv';
16
+ this.baseUrl = `${root.replace(/\/$/, '')}/${network}`;
17
+ }
18
+ /**
19
+ * GET with one automatic retry (invariant 5). 429s wait longer before the
20
+ * retry; persistent failure is exit 4.
21
+ */
22
+ async get(path) {
23
+ const url = this.baseUrl + path;
24
+ let lastError = '';
25
+ for (let attempt = 0; attempt < 2; attempt++) {
26
+ try {
27
+ const res = await fetch(url, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
28
+ if (res.ok)
29
+ return await res.text();
30
+ lastError = `HTTP ${res.status}`;
31
+ if (res.status === 404)
32
+ break; // not retryable
33
+ await sleep(res.status === 429 ? RATE_LIMIT_DELAY_MS : RETRY_DELAY_MS);
34
+ }
35
+ catch (e) {
36
+ lastError = e instanceof Error ? e.message : String(e);
37
+ if (attempt === 0)
38
+ await sleep(RETRY_DELAY_MS);
39
+ }
40
+ }
41
+ throw networkError(`WhatsOnChain request failed (${lastError}) for ${url}. Check your connection and retry; the API may be briefly rate-limiting.`);
42
+ }
43
+ async getJson(path) {
44
+ const body = await this.get(path);
45
+ try {
46
+ return JSON.parse(body);
47
+ }
48
+ catch {
49
+ throw networkError(`WhatsOnChain returned unparseable JSON for ${path}. Retry shortly.`);
50
+ }
51
+ }
52
+ async getBalance(address) {
53
+ return this.getJson(`/address/${address}/balance`);
54
+ }
55
+ async getUtxos(address) {
56
+ const rows = await this.getJson(`/address/${address}/unspent`);
57
+ return rows.map((r) => ({
58
+ txid: r.tx_hash,
59
+ vout: r.tx_pos,
60
+ satoshis: r.value,
61
+ height: r.height > 0 ? r.height : undefined,
62
+ }));
63
+ }
64
+ async getHistory(address) {
65
+ const rows = await this.getJson(`/address/${address}/history`);
66
+ return rows.map((r) => ({ txid: r.tx_hash, height: r.height }));
67
+ }
68
+ async getRawTx(txid) {
69
+ return (await this.get(`/tx/${txid}/hex`)).trim();
70
+ }
71
+ /**
72
+ * Broadcast is NOT silently retried: a retry after an ambiguous failure
73
+ * could double-report. A definitive API rejection returns ok=false; an
74
+ * ambiguous network failure throws (caller maps to exit 6, status unknown).
75
+ */
76
+ async broadcast(rawTxHex) {
77
+ const url = `${this.baseUrl}/tx/raw`;
78
+ let res;
79
+ try {
80
+ res = await fetch(url, {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ txhex: rawTxHex }),
84
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
85
+ });
86
+ }
87
+ catch (e) {
88
+ throw networkError(`Broadcast request did not complete (${e instanceof Error ? e.message : String(e)}). The transaction may or may not have propagated.`);
89
+ }
90
+ const body = (await res.text()).trim();
91
+ if (res.ok) {
92
+ // WoC returns the txid as a JSON string
93
+ const txid = body.replace(/^"|"$/g, '');
94
+ return { ok: true, txid };
95
+ }
96
+ return { ok: false, error: body || `HTTP ${res.status}` };
97
+ }
98
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env node
2
+ import { Command, CommanderError } from 'commander';
3
+ import { buildCtx } from './context.js';
4
+ import { EXIT } from './errors.js';
5
+ import { Output } from './output.js';
6
+ import { cmdInit } from './commands/init.js';
7
+ import { cmdBalance } from './commands/balance.js';
8
+ import { cmdSend } from './commands/send.js';
9
+ import { cmdRequest } from './commands/request.js';
10
+ import { cmdWatch } from './commands/watch.js';
11
+ import { cmdDonate } from './commands/donate.js';
12
+ import { cmdFetch } from './commands/fetch.js';
13
+ import { cmdMcp } from './commands/mcp.js';
14
+ import { cmdPolicyShow, cmdPolicyTest } from './commands/policy.js';
15
+ import { cmdServe } from './commands/serve.js';
16
+ import { cmdApprovalsApprove, cmdApprovalsList, cmdApprovalsReject, cmdApprovalsSetSecret, } from './commands/approvals.js';
17
+ const program = new Command();
18
+ /** Wrap a command action: build context, map errors to exit codes + JSON. */
19
+ function run(fn) {
20
+ return async (...cmdArgs) => {
21
+ // Commander passes (...args, options, command); we use optsWithGlobals.
22
+ const command = cmdArgs[cmdArgs.length - 1];
23
+ const opts = command.optsWithGlobals();
24
+ let ctx;
25
+ try {
26
+ ctx = buildCtx(opts);
27
+ // keep positional args + the options object, drop the trailing Command
28
+ await fn(ctx, ...cmdArgs.slice(0, -1));
29
+ process.exitCode = EXIT.OK;
30
+ }
31
+ catch (err) {
32
+ const out = ctx?.out ?? new Output(Boolean(opts.json));
33
+ process.exitCode = out.error(err);
34
+ }
35
+ };
36
+ }
37
+ program
38
+ .name('bsv-pay')
39
+ .description('Send and receive Bitcoin SV micropayments from the command line')
40
+ .version('0.2.0')
41
+ .option('--json', 'machine-readable JSON output (NDJSON for watch)')
42
+ .option('--testnet', 'use BSV testnet (state kept separate from mainnet)');
43
+ program
44
+ .command('init')
45
+ .description('Create or import a wallet')
46
+ .option('--import-seed', 'import an existing BIP-39 seed phrase')
47
+ .option('--import-wif', 'import a raw WIF private key (not recommended)')
48
+ .option('--force', 'overwrite an existing wallet')
49
+ .option('--no-encrypt', 'EXPLICIT OPT-IN: store the seed unencrypted (dangerous)')
50
+ .option('--brc100', 'connect a BRC-100 wallet (see --experimental-brc100)')
51
+ .option('--experimental-brc100', 'EXPERIMENTAL: delegate custody to a BRC-100 wallet app (keys never leave it)')
52
+ .action(run((ctx, opts) => cmdInit(ctx, opts)));
53
+ program
54
+ .command('balance')
55
+ .description('Show confirmed and unconfirmed balance across all tracked addresses')
56
+ .action(run((ctx) => cmdBalance(ctx)));
57
+ program
58
+ .command('send')
59
+ .description('Send satoshis to an address')
60
+ .argument('<address>', 'recipient BSV address')
61
+ .argument('<amount>', 'amount: bare satoshis, Nsats, or Nbsv')
62
+ .argument('[memo]', 'local-only memo for your ledger (never written on-chain)')
63
+ .option('-y, --yes', 'skip the confirmation prompt (spend limit still enforced)')
64
+ .option('--allow-large', 'with --yes, permit sends at/above the spend limit')
65
+ .option('--dry-run', 'build and display the transaction but never broadcast')
66
+ .option('--confirmed-only', 'spend only confirmed UTXOs')
67
+ .action(run((ctx, address, amount, memo, opts) => cmdSend(ctx, address, amount, memo, opts)));
68
+ program
69
+ .command('request')
70
+ .description('Create a payment request with a fresh address, URI, and QR code')
71
+ .argument('<amount>', 'amount: bare satoshis, Nsats, or Nbsv')
72
+ .argument('[memo]', 'local-only memo / request label')
73
+ .option('--wait', 'poll until the payment is seen (0-conf), then exit 0')
74
+ .option('--timeout <sec>', 'with --wait, give up after this many seconds', '600')
75
+ .action(run((ctx, amount, memo, opts) => cmdRequest(ctx, amount, memo, opts)));
76
+ program
77
+ .command('watch')
78
+ .description('Watch all tracked addresses for incoming payments')
79
+ .option('--interval <sec>', 'poll interval in seconds (floor 5)')
80
+ .action(run((ctx, opts) => cmdWatch(ctx, opts)));
81
+ program
82
+ .command('donate')
83
+ .description('Support bsv-pay development')
84
+ .argument('[amount]', 'amount to donate (default 10000 sats)')
85
+ .option('-y, --yes', 'skip the confirmation prompt')
86
+ .option('--allow-large', 'with --yes, permit sends at/above the spend limit')
87
+ .option('--dry-run', 'build and display the transaction but never broadcast')
88
+ .action(run((ctx, amount, opts) => cmdDonate(ctx, amount, opts)));
89
+ program
90
+ .command('fetch')
91
+ .description('Fetch a URL, automatically paying a BRC-105 402 paywall within policy')
92
+ .argument('<url>', 'http(s) URL to fetch')
93
+ .option('--max-price <amount>', 'refuse to pay more than this for the resource')
94
+ .action(run((ctx, url, opts) => cmdFetch(ctx, url, opts)));
95
+ program
96
+ .command('serve')
97
+ .description('Run a demo BRC-105 paywall server: each request pays --price into this wallet')
98
+ .requiredOption('--price <amount>', 'price per request: bare satoshis, Nsats, or Nbsv')
99
+ .option('--port <n>', 'port to listen on', '8402')
100
+ .option('--host <host>', 'interface to bind (default localhost-only)', '127.0.0.1')
101
+ .option('--body <text>', 'content to serve once paid')
102
+ .action(run((ctx, opts) => cmdServe(ctx, opts)));
103
+ program
104
+ .command('mcp')
105
+ .description('Serve MCP tools over stdio for AI agents (wallet unlocks at start; policy enforced in core)')
106
+ .action(run((ctx) => cmdMcp(ctx)));
107
+ const policy = program
108
+ .command('policy')
109
+ .description('Inspect and dry-run the spend policy (~/.bsv-pay/policy.toml)');
110
+ policy
111
+ .command('show')
112
+ .description('Show active policy rules, current budget usage, and pending approvals')
113
+ .action(run((ctx) => Promise.resolve(cmdPolicyShow(ctx))));
114
+ policy
115
+ .command('test')
116
+ .description('Dry-run a policy decision: exit 0 allow, 8 deny, 9 would-queue')
117
+ .argument('<address>', 'recipient BSV address')
118
+ .argument('<amount>', 'amount: bare satoshis, Nsats, or Nbsv')
119
+ .action(run((ctx, address, amount) => Promise.resolve(cmdPolicyTest(ctx, address, amount))));
120
+ const approvals = program
121
+ .command('approvals')
122
+ .description('Review and resolve payments queued by approval_threshold_sats');
123
+ approvals
124
+ .command('list')
125
+ .description('List payments waiting for human approval')
126
+ .action(run((ctx) => Promise.resolve(cmdApprovalsList(ctx))));
127
+ approvals
128
+ .command('approve')
129
+ .description('Approve and send a queued payment (interactive: approval secret required)')
130
+ .argument('<id>', 'approval id (or unambiguous prefix) from "approvals list"')
131
+ .action(run((ctx, id) => cmdApprovalsApprove(ctx, id)));
132
+ approvals
133
+ .command('reject')
134
+ .description('Reject a queued payment (interactive: approval secret required)')
135
+ .argument('<id>', 'approval id (or unambiguous prefix) from "approvals list"')
136
+ .action(run((ctx, id) => cmdApprovalsReject(ctx, id)));
137
+ approvals
138
+ .command('set-secret')
139
+ .description('Set or change the human approval secret (interactive only, by design)')
140
+ .action(run((ctx) => cmdApprovalsSetSecret(ctx)));
141
+ program.exitOverride();
142
+ try {
143
+ await program.parseAsync(process.argv);
144
+ }
145
+ catch (err) {
146
+ if (err instanceof CommanderError) {
147
+ // help/version display are success; everything else is invalid usage.
148
+ if (err.code === 'commander.helpDisplayed' ||
149
+ err.code === 'commander.version' ||
150
+ err.code === 'commander.help') {
151
+ process.exitCode = err.exitCode === 0 ? EXIT.OK : EXIT.USAGE;
152
+ }
153
+ else {
154
+ const json = process.argv.includes('--json');
155
+ if (json) {
156
+ process.stdout.write(JSON.stringify({
157
+ ok: false,
158
+ code: EXIT.USAGE,
159
+ error: 'invalid_usage',
160
+ message: err.message,
161
+ }) + '\n');
162
+ }
163
+ process.exitCode = EXIT.USAGE;
164
+ }
165
+ }
166
+ else {
167
+ process.exitCode = new Output(process.argv.includes('--json')).error(err);
168
+ }
169
+ }
@@ -0,0 +1,19 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import type { Ctx } from '../context.js';
3
+ /**
4
+ * Test seam ONLY: lets unit tests inject the hidden prompts. There is no
5
+ * CLI flag, env var, or config key that reaches these — the cli.ts wiring
6
+ * never passes them, so the only way to supply a secret to the real binary
7
+ * is typing it at a TTY.
8
+ */
9
+ export interface ApprovalPromptDeps {
10
+ promptSecret?: () => Promise<string>;
11
+ }
12
+ export declare function cmdApprovalsList(ctx: Ctx): void;
13
+ export declare function cmdApprovalsApprove(ctx: Ctx, id: string, provider?: ChainProvider, deps?: ApprovalPromptDeps): Promise<void>;
14
+ export declare function cmdApprovalsReject(ctx: Ctx, id: string, deps?: ApprovalPromptDeps): Promise<void>;
15
+ export interface SetSecretPromptDeps {
16
+ promptOldSecret?: () => Promise<string>;
17
+ promptNewSecret?: () => Promise<string>;
18
+ }
19
+ export declare function cmdApprovalsSetSecret(ctx: Ctx, deps?: SetSecretPromptDeps): Promise<void>;
@@ -0,0 +1,112 @@
1
+ import chalk from 'chalk';
2
+ import { executeSend, planApprovedSend } from '../core/send.js';
3
+ import { openWallet } from '../core/wallet.js';
4
+ import { usageError } from '../errors.js';
5
+ import { approvalSecretConfigured, badApprovalSecret, findPendingApproval, listPendingApprovals, resolveApproval, storeApprovalSecret, verifyApprovalSecret, } from '../policy/approvals.js';
6
+ import { askHidden, isInteractive } from '../prompt.js';
7
+ import { formatSats } from '../units.js';
8
+ import { obtainPassphrase } from '../wallet/wallet.js';
9
+ /** Approval verbs are human-only: a piped/agent shell has no TTY and stops here. */
10
+ function requireTty(action) {
11
+ if (!isInteractive()) {
12
+ throw usageError('tty_required', `${action} requires an interactive terminal. There is deliberately no flag or environment variable for it — a human must type the approval secret.`);
13
+ }
14
+ }
15
+ async function requireApprovalSecret(deps) {
16
+ if (!approvalSecretConfigured()) {
17
+ throw usageError('approval_secret_missing', 'No approval secret is set. A human must run "bsv-pay approvals set-secret" (interactive) before queued payments can be approved or rejected.');
18
+ }
19
+ const prompt = deps?.promptSecret ?? (() => askHidden('Approval secret: '));
20
+ const secret = await prompt();
21
+ if (!verifyApprovalSecret(secret))
22
+ throw badApprovalSecret();
23
+ }
24
+ export function cmdApprovalsList(ctx) {
25
+ const pending = listPendingApprovals(ctx.network);
26
+ if (pending.length === 0) {
27
+ ctx.out.info('No pending approvals.');
28
+ }
29
+ else {
30
+ ctx.out.info(chalk.bold(`Pending approvals (${ctx.network === 'test' ? 'testnet' : 'mainnet'})`));
31
+ for (const p of pending) {
32
+ ctx.out.info(` ${p.approvalId.slice(0, 8)} ${formatSats(p.amountSats)} -> ${p.address}` +
33
+ `${p.memo ? ` "${p.memo}"` : ''} (queued ${p.queuedAt})`);
34
+ }
35
+ ctx.out.info('');
36
+ ctx.out.info('Approve with: bsv-pay approvals approve <id> (requires the approval secret)');
37
+ }
38
+ ctx.out.result({
39
+ ok: true,
40
+ approvals: pending.map((p) => ({
41
+ id: p.approvalId,
42
+ address: p.address,
43
+ amount_sats: p.amountSats,
44
+ ...(p.memo ? { memo: p.memo } : {}),
45
+ queued_at: p.queuedAt,
46
+ })),
47
+ });
48
+ }
49
+ export async function cmdApprovalsApprove(ctx, id, provider, deps) {
50
+ requireTty('Approving a payment');
51
+ const pending = findPendingApproval(ctx.network, id);
52
+ await requireApprovalSecret(deps);
53
+ const core = { network: ctx.network, config: ctx.config, provider };
54
+ const wallet = await openWallet({
55
+ ...core,
56
+ passphrase: () => obtainPassphrase(),
57
+ onWarning: (text) => process.stderr.write(text + '\n'),
58
+ });
59
+ // Re-decided now, against today's ledger: every rule except the threshold
60
+ // still applies. A deny here leaves the approval pending.
61
+ const plan = await planApprovedSend(wallet, core, {
62
+ to: pending.address,
63
+ amountSats: pending.amountSats,
64
+ memo: pending.memo,
65
+ confirmedOnly: pending.confirmedOnly,
66
+ }, pending.approvalId);
67
+ const result = await executeSend(wallet, core, plan);
68
+ resolveApproval(ctx.network, pending.approvalId, 'approved', result.txid);
69
+ ctx.out.info(chalk.green('Approved and sent.'));
70
+ ctx.out.info(` Approval: ${pending.approvalId}`);
71
+ ctx.out.info(` Txid: ${result.txid}`);
72
+ ctx.out.info(` Explorer: ${result.explorerUrl}`);
73
+ ctx.out.result({
74
+ ok: true,
75
+ approval_id: pending.approvalId,
76
+ txid: result.txid,
77
+ recipient: pending.address,
78
+ amount_sats: result.amountSats,
79
+ fee_sats: result.feeSats,
80
+ explorer_url: result.explorerUrl,
81
+ });
82
+ }
83
+ export async function cmdApprovalsReject(ctx, id, deps) {
84
+ requireTty('Rejecting a payment');
85
+ const pending = findPendingApproval(ctx.network, id);
86
+ await requireApprovalSecret(deps);
87
+ resolveApproval(ctx.network, pending.approvalId, 'rejected');
88
+ ctx.out.info(chalk.yellow(`Rejected ${pending.approvalId} — nothing was sent.`));
89
+ ctx.out.result({ ok: true, approval_id: pending.approvalId, resolution: 'rejected' });
90
+ }
91
+ export async function cmdApprovalsSetSecret(ctx, deps) {
92
+ requireTty('Setting the approval secret');
93
+ if (approvalSecretConfigured()) {
94
+ const promptOld = deps?.promptOldSecret ?? (() => askHidden('Current approval secret: '));
95
+ if (!verifyApprovalSecret(await promptOld()))
96
+ throw badApprovalSecret();
97
+ }
98
+ const promptNew = deps?.promptNewSecret ?? (() => askHidden('New approval secret: '));
99
+ const first = await promptNew();
100
+ if (first === '') {
101
+ throw usageError('empty_approval_secret', 'The approval secret cannot be empty.');
102
+ }
103
+ const second = await (deps?.promptNewSecret
104
+ ? deps.promptNewSecret()
105
+ : askHidden('Repeat approval secret: '));
106
+ if (first !== second) {
107
+ throw usageError('approval_secret_mismatch', 'The secrets do not match; nothing was changed.');
108
+ }
109
+ storeApprovalSecret(first);
110
+ process.stderr.write(chalk.yellow('Approval secret set. Keep it in your head: never in a file, script, or environment variable — that separation is what stops an agent from approving its own payments.') + '\n');
111
+ ctx.out.result({ ok: true, approval_secret_configured: true });
112
+ }
@@ -0,0 +1,3 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import type { Ctx } from '../context.js';
3
+ export declare function cmdBalance(ctx: Ctx, provider?: ChainProvider): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk';
2
+ import { getBalance } from '../core/balance.js';
3
+ import { formatSats } from '../units.js';
4
+ export async function cmdBalance(ctx, provider) {
5
+ const balance = await getBalance({ network: ctx.network, config: ctx.config, provider });
6
+ ctx.out.info(chalk.bold(`Balance (${ctx.network === 'test' ? 'testnet' : 'mainnet'})`));
7
+ if (balance.backend === 'brc100') {
8
+ ctx.out.info(` Spendable: ${formatSats(balance.confirmedSats)}`);
9
+ ctx.out.info(' Custody: external BRC-100 wallet app (experimental)');
10
+ }
11
+ else {
12
+ ctx.out.info(` Confirmed: ${formatSats(balance.confirmedSats)}`);
13
+ ctx.out.info(` Unconfirmed: ${formatSats(balance.unconfirmedSats)}`);
14
+ ctx.out.info(` Total: ${formatSats(balance.confirmedSats + balance.unconfirmedSats)}`);
15
+ ctx.out.info(` Tracked addresses: ${balance.addresses.length}`);
16
+ }
17
+ ctx.out.result({
18
+ ok: true,
19
+ confirmed_sats: balance.confirmedSats,
20
+ unconfirmed_sats: balance.unconfirmedSats,
21
+ addresses: balance.addresses.map((a) => ({
22
+ address: a.address,
23
+ confirmed_sats: a.confirmedSats,
24
+ unconfirmed_sats: a.unconfirmedSats,
25
+ })),
26
+ ...(balance.backend ? { backend: balance.backend } : {}),
27
+ });
28
+ }
@@ -0,0 +1,8 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import type { Ctx } from '../context.js';
3
+ export interface DonateOptions {
4
+ yes?: boolean;
5
+ allowLarge?: boolean;
6
+ dryRun?: boolean;
7
+ }
8
+ export declare function cmdDonate(ctx: Ctx, amount: string | undefined, opts: DonateOptions, provider?: ChainProvider): Promise<void>;
@@ -0,0 +1,16 @@
1
+ import chalk from 'chalk';
2
+ import { cmdSend } from './send.js';
3
+ // TODO: the testnet address is still a well-known placeholder (no one holds
4
+ // its key) — replace if testnet donations ever matter.
5
+ const DONATION_ADDRESS = {
6
+ main: '131CswxfV8Swi8zUSc3XfH9tEJLxzxmpa4',
7
+ test: 'mipcBbFg9gMiCh81Kj8tqqdgoZub1ZJRfn',
8
+ };
9
+ const DEFAULT_AMOUNT = '10000';
10
+ export async function cmdDonate(ctx, amount, opts, provider) {
11
+ if (ctx.network === 'test') {
12
+ process.stderr.write(chalk.yellow('NOTE: the testnet donation address is a PLACEHOLDER — coins sent to it are unrecoverable. Use --dry-run.') + '\n');
13
+ }
14
+ process.stderr.write('Thanks for supporting bsv-pay!\n');
15
+ await cmdSend(ctx, DONATION_ADDRESS[ctx.network], amount ?? DEFAULT_AMOUNT, 'bsv-pay donation', { ...opts }, provider);
16
+ }
@@ -0,0 +1,13 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import type { Ctx } from '../context.js';
3
+ export interface FetchOptions {
4
+ maxPrice?: string;
5
+ }
6
+ /**
7
+ * `bsv-pay fetch <url>` — GET a resource, automatically paying a BRC-105
8
+ * 402 within policy. The body is the machine output (raw to stdout, or
9
+ * inside the --json object); everything about the payment goes to stderr.
10
+ * Exit 8 = capped/denied before money moved, 9 = queued for approval,
11
+ * 10 = paid but the server refused the content (txid in the error).
12
+ */
13
+ export declare function cmdFetch(ctx: Ctx, url: string, opts: FetchOptions, provider?: ChainProvider): Promise<void>;
@@ -0,0 +1,49 @@
1
+ import chalk from 'chalk';
2
+ import { openWallet } from '../core/wallet.js';
3
+ import { paidFetch } from '../http402/client.js';
4
+ import { formatSats, parseAmount } from '../units.js';
5
+ import { obtainPassphrase } from '../wallet/wallet.js';
6
+ /**
7
+ * `bsv-pay fetch <url>` — GET a resource, automatically paying a BRC-105
8
+ * 402 within policy. The body is the machine output (raw to stdout, or
9
+ * inside the --json object); everything about the payment goes to stderr.
10
+ * Exit 8 = capped/denied before money moved, 9 = queued for approval,
11
+ * 10 = paid but the server refused the content (txid in the error).
12
+ */
13
+ export async function cmdFetch(ctx, url, opts, provider) {
14
+ const maxPriceSats = opts.maxPrice !== undefined ? parseAmount(opts.maxPrice) : undefined;
15
+ const core = { network: ctx.network, config: ctx.config, provider };
16
+ const wallet = await openWallet({
17
+ ...core,
18
+ passphrase: () => obtainPassphrase(),
19
+ onWarning: (text) => process.stderr.write(text + '\n'),
20
+ });
21
+ const result = await paidFetch(wallet, core, { url, maxPriceSats });
22
+ if (result.paid) {
23
+ process.stderr.write(chalk.green(`paid ${formatSats(result.payment.amountSats)} (+${result.payment.feeSats} sats fee, txid ${result.payment.txid.slice(0, 12)}…) for ${url}`) + '\n');
24
+ }
25
+ else {
26
+ process.stderr.write(`no payment required (HTTP ${result.status})\n`);
27
+ }
28
+ if (ctx.json) {
29
+ ctx.out.result({
30
+ ok: true,
31
+ status: result.status,
32
+ paid: result.paid,
33
+ ...(result.contentType !== undefined && { content_type: result.contentType }),
34
+ ...(result.payment && {
35
+ amount_sats: result.payment.amountSats,
36
+ fee_sats: result.payment.feeSats,
37
+ txid: result.payment.txid,
38
+ address: result.payment.address,
39
+ }),
40
+ body: result.body,
41
+ });
42
+ }
43
+ else {
44
+ // the resource itself is the machine output
45
+ process.stdout.write(result.body);
46
+ if (result.body.length > 0 && !result.body.endsWith('\n'))
47
+ process.stdout.write('\n');
48
+ }
49
+ }
@@ -0,0 +1,11 @@
1
+ import type { Ctx } from '../context.js';
2
+ import { type ConnectBrc100Options } from '../wallet/brc100.js';
3
+ export interface InitOptions {
4
+ importSeed?: boolean;
5
+ importWif?: boolean;
6
+ force?: boolean;
7
+ encrypt?: boolean;
8
+ brc100?: boolean;
9
+ experimentalBrc100?: boolean;
10
+ }
11
+ export declare function cmdInit(ctx: Ctx, opts: InitOptions, brc100Connect?: ConnectBrc100Options): Promise<void>;