@1sat/cli 0.0.1 → 0.0.3

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,13 +1,13 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
7
7
  "bin": {
8
8
  "1sat": "./src/cli.ts"
9
9
  },
10
- "files": ["src", "bin"],
10
+ "files": ["src"],
11
11
  "scripts": {
12
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
13
  "dev": "bun run src/cli.ts",
@@ -83,7 +83,7 @@ async function identityInfo(
83
83
  if (opts.json) {
84
84
  output({ identityKey: publicKey }, opts)
85
85
  } else {
86
- printKeyValue('Identity Key', publicKey)
86
+ printKeyValue({ 'Identity Key': publicKey })
87
87
  }
88
88
  } finally {
89
89
  await destroy()
@@ -4,10 +4,18 @@
4
4
  * Manage OpNS name identity bindings.
5
5
  */
6
6
 
7
+ import {
8
+ getOpnsNames,
9
+ opnsDeregister as opnsDeregisterAction,
10
+ opnsRegister as opnsRegisterAction,
11
+ } from '@1sat/actions'
12
+ import { confirm, isCancel } from '@clack/prompts'
7
13
  import type { GlobalFlags } from '../args'
8
14
  import { extractFlag } from '../args'
15
+ import { loadContext } from '../context'
9
16
  import { printCommandHelp } from '../help'
10
- import { fatal } from '../output'
17
+ import { loadKey, resolvePassword } from '../keys'
18
+ import { fatal, formatLabel, formatValue, output } from '../output'
11
19
 
12
20
  export async function handleOpnsCommand(
13
21
  args: string[],
@@ -26,7 +34,7 @@ export async function handleOpnsCommand(
26
34
  printCommandHelp('opns', {
27
35
  register: 'Register identity on an OpNS name (--outpoint <op>)',
28
36
  deregister: 'Deregister identity from an OpNS name (--outpoint <op>)',
29
- lookup: 'Look up an OpNS name (--name <name>)',
37
+ lookup: 'List OpNS names from wallet',
30
38
  })
31
39
  if (subcommand && subcommand !== 'help') {
32
40
  process.exit(1)
@@ -34,40 +42,124 @@ export async function handleOpnsCommand(
34
42
  }
35
43
  }
36
44
 
37
- async function opnsRegister(args: string[], _opts: GlobalFlags): Promise<void> {
45
+ async function opnsRegister(args: string[], opts: GlobalFlags): Promise<void> {
38
46
  const outpoint = extractFlag(args, '--outpoint')
39
47
 
40
48
  if (!outpoint) fatal('Missing --outpoint <txid.vout>')
41
49
 
42
- // TODO: Load context via loadContext() + loadKey()
43
- // TODO: Look up OpNS ordinal from wallet by outpoint
44
- // TODO: Call opnsRegister.execute(ctx, { ordinal })
45
- // TODO: Output txid
46
- fatal('opns register is not yet implemented')
50
+ if (!opts.yes) {
51
+ const ok = await confirm({
52
+ message: `Register identity on OpNS name ${outpoint}?`,
53
+ })
54
+ if (isCancel(ok) || !ok) {
55
+ fatal('Registration cancelled.')
56
+ }
57
+ }
58
+
59
+ const privateKey = await loadKey(resolvePassword())
60
+ const { ctx, destroy } = await loadContext(privateKey, {
61
+ chain: opts.chain,
62
+ })
63
+
64
+ try {
65
+ // Look up the OpNS ordinal from the wallet
66
+ const namesResult = await getOpnsNames.execute(ctx, { limit: 10000 })
67
+ const ordinal = namesResult.outputs.find((o) => o.outpoint === outpoint)
68
+ if (!ordinal) {
69
+ fatal(`OpNS name not found in wallet: ${outpoint}`)
70
+ }
71
+
72
+ const result = await opnsRegisterAction.execute(ctx, {
73
+ ordinal,
74
+ inputBEEF: namesResult.BEEF as number[] | undefined,
75
+ })
76
+
77
+ if (result.error) {
78
+ fatal(result.error)
79
+ }
80
+
81
+ output(opts.json ? result : { txid: result.txid }, opts)
82
+ } finally {
83
+ await destroy()
84
+ }
47
85
  }
48
86
 
49
87
  async function opnsDeregister(
50
88
  args: string[],
51
- _opts: GlobalFlags,
89
+ opts: GlobalFlags,
52
90
  ): Promise<void> {
53
91
  const outpoint = extractFlag(args, '--outpoint')
54
92
 
55
93
  if (!outpoint) fatal('Missing --outpoint <txid.vout>')
56
94
 
57
- // TODO: Load context via loadContext() + loadKey()
58
- // TODO: Look up OpNS ordinal from wallet by outpoint
59
- // TODO: Call opnsDeregister.execute(ctx, { ordinal })
60
- // TODO: Output txid
61
- fatal('opns deregister is not yet implemented')
95
+ if (!opts.yes) {
96
+ const ok = await confirm({
97
+ message: `Deregister identity from OpNS name ${outpoint}?`,
98
+ })
99
+ if (isCancel(ok) || !ok) {
100
+ fatal('Deregistration cancelled.')
101
+ }
102
+ }
103
+
104
+ const privateKey = await loadKey(resolvePassword())
105
+ const { ctx, destroy } = await loadContext(privateKey, {
106
+ chain: opts.chain,
107
+ })
108
+
109
+ try {
110
+ // Look up the OpNS ordinal from the wallet
111
+ const namesResult = await getOpnsNames.execute(ctx, { limit: 10000 })
112
+ const ordinal = namesResult.outputs.find((o) => o.outpoint === outpoint)
113
+ if (!ordinal) {
114
+ fatal(`OpNS name not found in wallet: ${outpoint}`)
115
+ }
116
+
117
+ const result = await opnsDeregisterAction.execute(ctx, {
118
+ ordinal,
119
+ inputBEEF: namesResult.BEEF as number[] | undefined,
120
+ })
121
+
122
+ if (result.error) {
123
+ fatal(result.error)
124
+ }
125
+
126
+ output(opts.json ? result : { txid: result.txid }, opts)
127
+ } finally {
128
+ await destroy()
129
+ }
62
130
  }
63
131
 
64
- async function opnsLookup(args: string[], _opts: GlobalFlags): Promise<void> {
65
- const name = extractFlag(args, '--name')
132
+ async function opnsLookup(_args: string[], opts: GlobalFlags): Promise<void> {
133
+ const privateKey = await loadKey(resolvePassword())
134
+ const { ctx, destroy } = await loadContext(privateKey, {
135
+ chain: opts.chain,
136
+ })
66
137
 
67
- if (!name) fatal('Missing --name <name>')
138
+ try {
139
+ const result = await getOpnsNames.execute(ctx, { limit: 100 })
68
140
 
69
- // TODO: Load context via loadContext() + loadKey()
70
- // TODO: Use services.ordfs to look up OpNS name resolution
71
- // TODO: Output resolved identity info
72
- fatal('opns lookup is not yet implemented')
141
+ if (opts.json) {
142
+ output(result.outputs, opts)
143
+ return
144
+ }
145
+
146
+ if (result.outputs.length === 0) {
147
+ output('No OpNS names found.', opts)
148
+ return
149
+ }
150
+
151
+ for (const o of result.outputs) {
152
+ const nameTag = o.tags?.find((t) => t.startsWith('name:'))?.slice(5) ?? ''
153
+ const publishedTag = o.tags?.find((t) => t === 'opns:published')
154
+ const status = publishedTag ? 'registered' : 'unregistered'
155
+
156
+ console.log(
157
+ ` ${formatValue(o.outpoint)} ${nameTag ? formatValue(nameTag) : ''} ${formatLabel(status)}`,
158
+ )
159
+ }
160
+
161
+ console.log(`\n ${result.outputs.length} OpNS name(s) found.`)
162
+ } finally {
163
+ await destroy()
164
+ }
73
165
  }
@@ -4,10 +4,21 @@
4
4
  * Sweep assets from external wallets into the BRC-100 wallet.
5
5
  */
6
6
 
7
+ import {
8
+ prepareSweepInputs,
9
+ scanAddressUtxos,
10
+ sweepBsv,
11
+ sweepBsv21,
12
+ sweepOrdinals,
13
+ } from '@1sat/actions'
14
+ import { PrivateKey } from '@bsv/sdk'
15
+ import { confirm, isCancel } from '@clack/prompts'
7
16
  import type { GlobalFlags } from '../args'
8
17
  import { extractFlag } from '../args'
18
+ import { loadContext } from '../context'
9
19
  import { printCommandHelp } from '../help'
10
- import { fatal } from '../output'
20
+ import { loadKey, resolvePassword } from '../keys'
21
+ import { fatal, formatLabel, formatValue, output } from '../output'
11
22
 
12
23
  export async function handleSweepCommand(
13
24
  args: string[],
@@ -22,7 +33,7 @@ export async function handleSweepCommand(
22
33
  return sweepImport(rest, opts)
23
34
  default:
24
35
  printCommandHelp('sweep', {
25
- scan: 'Scan an address for UTXOs (--address <addr>)',
36
+ scan: 'Scan an address for UTXOs (--wif <key>)',
26
37
  import: 'Import UTXOs into wallet (--wif <key>)',
27
38
  })
28
39
  if (subcommand && subcommand !== 'help') {
@@ -31,24 +42,180 @@ export async function handleSweepCommand(
31
42
  }
32
43
  }
33
44
 
34
- async function sweepScan(args: string[], _opts: GlobalFlags): Promise<void> {
35
- const address = extractFlag(args, '--address')
45
+ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
46
+ const wif = extractFlag(args, '--wif')
47
+
48
+ if (!wif) fatal('Missing --wif <private-key>')
49
+
50
+ let address: string
51
+ try {
52
+ const pk = PrivateKey.fromWif(wif)
53
+ address = pk.toPublicKey().toAddress()
54
+ } catch {
55
+ fatal('Invalid WIF private key')
56
+ }
57
+
58
+ const privateKey = await loadKey(resolvePassword())
59
+ const { ctx, destroy } = await loadContext(privateKey, {
60
+ chain: opts.chain,
61
+ })
36
62
 
37
- if (!address) fatal('Missing --address <addr>')
63
+ try {
64
+ if (!ctx.services) {
65
+ fatal('Services required for sweep scan')
66
+ }
67
+
68
+ const result = await scanAddressUtxos(ctx.services, address)
69
+
70
+ if (opts.json) {
71
+ output(result, opts)
72
+ return
73
+ }
74
+
75
+ console.log(`\n Scan results for ${formatValue(address)}:\n`)
76
+
77
+ console.log(` ${formatLabel('Funding UTXOs:')} ${result.funding.length}`)
78
+ if (result.funding.length > 0) {
79
+ console.log(` ${formatLabel('Total funding:')} ${formatValue(result.totalFundingSats)} satoshis`)
80
+ for (const f of result.funding) {
81
+ console.log(` ${formatValue(f.outpoint)} ${formatLabel(`${f.satoshis} sats`)}`)
82
+ }
83
+ }
38
84
 
39
- // TODO: Load context via loadContext() + loadKey()
40
- // TODO: Use services to scan address for UTXOs (BSV, ordinals, tokens)
41
- // TODO: Display found UTXOs with type classification
42
- fatal('sweep scan is not yet implemented')
85
+ console.log(`\n ${formatLabel('Ordinal UTXOs:')} ${result.ordinals.length}`)
86
+ if (result.ordinals.length > 0) {
87
+ for (const o of result.ordinals) {
88
+ console.log(` ${formatValue(o.outpoint)}`)
89
+ }
90
+ }
91
+
92
+ console.log(`\n ${formatLabel('BSV-21 Tokens:')} ${result.bsv21Tokens.length}`)
93
+ if (result.bsv21Tokens.length > 0) {
94
+ for (const t of result.bsv21Tokens) {
95
+ console.log(
96
+ ` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel('amount:')} ${formatValue(t.totalAmount)} ${formatLabel('UTXOs:')} ${t.inputs.length} ${formatLabel('dec:')} ${t.decimals}`,
97
+ )
98
+ }
99
+ }
100
+
101
+ const total = result.funding.length + result.ordinals.length + result.bsv21Tokens.reduce((n, t) => n + t.inputs.length, 0)
102
+ console.log(`\n ${total} total UTXO(s) found.`)
103
+ } finally {
104
+ await destroy()
105
+ }
43
106
  }
44
107
 
45
- async function sweepImport(args: string[], _opts: GlobalFlags): Promise<void> {
108
+ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
46
109
  const wif = extractFlag(args, '--wif')
47
110
 
48
111
  if (!wif) fatal('Missing --wif <private-key>')
49
112
 
50
- // TODO: Load context via loadContext() + loadKey()
51
- // TODO: Call sweepBsv/sweepOrdinals/sweepBsv21 actions
52
- // TODO: Output sweep results: txids and amounts imported
53
- fatal('sweep import is not yet implemented')
113
+ let address: string
114
+ try {
115
+ const pk = PrivateKey.fromWif(wif)
116
+ address = pk.toPublicKey().toAddress()
117
+ } catch {
118
+ fatal('Invalid WIF private key')
119
+ }
120
+
121
+ const privateKey = await loadKey(resolvePassword())
122
+ const { ctx, destroy } = await loadContext(privateKey, {
123
+ chain: opts.chain,
124
+ })
125
+
126
+ try {
127
+ if (!ctx.services) {
128
+ fatal('Services required for sweep import')
129
+ }
130
+
131
+ // Scan first to discover what's available
132
+ const scan = await scanAddressUtxos(ctx.services, address)
133
+
134
+ const hasFunding = scan.funding.length > 0
135
+ const hasOrdinals = scan.ordinals.length > 0
136
+ const hasTokens = scan.bsv21Tokens.length > 0
137
+
138
+ if (!hasFunding && !hasOrdinals && !hasTokens) {
139
+ fatal(`No UTXOs found at ${address}`)
140
+ }
141
+
142
+ // Summarize what will be swept
143
+ const parts: string[] = []
144
+ if (hasFunding) parts.push(`${scan.totalFundingSats} sats (${scan.funding.length} UTXOs)`)
145
+ if (hasOrdinals) parts.push(`${scan.ordinals.length} ordinal(s)`)
146
+ if (hasTokens) {
147
+ for (const t of scan.bsv21Tokens) {
148
+ parts.push(`${t.totalAmount} ${t.symbol ?? t.tokenId.slice(0, 12)} token(s)`)
149
+ }
150
+ }
151
+
152
+ if (!opts.yes) {
153
+ const ok = await confirm({
154
+ message: `Sweep ${parts.join(', ')} from ${address}?`,
155
+ })
156
+ if (isCancel(ok) || !ok) {
157
+ fatal('Sweep cancelled.')
158
+ }
159
+ }
160
+
161
+ const txids: string[] = []
162
+
163
+ // Convert SweepInput to IndexedOutput shape for prepareSweepInputs
164
+ const toIndexed = (items: Array<{ outpoint: string; satoshis: number }>) =>
165
+ items.map((item) => ({
166
+ outpoint: item.outpoint,
167
+ satoshis: item.satoshis,
168
+ score: 0,
169
+ }))
170
+
171
+ // Sweep BSV funding UTXOs
172
+ if (hasFunding) {
173
+ const inputs = await prepareSweepInputs(ctx, toIndexed(scan.funding))
174
+
175
+ const result = await sweepBsv.execute(ctx, { inputs, wif })
176
+ if (result.error) {
177
+ fatal(`BSV sweep failed: ${result.error}`)
178
+ }
179
+ if (result.txid) txids.push(result.txid)
180
+ }
181
+
182
+ // Sweep ordinals
183
+ if (hasOrdinals) {
184
+ const inputs = await prepareSweepInputs(ctx, toIndexed(scan.ordinals))
185
+
186
+ const result = await sweepOrdinals.execute(ctx, { inputs, wif })
187
+ if (result.error) {
188
+ fatal(`Ordinals sweep failed: ${result.error}`)
189
+ }
190
+ if (result.txid) txids.push(result.txid)
191
+ }
192
+
193
+ // Sweep BSV-21 tokens (one sweep per tokenId)
194
+ if (hasTokens) {
195
+ for (const tokenGroup of scan.bsv21Tokens) {
196
+ const inputs = await prepareSweepInputs(ctx, toIndexed(tokenGroup.inputs))
197
+
198
+ const sweepInputs = inputs.map((inp, idx) => ({
199
+ ...inp,
200
+ tokenId: tokenGroup.inputs[idx].tokenId,
201
+ amount: tokenGroup.inputs[idx].amount,
202
+ }))
203
+
204
+ const result = await sweepBsv21.execute(ctx, { inputs: sweepInputs, wif })
205
+ if (result.error) {
206
+ fatal(`Token sweep failed (${tokenGroup.symbol ?? tokenGroup.tokenId.slice(0, 12)}): ${result.error}`)
207
+ }
208
+ if (result.txid) txids.push(result.txid)
209
+ }
210
+ }
211
+
212
+ output(
213
+ opts.json
214
+ ? { txids, swept: parts }
215
+ : `Sweep complete. ${txids.length} transaction(s): ${txids.join(', ')}`,
216
+ opts,
217
+ )
218
+ } finally {
219
+ await destroy()
220
+ }
54
221
  }
@@ -2,7 +2,12 @@
2
2
  * Token commands - balances, list, send, deploy, buy.
3
3
  */
4
4
 
5
- import { getBsv21Balances, listTokens, sendBsv21 } from '@1sat/actions'
5
+ import {
6
+ getBsv21Balances,
7
+ listTokens,
8
+ purchaseBsv21,
9
+ sendBsv21,
10
+ } from '@1sat/actions'
6
11
  import { confirm, isCancel } from '@clack/prompts'
7
12
  import type { GlobalFlags } from '../args'
8
13
  import { extractFlag } from '../args'
@@ -33,8 +38,8 @@ export async function handleTokensCommand(
33
38
  balances: 'Show token balances by token ID',
34
39
  list: 'List owned token UTXOs (--token-id <id>)',
35
40
  send: 'Transfer tokens (--token-id <id> --to <addr> --amount <n>)',
36
- deploy: 'Deploy a new BSV21 token (--symbol <sym> --amount <n>)',
37
- buy: 'Purchase listed tokens (--outpoint <op>)',
41
+ deploy: 'Deploy a new BSV21 token (not yet available)',
42
+ buy: 'Purchase listed tokens (--outpoint <op> --token-id <id> --amount <n>)',
38
43
  })
39
44
  if (subcommand && subcommand !== 'help') {
40
45
  process.exit(1)
@@ -170,25 +175,46 @@ async function tokenSend(args: string[], opts: GlobalFlags): Promise<void> {
170
175
  }
171
176
  }
172
177
 
173
- async function tokenDeploy(args: string[], _opts: GlobalFlags): Promise<void> {
174
- const symbol = extractFlag(args, '--symbol')
175
- const amountStr = extractFlag(args, '--amount')
176
- const _icon = extractFlag(args, '--icon')
177
-
178
- if (!symbol) fatal('Missing --symbol <sym>')
179
- if (!amountStr) fatal('Missing --amount <initial-supply>')
180
-
181
- // TODO: Call deployToken action
182
- // TODO: Output txid and token origin
183
- fatal('tokens deploy is not yet implemented')
178
+ async function tokenDeploy(_args: string[], _opts: GlobalFlags): Promise<void> {
179
+ fatal('tokens deploy is not yet available. No deploy action exists in the actions package.')
184
180
  }
185
181
 
186
- async function tokenBuy(args: string[], _opts: GlobalFlags): Promise<void> {
182
+ async function tokenBuy(args: string[], opts: GlobalFlags): Promise<void> {
187
183
  const outpoint = extractFlag(args, '--outpoint')
184
+ const tokenId = extractFlag(args, '--token-id')
185
+ const amountStr = extractFlag(args, '--amount')
188
186
 
189
187
  if (!outpoint) fatal('Missing --outpoint <txid.vout>')
188
+ if (!tokenId) fatal('Missing --token-id <id>')
189
+ if (!amountStr) fatal('Missing --amount <token-amount>')
190
+
191
+ if (!opts.yes) {
192
+ const ok = await confirm({
193
+ message: `Purchase ${amountStr} tokens (${tokenId.slice(0, 12)}...) from listing ${outpoint}?`,
194
+ })
195
+ if (isCancel(ok) || !ok) {
196
+ fatal('Purchase cancelled.')
197
+ }
198
+ }
190
199
 
191
- // TODO: Call purchaseTokenListing action
192
- // TODO: Output txid
193
- fatal('tokens buy is not yet implemented')
200
+ const privateKey = await loadKey(resolvePassword())
201
+ const { ctx, destroy } = await loadContext(privateKey, {
202
+ chain: opts.chain,
203
+ })
204
+
205
+ try {
206
+ const result = await purchaseBsv21.execute(ctx, {
207
+ tokenId,
208
+ outpoint,
209
+ amount: amountStr,
210
+ })
211
+
212
+ if (result.error) {
213
+ fatal(result.error)
214
+ }
215
+
216
+ output(opts.json ? result : { txid: result.txid }, opts)
217
+ } finally {
218
+ await destroy()
219
+ }
194
220
  }
package/src/help.ts CHANGED
@@ -2,18 +2,24 @@
2
2
  * Help text and version display for the 1sat CLI.
3
3
  */
4
4
 
5
- import { readFileSync } from 'node:fs'
6
5
  import chalk from 'chalk'
7
6
 
8
- export function getVersion(): string {
7
+ // Version is read at build time via Bun's import. Works in both source and compiled binary.
8
+ const VERSION = (() => {
9
9
  try {
10
+ // biome-ignore lint/style/noVar: dynamic require for Bun compiled binary compat
11
+ const { readFileSync } = require('node:fs')
10
12
  const pkg = JSON.parse(
11
13
  readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
12
14
  )
13
- return pkg.version || 'unknown'
15
+ return pkg.version || '0.0.1'
14
16
  } catch {
15
- return 'unknown'
17
+ return '0.0.1'
16
18
  }
19
+ })()
20
+
21
+ export function getVersion(): string {
22
+ return VERSION
17
23
  }
18
24
 
19
25
  export function printVersion(): void {
package/src/output.ts CHANGED
@@ -51,7 +51,23 @@ export function output(
51
51
 
52
52
  if (typeof data === 'object' && data !== null) {
53
53
  for (const [key, value] of Object.entries(data)) {
54
- console.log(`${formatLabel(`${key}:`)} ${formatValue(String(value))}`)
54
+ if (Array.isArray(value)) {
55
+ console.log(`${formatLabel(`${key}:`)}`)
56
+ for (const item of value) {
57
+ if (typeof item === 'object' && item !== null) {
58
+ const parts = Object.entries(item)
59
+ .map(([k, v]) => `${k}=${v}`)
60
+ .join(' ')
61
+ console.log(` ${formatValue(parts)}`)
62
+ } else {
63
+ console.log(` ${formatValue(String(item))}`)
64
+ }
65
+ }
66
+ } else if (typeof value === 'object' && value !== null) {
67
+ console.log(`${formatLabel(`${key}:`)} ${JSON.stringify(value)}`)
68
+ } else {
69
+ console.log(`${formatLabel(`${key}:`)} ${formatValue(String(value))}`)
70
+ }
55
71
  }
56
72
  return
57
73
  }
package/bin/1sat DELETED
Binary file