@1sat/cli 0.0.1

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.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Transaction utility commands - decode.
3
+ */
4
+
5
+ import { Transaction } from '@bsv/sdk'
6
+ import type { GlobalFlags } from '../args'
7
+ import { printCommandHelp } from '../help'
8
+ import { fatal, output } from '../output'
9
+
10
+ export async function handleTxCommand(
11
+ args: string[],
12
+ opts: GlobalFlags,
13
+ ): Promise<void> {
14
+ const [subcommand, ...rest] = args
15
+
16
+ switch (subcommand) {
17
+ case 'decode':
18
+ return txDecode(rest, opts)
19
+ default:
20
+ printCommandHelp('tx', {
21
+ decode: 'Decode a raw transaction hex',
22
+ })
23
+ if (subcommand && subcommand !== 'help') {
24
+ process.exit(1)
25
+ }
26
+ }
27
+ }
28
+
29
+ async function txDecode(args: string[], opts: GlobalFlags): Promise<void> {
30
+ const hex = args[0]
31
+
32
+ if (!hex) fatal('Missing transaction hex. Usage: 1sat tx decode <hex>')
33
+
34
+ let tx: Transaction
35
+ try {
36
+ tx = Transaction.fromHex(hex)
37
+ } catch (e) {
38
+ fatal(
39
+ `Failed to decode transaction: ${e instanceof Error ? e.message : String(e)}`,
40
+ )
41
+ }
42
+
43
+ const decoded = {
44
+ txid: tx.id('hex'),
45
+ version: tx.version,
46
+ lockTime: tx.lockTime,
47
+ inputs: tx.inputs.map((input, i) => ({
48
+ index: i,
49
+ sourceTXID: input.sourceTXID,
50
+ sourceOutputIndex: input.sourceOutputIndex,
51
+ sequence: input.sequence,
52
+ scriptLength: input.unlockingScript?.toBinary().length ?? 0,
53
+ })),
54
+ outputs: tx.outputs.map((out, i) => ({
55
+ index: i,
56
+ satoshis: out.satoshis,
57
+ scriptLength: out.lockingScript.toBinary().length,
58
+ scriptHex:
59
+ out.lockingScript.toHex().slice(0, 80) +
60
+ (out.lockingScript.toHex().length > 80 ? '...' : ''),
61
+ })),
62
+ }
63
+
64
+ output(decoded, opts)
65
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Wallet commands - balance, address, send, send-all, info.
3
+ */
4
+
5
+ import { deriveDepositAddresses, sendAllBsv, sendBsv } from '@1sat/actions'
6
+ import { confirm, isCancel } from '@clack/prompts'
7
+ import type { GlobalFlags } from '../args'
8
+ import { extractFlag } from '../args'
9
+ import { loadContext } from '../context'
10
+ import { printCommandHelp } from '../help'
11
+ import { loadKey, resolvePassword } from '../keys'
12
+ import { fatal, formatValue, output, printKeyValue } from '../output'
13
+
14
+ export async function handleWalletCommand(
15
+ args: string[],
16
+ opts: GlobalFlags,
17
+ ): Promise<void> {
18
+ const [subcommand, ...rest] = args
19
+
20
+ switch (subcommand) {
21
+ case 'balance':
22
+ return walletBalance(rest, opts)
23
+ case 'address':
24
+ return walletAddress(rest, opts)
25
+ case 'send':
26
+ return walletSend(rest, opts)
27
+ case 'send-all':
28
+ return walletSendAll(rest, opts)
29
+ case 'info':
30
+ return walletInfo(rest, opts)
31
+ default:
32
+ printCommandHelp('wallet', {
33
+ balance: 'Show wallet balance in satoshis',
34
+ address: 'Show deposit address',
35
+ send: 'Send BSV to an address (--to <addr> --sats <amount>)',
36
+ 'send-all': 'Send all BSV to an address (--to <addr>)',
37
+ info: 'Show wallet info (address, balance, network)',
38
+ })
39
+ if (subcommand && subcommand !== 'help') {
40
+ process.exit(1)
41
+ }
42
+ }
43
+ }
44
+
45
+ async function walletBalance(
46
+ _args: string[],
47
+ opts: GlobalFlags,
48
+ ): Promise<void> {
49
+ const privateKey = await loadKey(resolvePassword())
50
+ const { ctx, destroy } = await loadContext(privateKey, {
51
+ chain: opts.chain,
52
+ })
53
+
54
+ try {
55
+ const result = await ctx.wallet.listOutputs({
56
+ basket: 'default',
57
+ include: 'locking scripts',
58
+ limit: 10000,
59
+ })
60
+
61
+ const totalSatoshis = result.outputs.reduce((sum, o) => sum + o.satoshis, 0)
62
+
63
+ output(
64
+ opts.json
65
+ ? { satoshis: totalSatoshis, utxos: result.outputs.length }
66
+ : `${formatValue(totalSatoshis)} satoshis (${result.outputs.length} UTXOs)`,
67
+ opts,
68
+ )
69
+ } finally {
70
+ await destroy()
71
+ }
72
+ }
73
+
74
+ async function walletAddress(
75
+ _args: string[],
76
+ opts: GlobalFlags,
77
+ ): Promise<void> {
78
+ const privateKey = await loadKey(resolvePassword())
79
+ const { ctx, destroy } = await loadContext(privateKey, {
80
+ chain: opts.chain,
81
+ })
82
+
83
+ try {
84
+ const result = await deriveDepositAddresses.execute(ctx, {
85
+ prefix: '1sat',
86
+ count: 1,
87
+ })
88
+
89
+ const primary = result.derivations[0]
90
+ if (!primary) {
91
+ fatal('Failed to derive deposit address')
92
+ }
93
+
94
+ output(opts.json ? primary : primary.address, opts)
95
+ } finally {
96
+ await destroy()
97
+ }
98
+ }
99
+
100
+ async function walletSend(args: string[], opts: GlobalFlags): Promise<void> {
101
+ const to = extractFlag(args, '--to')
102
+ const satsStr = extractFlag(args, '--sats')
103
+
104
+ if (!to) fatal('Missing --to <address>')
105
+ if (!satsStr) fatal('Missing --sats <amount>')
106
+
107
+ const satoshis = Number(satsStr)
108
+ if (!Number.isFinite(satoshis) || satoshis <= 0) {
109
+ fatal('--sats must be a positive number')
110
+ }
111
+
112
+ if (!opts.yes) {
113
+ const ok = await confirm({
114
+ message: `Send ${satoshis} satoshis to ${to}?`,
115
+ })
116
+ if (isCancel(ok) || !ok) {
117
+ fatal('Send cancelled.')
118
+ }
119
+ }
120
+
121
+ const privateKey = await loadKey(resolvePassword())
122
+ const { ctx, destroy } = await loadContext(privateKey, {
123
+ chain: opts.chain,
124
+ })
125
+
126
+ try {
127
+ const result = await sendBsv.execute(ctx, {
128
+ requests: [{ address: to, satoshis }],
129
+ })
130
+
131
+ if (result.error) {
132
+ fatal(result.error)
133
+ }
134
+
135
+ output(opts.json ? result : { txid: result.txid }, opts)
136
+ } finally {
137
+ await destroy()
138
+ }
139
+ }
140
+
141
+ async function walletSendAll(args: string[], opts: GlobalFlags): Promise<void> {
142
+ const to = extractFlag(args, '--to')
143
+
144
+ if (!to) fatal('Missing --to <address>')
145
+
146
+ if (!opts.yes) {
147
+ const ok = await confirm({
148
+ message: `Send ALL BSV to ${to}? This will empty your wallet.`,
149
+ })
150
+ if (isCancel(ok) || !ok) {
151
+ fatal('Send cancelled.')
152
+ }
153
+ }
154
+
155
+ const privateKey = await loadKey(resolvePassword())
156
+ const { ctx, destroy } = await loadContext(privateKey, {
157
+ chain: opts.chain,
158
+ })
159
+
160
+ try {
161
+ const result = await sendAllBsv.execute(ctx, { destination: to })
162
+
163
+ if (result.error) {
164
+ fatal(result.error)
165
+ }
166
+
167
+ output(opts.json ? result : { txid: result.txid }, opts)
168
+ } finally {
169
+ await destroy()
170
+ }
171
+ }
172
+
173
+ async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
174
+ const privateKey = await loadKey(resolvePassword())
175
+ const { ctx, destroy } = await loadContext(privateKey, {
176
+ chain: opts.chain,
177
+ })
178
+
179
+ try {
180
+ const [addressResult, balanceResult, identityResult] = await Promise.all([
181
+ deriveDepositAddresses.execute(ctx, { prefix: '1sat', count: 1 }),
182
+ ctx.wallet.listOutputs({
183
+ basket: 'default',
184
+ include: 'locking scripts',
185
+ limit: 10000,
186
+ }),
187
+ ctx.wallet.getPublicKey({ identityKey: true }),
188
+ ])
189
+
190
+ const address = addressResult.derivations[0]?.address ?? 'unknown'
191
+ const totalSatoshis = balanceResult.outputs.reduce(
192
+ (sum, o) => sum + o.satoshis,
193
+ 0,
194
+ )
195
+
196
+ const info = {
197
+ chain: opts.chain,
198
+ address,
199
+ identityKey: identityResult.publicKey,
200
+ balance: totalSatoshis,
201
+ utxos: balanceResult.outputs.length,
202
+ }
203
+
204
+ if (opts.json) {
205
+ output(info, opts)
206
+ } else {
207
+ printKeyValue({
208
+ Chain: info.chain,
209
+ Address: info.address,
210
+ 'Identity Key': info.identityKey,
211
+ 'Balance (sats)': info.balance,
212
+ UTXOs: info.utxos,
213
+ })
214
+ }
215
+ } finally {
216
+ await destroy()
217
+ }
218
+ }
package/src/config.ts ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Config management for ~/.1sat/
3
+ *
4
+ * Handles persistent configuration on disk with secure file permissions.
5
+ */
6
+
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { homedir } from 'node:os'
9
+ import { join } from 'node:path'
10
+
11
+ const CONFIG_DIR = join(homedir(), '.1sat')
12
+ const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
13
+
14
+ export interface OneSatCliConfig {
15
+ /** Network: mainnet or testnet */
16
+ chain: 'main' | 'test'
17
+ /** Data directory for wallet databases */
18
+ dataDir: string
19
+ /** Remote storage URL for wallet backup */
20
+ remoteStorageUrl?: string
21
+ /** Storage identity key for wallet persistence */
22
+ storageIdentityKey?: string
23
+ }
24
+
25
+ const DEFAULT_CONFIG: OneSatCliConfig = {
26
+ chain: 'main',
27
+ dataDir: join(CONFIG_DIR, 'data'),
28
+ }
29
+
30
+ /**
31
+ * Ensure ~/.1sat/ exists with secure permissions.
32
+ */
33
+ export function ensureConfigDir(): void {
34
+ if (!existsSync(CONFIG_DIR)) {
35
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Load config from disk. Returns defaults if file doesn't exist.
41
+ */
42
+ export function loadConfig(): OneSatCliConfig {
43
+ if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG }
44
+ try {
45
+ const raw = readFileSync(CONFIG_FILE, 'utf8')
46
+ const parsed = JSON.parse(raw)
47
+ return { ...DEFAULT_CONFIG, ...parsed }
48
+ } catch {
49
+ return { ...DEFAULT_CONFIG }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Save config to disk with secure permissions.
55
+ */
56
+ export function saveConfig(config: OneSatCliConfig): void {
57
+ ensureConfigDir()
58
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), {
59
+ mode: 0o600,
60
+ })
61
+ }
62
+
63
+ /**
64
+ * Update specific config fields, preserving the rest.
65
+ */
66
+ export function updateConfig(patch: Partial<OneSatCliConfig>): OneSatCliConfig {
67
+ const config = loadConfig()
68
+ const next = { ...config, ...patch }
69
+ saveConfig(next)
70
+ return next
71
+ }
72
+
73
+ /**
74
+ * Get the config directory path.
75
+ */
76
+ export function getConfigDir(): string {
77
+ return CONFIG_DIR
78
+ }
79
+
80
+ /**
81
+ * Get the config file path.
82
+ */
83
+ export function getConfigFile(): string {
84
+ return CONFIG_FILE
85
+ }
86
+
87
+ /**
88
+ * Get the data directory, ensuring it exists.
89
+ */
90
+ export function ensureDataDir(): string {
91
+ const config = loadConfig()
92
+ const dataDir = config.dataDir
93
+ if (!existsSync(dataDir)) {
94
+ mkdirSync(dataDir, { recursive: true, mode: 0o700 })
95
+ }
96
+ return dataDir
97
+ }
package/src/context.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * OneSatContext factory for CLI commands.
3
+ *
4
+ * Creates a fully initialized wallet context with services and monitor.
5
+ */
6
+
7
+ import { type OneSatContext, createContext } from '@1sat/actions'
8
+ import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
9
+ import type { PrivateKey } from '@bsv/sdk'
10
+ import { ensureDataDir, loadConfig } from './config'
11
+
12
+ /** Extended context that includes cleanup */
13
+ export interface CliContext {
14
+ ctx: OneSatContext
15
+ walletResult: NodeWalletResult
16
+ destroy: () => Promise<void>
17
+ }
18
+
19
+ /**
20
+ * Create a fully initialized OneSatContext for CLI use.
21
+ *
22
+ * Sets up:
23
+ * - Node wallet with SQLite storage
24
+ * - 1Sat services for API access
25
+ * - Monitor for transaction lifecycle
26
+ */
27
+ export async function loadContext(
28
+ privateKey: PrivateKey,
29
+ opts: { chain: 'main' | 'test' },
30
+ ): Promise<CliContext> {
31
+ const config = loadConfig()
32
+ const dataDir = ensureDataDir()
33
+
34
+ const storageIdentityKey = config.storageIdentityKey ?? '1sat-cli-default'
35
+
36
+ const walletResult = await createNodeWallet({
37
+ privateKey,
38
+ chain: opts.chain,
39
+ storageIdentityKey,
40
+ storage: {
41
+ client: 'better-sqlite3',
42
+ connection: {
43
+ filename: `${dataDir}/wallet-${opts.chain}.db`,
44
+ },
45
+ useNullAsDefault: true,
46
+ },
47
+ remoteStorageUrl: config.remoteStorageUrl,
48
+ })
49
+
50
+ walletResult.monitor.startTasks()
51
+
52
+ const ctx = createContext(walletResult.wallet, {
53
+ services: walletResult.services,
54
+ chain: opts.chain,
55
+ dataDir,
56
+ })
57
+
58
+ return {
59
+ ctx,
60
+ walletResult,
61
+ destroy: walletResult.destroy,
62
+ }
63
+ }
package/src/help.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Help text and version display for the 1sat CLI.
3
+ */
4
+
5
+ import { readFileSync } from 'node:fs'
6
+ import chalk from 'chalk'
7
+
8
+ export function getVersion(): string {
9
+ try {
10
+ const pkg = JSON.parse(
11
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
12
+ )
13
+ return pkg.version || 'unknown'
14
+ } catch {
15
+ return 'unknown'
16
+ }
17
+ }
18
+
19
+ export function printVersion(): void {
20
+ console.log(`1sat ${getVersion()}`)
21
+ }
22
+
23
+ export function printHelp(): void {
24
+ const dim = chalk.dim
25
+ const cyan = chalk.cyan
26
+ const bold = chalk.bold
27
+
28
+ console.log(`
29
+ ${bold('1sat')} - CLI for 1Sat Ordinals SDK
30
+
31
+ ${bold('Usage:')}
32
+ 1sat <command> [subcommand] [options]
33
+
34
+ ${bold('Setup:')}
35
+ ${cyan('init')} Interactive wallet setup wizard
36
+ ${cyan('config')} <subcommand> Manage configuration
37
+
38
+ ${bold('Wallet:')}
39
+ ${cyan('wallet balance')} Show wallet balance
40
+ ${cyan('wallet address')} Show deposit address
41
+ ${cyan('wallet send')} Send BSV to an address
42
+ ${cyan('wallet send-all')} Send all BSV to an address
43
+ ${cyan('wallet info')} Show wallet info
44
+
45
+ ${bold('Ordinals:')}
46
+ ${cyan('ordinals list')} List owned ordinals
47
+ ${cyan('ordinals mint')} Mint a new ordinal inscription
48
+ ${cyan('ordinals transfer')} Transfer an ordinal
49
+ ${cyan('ordinals sell')} List an ordinal for sale
50
+ ${cyan('ordinals cancel')} Cancel an ordinal listing
51
+ ${cyan('ordinals buy')} Purchase a listed ordinal
52
+
53
+ ${bold('Tokens (BSV21):')}
54
+ ${cyan('tokens balances')} Show token balances
55
+ ${cyan('tokens list')} List owned token UTXOs
56
+ ${cyan('tokens send')} Transfer tokens
57
+ ${cyan('tokens deploy')} Deploy a new BSV21 token
58
+ ${cyan('tokens buy')} Purchase listed tokens
59
+
60
+ ${bold('Locks:')}
61
+ ${cyan('locks info')} Show lock information
62
+ ${cyan('locks lock')} Time-lock BSV
63
+ ${cyan('locks unlock')} Unlock matured BSV
64
+
65
+ ${bold('Identity (BAP):')}
66
+ ${cyan('identity create')} Create a new BAP identity
67
+ ${cyan('identity info')} Show identity information
68
+ ${cyan('identity sign')} Sign a message with identity key
69
+ ${cyan('identity verify')} Verify a signed message
70
+
71
+ ${bold('Social:')}
72
+ ${cyan('social post')} Create an on-chain social post
73
+
74
+ ${bold('OpNS:')}
75
+ ${cyan('opns register')} Register identity on OpNS name
76
+ ${cyan('opns deregister')} Deregister identity from OpNS name
77
+ ${cyan('opns lookup')} Look up an OpNS name
78
+
79
+ ${bold('Sweep:')}
80
+ ${cyan('sweep scan')} Scan an address for UTXOs
81
+ ${cyan('sweep import')} Import UTXOs into wallet
82
+
83
+ ${bold('Advanced:')}
84
+ ${cyan('action')} <name> [json] Execute a registered action by name
85
+ ${cyan('tx decode')} <hex> Decode a raw transaction
86
+
87
+ ${bold('Global Options:')}
88
+ ${dim('--json')} Output as JSON
89
+ ${dim('--quiet, -q')} Suppress output
90
+ ${dim('--yes, -y')} Skip confirmations
91
+ ${dim('--chain <main|test>')} Network (default: main)
92
+ ${dim('--help, -h')} Show help
93
+ ${dim('--version, -v')} Show version
94
+
95
+ ${bold('Environment Variables:')}
96
+ ${dim('PRIVATE_KEY_WIF')} Private key (bypasses encrypted keyfile)
97
+ ${dim('ONESAT_PASSWORD')} Password for encrypted keyfile
98
+
99
+ ${bold('Config:')} ~/.1sat/
100
+ `)
101
+ }
102
+
103
+ export function printCommandHelp(
104
+ command: string,
105
+ subcommands: Record<string, string>,
106
+ ): void {
107
+ const bold = chalk.bold
108
+ const cyan = chalk.cyan
109
+ const dim = chalk.dim
110
+
111
+ console.log(`\n${bold(`1sat ${command}`)}`)
112
+ console.log(`\n${bold('Subcommands:')}`)
113
+ for (const [sub, desc] of Object.entries(subcommands)) {
114
+ console.log(` ${cyan(sub.padEnd(20))} ${dim(desc)}`)
115
+ }
116
+ console.log()
117
+ }
package/src/keys.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Encrypted key management for the 1sat CLI.
3
+ *
4
+ * Key resolution priority:
5
+ * 1. PRIVATE_KEY_WIF env var
6
+ * 2. ~/.1sat/keys.bep encrypted file
7
+ * 3. Fail with "Run 1sat init"
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
11
+ import { join } from 'node:path'
12
+ import { PrivateKey } from '@bsv/sdk'
13
+ import { type WifBackup, decryptBackup, encryptBackup } from 'bitcoin-backup'
14
+ import { ensureConfigDir, getConfigDir } from './config'
15
+
16
+ const KEYS_FILE = 'keys.bep'
17
+
18
+ function getKeysPath(): string {
19
+ return join(getConfigDir(), KEYS_FILE)
20
+ }
21
+
22
+ /**
23
+ * Check if a key is available (env var or encrypted file).
24
+ */
25
+ export function hasKey(): boolean {
26
+ if (process.env.PRIVATE_KEY_WIF) return true
27
+ return existsSync(getKeysPath())
28
+ }
29
+
30
+ /**
31
+ * Load the private key from env or encrypted file.
32
+ *
33
+ * @param password - Required if loading from encrypted file
34
+ */
35
+ export async function loadKey(password?: string): Promise<PrivateKey> {
36
+ // Priority 1: Environment variable
37
+ const envWif = process.env.PRIVATE_KEY_WIF
38
+ if (envWif) {
39
+ return PrivateKey.fromWif(envWif)
40
+ }
41
+
42
+ // Priority 2: Encrypted file
43
+ const keysPath = getKeysPath()
44
+ if (!existsSync(keysPath)) {
45
+ throw new Error(
46
+ 'No key found. Run "1sat init" to set up your wallet, or set PRIVATE_KEY_WIF.',
47
+ )
48
+ }
49
+
50
+ if (!password) {
51
+ throw new Error(
52
+ 'Password required to decrypt key file. Pass --password or set ONESAT_PASSWORD.',
53
+ )
54
+ }
55
+
56
+ const encrypted = readFileSync(keysPath, 'utf8')
57
+ const backup = await decryptBackup(encrypted, password)
58
+ if (!('wif' in backup) || typeof backup.wif !== 'string') {
59
+ throw new Error('Key file does not contain a WIF key.')
60
+ }
61
+ return PrivateKey.fromWif(backup.wif)
62
+ }
63
+
64
+ /**
65
+ * Save a WIF-encoded private key to encrypted file.
66
+ */
67
+ export async function saveKey(wif: string, password: string): Promise<void> {
68
+ ensureConfigDir()
69
+
70
+ // Validate the WIF before saving
71
+ PrivateKey.fromWif(wif)
72
+
73
+ const payload: WifBackup = {
74
+ wif,
75
+ label: '1sat-cli',
76
+ createdAt: new Date().toISOString(),
77
+ }
78
+ const encrypted = await encryptBackup(payload, password)
79
+ const keysPath = getKeysPath()
80
+ writeFileSync(keysPath, encrypted, { mode: 0o600 })
81
+ }
82
+
83
+ /**
84
+ * Resolve a password from flag or environment variable.
85
+ */
86
+ export function resolvePassword(flagValue?: string): string | undefined {
87
+ return flagValue ?? process.env.ONESAT_PASSWORD
88
+ }