@1sat/cli 0.0.21 → 0.0.23
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 +40 -33
- package/src/args.ts +18 -0
- package/src/commands/config.ts +3 -1
- package/src/commands/identity.ts +37 -1
- package/src/commands/init.ts +4 -11
- package/src/commands/mcp-proxy.ts +49 -16
- package/src/commands/remote.ts +14 -18
- package/src/commands/sweep.ts +7 -5
- package/src/commands/wallet.ts +311 -1
- package/src/context.ts +14 -1
- package/src/help.ts +17 -0
package/package.json
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
2
|
+
"name": "@1sat/cli",
|
|
3
|
+
"version": "0.0.23",
|
|
4
|
+
"description": "CLI for 1Sat Ordinals SDK",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/cli.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"1sat": "./src/cli.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun build ./src/cli.ts --compile --outfile=bin/1sat --external pg --external pg-native --external pg-query-stream --external tedious --external oracledb --external mysql",
|
|
15
|
+
"dev": "bun run src/cli.ts",
|
|
16
|
+
"lint": "biome check src"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"1sat",
|
|
20
|
+
"bsv",
|
|
21
|
+
"ordinals",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@1sat/actions": "0.0.83",
|
|
27
|
+
"@1sat/client": "0.0.20",
|
|
28
|
+
"@1sat/types": "0.0.15",
|
|
29
|
+
"@1sat/wallet-node": "0.0.23",
|
|
30
|
+
"@bsv/sdk": "^2.0.6",
|
|
31
|
+
"chalk": "^5.0.0",
|
|
32
|
+
"@clack/prompts": "^0.8.0",
|
|
33
|
+
"bitcoin-backup": "^0.0.11",
|
|
34
|
+
"dotenv": "^17.0.0",
|
|
35
|
+
"evlog": "^2.10.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/bun": "^1.3.9",
|
|
39
|
+
"typescript": "^5.9.3"
|
|
40
|
+
}
|
|
41
|
+
}
|
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/commands/config.ts
CHANGED
|
@@ -105,7 +105,9 @@ function configSet(args: string[], opts: GlobalFlags): void {
|
|
|
105
105
|
if (key === 'monitorIntervalMinutes') {
|
|
106
106
|
const n = Number(value)
|
|
107
107
|
if (!Number.isFinite(n) || n < 0) {
|
|
108
|
-
fatal(
|
|
108
|
+
fatal(
|
|
109
|
+
'monitorIntervalMinutes must be a non-negative number (0 to disable)',
|
|
110
|
+
)
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
package/src/commands/identity.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* BAP (Bitcoin Attestation Protocol) identity management.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { publishIdentity, signBsm } from '@1sat/actions'
|
|
7
|
+
import { publishIdentity, signBsm, updateProfile } from '@1sat/actions'
|
|
8
8
|
import type { GlobalFlags } from '../args'
|
|
9
9
|
import { extractFlag } from '../args'
|
|
10
10
|
import { loadContext } from '../context'
|
|
@@ -21,6 +21,8 @@ export async function handleIdentityCommand(
|
|
|
21
21
|
switch (subcommand) {
|
|
22
22
|
case 'create':
|
|
23
23
|
return identityCreate(rest, opts)
|
|
24
|
+
case 'update-profile':
|
|
25
|
+
return identityUpdateProfile(rest, opts)
|
|
24
26
|
case 'info':
|
|
25
27
|
return identityInfo(rest, opts)
|
|
26
28
|
case 'sign':
|
|
@@ -30,6 +32,7 @@ export async function handleIdentityCommand(
|
|
|
30
32
|
default:
|
|
31
33
|
printCommandHelp('identity', {
|
|
32
34
|
create: 'Create/publish a BAP identity',
|
|
35
|
+
'update-profile': 'Update BAP identity profile (--profile <json>)',
|
|
33
36
|
info: 'Show BAP identity information',
|
|
34
37
|
sign: 'Sign a message with identity key (--message <text>)',
|
|
35
38
|
verify:
|
|
@@ -128,3 +131,36 @@ async function identityVerify(
|
|
|
128
131
|
'identity verify is not yet implemented (needs direct BSM.verify integration)',
|
|
129
132
|
)
|
|
130
133
|
}
|
|
134
|
+
|
|
135
|
+
async function identityUpdateProfile(
|
|
136
|
+
args: string[],
|
|
137
|
+
opts: GlobalFlags,
|
|
138
|
+
): Promise<void> {
|
|
139
|
+
const profileStr = extractFlag(args, '--profile')
|
|
140
|
+
|
|
141
|
+
if (!profileStr) fatal('Missing --profile <json>')
|
|
142
|
+
|
|
143
|
+
let profile: Record<string, unknown>
|
|
144
|
+
try {
|
|
145
|
+
profile = JSON.parse(profileStr)
|
|
146
|
+
} catch {
|
|
147
|
+
fatal(`Invalid JSON in --profile: ${profileStr}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const privateKey = await loadKey(resolvePassword())
|
|
151
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
152
|
+
chain: opts.chain,
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const result = await updateProfile.execute(ctx, { profile })
|
|
157
|
+
|
|
158
|
+
if (result.error) {
|
|
159
|
+
fatal(result.error)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
output(result, opts)
|
|
163
|
+
} finally {
|
|
164
|
+
await destroy()
|
|
165
|
+
}
|
|
166
|
+
}
|
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,16 +163,8 @@ export async function handleInitCommand(
|
|
|
162
163
|
// bitcoin-backup Touch ID not available — skip silently
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
// 4.
|
|
166
|
-
const storageId =
|
|
167
|
-
message: 'Storage identity key (for wallet persistence):',
|
|
168
|
-
defaultValue: '1sat-cli-default',
|
|
169
|
-
placeholder: '1sat-cli-default',
|
|
170
|
-
})
|
|
171
|
-
if (isCancel(storageId)) {
|
|
172
|
-
cancel('Setup cancelled.')
|
|
173
|
-
process.exit(0)
|
|
174
|
-
}
|
|
166
|
+
// 4. Generate random storage identity key
|
|
167
|
+
const storageId = `1sat-cli-${randomBytes(8).toString('hex')}`
|
|
175
168
|
|
|
176
169
|
// 5. Optional: remote storage configuration
|
|
177
170
|
const useRemote = await confirm({
|
|
@@ -220,7 +213,7 @@ export async function handleInitCommand(
|
|
|
220
213
|
saveConfig({
|
|
221
214
|
...loadConfig(),
|
|
222
215
|
chain: chain as 'main' | 'test',
|
|
223
|
-
storageIdentityKey: storageId
|
|
216
|
+
storageIdentityKey: storageId,
|
|
224
217
|
activeRemote,
|
|
225
218
|
})
|
|
226
219
|
|
|
@@ -61,33 +61,54 @@ async function handshake(key: PrivateKey): Promise<Session> {
|
|
|
61
61
|
if (!res.ok) throw new Error(`Handshake failed: HTTP ${res.status}`)
|
|
62
62
|
|
|
63
63
|
const data = await res.json()
|
|
64
|
-
if (data.messageType !== 'initialResponse')
|
|
64
|
+
if (data.messageType !== 'initialResponse')
|
|
65
|
+
throw new Error(`Unexpected: ${data.messageType}`)
|
|
65
66
|
|
|
66
67
|
const serverNonce = data.nonce as string
|
|
67
68
|
const deriver = new KeyDeriver(key)
|
|
68
69
|
const serverPub = deriver.derivePublicKey(
|
|
69
|
-
AUTH_PROTOCOL_ID,
|
|
70
|
+
AUTH_PROTOCOL_ID,
|
|
71
|
+
`${serverNonce} ${clientNonce}`,
|
|
72
|
+
data.identityKey as string,
|
|
73
|
+
false,
|
|
70
74
|
)
|
|
71
75
|
|
|
72
76
|
const clientNonceBytes = toArray(clientNonce, 'base64')
|
|
73
77
|
const serverNonceBytes = toArray(serverNonce, 'base64')
|
|
74
|
-
const msgHash = Hash.sha256(
|
|
78
|
+
const msgHash = Hash.sha256(
|
|
79
|
+
Array.from(new Uint8Array([...clientNonceBytes, ...serverNonceBytes])),
|
|
80
|
+
)
|
|
75
81
|
|
|
76
82
|
const sig = Signature.fromDER(data.signature as string, 'hex')
|
|
77
|
-
if (!serverPub.verify(Array.from(msgHash), sig))
|
|
83
|
+
if (!serverPub.verify(Array.from(msgHash), sig))
|
|
84
|
+
throw new Error('Server signature verification failed')
|
|
78
85
|
|
|
79
86
|
const log = createLogger({ context: 'auth' })
|
|
80
|
-
log.set({
|
|
87
|
+
log.set({
|
|
88
|
+
event: 'handshake_complete',
|
|
89
|
+
serverIdentityKey: (data.identityKey as string).slice(0, 16),
|
|
90
|
+
})
|
|
81
91
|
log.emit()
|
|
82
92
|
|
|
83
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
serverIdentityKey: data.identityKey as string,
|
|
95
|
+
serverNonce,
|
|
96
|
+
clientKey: key,
|
|
97
|
+
}
|
|
84
98
|
}
|
|
85
99
|
|
|
86
|
-
function signHeaders(
|
|
100
|
+
function signHeaders(
|
|
101
|
+
session: Session,
|
|
102
|
+
pathname: string,
|
|
103
|
+
): Record<string, string> {
|
|
87
104
|
const { serverIdentityKey, serverNonce, clientKey: key } = session
|
|
88
105
|
const nonce = generateNonce()
|
|
89
106
|
const deriver = new KeyDeriver(key)
|
|
90
|
-
const derivedKey = deriver.derivePrivateKey(
|
|
107
|
+
const derivedKey = deriver.derivePrivateKey(
|
|
108
|
+
AUTH_PROTOCOL_ID,
|
|
109
|
+
`${nonce} ${serverNonce}`,
|
|
110
|
+
serverIdentityKey,
|
|
111
|
+
)
|
|
91
112
|
const payload = new TextEncoder().encode(pathname)
|
|
92
113
|
const msgHash = Hash.sha256(Array.from(payload))
|
|
93
114
|
const sig = derivedKey.sign(Array.from(msgHash))
|
|
@@ -109,7 +130,9 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
109
130
|
} catch {
|
|
110
131
|
startLog.set({ event: 'server_unreachable', url: MCP_URL })
|
|
111
132
|
startLog.emit()
|
|
112
|
-
process.stderr.write(
|
|
133
|
+
process.stderr.write(
|
|
134
|
+
`[1sat mcp-proxy] Server not reachable at ${MCP_URL} — is 1Sat wallet running?\n`,
|
|
135
|
+
)
|
|
113
136
|
process.exit(1)
|
|
114
137
|
}
|
|
115
138
|
|
|
@@ -156,7 +179,11 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
156
179
|
method = JSON.parse(line).method
|
|
157
180
|
} catch {}
|
|
158
181
|
|
|
159
|
-
const res = await fetch(`${MCP_URL}/mcp`, {
|
|
182
|
+
const res = await fetch(`${MCP_URL}/mcp`, {
|
|
183
|
+
method: 'POST',
|
|
184
|
+
headers,
|
|
185
|
+
body: line,
|
|
186
|
+
})
|
|
160
187
|
|
|
161
188
|
const sessionHeader = res.headers.get('mcp-session-id')
|
|
162
189
|
if (sessionHeader) mcpSessionId = sessionHeader
|
|
@@ -202,13 +229,19 @@ export async function handleMcpProxyCommand(): Promise<void> {
|
|
|
202
229
|
}
|
|
203
230
|
} catch (err) {
|
|
204
231
|
const msg = err instanceof Error ? err.message : String(err)
|
|
205
|
-
reqLog.set({
|
|
232
|
+
reqLog.set({
|
|
233
|
+
event: 'request_failed',
|
|
234
|
+
requestNum: requestCount,
|
|
235
|
+
error: msg,
|
|
236
|
+
})
|
|
206
237
|
reqLog.emit()
|
|
207
|
-
process.stdout.write(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
238
|
+
process.stdout.write(
|
|
239
|
+
`${JSON.stringify({
|
|
240
|
+
jsonrpc: '2.0',
|
|
241
|
+
error: { code: -32000, message: `MCP proxy error: ${msg}` },
|
|
242
|
+
id: null,
|
|
243
|
+
})}\n`,
|
|
244
|
+
)
|
|
212
245
|
}
|
|
213
246
|
}
|
|
214
247
|
}
|
package/src/commands/remote.ts
CHANGED
|
@@ -81,6 +81,9 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
81
81
|
chain: opts.chain,
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
// Load config early — needed for storage identity key lookups
|
|
85
|
+
const config = loadConfig()
|
|
86
|
+
|
|
84
87
|
try {
|
|
85
88
|
// For backup-only, we use StorageClient via wallet-node
|
|
86
89
|
// biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
|
|
@@ -99,7 +102,6 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
99
102
|
await walletResult.storage.updateBackups()
|
|
100
103
|
|
|
101
104
|
// Persist to config — connectivity will be validated on next monitor run
|
|
102
|
-
const config = loadConfig()
|
|
103
105
|
const existing = config.backups ?? []
|
|
104
106
|
if (!existing.includes(url)) {
|
|
105
107
|
saveConfig({ ...config, backups: [...existing, url] })
|
|
@@ -131,9 +133,11 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
131
133
|
})
|
|
132
134
|
|
|
133
135
|
try {
|
|
134
|
-
const backups = walletResult.storage.getBackupStores?.() ?? []
|
|
135
136
|
const config = loadConfig()
|
|
136
137
|
|
|
138
|
+
// Use config.backups for display (remoteClients only populated for active connections)
|
|
139
|
+
const backupUrls = config.backups ?? []
|
|
140
|
+
|
|
137
141
|
// Use config for active determination — WalletStorageManager internal state
|
|
138
142
|
// can be misleading (a backup may appear as active after addWalletStorageProvider)
|
|
139
143
|
const isRemoteActive = Boolean(config.activeRemote)
|
|
@@ -142,7 +146,7 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
142
146
|
output(
|
|
143
147
|
{
|
|
144
148
|
activeStorage: isRemoteActive ? 'remote' : 'local',
|
|
145
|
-
backups:
|
|
149
|
+
backups: backupUrls,
|
|
146
150
|
config: {
|
|
147
151
|
activeRemote: config.activeRemote ?? null,
|
|
148
152
|
backups: config.backups ?? [],
|
|
@@ -154,27 +158,19 @@ async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
console.log()
|
|
157
|
-
console.log(
|
|
161
|
+
console.log(
|
|
162
|
+
` ${bold('Active Storage:')} ${isRemoteActive ? 'remote' : 'local'}`,
|
|
163
|
+
)
|
|
158
164
|
if (isRemoteActive) {
|
|
159
|
-
console.log(
|
|
160
|
-
` ${bold('Active Remote:')} ${config.activeRemote}`,
|
|
161
|
-
)
|
|
165
|
+
console.log(` ${bold('Active Remote:')} ${config.activeRemote}`)
|
|
162
166
|
}
|
|
163
167
|
console.log()
|
|
164
|
-
if (
|
|
168
|
+
if (backupUrls.length === 0 && !config.backups?.length) {
|
|
165
169
|
console.log(' No remote storages configured')
|
|
166
170
|
} else {
|
|
167
171
|
console.log(` ${bold('Backups:')}`)
|
|
168
|
-
const
|
|
169
|
-
|
|
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
|
-
}
|
|
172
|
+
for (const url of backupUrls) {
|
|
173
|
+
console.log(` ● ${url}`)
|
|
178
174
|
}
|
|
179
175
|
}
|
|
180
176
|
console.log()
|
package/src/commands/sweep.ts
CHANGED
|
@@ -106,9 +106,7 @@ async function sweepScan(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
console.log(
|
|
110
|
-
`\n ${formatLabel('RUN Tokens:')} ${result.run.length}`,
|
|
111
|
-
)
|
|
109
|
+
console.log(`\n ${formatLabel('RUN Tokens:')} ${result.run.length}`)
|
|
112
110
|
if (result.run.length > 0) {
|
|
113
111
|
const runSats = result.run.reduce((sum, r) => sum + (r.satoshis ?? 0), 0)
|
|
114
112
|
console.log(
|
|
@@ -161,11 +159,15 @@ async function sweepImport(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
161
159
|
|
|
162
160
|
const hasFunding = scan.funding.length > 0
|
|
163
161
|
const hasOrdinals = scan.ordinals.length > 0
|
|
164
|
-
const hasTokens = scan.bsv21Tokens.some(
|
|
162
|
+
const hasTokens = scan.bsv21Tokens.some(
|
|
163
|
+
(t) => t.isActive && t.outputs.length > 0,
|
|
164
|
+
)
|
|
165
165
|
|
|
166
166
|
if (!hasFunding && !hasOrdinals && !hasTokens) {
|
|
167
167
|
if (scan.run.length > 0) {
|
|
168
|
-
fatal(
|
|
168
|
+
fatal(
|
|
169
|
+
`No sweepable UTXOs found at ${address} (${scan.run.length} RUN token output(s) excluded)`,
|
|
170
|
+
)
|
|
169
171
|
}
|
|
170
172
|
fatal(`No UTXOs found at ${address}`)
|
|
171
173
|
}
|
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,17 @@ 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':
|
|
56
|
+
'List wallet outputs (--basket <name> [--tags <t1,t2>] [--limit N] [--include-tags] [--include <val>])',
|
|
57
|
+
'relinquish-output':
|
|
58
|
+
'Remove output from basket (--basket <name> --output <txid.vout>)',
|
|
59
|
+
'list-actions': 'List wallet actions [--labels <l1,l2>] [--limit N]',
|
|
60
|
+
'create-action': 'Create action (JSON args)',
|
|
61
|
+
'sign-action': 'Sign action (JSON args)',
|
|
62
|
+
'abort-action': 'Abort action (--reference <ref>)',
|
|
63
|
+
'list-certificates': 'List certificates',
|
|
64
|
+
'relinquish-certificate':
|
|
65
|
+
'Relinquish certificate (--type <t> --serialNumber <s> --certifier <c>)',
|
|
38
66
|
})
|
|
39
67
|
if (subcommand && subcommand !== 'help') {
|
|
40
68
|
process.exit(1)
|
|
@@ -216,3 +244,285 @@ async function walletInfo(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
216
244
|
await destroy()
|
|
217
245
|
}
|
|
218
246
|
}
|
|
247
|
+
|
|
248
|
+
// BRC-100 Interface Commands
|
|
249
|
+
|
|
250
|
+
async function walletListOutputs(
|
|
251
|
+
args: string[],
|
|
252
|
+
opts: GlobalFlags,
|
|
253
|
+
): Promise<void> {
|
|
254
|
+
const basket = extractFlag(args, '--basket')
|
|
255
|
+
if (!basket) fatal('Missing required --basket <name>')
|
|
256
|
+
|
|
257
|
+
const tags = extractFlags(args, '--tags')
|
|
258
|
+
const limitStr = extractFlag(args, '--limit')
|
|
259
|
+
const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
|
|
260
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
261
|
+
fatal('--limit must be between 1 and 10000')
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const includeTags = args.includes('--include-tags')
|
|
265
|
+
const include = extractFlag(args, '--include')
|
|
266
|
+
|
|
267
|
+
const privateKey = await loadKey(resolvePassword())
|
|
268
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
269
|
+
chain: opts.chain,
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const listArgs: Parameters<typeof ctx.wallet.listOutputs>[0] = {
|
|
274
|
+
basket,
|
|
275
|
+
limit,
|
|
276
|
+
includeTags,
|
|
277
|
+
include,
|
|
278
|
+
}
|
|
279
|
+
if (tags.length > 0) {
|
|
280
|
+
listArgs.tags = tags
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const result = await ctx.wallet.listOutputs(listArgs)
|
|
284
|
+
|
|
285
|
+
if (opts.json) {
|
|
286
|
+
output(result, opts)
|
|
287
|
+
} else {
|
|
288
|
+
console.log(
|
|
289
|
+
`\n${result.totalOutputs} total outputs in basket '${basket}':\n`,
|
|
290
|
+
)
|
|
291
|
+
for (const out of result.outputs) {
|
|
292
|
+
const tags = out.tags?.join(', ') || 'none'
|
|
293
|
+
console.log(` ${out.outpoint} | ${out.satoshis} sats | tags: ${tags}`)
|
|
294
|
+
}
|
|
295
|
+
console.log()
|
|
296
|
+
}
|
|
297
|
+
} finally {
|
|
298
|
+
await destroy()
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function walletRelinquishOutput(
|
|
303
|
+
args: string[],
|
|
304
|
+
opts: GlobalFlags,
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
const basket = extractFlag(args, '--basket')
|
|
307
|
+
const outpoint = extractFlag(args, '--output')
|
|
308
|
+
|
|
309
|
+
if (!basket) fatal('Missing required --basket <name>')
|
|
310
|
+
if (!outpoint) fatal('Missing required --output <txid.vout>')
|
|
311
|
+
|
|
312
|
+
// Validate output format (txid.vout)
|
|
313
|
+
if (!outpoint.includes('.') || outpoint.split('.').length !== 2) {
|
|
314
|
+
fatal('Invalid --output format. Expected: txid.vout (e.g., abc123...0)')
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const privateKey = await loadKey(resolvePassword())
|
|
318
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
319
|
+
chain: opts.chain,
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const result = await ctx.wallet.relinquishOutput({
|
|
324
|
+
basket,
|
|
325
|
+
output: outpoint,
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if (opts.json) {
|
|
329
|
+
output(result, opts)
|
|
330
|
+
} else {
|
|
331
|
+
output({ relinquished: true, basket, output: outpoint }, opts)
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
await destroy()
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function walletListActions(
|
|
339
|
+
args: string[],
|
|
340
|
+
opts: GlobalFlags,
|
|
341
|
+
): Promise<void> {
|
|
342
|
+
const labels = extractFlags(args, '--labels')
|
|
343
|
+
const limitStr = extractFlag(args, '--limit')
|
|
344
|
+
const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
|
|
345
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
346
|
+
fatal('--limit must be between 1 and 10000')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const privateKey = await loadKey(resolvePassword())
|
|
350
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
351
|
+
chain: opts.chain,
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const listArgs: Parameters<typeof ctx.wallet.listActions>[0] = {
|
|
356
|
+
labels: labels.length > 0 ? labels : ['*'], // Default to all labels if none specified
|
|
357
|
+
limit,
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = await ctx.wallet.listActions(listArgs)
|
|
361
|
+
|
|
362
|
+
if (opts.json) {
|
|
363
|
+
output(result, opts)
|
|
364
|
+
} else {
|
|
365
|
+
console.log(`\n${result.totalActions} total actions:\n`)
|
|
366
|
+
for (const action of result.actions) {
|
|
367
|
+
console.log(` ${action.txid || 'pending'} | status: ${action.status}`)
|
|
368
|
+
}
|
|
369
|
+
console.log()
|
|
370
|
+
}
|
|
371
|
+
} finally {
|
|
372
|
+
await destroy()
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function walletCreateAction(
|
|
377
|
+
args: string[],
|
|
378
|
+
opts: GlobalFlags,
|
|
379
|
+
): Promise<void> {
|
|
380
|
+
const jsonInput = args[0]
|
|
381
|
+
|
|
382
|
+
if (!jsonInput)
|
|
383
|
+
fatal("Missing JSON arguments. Usage: wallet create-action '{...}'")
|
|
384
|
+
|
|
385
|
+
let actionArgs: Parameters<typeof ctx.wallet.createAction>[0]
|
|
386
|
+
try {
|
|
387
|
+
actionArgs = JSON.parse(jsonInput)
|
|
388
|
+
} catch {
|
|
389
|
+
fatal(`Invalid JSON: ${jsonInput}`)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const privateKey = await loadKey(resolvePassword())
|
|
393
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
394
|
+
chain: opts.chain,
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
const result = await ctx.wallet.createAction(actionArgs)
|
|
399
|
+
output(result, opts)
|
|
400
|
+
} finally {
|
|
401
|
+
await destroy()
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function walletSignAction(
|
|
406
|
+
args: string[],
|
|
407
|
+
opts: GlobalFlags,
|
|
408
|
+
): Promise<void> {
|
|
409
|
+
const jsonInput = args[0]
|
|
410
|
+
|
|
411
|
+
if (!jsonInput)
|
|
412
|
+
fatal("Missing JSON arguments. Usage: wallet sign-action '{...}'")
|
|
413
|
+
|
|
414
|
+
let signArgs: Parameters<typeof ctx.wallet.signAction>[0]
|
|
415
|
+
try {
|
|
416
|
+
signArgs = JSON.parse(jsonInput)
|
|
417
|
+
} catch {
|
|
418
|
+
fatal(`Invalid JSON: ${jsonInput}`)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const privateKey = await loadKey(resolvePassword())
|
|
422
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
423
|
+
chain: opts.chain,
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
try {
|
|
427
|
+
const result = await ctx.wallet.signAction(signArgs)
|
|
428
|
+
output(result, opts)
|
|
429
|
+
} finally {
|
|
430
|
+
await destroy()
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function walletAbortAction(
|
|
435
|
+
args: string[],
|
|
436
|
+
opts: GlobalFlags,
|
|
437
|
+
): Promise<void> {
|
|
438
|
+
const reference = extractFlag(args, '--reference')
|
|
439
|
+
|
|
440
|
+
if (!reference) fatal('Missing required --reference <ref>')
|
|
441
|
+
|
|
442
|
+
const privateKey = await loadKey(resolvePassword())
|
|
443
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
444
|
+
chain: opts.chain,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
const result = await ctx.wallet.abortAction({ reference })
|
|
449
|
+
output(result, opts)
|
|
450
|
+
} finally {
|
|
451
|
+
await destroy()
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function walletListCertificates(
|
|
456
|
+
args: string[],
|
|
457
|
+
opts: GlobalFlags,
|
|
458
|
+
): Promise<void> {
|
|
459
|
+
const certifiers = extractFlags(args, '--certifiers')
|
|
460
|
+
const types = extractFlags(args, '--types')
|
|
461
|
+
const limitStr = extractFlag(args, '--limit')
|
|
462
|
+
const limit = limitStr ? Number.parseInt(limitStr, 10) : 10
|
|
463
|
+
|
|
464
|
+
if (!Number.isFinite(limit) || limit < 1 || limit > 10000) {
|
|
465
|
+
fatal('--limit must be between 1 and 10000')
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const privateKey = await loadKey(resolvePassword())
|
|
469
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
470
|
+
chain: opts.chain,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const listArgs: Parameters<typeof ctx.wallet.listCertificates>[0] = {
|
|
475
|
+
certifiers: certifiers.length > 0 ? certifiers : [],
|
|
476
|
+
types: types.length > 0 ? types : [],
|
|
477
|
+
limit,
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const result = await ctx.wallet.listCertificates(listArgs)
|
|
481
|
+
|
|
482
|
+
if (opts.json) {
|
|
483
|
+
output(result, opts)
|
|
484
|
+
} else {
|
|
485
|
+
console.log(`\n${result.totalCertificates} total certificates:\n`)
|
|
486
|
+
for (const cert of result.certificates) {
|
|
487
|
+
console.log(` ${cert.type} | ${cert.serialNumber} | ${cert.certifier}`)
|
|
488
|
+
}
|
|
489
|
+
console.log()
|
|
490
|
+
}
|
|
491
|
+
} finally {
|
|
492
|
+
await destroy()
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
async function walletRelinquishCertificate(
|
|
497
|
+
args: string[],
|
|
498
|
+
opts: GlobalFlags,
|
|
499
|
+
): Promise<void> {
|
|
500
|
+
const type = extractFlag(args, '--type')
|
|
501
|
+
const serialNumber = extractFlag(args, '--serialNumber')
|
|
502
|
+
const certifier = extractFlag(args, '--certifier')
|
|
503
|
+
|
|
504
|
+
if (!type) fatal('Missing required --type <type>')
|
|
505
|
+
if (!serialNumber) fatal('Missing required --serialNumber <serial>')
|
|
506
|
+
if (!certifier) fatal('Missing required --certifier <certifier>')
|
|
507
|
+
|
|
508
|
+
const privateKey = await loadKey(resolvePassword())
|
|
509
|
+
const { ctx, destroy } = await loadContext(privateKey, {
|
|
510
|
+
chain: opts.chain,
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const result = await ctx.wallet.relinquishCertificate({
|
|
515
|
+
type,
|
|
516
|
+
serialNumber,
|
|
517
|
+
certifier,
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
if (opts.json) {
|
|
521
|
+
output(result, opts)
|
|
522
|
+
} else {
|
|
523
|
+
output({ relinquished: true, type, serialNumber, certifier }, opts)
|
|
524
|
+
}
|
|
525
|
+
} finally {
|
|
526
|
+
await destroy()
|
|
527
|
+
}
|
|
528
|
+
}
|
package/src/context.ts
CHANGED
|
@@ -38,7 +38,20 @@ async function runMonitorIfStale(
|
|
|
38
38
|
const intervalMs = intervalMinutes * 60 * 1000
|
|
39
39
|
|
|
40
40
|
if (elapsed >= intervalMs) {
|
|
41
|
-
|
|
41
|
+
const originalLog = console.log
|
|
42
|
+
const originalInfo = console.info
|
|
43
|
+
const originalWarn = console.warn
|
|
44
|
+
console.log = () => {}
|
|
45
|
+
console.info = () => {}
|
|
46
|
+
console.warn = () => {}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await walletResult.monitor.runOnce()
|
|
50
|
+
} finally {
|
|
51
|
+
console.log = originalLog
|
|
52
|
+
console.info = originalInfo
|
|
53
|
+
console.warn = originalWarn
|
|
54
|
+
}
|
|
42
55
|
saveMonitorState({ lastMonitorRun: Date.now() })
|
|
43
56
|
}
|
|
44
57
|
}
|
package/src/help.ts
CHANGED
|
@@ -47,6 +47,22 @@ ${bold('Wallet:')}
|
|
|
47
47
|
${cyan('wallet send-all')} Send all BSV to an address
|
|
48
48
|
${cyan('wallet info')} Show wallet info
|
|
49
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
|
+
|
|
50
66
|
${bold('Ordinals:')}
|
|
51
67
|
${cyan('ordinals list')} List owned ordinals
|
|
52
68
|
${cyan('ordinals mint')} Mint a new ordinal inscription
|
|
@@ -69,6 +85,7 @@ ${bold('Locks:')}
|
|
|
69
85
|
|
|
70
86
|
${bold('Identity (BAP):')}
|
|
71
87
|
${cyan('identity create')} Create a new BAP identity
|
|
88
|
+
${cyan('identity update-profile')} Update BAP identity profile (--profile <json>)
|
|
72
89
|
${cyan('identity info')} Show identity information
|
|
73
90
|
${cyan('identity sign')} Sign a message with identity key
|
|
74
91
|
${cyan('identity verify')} Verify a signed message
|