@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.
- package/bin/1sat +0 -0
- package/package.json +32 -0
- package/src/args.ts +100 -0
- package/src/cli.ts +105 -0
- package/src/commands/action.ts +79 -0
- package/src/commands/config.ts +109 -0
- package/src/commands/identity.ts +133 -0
- package/src/commands/init.ts +172 -0
- package/src/commands/locks.ts +126 -0
- package/src/commands/opns.ts +73 -0
- package/src/commands/ordinals.ts +355 -0
- package/src/commands/social.ts +56 -0
- package/src/commands/sweep.ts +54 -0
- package/src/commands/tokens.ts +194 -0
- package/src/commands/tx.ts +65 -0
- package/src/commands/wallet.ts +218 -0
- package/src/config.ts +97 -0
- package/src/context.ts +63 -0
- package/src/help.ts +117 -0
- package/src/keys.ts +88 -0
- package/src/output.ts +82 -0
|
@@ -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
|
+
}
|