@obbyx/cli 0.0.0-stage → 0.1.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 (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -2
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +117 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/address.d.ts +3 -0
  8. package/dist/commands/address.d.ts.map +1 -0
  9. package/dist/commands/address.js +13 -0
  10. package/dist/commands/address.js.map +1 -0
  11. package/dist/commands/balance.d.ts +3 -0
  12. package/dist/commands/balance.d.ts.map +1 -0
  13. package/dist/commands/balance.js +30 -0
  14. package/dist/commands/balance.js.map +1 -0
  15. package/dist/commands/channel-cli.d.ts +11 -0
  16. package/dist/commands/channel-cli.d.ts.map +1 -0
  17. package/dist/commands/channel-cli.js +77 -0
  18. package/dist/commands/channel-cli.js.map +1 -0
  19. package/dist/commands/channel.d.ts +11 -0
  20. package/dist/commands/channel.d.ts.map +1 -0
  21. package/dist/commands/channel.js +77 -0
  22. package/dist/commands/channel.js.map +1 -0
  23. package/dist/commands/decode.d.ts +5 -0
  24. package/dist/commands/decode.d.ts.map +1 -0
  25. package/dist/commands/decode.js +17 -0
  26. package/dist/commands/decode.js.map +1 -0
  27. package/dist/commands/doctor-command.d.ts +5 -0
  28. package/dist/commands/doctor-command.d.ts.map +1 -0
  29. package/dist/commands/doctor-command.js +17 -0
  30. package/dist/commands/doctor-command.js.map +1 -0
  31. package/dist/commands/doctor.d.ts +5 -0
  32. package/dist/commands/doctor.d.ts.map +1 -0
  33. package/dist/commands/doctor.js +17 -0
  34. package/dist/commands/doctor.js.map +1 -0
  35. package/dist/commands/faucet.d.ts +5 -0
  36. package/dist/commands/faucet.d.ts.map +1 -0
  37. package/dist/commands/faucet.js +42 -0
  38. package/dist/commands/faucet.js.map +1 -0
  39. package/dist/commands/init.d.ts +5 -0
  40. package/dist/commands/init.d.ts.map +1 -0
  41. package/dist/commands/init.js +26 -0
  42. package/dist/commands/init.js.map +1 -0
  43. package/dist/commands/logs-command.d.ts +12 -0
  44. package/dist/commands/logs-command.d.ts.map +1 -0
  45. package/dist/commands/logs-command.js +32 -0
  46. package/dist/commands/logs-command.js.map +1 -0
  47. package/dist/commands/logs.d.ts +12 -0
  48. package/dist/commands/logs.d.ts.map +1 -0
  49. package/dist/commands/logs.js +32 -0
  50. package/dist/commands/logs.js.map +1 -0
  51. package/dist/commands/pay.d.ts +14 -0
  52. package/dist/commands/pay.d.ts.map +1 -0
  53. package/dist/commands/pay.js +115 -0
  54. package/dist/commands/pay.js.map +1 -0
  55. package/dist/commands/types.d.ts +27 -0
  56. package/dist/commands/types.d.ts.map +1 -0
  57. package/dist/commands/types.js +2 -0
  58. package/dist/commands/types.js.map +1 -0
  59. package/dist/faucet/device.d.ts +74 -0
  60. package/dist/faucet/device.d.ts.map +1 -0
  61. package/dist/faucet/device.js +222 -0
  62. package/dist/faucet/device.js.map +1 -0
  63. package/dist/faucet/faucet.d.ts +40 -0
  64. package/dist/faucet/faucet.d.ts.map +1 -0
  65. package/dist/faucet/faucet.js +111 -0
  66. package/dist/faucet/faucet.js.map +1 -0
  67. package/dist/lib/channels.d.ts +24 -0
  68. package/dist/lib/channels.d.ts.map +1 -0
  69. package/dist/lib/channels.js +111 -0
  70. package/dist/lib/channels.js.map +1 -0
  71. package/dist/lib/doctor.d.ts +7 -0
  72. package/dist/lib/doctor.d.ts.map +1 -0
  73. package/dist/lib/doctor.js +97 -0
  74. package/dist/lib/doctor.js.map +1 -0
  75. package/dist/lib/logs.d.ts +16 -0
  76. package/dist/lib/logs.d.ts.map +1 -0
  77. package/dist/lib/logs.js +64 -0
  78. package/dist/lib/logs.js.map +1 -0
  79. package/dist/main.d.ts +3 -0
  80. package/dist/main.d.ts.map +1 -0
  81. package/dist/main.js +11 -0
  82. package/dist/main.js.map +1 -0
  83. package/dist/runtime.d.ts +21 -0
  84. package/dist/runtime.d.ts.map +1 -0
  85. package/dist/runtime.js +86 -0
  86. package/dist/runtime.js.map +1 -0
  87. package/package.json +52 -3
  88. package/src/cli.ts +139 -0
  89. package/src/commands/address.ts +14 -0
  90. package/src/commands/balance.ts +35 -0
  91. package/src/commands/channel.ts +96 -0
  92. package/src/commands/decode.ts +19 -0
  93. package/src/commands/doctor.ts +21 -0
  94. package/src/commands/faucet.ts +48 -0
  95. package/src/commands/init.ts +28 -0
  96. package/src/commands/logs.ts +48 -0
  97. package/src/commands/pay.ts +135 -0
  98. package/src/commands/types.ts +31 -0
  99. package/src/faucet/device.ts +330 -0
  100. package/src/faucet/faucet.ts +141 -0
  101. package/src/lib/channels.ts +161 -0
  102. package/src/lib/doctor.ts +133 -0
  103. package/src/lib/logs.ts +79 -0
  104. package/src/main.ts +14 -0
  105. package/src/runtime.ts +123 -0
@@ -0,0 +1,96 @@
1
+ import type { Argv } from 'yargs';
2
+ import { loadWallet } from '@obbyx/obyte/wallet';
3
+ import { channelCommand } from '../lib/channels.js';
4
+ import type { CommandContext, Cmd, GlobalArgs } from './types.js';
5
+ import { cliLogger, connect, reservingComposer, resolveNetwork } from '../runtime.js';
6
+
7
+ interface ChannelArgs {
8
+ target?: string;
9
+ deposit?: string;
10
+ amount?: string;
11
+ window?: string;
12
+ }
13
+
14
+ /** `obbyx channel <open|list|status|deposit|close|confirm>`. */
15
+ export function channelCommands({ out, done }: CommandContext): Cmd<ChannelArgs>[] {
16
+ const channelRun =
17
+ (sub: string) =>
18
+ async (a: GlobalArgs & ChannelArgs): Promise<void> => {
19
+ const network = resolveNetwork(a.network);
20
+ const wallet = loadWallet(network);
21
+ const logger = cliLogger();
22
+ const client = connect(network, a.hub);
23
+ try {
24
+ done(
25
+ await channelCommand(
26
+ {
27
+ sub,
28
+ target: a.target,
29
+ ...(a.deposit === undefined ? {} : { deposit: a.deposit }),
30
+ ...(a.amount === undefined ? {} : { amount: a.amount }),
31
+ ...(a.window === undefined ? {} : { timeout: a.window }),
32
+ },
33
+ {
34
+ network,
35
+ client,
36
+ privateKey: wallet.privateKey,
37
+ logger,
38
+ out,
39
+ composer: reservingComposer(client, network, wallet.address, logger),
40
+ },
41
+ ),
42
+ );
43
+ } finally {
44
+ client.close();
45
+ }
46
+ };
47
+ const aaArg = (y: Argv) =>
48
+ y.positional('target', { type: 'string', demandOption: true, describe: 'Channel AA address' });
49
+
50
+ return [
51
+ {
52
+ command: 'open <target>',
53
+ describe: 'Open a payment channel to a seller (instant micropayments)',
54
+ builder: (y) =>
55
+ y
56
+ .positional('target', { type: 'string', demandOption: true, describe: 'Seller address' })
57
+ .option('deposit', { type: 'string', demandOption: true, describe: 'Deposit, GBYTE' })
58
+ .option('window', {
59
+ type: 'string',
60
+ default: '86400',
61
+ describe: 'Dispute window, seconds (sellers require a minimum)',
62
+ }),
63
+ handler: channelRun('open'),
64
+ },
65
+ { command: 'list', describe: 'List your channels', handler: channelRun('list') },
66
+ {
67
+ command: 'status <target>',
68
+ describe: 'Show a channel (balances, spent, period)',
69
+ builder: (y) => aaArg(y),
70
+ handler: channelRun('status'),
71
+ },
72
+ {
73
+ command: 'deposit <target>',
74
+ describe: 'Top up a channel',
75
+ builder: (y) =>
76
+ aaArg(y).option('amount', {
77
+ type: 'string',
78
+ demandOption: true,
79
+ describe: 'Amount, GBYTE',
80
+ }),
81
+ handler: channelRun('deposit'),
82
+ },
83
+ {
84
+ command: 'close <target>',
85
+ describe: 'Start closing a channel (the dispute window begins)',
86
+ builder: (y) => aaArg(y),
87
+ handler: channelRun('close'),
88
+ },
89
+ {
90
+ command: 'confirm <target>',
91
+ describe: 'Finish closing after the dispute window and get the rest back',
92
+ builder: (y) => aaArg(y),
93
+ handler: channelRun('confirm'),
94
+ },
95
+ ];
96
+ }
@@ -0,0 +1,19 @@
1
+ import { decodeAny } from '../runtime.js';
2
+ import type { CommandContext, Cmd } from './types.js';
3
+
4
+ export function decodeCommand({ out, done }: CommandContext): Cmd<{ value: string }> {
5
+ return {
6
+ command: 'decode <value>',
7
+ describe: 'Decode an x402 header (PAYMENT-REQUIRED / -SIGNATURE / -RESPONSE)',
8
+ builder: (y) =>
9
+ y.positional('value', {
10
+ type: 'string',
11
+ demandOption: true,
12
+ describe: 'Base64 header value',
13
+ }),
14
+ handler: (a) => {
15
+ out(JSON.stringify(decodeAny(a.value), null, 2));
16
+ done(0);
17
+ },
18
+ };
19
+ }
@@ -0,0 +1,21 @@
1
+ import { doctor } from '../lib/doctor.js';
2
+ import { resolveNetwork } from '../runtime.js';
3
+ import type { CommandContext, Cmd } from './types.js';
4
+
5
+ export function doctorCommand({ out, done }: CommandContext): Cmd<{ facilitator?: string }> {
6
+ return {
7
+ command: 'doctor',
8
+ describe: 'Check wallet, hub, balance, pending payments',
9
+ builder: (y) =>
10
+ y.option('facilitator', { type: 'string', describe: 'Facilitator URL to check' }),
11
+ handler: async (a) => {
12
+ done(
13
+ await doctor(resolveNetwork(a.network), {
14
+ ...(a.hub === undefined ? {} : { hub: a.hub }),
15
+ ...(a.facilitator === undefined ? {} : { facilitator: a.facilitator }),
16
+ out,
17
+ }),
18
+ );
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,48 @@
1
+ import { loadWallet } from '@obbyx/obyte/wallet';
2
+ import { requestFromFaucet } from '../faucet/faucet.js';
3
+ import { cliLogger, resolveNetwork, seconds } from '../runtime.js';
4
+ import type { CommandContext, Cmd } from './types.js';
5
+
6
+ export function faucetCommand({ out, done }: CommandContext): Cmd<{ timeout: string }> {
7
+ return {
8
+ command: 'faucet',
9
+ describe: 'Get free testnet bytes (once per 24 h)',
10
+ builder: (y) =>
11
+ y.option('timeout', {
12
+ type: 'string',
13
+ default: '60',
14
+ describe: 'How long to wait for the faucet bot, seconds',
15
+ }),
16
+ handler: async (a) => {
17
+ const network = resolveNetwork(a.network);
18
+ const wallet = loadWallet(network);
19
+ out(`Asking the ${network.caip2} faucet to fund ${wallet.address} ...`);
20
+ const result = await requestFromFaucet({
21
+ network,
22
+ address: wallet.address,
23
+ logger: cliLogger(),
24
+ timeoutMs: seconds(a.timeout, 'timeout') * 1000,
25
+ onBotText: (text) => out(` faucet: ${text}`),
26
+ });
27
+ switch (result.status) {
28
+ case 'sent':
29
+ out(
30
+ 'Bytes are on the way. They become spendable once stable (minutes, up to ~2 h on a quiet network): obbyx balance',
31
+ );
32
+ return done(0);
33
+ case 'rate_limited':
34
+ out('The faucet pays each device once per 24 hours. Try again later.');
35
+ return done(2);
36
+ case 'rejected':
37
+ out('The faucet rejected the request (see its message above).');
38
+ return done(2);
39
+ case 'no_answer':
40
+ out(
41
+ 'No answer from the faucet yet. Check `obbyx balance` in a few minutes or retry later.',
42
+ );
43
+ return done(3);
44
+ }
45
+ done(3);
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,28 @@
1
+ import { createWallet, walletPath } from '@obbyx/obyte/wallet';
2
+ import { resolveNetwork } from '../runtime.js';
3
+ import type { CommandContext, Cmd } from './types.js';
4
+
5
+ export function initCommand({ out, done }: CommandContext): Cmd<{ force: boolean }> {
6
+ return {
7
+ command: 'init',
8
+ describe: 'Create a local wallet',
9
+ builder: (y) =>
10
+ y.option('force', {
11
+ type: 'boolean',
12
+ default: false,
13
+ describe: 'Overwrite an existing wallet',
14
+ }),
15
+ handler: (a) => {
16
+ const network = resolveNetwork(a.network);
17
+ const wallet = createWallet(network, { force: a.force });
18
+ out(`Wallet created for ${network.caip2}`);
19
+ out(` address: ${wallet.address}`);
20
+ out(` file: ${walletPath(network)} (keep it private; it holds your key)`);
21
+ if (network.testnet) {
22
+ out();
23
+ out('Fund it with testnet bytes: obbyx faucet (then: obbyx balance)');
24
+ }
25
+ done(0);
26
+ },
27
+ };
28
+ }
@@ -0,0 +1,48 @@
1
+ import { logs } from '../lib/logs.js';
2
+ import type { CommandContext, Cmd } from './types.js';
3
+
4
+ interface LogsArgs {
5
+ files: string[];
6
+ binding?: string;
7
+ proof?: string;
8
+ aa?: string;
9
+ request?: string;
10
+ level?: string;
11
+ }
12
+ export function logsCommand({ out, done }: CommandContext): Cmd<LogsArgs> {
13
+ return {
14
+ command: 'logs <files..>',
15
+ describe: 'Follow one payment through JSON log files',
16
+ builder: (y) =>
17
+ y
18
+ .positional('files', {
19
+ type: 'string',
20
+ array: true,
21
+ demandOption: true,
22
+ describe: 'JSON log files',
23
+ })
24
+ .option('binding', { type: 'string', describe: 'Only entries of this payment binding' })
25
+ .option('proof', { type: 'string', describe: 'Only entries of this proof id' })
26
+ .option('aa', { type: 'string', describe: 'Only entries of this channel AA' })
27
+ .option('request', { type: 'string', describe: 'Only entries of this request id' })
28
+ .option('level', {
29
+ type: 'string',
30
+ describe: 'Minimum level (debug, info, warn, ...)',
31
+ }),
32
+ handler: async (a) => {
33
+ done(
34
+ await logs(
35
+ a.files,
36
+ {
37
+ ...(a.binding === undefined ? {} : { binding: a.binding }),
38
+ ...(a.proof === undefined ? {} : { proof: a.proof }),
39
+ ...(a.aa === undefined ? {} : { aa: a.aa }),
40
+ ...(a.request === undefined ? {} : { request: a.request }),
41
+ ...(a.level === undefined ? {} : { level: a.level }),
42
+ },
43
+ out,
44
+ ),
45
+ );
46
+ },
47
+ };
48
+ }
@@ -0,0 +1,135 @@
1
+ import { ObbyxError, fromDisplay } from '@obbyx/obyte';
2
+ import { loadWallet } from '@obbyx/obyte/wallet';
3
+ import { BatchObyteClient, ClientChannelStore, ExactObyteClient } from '@obbyx/x402-obyte';
4
+ import { PaymentPendingError, payingFetch } from '@obbyx/sdk';
5
+ import { channelStorePath } from '../lib/channels.js';
6
+ import {
7
+ bytesArg,
8
+ cliLogger,
9
+ connect,
10
+ printPayment,
11
+ reservingComposer,
12
+ resolveNetwork,
13
+ seconds,
14
+ } from '../runtime.js';
15
+ import type { CommandContext, Cmd } from './types.js';
16
+
17
+ interface PayArgs {
18
+ url: string;
19
+ max: string;
20
+ method?: string;
21
+ data?: string;
22
+ header?: string[];
23
+ payment?: string;
24
+ wait: string;
25
+ 'max-fee': string;
26
+ }
27
+ export function payCommand({ out, err, done }: CommandContext): Cmd<PayArgs> {
28
+ return {
29
+ command: 'pay <url>',
30
+ describe: 'Call a paid API, paying at most --max',
31
+ builder: (y) =>
32
+ y
33
+ .positional('url', { type: 'string', demandOption: true, describe: 'URL of the paid API' })
34
+ .option('max', {
35
+ type: 'string',
36
+ demandOption: 'pay needs --max <GBYTE>: the most you allow this call to cost',
37
+ describe: 'Spending limit for this call, in GBYTE (e.g. 0.00002)',
38
+ })
39
+ .option('method', {
40
+ type: 'string',
41
+ describe: 'HTTP method (default GET, or POST with --data)',
42
+ })
43
+ .option('data', { type: 'string', describe: 'Request body (JSON by default)' })
44
+ .option('header', {
45
+ type: 'string',
46
+ array: true,
47
+ describe: "Extra header 'Name: value' (repeatable)",
48
+ })
49
+ .option('payment', {
50
+ type: 'string',
51
+ describe: 'Retry a pending request with the same payment (printed by pay)',
52
+ })
53
+ .option('wait', {
54
+ type: 'string',
55
+ default: '600',
56
+ describe:
57
+ 'How long to wait for the payment to become final, seconds (testnet can take minutes). It never pays twice meanwhile.',
58
+ })
59
+ .option('max-fee', {
60
+ type: 'string',
61
+ default: '10000',
62
+ describe: 'Most network fee (bytes) an on-chain (exact) payment may cost',
63
+ }),
64
+ handler: async (a) => {
65
+ const network = resolveNetwork(a.network);
66
+ // Validate every argument before touching the wallet or the network.
67
+ const max = fromDisplay(a.max, 9);
68
+ const maxFee = bytesArg(a['max-fee'], 'max-fee');
69
+ const waitMs = seconds(a.wait, 'wait') * 1000;
70
+ const wallet = loadWallet(network);
71
+ const logger = cliLogger();
72
+ const client = connect(network, a.hub);
73
+ try {
74
+ const composer = reservingComposer(client, network, wallet.address, logger);
75
+ const mechanism = new ExactObyteClient({
76
+ client,
77
+ network: network.caip2,
78
+ privateKey: wallet.privateKey,
79
+ logger,
80
+ composer,
81
+ maxFee,
82
+ });
83
+ // Use a payment channel to this seller when one is open (instant, no fee per call).
84
+ const batch = new BatchObyteClient({
85
+ client,
86
+ network,
87
+ privateKey: wallet.privateKey,
88
+ store: new ClientChannelStore(channelStorePath(network)),
89
+ logger,
90
+ maxAmount: new Map([['base', max]]),
91
+ composer,
92
+ });
93
+ const headers: Record<string, string> = {};
94
+ for (const h of a.header ?? []) {
95
+ const i = h.indexOf(':');
96
+ if (i <= 0) throw new ObbyxError('usage', `Bad --header "${h}" (expected "Name: value")`);
97
+ headers[h.slice(0, i).trim()] = h.slice(i + 1).trim();
98
+ }
99
+ if (a.data !== undefined) headers['content-type'] ??= 'application/json';
100
+ const method = a.method ?? (a.data === undefined ? 'GET' : 'POST');
101
+ try {
102
+ const result = await payingFetch(
103
+ a.url,
104
+ {
105
+ method,
106
+ headers,
107
+ ...(a.data === undefined ? {} : { body: a.data }),
108
+ ...(a.payment === undefined ? {} : { paymentHeader: a.payment }),
109
+ },
110
+ {
111
+ mechanisms: { exact: mechanism, batch },
112
+ network: network.caip2,
113
+ maxAmount: new Map([['base', max]]),
114
+ logger,
115
+ pendingTimeoutMs: waitMs,
116
+ },
117
+ );
118
+ err(`HTTP ${result.response.status}`);
119
+ if (result.payment) printPayment(err, network, result.payment);
120
+ out(await result.response.text());
121
+ return done(result.response.ok ? 0 : 2);
122
+ } catch (e) {
123
+ if (e instanceof PaymentPendingError) {
124
+ err(`Payment ${e.transaction} is not final yet. Retry the same request later with:`);
125
+ err(` --payment ${e.paymentHeader}`);
126
+ return done(3);
127
+ }
128
+ throw e;
129
+ }
130
+ } finally {
131
+ client.close();
132
+ }
133
+ },
134
+ };
135
+ }
@@ -0,0 +1,31 @@
1
+ import type { Argv } from 'yargs';
2
+
3
+ /** Output of the CLI (stdout / stderr), injectable for tests. */
4
+ export interface CliIo {
5
+ out(line?: string): void;
6
+ err(line: string): void;
7
+ }
8
+
9
+ /** Options every command accepts. */
10
+ export interface GlobalArgs {
11
+ network: string;
12
+ hub: string | undefined;
13
+ }
14
+
15
+ /** What a command needs from the runner: output and a way to set the exit code. */
16
+ export interface CommandContext {
17
+ readonly out: (line?: string) => void;
18
+ readonly err: (line: string) => void;
19
+ /** yargs handlers cannot return a value: the exit code is set here. */
20
+ readonly done: (code: number) => void;
21
+ }
22
+
23
+ /** One command; the builder declares the options/positionals that `A` describes. */
24
+ export interface Cmd<A = object> {
25
+ readonly command: string;
26
+ readonly describe: string;
27
+ readonly builder?: (y: Argv) => Argv;
28
+ readonly handler: (args: GlobalArgs & A) => void | Promise<void>;
29
+ }
30
+
31
+ export type AnyCmd = Cmd<never>;