@1sat/cli 0.0.44 → 0.0.46

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.44",
3
+ "version": "0.0.46",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -25,13 +25,13 @@
25
25
  ],
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@1sat/actions": "0.0.110",
29
- "@1sat/client": "0.0.24",
28
+ "@1sat/actions": "0.0.112",
29
+ "@1sat/client": "0.0.25",
30
30
  "@1sat/types": "0.0.18",
31
- "@1sat/wallet-node": "0.0.39",
32
- "@1sat/wallet-server": "0.0.9",
31
+ "@1sat/wallet-node": "0.0.40",
32
+ "@1sat/wallet-server": "0.0.10",
33
33
  "@bsv/sdk": "^2.0.13",
34
- "@bsv/wallet-toolbox": "npm:@bopen-io/wallet-toolbox@2.1.21-parity-fix.1",
34
+ "@bsv/wallet-toolbox": "npm:@bopen-io/wallet-toolbox@2.1.21-parity-fix.2",
35
35
  "chalk": "^5.0.0",
36
36
  "@clack/prompts": "^0.8.0",
37
37
  "bitcoin-backup": "^0.0.11",
@@ -8,7 +8,7 @@
8
8
  import { actionRegistry } from '@1sat/actions'
9
9
  import type { GlobalFlags } from '../args'
10
10
  import { loadContext } from '../context'
11
- import { loadKey, resolvePassword } from '../keys'
11
+ import { loadKey } from '../keys'
12
12
  import { fatal, output } from '../output'
13
13
 
14
14
  export async function handleActionCommand(
@@ -65,7 +65,7 @@ export async function handleActionCommand(
65
65
  }
66
66
  }
67
67
 
68
- const privateKey = await loadKey(resolvePassword())
68
+ const privateKey = await loadKey()
69
69
  const { ctx, destroy } = await loadContext(privateKey, {
70
70
  chain: opts.chain,
71
71
  })
@@ -9,7 +9,7 @@ import type { GlobalFlags } from '../args'
9
9
  import { extractFlag } from '../args'
10
10
  import { loadContext } from '../context'
11
11
  import { printCommandHelp } from '../help'
12
- import { loadKey, resolvePassword } from '../keys'
12
+ import { loadKey } from '../keys'
13
13
  import { fatal, output, printKeyValue } from '../output'
14
14
 
15
15
  export async function handleIdentityCommand(
@@ -34,7 +34,7 @@ export async function handleIdentityCommand(
34
34
  create: 'Create/publish a BAP identity',
35
35
  'update-profile': 'Update BAP identity profile (--profile <json>)',
36
36
  info: 'Show BAP identity information',
37
- sign: 'Sign a message with identity key (--message <text>)',
37
+ sign: 'Sign a message with identity key (--message <text> [--encoding <utf8|hex|base64>])',
38
38
  verify:
39
39
  'Verify a signed message (--message <text> --sig <sig> --address <addr>)',
40
40
  })
@@ -48,7 +48,7 @@ async function identityCreate(
48
48
  _args: string[],
49
49
  opts: GlobalFlags,
50
50
  ): Promise<void> {
51
- const privateKey = await loadKey(resolvePassword())
51
+ const privateKey = await loadKey()
52
52
  const { ctx, destroy } = await loadContext(privateKey, {
53
53
  chain: opts.chain,
54
54
  })
@@ -67,7 +67,7 @@ async function identityCreate(
67
67
  }
68
68
 
69
69
  async function identityInfo(_args: string[], opts: GlobalFlags): Promise<void> {
70
- const privateKey = await loadKey(resolvePassword())
70
+ const privateKey = await loadKey()
71
71
  const { ctx, destroy } = await loadContext(privateKey, {
72
72
  chain: opts.chain,
73
73
  })
@@ -92,16 +92,28 @@ async function identityInfo(_args: string[], opts: GlobalFlags): Promise<void> {
92
92
 
93
93
  async function identitySign(args: string[], opts: GlobalFlags): Promise<void> {
94
94
  const message = extractFlag(args, '--message')
95
+ const encoding = extractFlag(args, '--encoding')
95
96
 
96
97
  if (!message) fatal('Missing --message <text>')
98
+ if (
99
+ encoding !== undefined &&
100
+ encoding !== 'utf8' &&
101
+ encoding !== 'hex' &&
102
+ encoding !== 'base64'
103
+ ) {
104
+ fatal('--encoding must be one of: utf8, hex, base64')
105
+ }
97
106
 
98
- const privateKey = await loadKey(resolvePassword())
107
+ const privateKey = await loadKey()
99
108
  const { ctx, destroy } = await loadContext(privateKey, {
100
109
  chain: opts.chain,
101
110
  })
102
111
 
103
112
  try {
104
- const result = await signBsm.execute(ctx, { message })
113
+ const result = await signBsm.execute(ctx, {
114
+ message,
115
+ ...(encoding ? { encoding: encoding as 'utf8' | 'hex' | 'base64' } : {}),
116
+ })
105
117
 
106
118
  if (result.error) {
107
119
  fatal(result.error)
@@ -147,7 +159,7 @@ async function identityUpdateProfile(
147
159
  fatal(`Invalid JSON in --profile: ${profileStr}`)
148
160
  }
149
161
 
150
- const privateKey = await loadKey(resolvePassword())
162
+ const privateKey = await loadKey()
151
163
  const { ctx, destroy } = await loadContext(privateKey, {
152
164
  chain: opts.chain,
153
165
  })
@@ -18,11 +18,10 @@ import {
18
18
  outro,
19
19
  password,
20
20
  select,
21
- text,
22
21
  } from '@clack/prompts'
23
22
  import type { GlobalFlags } from '../args'
24
23
  import { ensureConfigDir, loadConfig, saveConfig } from '../config'
25
- import { cacheKeyPassword, hasKey, saveKey } from '../keys'
24
+ import { hasKey, saveKey } from '../keys'
26
25
  import { fatal, formatSuccess, formatValue, formatWarning } from '../output'
27
26
 
28
27
  export async function handleInitCommand(
@@ -144,82 +143,30 @@ export async function handleInitCommand(
144
143
  fatal('Passwords do not match.')
145
144
  }
146
145
 
147
- // 3.5. Touch ID protection (macOS arm64)
148
- let useTouchID = false
149
- try {
150
- const { isTouchIDAvailable } = await import('bitcoin-backup')
151
- if (isTouchIDAvailable()) {
152
- const enableTouchID = await confirm({
153
- message:
154
- 'Enable Touch ID? (unlock your wallet without typing a password)',
155
- })
156
- if (isCancel(enableTouchID)) {
157
- cancel('Setup cancelled.')
158
- process.exit(0)
159
- }
160
- useTouchID = enableTouchID as boolean
161
- }
162
- } catch {
163
- // bitcoin-backup Touch ID not available — skip silently
164
- }
165
-
166
146
  // 4. Generate random storage identity key
167
147
  const storageId = `1sat-cli-${randomBytes(8).toString('hex')}`
168
148
 
169
- // 5. Optional: remote storage configuration
170
- const useRemote = await confirm({
171
- message: 'Configure remote storage? (remote is active, local is backup)',
172
- defaultValue: false,
173
- })
174
- let activeRemote: string | undefined
175
-
176
- if (useRemote) {
177
- const url = await text({
178
- message: 'Primary remote storage URL:',
179
- validate(value) {
180
- if (!value) return 'Required'
181
- try {
182
- new URL(value)
183
- } catch {
184
- return 'Invalid URL'
185
- }
186
- },
187
- })
188
- if (isCancel(url)) {
189
- cancel('Setup cancelled.')
190
- process.exit(0)
191
- }
192
- activeRemote = url as string
193
- }
194
-
195
- // 6. Save everything
149
+ // 5. Save everything
196
150
  ensureConfigDir()
197
151
 
198
152
  await saveKey(wif, pw as string)
199
153
 
200
- // Cache password with Touch ID if user opted in
201
- if (useTouchID) {
202
- try {
203
- await cacheKeyPassword(pw as string)
204
- } catch {
205
- console.log(
206
- formatWarning(
207
- ' Touch ID caching failed. You can enable it later with "1sat touchid enable".',
208
- ),
209
- )
210
- }
211
- }
212
-
213
154
  saveConfig({
214
155
  ...loadConfig(),
215
156
  chain: chain as 'main' | 'test',
216
157
  storageIdentityKey: storageId,
217
- activeRemote,
218
158
  })
219
159
 
220
160
  const pk = PrivateKey.fromWif(wif)
221
161
  const address = pk.toPublicKey().toAddress()
222
162
 
223
- const touchIdNote = useTouchID ? ' (Touch ID enabled)' : ''
224
- outro(formatSuccess(`Wallet configured${touchIdNote}! Address: ${address}`))
163
+ outro(formatSuccess(`Wallet configured! Address: ${address}`))
164
+
165
+ console.log()
166
+ console.log(
167
+ ` Want hosted storage? Run ${formatValue('1sat remote add <url>')} to attach a backup remote,`,
168
+ )
169
+ console.log(
170
+ ` then ${formatValue('1sat remote set-active <url>')} to make it primary.`,
171
+ )
225
172
  }
@@ -8,7 +8,7 @@ import type { GlobalFlags } from '../args'
8
8
  import { extractFlag } from '../args'
9
9
  import { loadContext } from '../context'
10
10
  import { printCommandHelp } from '../help'
11
- import { loadKey, resolvePassword } from '../keys'
11
+ import { loadKey } from '../keys'
12
12
  import { fatal, output, printKeyValue } from '../output'
13
13
 
14
14
  export async function handleLocksCommand(
@@ -37,7 +37,7 @@ export async function handleLocksCommand(
37
37
  }
38
38
 
39
39
  async function locksInfo(_args: string[], opts: GlobalFlags): Promise<void> {
40
- const privateKey = await loadKey(resolvePassword())
40
+ const privateKey = await loadKey()
41
41
  const { ctx, destroy } = await loadContext(privateKey, {
42
42
  chain: opts.chain,
43
43
  })
@@ -86,7 +86,7 @@ async function locksLock(args: string[], opts: GlobalFlags): Promise<void> {
86
86
  }
87
87
  }
88
88
 
89
- const privateKey = await loadKey(resolvePassword())
89
+ const privateKey = await loadKey()
90
90
  const { ctx, destroy } = await loadContext(privateKey, {
91
91
  chain: opts.chain,
92
92
  })
@@ -107,7 +107,7 @@ async function locksLock(args: string[], opts: GlobalFlags): Promise<void> {
107
107
  }
108
108
 
109
109
  async function locksUnlock(_args: string[], opts: GlobalFlags): Promise<void> {
110
- const privateKey = await loadKey(resolvePassword())
110
+ const privateKey = await loadKey()
111
111
  const { ctx, destroy } = await loadContext(privateKey, {
112
112
  chain: opts.chain,
113
113
  })
@@ -15,7 +15,7 @@ import type { GlobalFlags } from '../args'
15
15
  import { extractFlag } from '../args'
16
16
  import { loadContext } from '../context'
17
17
  import { printCommandHelp } from '../help'
18
- import { loadKey, resolvePassword } from '../keys'
18
+ import { loadKey } from '../keys'
19
19
  import { fatal, formatLabel, formatValue, output } from '../output'
20
20
 
21
21
  export async function handleOpnsCommand(
@@ -57,7 +57,7 @@ async function opnsRegister(args: string[], opts: GlobalFlags): Promise<void> {
57
57
  }
58
58
  }
59
59
 
60
- const privateKey = await loadKey(resolvePassword())
60
+ const privateKey = await loadKey()
61
61
  const { ctx, destroy } = await loadContext(privateKey, {
62
62
  chain: opts.chain,
63
63
  })
@@ -102,7 +102,7 @@ async function opnsDeregister(
102
102
  }
103
103
  }
104
104
 
105
- const privateKey = await loadKey(resolvePassword())
105
+ const privateKey = await loadKey()
106
106
  const { ctx, destroy } = await loadContext(privateKey, {
107
107
  chain: opts.chain,
108
108
  })
@@ -131,7 +131,7 @@ async function opnsDeregister(
131
131
  }
132
132
 
133
133
  async function opnsLookup(_args: string[], opts: GlobalFlags): Promise<void> {
134
- const privateKey = await loadKey(resolvePassword())
134
+ const privateKey = await loadKey()
135
135
  const { ctx, destroy } = await loadContext(privateKey, {
136
136
  chain: opts.chain,
137
137
  })
@@ -17,10 +17,10 @@ import {
17
17
  import { Utils } from '@bsv/sdk'
18
18
  import { confirm, isCancel } from '@clack/prompts'
19
19
  import type { GlobalFlags } from '../args'
20
- import { extractFlag } from '../args'
20
+ import { extractFlag, hasFlag } from '../args'
21
21
  import { loadContext } from '../context'
22
22
  import { printCommandHelp } from '../help'
23
- import { loadKey, resolvePassword } from '../keys'
23
+ import { loadKey } from '../keys'
24
24
  import { fatal, formatLabel, formatValue, output } from '../output'
25
25
 
26
26
  export async function handleOrdinalsCommand(
@@ -45,7 +45,7 @@ export async function handleOrdinalsCommand(
45
45
  default:
46
46
  printCommandHelp('ordinals', {
47
47
  list: 'List owned ordinals/inscriptions',
48
- mint: 'Mint a new ordinal inscription (--file <path> --type <mime>)',
48
+ mint: 'Mint a new ordinal inscription (--file <path> [--type <mime>] [--map <json>] [--sign-with-bap])',
49
49
  transfer: 'Transfer an ordinal (--outpoint <op> --to <addr>)',
50
50
  sell: 'List an ordinal for sale (--outpoint <op> --price <sats>)',
51
51
  cancel: 'Cancel an ordinal listing (--outpoint <op>)',
@@ -58,7 +58,7 @@ export async function handleOrdinalsCommand(
58
58
  }
59
59
 
60
60
  async function ordinalsList(_args: string[], opts: GlobalFlags): Promise<void> {
61
- const privateKey = await loadKey(resolvePassword())
61
+ const privateKey = await loadKey()
62
62
  const { ctx, destroy } = await loadContext(privateKey, {
63
63
  chain: opts.chain,
64
64
  })
@@ -121,6 +121,8 @@ const MIME_TYPES: Record<string, string> = {
121
121
  async function ordinalsMint(args: string[], opts: GlobalFlags): Promise<void> {
122
122
  const file = extractFlag(args, '--file')
123
123
  const type = extractFlag(args, '--type')
124
+ const mapStr = extractFlag(args, '--map')
125
+ const signWithBAP = hasFlag(args, '--sign-with-bap')
124
126
 
125
127
  if (!file) fatal('Missing --file <path>')
126
128
 
@@ -131,6 +133,29 @@ async function ordinalsMint(args: string[], opts: GlobalFlags): Promise<void> {
131
133
  )
132
134
  }
133
135
 
136
+ let map: Record<string, string> | undefined
137
+ if (mapStr !== undefined) {
138
+ let parsed: unknown
139
+ try {
140
+ parsed = JSON.parse(mapStr)
141
+ } catch (err) {
142
+ fatal(`--map must be valid JSON: ${(err as Error).message}`)
143
+ }
144
+ if (
145
+ typeof parsed !== 'object' ||
146
+ parsed === null ||
147
+ Array.isArray(parsed)
148
+ ) {
149
+ fatal('--map must be a JSON object')
150
+ }
151
+ for (const [k, v] of Object.entries(parsed as Record<string, unknown>)) {
152
+ if (typeof v !== 'string') {
153
+ fatal(`--map values must be strings (key "${k}" is ${typeof v})`)
154
+ }
155
+ }
156
+ map = parsed as Record<string, string>
157
+ }
158
+
134
159
  let fileBytes: Uint8Array
135
160
  try {
136
161
  fileBytes = readFileSync(file)
@@ -149,7 +174,7 @@ async function ordinalsMint(args: string[], opts: GlobalFlags): Promise<void> {
149
174
  }
150
175
  }
151
176
 
152
- const privateKey = await loadKey(resolvePassword())
177
+ const privateKey = await loadKey()
153
178
  const { ctx, destroy } = await loadContext(privateKey, {
154
179
  chain: opts.chain,
155
180
  })
@@ -158,6 +183,8 @@ async function ordinalsMint(args: string[], opts: GlobalFlags): Promise<void> {
158
183
  const result = await inscribe.execute(ctx, {
159
184
  base64Content,
160
185
  contentType,
186
+ ...(map ? { map } : {}),
187
+ ...(signWithBAP ? { signWithBAP: true } : {}),
161
188
  })
162
189
 
163
190
  if (result.error) {
@@ -189,7 +216,7 @@ async function ordinalsTransfer(
189
216
  }
190
217
  }
191
218
 
192
- const privateKey = await loadKey(resolvePassword())
219
+ const privateKey = await loadKey()
193
220
  const { ctx, destroy } = await loadContext(privateKey, {
194
221
  chain: opts.chain,
195
222
  })
@@ -238,7 +265,7 @@ async function ordinalsSell(args: string[], opts: GlobalFlags): Promise<void> {
238
265
  }
239
266
  }
240
267
 
241
- const privateKey = await loadKey(resolvePassword())
268
+ const privateKey = await loadKey()
242
269
  const { ctx, destroy } = await loadContext(privateKey, {
243
270
  chain: opts.chain,
244
271
  })
@@ -295,7 +322,7 @@ async function ordinalsCancel(
295
322
  }
296
323
  }
297
324
 
298
- const privateKey = await loadKey(resolvePassword())
325
+ const privateKey = await loadKey()
299
326
  const { ctx, destroy } = await loadContext(privateKey, {
300
327
  chain: opts.chain,
301
328
  })
@@ -337,7 +364,7 @@ async function ordinalsBuy(args: string[], opts: GlobalFlags): Promise<void> {
337
364
  }
338
365
  }
339
366
 
340
- const privateKey = await loadKey(resolvePassword())
367
+ const privateKey = await loadKey()
341
368
  const { ctx, destroy } = await loadContext(privateKey, {
342
369
  chain: opts.chain,
343
370
  })
@@ -15,7 +15,7 @@ import type { GlobalFlags } from '../args'
15
15
  import { loadConfig, saveConfig } from '../config'
16
16
  import { loadContext } from '../context'
17
17
  import { printCommandHelp } from '../help'
18
- import { loadKey, resolvePassword } from '../keys'
18
+ import { loadKey } from '../keys'
19
19
  import { fatal, formatSuccess, formatWarning, output } from '../output'
20
20
 
21
21
  export async function handleRemoteCommand(
@@ -47,8 +47,7 @@ export async function handleRemoteCommand(
47
47
  'Switch active storage (1sat remote set-active <url | local>)',
48
48
  status:
49
49
  'Fetch GET /account/status from a remote (1sat remote status [url])',
50
- topup:
51
- 'Buy capacity on a remote (1sat remote topup [url] [--units N])',
50
+ topup: 'Buy capacity on a remote (1sat remote topup [url] [--units N])',
52
51
  })
53
52
  if (subcommand && subcommand !== 'help') {
54
53
  process.exit(1)
@@ -86,7 +85,7 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
86
85
  }
87
86
  }
88
87
 
89
- const privateKey = await loadKey(resolvePassword())
88
+ const privateKey = await loadKey()
90
89
  const { walletResult, destroy } = await loadContext(privateKey, {
91
90
  chain: opts.chain,
92
91
  })
@@ -133,7 +132,7 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
133
132
  // ============================================================================
134
133
 
135
134
  async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
136
- const privateKey = await loadKey(resolvePassword())
135
+ const privateKey = await loadKey()
137
136
  const { destroy } = await loadContext(privateKey, {
138
137
  chain: opts.chain,
139
138
  })
@@ -259,7 +258,7 @@ async function remoteSetActive(
259
258
  fatal('No remote storages configured')
260
259
  }
261
260
 
262
- const privateKey = await loadKey(resolvePassword())
261
+ const privateKey = await loadKey()
263
262
  const { walletResult, destroy } = await loadContext(privateKey, {
264
263
  chain: opts.chain,
265
264
  })
@@ -299,7 +298,7 @@ async function remoteSetActive(
299
298
  fatal(`Invalid URL: ${target}`)
300
299
  }
301
300
 
302
- const privateKey = await loadKey(resolvePassword())
301
+ const privateKey = await loadKey()
303
302
  const { walletResult, destroy } = await loadContext(privateKey, {
304
303
  chain: opts.chain,
305
304
  })
@@ -345,7 +344,7 @@ async function remoteStatus(args: string[], opts: GlobalFlags): Promise<void> {
345
344
  fatal(`Invalid URL: ${url}`)
346
345
  }
347
346
 
348
- const privateKey = await loadKey(resolvePassword())
347
+ const privateKey = await loadKey()
349
348
  const { walletResult, destroy } = await loadContext(privateKey, {
350
349
  chain: opts.chain,
351
350
  })
@@ -430,7 +429,7 @@ async function remoteTopup(args: string[], opts: GlobalFlags): Promise<void> {
430
429
  fatal(`Invalid URL: ${url}`)
431
430
  }
432
431
 
433
- const privateKey = await loadKey(resolvePassword())
432
+ const privateKey = await loadKey()
434
433
  const { walletResult, destroy } = await loadContext(privateKey, {
435
434
  chain: opts.chain,
436
435
  })
@@ -452,7 +451,8 @@ async function remoteTopup(args: string[], opts: GlobalFlags): Promise<void> {
452
451
  ` ${bold('New capacity:')} ${formatBytes(result.status.capacityBytes)} (${formatBytes(result.status.usedBytes)} used, ${formatBytes(result.status.deficitBytes)} deficit)`,
453
452
  )
454
453
  if (result.status.paidThroughBlock != null) {
455
- const remaining = result.status.paidThroughBlock - result.status.currentBlock
454
+ const remaining =
455
+ result.status.paidThroughBlock - result.status.currentBlock
456
456
  console.log(
457
457
  ` ${bold('Paid through:')} block ${result.status.paidThroughBlock} (${remaining} blocks left)`,
458
458
  )
@@ -31,7 +31,7 @@ import {
31
31
  } from '../config'
32
32
  import { ensureDataDir } from '../config'
33
33
  import { printCommandHelp } from '../help'
34
- import { loadKey, resolvePassword } from '../keys'
34
+ import { loadKey } from '../keys'
35
35
  import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
36
36
  import { fatal } from '../output'
37
37
 
@@ -138,7 +138,7 @@ async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
138
138
 
139
139
  let privateKey: PrivateKey
140
140
  try {
141
- privateKey = await loadKey(resolvePassword())
141
+ privateKey = await loadKey()
142
142
  } catch (err) {
143
143
  fatal((err as Error).message)
144
144
  }
@@ -6,10 +6,10 @@
6
6
 
7
7
  import { createSocialPost } from '@1sat/actions'
8
8
  import type { GlobalFlags } from '../args'
9
- import { extractFlag } from '../args'
9
+ import { extractFlag, extractFlags } from '../args'
10
10
  import { loadContext } from '../context'
11
11
  import { printCommandHelp } from '../help'
12
- import { loadKey, resolvePassword } from '../keys'
12
+ import { loadKey } from '../keys'
13
13
  import { fatal, output } from '../output'
14
14
 
15
15
  export async function handleSocialCommand(
@@ -23,7 +23,7 @@ export async function handleSocialCommand(
23
23
  return socialPost(rest, opts)
24
24
  default:
25
25
  printCommandHelp('social', {
26
- post: 'Create an on-chain social post (--content <text> --app <name>)',
26
+ post: 'Create an on-chain social post (--content <text> [--app <name>] [--content-type <text/plain|text/markdown>] [--tags <t1,t2>])',
27
27
  })
28
28
  if (subcommand && subcommand !== 'help') {
29
29
  process.exit(1)
@@ -34,16 +34,32 @@ export async function handleSocialCommand(
34
34
  async function socialPost(args: string[], opts: GlobalFlags): Promise<void> {
35
35
  const content = extractFlag(args, '--content')
36
36
  const app = extractFlag(args, '--app') ?? '1sat-cli'
37
+ const contentType = extractFlag(args, '--content-type')
38
+ const tags = extractFlags(args, '--tags')
37
39
 
38
40
  if (!content) fatal('Missing --content <text>')
41
+ if (
42
+ contentType !== undefined &&
43
+ contentType !== 'text/plain' &&
44
+ contentType !== 'text/markdown'
45
+ ) {
46
+ fatal('--content-type must be one of: text/plain, text/markdown')
47
+ }
39
48
 
40
- const privateKey = await loadKey(resolvePassword())
49
+ const privateKey = await loadKey()
41
50
  const { ctx, destroy } = await loadContext(privateKey, {
42
51
  chain: opts.chain,
43
52
  })
44
53
 
45
54
  try {
46
- const result = await createSocialPost.execute(ctx, { app, content })
55
+ const result = await createSocialPost.execute(ctx, {
56
+ app,
57
+ content,
58
+ ...(contentType
59
+ ? { contentType: contentType as 'text/plain' | 'text/markdown' }
60
+ : {}),
61
+ ...(tags.length ? { tags } : {}),
62
+ })
47
63
 
48
64
  if (result.error) {
49
65
  fatal(result.error)
@@ -17,7 +17,7 @@ import type { GlobalFlags } from '../args'
17
17
  import { extractFlag } from '../args'
18
18
  import { loadContext } from '../context'
19
19
  import { printCommandHelp } from '../help'
20
- import { loadKey, resolvePassword } from '../keys'
20
+ import { loadKey } from '../keys'
21
21
  import { fatal, formatLabel, formatValue, output } from '../output'
22
22
 
23
23
  export async function handleSweepCommand(
@@ -55,7 +55,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
55
55
  fatal('Invalid WIF private key')
56
56
  }
57
57
 
58
- const privateKey = await loadKey(resolvePassword())
58
+ const privateKey = await loadKey()
59
59
  const { ctx, destroy } = await loadContext(privateKey, {
60
60
  chain: opts.chain,
61
61
  })
@@ -145,7 +145,7 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
145
145
  fatal('Invalid WIF private key')
146
146
  }
147
147
 
148
- const privateKey = await loadKey(resolvePassword())
148
+ const privateKey = await loadKey()
149
149
  const { ctx, destroy } = await loadContext(privateKey, {
150
150
  chain: opts.chain,
151
151
  })
@@ -14,7 +14,7 @@ import type { GlobalFlags } from '../args'
14
14
  import { extractFlag } from '../args'
15
15
  import { loadContext } from '../context'
16
16
  import { printCommandHelp } from '../help'
17
- import { loadKey, resolvePassword } from '../keys'
17
+ import { loadKey } from '../keys'
18
18
  import { fatal, formatLabel, formatValue, output } from '../output'
19
19
 
20
20
  export async function handleTokensCommand(
@@ -52,7 +52,7 @@ async function tokenBalances(
52
52
  _args: string[],
53
53
  opts: GlobalFlags,
54
54
  ): Promise<void> {
55
- const privateKey = await loadKey(resolvePassword())
55
+ const privateKey = await loadKey()
56
56
  const { ctx, destroy } = await loadContext(privateKey, {
57
57
  chain: opts.chain,
58
58
  })
@@ -84,7 +84,7 @@ async function tokenBalances(
84
84
  async function tokenList(args: string[], opts: GlobalFlags): Promise<void> {
85
85
  const tokenId = extractFlag(args, '--token-id')
86
86
 
87
- const privateKey = await loadKey(resolvePassword())
87
+ const privateKey = await loadKey()
88
88
  const { ctx, destroy } = await loadContext(privateKey, {
89
89
  chain: opts.chain,
90
90
  })
@@ -154,7 +154,7 @@ async function tokenSend(args: string[], opts: GlobalFlags): Promise<void> {
154
154
  }
155
155
  }
156
156
 
157
- const privateKey = await loadKey(resolvePassword())
157
+ const privateKey = await loadKey()
158
158
  const { ctx, destroy } = await loadContext(privateKey, {
159
159
  chain: opts.chain,
160
160
  })
@@ -199,7 +199,7 @@ async function tokenBuy(args: string[], opts: GlobalFlags): Promise<void> {
199
199
  }
200
200
  }
201
201
 
202
- const privateKey = await loadKey(resolvePassword())
202
+ const privateKey = await loadKey()
203
203
  const { ctx, destroy } = await loadContext(privateKey, {
204
204
  chain: opts.chain,
205
205
  })
@@ -14,7 +14,7 @@ import type { GlobalFlags } from '../args'
14
14
  import { extractFlag, extractFlags } from '../args'
15
15
  import { loadContext } from '../context'
16
16
  import { printCommandHelp } from '../help'
17
- import { loadKey, resolvePassword } from '../keys'
17
+ import { loadKey } from '../keys'
18
18
  import { fatal, formatValue, output, printKeyValue } from '../output'
19
19
 
20
20
  export async function handleWalletCommand(
@@ -55,8 +55,9 @@ export async function handleWalletCommand(
55
55
  default:
56
56
  printCommandHelp('wallet', {
57
57
  balance: 'Show wallet balance in satoshis',
58
- address: 'Show deposit address',
59
- send: 'Send BSV to an address (--to <addr> --sats <amount>)',
58
+ address:
59
+ 'Show deposit address [--prefix <p>] [--start-index <n>] [--count <n>]',
60
+ send: 'Send BSV (--to <addr> --sats <n> | --script <hex> --sats <n> | --data-asm "<asm>")',
60
61
  'send-all': 'Send all BSV to an address (--to <addr>)',
61
62
  sync: 'Sync inbound payments at BRC-29 deposit addresses [--prefix <p>] [--start-index <n>] [--count <n>]',
62
63
  info: 'Show wallet info (address, balance, network)',
@@ -82,7 +83,7 @@ async function walletBalance(
82
83
  _args: string[],
83
84
  opts: GlobalFlags,
84
85
  ): Promise<void> {
85
- const privateKey = await loadKey(resolvePassword())
86
+ const privateKey = await loadKey()
86
87
  const { ctx, destroy } = await loadContext(privateKey, {
87
88
  chain: opts.chain,
88
89
  })
@@ -107,27 +108,44 @@ async function walletBalance(
107
108
  }
108
109
  }
109
110
 
110
- async function walletAddress(
111
- _args: string[],
112
- opts: GlobalFlags,
113
- ): Promise<void> {
114
- const privateKey = await loadKey(resolvePassword())
111
+ async function walletAddress(args: string[], opts: GlobalFlags): Promise<void> {
112
+ const prefix = extractFlag(args, '--prefix') ?? '1sat'
113
+ const startIndexStr = extractFlag(args, '--start-index')
114
+ const countStr = extractFlag(args, '--count')
115
+
116
+ const startIndex = startIndexStr === undefined ? 0 : Number(startIndexStr)
117
+ const count = countStr === undefined ? 1 : Number(countStr)
118
+
119
+ if (!Number.isInteger(startIndex) || startIndex < 0) {
120
+ fatal('--start-index must be a non-negative integer')
121
+ }
122
+ if (!Number.isInteger(count) || count < 1) {
123
+ fatal('--count must be a positive integer')
124
+ }
125
+
126
+ const privateKey = await loadKey()
115
127
  const { ctx, destroy } = await loadContext(privateKey, {
116
128
  chain: opts.chain,
117
129
  })
118
130
 
119
131
  try {
120
132
  const result = await deriveDepositAddresses.execute(ctx, {
121
- prefix: '1sat',
122
- count: 1,
133
+ prefix,
134
+ startIndex,
135
+ count,
123
136
  })
124
137
 
125
- const primary = result.derivations[0]
126
- if (!primary) {
138
+ if (!result.derivations.length) {
127
139
  fatal('Failed to derive deposit address')
128
140
  }
129
141
 
130
- output(opts.json ? primary : primary.address, opts)
142
+ if (opts.json) {
143
+ output(count === 1 ? result.derivations[0] : result.derivations, opts)
144
+ } else if (!opts.quiet) {
145
+ for (const d of result.derivations) {
146
+ console.log(d.address)
147
+ }
148
+ }
131
149
  } finally {
132
150
  await destroy()
133
151
  }
@@ -135,34 +153,68 @@ async function walletAddress(
135
153
 
136
154
  async function walletSend(args: string[], opts: GlobalFlags): Promise<void> {
137
155
  const to = extractFlag(args, '--to')
156
+ const script = extractFlag(args, '--script')
157
+ const dataAsm = extractFlag(args, '--data-asm')
138
158
  const satsStr = extractFlag(args, '--sats')
139
159
 
140
- if (!to) fatal('Missing --to <address>')
141
- if (!satsStr) fatal('Missing --sats <amount>')
160
+ const modes = [to, script, dataAsm].filter((v) => v !== undefined).length
161
+ if (modes === 0) {
162
+ fatal(
163
+ 'Specify one of --to <address>, --script <hex>, or --data-asm "<asm>"',
164
+ )
165
+ }
166
+ if (modes > 1) {
167
+ fatal('--to, --script, and --data-asm are mutually exclusive')
168
+ }
142
169
 
143
- const satoshis = Number(satsStr)
144
- if (!Number.isFinite(satoshis) || satoshis <= 0) {
145
- fatal('--sats must be a positive number')
170
+ let request: {
171
+ address?: string
172
+ script?: string
173
+ data?: string[]
174
+ satoshis: number
175
+ }
176
+ let confirmMessage: string
177
+
178
+ if (dataAsm !== undefined) {
179
+ if (satsStr !== undefined) {
180
+ fatal(
181
+ '--sats is not allowed with --data-asm (OP_RETURN outputs are 0 sats)',
182
+ )
183
+ }
184
+ request = { data: [dataAsm], satoshis: 0 }
185
+ confirmMessage = `Publish OP_RETURN data "${dataAsm}"?`
186
+ } else {
187
+ if (!satsStr) fatal('Missing --sats <amount>')
188
+ const satoshis = Number(satsStr)
189
+ if (!Number.isFinite(satoshis) || satoshis <= 0) {
190
+ fatal('--sats must be a positive number')
191
+ }
192
+ if (script !== undefined) {
193
+ if (!/^[0-9a-fA-F]*$/.test(script) || script.length % 2 !== 0) {
194
+ fatal('--script must be a hex string')
195
+ }
196
+ request = { script, satoshis }
197
+ confirmMessage = `Send ${satoshis} satoshis to custom script?`
198
+ } else {
199
+ request = { address: to, satoshis }
200
+ confirmMessage = `Send ${satoshis} satoshis to ${to}?`
201
+ }
146
202
  }
147
203
 
148
204
  if (!opts.yes) {
149
- const ok = await confirm({
150
- message: `Send ${satoshis} satoshis to ${to}?`,
151
- })
205
+ const ok = await confirm({ message: confirmMessage })
152
206
  if (isCancel(ok) || !ok) {
153
207
  fatal('Send cancelled.')
154
208
  }
155
209
  }
156
210
 
157
- const privateKey = await loadKey(resolvePassword())
211
+ const privateKey = await loadKey()
158
212
  const { ctx, destroy } = await loadContext(privateKey, {
159
213
  chain: opts.chain,
160
214
  })
161
215
 
162
216
  try {
163
- const result = await sendBsv.execute(ctx, {
164
- requests: [{ address: to, satoshis }],
165
- })
217
+ const result = await sendBsv.execute(ctx, { requests: [request] })
166
218
 
167
219
  if (result.error) {
168
220
  fatal(result.error)
@@ -188,7 +240,7 @@ async function walletSendAll(args: string[], opts: GlobalFlags): Promise<void> {
188
240
  }
189
241
  }
190
242
 
191
- const privateKey = await loadKey(resolvePassword())
243
+ const privateKey = await loadKey()
192
244
  const { ctx, destroy } = await loadContext(privateKey, {
193
245
  chain: opts.chain,
194
246
  })
@@ -213,16 +265,20 @@ async function walletSync(args: string[], opts: GlobalFlags): Promise<void> {
213
265
 
214
266
  const startIndex =
215
267
  startIndexRaw !== undefined ? Number.parseInt(startIndexRaw, 10) : undefined
216
- const count = countRaw !== undefined ? Number.parseInt(countRaw, 10) : undefined
268
+ const count =
269
+ countRaw !== undefined ? Number.parseInt(countRaw, 10) : undefined
217
270
 
218
- if (startIndex !== undefined && (!Number.isFinite(startIndex) || startIndex < 0)) {
271
+ if (
272
+ startIndex !== undefined &&
273
+ (!Number.isFinite(startIndex) || startIndex < 0)
274
+ ) {
219
275
  fatal('--start-index must be a non-negative integer')
220
276
  }
221
277
  if (count !== undefined && (!Number.isFinite(count) || count < 1)) {
222
278
  fatal('--count must be a positive integer')
223
279
  }
224
280
 
225
- const privateKey = await loadKey(resolvePassword())
281
+ const privateKey = await loadKey()
226
282
  const { ctx, destroy } = await loadContext(privateKey, {
227
283
  chain: opts.chain,
228
284
  })
@@ -253,7 +309,7 @@ async function walletSync(args: string[], opts: GlobalFlags): Promise<void> {
253
309
  }
254
310
 
255
311
  async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
256
- const privateKey = await loadKey(resolvePassword())
312
+ const privateKey = await loadKey()
257
313
  const { ctx, destroy } = await loadContext(privateKey, {
258
314
  chain: opts.chain,
259
315
  })
@@ -318,7 +374,7 @@ async function walletListOutputs(
318
374
  const includeTags = args.includes('--include-tags')
319
375
  const include = extractFlag(args, '--include')
320
376
 
321
- const privateKey = await loadKey(resolvePassword())
377
+ const privateKey = await loadKey()
322
378
  const { ctx, destroy } = await loadContext(privateKey, {
323
379
  chain: opts.chain,
324
380
  })
@@ -368,7 +424,7 @@ async function walletRelinquishOutput(
368
424
  fatal('Invalid --output format. Expected: txid.vout (e.g., abc123...0)')
369
425
  }
370
426
 
371
- const privateKey = await loadKey(resolvePassword())
427
+ const privateKey = await loadKey()
372
428
  const { ctx, destroy } = await loadContext(privateKey, {
373
429
  chain: opts.chain,
374
430
  })
@@ -400,7 +456,7 @@ async function walletListActions(
400
456
  fatal('--limit must be between 1 and 10000')
401
457
  }
402
458
 
403
- const privateKey = await loadKey(resolvePassword())
459
+ const privateKey = await loadKey()
404
460
  const { ctx, destroy } = await loadContext(privateKey, {
405
461
  chain: opts.chain,
406
462
  })
@@ -443,7 +499,7 @@ async function walletCreateAction(
443
499
  fatal(`Invalid JSON: ${jsonInput}`)
444
500
  }
445
501
 
446
- const privateKey = await loadKey(resolvePassword())
502
+ const privateKey = await loadKey()
447
503
  const { ctx, destroy } = await loadContext(privateKey, {
448
504
  chain: opts.chain,
449
505
  })
@@ -472,7 +528,7 @@ async function walletSignAction(
472
528
  fatal(`Invalid JSON: ${jsonInput}`)
473
529
  }
474
530
 
475
- const privateKey = await loadKey(resolvePassword())
531
+ const privateKey = await loadKey()
476
532
  const { ctx, destroy } = await loadContext(privateKey, {
477
533
  chain: opts.chain,
478
534
  })
@@ -493,7 +549,7 @@ async function walletAbortAction(
493
549
 
494
550
  if (!reference) fatal('Missing required --reference <ref>')
495
551
 
496
- const privateKey = await loadKey(resolvePassword())
552
+ const privateKey = await loadKey()
497
553
  const { ctx, destroy } = await loadContext(privateKey, {
498
554
  chain: opts.chain,
499
555
  })
@@ -519,7 +575,7 @@ async function walletListCertificates(
519
575
  fatal('--limit must be between 1 and 10000')
520
576
  }
521
577
 
522
- const privateKey = await loadKey(resolvePassword())
578
+ const privateKey = await loadKey()
523
579
  const { ctx, destroy } = await loadContext(privateKey, {
524
580
  chain: opts.chain,
525
581
  })
@@ -559,7 +615,7 @@ async function walletRelinquishCertificate(
559
615
  if (!serialNumber) fatal('Missing required --serialNumber <serial>')
560
616
  if (!certifier) fatal('Missing required --certifier <certifier>')
561
617
 
562
- const privateKey = await loadKey(resolvePassword())
618
+ const privateKey = await loadKey()
563
619
  const { ctx, destroy } = await loadContext(privateKey, {
564
620
  chain: opts.chain,
565
621
  })
package/src/keys.ts CHANGED
@@ -1,17 +1,21 @@
1
1
  /**
2
2
  * Encrypted key management for the 1sat CLI.
3
3
  *
4
- * Key resolution priority:
4
+ * Key resolution priority inside loadKey():
5
5
  * 1. PRIVATE_KEY_WIF env var (headless/CI)
6
- * 2. Touch ID cached password → decrypt keys.bep (macOS arm64)
7
- * 3. Explicit password → decrypt keys.bep
6
+ * 2. ONESAT_PASSWORD env var → decrypt keys.bep
7
+ * 3. Interactive TTY prompt → decrypt keys.bep
8
8
  * 4. Fail with guidance
9
+ *
10
+ * A biometric vault tier will be added once @1sat/wallet-mac is
11
+ * CLI-ready (resolves its own enclave binary relative to its module
12
+ * directory instead of wallet-desktop's layout).
9
13
  */
10
14
 
11
15
  import { existsSync, readFileSync, writeFileSync } from 'node:fs'
12
- import { arch, platform } from 'node:os'
13
16
  import { join } from 'node:path'
14
17
  import { PrivateKey } from '@bsv/sdk'
18
+ import { isCancel, password as promptPassword } from '@clack/prompts'
15
19
  import { type WifBackup, decryptBackup, encryptBackup } from 'bitcoin-backup'
16
20
  import { ensureConfigDir, getConfigDir } from './config'
17
21
 
@@ -30,28 +34,14 @@ export function hasKey(): boolean {
30
34
  }
31
35
 
32
36
  /**
33
- * Check if Touch ID is available for password caching.
37
+ * Load the private key from env, env password, or TTY prompt.
34
38
  */
35
- export function isTouchIDAvailable(): boolean {
36
- return platform() === 'darwin' && arch() === 'arm64'
37
- }
38
-
39
- /**
40
- * Load the private key from env, Touch ID cache, or password.
41
- *
42
- * Resolution order:
43
- * 1. PRIVATE_KEY_WIF env var
44
- * 2. Touch ID cached password (if available)
45
- * 3. Explicit password parameter
46
- */
47
- export async function loadKey(password?: string): Promise<PrivateKey> {
48
- // Priority 1: Environment variable
39
+ export async function loadKey(): Promise<PrivateKey> {
49
40
  const envWif = process.env.PRIVATE_KEY_WIF
50
41
  if (envWif) {
51
42
  return PrivateKey.fromWif(envWif)
52
43
  }
53
44
 
54
- // Priority 2: Encrypted file
55
45
  const keysPath = getKeysPath()
56
46
  if (!existsSync(keysPath)) {
57
47
  throw new Error(
@@ -61,24 +51,21 @@ export async function loadKey(password?: string): Promise<PrivateKey> {
61
51
 
62
52
  const encrypted = readFileSync(keysPath, 'utf8')
63
53
 
64
- // Priority 2a: Try Touch ID cached password
65
- let resolvedPassword = password
66
- if (!resolvedPassword) {
67
- try {
68
- const { getCachedPassword } = await import('bitcoin-backup')
69
- const cached = await getCachedPassword(keysPath)
70
- if (cached) {
71
- resolvedPassword = cached
72
- }
73
- } catch {
74
- // Touch ID not available or no cached password — fall through
75
- }
76
- }
54
+ let resolvedPassword = process.env.ONESAT_PASSWORD
77
55
 
78
56
  if (!resolvedPassword) {
79
- throw new Error(
80
- 'Password required to decrypt key file. Pass --password, set ONESAT_PASSWORD, or run "1sat init --touchid" to enable Touch ID.',
81
- )
57
+ if (!process.stdin.isTTY) {
58
+ throw new Error(
59
+ 'Password required to decrypt key file. Set ONESAT_PASSWORD or run in an interactive terminal.',
60
+ )
61
+ }
62
+ const input = await promptPassword({
63
+ message: 'Password:',
64
+ })
65
+ if (isCancel(input) || typeof input !== 'string' || input.length === 0) {
66
+ throw new Error('Password required to decrypt key file.')
67
+ }
68
+ resolvedPassword = input
82
69
  }
83
70
 
84
71
  const backup = await decryptBackup(encrypted, resolvedPassword)
@@ -93,8 +80,6 @@ export async function loadKey(password?: string): Promise<PrivateKey> {
93
80
  */
94
81
  export async function saveKey(wif: string, password: string): Promise<void> {
95
82
  ensureConfigDir()
96
-
97
- // Validate the WIF before saving
98
83
  PrivateKey.fromWif(wif)
99
84
 
100
85
  const payload: WifBackup = {
@@ -103,29 +88,5 @@ export async function saveKey(wif: string, password: string): Promise<void> {
103
88
  createdAt: new Date().toISOString(),
104
89
  }
105
90
  const encrypted = await encryptBackup(payload, password)
106
- const keysPath = getKeysPath()
107
- writeFileSync(keysPath, encrypted, { mode: 0o600 })
108
- }
109
-
110
- /**
111
- * Cache the password for keys.bep using Touch ID.
112
- */
113
- export async function cacheKeyPassword(password: string): Promise<void> {
114
- const { cachePassword } = await import('bitcoin-backup')
115
- await cachePassword(getKeysPath(), password)
116
- }
117
-
118
- /**
119
- * Remove the cached password for keys.bep.
120
- */
121
- export async function forgetKeyPassword(): Promise<void> {
122
- const { forgetPassword } = await import('bitcoin-backup')
123
- await forgetPassword(getKeysPath())
124
- }
125
-
126
- /**
127
- * Resolve a password from flag or environment variable.
128
- */
129
- export function resolvePassword(flagValue?: string): string | undefined {
130
- return flagValue ?? process.env.ONESAT_PASSWORD
91
+ writeFileSync(getKeysPath(), encrypted, { mode: 0o600 })
131
92
  }