@1sat/cli 0.0.18 → 0.0.21
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 +3 -3
- package/src/cli.ts +6 -1
- package/src/commands/config.ts +32 -2
- package/src/commands/init.ts +28 -1
- package/src/commands/opns.ts +2 -1
- package/src/commands/ordinals.ts +2 -1
- package/src/commands/remote.ts +334 -0
- package/src/commands/sweep.ts +25 -41
- package/src/commands/tokens.ts +5 -4
- package/src/config.ts +33 -4
- package/src/context.ts +33 -10
- package/src/help.ts +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1sat/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.21",
|
|
4
4
|
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/cli.ts",
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"keywords": ["1sat", "bsv", "ordinals", "cli"],
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@1sat/actions": "0.0.
|
|
20
|
-
"@1sat/client": "0.0.
|
|
19
|
+
"@1sat/actions": "0.0.82",
|
|
20
|
+
"@1sat/client": "0.0.20",
|
|
21
21
|
"@1sat/types": "0.0.14",
|
|
22
22
|
"@1sat/wallet-node": ">=0.0.13",
|
|
23
23
|
"@bsv/sdk": "^2.0.6",
|
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
|
@@ -173,7 +173,33 @@ export async function handleInitCommand(
|
|
|
173
173
|
process.exit(0)
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
// 5.
|
|
176
|
+
// 5. Optional: remote storage configuration
|
|
177
|
+
const useRemote = await confirm({
|
|
178
|
+
message: 'Configure remote storage? (remote is active, local is backup)',
|
|
179
|
+
defaultValue: false,
|
|
180
|
+
})
|
|
181
|
+
let activeRemote: string | undefined
|
|
182
|
+
|
|
183
|
+
if (useRemote) {
|
|
184
|
+
const url = await text({
|
|
185
|
+
message: 'Primary remote storage URL:',
|
|
186
|
+
validate(value) {
|
|
187
|
+
if (!value) return 'Required'
|
|
188
|
+
try {
|
|
189
|
+
new URL(value)
|
|
190
|
+
} catch {
|
|
191
|
+
return 'Invalid URL'
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
if (isCancel(url)) {
|
|
196
|
+
cancel('Setup cancelled.')
|
|
197
|
+
process.exit(0)
|
|
198
|
+
}
|
|
199
|
+
activeRemote = url as string
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 6. Save everything
|
|
177
203
|
ensureConfigDir()
|
|
178
204
|
|
|
179
205
|
await saveKey(wif, pw as string)
|
|
@@ -195,6 +221,7 @@ export async function handleInitCommand(
|
|
|
195
221
|
...loadConfig(),
|
|
196
222
|
chain: chain as 'main' | 'test',
|
|
197
223
|
storageIdentityKey: storageId as string,
|
|
224
|
+
activeRemote,
|
|
198
225
|
})
|
|
199
226
|
|
|
200
227
|
const pk = PrivateKey.fromWif(wif)
|
package/src/commands/opns.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
|
+
getDisplayValue,
|
|
8
9
|
getOpnsNames,
|
|
9
10
|
opnsDeregister as opnsDeregisterAction,
|
|
10
11
|
opnsRegister as opnsRegisterAction,
|
|
@@ -149,7 +150,7 @@ async function opnsLookup(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
149
150
|
}
|
|
150
151
|
|
|
151
152
|
for (const o of result.outputs) {
|
|
152
|
-
const nameTag = o
|
|
153
|
+
const nameTag = getDisplayValue(o, 'name', 'name') ?? ''
|
|
153
154
|
const publishedTag = o.tags?.find((t) => t === 'opns:published')
|
|
154
155
|
const status = publishedTag ? 'registered' : 'unregistered'
|
|
155
156
|
|
package/src/commands/ordinals.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { basename, extname } from 'node:path'
|
|
|
7
7
|
import {
|
|
8
8
|
cancelListing,
|
|
9
9
|
deriveDepositAddresses,
|
|
10
|
+
getDisplayValue,
|
|
10
11
|
getOrdinals,
|
|
11
12
|
inscribe,
|
|
12
13
|
listOrdinal,
|
|
@@ -83,7 +84,7 @@ async function ordinalsList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
83
84
|
o.tags?.find((t) => t.startsWith('type:'))?.slice(5) ?? 'unknown'
|
|
84
85
|
const originTag =
|
|
85
86
|
o.tags?.find((t) => t.startsWith('origin:'))?.slice(7) ?? ''
|
|
86
|
-
const nameTag = o
|
|
87
|
+
const nameTag = getDisplayValue(o, 'name', 'name') ?? ''
|
|
87
88
|
|
|
88
89
|
console.log(
|
|
89
90
|
` ${formatValue(o.outpoint)} ${formatLabel(typeTag)}${nameTag ? ` ${nameTag}` : ''}${originTag ? ` origin:${originTag}` : ''}`,
|
|
@@ -0,0 +1,334 @@
|
|
|
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
|
+
try {
|
|
85
|
+
// For backup-only, we use StorageClient via wallet-node
|
|
86
|
+
// biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
|
|
87
|
+
const { StorageClient } = await import('@1sat/wallet-node')
|
|
88
|
+
const wallet = walletResult.wallet as any
|
|
89
|
+
const client = new (StorageClient as any)(wallet, url)
|
|
90
|
+
await walletResult.storage.addWalletStorageProvider(client)
|
|
91
|
+
|
|
92
|
+
// When adding a backup, the remote may report itself as "active" which
|
|
93
|
+
// creates a conflicting active state. Re-assert local as active before syncing.
|
|
94
|
+
if (!walletResult.storage.isActiveEnabled) {
|
|
95
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
96
|
+
await walletResult.storage.setActive(localKey)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await walletResult.storage.updateBackups()
|
|
100
|
+
|
|
101
|
+
// Persist to config — connectivity will be validated on next monitor run
|
|
102
|
+
const config = loadConfig()
|
|
103
|
+
const existing = config.backups ?? []
|
|
104
|
+
if (!existing.includes(url)) {
|
|
105
|
+
saveConfig({ ...config, backups: [...existing, url] })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (opts.json) {
|
|
109
|
+
output({ url, status: 'added' }, opts)
|
|
110
|
+
} else {
|
|
111
|
+
console.log(formatSuccess(` Added ${url} as backup`))
|
|
112
|
+
console.log(
|
|
113
|
+
formatWarning(
|
|
114
|
+
' Note: Use "1sat remote set-active <url>" to make this remote the primary storage',
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
} finally {
|
|
119
|
+
await destroy()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// remote list
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
128
|
+
const privateKey = await loadKey(resolvePassword())
|
|
129
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
130
|
+
chain: opts.chain,
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const backups = walletResult.storage.getBackupStores?.() ?? []
|
|
135
|
+
const config = loadConfig()
|
|
136
|
+
|
|
137
|
+
// Use config for active determination — WalletStorageManager internal state
|
|
138
|
+
// can be misleading (a backup may appear as active after addWalletStorageProvider)
|
|
139
|
+
const isRemoteActive = Boolean(config.activeRemote)
|
|
140
|
+
|
|
141
|
+
if (opts.json) {
|
|
142
|
+
output(
|
|
143
|
+
{
|
|
144
|
+
activeStorage: isRemoteActive ? 'remote' : 'local',
|
|
145
|
+
backups: walletResult.storage.getAllStores?.() ?? [],
|
|
146
|
+
config: {
|
|
147
|
+
activeRemote: config.activeRemote ?? null,
|
|
148
|
+
backups: config.backups ?? [],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
opts,
|
|
152
|
+
)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log()
|
|
157
|
+
console.log(` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`)
|
|
158
|
+
if (isRemoteActive) {
|
|
159
|
+
console.log(
|
|
160
|
+
` ${bold('Active Remote:')} ${config.activeRemote}`,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
console.log()
|
|
164
|
+
if (backups.length === 0 && !config.backups?.length) {
|
|
165
|
+
console.log(' No remote storages configured')
|
|
166
|
+
} else {
|
|
167
|
+
console.log(` ${bold('Backups:')}`)
|
|
168
|
+
const known = new Set(config.backups ?? [])
|
|
169
|
+
for (const b of backups) {
|
|
170
|
+
const isKnown = known.has(b)
|
|
171
|
+
console.log(` ${isKnown ? '●' : '○'} ${b}`)
|
|
172
|
+
}
|
|
173
|
+
// Show configured but not yet connected
|
|
174
|
+
for (const url of config.backups ?? []) {
|
|
175
|
+
if (!backups.includes(url)) {
|
|
176
|
+
console.log(` ? ${url} (not connected)`)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log()
|
|
181
|
+
} finally {
|
|
182
|
+
await destroy()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ============================================================================
|
|
187
|
+
// remote delete
|
|
188
|
+
// ============================================================================
|
|
189
|
+
|
|
190
|
+
async function remoteDelete(args: string[], opts: GlobalFlags): Promise<void> {
|
|
191
|
+
let url = args[0]
|
|
192
|
+
|
|
193
|
+
if (!url) {
|
|
194
|
+
url = (await text({
|
|
195
|
+
message: 'Remote storage URL to remove:',
|
|
196
|
+
validate(value) {
|
|
197
|
+
if (!value) return 'Required'
|
|
198
|
+
},
|
|
199
|
+
})) as string
|
|
200
|
+
if (isCancel(url)) {
|
|
201
|
+
fatal('Cancelled')
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const config = loadConfig()
|
|
206
|
+
const backups = config.backups ?? []
|
|
207
|
+
|
|
208
|
+
if (!backups.includes(url)) {
|
|
209
|
+
fatal(`Remote not found in config: ${url}`)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Confirm
|
|
213
|
+
const confirmed = await confirm({
|
|
214
|
+
message: `Remove ${url} from backups?`,
|
|
215
|
+
defaultValue: false,
|
|
216
|
+
})
|
|
217
|
+
if (isCancel(confirmed) || !confirmed) {
|
|
218
|
+
fatal('Cancelled')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
saveConfig({ ...config, backups: backups.filter((u) => u !== url) })
|
|
222
|
+
|
|
223
|
+
if (opts.json) {
|
|
224
|
+
output({ url, status: 'removed' }, opts)
|
|
225
|
+
} else {
|
|
226
|
+
console.log(formatSuccess(` Removed ${url} from backups`))
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================================================
|
|
231
|
+
// remote set-active
|
|
232
|
+
// ============================================================================
|
|
233
|
+
|
|
234
|
+
async function remoteSetActive(
|
|
235
|
+
args: string[],
|
|
236
|
+
opts: GlobalFlags,
|
|
237
|
+
): Promise<void> {
|
|
238
|
+
let target = args[0]
|
|
239
|
+
|
|
240
|
+
if (!target) {
|
|
241
|
+
target = (await text({
|
|
242
|
+
message: 'Set active storage to (url or "local"):',
|
|
243
|
+
validate(value) {
|
|
244
|
+
if (!value) return 'Required'
|
|
245
|
+
},
|
|
246
|
+
})) as string
|
|
247
|
+
if (isCancel(target)) {
|
|
248
|
+
fatal('Cancelled')
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const config = loadConfig()
|
|
253
|
+
|
|
254
|
+
if (target === 'local') {
|
|
255
|
+
// Switch back to local
|
|
256
|
+
if (!config.activeRemote && !config.backups?.length) {
|
|
257
|
+
fatal('No remote storages configured')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const privateKey = await loadKey(resolvePassword())
|
|
261
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
262
|
+
chain: opts.chain,
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
// Find the local storage's storageIdentityKey
|
|
267
|
+
const localKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
268
|
+
|
|
269
|
+
if (opts.json) {
|
|
270
|
+
output({ target: 'local', status: 'migrating' }, opts)
|
|
271
|
+
} else {
|
|
272
|
+
console.log(' Switching active storage to local...')
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
await walletResult.storage.setActive(localKey)
|
|
276
|
+
|
|
277
|
+
// Clear activeRemote from config
|
|
278
|
+
saveConfig({ ...config, activeRemote: undefined })
|
|
279
|
+
|
|
280
|
+
if (opts.json) {
|
|
281
|
+
output(
|
|
282
|
+
{ target: 'local', status: 'active', storageIdentityKey: localKey },
|
|
283
|
+
opts,
|
|
284
|
+
)
|
|
285
|
+
} else {
|
|
286
|
+
console.log(formatSuccess(' Local storage is now active'))
|
|
287
|
+
}
|
|
288
|
+
} finally {
|
|
289
|
+
await destroy()
|
|
290
|
+
}
|
|
291
|
+
} else {
|
|
292
|
+
// Switch to a remote
|
|
293
|
+
// Validate URL
|
|
294
|
+
try {
|
|
295
|
+
new URL(target)
|
|
296
|
+
} catch {
|
|
297
|
+
fatal(`Invalid URL: ${target}`)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const privateKey = await loadKey(resolvePassword())
|
|
301
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
302
|
+
chain: opts.chain,
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
if (opts.json) {
|
|
307
|
+
output({ target, status: 'migrating' }, opts)
|
|
308
|
+
} else {
|
|
309
|
+
console.log(` Switching active storage to ${target}...`)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
await walletResult.migrateRemote(target)
|
|
313
|
+
|
|
314
|
+
// Persist to config
|
|
315
|
+
saveConfig({ ...config, activeRemote: target })
|
|
316
|
+
|
|
317
|
+
if (opts.json) {
|
|
318
|
+
output({ target, status: 'active' }, opts)
|
|
319
|
+
} else {
|
|
320
|
+
console.log(formatSuccess(` ${target} is now active`))
|
|
321
|
+
}
|
|
322
|
+
} finally {
|
|
323
|
+
await destroy()
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// Helpers
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
function bold(s: string): string {
|
|
333
|
+
return `\x1b[1m${s}\x1b[0m`
|
|
334
|
+
}
|
package/src/commands/sweep.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
prepareSweepInputs,
|
|
9
|
-
|
|
9
|
+
scanAddress,
|
|
10
10
|
sweepBsv,
|
|
11
11
|
sweepBsv21,
|
|
12
12
|
sweepOrdinals,
|
|
@@ -65,7 +65,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
65
65
|
fatal('Services required for sweep scan')
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const result = await
|
|
68
|
+
const result = await scanAddress(ctx.services, address)
|
|
69
69
|
|
|
70
70
|
if (opts.json) {
|
|
71
71
|
output(result, opts)
|
|
@@ -81,7 +81,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
81
81
|
)
|
|
82
82
|
for (const f of result.funding) {
|
|
83
83
|
console.log(
|
|
84
|
-
` ${formatValue(f.outpoint)} ${formatLabel(`${f.satoshis} sats`)}`,
|
|
84
|
+
` ${formatValue(f.outpoint)} ${formatLabel(`${f.satoshis ?? 0} sats`)}`,
|
|
85
85
|
)
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -101,7 +101,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
101
101
|
if (result.bsv21Tokens.length > 0) {
|
|
102
102
|
for (const t of result.bsv21Tokens) {
|
|
103
103
|
console.log(
|
|
104
|
-
` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel('amount:')} ${formatValue(t.totalAmount)} ${formatLabel('UTXOs:')} ${t.
|
|
104
|
+
` ${formatValue(t.symbol ?? t.tokenId.slice(0, 12))} ${formatLabel('amount:')} ${formatValue(t.totalAmount.toString())} ${formatLabel('UTXOs:')} ${t.outputs.length} ${formatLabel('dec:')} ${t.decimals}`,
|
|
105
105
|
)
|
|
106
106
|
}
|
|
107
107
|
}
|
|
@@ -110,13 +110,13 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
110
110
|
`\n ${formatLabel('RUN Tokens:')} ${result.run.length}`,
|
|
111
111
|
)
|
|
112
112
|
if (result.run.length > 0) {
|
|
113
|
-
const runSats = result.run.reduce((sum, r) => sum + r.satoshis, 0)
|
|
113
|
+
const runSats = result.run.reduce((sum, r) => sum + (r.satoshis ?? 0), 0)
|
|
114
114
|
console.log(
|
|
115
115
|
` ${formatLabel('Total locked:')} ${formatValue(runSats)} satoshis (not sweepable)`,
|
|
116
116
|
)
|
|
117
117
|
for (const r of result.run) {
|
|
118
118
|
console.log(
|
|
119
|
-
` ${formatValue(r.outpoint)} ${formatLabel(`${r.satoshis} sats`)}`,
|
|
119
|
+
` ${formatValue(r.outpoint)} ${formatLabel(`${r.satoshis ?? 0} sats`)}`,
|
|
120
120
|
)
|
|
121
121
|
}
|
|
122
122
|
}
|
|
@@ -124,7 +124,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
124
124
|
const total =
|
|
125
125
|
result.funding.length +
|
|
126
126
|
result.ordinals.length +
|
|
127
|
-
result.bsv21Tokens.reduce((n, t) => n + t.
|
|
127
|
+
result.bsv21Tokens.reduce((n, t) => n + t.outputs.length, 0)
|
|
128
128
|
console.log(`\n ${total} sweepable UTXO(s) found.`)
|
|
129
129
|
if (result.run.length > 0) {
|
|
130
130
|
console.log(` ${result.run.length} RUN token output(s) excluded.`)
|
|
@@ -157,12 +157,11 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
157
157
|
fatal('Services required for sweep import')
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
|
|
161
|
-
const scan = await scanAddressUtxos(ctx.services, address)
|
|
160
|
+
const scan = await scanAddress(ctx.services, address)
|
|
162
161
|
|
|
163
162
|
const hasFunding = scan.funding.length > 0
|
|
164
163
|
const hasOrdinals = scan.ordinals.length > 0
|
|
165
|
-
const hasTokens = scan.bsv21Tokens.length > 0
|
|
164
|
+
const hasTokens = scan.bsv21Tokens.some((t) => t.isActive && t.outputs.length > 0)
|
|
166
165
|
|
|
167
166
|
if (!hasFunding && !hasOrdinals && !hasTokens) {
|
|
168
167
|
if (scan.run.length > 0) {
|
|
@@ -171,16 +170,17 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
171
170
|
fatal(`No UTXOs found at ${address}`)
|
|
172
171
|
}
|
|
173
172
|
|
|
174
|
-
// Summarize what will be swept
|
|
175
173
|
const parts: string[] = []
|
|
176
174
|
if (hasFunding)
|
|
177
175
|
parts.push(`${scan.totalFundingSats} sats (${scan.funding.length} UTXOs)`)
|
|
178
176
|
if (hasOrdinals) parts.push(`${scan.ordinals.length} ordinal(s)`)
|
|
179
177
|
if (hasTokens) {
|
|
180
178
|
for (const t of scan.bsv21Tokens) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
179
|
+
if (t.isActive && t.outputs.length > 0) {
|
|
180
|
+
parts.push(
|
|
181
|
+
`${t.totalAmount} ${t.symbol ?? t.tokenId.slice(0, 12)} token(s)`,
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
184
|
}
|
|
185
185
|
}
|
|
186
186
|
if (scan.run.length > 0) {
|
|
@@ -198,18 +198,9 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
198
198
|
|
|
199
199
|
const txids: string[] = []
|
|
200
200
|
|
|
201
|
-
// Convert SweepInput to IndexedOutput shape for prepareSweepInputs
|
|
202
|
-
const toIndexed = (items: Array<{ outpoint: string; satoshis: number }>) =>
|
|
203
|
-
items.map((item) => ({
|
|
204
|
-
outpoint: item.outpoint,
|
|
205
|
-
satoshis: item.satoshis,
|
|
206
|
-
score: 0,
|
|
207
|
-
}))
|
|
208
|
-
|
|
209
201
|
// Sweep BSV funding UTXOs
|
|
210
202
|
if (hasFunding) {
|
|
211
|
-
const inputs = await prepareSweepInputs(ctx,
|
|
212
|
-
|
|
203
|
+
const inputs = await prepareSweepInputs(ctx, scan.funding)
|
|
213
204
|
const result = await sweepBsv.execute(ctx, { inputs, wif })
|
|
214
205
|
if (result.error) {
|
|
215
206
|
fatal(`BSV sweep failed: ${result.error}`)
|
|
@@ -219,8 +210,7 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
219
210
|
|
|
220
211
|
// Sweep ordinals
|
|
221
212
|
if (hasOrdinals) {
|
|
222
|
-
const inputs = await prepareSweepInputs(ctx,
|
|
223
|
-
|
|
213
|
+
const inputs = await prepareSweepInputs(ctx, scan.ordinals)
|
|
224
214
|
const result = await sweepOrdinals.execute(ctx, { inputs, wif })
|
|
225
215
|
if (result.error) {
|
|
226
216
|
fatal(`Ordinals sweep failed: ${result.error}`)
|
|
@@ -228,27 +218,21 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
228
218
|
if (result.txid) txids.push(result.txid)
|
|
229
219
|
}
|
|
230
220
|
|
|
231
|
-
// Sweep BSV-21 tokens (one sweep per tokenId)
|
|
221
|
+
// Sweep BSV-21 tokens (one sweep per active tokenId)
|
|
232
222
|
if (hasTokens) {
|
|
233
|
-
for (const
|
|
234
|
-
|
|
235
|
-
ctx,
|
|
236
|
-
toIndexed(tokenGroup.inputs),
|
|
237
|
-
)
|
|
223
|
+
for (const token of scan.bsv21Tokens) {
|
|
224
|
+
if (!token.isActive || token.outputs.length === 0) continue
|
|
238
225
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
tokenId:
|
|
242
|
-
amount:
|
|
226
|
+
const inputs = token.outputs.map((out) => ({
|
|
227
|
+
outpoint: out.outpoint,
|
|
228
|
+
tokenId: token.tokenId,
|
|
229
|
+
amount: token.amounts.get(out.outpoint) ?? '0',
|
|
243
230
|
}))
|
|
244
231
|
|
|
245
|
-
const result = await sweepBsv21.execute(ctx, {
|
|
246
|
-
inputs: sweepInputs,
|
|
247
|
-
wif,
|
|
248
|
-
})
|
|
232
|
+
const result = await sweepBsv21.execute(ctx, { inputs, wif })
|
|
249
233
|
if (result.error) {
|
|
250
234
|
fatal(
|
|
251
|
-
`Token sweep failed (${
|
|
235
|
+
`Token sweep failed (${token.symbol ?? token.tokenId.slice(0, 12)}): ${result.error}`,
|
|
252
236
|
)
|
|
253
237
|
}
|
|
254
238
|
if (result.txid) txids.push(result.txid)
|
package/src/commands/tokens.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
getBsv21Balances,
|
|
7
|
+
getDisplayValue,
|
|
7
8
|
listTokens,
|
|
8
9
|
purchaseBsv21,
|
|
9
10
|
sendBsv21,
|
|
@@ -93,8 +94,8 @@ async function tokenList(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
93
94
|
|
|
94
95
|
const filtered = tokenId
|
|
95
96
|
? outputs.filter((o) => {
|
|
96
|
-
const idTag = o.tags?.find((t) => t.startsWith('
|
|
97
|
-
return idTag && idTag.slice(
|
|
97
|
+
const idTag = o.tags?.find((t) => t.startsWith('bsv21:'))
|
|
98
|
+
return idTag && idTag.slice(6) === tokenId
|
|
98
99
|
})
|
|
99
100
|
: outputs
|
|
100
101
|
|
|
@@ -115,9 +116,9 @@ async function tokenList(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
115
116
|
|
|
116
117
|
for (const o of filtered) {
|
|
117
118
|
const idTag =
|
|
118
|
-
o.tags?.find((t) => t.startsWith('
|
|
119
|
+
o.tags?.find((t) => t.startsWith('bsv21:'))?.slice(6) ?? 'unknown'
|
|
119
120
|
const amtTag = o.tags?.find((t) => t.startsWith('amt:'))?.slice(4) ?? '0'
|
|
120
|
-
const symTag = o
|
|
121
|
+
const symTag = getDisplayValue(o, 'sym', 'sym') ?? ''
|
|
121
122
|
|
|
122
123
|
console.log(
|
|
123
124
|
` ${formatValue(o.outpoint)} ${formatLabel(symTag || idTag.slice(0, 12))} ${formatLabel('amt:')} ${formatValue(amtTag)}`,
|
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
|
|
@@ -100,7 +101,7 @@ ${bold('Environment Variables:')}
|
|
|
100
101
|
${dim('PRIVATE_KEY_WIF')} Private key (bypasses encrypted keyfile)
|
|
101
102
|
${dim('ONESAT_PASSWORD')} Password for encrypted keyfile
|
|
102
103
|
|
|
103
|
-
${bold('Config:')} ~/.1sat/
|
|
104
|
+
${bold('Config:')} ~/.1sat/cli/
|
|
104
105
|
`)
|
|
105
106
|
}
|
|
106
107
|
|