@1sat/cli 0.0.32 → 0.0.34
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 -36
- package/src/cli.ts +1 -0
- package/src/commands/config.ts +2 -8
- package/src/commands/remote.ts +158 -6
- package/src/commands/serve.ts +65 -47
- package/src/config.ts +4 -7
- package/src/context.ts +8 -1
- package/src/monitor-lock.ts +50 -0
package/package.json
CHANGED
|
@@ -1,38 +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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
2
|
+
"name": "@1sat/cli",
|
|
3
|
+
"version": "0.0.34",
|
|
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.100",
|
|
27
|
+
"@1sat/client": "0.0.21",
|
|
28
|
+
"@1sat/types": "0.0.17",
|
|
29
|
+
"@1sat/wallet-node": "0.0.31",
|
|
30
|
+
"@1sat/wallet-server": "0.0.3",
|
|
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
|
+
}
|
|
38
45
|
}
|
package/src/cli.ts
CHANGED
package/src/commands/config.ts
CHANGED
|
@@ -133,14 +133,8 @@ function validateKnownPath(key: string, value: unknown): void {
|
|
|
133
133
|
}
|
|
134
134
|
break
|
|
135
135
|
case 'server.storage.provider':
|
|
136
|
-
if (
|
|
137
|
-
|
|
138
|
-
value !== 'knex-sqlite' &&
|
|
139
|
-
value !== 'knex-pg'
|
|
140
|
-
) {
|
|
141
|
-
fatal(
|
|
142
|
-
"server.storage.provider must be 'bun-sqlite' | 'knex-sqlite' | 'knex-pg'",
|
|
143
|
-
)
|
|
136
|
+
if (value !== 'bun-sqlite' && value !== 'knex-pg') {
|
|
137
|
+
fatal("server.storage.provider must be 'bun-sqlite' | 'knex-pg'")
|
|
144
138
|
}
|
|
145
139
|
break
|
|
146
140
|
case 'server.port':
|
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
|
// ============================================================================
|
package/src/commands/serve.ts
CHANGED
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { join } from 'node:path'
|
|
19
|
-
import { OneSatServices } from '@1sat/client'
|
|
20
|
-
import { type NodeWalletResult, createNodeWallet } from '@1sat/wallet-node'
|
|
21
19
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
type NodeWalletResult,
|
|
21
|
+
type NodeWalletStorageConfig,
|
|
22
|
+
createNodeWallet,
|
|
23
|
+
} from '@1sat/wallet-node'
|
|
24
|
+
import { createWalletServer } from '@1sat/wallet-server'
|
|
25
25
|
import type { PrivateKey } from '@bsv/sdk'
|
|
26
|
-
import { type Knex, knex } from 'knex'
|
|
27
26
|
import type { GlobalFlags } from '../args'
|
|
28
27
|
import {
|
|
29
28
|
type ServerAccountsConfig,
|
|
@@ -33,13 +32,15 @@ import {
|
|
|
33
32
|
import { ensureDataDir } from '../config'
|
|
34
33
|
import { printCommandHelp } from '../help'
|
|
35
34
|
import { loadKey, resolvePassword } from '../keys'
|
|
35
|
+
import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
|
|
36
36
|
import { fatal } from '../output'
|
|
37
37
|
|
|
38
38
|
const DEFAULT_HOST = '127.0.0.1'
|
|
39
39
|
const DEFAULT_PORT = 8100
|
|
40
40
|
const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
|
|
41
41
|
const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
|
|
42
|
-
const
|
|
42
|
+
const DEFAULT_PURCHASE_UNIT_BYTES = 1_073_741_824 // 1 GB chunks for production
|
|
43
|
+
const DEFAULT_SATS_PER_UNIT = 1_000_000
|
|
43
44
|
const DEFAULT_DURATION_BLOCKS = 4383
|
|
44
45
|
const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
|
|
45
46
|
|
|
@@ -63,7 +64,8 @@ interface ResolvedServe {
|
|
|
63
64
|
interface ResolvedAccounts {
|
|
64
65
|
enabled: boolean
|
|
65
66
|
baselineBytes: number
|
|
66
|
-
|
|
67
|
+
purchaseUnitBytes: number
|
|
68
|
+
satsPerUnit: number
|
|
67
69
|
durationBlocks: number
|
|
68
70
|
freeIdentityKeys: string[]
|
|
69
71
|
}
|
|
@@ -89,14 +91,7 @@ export async function handleServeCommand(
|
|
|
89
91
|
const handles: Stoppable[] = []
|
|
90
92
|
|
|
91
93
|
try {
|
|
92
|
-
|
|
93
|
-
handles.push(await runBunSqlite(resolved, mode))
|
|
94
|
-
} else {
|
|
95
|
-
fatal(
|
|
96
|
-
`server.storage.provider '${resolved.storage.provider}' is not yet wired through the shared wallet factory. Only bun-sqlite is supported at the moment.`,
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
94
|
+
handles.push(await runWithStorage(resolved, mode))
|
|
100
95
|
await waitForShutdown()
|
|
101
96
|
} finally {
|
|
102
97
|
for (const h of handles.reverse()) {
|
|
@@ -169,11 +164,28 @@ function deriveSqliteFilename(dataDir: string, chain: string): string {
|
|
|
169
164
|
return join(dataDir, `wallet-${chain}.db`)
|
|
170
165
|
}
|
|
171
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
|
+
|
|
172
182
|
function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
|
|
173
183
|
return {
|
|
174
184
|
enabled: accounts?.enabled ?? false,
|
|
175
185
|
baselineBytes: accounts?.baselineBytes ?? DEFAULT_BASELINE_BYTES,
|
|
176
|
-
|
|
186
|
+
purchaseUnitBytes:
|
|
187
|
+
accounts?.purchaseUnitBytes ?? DEFAULT_PURCHASE_UNIT_BYTES,
|
|
188
|
+
satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
|
|
177
189
|
durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
|
|
178
190
|
freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
|
|
179
191
|
}
|
|
@@ -184,25 +196,33 @@ interface Stoppable {
|
|
|
184
196
|
}
|
|
185
197
|
|
|
186
198
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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>`.
|
|
191
204
|
*/
|
|
192
|
-
async function
|
|
205
|
+
async function runWithStorage(
|
|
193
206
|
resolved: ResolvedServe,
|
|
194
207
|
mode: ServeMode,
|
|
195
208
|
): Promise<Stoppable> {
|
|
209
|
+
const storage = resolveWalletStorageConfig(resolved)
|
|
196
210
|
const walletResult = await createNodeWallet({
|
|
197
211
|
privateKey: resolved.privateKey,
|
|
198
212
|
chain: resolved.chain,
|
|
199
213
|
storageIdentityKey: resolved.storageIdentityKey,
|
|
200
|
-
|
|
214
|
+
storage,
|
|
201
215
|
activeRemote: resolved.activeRemote,
|
|
202
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',
|
|
203
220
|
})
|
|
204
221
|
|
|
205
|
-
const accounts =
|
|
222
|
+
const accounts =
|
|
223
|
+
mode === 'monitor'
|
|
224
|
+
? undefined
|
|
225
|
+
: await buildAccountsForServer(resolved, walletResult)
|
|
206
226
|
|
|
207
227
|
const serverHandle =
|
|
208
228
|
mode === 'monitor'
|
|
@@ -210,7 +230,13 @@ async function runBunSqlite(
|
|
|
210
230
|
: await startWalletServer(resolved, walletResult, accounts)
|
|
211
231
|
|
|
212
232
|
if (mode !== 'wallet') {
|
|
213
|
-
|
|
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)
|
|
214
240
|
console.log('[monitor] started')
|
|
215
241
|
}
|
|
216
242
|
|
|
@@ -218,9 +244,11 @@ async function runBunSqlite(
|
|
|
218
244
|
async stop() {
|
|
219
245
|
if (mode !== 'wallet') {
|
|
220
246
|
walletResult.monitor.stopTasks()
|
|
247
|
+
clearMonitorPid(resolved.dataDir)
|
|
221
248
|
}
|
|
222
249
|
if (serverHandle) await serverHandle.stop()
|
|
223
|
-
|
|
250
|
+
// Accounts shares the wallet's connection — walletResult.destroy
|
|
251
|
+
// below closes it. No separate teardown needed.
|
|
224
252
|
await walletResult.destroy()
|
|
225
253
|
},
|
|
226
254
|
}
|
|
@@ -232,8 +260,9 @@ async function startWalletServer(
|
|
|
232
260
|
accounts: AccountsRuntime | undefined,
|
|
233
261
|
): Promise<{ stop(): Promise<void> }> {
|
|
234
262
|
const handle = createWalletServer({
|
|
235
|
-
|
|
236
|
-
|
|
263
|
+
wallet: walletResult.wallet,
|
|
264
|
+
storage: walletResult.getActiveStorage(),
|
|
265
|
+
serverIdentityKey: walletResult.wallet.identityKey,
|
|
237
266
|
listen: { port: resolved.port, host: resolved.host },
|
|
238
267
|
publicPath: '/',
|
|
239
268
|
internalPath: null,
|
|
@@ -249,38 +278,27 @@ interface AccountsRuntime {
|
|
|
249
278
|
walletServerAccounts: NonNullable<
|
|
250
279
|
Parameters<typeof createWalletServer>[0]['accounts']
|
|
251
280
|
>
|
|
252
|
-
knex: Knex
|
|
253
281
|
}
|
|
254
282
|
|
|
255
283
|
async function buildAccountsForServer(
|
|
256
284
|
resolved: ResolvedServe,
|
|
285
|
+
walletResult: NodeWalletResult,
|
|
257
286
|
): Promise<AccountsRuntime | undefined> {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// opening the same file via better-sqlite3; for future pg wallets it
|
|
262
|
-
// shares the pg connection. Always a file path for now.
|
|
263
|
-
const accountsKnex = knex({
|
|
264
|
-
client: 'better-sqlite3',
|
|
265
|
-
connection: { filename: resolved.sqliteFilename },
|
|
266
|
-
useNullAsDefault: true,
|
|
267
|
-
})
|
|
268
|
-
await runAccountsMigrations(accountsKnex)
|
|
269
|
-
|
|
270
|
-
const services = new OneSatServices(resolved.chain, resolved.onesatURL)
|
|
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
|
|
271
290
|
|
|
272
291
|
return {
|
|
273
|
-
knex: accountsKnex,
|
|
274
292
|
walletServerAccounts: {
|
|
275
293
|
config: {
|
|
276
|
-
enabled:
|
|
294
|
+
enabled: resolved.accounts.enabled,
|
|
277
295
|
baselineBytes: resolved.accounts.baselineBytes,
|
|
278
|
-
|
|
296
|
+
purchaseUnitBytes: resolved.accounts.purchaseUnitBytes,
|
|
297
|
+
satsPerUnit: resolved.accounts.satsPerUnit,
|
|
279
298
|
durationBlocks: resolved.accounts.durationBlocks,
|
|
280
299
|
freeIdentityKeys: resolved.accounts.freeIdentityKeys,
|
|
281
300
|
},
|
|
282
|
-
|
|
283
|
-
currentBlock: () => services.chaintracks.currentHeight(),
|
|
301
|
+
currentBlock: () => walletResult.services.chaintracks.currentHeight(),
|
|
284
302
|
},
|
|
285
303
|
}
|
|
286
304
|
}
|
package/src/config.ts
CHANGED
|
@@ -15,10 +15,6 @@ export interface ServerStorageBunSqliteConfig {
|
|
|
15
15
|
provider: 'bun-sqlite'
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface ServerStorageKnexSqliteConfig {
|
|
19
|
-
provider: 'knex-sqlite'
|
|
20
|
-
}
|
|
21
|
-
|
|
22
18
|
export interface ServerStorageKnexPgConfig {
|
|
23
19
|
provider: 'knex-pg'
|
|
24
20
|
/** Postgres connection URL (required for knex-pg). */
|
|
@@ -27,7 +23,6 @@ export interface ServerStorageKnexPgConfig {
|
|
|
27
23
|
|
|
28
24
|
export type ServerStorageConfig =
|
|
29
25
|
| ServerStorageBunSqliteConfig
|
|
30
|
-
| ServerStorageKnexSqliteConfig
|
|
31
26
|
| ServerStorageKnexPgConfig
|
|
32
27
|
|
|
33
28
|
export interface ServerAccountsConfig {
|
|
@@ -35,8 +30,10 @@ export interface ServerAccountsConfig {
|
|
|
35
30
|
enabled?: boolean
|
|
36
31
|
/** Free baseline per identity key, in bytes. */
|
|
37
32
|
baselineBytes?: number
|
|
38
|
-
/**
|
|
39
|
-
|
|
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
|
|
40
37
|
/** Block window a payment remains valid for. */
|
|
41
38
|
durationBlocks?: number
|
|
42
39
|
/** Identity keys that bypass metering (server's own key is auto-added). */
|
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
|
+
}
|