@1sat/cli 0.0.21 → 0.0.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
package/src/args.ts CHANGED
@@ -82,6 +82,24 @@ export function extractFlag(args: string[], flag: string): string | undefined {
82
82
  return args[idx + 1]
83
83
  }
84
84
 
85
+ /**
86
+ * Extract multiple values for a flag, supporting comma-delimited and multiple flag occurrences.
87
+ * Example: --tags a,b,c OR --tags a --tags b --tags c
88
+ */
89
+ export function extractFlags(args: string[], flag: string): string[] {
90
+ const values: string[] = []
91
+ for (let i = 0; i < args.length - 1; i++) {
92
+ if (args[i] === flag) {
93
+ const value = args[i + 1]
94
+ if (value && !value.startsWith('--')) {
95
+ // Split comma-delimited values
96
+ values.push(...value.split(',').filter(v => v.length > 0))
97
+ }
98
+ }
99
+ }
100
+ return values
101
+ }
102
+
85
103
  /**
86
104
  * Check if a boolean flag is present.
87
105
  */
@@ -8,6 +8,7 @@
8
8
  * 4. Config file creation
9
9
  */
10
10
 
11
+ import { randomBytes } from 'node:crypto'
11
12
  import { PrivateKey } from '@bsv/sdk'
12
13
  import {
13
14
  cancel,
@@ -162,16 +163,8 @@ export async function handleInitCommand(
162
163
  // bitcoin-backup Touch ID not available — skip silently
163
164
  }
164
165
 
165
- // 4. Optional: storage identity key
166
- const storageId = await text({
167
- message: 'Storage identity key (for wallet persistence):',
168
- defaultValue: '1sat-cli-default',
169
- placeholder: '1sat-cli-default',
170
- })
171
- if (isCancel(storageId)) {
172
- cancel('Setup cancelled.')
173
- process.exit(0)
174
- }
166
+ // 4. Generate random storage identity key
167
+ const storageId = `1sat-cli-${randomBytes(8).toString('hex')}`
175
168
 
176
169
  // 5. Optional: remote storage configuration
177
170
  const useRemote = await confirm({
@@ -220,7 +213,7 @@ export async function handleInitCommand(
220
213
  saveConfig({
221
214
  ...loadConfig(),
222
215
  chain: chain as 'main' | 'test',
223
- storageIdentityKey: storageId as string,
216
+ storageIdentityKey: storageId,
224
217
  activeRemote,
225
218
  })
226
219
 
@@ -81,6 +81,9 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
81
81
  chain: opts.chain,
82
82
  })
83
83
 
84
+ // Load config early — needed for storage identity key lookups
85
+ const config = loadConfig()
86
+
84
87
  try {
85
88
  // For backup-only, we use StorageClient via wallet-node
86
89
  // biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
@@ -99,7 +102,6 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
99
102
  await walletResult.storage.updateBackups()
100
103
 
101
104
  // Persist to config — connectivity will be validated on next monitor run
102
- const config = loadConfig()
103
105
  const existing = config.backups ?? []
104
106
  if (!existing.includes(url)) {
105
107
  saveConfig({ ...config, backups: [...existing, url] })
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Wallet commands - balance, address, send, send-all, info.
3
+ * BRC-100 interface commands - list-outputs, relinquish-output, list-actions, etc.
3
4
  */
4
5
 
5
6
  import { deriveDepositAddresses, sendAllBsv, sendBsv } from '@1sat/actions'
6
7
  import { confirm, isCancel } from '@clack/prompts'
7
8
  import type { GlobalFlags } from '../args'
8
- import { extractFlag } from '../args'
9
+ import { extractFlag, extractFlags } from '../args'
9
10
  import { loadContext } from '../context'
10
11
  import { printCommandHelp } from '../help'
11
12
  import { loadKey, resolvePassword } from '../keys'
@@ -28,6 +29,22 @@ export async function handleWalletCommand(
28
29
  return walletSendAll(rest, opts)
29
30
  case 'info':
30
31
  return walletInfo(rest, opts)
32
+ case 'list-outputs':
33
+ return walletListOutputs(rest, opts)
34
+ case 'relinquish-output':
35
+ return walletRelinquishOutput(rest, opts)
36
+ case 'list-actions':
37
+ return walletListActions(rest, opts)
38
+ case 'create-action':
39
+ return walletCreateAction(rest, opts)
40
+ case 'sign-action':
41
+ return walletSignAction(rest, opts)
42
+ case 'abort-action':
43
+ return walletAbortAction(rest, opts)
44
+ case 'list-certificates':
45
+ return walletListCertificates(rest, opts)
46
+ case 'relinquish-certificate':
47
+ return walletRelinquishCertificate(rest, opts)
31
48
  default:
32
49
  printCommandHelp('wallet', {
33
50
  balance: 'Show wallet balance in satoshis',
@@ -35,6 +52,14 @@ export async function handleWalletCommand(
35
52
  send: 'Send BSV to an address (--to <addr> --sats <amount>)',
36
53
  'send-all': 'Send all BSV to an address (--to <addr>)',
37
54
  info: 'Show wallet info (address, balance, network)',
55
+ 'list-outputs': 'List wallet outputs (--basket <name> [--tags <t1,t2>] [--limit N])',
56
+ 'relinquish-output': 'Remove output from basket (--basket <name> --output <txid.vout>)',
57
+ 'list-actions': 'List wallet actions [--labels <l1,l2>] [--limit N]',
58
+ 'create-action': 'Create action (JSON args)',
59
+ 'sign-action': 'Sign action (JSON args)',
60
+ 'abort-action': 'Abort action (--reference <ref>)',
61
+ 'list-certificates': 'List certificates',
62
+ 'relinquish-certificate': 'Relinquish certificate (--type <t> --serialNumber <s> --certifier <c>)',
38
63
  })
39
64
  if (subcommand && subcommand !== 'help') {
40
65
  process.exit(1)
@@ -216,3 +241,258 @@ async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
216
241
  await destroy()
217
242
  }
218
243
  }
244
+
245
+ // BRC-100 Interface Commands
246
+
247
+ async function walletListOutputs(args: string[], opts: GlobalFlags): Promise<void> {
248
+ const basket = extractFlag(args, '--basket')
249
+ if (!basket) fatal('Missing required --basket <name>')
250
+
251
+ const tags = extractFlags(args, '--tags')
252
+ const limitStr = extractFlag(args, '--limit')
253
+ const limit = limitStr ? parseInt(limitStr, 10) : 10
254
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
255
+ fatal('--limit must be between 1 and 10000')
256
+ }
257
+
258
+ const privateKey = await loadKey(resolvePassword())
259
+ const { ctx, destroy } = await loadContext(privateKey, {
260
+ chain: opts.chain,
261
+ })
262
+
263
+ try {
264
+ const listArgs: Parameters<typeof ctx.wallet.listOutputs>[0] = {
265
+ basket,
266
+ limit,
267
+ }
268
+ if (tags.length > 0) {
269
+ listArgs.tags = tags
270
+ }
271
+
272
+ const result = await ctx.wallet.listOutputs(listArgs)
273
+
274
+ if (opts.json) {
275
+ output(result, opts)
276
+ } else {
277
+ console.log(`\n${result.totalOutputs} total outputs in basket '${basket}':\n`)
278
+ for (const out of result.outputs) {
279
+ const tags = out.tags?.join(', ') || 'none'
280
+ console.log(` ${out.outpoint} | ${out.satoshis} sats | tags: ${tags}`)
281
+ }
282
+ console.log()
283
+ }
284
+ } finally {
285
+ await destroy()
286
+ }
287
+ }
288
+
289
+ async function walletRelinquishOutput(
290
+ args: string[],
291
+ opts: GlobalFlags,
292
+ ): Promise<void> {
293
+ const basket = extractFlag(args, '--basket')
294
+ const outpoint = extractFlag(args, '--output')
295
+
296
+ if (!basket) fatal('Missing required --basket <name>')
297
+ if (!outpoint) fatal('Missing required --output <txid.vout>')
298
+
299
+ // Validate output format (txid.vout)
300
+ if (!outpoint.includes('.') || outpoint.split('.').length !== 2) {
301
+ fatal('Invalid --output format. Expected: txid.vout (e.g., abc123...0)')
302
+ }
303
+
304
+ const privateKey = await loadKey(resolvePassword())
305
+ const { ctx, destroy } = await loadContext(privateKey, {
306
+ chain: opts.chain,
307
+ })
308
+
309
+ try {
310
+ const result = await ctx.wallet.relinquishOutput({ basket, output: outpoint })
311
+
312
+ if (opts.json) {
313
+ output(result, opts)
314
+ } else {
315
+ output({ relinquished: true, basket, output: outpoint }, opts)
316
+ }
317
+ } finally {
318
+ await destroy()
319
+ }
320
+ }
321
+
322
+ async function walletListActions(args: string[], opts: GlobalFlags): Promise<void> {
323
+ const labels = extractFlags(args, '--labels')
324
+ const limitStr = extractFlag(args, '--limit')
325
+ const limit = limitStr ? parseInt(limitStr, 10) : 10
326
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
327
+ fatal('--limit must be between 1 and 10000')
328
+ }
329
+
330
+ const privateKey = await loadKey(resolvePassword())
331
+ const { ctx, destroy } = await loadContext(privateKey, {
332
+ chain: opts.chain,
333
+ })
334
+
335
+ try {
336
+ const listArgs: Parameters<typeof ctx.wallet.listActions>[0] = {
337
+ labels: labels.length > 0 ? labels : ['*'], // Default to all labels if none specified
338
+ limit,
339
+ }
340
+
341
+ const result = await ctx.wallet.listActions(listArgs)
342
+
343
+ if (opts.json) {
344
+ output(result, opts)
345
+ } else {
346
+ console.log(`\n${result.totalActions} total actions:\n`)
347
+ for (const action of result.actions) {
348
+ console.log(` ${action.txid || 'pending'} | status: ${action.status}`)
349
+ }
350
+ console.log()
351
+ }
352
+ } finally {
353
+ await destroy()
354
+ }
355
+ }
356
+
357
+ async function walletCreateAction(args: string[], opts: GlobalFlags): Promise<void> {
358
+ const jsonInput = args[0]
359
+
360
+ if (!jsonInput) fatal('Missing JSON arguments. Usage: wallet create-action \'{...}\'')
361
+
362
+ let actionArgs: Parameters<typeof ctx.wallet.createAction>[0]
363
+ try {
364
+ actionArgs = JSON.parse(jsonInput)
365
+ } catch {
366
+ fatal(`Invalid JSON: ${jsonInput}`)
367
+ }
368
+
369
+ const privateKey = await loadKey(resolvePassword())
370
+ const { ctx, destroy } = await loadContext(privateKey, {
371
+ chain: opts.chain,
372
+ })
373
+
374
+ try {
375
+ const result = await ctx.wallet.createAction(actionArgs)
376
+ output(result, opts)
377
+ } finally {
378
+ await destroy()
379
+ }
380
+ }
381
+
382
+ async function walletSignAction(args: string[], opts: GlobalFlags): Promise<void> {
383
+ const jsonInput = args[0]
384
+
385
+ if (!jsonInput) fatal('Missing JSON arguments. Usage: wallet sign-action \'{...}\'')
386
+
387
+ let signArgs: Parameters<typeof ctx.wallet.signAction>[0]
388
+ try {
389
+ signArgs = JSON.parse(jsonInput)
390
+ } catch {
391
+ fatal(`Invalid JSON: ${jsonInput}`)
392
+ }
393
+
394
+ const privateKey = await loadKey(resolvePassword())
395
+ const { ctx, destroy } = await loadContext(privateKey, {
396
+ chain: opts.chain,
397
+ })
398
+
399
+ try {
400
+ const result = await ctx.wallet.signAction(signArgs)
401
+ output(result, opts)
402
+ } finally {
403
+ await destroy()
404
+ }
405
+ }
406
+
407
+ async function walletAbortAction(args: string[], opts: GlobalFlags): Promise<void> {
408
+ const reference = extractFlag(args, '--reference')
409
+
410
+ if (!reference) fatal('Missing required --reference <ref>')
411
+
412
+ const privateKey = await loadKey(resolvePassword())
413
+ const { ctx, destroy } = await loadContext(privateKey, {
414
+ chain: opts.chain,
415
+ })
416
+
417
+ try {
418
+ const result = await ctx.wallet.abortAction({ reference })
419
+ output(result, opts)
420
+ } finally {
421
+ await destroy()
422
+ }
423
+ }
424
+
425
+ async function walletListCertificates(
426
+ args: string[],
427
+ opts: GlobalFlags,
428
+ ): Promise<void> {
429
+ const certifiers = extractFlags(args, '--certifiers')
430
+ const types = extractFlags(args, '--types')
431
+ const limitStr = extractFlag(args, '--limit')
432
+ const limit = limitStr ? parseInt(limitStr, 10) : 10
433
+
434
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
435
+ fatal('--limit must be between 1 and 10000')
436
+ }
437
+
438
+ const privateKey = await loadKey(resolvePassword())
439
+ const { ctx, destroy } = await loadContext(privateKey, {
440
+ chain: opts.chain,
441
+ })
442
+
443
+ try {
444
+ const listArgs: Parameters<typeof ctx.wallet.listCertificates>[0] = {
445
+ certifiers: certifiers.length > 0 ? certifiers : [],
446
+ types: types.length > 0 ? types : [],
447
+ limit,
448
+ }
449
+
450
+ const result = await ctx.wallet.listCertificates(listArgs)
451
+
452
+ if (opts.json) {
453
+ output(result, opts)
454
+ } else {
455
+ console.log(`\n${result.totalCertificates} total certificates:\n`)
456
+ for (const cert of result.certificates) {
457
+ console.log(` ${cert.type} | ${cert.serialNumber} | ${cert.certifier}`)
458
+ }
459
+ console.log()
460
+ }
461
+ } finally {
462
+ await destroy()
463
+ }
464
+ }
465
+
466
+ async function walletRelinquishCertificate(
467
+ args: string[],
468
+ opts: GlobalFlags,
469
+ ): Promise<void> {
470
+ const type = extractFlag(args, '--type')
471
+ const serialNumber = extractFlag(args, '--serialNumber')
472
+ const certifier = extractFlag(args, '--certifier')
473
+
474
+ if (!type) fatal('Missing required --type <type>')
475
+ if (!serialNumber) fatal('Missing required --serialNumber <serial>')
476
+ if (!certifier) fatal('Missing required --certifier <certifier>')
477
+
478
+ const privateKey = await loadKey(resolvePassword())
479
+ const { ctx, destroy } = await loadContext(privateKey, {
480
+ chain: opts.chain,
481
+ })
482
+
483
+ try {
484
+ const result = await ctx.wallet.relinquishCertificate({
485
+ type,
486
+ serialNumber,
487
+ certifier,
488
+ })
489
+
490
+ if (opts.json) {
491
+ output(result, opts)
492
+ } else {
493
+ output({ relinquished: true, type, serialNumber, certifier }, opts)
494
+ }
495
+ } finally {
496
+ await destroy()
497
+ }
498
+ }
package/src/help.ts CHANGED
@@ -47,6 +47,22 @@ ${bold('Wallet:')}
47
47
  ${cyan('wallet send-all')} Send all BSV to an address
48
48
  ${cyan('wallet info')} Show wallet info
49
49
 
50
+ ${bold('Wallet (BRC-100 Interface):')}
51
+ ${cyan('wallet list-outputs')} List outputs in basket
52
+ ${dim('--basket <name> [--tags <t1,t2>] [--limit N]')}
53
+ ${cyan('wallet relinquish-output')} Remove output from basket
54
+ ${dim('--basket <name> --output <txid.vout>')}
55
+ ${cyan('wallet list-actions')} List wallet actions
56
+ ${dim('[--labels <l1,l2>] [--limit N]')}
57
+ ${cyan('wallet create-action')} Create raw action (JSON args)
58
+ ${cyan('wallet sign-action')} Sign raw action (JSON args)
59
+ ${cyan('wallet abort-action')} Abort pending action
60
+ ${dim('--reference <ref>')}
61
+ ${cyan('wallet list-certificates')} List certificates
62
+ ${dim('[--certifiers <c1,c2>] [--types <t1,t2>] [--limit N]')}
63
+ ${cyan('wallet relinquish-certificate')} Relinquish certificate
64
+ ${dim('--type <t> --serialNumber <s> --certifier <c>')}
65
+
50
66
  ${bold('Ordinals:')}
51
67
  ${cyan('ordinals list')} List owned ordinals
52
68
  ${cyan('ordinals mint')} Mint a new ordinal inscription