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,10 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import type { Ctx } from '../context.js';
3
+ export interface WatchOptions {
4
+ interval?: string;
5
+ }
6
+ /**
7
+ * Poll all tracked addresses; emit one event per incoming payment (pending at
8
+ * 0-conf, then a confirmation event). `maxCycles` exists for tests.
9
+ */
10
+ export declare function cmdWatch(ctx: Ctx, opts: WatchOptions, provider?: ChainProvider, maxCycles?: number): Promise<void>;
@@ -0,0 +1,163 @@
1
+ import chalk from 'chalk';
2
+ import { WhatsOnChainProvider } from '../chain/whatsonchain.js';
3
+ import { usageError } from '../errors.js';
4
+ import { appendLedger, readLedger, trackedAddressesFromLedger } from '../ledger.js';
5
+ import { formatSats } from '../units.js';
6
+ import { brc100ReceiveNotSupported } from '../wallet/brc100.js';
7
+ import { readWalletFile } from '../wallet/wallet.js';
8
+ import { explorerTxUrl } from './send.js';
9
+ const MIN_INTERVAL_SECS = 5;
10
+ const MAX_BACKOFF_MULTIPLIER = 8;
11
+ function interruptibleSleep(ms, signal) {
12
+ return new Promise((resolve) => {
13
+ if (signal.aborted)
14
+ return resolve();
15
+ const t = setTimeout(resolve, ms);
16
+ signal.addEventListener('abort', () => {
17
+ clearTimeout(t);
18
+ resolve();
19
+ }, { once: true });
20
+ });
21
+ }
22
+ /** memo per request address, from the ledger (memos are local-only). */
23
+ function memosByAddress(network) {
24
+ const map = new Map();
25
+ for (const e of readLedger(network)) {
26
+ if (e.type === 'address_issued' && e.memo)
27
+ map.set(e.address, e.memo);
28
+ }
29
+ return map;
30
+ }
31
+ function receivedTxids(network) {
32
+ const set = new Set();
33
+ for (const e of readLedger(network)) {
34
+ if (e.type === 'receive')
35
+ set.add(`${e.txid}:${e.address}`);
36
+ }
37
+ return set;
38
+ }
39
+ /**
40
+ * Poll all tracked addresses; emit one event per incoming payment (pending at
41
+ * 0-conf, then a confirmation event). `maxCycles` exists for tests.
42
+ */
43
+ export async function cmdWatch(ctx, opts, provider, maxCycles = Infinity) {
44
+ const chain = provider ?? new WhatsOnChainProvider(ctx.network);
45
+ const file = readWalletFile(ctx.network); // exit 2 with guidance when no wallet
46
+ if (file.backend === 'brc100')
47
+ throw brc100ReceiveNotSupported();
48
+ let intervalSecs = ctx.config.pollIntervalSecs;
49
+ if (opts.interval !== undefined) {
50
+ const n = Number(opts.interval);
51
+ if (!Number.isFinite(n) || n <= 0) {
52
+ throw usageError('invalid_interval', `--interval must be a positive number of seconds (got "${opts.interval}").`);
53
+ }
54
+ intervalSecs = n;
55
+ }
56
+ if (intervalSecs < MIN_INTERVAL_SECS) {
57
+ process.stderr.write(chalk.yellow(`Interval raised to the ${MIN_INTERVAL_SECS}s floor (public API rate limits).`) +
58
+ '\n');
59
+ intervalSecs = MIN_INTERVAL_SECS;
60
+ }
61
+ // Clean Ctrl-C: abort the current sleep, then fall through to the summary.
62
+ const abort = new AbortController();
63
+ let stopping = false;
64
+ const onSigint = () => {
65
+ stopping = true;
66
+ abort.abort();
67
+ };
68
+ process.once('SIGINT', onSigint);
69
+ /** address -> txid -> confirmed? */
70
+ const known = new Map();
71
+ const alreadyLedgered = receivedTxids(ctx.network);
72
+ let sessionTotal = 0;
73
+ let baselined = false;
74
+ let backoff = 1;
75
+ ctx.out.info(chalk.bold(`Watching ${ctx.network === 'test' ? 'testnet' : 'mainnet'} wallet (every ${intervalSecs}s) — Ctrl-C to stop.`));
76
+ const emit = (obj, human) => {
77
+ ctx.out.info(human);
78
+ if (ctx.json)
79
+ process.stdout.write(JSON.stringify(obj) + '\n');
80
+ };
81
+ for (let cycle = 0; cycle < maxCycles && !stopping; cycle++) {
82
+ try {
83
+ const memos = memosByAddress(ctx.network);
84
+ // refresh each cycle: requests issued mid-session get watched too
85
+ for (const address of trackedAddressesFromLedger(ctx.network)) {
86
+ if (stopping)
87
+ break;
88
+ const utxos = await chain.getUtxos(address);
89
+ const forAddr = known.get(address) ?? new Map();
90
+ known.set(address, forAddr);
91
+ // group this address's UTXOs by txid
92
+ const byTxid = new Map();
93
+ for (const u of utxos) {
94
+ const cur = byTxid.get(u.txid) ?? { sats: 0, confirmed: false };
95
+ cur.sats += u.satoshis;
96
+ cur.confirmed = (u.height ?? 0) > 0;
97
+ byTxid.set(u.txid, cur);
98
+ }
99
+ for (const [txid, info] of byTxid) {
100
+ const prior = forAddr.get(txid);
101
+ if (prior === undefined) {
102
+ forAddr.set(txid, info.confirmed);
103
+ if (!baselined)
104
+ continue; // pre-existing funds are not session events
105
+ sessionTotal += info.sats;
106
+ const memo = memos.get(address);
107
+ const status = info.confirmed ? 'confirmed' : 'pending';
108
+ emit({
109
+ event: 'payment',
110
+ status,
111
+ address,
112
+ txid,
113
+ amount_sats: info.sats,
114
+ ...(memo ? { memo } : {}),
115
+ session_total_sats: sessionTotal,
116
+ explorer_url: explorerTxUrl(ctx.network, txid),
117
+ }, `${chalk.green('+' + formatSats(info.sats))} ${status === 'pending' ? chalk.yellow('[pending]') : chalk.green('[confirmed]')}` +
118
+ `${memo ? ` "${memo}"` : ''} ${explorerTxUrl(ctx.network, txid)} (session total ${formatSats(sessionTotal)})`);
119
+ if (!alreadyLedgered.has(`${txid}:${address}`)) {
120
+ appendLedger(ctx.network, {
121
+ type: 'receive',
122
+ txid,
123
+ amount_sats: info.sats,
124
+ address,
125
+ memo,
126
+ timestamp: new Date().toISOString(),
127
+ status,
128
+ });
129
+ alreadyLedgered.add(`${txid}:${address}`);
130
+ }
131
+ }
132
+ else if (prior === false && info.confirmed) {
133
+ forAddr.set(txid, true);
134
+ emit({
135
+ event: 'confirmed',
136
+ address,
137
+ txid,
138
+ amount_sats: info.sats,
139
+ session_total_sats: sessionTotal,
140
+ explorer_url: explorerTxUrl(ctx.network, txid),
141
+ }, `${chalk.green('✓ confirmed')} ${formatSats(info.sats)} ${explorerTxUrl(ctx.network, txid)}`);
142
+ }
143
+ }
144
+ }
145
+ baselined = true;
146
+ backoff = 1; // healthy cycle resets any rate-limit backoff
147
+ }
148
+ catch (e) {
149
+ // Never crash the session on API trouble: back off and keep going.
150
+ backoff = Math.min(backoff * 2, MAX_BACKOFF_MULTIPLIER);
151
+ process.stderr.write(chalk.yellow(`watch: chain query failed (${e instanceof Error ? e.message.split('.')[0] : String(e)}); backing off to ${intervalSecs * backoff}s.`) + '\n');
152
+ }
153
+ if (cycle + 1 < maxCycles && !stopping) {
154
+ await interruptibleSleep(intervalSecs * backoff * 1000, abort.signal);
155
+ }
156
+ }
157
+ process.removeListener('SIGINT', onSigint);
158
+ if (ctx.json) {
159
+ process.stdout.write(JSON.stringify({ event: 'watch_stopped', session_total_sats: sessionTotal }) + '\n');
160
+ }
161
+ ctx.out.info('');
162
+ ctx.out.info(`Watch stopped. Session total received: ${formatSats(sessionTotal)}.`);
163
+ }
@@ -0,0 +1,16 @@
1
+ import { type Network } from './paths.js';
2
+ export interface Config {
3
+ /** Default network; --testnet always wins. */
4
+ network: Network;
5
+ /** Fee rate in satoshis per kilobyte. */
6
+ feeRateSatsPerKb: number;
7
+ /** watch/request --wait poll interval, seconds (floor 5). */
8
+ pollIntervalSecs: number;
9
+ /** Per-transaction spend limit in satoshis. */
10
+ spendLimitSats: number;
11
+ /** Show fiat equivalents in human output. */
12
+ fiatDisplay: boolean;
13
+ }
14
+ export declare const DEFAULT_CONFIG: Config;
15
+ /** Load ~/.bsv-pay/config.toml, falling back to defaults when absent. */
16
+ export declare function loadConfig(): Config;
package/dist/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import fs from 'node:fs';
2
+ import { parse as parseToml } from 'smol-toml';
3
+ import { usageError } from './errors.js';
4
+ import { configPath } from './paths.js';
5
+ export const DEFAULT_CONFIG = {
6
+ network: 'main',
7
+ feeRateSatsPerKb: 50,
8
+ pollIntervalSecs: 10,
9
+ spendLimitSats: 100_000,
10
+ fiatDisplay: false,
11
+ };
12
+ function asNumber(v, key) {
13
+ if (typeof v !== 'number' || !Number.isFinite(v) || v < 0) {
14
+ throw usageError('invalid_config', `Config key "${key}" must be a non-negative number. Fix ${configPath()}.`);
15
+ }
16
+ return v;
17
+ }
18
+ /** Load ~/.bsv-pay/config.toml, falling back to defaults when absent. */
19
+ export function loadConfig() {
20
+ const file = configPath();
21
+ if (!fs.existsSync(file))
22
+ return { ...DEFAULT_CONFIG };
23
+ let doc;
24
+ try {
25
+ doc = parseToml(fs.readFileSync(file, 'utf8'));
26
+ }
27
+ catch (e) {
28
+ throw usageError('invalid_config', `Cannot parse ${file}: ${e instanceof Error ? e.message : String(e)}. Fix the TOML syntax or delete the file to use defaults.`);
29
+ }
30
+ const cfg = { ...DEFAULT_CONFIG };
31
+ if ('network' in doc) {
32
+ if (doc.network !== 'main' && doc.network !== 'test') {
33
+ throw usageError('invalid_config', `Config "network" must be "main" or "test" (got ${JSON.stringify(doc.network)}). Fix ${file}.`);
34
+ }
35
+ cfg.network = doc.network;
36
+ }
37
+ if ('fee_rate_sats_per_kb' in doc)
38
+ cfg.feeRateSatsPerKb = asNumber(doc.fee_rate_sats_per_kb, 'fee_rate_sats_per_kb');
39
+ if ('poll_interval_secs' in doc) {
40
+ cfg.pollIntervalSecs = Math.max(5, asNumber(doc.poll_interval_secs, 'poll_interval_secs'));
41
+ }
42
+ if ('spend_limit_sats' in doc)
43
+ cfg.spendLimitSats = asNumber(doc.spend_limit_sats, 'spend_limit_sats');
44
+ if ('fiat_display' in doc) {
45
+ if (typeof doc.fiat_display !== 'boolean') {
46
+ throw usageError('invalid_config', `Config "fiat_display" must be true or false. Fix ${file}.`);
47
+ }
48
+ cfg.fiatDisplay = doc.fiat_display;
49
+ }
50
+ return cfg;
51
+ }
@@ -0,0 +1,13 @@
1
+ import { type Config } from './config.js';
2
+ import { Output } from './output.js';
3
+ import type { Network } from './paths.js';
4
+ export interface Ctx {
5
+ out: Output;
6
+ json: boolean;
7
+ network: Network;
8
+ config: Config;
9
+ }
10
+ export declare function buildCtx(opts: {
11
+ json?: boolean;
12
+ testnet?: boolean;
13
+ }): Ctx;
@@ -0,0 +1,12 @@
1
+ import { loadConfig } from './config.js';
2
+ import { Output } from './output.js';
3
+ export function buildCtx(opts) {
4
+ const config = loadConfig();
5
+ const network = opts.testnet ? 'test' : config.network;
6
+ return {
7
+ out: new Output(Boolean(opts.json)),
8
+ json: Boolean(opts.json),
9
+ network,
10
+ config,
11
+ };
12
+ }
@@ -0,0 +1,24 @@
1
+ import { type CoreOptions } from './context.js';
2
+ export interface AddressBalanceResult {
3
+ address: string;
4
+ confirmedSats: number;
5
+ unconfirmedSats: number;
6
+ }
7
+ export interface BalanceResult {
8
+ confirmedSats: number;
9
+ unconfirmedSats: number;
10
+ addresses: AddressBalanceResult[];
11
+ /** BRC-100 custody (additive, M12): the external wallet reported the total. */
12
+ backend?: 'brc100';
13
+ }
14
+ /**
15
+ * Aggregate balance across every address the wallet has issued. Addresses
16
+ * come from the ledger, so no passphrase or unlock is needed (read-only
17
+ * commands don't unlock — see DECISIONS.md M3). Throws code 2 `no_wallet`
18
+ * when no wallet exists.
19
+ *
20
+ * BRC-100 custody: the external wallet reports one spendable total — it
21
+ * does not expose per-address detail or a confirmed/unconfirmed split, so
22
+ * the total lands in confirmedSats and `addresses` is empty.
23
+ */
24
+ export declare function getBalance(opts: CoreOptions): Promise<BalanceResult>;
@@ -0,0 +1,34 @@
1
+ import { trackedAddressesFromLedger } from '../ledger.js';
2
+ import { connectBrc100 } from '../wallet/brc100.js';
3
+ import { readWalletFile } from '../wallet/wallet.js';
4
+ import { resolveCore } from './context.js';
5
+ /**
6
+ * Aggregate balance across every address the wallet has issued. Addresses
7
+ * come from the ledger, so no passphrase or unlock is needed (read-only
8
+ * commands don't unlock — see DECISIONS.md M3). Throws code 2 `no_wallet`
9
+ * when no wallet exists.
10
+ *
11
+ * BRC-100 custody: the external wallet reports one spendable total — it
12
+ * does not expose per-address detail or a confirmed/unconfirmed split, so
13
+ * the total lands in confirmedSats and `addresses` is empty.
14
+ */
15
+ export async function getBalance(opts) {
16
+ const { network, provider } = resolveCore(opts);
17
+ const file = readWalletFile(network); // throws no_wallet with guidance when absent
18
+ if (file.backend === 'brc100') {
19
+ const brc100 = await connectBrc100(network, { url: file.brc100_url, wallet: opts.brc100 });
20
+ const totalSats = await brc100.getBalanceSats();
21
+ return { confirmedSats: totalSats, unconfirmedSats: 0, addresses: [], backend: 'brc100' };
22
+ }
23
+ const addresses = trackedAddressesFromLedger(network);
24
+ let confirmedSats = 0;
25
+ let unconfirmedSats = 0;
26
+ const perAddress = [];
27
+ for (const address of addresses) {
28
+ const b = await provider.getBalance(address);
29
+ confirmedSats += b.confirmed;
30
+ unconfirmedSats += b.unconfirmed;
31
+ perAddress.push({ address, confirmedSats: b.confirmed, unconfirmedSats: b.unconfirmed });
32
+ }
33
+ return { confirmedSats, unconfirmedSats, addresses: perAddress };
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { ChainProvider } from '../chain/provider.js';
2
+ import { type Config } from '../config.js';
3
+ import type { Network } from '../paths.js';
4
+ import type { Brc100Interface } from '../wallet/brc100.js';
5
+ /**
6
+ * Options every core function accepts. Config is loaded from
7
+ * ~/.bsv-pay/config.toml (like the CLI) and merged with any overrides, so
8
+ * library and CLI behavior stay consistent. The provider defaults to
9
+ * WhatsOnChain; tests and the local e2e mock inject their own.
10
+ */
11
+ export interface CoreOptions {
12
+ network: Network;
13
+ config?: Partial<Config>;
14
+ provider?: ChainProvider;
15
+ /**
16
+ * Inject a connected BRC-100 wallet interface (tests, embedders). Used
17
+ * only when the wallet file delegates to BRC-100 custody; the default is
18
+ * an HTTP connection to the wallet app recorded at init.
19
+ */
20
+ brc100?: Brc100Interface;
21
+ }
22
+ export interface ResolvedCore {
23
+ network: Network;
24
+ config: Config;
25
+ provider: ChainProvider;
26
+ }
27
+ export declare function resolveCore(opts: CoreOptions): ResolvedCore;
@@ -0,0 +1,9 @@
1
+ import { WhatsOnChainProvider } from '../chain/whatsonchain.js';
2
+ import { loadConfig } from '../config.js';
3
+ export function resolveCore(opts) {
4
+ return {
5
+ network: opts.network,
6
+ config: { ...loadConfig(), ...opts.config },
7
+ provider: opts.provider ?? new WhatsOnChainProvider(opts.network),
8
+ };
9
+ }
@@ -0,0 +1,18 @@
1
+ import { type LedgerEntry } from '../ledger.js';
2
+ import type { CoreOptions } from './context.js';
3
+ export type MoneyMovement = Extract<LedgerEntry, {
4
+ type: 'send' | 'receive';
5
+ }>;
6
+ export interface HistoryParams {
7
+ /** Maximum entries to return (after filtering), newest first. */
8
+ limit?: number;
9
+ /** Restrict to sends or receives; default both. */
10
+ type?: 'send' | 'receive';
11
+ }
12
+ /**
13
+ * Money movements from the local append-only ledger, newest first. This is
14
+ * deliberately ledger-backed — no chain scan — so it is fast, offline, and
15
+ * reflects exactly what this wallet recorded (including memos, which exist
16
+ * only locally). Throws code 2 `no_wallet` when no wallet exists.
17
+ */
18
+ export declare function getHistory(opts: CoreOptions, params?: HistoryParams): MoneyMovement[];
@@ -0,0 +1,15 @@
1
+ import { readLedger } from '../ledger.js';
2
+ import { readWalletFile } from '../wallet/wallet.js';
3
+ /**
4
+ * Money movements from the local append-only ledger, newest first. This is
5
+ * deliberately ledger-backed — no chain scan — so it is fast, offline, and
6
+ * reflects exactly what this wallet recorded (including memos, which exist
7
+ * only locally). Throws code 2 `no_wallet` when no wallet exists.
8
+ */
9
+ export function getHistory(opts, params = {}) {
10
+ readWalletFile(opts.network); // throws no_wallet with guidance when absent
11
+ const movements = readLedger(opts.network).filter((e) => e.type === 'send' || e.type === 'receive');
12
+ const filtered = params.type ? movements.filter((e) => e.type === params.type) : movements;
13
+ filtered.reverse(); // ledger is append-only, so reversed = newest first
14
+ return params.limit !== undefined ? filtered.slice(0, params.limit) : filtered;
15
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * bsv-pay/core — programmatic API over the same engine the CLI uses.
3
+ *
4
+ * Contract: typed results, typed errors (BsvPayError carries the same code
5
+ * numbers the CLI maps to exit codes), no process.exit, no console output,
6
+ * and no key material in any return value (invariant 1). This file is the
7
+ * only supported import path; everything else under core/ is internal.
8
+ */
9
+ export { CliError as BsvPayError, EXIT, type ExitCode } from '../errors.js';
10
+ export type { Network } from '../paths.js';
11
+ export type { Config } from '../config.js';
12
+ export type { ChainProvider, Utxo, AddressBalance, HistoryItem, BroadcastResult, } from '../chain/provider.js';
13
+ export type { CoreOptions } from './context.js';
14
+ export { openWallet, CoreWallet, type OpenWalletOptions } from './wallet.js';
15
+ export { getBalance, type BalanceResult, type AddressBalanceResult } from './balance.js';
16
+ export { planSend, executeSend, send, explorerTxUrl, type SendParams, type SendPlan, type SendResult, } from './send.js';
17
+ export { getHistory, type HistoryParams, type MoneyMovement } from './history.js';
18
+ export { getPolicyStatus, type PolicyStatusResult, type PendingApprovalStatus, } from './policy-status.js';
19
+ export { createRequest, awaitPayment, buildPaymentUri, type RequestParams, type RequestResult, type AwaitPaymentParams, type PaymentResult, } from './request.js';
20
+ export { paidFetch, type PaidFetchParams, type PaidFetchResult, type PaidFetchPayment, } from '../http402/client.js';
21
+ export { requirePayment, type RequirePaymentOptions, type PaymentReceipt, type PaidRequest, type PaidRequestHandler, } from '../http402/middleware.js';
22
+ export type { LedgerEntry } from '../ledger.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * bsv-pay/core — programmatic API over the same engine the CLI uses.
3
+ *
4
+ * Contract: typed results, typed errors (BsvPayError carries the same code
5
+ * numbers the CLI maps to exit codes), no process.exit, no console output,
6
+ * and no key material in any return value (invariant 1). This file is the
7
+ * only supported import path; everything else under core/ is internal.
8
+ */
9
+ export { CliError as BsvPayError, EXIT } from '../errors.js';
10
+ export { openWallet, CoreWallet } from './wallet.js';
11
+ export { getBalance } from './balance.js';
12
+ export { planSend, executeSend, send, explorerTxUrl, } from './send.js';
13
+ export { getHistory } from './history.js';
14
+ export { getPolicyStatus, } from './policy-status.js';
15
+ export { createRequest, awaitPayment, buildPaymentUri, } from './request.js';
16
+ export { paidFetch, } from '../http402/client.js';
17
+ export { requirePayment, } from '../http402/middleware.js';
@@ -0,0 +1,22 @@
1
+ import type { Brc100Wallet } from '../wallet/brc100.js';
2
+ import type { Wallet } from '../wallet/wallet.js';
3
+ import type { CoreWallet } from './wallet.js';
4
+ /**
5
+ * Module-private bridge between the public CoreWallet (addresses/metadata
6
+ * only — invariant 1) and the custody backend behind it: the local signing
7
+ * Wallet, or the BRC-100 external-wallet handle (key-capable by proxy, so
8
+ * it stays behind the same boundary). Core modules register and unwrap
9
+ * here; this file is deliberately NOT exported from core/index.ts, so
10
+ * key-capable objects never cross the public library boundary.
11
+ */
12
+ export type InnerBackend = {
13
+ kind: 'local';
14
+ wallet: Wallet;
15
+ } | {
16
+ kind: 'brc100';
17
+ wallet: Brc100Wallet;
18
+ };
19
+ export declare function registerWallet(pub: CoreWallet, backend: InnerBackend): void;
20
+ export declare function unwrapBackend(pub: CoreWallet): InnerBackend;
21
+ /** The local signing wallet. Callers must branch on backend kind first. */
22
+ export declare function unwrapWallet(pub: CoreWallet): Wallet;
@@ -0,0 +1,19 @@
1
+ const inner = new WeakMap();
2
+ export function registerWallet(pub, backend) {
3
+ inner.set(pub, backend);
4
+ }
5
+ export function unwrapBackend(pub) {
6
+ const backend = inner.get(pub);
7
+ if (!backend) {
8
+ throw new Error('CoreWallet is not registered; obtain it from openWallet().');
9
+ }
10
+ return backend;
11
+ }
12
+ /** The local signing wallet. Callers must branch on backend kind first. */
13
+ export function unwrapWallet(pub) {
14
+ const backend = unwrapBackend(pub);
15
+ if (backend.kind !== 'local') {
16
+ throw new Error('Internal misuse: this code path needs the local signing wallet but the CoreWallet delegates to BRC-100 custody.');
17
+ }
18
+ return backend.wallet;
19
+ }
@@ -0,0 +1,55 @@
1
+ import type { Network } from '../paths.js';
2
+ import type { CoreOptions } from './context.js';
3
+ export interface PendingApprovalStatus {
4
+ approvalId: string;
5
+ address: string;
6
+ amountSats: number;
7
+ memo?: string;
8
+ confirmedOnly?: boolean;
9
+ queuedAt: string;
10
+ }
11
+ /**
12
+ * A point-in-time view of the active policy and how much headroom is left.
13
+ * Limits that are not configured are absent (absent = unlimited, except the
14
+ * per-tx pair where absent means "only the other one applies"). Remaining
15
+ * values are clamped at 0 — they are planning hints, not promises: the
16
+ * binding check is the policy gate at spend time.
17
+ */
18
+ export interface PolicyStatusResult {
19
+ source: 'defaults' | 'file';
20
+ network: Network;
21
+ /** Hard per-transaction cap (policy.toml). No override exists. */
22
+ perTxLimitSats?: number;
23
+ /** Legacy confirmable limit from config.toml (pre-policy behavior). */
24
+ softPerTxLimitSats?: number;
25
+ dailyBudgetSats?: number;
26
+ dailyRemainingSats?: number;
27
+ sessionBudgetSats?: number;
28
+ sessionRemainingSats?: number;
29
+ rateLimitPerMinute?: number;
30
+ remainingThisMinute?: number;
31
+ rateLimitPerHour?: number;
32
+ remainingThisHour?: number;
33
+ /** At/above this, spends queue for human approval instead of sending. */
34
+ approvalThresholdSats?: number;
35
+ /** False with a threshold set means queued payments cannot be approved. */
36
+ approvalSecretConfigured: boolean;
37
+ /** When non-empty, only these recipients are allowed. */
38
+ allowlist: string[];
39
+ /** Always wins over everything else. */
40
+ denylist: string[];
41
+ usage: {
42
+ dailySpentSats: number;
43
+ sessionSpentSats: number;
44
+ sendsLastMinute: number;
45
+ sendsLastHour: number;
46
+ };
47
+ pendingApprovals: PendingApprovalStatus[];
48
+ }
49
+ /**
50
+ * Read the active policy, current usage (recomputed from the ledger), and
51
+ * the pending-approval queue. No wallet unlock, no network I/O — works
52
+ * before init and never touches keys. This is what agents should call to
53
+ * plan within their allowance instead of discovering denials by failing.
54
+ */
55
+ export declare function getPolicyStatus(opts: CoreOptions): PolicyStatusResult;
@@ -0,0 +1,49 @@
1
+ import { loadConfig } from '../config.js';
2
+ import { approvalSecretConfigured, listPendingApprovals } from '../policy/approvals.js';
3
+ import { readUsage } from '../policy/budget.js';
4
+ import { loadPolicy } from '../policy/policy.js';
5
+ /**
6
+ * Read the active policy, current usage (recomputed from the ledger), and
7
+ * the pending-approval queue. No wallet unlock, no network I/O — works
8
+ * before init and never touches keys. This is what agents should call to
9
+ * plan within their allowance instead of discovering denials by failing.
10
+ */
11
+ export function getPolicyStatus(opts) {
12
+ const config = { ...loadConfig(), ...opts.config };
13
+ const policy = loadPolicy(opts.network, config);
14
+ const usage = readUsage(opts.network);
15
+ const pending = listPendingApprovals(opts.network);
16
+ const remaining = (budget, used) => budget === undefined ? undefined : Math.max(0, budget - used);
17
+ return {
18
+ source: policy.source,
19
+ network: opts.network,
20
+ perTxLimitSats: policy.perTxLimitSats,
21
+ softPerTxLimitSats: policy.softPerTxLimitSats,
22
+ dailyBudgetSats: policy.dailyBudgetSats,
23
+ dailyRemainingSats: remaining(policy.dailyBudgetSats, usage.dailySpentSats),
24
+ sessionBudgetSats: policy.sessionBudgetSats,
25
+ sessionRemainingSats: remaining(policy.sessionBudgetSats, usage.sessionSpentSats),
26
+ rateLimitPerMinute: policy.rateLimitPerMinute,
27
+ remainingThisMinute: remaining(policy.rateLimitPerMinute, usage.sendsLastMinute),
28
+ rateLimitPerHour: policy.rateLimitPerHour,
29
+ remainingThisHour: remaining(policy.rateLimitPerHour, usage.sendsLastHour),
30
+ approvalThresholdSats: policy.approvalThresholdSats,
31
+ approvalSecretConfigured: approvalSecretConfigured(),
32
+ allowlist: [...policy.allowlist],
33
+ denylist: [...policy.denylist],
34
+ usage: {
35
+ dailySpentSats: usage.dailySpentSats,
36
+ sessionSpentSats: usage.sessionSpentSats,
37
+ sendsLastMinute: usage.sendsLastMinute,
38
+ sendsLastHour: usage.sendsLastHour,
39
+ },
40
+ pendingApprovals: pending.map((p) => ({
41
+ approvalId: p.approvalId,
42
+ address: p.address,
43
+ amountSats: p.amountSats,
44
+ memo: p.memo,
45
+ confirmedOnly: p.confirmedOnly,
46
+ queuedAt: p.queuedAt,
47
+ })),
48
+ };
49
+ }
@@ -0,0 +1,43 @@
1
+ import type { Network } from '../paths.js';
2
+ import { type CoreOptions } from './context.js';
3
+ import type { CoreWallet } from './wallet.js';
4
+ /** BIP-21-style URI with the BSV `sv` discriminator. Amount is in BSV. */
5
+ export declare function buildPaymentUri(address: string, amountSats: number, memo?: string): string;
6
+ export interface RequestParams {
7
+ amountSats: number;
8
+ memo?: string;
9
+ }
10
+ export interface RequestResult {
11
+ address: string;
12
+ amountSats: number;
13
+ memo?: string;
14
+ uri: string;
15
+ network: Network;
16
+ }
17
+ /**
18
+ * Issue a fresh receiving address (one per request, so matching is
19
+ * unambiguous) and build the BIP-21 URI. The address is persisted to the
20
+ * wallet counter and ledger immediately.
21
+ */
22
+ export declare function createRequest(wallet: CoreWallet, params: RequestParams): RequestResult;
23
+ export interface AwaitPaymentParams {
24
+ address: string;
25
+ timeoutMs: number;
26
+ /** Default: config poll_interval_secs. */
27
+ pollIntervalMs?: number;
28
+ /** Recorded on the ledger receive entry (memos are local-only). */
29
+ memo?: string;
30
+ }
31
+ export interface PaymentResult {
32
+ address: string;
33
+ txid: string;
34
+ receivedSats: number;
35
+ confirmed: boolean;
36
+ }
37
+ /**
38
+ * Poll an address until the first incoming payment appears at 0-conf, then
39
+ * record the receive in the ledger (invariant 6) and return it. Transient
40
+ * chain failures keep polling until the deadline. Timeout throws code 4
41
+ * `request_timeout`.
42
+ */
43
+ export declare function awaitPayment(opts: CoreOptions, params: AwaitPaymentParams): Promise<PaymentResult>;