@1sat/cli 0.0.21 → 0.0.23

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,34 +1,41 @@
1
1
  {
2
- "name": "@1sat/cli",
3
- "version": "0.0.21",
4
- "description": "CLI for 1Sat Ordinals SDK",
5
- "type": "module",
6
- "main": "./src/cli.ts",
7
- "bin": {
8
- "1sat": "./src/cli.ts"
9
- },
10
- "files": ["src"],
11
- "scripts": {
12
- "build": "bun build ./src/cli.ts --compile --outfile=bin/1sat --external pg --external pg-native --external pg-query-stream --external tedious --external oracledb --external mysql",
13
- "dev": "bun run src/cli.ts",
14
- "lint": "biome check src"
15
- },
16
- "keywords": ["1sat", "bsv", "ordinals", "cli"],
17
- "license": "MIT",
18
- "dependencies": {
19
- "@1sat/actions": "0.0.82",
20
- "@1sat/client": "0.0.20",
21
- "@1sat/types": "0.0.14",
22
- "@1sat/wallet-node": ">=0.0.13",
23
- "@bsv/sdk": "^2.0.6",
24
- "chalk": "^5.0.0",
25
- "@clack/prompts": "^0.8.0",
26
- "bitcoin-backup": "^0.0.11",
27
- "dotenv": "^17.0.0",
28
- "evlog": "^2.10.0"
29
- },
30
- "devDependencies": {
31
- "@types/bun": "^1.3.9",
32
- "typescript": "^5.9.3"
33
- }
34
- }
2
+ "name": "@1sat/cli",
3
+ "version": "0.0.23",
4
+ "description": "CLI for 1Sat Ordinals SDK",
5
+ "type": "module",
6
+ "main": "./src/cli.ts",
7
+ "bin": {
8
+ "1sat": "./src/cli.ts"
9
+ },
10
+ "files": [
11
+ "src"
12
+ ],
13
+ "scripts": {
14
+ "build": "bun build ./src/cli.ts --compile --outfile=bin/1sat --external pg --external pg-native --external pg-query-stream --external tedious --external oracledb --external mysql",
15
+ "dev": "bun run src/cli.ts",
16
+ "lint": "biome check src"
17
+ },
18
+ "keywords": [
19
+ "1sat",
20
+ "bsv",
21
+ "ordinals",
22
+ "cli"
23
+ ],
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "@1sat/actions": "0.0.83",
27
+ "@1sat/client": "0.0.20",
28
+ "@1sat/types": "0.0.15",
29
+ "@1sat/wallet-node": "0.0.23",
30
+ "@bsv/sdk": "^2.0.6",
31
+ "chalk": "^5.0.0",
32
+ "@clack/prompts": "^0.8.0",
33
+ "bitcoin-backup": "^0.0.11",
34
+ "dotenv": "^17.0.0",
35
+ "evlog": "^2.10.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/bun": "^1.3.9",
39
+ "typescript": "^5.9.3"
40
+ }
41
+ }
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
  */
@@ -105,7 +105,9 @@ function configSet(args: string[], opts: GlobalFlags): void {
105
105
  if (key === 'monitorIntervalMinutes') {
106
106
  const n = Number(value)
107
107
  if (!Number.isFinite(n) || n < 0) {
108
- fatal('monitorIntervalMinutes must be a non-negative number (0 to disable)')
108
+ fatal(
109
+ 'monitorIntervalMinutes must be a non-negative number (0 to disable)',
110
+ )
109
111
  }
110
112
  }
111
113
 
@@ -4,7 +4,7 @@
4
4
  * BAP (Bitcoin Attestation Protocol) identity management.
5
5
  */
6
6
 
7
- import { publishIdentity, signBsm } from '@1sat/actions'
7
+ import { publishIdentity, signBsm, updateProfile } from '@1sat/actions'
8
8
  import type { GlobalFlags } from '../args'
9
9
  import { extractFlag } from '../args'
10
10
  import { loadContext } from '../context'
@@ -21,6 +21,8 @@ export async function handleIdentityCommand(
21
21
  switch (subcommand) {
22
22
  case 'create':
23
23
  return identityCreate(rest, opts)
24
+ case 'update-profile':
25
+ return identityUpdateProfile(rest, opts)
24
26
  case 'info':
25
27
  return identityInfo(rest, opts)
26
28
  case 'sign':
@@ -30,6 +32,7 @@ export async function handleIdentityCommand(
30
32
  default:
31
33
  printCommandHelp('identity', {
32
34
  create: 'Create/publish a BAP identity',
35
+ 'update-profile': 'Update BAP identity profile (--profile <json>)',
33
36
  info: 'Show BAP identity information',
34
37
  sign: 'Sign a message with identity key (--message <text>)',
35
38
  verify:
@@ -128,3 +131,36 @@ async function identityVerify(
128
131
  'identity verify is not yet implemented (needs direct BSM.verify integration)',
129
132
  )
130
133
  }
134
+
135
+ async function identityUpdateProfile(
136
+ args: string[],
137
+ opts: GlobalFlags,
138
+ ): Promise<void> {
139
+ const profileStr = extractFlag(args, '--profile')
140
+
141
+ if (!profileStr) fatal('Missing --profile <json>')
142
+
143
+ let profile: Record<string, unknown>
144
+ try {
145
+ profile = JSON.parse(profileStr)
146
+ } catch {
147
+ fatal(`Invalid JSON in --profile: ${profileStr}`)
148
+ }
149
+
150
+ const privateKey = await loadKey(resolvePassword())
151
+ const { ctx, destroy } = await loadContext(privateKey, {
152
+ chain: opts.chain,
153
+ })
154
+
155
+ try {
156
+ const result = await updateProfile.execute(ctx, { profile })
157
+
158
+ if (result.error) {
159
+ fatal(result.error)
160
+ }
161
+
162
+ output(result, opts)
163
+ } finally {
164
+ await destroy()
165
+ }
166
+ }
@@ -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
 
@@ -61,33 +61,54 @@ async function handshake(key: PrivateKey): Promise<Session> {
61
61
  if (!res.ok) throw new Error(`Handshake failed: HTTP ${res.status}`)
62
62
 
63
63
  const data = await res.json()
64
- if (data.messageType !== 'initialResponse') throw new Error(`Unexpected: ${data.messageType}`)
64
+ if (data.messageType !== 'initialResponse')
65
+ throw new Error(`Unexpected: ${data.messageType}`)
65
66
 
66
67
  const serverNonce = data.nonce as string
67
68
  const deriver = new KeyDeriver(key)
68
69
  const serverPub = deriver.derivePublicKey(
69
- AUTH_PROTOCOL_ID, `${serverNonce} ${clientNonce}`, data.identityKey as string, false,
70
+ AUTH_PROTOCOL_ID,
71
+ `${serverNonce} ${clientNonce}`,
72
+ data.identityKey as string,
73
+ false,
70
74
  )
71
75
 
72
76
  const clientNonceBytes = toArray(clientNonce, 'base64')
73
77
  const serverNonceBytes = toArray(serverNonce, 'base64')
74
- const msgHash = Hash.sha256(Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])))
78
+ const msgHash = Hash.sha256(
79
+ Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])),
80
+ )
75
81
 
76
82
  const sig = Signature.fromDER(data.signature as string, 'hex')
77
- if (!serverPub.verify(Array.from(msgHash), sig)) throw new Error('Server signature verification failed')
83
+ if (!serverPub.verify(Array.from(msgHash), sig))
84
+ throw new Error('Server signature verification failed')
78
85
 
79
86
  const log = createLogger({ context: 'auth' })
80
- log.set({ event: 'handshake_complete', serverIdentityKey: (data.identityKey as string).slice(0, 16) })
87
+ log.set({
88
+ event: 'handshake_complete',
89
+ serverIdentityKey: (data.identityKey as string).slice(0, 16),
90
+ })
81
91
  log.emit()
82
92
 
83
- return { serverIdentityKey: data.identityKey as string, serverNonce, clientKey: key }
93
+ return {
94
+ serverIdentityKey: data.identityKey as string,
95
+ serverNonce,
96
+ clientKey: key,
97
+ }
84
98
  }
85
99
 
86
- function signHeaders(session: Session, pathname: string): Record<string, string> {
100
+ function signHeaders(
101
+ session: Session,
102
+ pathname: string,
103
+ ): Record<string, string> {
87
104
  const { serverIdentityKey, serverNonce, clientKey: key } = session
88
105
  const nonce = generateNonce()
89
106
  const deriver = new KeyDeriver(key)
90
- const derivedKey = deriver.derivePrivateKey(AUTH_PROTOCOL_ID, `${nonce} ${serverNonce}`, serverIdentityKey)
107
+ const derivedKey = deriver.derivePrivateKey(
108
+ AUTH_PROTOCOL_ID,
109
+ `${nonce} ${serverNonce}`,
110
+ serverIdentityKey,
111
+ )
91
112
  const payload = new TextEncoder().encode(pathname)
92
113
  const msgHash = Hash.sha256(Array.from(payload))
93
114
  const sig = derivedKey.sign(Array.from(msgHash))
@@ -109,7 +130,9 @@ export async function handleMcpProxyCommand(): Promise<void> {
109
130
  } catch {
110
131
  startLog.set({ event: 'server_unreachable', url: MCP_URL })
111
132
  startLog.emit()
112
- process.stderr.write(`[1sat mcp-proxy] Server not reachable at ${MCP_URL} — is 1Sat wallet running?\n`)
133
+ process.stderr.write(
134
+ `[1sat mcp-proxy] Server not reachable at ${MCP_URL} — is 1Sat wallet running?\n`,
135
+ )
113
136
  process.exit(1)
114
137
  }
115
138
 
@@ -156,7 +179,11 @@ export async function handleMcpProxyCommand(): Promise<void> {
156
179
  method = JSON.parse(line).method
157
180
  } catch {}
158
181
 
159
- const res = await fetch(`${MCP_URL}/mcp`, { method: 'POST', headers, body: line })
182
+ const res = await fetch(`${MCP_URL}/mcp`, {
183
+ method: 'POST',
184
+ headers,
185
+ body: line,
186
+ })
160
187
 
161
188
  const sessionHeader = res.headers.get('mcp-session-id')
162
189
  if (sessionHeader) mcpSessionId = sessionHeader
@@ -202,13 +229,19 @@ export async function handleMcpProxyCommand(): Promise<void> {
202
229
  }
203
230
  } catch (err) {
204
231
  const msg = err instanceof Error ? err.message : String(err)
205
- reqLog.set({ event: 'request_failed', requestNum: requestCount, error: msg })
232
+ reqLog.set({
233
+ event: 'request_failed',
234
+ requestNum: requestCount,
235
+ error: msg,
236
+ })
206
237
  reqLog.emit()
207
- process.stdout.write(`${JSON.stringify({
208
- jsonrpc: '2.0',
209
- error: { code: -32000, message: `MCP proxy error: ${msg}` },
210
- id: null,
211
- })}\n`)
238
+ process.stdout.write(
239
+ `${JSON.stringify({
240
+ jsonrpc: '2.0',
241
+ error: { code: -32000, message: `MCP proxy error: ${msg}` },
242
+ id: null,
243
+ })}\n`,
244
+ )
212
245
  }
213
246
  }
214
247
  }
@@ -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] })
@@ -131,9 +133,11 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
131
133
  })
132
134
 
133
135
  try {
134
- const backups = walletResult.storage.getBackupStores?.() ?? []
135
136
  const config = loadConfig()
136
137
 
138
+ // Use config.backups for display (remoteClients only populated for active connections)
139
+ const backupUrls = config.backups ?? []
140
+
137
141
  // Use config for active determination — WalletStorageManager internal state
138
142
  // can be misleading (a backup may appear as active after addWalletStorageProvider)
139
143
  const isRemoteActive = Boolean(config.activeRemote)
@@ -142,7 +146,7 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
142
146
  output(
143
147
  {
144
148
  activeStorage: isRemoteActive ? 'remote' : 'local',
145
- backups: walletResult.storage.getAllStores?.() ?? [],
149
+ backups: backupUrls,
146
150
  config: {
147
151
  activeRemote: config.activeRemote ?? null,
148
152
  backups: config.backups ?? [],
@@ -154,27 +158,19 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
154
158
  }
155
159
 
156
160
  console.log()
157
- console.log(` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`)
161
+ console.log(
162
+ ` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`,
163
+ )
158
164
  if (isRemoteActive) {
159
- console.log(
160
- ` ${bold('Active Remote:')} ${config.activeRemote}`,
161
- )
165
+ console.log(` ${bold('Active Remote:')} ${config.activeRemote}`)
162
166
  }
163
167
  console.log()
164
- if (backups.length === 0 && !config.backups?.length) {
168
+ if (backupUrls.length === 0 && !config.backups?.length) {
165
169
  console.log(' No remote storages configured')
166
170
  } else {
167
171
  console.log(` ${bold('Backups:')}`)
168
- const known = new Set(config.backups ?? [])
169
- for (const b of backups) {
170
- const isKnown = known.has(b)
171
- console.log(` ${isKnown ? '●' : '○'} ${b}`)
172
- }
173
- // Show configured but not yet connected
174
- for (const url of config.backups ?? []) {
175
- if (!backups.includes(url)) {
176
- console.log(` ? ${url} (not connected)`)
177
- }
172
+ for (const url of backupUrls) {
173
+ console.log(` ● ${url}`)
178
174
  }
179
175
  }
180
176
  console.log()
@@ -106,9 +106,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
106
106
  }
107
107
  }
108
108
 
109
- console.log(
110
- `\n ${formatLabel('RUN Tokens:')} ${result.run.length}`,
111
- )
109
+ console.log(`\n ${formatLabel('RUN Tokens:')} ${result.run.length}`)
112
110
  if (result.run.length > 0) {
113
111
  const runSats = result.run.reduce((sum, r) => sum + (r.satoshis ?? 0), 0)
114
112
  console.log(
@@ -161,11 +159,15 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
161
159
 
162
160
  const hasFunding = scan.funding.length > 0
163
161
  const hasOrdinals = scan.ordinals.length > 0
164
- const hasTokens = scan.bsv21Tokens.some((t) => t.isActive && t.outputs.length > 0)
162
+ const hasTokens = scan.bsv21Tokens.some(
163
+ (t) => t.isActive && t.outputs.length > 0,
164
+ )
165
165
 
166
166
  if (!hasFunding && !hasOrdinals && !hasTokens) {
167
167
  if (scan.run.length > 0) {
168
- fatal(`No sweepable UTXOs found at ${address} (${scan.run.length} RUN token output(s) excluded)`)
168
+ fatal(
169
+ `No sweepable UTXOs found at ${address} (${scan.run.length} RUN token output(s) excluded)`,
170
+ )
169
171
  }
170
172
  fatal(`No UTXOs found at ${address}`)
171
173
  }
@@ -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,17 @@ 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':
56
+ 'List wallet outputs (--basket <name> [--tags <t1,t2>] [--limit N] [--include-tags] [--include <val>])',
57
+ 'relinquish-output':
58
+ 'Remove output from basket (--basket <name> --output <txid.vout>)',
59
+ 'list-actions': 'List wallet actions [--labels <l1,l2>] [--limit N]',
60
+ 'create-action': 'Create action (JSON args)',
61
+ 'sign-action': 'Sign action (JSON args)',
62
+ 'abort-action': 'Abort action (--reference <ref>)',
63
+ 'list-certificates': 'List certificates',
64
+ 'relinquish-certificate':
65
+ 'Relinquish certificate (--type <t> --serialNumber <s> --certifier <c>)',
38
66
  })
39
67
  if (subcommand && subcommand !== 'help') {
40
68
  process.exit(1)
@@ -216,3 +244,285 @@ async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
216
244
  await destroy()
217
245
  }
218
246
  }
247
+
248
+ // BRC-100 Interface Commands
249
+
250
+ async function walletListOutputs(
251
+ args: string[],
252
+ opts: GlobalFlags,
253
+ ): Promise<void> {
254
+ const basket = extractFlag(args, '--basket')
255
+ if (!basket) fatal('Missing required --basket <name>')
256
+
257
+ const tags = extractFlags(args, '--tags')
258
+ const limitStr = extractFlag(args, '--limit')
259
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
260
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
261
+ fatal('--limit must be between 1 and 10000')
262
+ }
263
+
264
+ const includeTags = args.includes('--include-tags')
265
+ const include = extractFlag(args, '--include')
266
+
267
+ const privateKey = await loadKey(resolvePassword())
268
+ const { ctx, destroy } = await loadContext(privateKey, {
269
+ chain: opts.chain,
270
+ })
271
+
272
+ try {
273
+ const listArgs: Parameters<typeof ctx.wallet.listOutputs>[0] = {
274
+ basket,
275
+ limit,
276
+ includeTags,
277
+ include,
278
+ }
279
+ if (tags.length > 0) {
280
+ listArgs.tags = tags
281
+ }
282
+
283
+ const result = await ctx.wallet.listOutputs(listArgs)
284
+
285
+ if (opts.json) {
286
+ output(result, opts)
287
+ } else {
288
+ console.log(
289
+ `\n${result.totalOutputs} total outputs in basket '${basket}':\n`,
290
+ )
291
+ for (const out of result.outputs) {
292
+ const tags = out.tags?.join(', ') || 'none'
293
+ console.log(` ${out.outpoint} | ${out.satoshis} sats | tags: ${tags}`)
294
+ }
295
+ console.log()
296
+ }
297
+ } finally {
298
+ await destroy()
299
+ }
300
+ }
301
+
302
+ async function walletRelinquishOutput(
303
+ args: string[],
304
+ opts: GlobalFlags,
305
+ ): Promise<void> {
306
+ const basket = extractFlag(args, '--basket')
307
+ const outpoint = extractFlag(args, '--output')
308
+
309
+ if (!basket) fatal('Missing required --basket <name>')
310
+ if (!outpoint) fatal('Missing required --output <txid.vout>')
311
+
312
+ // Validate output format (txid.vout)
313
+ if (!outpoint.includes('.') || outpoint.split('.').length !== 2) {
314
+ fatal('Invalid --output format. Expected: txid.vout (e.g., abc123...0)')
315
+ }
316
+
317
+ const privateKey = await loadKey(resolvePassword())
318
+ const { ctx, destroy } = await loadContext(privateKey, {
319
+ chain: opts.chain,
320
+ })
321
+
322
+ try {
323
+ const result = await ctx.wallet.relinquishOutput({
324
+ basket,
325
+ output: outpoint,
326
+ })
327
+
328
+ if (opts.json) {
329
+ output(result, opts)
330
+ } else {
331
+ output({ relinquished: true, basket, output: outpoint }, opts)
332
+ }
333
+ } finally {
334
+ await destroy()
335
+ }
336
+ }
337
+
338
+ async function walletListActions(
339
+ args: string[],
340
+ opts: GlobalFlags,
341
+ ): Promise<void> {
342
+ const labels = extractFlags(args, '--labels')
343
+ const limitStr = extractFlag(args, '--limit')
344
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
345
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
346
+ fatal('--limit must be between 1 and 10000')
347
+ }
348
+
349
+ const privateKey = await loadKey(resolvePassword())
350
+ const { ctx, destroy } = await loadContext(privateKey, {
351
+ chain: opts.chain,
352
+ })
353
+
354
+ try {
355
+ const listArgs: Parameters<typeof ctx.wallet.listActions>[0] = {
356
+ labels: labels.length > 0 ? labels : ['*'], // Default to all labels if none specified
357
+ limit,
358
+ }
359
+
360
+ const result = await ctx.wallet.listActions(listArgs)
361
+
362
+ if (opts.json) {
363
+ output(result, opts)
364
+ } else {
365
+ console.log(`\n${result.totalActions} total actions:\n`)
366
+ for (const action of result.actions) {
367
+ console.log(` ${action.txid || 'pending'} | status: ${action.status}`)
368
+ }
369
+ console.log()
370
+ }
371
+ } finally {
372
+ await destroy()
373
+ }
374
+ }
375
+
376
+ async function walletCreateAction(
377
+ args: string[],
378
+ opts: GlobalFlags,
379
+ ): Promise<void> {
380
+ const jsonInput = args[0]
381
+
382
+ if (!jsonInput)
383
+ fatal("Missing JSON arguments. Usage: wallet create-action '{...}'")
384
+
385
+ let actionArgs: Parameters<typeof ctx.wallet.createAction>[0]
386
+ try {
387
+ actionArgs = JSON.parse(jsonInput)
388
+ } catch {
389
+ fatal(`Invalid JSON: ${jsonInput}`)
390
+ }
391
+
392
+ const privateKey = await loadKey(resolvePassword())
393
+ const { ctx, destroy } = await loadContext(privateKey, {
394
+ chain: opts.chain,
395
+ })
396
+
397
+ try {
398
+ const result = await ctx.wallet.createAction(actionArgs)
399
+ output(result, opts)
400
+ } finally {
401
+ await destroy()
402
+ }
403
+ }
404
+
405
+ async function walletSignAction(
406
+ args: string[],
407
+ opts: GlobalFlags,
408
+ ): Promise<void> {
409
+ const jsonInput = args[0]
410
+
411
+ if (!jsonInput)
412
+ fatal("Missing JSON arguments. Usage: wallet sign-action '{...}'")
413
+
414
+ let signArgs: Parameters<typeof ctx.wallet.signAction>[0]
415
+ try {
416
+ signArgs = JSON.parse(jsonInput)
417
+ } catch {
418
+ fatal(`Invalid JSON: ${jsonInput}`)
419
+ }
420
+
421
+ const privateKey = await loadKey(resolvePassword())
422
+ const { ctx, destroy } = await loadContext(privateKey, {
423
+ chain: opts.chain,
424
+ })
425
+
426
+ try {
427
+ const result = await ctx.wallet.signAction(signArgs)
428
+ output(result, opts)
429
+ } finally {
430
+ await destroy()
431
+ }
432
+ }
433
+
434
+ async function walletAbortAction(
435
+ args: string[],
436
+ opts: GlobalFlags,
437
+ ): Promise<void> {
438
+ const reference = extractFlag(args, '--reference')
439
+
440
+ if (!reference) fatal('Missing required --reference <ref>')
441
+
442
+ const privateKey = await loadKey(resolvePassword())
443
+ const { ctx, destroy } = await loadContext(privateKey, {
444
+ chain: opts.chain,
445
+ })
446
+
447
+ try {
448
+ const result = await ctx.wallet.abortAction({ reference })
449
+ output(result, opts)
450
+ } finally {
451
+ await destroy()
452
+ }
453
+ }
454
+
455
+ async function walletListCertificates(
456
+ args: string[],
457
+ opts: GlobalFlags,
458
+ ): Promise<void> {
459
+ const certifiers = extractFlags(args, '--certifiers')
460
+ const types = extractFlags(args, '--types')
461
+ const limitStr = extractFlag(args, '--limit')
462
+ const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
463
+
464
+ if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
465
+ fatal('--limit must be between 1 and 10000')
466
+ }
467
+
468
+ const privateKey = await loadKey(resolvePassword())
469
+ const { ctx, destroy } = await loadContext(privateKey, {
470
+ chain: opts.chain,
471
+ })
472
+
473
+ try {
474
+ const listArgs: Parameters<typeof ctx.wallet.listCertificates>[0] = {
475
+ certifiers: certifiers.length > 0 ? certifiers : [],
476
+ types: types.length > 0 ? types : [],
477
+ limit,
478
+ }
479
+
480
+ const result = await ctx.wallet.listCertificates(listArgs)
481
+
482
+ if (opts.json) {
483
+ output(result, opts)
484
+ } else {
485
+ console.log(`\n${result.totalCertificates} total certificates:\n`)
486
+ for (const cert of result.certificates) {
487
+ console.log(` ${cert.type} | ${cert.serialNumber} | ${cert.certifier}`)
488
+ }
489
+ console.log()
490
+ }
491
+ } finally {
492
+ await destroy()
493
+ }
494
+ }
495
+
496
+ async function walletRelinquishCertificate(
497
+ args: string[],
498
+ opts: GlobalFlags,
499
+ ): Promise<void> {
500
+ const type = extractFlag(args, '--type')
501
+ const serialNumber = extractFlag(args, '--serialNumber')
502
+ const certifier = extractFlag(args, '--certifier')
503
+
504
+ if (!type) fatal('Missing required --type <type>')
505
+ if (!serialNumber) fatal('Missing required --serialNumber <serial>')
506
+ if (!certifier) fatal('Missing required --certifier <certifier>')
507
+
508
+ const privateKey = await loadKey(resolvePassword())
509
+ const { ctx, destroy } = await loadContext(privateKey, {
510
+ chain: opts.chain,
511
+ })
512
+
513
+ try {
514
+ const result = await ctx.wallet.relinquishCertificate({
515
+ type,
516
+ serialNumber,
517
+ certifier,
518
+ })
519
+
520
+ if (opts.json) {
521
+ output(result, opts)
522
+ } else {
523
+ output({ relinquished: true, type, serialNumber, certifier }, opts)
524
+ }
525
+ } finally {
526
+ await destroy()
527
+ }
528
+ }
package/src/context.ts CHANGED
@@ -38,7 +38,20 @@ async function runMonitorIfStale(
38
38
  const intervalMs = intervalMinutes * 60 * 1000
39
39
 
40
40
  if (elapsed >= intervalMs) {
41
- await walletResult.monitor.runOnce()
41
+ const originalLog = console.log
42
+ const originalInfo = console.info
43
+ const originalWarn = console.warn
44
+ console.log = () => {}
45
+ console.info = () => {}
46
+ console.warn = () => {}
47
+
48
+ try {
49
+ await walletResult.monitor.runOnce()
50
+ } finally {
51
+ console.log = originalLog
52
+ console.info = originalInfo
53
+ console.warn = originalWarn
54
+ }
42
55
  saveMonitorState({ lastMonitorRun: Date.now() })
43
56
  }
44
57
  }
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
@@ -69,6 +85,7 @@ ${bold('Locks:')}
69
85
 
70
86
  ${bold('Identity (BAP):')}
71
87
  ${cyan('identity create')} Create a new BAP identity
88
+ ${cyan('identity update-profile')} Update BAP identity profile (--profile <json>)
72
89
  ${cyan('identity info')} Show identity information
73
90
  ${cyan('identity sign')} Sign a message with identity key
74
91
  ${cyan('identity verify')} Verify a signed message