@1sat/cli 0.0.32 → 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 CHANGED
@@ -1,38 +1,45 @@
1
1
  {
2
- "name": "@1sat/cli",
3
- "version": "0.0.32",
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": ["src"],
11
- "scripts": {
12
- "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",
13
- "dev": "bun run src/cli.ts",
14
- "lint": "biome check src"
15
- },
16
- "keywords": ["1sat", "bsv", "ordinals", "cli"],
17
- "license": "MIT",
18
- "dependencies": {
19
- "@1sat/actions": "0.0.99",
20
- "@1sat/client": "0.0.21",
21
- "@1sat/types": "0.0.17",
22
- "@1sat/wallet-node": "0.0.29",
23
- "@1sat/wallet-server": "0.0.1",
24
- "@bsv/sdk": "^2.0.13",
25
- "@bsv/wallet-toolbox": "^2.1.21",
26
- "chalk": "^5.0.0",
27
- "@clack/prompts": "^0.8.0",
28
- "bitcoin-backup": "^0.0.11",
29
- "dotenv": "^17.0.0",
30
- "evlog": "^2.10.0",
31
- "knex": "^3.1.0",
32
- "pg": "^8.11.3"
33
- },
34
- "devDependencies": {
35
- "@types/bun": "^1.3.9",
36
- "typescript": "^5.9.3"
37
- }
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
+ }
38
45
  }
package/src/cli.ts CHANGED
@@ -116,5 +116,6 @@ async function main(): Promise<void> {
116
116
 
117
117
  main().catch((err) => {
118
118
  console.error(formatError(`Error: ${err.message}`))
119
+ if (process.env.DEBUG) console.error(err.stack)
119
120
  process.exit(1)
120
121
  })
@@ -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
- value !== 'bun-sqlite' &&
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':
@@ -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
- // For backup-only, we use StorageClient via wallet-node
89
- // biome-ignore lint/suspicious/noExplicitAny: StorageClient constructor not typed in wallet-toolbox
90
- const { StorageClient } = await import('@1sat/wallet-node')
91
- const wallet = walletResult.wallet as any
92
- const client = new (StorageClient as any)(wallet, url)
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 { walletResult, destroy } = await loadContext(privateKey, {
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
  // ============================================================================
@@ -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
- createWalletServer,
23
- runMigrations as runAccountsMigrations,
24
- } from '@1sat/wallet-server'
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 DEFAULT_SATS_PER_GB = 1_000_000
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
- satsPerGb: number
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
- if (resolved.storage.provider === 'bun-sqlite') {
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
- satsPerGb: accounts?.satsPerGb ?? DEFAULT_SATS_PER_GB,
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
- * bun-sqlite path: construct the wallet via the same `createNodeWallet`
188
- * factory the CLI uses. Server + monitor operate on that single wallet
189
- * instance, so `activeRemote`, `backups`, and `storageIdentityKey` behave
190
- * identically to `1sat wallet <command>`.
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 runBunSqlite(
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
- filename: resolved.sqliteFilename,
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 = await buildAccountsForServer(resolved)
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
- await walletResult.monitor.startTasks()
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
- if (accounts) await accounts.knex.destroy()
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
- storage: walletResult.storage,
236
- serverPrivateKey: resolved.privateKey.toHex(),
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
- if (!resolved.accounts.enabled) return undefined
259
-
260
- // Accounts uses its own knex pool. For bun-sqlite wallets that means
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: true,
294
+ enabled: resolved.accounts.enabled,
277
295
  baselineBytes: resolved.accounts.baselineBytes,
278
- satsPerGb: resolved.accounts.satsPerGb,
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
- knex: accountsKnex,
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
- /** Sats charged per GB of paid capacity per `durationBlocks`. */
39
- satsPerGb?: 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
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
- filename: `${dataDir}/wallet-${opts.chain}.db`,
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
+ }