@1sat/cli 0.0.20 → 0.0.22
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 +1 -1
- package/src/args.ts +18 -0
- package/src/cli.ts +6 -1
- package/src/commands/config.ts +32 -2
- package/src/commands/init.ts +30 -10
- package/src/commands/remote.ts +336 -0
- package/src/commands/wallet.ts +281 -1
- package/src/config.ts +33 -4
- package/src/context.ts +33 -10
- package/src/help.ts +18 -1
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -82,6 +82,24 @@ export function extractFlag(args: string[], flag: string): string | undefined {
|
|
|
82
82
|
return args[idx + 1]
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Extract multiple values for a flag, supporting comma-delimited and multiple flag occurrences.
|
|
87
|
+
* Example: --tags a,b,c OR --tags a --tags b --tags c
|
|
88
|
+
*/
|
|
89
|
+
export function extractFlags(args: string[], flag: string): string[] {
|
|
90
|
+
const values: string[] = []
|
|
91
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
92
|
+
if (args[i] === flag) {
|
|
93
|
+
const value = args[i + 1]
|
|
94
|
+
if (value && !value.startsWith('--')) {
|
|
95
|
+
// Split comma-delimited values
|
|
96
|
+
values.push(...value.split(',').filter(v => v.length > 0))
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return values
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
/**
|
|
86
104
|
* Check if a boolean flag is present.
|
|
87
105
|
*/
|
package/src/cli.ts
CHANGED
|
@@ -8,13 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { parseGlobalFlags } from './args'
|
|
10
10
|
import { handleActionCommand } from './commands/action'
|
|
11
|
-
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
12
11
|
import { handleConfigCommand } from './commands/config'
|
|
13
12
|
import { handleIdentityCommand } from './commands/identity'
|
|
14
13
|
import { handleInitCommand } from './commands/init'
|
|
15
14
|
import { handleLocksCommand } from './commands/locks'
|
|
15
|
+
import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
16
16
|
import { handleOpnsCommand } from './commands/opns'
|
|
17
17
|
import { handleOrdinalsCommand } from './commands/ordinals'
|
|
18
|
+
import { handleRemoteCommand } from './commands/remote'
|
|
18
19
|
import { handleSocialCommand } from './commands/social'
|
|
19
20
|
import { handleSweepCommand } from './commands/sweep'
|
|
20
21
|
import { handleTokensCommand } from './commands/tokens'
|
|
@@ -49,6 +50,10 @@ async function main(): Promise<void> {
|
|
|
49
50
|
await handleConfigCommand(rest, flags)
|
|
50
51
|
break
|
|
51
52
|
|
|
53
|
+
case 'remote':
|
|
54
|
+
await handleRemoteCommand(rest, flags)
|
|
55
|
+
break
|
|
56
|
+
|
|
52
57
|
case 'wallet':
|
|
53
58
|
await handleWalletCommand(rest, flags)
|
|
54
59
|
break
|
package/src/commands/config.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Subcommands:
|
|
5
5
|
* show - Display current configuration
|
|
6
6
|
* set - Set a configuration value
|
|
7
|
+
* unset - Remove a configuration key
|
|
7
8
|
* path - Print config directory path
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -28,8 +29,8 @@ import {
|
|
|
28
29
|
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
29
30
|
'chain',
|
|
30
31
|
'dataDir',
|
|
31
|
-
'activeRemote',
|
|
32
32
|
'storageIdentityKey',
|
|
33
|
+
'monitorIntervalMinutes',
|
|
33
34
|
]
|
|
34
35
|
|
|
35
36
|
export async function handleConfigCommand(
|
|
@@ -43,12 +44,15 @@ export async function handleConfigCommand(
|
|
|
43
44
|
return configShow(opts)
|
|
44
45
|
case 'set':
|
|
45
46
|
return configSet(rest, opts)
|
|
47
|
+
case 'unset':
|
|
48
|
+
return configUnset(rest, opts)
|
|
46
49
|
case 'path':
|
|
47
50
|
return configPath(opts)
|
|
48
51
|
default:
|
|
49
52
|
printCommandHelp('config', {
|
|
50
53
|
show: 'Display current configuration',
|
|
51
54
|
set: 'Set a config value (e.g. 1sat config set chain test)',
|
|
55
|
+
unset: 'Remove a configuration key (e.g. 1sat config unset chain)',
|
|
52
56
|
path: 'Print config directory path',
|
|
53
57
|
})
|
|
54
58
|
if (subcommand && subcommand !== 'help') {
|
|
@@ -69,8 +73,8 @@ function configShow(opts: GlobalFlags): void {
|
|
|
69
73
|
printKeyValue({
|
|
70
74
|
chain: config.chain,
|
|
71
75
|
dataDir: config.dataDir,
|
|
72
|
-
activeRemote: config.activeRemote ?? '(not set)',
|
|
73
76
|
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
77
|
+
monitorIntervalMinutes: config.monitorIntervalMinutes,
|
|
74
78
|
})
|
|
75
79
|
console.log()
|
|
76
80
|
console.log(
|
|
@@ -98,11 +102,37 @@ function configSet(args: string[], opts: GlobalFlags): void {
|
|
|
98
102
|
if (key === 'chain' && value !== 'main' && value !== 'test') {
|
|
99
103
|
fatal("chain must be 'main' or 'test'")
|
|
100
104
|
}
|
|
105
|
+
if (key === 'monitorIntervalMinutes') {
|
|
106
|
+
const n = Number(value)
|
|
107
|
+
if (!Number.isFinite(n) || n < 0) {
|
|
108
|
+
fatal('monitorIntervalMinutes must be a non-negative number (0 to disable)')
|
|
109
|
+
}
|
|
110
|
+
}
|
|
101
111
|
|
|
102
112
|
const updated = updateConfig({ [key]: value })
|
|
103
113
|
output(opts.json ? updated : formatSuccess(`Set ${key} = ${value}`), opts)
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
function configUnset(args: string[], opts: GlobalFlags): void {
|
|
117
|
+
const [key] = args
|
|
118
|
+
|
|
119
|
+
if (!key) {
|
|
120
|
+
fatal(
|
|
121
|
+
`Usage: 1sat config unset <key>\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!SETTABLE_KEYS.includes(key as keyof OneSatCliConfig)) {
|
|
126
|
+
fatal(
|
|
127
|
+
`Unknown config key: ${key}\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// For other keys, use undefined
|
|
132
|
+
const updated = updateConfig({ [key]: undefined })
|
|
133
|
+
output(opts.json ? updated : formatSuccess(`Unset ${key}`), opts)
|
|
134
|
+
}
|
|
135
|
+
|
|
106
136
|
function configPath(opts: GlobalFlags): void {
|
|
107
137
|
const dir = getConfigDir()
|
|
108
138
|
output(opts.json ? { path: dir } : dir, opts)
|
package/src/commands/init.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 4. Config file creation
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { randomBytes } from 'node:crypto'
|
|
11
12
|
import { PrivateKey } from '@bsv/sdk'
|
|
12
13
|
import {
|
|
13
14
|
cancel,
|
|
@@ -162,18 +163,36 @@ export async function handleInitCommand(
|
|
|
162
163
|
// bitcoin-backup Touch ID not available — skip silently
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
// 4.
|
|
166
|
-
const storageId =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
// 4. Generate random storage identity key
|
|
167
|
+
const storageId = `1sat-cli-${randomBytes(8).toString('hex')}`
|
|
168
|
+
|
|
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,
|
|
170
173
|
})
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
174
193
|
}
|
|
175
194
|
|
|
176
|
-
//
|
|
195
|
+
// 6. Save everything
|
|
177
196
|
ensureConfigDir()
|
|
178
197
|
|
|
179
198
|
await saveKey(wif, pw as string)
|
|
@@ -194,7 +213,8 @@ export async function handleInitCommand(
|
|
|
194
213
|
saveConfig({
|
|
195
214
|
...loadConfig(),
|
|
196
215
|
chain: chain as 'main' | 'test',
|
|
197
|
-
storageIdentityKey: storageId
|
|
216
|
+
storageIdentityKey: storageId,
|
|
217
|
+
activeRemote,
|
|
198
218
|
})
|
|
199
219
|
|
|
200
220
|
const pk = PrivateKey.fromWif(wif)
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote storage management commands.
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* add - Add a remote as backup (validates with immediate sync)
|
|
6
|
+
* list - Show all remotes and their status
|
|
7
|
+
* delete - Remove a remote from the backup list
|
|
8
|
+
* set-active - Switch active storage to a remote or back to local
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { confirm, isCancel, text } from '@clack/prompts'
|
|
12
|
+
import type { GlobalFlags } from '../args'
|
|
13
|
+
import { loadConfig, saveConfig } from '../config'
|
|
14
|
+
import { loadContext } from '../context'
|
|
15
|
+
import { printCommandHelp } from '../help'
|
|
16
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
17
|
+
import { fatal, formatSuccess, formatWarning, output } from '../output'
|
|
18
|
+
|
|
19
|
+
export async function handleRemoteCommand(
|
|
20
|
+
args: string[],
|
|
21
|
+
opts: GlobalFlags,
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const [subcommand, ...rest] = args
|
|
24
|
+
|
|
25
|
+
switch (subcommand) {
|
|
26
|
+
case 'add':
|
|
27
|
+
return remoteAdd(rest, opts)
|
|
28
|
+
case 'list':
|
|
29
|
+
return remoteList(rest, opts)
|
|
30
|
+
case 'delete':
|
|
31
|
+
return remoteDelete(rest, opts)
|
|
32
|
+
case 'set-active':
|
|
33
|
+
return remoteSetActive(rest, opts)
|
|
34
|
+
default:
|
|
35
|
+
printCommandHelp('remote', {
|
|
36
|
+
add: 'Add a remote storage as backup (1sat remote add <url>)',
|
|
37
|
+
list: 'List all configured remotes and their status',
|
|
38
|
+
delete:
|
|
39
|
+
'Remove a remote from the backup list (1sat remote delete <url>)',
|
|
40
|
+
'set-active':
|
|
41
|
+
'Switch active storage (1sat remote set-active <url | local>)',
|
|
42
|
+
})
|
|
43
|
+
if (subcommand && subcommand !== 'help') {
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// remote add
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
54
|
+
let url = args[0]
|
|
55
|
+
|
|
56
|
+
if (!url) {
|
|
57
|
+
url = (await text({
|
|
58
|
+
message: 'Remote storage URL:',
|
|
59
|
+
validate(value) {
|
|
60
|
+
if (!value) return 'Required'
|
|
61
|
+
try {
|
|
62
|
+
new URL(value)
|
|
63
|
+
} catch {
|
|
64
|
+
return 'Invalid URL'
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
})) as string
|
|
68
|
+
if (isCancel(url)) {
|
|
69
|
+
fatal('Cancelled')
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
try {
|
|
73
|
+
new URL(url)
|
|
74
|
+
} catch {
|
|
75
|
+
fatal(`Invalid URL: ${url}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const privateKey = await loadKey(resolvePassword())
|
|
80
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
81
|
+
chain: opts.chain,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// Load config early — needed for storage identity key lookups
|
|
85
|
+
const config = loadConfig()
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// For backup-only, we use StorageClient via wallet-node
|
|
89
|
+
// biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
|
|
90
|
+
const { StorageClient } = await import('@1sat/wallet-node')
|
|
91
|
+
const wallet = walletResult.wallet as any
|
|
92
|
+
const client = new (StorageClient as any)(wallet, url)
|
|
93
|
+
await walletResult.storage.addWalletStorageProvider(client)
|
|
94
|
+
|
|
95
|
+
// When adding a backup, the remote may report itself as "active" which
|
|
96
|
+
// creates a conflicting active state. Re-assert local as active before syncing.
|
|
97
|
+
if (!walletResult.storage.isActiveEnabled) {
|
|
98
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
99
|
+
await walletResult.storage.setActive(localKey)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await walletResult.storage.updateBackups()
|
|
103
|
+
|
|
104
|
+
// Persist to config — connectivity will be validated on next monitor run
|
|
105
|
+
const existing = config.backups ?? []
|
|
106
|
+
if (!existing.includes(url)) {
|
|
107
|
+
saveConfig({ ...config, backups: [...existing, url] })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (opts.json) {
|
|
111
|
+
output({ url, status: 'added' }, opts)
|
|
112
|
+
} else {
|
|
113
|
+
console.log(formatSuccess(` Added ${url} as backup`))
|
|
114
|
+
console.log(
|
|
115
|
+
formatWarning(
|
|
116
|
+
' Note: Use "1sat remote set-active <url>" to make this remote the primary storage',
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
await destroy()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// remote list
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
130
|
+
const privateKey = await loadKey(resolvePassword())
|
|
131
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
132
|
+
chain: opts.chain,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const backups = walletResult.storage.getBackupStores?.() ?? []
|
|
137
|
+
const config = loadConfig()
|
|
138
|
+
|
|
139
|
+
// Use config for active determination — WalletStorageManager internal state
|
|
140
|
+
// can be misleading (a backup may appear as active after addWalletStorageProvider)
|
|
141
|
+
const isRemoteActive = Boolean(config.activeRemote)
|
|
142
|
+
|
|
143
|
+
if (opts.json) {
|
|
144
|
+
output(
|
|
145
|
+
{
|
|
146
|
+
activeStorage: isRemoteActive ? 'remote' : 'local',
|
|
147
|
+
backups: walletResult.storage.getAllStores?.() ?? [],
|
|
148
|
+
config: {
|
|
149
|
+
activeRemote: config.activeRemote ?? null,
|
|
150
|
+
backups: config.backups ?? [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
opts,
|
|
154
|
+
)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log()
|
|
159
|
+
console.log(` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`)
|
|
160
|
+
if (isRemoteActive) {
|
|
161
|
+
console.log(
|
|
162
|
+
` ${bold('Active Remote:')} ${config.activeRemote}`,
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
console.log()
|
|
166
|
+
if (backups.length === 0 && !config.backups?.length) {
|
|
167
|
+
console.log(' No remote storages configured')
|
|
168
|
+
} else {
|
|
169
|
+
console.log(` ${bold('Backups:')}`)
|
|
170
|
+
const known = new Set(config.backups ?? [])
|
|
171
|
+
for (const b of backups) {
|
|
172
|
+
const isKnown = known.has(b)
|
|
173
|
+
console.log(` ${isKnown ? '●' : '○'} ${b}`)
|
|
174
|
+
}
|
|
175
|
+
// Show configured but not yet connected
|
|
176
|
+
for (const url of config.backups ?? []) {
|
|
177
|
+
if (!backups.includes(url)) {
|
|
178
|
+
console.log(` ? ${url} (not connected)`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
console.log()
|
|
183
|
+
} finally {
|
|
184
|
+
await destroy()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// remote delete
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
async function remoteDelete(args: string[], opts: GlobalFlags): Promise<void> {
|
|
193
|
+
let url = args[0]
|
|
194
|
+
|
|
195
|
+
if (!url) {
|
|
196
|
+
url = (await text({
|
|
197
|
+
message: 'Remote storage URL to remove:',
|
|
198
|
+
validate(value) {
|
|
199
|
+
if (!value) return 'Required'
|
|
200
|
+
},
|
|
201
|
+
})) as string
|
|
202
|
+
if (isCancel(url)) {
|
|
203
|
+
fatal('Cancelled')
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const config = loadConfig()
|
|
208
|
+
const backups = config.backups ?? []
|
|
209
|
+
|
|
210
|
+
if (!backups.includes(url)) {
|
|
211
|
+
fatal(`Remote not found in config: ${url}`)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Confirm
|
|
215
|
+
const confirmed = await confirm({
|
|
216
|
+
message: `Remove ${url} from backups?`,
|
|
217
|
+
defaultValue: false,
|
|
218
|
+
})
|
|
219
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
220
|
+
fatal('Cancelled')
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
saveConfig({ ...config, backups: backups.filter((u) => u !== url) })
|
|
224
|
+
|
|
225
|
+
if (opts.json) {
|
|
226
|
+
output({ url, status: 'removed' }, opts)
|
|
227
|
+
} else {
|
|
228
|
+
console.log(formatSuccess(` Removed ${url} from backups`))
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ============================================================================
|
|
233
|
+
// remote set-active
|
|
234
|
+
// ============================================================================
|
|
235
|
+
|
|
236
|
+
async function remoteSetActive(
|
|
237
|
+
args: string[],
|
|
238
|
+
opts: GlobalFlags,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
let target = args[0]
|
|
241
|
+
|
|
242
|
+
if (!target) {
|
|
243
|
+
target = (await text({
|
|
244
|
+
message: 'Set active storage to (url or "local"):',
|
|
245
|
+
validate(value) {
|
|
246
|
+
if (!value) return 'Required'
|
|
247
|
+
},
|
|
248
|
+
})) as string
|
|
249
|
+
if (isCancel(target)) {
|
|
250
|
+
fatal('Cancelled')
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const config = loadConfig()
|
|
255
|
+
|
|
256
|
+
if (target === 'local') {
|
|
257
|
+
// Switch back to local
|
|
258
|
+
if (!config.activeRemote && !config.backups?.length) {
|
|
259
|
+
fatal('No remote storages configured')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const privateKey = await loadKey(resolvePassword())
|
|
263
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
264
|
+
chain: opts.chain,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
// Find the local storage's storageIdentityKey
|
|
269
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
270
|
+
|
|
271
|
+
if (opts.json) {
|
|
272
|
+
output({ target: 'local', status: 'migrating' }, opts)
|
|
273
|
+
} else {
|
|
274
|
+
console.log(' Switching active storage to local...')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
await walletResult.storage.setActive(localKey)
|
|
278
|
+
|
|
279
|
+
// Clear activeRemote from config
|
|
280
|
+
saveConfig({ ...config, activeRemote: undefined })
|
|
281
|
+
|
|
282
|
+
if (opts.json) {
|
|
283
|
+
output(
|
|
284
|
+
{ target: 'local', status: 'active', storageIdentityKey: localKey },
|
|
285
|
+
opts,
|
|
286
|
+
)
|
|
287
|
+
} else {
|
|
288
|
+
console.log(formatSuccess(' Local storage is now active'))
|
|
289
|
+
}
|
|
290
|
+
} finally {
|
|
291
|
+
await destroy()
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
// Switch to a remote
|
|
295
|
+
// Validate URL
|
|
296
|
+
try {
|
|
297
|
+
new URL(target)
|
|
298
|
+
} catch {
|
|
299
|
+
fatal(`Invalid URL: ${target}`)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const privateKey = await loadKey(resolvePassword())
|
|
303
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
304
|
+
chain: opts.chain,
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
if (opts.json) {
|
|
309
|
+
output({ target, status: 'migrating' }, opts)
|
|
310
|
+
} else {
|
|
311
|
+
console.log(` Switching active storage to ${target}...`)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await walletResult.migrateRemote(target)
|
|
315
|
+
|
|
316
|
+
// Persist to config
|
|
317
|
+
saveConfig({ ...config, activeRemote: target })
|
|
318
|
+
|
|
319
|
+
if (opts.json) {
|
|
320
|
+
output({ target, status: 'active' }, opts)
|
|
321
|
+
} else {
|
|
322
|
+
console.log(formatSuccess(` ${target} is now active`))
|
|
323
|
+
}
|
|
324
|
+
} finally {
|
|
325
|
+
await destroy()
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// Helpers
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
function bold(s: string): string {
|
|
335
|
+
return `\x1b[1m${s}\x1b[0m`
|
|
336
|
+
}
|
package/src/commands/wallet.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Wallet commands - balance, address, send, send-all, info.
|
|
3
|
+
* BRC-100 interface commands - list-outputs, relinquish-output, list-actions, etc.
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { deriveDepositAddresses, sendAllBsv, sendBsv } from '@1sat/actions'
|
|
6
7
|
import { confirm, isCancel } from '@clack/prompts'
|
|
7
8
|
import type { GlobalFlags } from '../args'
|
|
8
|
-
import { extractFlag } from '../args'
|
|
9
|
+
import { extractFlag, extractFlags } from '../args'
|
|
9
10
|
import { loadContext } from '../context'
|
|
10
11
|
import { printCommandHelp } from '../help'
|
|
11
12
|
import { loadKey, resolvePassword } from '../keys'
|
|
@@ -28,6 +29,22 @@ export async function handleWalletCommand(
|
|
|
28
29
|
return walletSendAll(rest, opts)
|
|
29
30
|
case 'info':
|
|
30
31
|
return walletInfo(rest, opts)
|
|
32
|
+
case 'list-outputs':
|
|
33
|
+
return walletListOutputs(rest, opts)
|
|
34
|
+
case 'relinquish-output':
|
|
35
|
+
return walletRelinquishOutput(rest, opts)
|
|
36
|
+
case 'list-actions':
|
|
37
|
+
return walletListActions(rest, opts)
|
|
38
|
+
case 'create-action':
|
|
39
|
+
return walletCreateAction(rest, opts)
|
|
40
|
+
case 'sign-action':
|
|
41
|
+
return walletSignAction(rest, opts)
|
|
42
|
+
case 'abort-action':
|
|
43
|
+
return walletAbortAction(rest, opts)
|
|
44
|
+
case 'list-certificates':
|
|
45
|
+
return walletListCertificates(rest, opts)
|
|
46
|
+
case 'relinquish-certificate':
|
|
47
|
+
return walletRelinquishCertificate(rest, opts)
|
|
31
48
|
default:
|
|
32
49
|
printCommandHelp('wallet', {
|
|
33
50
|
balance: 'Show wallet balance in satoshis',
|
|
@@ -35,6 +52,14 @@ export async function handleWalletCommand(
|
|
|
35
52
|
send: 'Send BSV to an address (--to <addr> --sats <amount>)',
|
|
36
53
|
'send-all': 'Send all BSV to an address (--to <addr>)',
|
|
37
54
|
info: 'Show wallet info (address, balance, network)',
|
|
55
|
+
'list-outputs': 'List wallet outputs (--basket <name> [--tags <t1,t2>] [--limit N])',
|
|
56
|
+
'relinquish-output': 'Remove output from basket (--basket <name> --output <txid.vout>)',
|
|
57
|
+
'list-actions': 'List wallet actions [--labels <l1,l2>] [--limit N]',
|
|
58
|
+
'create-action': 'Create action (JSON args)',
|
|
59
|
+
'sign-action': 'Sign action (JSON args)',
|
|
60
|
+
'abort-action': 'Abort action (--reference <ref>)',
|
|
61
|
+
'list-certificates': 'List certificates',
|
|
62
|
+
'relinquish-certificate': 'Relinquish certificate (--type <t> --serialNumber <s> --certifier <c>)',
|
|
38
63
|
})
|
|
39
64
|
if (subcommand && subcommand !== 'help') {
|
|
40
65
|
process.exit(1)
|
|
@@ -216,3 +241,258 @@ async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
216
241
|
await destroy()
|
|
217
242
|
}
|
|
218
243
|
}
|
|
244
|
+
|
|
245
|
+
// BRC-100 Interface Commands
|
|
246
|
+
|
|
247
|
+
async function walletListOutputs(args: string[], opts: GlobalFlags): Promise<void> {
|
|
248
|
+
const basket = extractFlag(args, '--basket')
|
|
249
|
+
if (!basket) fatal('Missing required --basket <name>')
|
|
250
|
+
|
|
251
|
+
const tags = extractFlags(args, '--tags')
|
|
252
|
+
const limitStr = extractFlag(args, '--limit')
|
|
253
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 10
|
|
254
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
255
|
+
fatal('--limit must be between 1 and 10000')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const privateKey = await loadKey(resolvePassword())
|
|
259
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
260
|
+
chain: opts.chain,
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const listArgs: Parameters<typeof ctx.wallet.listOutputs>[0] = {
|
|
265
|
+
basket,
|
|
266
|
+
limit,
|
|
267
|
+
}
|
|
268
|
+
if (tags.length > 0) {
|
|
269
|
+
listArgs.tags = tags
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = await ctx.wallet.listOutputs(listArgs)
|
|
273
|
+
|
|
274
|
+
if (opts.json) {
|
|
275
|
+
output(result, opts)
|
|
276
|
+
} else {
|
|
277
|
+
console.log(`\n${result.totalOutputs} total outputs in basket '${basket}':\n`)
|
|
278
|
+
for (const out of result.outputs) {
|
|
279
|
+
const tags = out.tags?.join(', ') || 'none'
|
|
280
|
+
console.log(` ${out.outpoint} | ${out.satoshis} sats | tags: ${tags}`)
|
|
281
|
+
}
|
|
282
|
+
console.log()
|
|
283
|
+
}
|
|
284
|
+
} finally {
|
|
285
|
+
await destroy()
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function walletRelinquishOutput(
|
|
290
|
+
args: string[],
|
|
291
|
+
opts: GlobalFlags,
|
|
292
|
+
): Promise<void> {
|
|
293
|
+
const basket = extractFlag(args, '--basket')
|
|
294
|
+
const outpoint = extractFlag(args, '--output')
|
|
295
|
+
|
|
296
|
+
if (!basket) fatal('Missing required --basket <name>')
|
|
297
|
+
if (!outpoint) fatal('Missing required --output <txid.vout>')
|
|
298
|
+
|
|
299
|
+
// Validate output format (txid.vout)
|
|
300
|
+
if (!outpoint.includes('.') || outpoint.split('.').length !== 2) {
|
|
301
|
+
fatal('Invalid --output format. Expected: txid.vout (e.g., abc123...0)')
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const privateKey = await loadKey(resolvePassword())
|
|
305
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
306
|
+
chain: opts.chain,
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const result = await ctx.wallet.relinquishOutput({ basket, output: outpoint })
|
|
311
|
+
|
|
312
|
+
if (opts.json) {
|
|
313
|
+
output(result, opts)
|
|
314
|
+
} else {
|
|
315
|
+
output({ relinquished: true, basket, output: outpoint }, opts)
|
|
316
|
+
}
|
|
317
|
+
} finally {
|
|
318
|
+
await destroy()
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function walletListActions(args: string[], opts: GlobalFlags): Promise<void> {
|
|
323
|
+
const labels = extractFlags(args, '--labels')
|
|
324
|
+
const limitStr = extractFlag(args, '--limit')
|
|
325
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 10
|
|
326
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
327
|
+
fatal('--limit must be between 1 and 10000')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const privateKey = await loadKey(resolvePassword())
|
|
331
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
332
|
+
chain: opts.chain,
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const listArgs: Parameters<typeof ctx.wallet.listActions>[0] = {
|
|
337
|
+
labels: labels.length > 0 ? labels : ['*'], // Default to all labels if none specified
|
|
338
|
+
limit,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const result = await ctx.wallet.listActions(listArgs)
|
|
342
|
+
|
|
343
|
+
if (opts.json) {
|
|
344
|
+
output(result, opts)
|
|
345
|
+
} else {
|
|
346
|
+
console.log(`\n${result.totalActions} total actions:\n`)
|
|
347
|
+
for (const action of result.actions) {
|
|
348
|
+
console.log(` ${action.txid || 'pending'} | status: ${action.status}`)
|
|
349
|
+
}
|
|
350
|
+
console.log()
|
|
351
|
+
}
|
|
352
|
+
} finally {
|
|
353
|
+
await destroy()
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function walletCreateAction(args: string[], opts: GlobalFlags): Promise<void> {
|
|
358
|
+
const jsonInput = args[0]
|
|
359
|
+
|
|
360
|
+
if (!jsonInput) fatal('Missing JSON arguments. Usage: wallet create-action \'{...}\'')
|
|
361
|
+
|
|
362
|
+
let actionArgs: Parameters<typeof ctx.wallet.createAction>[0]
|
|
363
|
+
try {
|
|
364
|
+
actionArgs = JSON.parse(jsonInput)
|
|
365
|
+
} catch {
|
|
366
|
+
fatal(`Invalid JSON: ${jsonInput}`)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const privateKey = await loadKey(resolvePassword())
|
|
370
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
371
|
+
chain: opts.chain,
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const result = await ctx.wallet.createAction(actionArgs)
|
|
376
|
+
output(result, opts)
|
|
377
|
+
} finally {
|
|
378
|
+
await destroy()
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function walletSignAction(args: string[], opts: GlobalFlags): Promise<void> {
|
|
383
|
+
const jsonInput = args[0]
|
|
384
|
+
|
|
385
|
+
if (!jsonInput) fatal('Missing JSON arguments. Usage: wallet sign-action \'{...}\'')
|
|
386
|
+
|
|
387
|
+
let signArgs: Parameters<typeof ctx.wallet.signAction>[0]
|
|
388
|
+
try {
|
|
389
|
+
signArgs = JSON.parse(jsonInput)
|
|
390
|
+
} catch {
|
|
391
|
+
fatal(`Invalid JSON: ${jsonInput}`)
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const privateKey = await loadKey(resolvePassword())
|
|
395
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
396
|
+
chain: opts.chain,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const result = await ctx.wallet.signAction(signArgs)
|
|
401
|
+
output(result, opts)
|
|
402
|
+
} finally {
|
|
403
|
+
await destroy()
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function walletAbortAction(args: string[], opts: GlobalFlags): Promise<void> {
|
|
408
|
+
const reference = extractFlag(args, '--reference')
|
|
409
|
+
|
|
410
|
+
if (!reference) fatal('Missing required --reference <ref>')
|
|
411
|
+
|
|
412
|
+
const privateKey = await loadKey(resolvePassword())
|
|
413
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
414
|
+
chain: opts.chain,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
const result = await ctx.wallet.abortAction({ reference })
|
|
419
|
+
output(result, opts)
|
|
420
|
+
} finally {
|
|
421
|
+
await destroy()
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function walletListCertificates(
|
|
426
|
+
args: string[],
|
|
427
|
+
opts: GlobalFlags,
|
|
428
|
+
): Promise<void> {
|
|
429
|
+
const certifiers = extractFlags(args, '--certifiers')
|
|
430
|
+
const types = extractFlags(args, '--types')
|
|
431
|
+
const limitStr = extractFlag(args, '--limit')
|
|
432
|
+
const limit = limitStr ? parseInt(limitStr, 10) : 10
|
|
433
|
+
|
|
434
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
435
|
+
fatal('--limit must be between 1 and 10000')
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const privateKey = await loadKey(resolvePassword())
|
|
439
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
440
|
+
chain: opts.chain,
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
const listArgs: Parameters<typeof ctx.wallet.listCertificates>[0] = {
|
|
445
|
+
certifiers: certifiers.length > 0 ? certifiers : [],
|
|
446
|
+
types: types.length > 0 ? types : [],
|
|
447
|
+
limit,
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const result = await ctx.wallet.listCertificates(listArgs)
|
|
451
|
+
|
|
452
|
+
if (opts.json) {
|
|
453
|
+
output(result, opts)
|
|
454
|
+
} else {
|
|
455
|
+
console.log(`\n${result.totalCertificates} total certificates:\n`)
|
|
456
|
+
for (const cert of result.certificates) {
|
|
457
|
+
console.log(` ${cert.type} | ${cert.serialNumber} | ${cert.certifier}`)
|
|
458
|
+
}
|
|
459
|
+
console.log()
|
|
460
|
+
}
|
|
461
|
+
} finally {
|
|
462
|
+
await destroy()
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function walletRelinquishCertificate(
|
|
467
|
+
args: string[],
|
|
468
|
+
opts: GlobalFlags,
|
|
469
|
+
): Promise<void> {
|
|
470
|
+
const type = extractFlag(args, '--type')
|
|
471
|
+
const serialNumber = extractFlag(args, '--serialNumber')
|
|
472
|
+
const certifier = extractFlag(args, '--certifier')
|
|
473
|
+
|
|
474
|
+
if (!type) fatal('Missing required --type <type>')
|
|
475
|
+
if (!serialNumber) fatal('Missing required --serialNumber <serial>')
|
|
476
|
+
if (!certifier) fatal('Missing required --certifier <certifier>')
|
|
477
|
+
|
|
478
|
+
const privateKey = await loadKey(resolvePassword())
|
|
479
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
480
|
+
chain: opts.chain,
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
const result = await ctx.wallet.relinquishCertificate({
|
|
485
|
+
type,
|
|
486
|
+
serialNumber,
|
|
487
|
+
certifier,
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
if (opts.json) {
|
|
491
|
+
output(result, opts)
|
|
492
|
+
} else {
|
|
493
|
+
output({ relinquished: true, type, serialNumber, certifier }, opts)
|
|
494
|
+
}
|
|
495
|
+
} finally {
|
|
496
|
+
await destroy()
|
|
497
|
+
}
|
|
498
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config management for ~/.1sat/
|
|
2
|
+
* Config management for ~/.1sat/cli/
|
|
3
3
|
*
|
|
4
4
|
* Handles persistent configuration on disk with secure file permissions.
|
|
5
5
|
*/
|
|
@@ -8,7 +8,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
9
|
import { join } from 'node:path'
|
|
10
10
|
|
|
11
|
-
const CONFIG_DIR = join(homedir(), '.1sat')
|
|
11
|
+
const CONFIG_DIR = join(homedir(), '.1sat', 'cli')
|
|
12
12
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
13
13
|
|
|
14
14
|
export interface OneSatCliConfig {
|
|
@@ -16,19 +16,48 @@ export interface OneSatCliConfig {
|
|
|
16
16
|
chain: 'main' | 'test'
|
|
17
17
|
/** Data directory for wallet databases */
|
|
18
18
|
dataDir: string
|
|
19
|
-
/**
|
|
19
|
+
/** Primary remote storage URL (active or backup) */
|
|
20
20
|
activeRemote?: string
|
|
21
|
+
/** Backup remote storage URLs */
|
|
22
|
+
backups?: string[]
|
|
21
23
|
/** Storage identity key for wallet persistence */
|
|
22
24
|
storageIdentityKey?: string
|
|
25
|
+
/** How often to run the monitor refresh (minutes). 0 disables auto-refresh. */
|
|
26
|
+
monitorIntervalMinutes: number
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
const DEFAULT_CONFIG: OneSatCliConfig = {
|
|
26
30
|
chain: 'main',
|
|
27
31
|
dataDir: join(CONFIG_DIR, 'data'),
|
|
32
|
+
monitorIntervalMinutes: 5,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Monitor state — stored separately so we don't rewrite config.json on every command
|
|
36
|
+
const MONITOR_STATE_FILE = join(CONFIG_DIR, 'monitor-state.json')
|
|
37
|
+
|
|
38
|
+
export interface MonitorState {
|
|
39
|
+
lastMonitorRun: number // unix ms timestamp
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function loadMonitorState(): MonitorState {
|
|
43
|
+
if (!existsSync(MONITOR_STATE_FILE)) return { lastMonitorRun: 0 }
|
|
44
|
+
try {
|
|
45
|
+
const raw = readFileSync(MONITOR_STATE_FILE, 'utf8')
|
|
46
|
+
return JSON.parse(raw)
|
|
47
|
+
} catch {
|
|
48
|
+
return { lastMonitorRun: 0 }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveMonitorState(state: MonitorState): void {
|
|
53
|
+
ensureConfigDir()
|
|
54
|
+
writeFileSync(MONITOR_STATE_FILE, JSON.stringify(state, null, 2), {
|
|
55
|
+
mode: 0o600,
|
|
56
|
+
})
|
|
28
57
|
}
|
|
29
58
|
|
|
30
59
|
/**
|
|
31
|
-
* Ensure ~/.1sat/ exists with secure permissions.
|
|
60
|
+
* Ensure ~/.1sat/cli/ exists with secure permissions.
|
|
32
61
|
*/
|
|
33
62
|
export function ensureConfigDir(): void {
|
|
34
63
|
if (!existsSync(CONFIG_DIR)) {
|
package/src/context.ts
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
import { type OneSatContext, createContext } from '@1sat/actions'
|
|
8
8
|
import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
9
9
|
import type { PrivateKey } from '@bsv/sdk'
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ensureDataDir,
|
|
12
|
+
loadConfig,
|
|
13
|
+
loadMonitorState,
|
|
14
|
+
saveMonitorState,
|
|
15
|
+
} from './config'
|
|
11
16
|
|
|
12
17
|
/** Extended context that includes cleanup */
|
|
13
18
|
export interface CliContext {
|
|
@@ -16,13 +21,35 @@ export interface CliContext {
|
|
|
16
21
|
destroy: () => Promise<void>
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Run the monitor once if the interval has elapsed.
|
|
26
|
+
* This is called lazily by commands that need current state.
|
|
27
|
+
*/
|
|
28
|
+
async function runMonitorIfStale(
|
|
29
|
+
walletResult: NodeWalletResult,
|
|
30
|
+
intervalMinutes: number,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (!walletResult.monitor) return // remote active, monitor runs remotely
|
|
33
|
+
if (intervalMinutes === 0) return // disabled by user
|
|
34
|
+
|
|
35
|
+
const state = loadMonitorState()
|
|
36
|
+
const now = Date.now()
|
|
37
|
+
const elapsed = now - state.lastMonitorRun
|
|
38
|
+
const intervalMs = intervalMinutes * 60 * 1000
|
|
39
|
+
|
|
40
|
+
if (elapsed >= intervalMs) {
|
|
41
|
+
await walletResult.monitor.runOnce()
|
|
42
|
+
saveMonitorState({ lastMonitorRun: Date.now() })
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
19
46
|
/**
|
|
20
47
|
* Create a fully initialized OneSatContext for CLI use.
|
|
21
48
|
*
|
|
22
49
|
* Sets up:
|
|
23
50
|
* - Node wallet with SQLite storage
|
|
24
51
|
* - 1Sat services for API access
|
|
25
|
-
* - Monitor for transaction lifecycle
|
|
52
|
+
* - Monitor for transaction lifecycle (lazy, interval-based)
|
|
26
53
|
*/
|
|
27
54
|
export async function loadContext(
|
|
28
55
|
privateKey: PrivateKey,
|
|
@@ -37,17 +64,13 @@ export async function loadContext(
|
|
|
37
64
|
privateKey,
|
|
38
65
|
chain: opts.chain,
|
|
39
66
|
storageIdentityKey,
|
|
40
|
-
|
|
41
|
-
client: 'better-sqlite3',
|
|
42
|
-
connection: {
|
|
43
|
-
filename: `${dataDir}/wallet-${opts.chain}.db`,
|
|
44
|
-
},
|
|
45
|
-
useNullAsDefault: true,
|
|
46
|
-
},
|
|
67
|
+
filename: `${dataDir}/wallet-${opts.chain}.db`,
|
|
47
68
|
activeRemote: config.activeRemote,
|
|
69
|
+
backups: config.backups,
|
|
48
70
|
})
|
|
49
71
|
|
|
50
|
-
|
|
72
|
+
// Run monitor once if interval has elapsed (lazy refresh)
|
|
73
|
+
await runMonitorIfStale(walletResult, config.monitorIntervalMinutes)
|
|
51
74
|
|
|
52
75
|
const ctx = createContext(walletResult.wallet, {
|
|
53
76
|
services: walletResult.services,
|
package/src/help.ts
CHANGED
|
@@ -38,6 +38,7 @@ ${bold('Usage:')}
|
|
|
38
38
|
${bold('Setup:')}
|
|
39
39
|
${cyan('init')} Interactive wallet setup wizard
|
|
40
40
|
${cyan('config')} <subcommand> Manage configuration
|
|
41
|
+
${cyan('remote')} <subcommand> Manage remote storage
|
|
41
42
|
|
|
42
43
|
${bold('Wallet:')}
|
|
43
44
|
${cyan('wallet balance')} Show wallet balance
|
|
@@ -46,6 +47,22 @@ ${bold('Wallet:')}
|
|
|
46
47
|
${cyan('wallet send-all')} Send all BSV to an address
|
|
47
48
|
${cyan('wallet info')} Show wallet info
|
|
48
49
|
|
|
50
|
+
${bold('Wallet (BRC-100 Interface):')}
|
|
51
|
+
${cyan('wallet list-outputs')} List outputs in basket
|
|
52
|
+
${dim('--basket <name> [--tags <t1,t2>] [--limit N]')}
|
|
53
|
+
${cyan('wallet relinquish-output')} Remove output from basket
|
|
54
|
+
${dim('--basket <name> --output <txid.vout>')}
|
|
55
|
+
${cyan('wallet list-actions')} List wallet actions
|
|
56
|
+
${dim('[--labels <l1,l2>] [--limit N]')}
|
|
57
|
+
${cyan('wallet create-action')} Create raw action (JSON args)
|
|
58
|
+
${cyan('wallet sign-action')} Sign raw action (JSON args)
|
|
59
|
+
${cyan('wallet abort-action')} Abort pending action
|
|
60
|
+
${dim('--reference <ref>')}
|
|
61
|
+
${cyan('wallet list-certificates')} List certificates
|
|
62
|
+
${dim('[--certifiers <c1,c2>] [--types <t1,t2>] [--limit N]')}
|
|
63
|
+
${cyan('wallet relinquish-certificate')} Relinquish certificate
|
|
64
|
+
${dim('--type <t> --serialNumber <s> --certifier <c>')}
|
|
65
|
+
|
|
49
66
|
${bold('Ordinals:')}
|
|
50
67
|
${cyan('ordinals list')} List owned ordinals
|
|
51
68
|
${cyan('ordinals mint')} Mint a new ordinal inscription
|
|
@@ -100,7 +117,7 @@ ${bold('Environment Variables:')}
|
|
|
100
117
|
${dim('PRIVATE_KEY_WIF')} Private key (bypasses encrypted keyfile)
|
|
101
118
|
${dim('ONESAT_PASSWORD')} Password for encrypted keyfile
|
|
102
119
|
|
|
103
|
-
${bold('Config:')} ~/.1sat/
|
|
120
|
+
${bold('Config:')} ~/.1sat/cli/
|
|
104
121
|
`)
|
|
105
122
|
}
|
|
106
123
|
|