@1sat/cli 0.0.31 → 0.0.33
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 +43 -32
- package/src/cli.ts +6 -0
- package/src/commands/config.ts +66 -41
- package/src/commands/remote.ts +158 -6
- package/src/commands/serve.ts +315 -0
- package/src/config.ts +102 -0
- package/src/context.ts +8 -1
- package/src/monitor-lock.ts +50 -0
package/package.json
CHANGED
|
@@ -1,34 +1,45 @@
|
|
|
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
|
-
|
|
2
|
+
"name": "@1sat/cli",
|
|
3
|
+
"version": "0.0.33",
|
|
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.99",
|
|
27
|
+
"@1sat/client": "0.0.21",
|
|
28
|
+
"@1sat/types": "0.0.17",
|
|
29
|
+
"@1sat/wallet-node": "0.0.30",
|
|
30
|
+
"@1sat/wallet-server": "0.0.2",
|
|
31
|
+
"@bsv/sdk": "^2.0.13",
|
|
32
|
+
"@bsv/wallet-toolbox": "^2.1.21",
|
|
33
|
+
"chalk": "^5.0.0",
|
|
34
|
+
"@clack/prompts": "^0.8.0",
|
|
35
|
+
"bitcoin-backup": "^0.0.11",
|
|
36
|
+
"dotenv": "^17.0.0",
|
|
37
|
+
"evlog": "^2.10.0",
|
|
38
|
+
"knex": "^3.1.0",
|
|
39
|
+
"pg": "^8.11.3"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/bun": "^1.3.9",
|
|
43
|
+
"typescript": "^5.9.3"
|
|
44
|
+
}
|
|
34
45
|
}
|
package/src/cli.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { handleMcpProxyCommand } from './commands/mcp-proxy'
|
|
|
16
16
|
import { handleOpnsCommand } from './commands/opns'
|
|
17
17
|
import { handleOrdinalsCommand } from './commands/ordinals'
|
|
18
18
|
import { handleRemoteCommand } from './commands/remote'
|
|
19
|
+
import { handleServeCommand } from './commands/serve'
|
|
19
20
|
import { handleSocialCommand } from './commands/social'
|
|
20
21
|
import { handleSweepCommand } from './commands/sweep'
|
|
21
22
|
import { handleTokensCommand } from './commands/tokens'
|
|
@@ -98,6 +99,10 @@ async function main(): Promise<void> {
|
|
|
98
99
|
await handleMcpProxyCommand()
|
|
99
100
|
break
|
|
100
101
|
|
|
102
|
+
case 'serve':
|
|
103
|
+
await handleServeCommand(rest, flags)
|
|
104
|
+
break
|
|
105
|
+
|
|
101
106
|
case 'help':
|
|
102
107
|
printHelp()
|
|
103
108
|
break
|
|
@@ -111,5 +116,6 @@ async function main(): Promise<void> {
|
|
|
111
116
|
|
|
112
117
|
main().catch((err) => {
|
|
113
118
|
console.error(formatError(`Error: ${err.message}`))
|
|
119
|
+
if (process.env.DEBUG) console.error(err.stack)
|
|
114
120
|
process.exit(1)
|
|
115
121
|
})
|
package/src/commands/config.ts
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
|
|
11
11
|
import type { GlobalFlags } from '../args'
|
|
12
12
|
import {
|
|
13
|
-
type OneSatCliConfig,
|
|
14
13
|
getConfigDir,
|
|
15
14
|
getConfigFile,
|
|
16
15
|
loadConfig,
|
|
17
|
-
|
|
16
|
+
parseConfigValue,
|
|
17
|
+
setConfigPath,
|
|
18
|
+
unsetConfigPath,
|
|
18
19
|
} from '../config'
|
|
19
20
|
import { printCommandHelp } from '../help'
|
|
20
21
|
import {
|
|
@@ -23,15 +24,8 @@ import {
|
|
|
23
24
|
formatSuccess,
|
|
24
25
|
formatValue,
|
|
25
26
|
output,
|
|
26
|
-
printKeyValue,
|
|
27
27
|
} from '../output'
|
|
28
28
|
|
|
29
|
-
const SETTABLE_KEYS: Array<keyof OneSatCliConfig> = [
|
|
30
|
-
'chain',
|
|
31
|
-
'dataDir',
|
|
32
|
-
'storageIdentityKey',
|
|
33
|
-
]
|
|
34
|
-
|
|
35
29
|
export async function handleConfigCommand(
|
|
36
30
|
args: string[],
|
|
37
31
|
opts: GlobalFlags,
|
|
@@ -69,11 +63,7 @@ function configShow(opts: GlobalFlags): void {
|
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
console.log()
|
|
72
|
-
|
|
73
|
-
chain: config.chain,
|
|
74
|
-
dataDir: config.dataDir,
|
|
75
|
-
storageIdentityKey: config.storageIdentityKey ?? '(not set)',
|
|
76
|
-
})
|
|
66
|
+
printNested(config, '')
|
|
77
67
|
console.log()
|
|
78
68
|
console.log(
|
|
79
69
|
` ${formatLabel('config file:')} ${formatValue(getConfigFile())}`,
|
|
@@ -81,48 +71,83 @@ function configShow(opts: GlobalFlags): void {
|
|
|
81
71
|
console.log()
|
|
82
72
|
}
|
|
83
73
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fatal(
|
|
89
|
-
`Usage: 1sat config set <key> <value>\n\nSettable keys: ${SETTABLE_KEYS.join(', ')}`,
|
|
74
|
+
function printNested(value: unknown, prefix: string): void {
|
|
75
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
76
|
+
console.log(
|
|
77
|
+
` ${formatLabel(`${prefix || '(value)'}:`)} ${formatValue(String(value))}`,
|
|
90
78
|
)
|
|
79
|
+
return
|
|
91
80
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
const obj = value as Record<string, unknown>
|
|
82
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
83
|
+
const path = prefix ? `${prefix}.${k}` : k
|
|
84
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
85
|
+
printNested(v, path)
|
|
86
|
+
} else {
|
|
87
|
+
const display = Array.isArray(v)
|
|
88
|
+
? JSON.stringify(v)
|
|
89
|
+
: String(v ?? '(not set)')
|
|
90
|
+
console.log(` ${formatLabel(`${path}:`)} ${formatValue(display)}`)
|
|
91
|
+
}
|
|
97
92
|
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function configSet(args: string[], opts: GlobalFlags): void {
|
|
96
|
+
const [key, ...valueArgs] = args
|
|
97
|
+
const value = valueArgs.join(' ')
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
fatal("chain must be 'main' or 'test'")
|
|
99
|
+
if (!key || valueArgs.length === 0) {
|
|
100
|
+
fatal('Usage: 1sat config set <dotted.path> <value>')
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
const
|
|
105
|
-
|
|
103
|
+
const parsed = parseConfigValue(value)
|
|
104
|
+
validateKnownPath(key, parsed)
|
|
105
|
+
|
|
106
|
+
setConfigPath(key, parsed)
|
|
107
|
+
output(
|
|
108
|
+
opts.json ? { [key]: parsed } : formatSuccess(`Set ${key} = ${value}`),
|
|
109
|
+
opts,
|
|
110
|
+
)
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
function configUnset(args: string[], opts: GlobalFlags): void {
|
|
109
114
|
const [key] = args
|
|
110
115
|
|
|
111
116
|
if (!key) {
|
|
112
|
-
fatal(
|
|
113
|
-
`Usage: 1sat config unset <key>\n\nKeys that can be unset: ${SETTABLE_KEYS.join(', ')}`,
|
|
114
|
-
)
|
|
117
|
+
fatal('Usage: 1sat config unset <dotted.path>')
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
)
|
|
121
|
-
}
|
|
120
|
+
unsetConfigPath(key)
|
|
121
|
+
output(opts.json ? { [key]: undefined } : formatSuccess(`Unset ${key}`), opts)
|
|
122
|
+
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Enforce the few semantic constraints we care about. Unknown paths are
|
|
126
|
+
* allowed — callers that read them can validate at consumption time.
|
|
127
|
+
*/
|
|
128
|
+
function validateKnownPath(key: string, value: unknown): void {
|
|
129
|
+
switch (key) {
|
|
130
|
+
case 'chain':
|
|
131
|
+
if (value !== 'main' && value !== 'test') {
|
|
132
|
+
fatal("chain must be 'main' or 'test'")
|
|
133
|
+
}
|
|
134
|
+
break
|
|
135
|
+
case 'server.storage.provider':
|
|
136
|
+
if (value !== 'bun-sqlite' && value !== 'knex-pg') {
|
|
137
|
+
fatal("server.storage.provider must be 'bun-sqlite' | 'knex-pg'")
|
|
138
|
+
}
|
|
139
|
+
break
|
|
140
|
+
case 'server.port':
|
|
141
|
+
if (typeof value !== 'number' || value <= 0 || !Number.isInteger(value)) {
|
|
142
|
+
fatal('server.port must be a positive integer')
|
|
143
|
+
}
|
|
144
|
+
break
|
|
145
|
+
case 'server.accounts.enabled':
|
|
146
|
+
if (typeof value !== 'boolean') {
|
|
147
|
+
fatal('server.accounts.enabled must be true or false')
|
|
148
|
+
}
|
|
149
|
+
break
|
|
150
|
+
}
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
function configPath(opts: GlobalFlags): void {
|
package/src/commands/remote.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* set-active - Switch active storage to a remote or back to local
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { StorageClient } from '@1sat/wallet-node'
|
|
12
|
+
import { WalletServerClient, topUpStorage } from '@1sat/wallet-server'
|
|
11
13
|
import { confirm, isCancel, text } from '@clack/prompts'
|
|
12
14
|
import type { GlobalFlags } from '../args'
|
|
13
15
|
import { loadConfig, saveConfig } from '../config'
|
|
@@ -31,6 +33,10 @@ export async function handleRemoteCommand(
|
|
|
31
33
|
return remoteDelete(rest, opts)
|
|
32
34
|
case 'set-active':
|
|
33
35
|
return remoteSetActive(rest, opts)
|
|
36
|
+
case 'status':
|
|
37
|
+
return remoteStatus(rest, opts)
|
|
38
|
+
case 'topup':
|
|
39
|
+
return remoteTopup(rest, opts)
|
|
34
40
|
default:
|
|
35
41
|
printCommandHelp('remote', {
|
|
36
42
|
add: 'Add a remote storage as backup (1sat remote add <url>)',
|
|
@@ -39,6 +45,10 @@ export async function handleRemoteCommand(
|
|
|
39
45
|
'Remove a remote from the backup list (1sat remote delete <url>)',
|
|
40
46
|
'set-active':
|
|
41
47
|
'Switch active storage (1sat remote set-active <url | local>)',
|
|
48
|
+
status:
|
|
49
|
+
'Fetch GET /account/status from a remote (1sat remote status [url])',
|
|
50
|
+
topup:
|
|
51
|
+
'Buy capacity on a remote (1sat remote topup [url] [--units N])',
|
|
42
52
|
})
|
|
43
53
|
if (subcommand && subcommand !== 'help') {
|
|
44
54
|
process.exit(1)
|
|
@@ -85,11 +95,7 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
85
95
|
const config = loadConfig()
|
|
86
96
|
|
|
87
97
|
try {
|
|
88
|
-
|
|
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)
|
|
98
|
+
const client = new StorageClient(walletResult.wallet, url)
|
|
93
99
|
await walletResult.storage.addWalletStorageProvider(client)
|
|
94
100
|
|
|
95
101
|
// When adding a backup, the remote may report itself as "active" which
|
|
@@ -128,7 +134,7 @@ async function remoteAdd(args: string[], opts: GlobalFlags): Promise<void> {
|
|
|
128
134
|
|
|
129
135
|
async function remoteList(_args: string[], opts: GlobalFlags): Promise<void> {
|
|
130
136
|
const privateKey = await loadKey(resolvePassword())
|
|
131
|
-
const {
|
|
137
|
+
const { destroy } = await loadContext(privateKey, {
|
|
132
138
|
chain: opts.chain,
|
|
133
139
|
})
|
|
134
140
|
|
|
@@ -321,6 +327,152 @@ async function remoteSetActive(
|
|
|
321
327
|
}
|
|
322
328
|
}
|
|
323
329
|
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// remote status
|
|
332
|
+
// ============================================================================
|
|
333
|
+
|
|
334
|
+
async function remoteStatus(args: string[], opts: GlobalFlags): Promise<void> {
|
|
335
|
+
const config = loadConfig()
|
|
336
|
+
const url = args[0] ?? config.activeRemote ?? config.backups?.[0]
|
|
337
|
+
if (!url) {
|
|
338
|
+
fatal(
|
|
339
|
+
'No remote URL supplied. Pass one as an argument or configure activeRemote/backups first.',
|
|
340
|
+
)
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
new URL(url)
|
|
344
|
+
} catch {
|
|
345
|
+
fatal(`Invalid URL: ${url}`)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const privateKey = await loadKey(resolvePassword())
|
|
349
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
350
|
+
chain: opts.chain,
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const client = new WalletServerClient(url, walletResult.wallet)
|
|
355
|
+
const status = await client.accountStatus()
|
|
356
|
+
|
|
357
|
+
if (opts.json) {
|
|
358
|
+
output(status, opts)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
console.log()
|
|
363
|
+
console.log(` ${bold('Remote:')} ${url}`)
|
|
364
|
+
console.log(` ${bold('Identity:')} ${status.identityKey}`)
|
|
365
|
+
console.log(
|
|
366
|
+
` ${bold('Accounts:')} ${status.accountsEnabled ? 'on' : 'off'}`,
|
|
367
|
+
)
|
|
368
|
+
if (status.currentBlock != null) {
|
|
369
|
+
console.log(` ${bold('Chain tip:')} block ${status.currentBlock}`)
|
|
370
|
+
}
|
|
371
|
+
if (status.usedBytes != null) {
|
|
372
|
+
console.log(` ${bold('Used:')} ${formatBytes(status.usedBytes)}`)
|
|
373
|
+
}
|
|
374
|
+
if (status.accountsEnabled) {
|
|
375
|
+
console.log(` ${bold('Baseline:')} ${formatBytes(status.baselineBytes)}`)
|
|
376
|
+
console.log(` ${bold('Paid:')} ${formatBytes(status.paidBytes)}`)
|
|
377
|
+
console.log(` ${bold('Capacity:')} ${formatBytes(status.capacityBytes)}`)
|
|
378
|
+
if (status.deficitBytes > 0) {
|
|
379
|
+
console.log(
|
|
380
|
+
` ${bold('Deficit:')} ${formatBytes(status.deficitBytes)} (next write will trigger 402)`,
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
if (status.paidThroughBlock != null) {
|
|
384
|
+
const remaining = status.paidThroughBlock - status.currentBlock
|
|
385
|
+
console.log(
|
|
386
|
+
` ${bold('Paid through:')} block ${status.paidThroughBlock} (${remaining} blocks left)`,
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
console.log(
|
|
390
|
+
` ${bold('Pricing:')} ${status.pricing.satsPerUnit} sats per ${formatBytes(status.pricing.purchaseUnitBytes)} over ${status.pricing.durationBlocks} blocks`,
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
console.log()
|
|
394
|
+
} finally {
|
|
395
|
+
await destroy()
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ============================================================================
|
|
400
|
+
// remote topup
|
|
401
|
+
// ============================================================================
|
|
402
|
+
|
|
403
|
+
async function remoteTopup(args: string[], opts: GlobalFlags): Promise<void> {
|
|
404
|
+
let url: string | undefined
|
|
405
|
+
let units: number | undefined
|
|
406
|
+
for (let i = 0; i < args.length; i++) {
|
|
407
|
+
const arg = args[i]
|
|
408
|
+
if (arg === '--units') {
|
|
409
|
+
const val = args[++i]
|
|
410
|
+
const parsed = Number(val)
|
|
411
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
412
|
+
fatal(`--units requires a positive integer, got "${val}"`)
|
|
413
|
+
}
|
|
414
|
+
units = parsed
|
|
415
|
+
} else if (!url) {
|
|
416
|
+
url = arg
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const config = loadConfig()
|
|
421
|
+
url = url ?? config.activeRemote ?? config.backups?.[0]
|
|
422
|
+
if (!url) {
|
|
423
|
+
fatal(
|
|
424
|
+
'No remote URL supplied. Pass one as an argument or configure activeRemote/backups first.',
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
new URL(url)
|
|
429
|
+
} catch {
|
|
430
|
+
fatal(`Invalid URL: ${url}`)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const privateKey = await loadKey(resolvePassword())
|
|
434
|
+
const { walletResult, destroy } = await loadContext(privateKey, {
|
|
435
|
+
chain: opts.chain,
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const result = await topUpStorage(walletResult.wallet, url, { units })
|
|
440
|
+
|
|
441
|
+
if (opts.json) {
|
|
442
|
+
output(result, opts)
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log()
|
|
447
|
+
console.log(` ${bold('Remote:')} ${url}`)
|
|
448
|
+
console.log(` ${bold('Units bought:')} ${result.unitsBought}`)
|
|
449
|
+
console.log(` ${bold('Sats paid:')} ${result.satsPaid}`)
|
|
450
|
+
console.log(` ${bold('Payment txid:')} ${result.txid}`)
|
|
451
|
+
if (result.status.accountsEnabled) {
|
|
452
|
+
console.log()
|
|
453
|
+
console.log(
|
|
454
|
+
` ${bold('New capacity:')} ${formatBytes(result.status.capacityBytes)} (${formatBytes(result.status.usedBytes)} used, ${formatBytes(result.status.deficitBytes)} deficit)`,
|
|
455
|
+
)
|
|
456
|
+
if (result.status.paidThroughBlock != null) {
|
|
457
|
+
const remaining = result.status.paidThroughBlock - result.status.currentBlock
|
|
458
|
+
console.log(
|
|
459
|
+
` ${bold('Paid through:')} block ${result.status.paidThroughBlock} (${remaining} blocks left)`,
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
console.log()
|
|
464
|
+
} finally {
|
|
465
|
+
await destroy()
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function formatBytes(n: number): string {
|
|
470
|
+
if (n < 1024) return `${n} B`
|
|
471
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`
|
|
472
|
+
if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`
|
|
473
|
+
return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
|
474
|
+
}
|
|
475
|
+
|
|
324
476
|
// ============================================================================
|
|
325
477
|
// Helpers
|
|
326
478
|
// ============================================================================
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `1sat serve` command — launch wallet server and/or monitor.
|
|
3
|
+
*
|
|
4
|
+
* 1sat serve Wallet server + monitor daemon
|
|
5
|
+
* 1sat serve wallet Wallet server only
|
|
6
|
+
* 1sat serve monitor Monitor daemon only
|
|
7
|
+
*
|
|
8
|
+
* The server wraps the same wallet instance the CLI uses. Storage, active
|
|
9
|
+
* remote, and backups all come from `~/.1sat/cli/config.json` via the same
|
|
10
|
+
* `createNodeWallet` factory `1sat wallet` commands use.
|
|
11
|
+
*
|
|
12
|
+
* Server-only settings live under `server.*` in the config:
|
|
13
|
+
* 1sat config set server.port 8100
|
|
14
|
+
* 1sat config set server.host 0.0.0.0
|
|
15
|
+
* 1sat config set server.accounts.enabled true
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import {
|
|
20
|
+
type NodeWalletResult,
|
|
21
|
+
type NodeWalletStorageConfig,
|
|
22
|
+
createNodeWallet,
|
|
23
|
+
} from '@1sat/wallet-node'
|
|
24
|
+
import { createWalletServer } from '@1sat/wallet-server'
|
|
25
|
+
import type { PrivateKey } from '@bsv/sdk'
|
|
26
|
+
import type { GlobalFlags } from '../args'
|
|
27
|
+
import {
|
|
28
|
+
type ServerAccountsConfig,
|
|
29
|
+
type ServerStorageConfig,
|
|
30
|
+
loadConfig,
|
|
31
|
+
} from '../config'
|
|
32
|
+
import { ensureDataDir } from '../config'
|
|
33
|
+
import { printCommandHelp } from '../help'
|
|
34
|
+
import { loadKey, resolvePassword } from '../keys'
|
|
35
|
+
import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
|
|
36
|
+
import { fatal } from '../output'
|
|
37
|
+
|
|
38
|
+
const DEFAULT_HOST = '127.0.0.1'
|
|
39
|
+
const DEFAULT_PORT = 8100
|
|
40
|
+
const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
|
|
41
|
+
const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
|
|
42
|
+
const DEFAULT_PURCHASE_UNIT_BYTES = 1_073_741_824 // 1 GB chunks for production
|
|
43
|
+
const DEFAULT_SATS_PER_UNIT = 1_000_000
|
|
44
|
+
const DEFAULT_DURATION_BLOCKS = 4383
|
|
45
|
+
const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
|
|
46
|
+
|
|
47
|
+
type ServeMode = 'all' | 'wallet' | 'monitor'
|
|
48
|
+
|
|
49
|
+
interface ResolvedServe {
|
|
50
|
+
chain: 'main' | 'test'
|
|
51
|
+
host: string
|
|
52
|
+
port: number
|
|
53
|
+
onesatURL: string
|
|
54
|
+
storage: ServerStorageConfig
|
|
55
|
+
dataDir: string
|
|
56
|
+
sqliteFilename: string
|
|
57
|
+
storageIdentityKey: string
|
|
58
|
+
activeRemote?: string
|
|
59
|
+
backups?: string[]
|
|
60
|
+
accounts: ResolvedAccounts
|
|
61
|
+
privateKey: PrivateKey
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ResolvedAccounts {
|
|
65
|
+
enabled: boolean
|
|
66
|
+
baselineBytes: number
|
|
67
|
+
purchaseUnitBytes: number
|
|
68
|
+
satsPerUnit: number
|
|
69
|
+
durationBlocks: number
|
|
70
|
+
freeIdentityKeys: string[]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function handleServeCommand(
|
|
74
|
+
args: string[],
|
|
75
|
+
opts: GlobalFlags,
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const [subcommand] = args
|
|
78
|
+
const mode = resolveMode(subcommand)
|
|
79
|
+
|
|
80
|
+
if (mode === null) {
|
|
81
|
+
printCommandHelp('serve', {
|
|
82
|
+
'(no subcommand)': 'Wallet server plus monitor daemon',
|
|
83
|
+
wallet: 'Wallet server only',
|
|
84
|
+
monitor: 'Monitor daemon only',
|
|
85
|
+
})
|
|
86
|
+
if (subcommand && subcommand !== 'help') process.exit(1)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const resolved = await resolveServe(opts)
|
|
91
|
+
const handles: Stoppable[] = []
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
handles.push(await runWithStorage(resolved, mode))
|
|
95
|
+
await waitForShutdown()
|
|
96
|
+
} finally {
|
|
97
|
+
for (const h of handles.reverse()) {
|
|
98
|
+
try {
|
|
99
|
+
await h.stop()
|
|
100
|
+
} catch (err) {
|
|
101
|
+
console.error(`Error during shutdown: ${(err as Error).message}`)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveMode(subcommand: string | undefined): ServeMode | null {
|
|
108
|
+
if (!subcommand) return 'all'
|
|
109
|
+
switch (subcommand) {
|
|
110
|
+
case 'wallet':
|
|
111
|
+
case 'monitor':
|
|
112
|
+
return subcommand
|
|
113
|
+
default:
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Load the CLI config, apply serve defaults, and resolve the server identity
|
|
120
|
+
* key via the existing keyring mechanism.
|
|
121
|
+
*/
|
|
122
|
+
async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
|
|
123
|
+
const config = loadConfig()
|
|
124
|
+
const server = config.server ?? {}
|
|
125
|
+
|
|
126
|
+
const storage: ServerStorageConfig = server.storage ?? {
|
|
127
|
+
provider: 'bun-sqlite',
|
|
128
|
+
}
|
|
129
|
+
if (storage.provider === 'knex-pg' && !storage.dbUrl) {
|
|
130
|
+
fatal(
|
|
131
|
+
'server.storage.provider is knex-pg but server.storage.dbUrl is not set. ' +
|
|
132
|
+
'Set it with: 1sat config set server.storage.dbUrl postgres://…',
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const dataDir = ensureDataDir()
|
|
137
|
+
const chain = opts.chain ?? config.chain ?? 'main'
|
|
138
|
+
|
|
139
|
+
let privateKey: PrivateKey
|
|
140
|
+
try {
|
|
141
|
+
privateKey = await loadKey(resolvePassword())
|
|
142
|
+
} catch (err) {
|
|
143
|
+
fatal((err as Error).message)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
chain,
|
|
148
|
+
host: server.host ?? DEFAULT_HOST,
|
|
149
|
+
port: server.port ?? DEFAULT_PORT,
|
|
150
|
+
onesatURL: DEFAULT_ONESAT_URL,
|
|
151
|
+
storage,
|
|
152
|
+
dataDir,
|
|
153
|
+
sqliteFilename: deriveSqliteFilename(dataDir, chain),
|
|
154
|
+
storageIdentityKey:
|
|
155
|
+
config.storageIdentityKey ?? DEFAULT_STORAGE_IDENTITY_KEY,
|
|
156
|
+
activeRemote: config.activeRemote,
|
|
157
|
+
backups: config.backups,
|
|
158
|
+
accounts: resolveAccounts(server.accounts),
|
|
159
|
+
privateKey,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function deriveSqliteFilename(dataDir: string, chain: string): string {
|
|
164
|
+
return join(dataDir, `wallet-${chain}.db`)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function resolveWalletStorageConfig(
|
|
168
|
+
resolved: ResolvedServe,
|
|
169
|
+
): NodeWalletStorageConfig {
|
|
170
|
+
const storage = resolved.storage
|
|
171
|
+
if (storage.provider === 'bun-sqlite') {
|
|
172
|
+
return { provider: 'bun-sqlite', filename: resolved.sqliteFilename }
|
|
173
|
+
}
|
|
174
|
+
if (storage.provider === 'knex-pg') {
|
|
175
|
+
return { provider: 'knex-pg', dbUrl: storage.dbUrl }
|
|
176
|
+
}
|
|
177
|
+
fatal(
|
|
178
|
+
`server.storage.provider '${storage.provider}' is not supported. Use 'bun-sqlite' or 'knex-pg'.`,
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
|
|
183
|
+
return {
|
|
184
|
+
enabled: accounts?.enabled ?? false,
|
|
185
|
+
baselineBytes: accounts?.baselineBytes ?? DEFAULT_BASELINE_BYTES,
|
|
186
|
+
purchaseUnitBytes:
|
|
187
|
+
accounts?.purchaseUnitBytes ?? DEFAULT_PURCHASE_UNIT_BYTES,
|
|
188
|
+
satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
|
|
189
|
+
durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
|
|
190
|
+
freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
interface Stoppable {
|
|
195
|
+
stop(): Promise<void>
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Construct the wallet via the same `createNodeWallet` factory the CLI
|
|
200
|
+
* uses, with storage provider (bun-sqlite / knex-pg) chosen from config.
|
|
201
|
+
* Server + monitor operate on that single wallet instance, so
|
|
202
|
+
* `activeRemote`, `backups`, and `storageIdentityKey` behave identically
|
|
203
|
+
* to `1sat wallet <command>`.
|
|
204
|
+
*/
|
|
205
|
+
async function runWithStorage(
|
|
206
|
+
resolved: ResolvedServe,
|
|
207
|
+
mode: ServeMode,
|
|
208
|
+
): Promise<Stoppable> {
|
|
209
|
+
const storage = resolveWalletStorageConfig(resolved)
|
|
210
|
+
const walletResult = await createNodeWallet({
|
|
211
|
+
privateKey: resolved.privateKey,
|
|
212
|
+
chain: resolved.chain,
|
|
213
|
+
storageIdentityKey: resolved.storageIdentityKey,
|
|
214
|
+
storage,
|
|
215
|
+
activeRemote: resolved.activeRemote,
|
|
216
|
+
backups: resolved.backups,
|
|
217
|
+
// Server owns the monitor loop; suppress the factory's initial
|
|
218
|
+
// runOnce so CLI invocations in the same data dir don't race with it.
|
|
219
|
+
skipInitialMonitor: mode !== 'wallet',
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const accounts =
|
|
223
|
+
mode === 'monitor'
|
|
224
|
+
? undefined
|
|
225
|
+
: await buildAccountsForServer(resolved, walletResult)
|
|
226
|
+
|
|
227
|
+
const serverHandle =
|
|
228
|
+
mode === 'monitor'
|
|
229
|
+
? undefined
|
|
230
|
+
: await startWalletServer(resolved, walletResult, accounts)
|
|
231
|
+
|
|
232
|
+
if (mode !== 'wallet') {
|
|
233
|
+
// startTasks loops until stopTasks flips its flag. Fire without
|
|
234
|
+
// awaiting so the caller can install shutdown handlers and write
|
|
235
|
+
// the monitor pid file.
|
|
236
|
+
walletResult.monitor.startTasks().catch((err: unknown) => {
|
|
237
|
+
console.error('[monitor] task loop exited:', err)
|
|
238
|
+
})
|
|
239
|
+
writeMonitorPid(resolved.dataDir)
|
|
240
|
+
console.log('[monitor] started')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
async stop() {
|
|
245
|
+
if (mode !== 'wallet') {
|
|
246
|
+
walletResult.monitor.stopTasks()
|
|
247
|
+
clearMonitorPid(resolved.dataDir)
|
|
248
|
+
}
|
|
249
|
+
if (serverHandle) await serverHandle.stop()
|
|
250
|
+
// Accounts shares the wallet's connection — walletResult.destroy
|
|
251
|
+
// below closes it. No separate teardown needed.
|
|
252
|
+
await walletResult.destroy()
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function startWalletServer(
|
|
258
|
+
resolved: ResolvedServe,
|
|
259
|
+
walletResult: NodeWalletResult,
|
|
260
|
+
accounts: AccountsRuntime | undefined,
|
|
261
|
+
): Promise<{ stop(): Promise<void> }> {
|
|
262
|
+
const handle = createWalletServer({
|
|
263
|
+
wallet: walletResult.wallet,
|
|
264
|
+
storage: walletResult.getActiveStorage(),
|
|
265
|
+
serverIdentityKey: walletResult.wallet.identityKey,
|
|
266
|
+
listen: { port: resolved.port, host: resolved.host },
|
|
267
|
+
publicPath: '/',
|
|
268
|
+
internalPath: null,
|
|
269
|
+
accounts: accounts?.walletServerAccounts,
|
|
270
|
+
})
|
|
271
|
+
const port = await handle.start()
|
|
272
|
+
const accountsNote = resolved.accounts.enabled ? ' (accounts: on)' : ''
|
|
273
|
+
console.log(`[wallet] listening on ${resolved.host}:${port}${accountsNote}`)
|
|
274
|
+
return { stop: () => handle.stop() }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
interface AccountsRuntime {
|
|
278
|
+
walletServerAccounts: NonNullable<
|
|
279
|
+
Parameters<typeof createWalletServer>[0]['accounts']
|
|
280
|
+
>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function buildAccountsForServer(
|
|
284
|
+
resolved: ResolvedServe,
|
|
285
|
+
walletResult: NodeWalletResult,
|
|
286
|
+
): Promise<AccountsRuntime | undefined> {
|
|
287
|
+
// When the wallet is remote-primary the server is fronting someone else's
|
|
288
|
+
// storage; accounts semantics don't apply.
|
|
289
|
+
if (resolved.activeRemote) return undefined
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
walletServerAccounts: {
|
|
293
|
+
config: {
|
|
294
|
+
enabled: resolved.accounts.enabled,
|
|
295
|
+
baselineBytes: resolved.accounts.baselineBytes,
|
|
296
|
+
purchaseUnitBytes: resolved.accounts.purchaseUnitBytes,
|
|
297
|
+
satsPerUnit: resolved.accounts.satsPerUnit,
|
|
298
|
+
durationBlocks: resolved.accounts.durationBlocks,
|
|
299
|
+
freeIdentityKeys: resolved.accounts.freeIdentityKeys,
|
|
300
|
+
},
|
|
301
|
+
currentBlock: () => walletResult.services.chaintracks.currentHeight(),
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function waitForShutdown(): Promise<void> {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const handler = (sig: string) => {
|
|
309
|
+
console.log(`Received ${sig}, shutting down...`)
|
|
310
|
+
resolve()
|
|
311
|
+
}
|
|
312
|
+
process.once('SIGINT', () => handler('SIGINT'))
|
|
313
|
+
process.once('SIGTERM', () => handler('SIGTERM'))
|
|
314
|
+
})
|
|
315
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -11,6 +11,46 @@ import { join } from 'node:path'
|
|
|
11
11
|
const CONFIG_DIR = join(homedir(), '.1sat', 'cli')
|
|
12
12
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json')
|
|
13
13
|
|
|
14
|
+
export interface ServerStorageBunSqliteConfig {
|
|
15
|
+
provider: 'bun-sqlite'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ServerStorageKnexPgConfig {
|
|
19
|
+
provider: 'knex-pg'
|
|
20
|
+
/** Postgres connection URL (required for knex-pg). */
|
|
21
|
+
dbUrl: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ServerStorageConfig =
|
|
25
|
+
| ServerStorageBunSqliteConfig
|
|
26
|
+
| ServerStorageKnexPgConfig
|
|
27
|
+
|
|
28
|
+
export interface ServerAccountsConfig {
|
|
29
|
+
/** Master toggle. Defaults to false when omitted. */
|
|
30
|
+
enabled?: boolean
|
|
31
|
+
/** Free baseline per identity key, in bytes. */
|
|
32
|
+
baselineBytes?: number
|
|
33
|
+
/** Purchase chunk size in bytes. Deficits round up to whole chunks. Defaults to 1 GB. */
|
|
34
|
+
purchaseUnitBytes?: number
|
|
35
|
+
/** Sats charged per purchase unit. */
|
|
36
|
+
satsPerUnit?: number
|
|
37
|
+
/** Block window a payment remains valid for. */
|
|
38
|
+
durationBlocks?: number
|
|
39
|
+
/** Identity keys that bypass metering (server's own key is auto-added). */
|
|
40
|
+
freeIdentityKeys?: string[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ServerConfig {
|
|
44
|
+
/** Hostname to bind. Defaults to `127.0.0.1`. */
|
|
45
|
+
host?: string
|
|
46
|
+
/** Port to bind. Defaults to `8100`. */
|
|
47
|
+
port?: number
|
|
48
|
+
/** Storage backend. Defaults to `{ provider: 'bun-sqlite' }`. */
|
|
49
|
+
storage?: ServerStorageConfig
|
|
50
|
+
/** Optional account/metering layer (opt-in per-deployment). */
|
|
51
|
+
accounts?: ServerAccountsConfig
|
|
52
|
+
}
|
|
53
|
+
|
|
14
54
|
export interface OneSatCliConfig {
|
|
15
55
|
/** Network: mainnet or testnet */
|
|
16
56
|
chain: 'main' | 'test'
|
|
@@ -22,6 +62,8 @@ export interface OneSatCliConfig {
|
|
|
22
62
|
backups?: string[]
|
|
23
63
|
/** Storage identity key for wallet persistence */
|
|
24
64
|
storageIdentityKey?: string
|
|
65
|
+
/** Settings read by `1sat serve` subcommands. Absent for client-only installs. */
|
|
66
|
+
server?: ServerConfig
|
|
25
67
|
}
|
|
26
68
|
|
|
27
69
|
const DEFAULT_CONFIG: OneSatCliConfig = {
|
|
@@ -72,6 +114,66 @@ export function updateConfig(patch: Partial<OneSatCliConfig>): OneSatCliConfig {
|
|
|
72
114
|
return next
|
|
73
115
|
}
|
|
74
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Set a value at a dotted path inside the config, creating intermediate
|
|
119
|
+
* objects as needed. `value` is stored as-is; callers are responsible for
|
|
120
|
+
* type coercion (typically via `parseConfigValue`).
|
|
121
|
+
*/
|
|
122
|
+
export function setConfigPath(path: string, value: unknown): OneSatCliConfig {
|
|
123
|
+
if (!path) throw new Error('setConfigPath requires a non-empty path')
|
|
124
|
+
const config = loadConfig() as Record<string, unknown>
|
|
125
|
+
const segments = path.split('.')
|
|
126
|
+
let cursor: Record<string, unknown> = config
|
|
127
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
128
|
+
const key = segments[i]
|
|
129
|
+
const existing = cursor[key]
|
|
130
|
+
if (
|
|
131
|
+
existing == null ||
|
|
132
|
+
typeof existing !== 'object' ||
|
|
133
|
+
Array.isArray(existing)
|
|
134
|
+
) {
|
|
135
|
+
cursor[key] = {}
|
|
136
|
+
}
|
|
137
|
+
cursor = cursor[key] as Record<string, unknown>
|
|
138
|
+
}
|
|
139
|
+
cursor[segments[segments.length - 1]] = value
|
|
140
|
+
saveConfig(config as OneSatCliConfig)
|
|
141
|
+
return config as OneSatCliConfig
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove a value at a dotted path. Leaves empty parent objects in place to
|
|
146
|
+
* keep the file shape explicit.
|
|
147
|
+
*/
|
|
148
|
+
export function unsetConfigPath(path: string): OneSatCliConfig {
|
|
149
|
+
if (!path) throw new Error('unsetConfigPath requires a non-empty path')
|
|
150
|
+
const config = loadConfig() as Record<string, unknown>
|
|
151
|
+
const segments = path.split('.')
|
|
152
|
+
let cursor: Record<string, unknown> | undefined = config
|
|
153
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
154
|
+
const next = cursor?.[segments[i]]
|
|
155
|
+
if (next == null || typeof next !== 'object')
|
|
156
|
+
return config as OneSatCliConfig
|
|
157
|
+
cursor = next as Record<string, unknown>
|
|
158
|
+
}
|
|
159
|
+
if (cursor) delete cursor[segments[segments.length - 1]]
|
|
160
|
+
saveConfig(config as OneSatCliConfig)
|
|
161
|
+
return config as OneSatCliConfig
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse a raw CLI argument into a typed JSON value. Tries `JSON.parse`
|
|
166
|
+
* first so numbers, booleans, objects and arrays round-trip naturally;
|
|
167
|
+
* falls back to the raw string for bare words that aren't valid JSON.
|
|
168
|
+
*/
|
|
169
|
+
export function parseConfigValue(raw: string): unknown {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(raw)
|
|
172
|
+
} catch {
|
|
173
|
+
return raw
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
75
177
|
/**
|
|
76
178
|
* Get the config directory path.
|
|
77
179
|
*/
|
package/src/context.ts
CHANGED
|
@@ -8,6 +8,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
10
|
import { ensureDataDir, loadConfig } from './config'
|
|
11
|
+
import { readLiveMonitorPid } from './monitor-lock'
|
|
11
12
|
|
|
12
13
|
/** Extended context that includes cleanup */
|
|
13
14
|
export interface CliContext {
|
|
@@ -36,13 +37,19 @@ export async function loadContext(
|
|
|
36
37
|
|
|
37
38
|
const storageIdentityKey = config.storageIdentityKey ?? '1sat-cli-default'
|
|
38
39
|
|
|
40
|
+
const skipInitialMonitor = readLiveMonitorPid(dataDir) !== undefined
|
|
41
|
+
|
|
39
42
|
const walletResult = await createNodeWallet({
|
|
40
43
|
privateKey,
|
|
41
44
|
chain: opts.chain,
|
|
42
45
|
storageIdentityKey,
|
|
43
|
-
|
|
46
|
+
storage: {
|
|
47
|
+
provider: 'bun-sqlite',
|
|
48
|
+
filename: `${dataDir}/wallet-${opts.chain}.db`,
|
|
49
|
+
},
|
|
44
50
|
activeRemote: config.activeRemote,
|
|
45
51
|
backups: config.backups,
|
|
52
|
+
skipInitialMonitor,
|
|
46
53
|
})
|
|
47
54
|
|
|
48
55
|
const ctx = createContext(walletResult.wallet, {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID lock file coordinating a single monitor owner across CLI invocations
|
|
3
|
+
* and a long-running `1sat serve` process.
|
|
4
|
+
*
|
|
5
|
+
* - `1sat serve` (modes that run the monitor) writes the pid on startup and
|
|
6
|
+
* removes it on clean shutdown.
|
|
7
|
+
* - CLI invocations read the file and, if the pid is alive, skip firing
|
|
8
|
+
* their own `monitor.runOnce()` to avoid duplicate work against the same
|
|
9
|
+
* SQLite file.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
|
|
15
|
+
export const MONITOR_PID_FILENAME = 'monitor.pid'
|
|
16
|
+
|
|
17
|
+
export function monitorPidPath(dataDir: string): string {
|
|
18
|
+
return join(dataDir, MONITOR_PID_FILENAME)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeMonitorPid(
|
|
22
|
+
dataDir: string,
|
|
23
|
+
pid: number = process.pid,
|
|
24
|
+
): void {
|
|
25
|
+
writeFileSync(monitorPidPath(dataDir), `${pid}\n`, 'utf8')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clearMonitorPid(dataDir: string): void {
|
|
29
|
+
try {
|
|
30
|
+
unlinkSync(monitorPidPath(dataDir))
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Returns the pid of a live monitor owner, or undefined if none. */
|
|
35
|
+
export function readLiveMonitorPid(dataDir: string): number | undefined {
|
|
36
|
+
let raw: string
|
|
37
|
+
try {
|
|
38
|
+
raw = readFileSync(monitorPidPath(dataDir), 'utf8')
|
|
39
|
+
} catch {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
const pid = Number.parseInt(raw.trim(), 10)
|
|
43
|
+
if (!Number.isInteger(pid) || pid <= 0) return undefined
|
|
44
|
+
try {
|
|
45
|
+
process.kill(pid, 0)
|
|
46
|
+
return pid
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|