@1sat/cli 0.0.83 → 0.0.86

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,49 +1,50 @@
1
1
  {
2
- "name": "@1sat/cli",
3
- "version": "0.0.83",
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
- "scripts"
13
- ],
14
- "scripts": {
15
- "preinstall": "node scripts/check-bun.cjs",
16
- "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 --external mariadb --external 'mariadb/*'",
17
- "dev": "bun run src/cli.ts",
18
- "lint": "biome check src",
19
- "test": "bun test"
20
- },
21
- "keywords": [
22
- "1sat",
23
- "bsv",
24
- "ordinals",
25
- "cli"
26
- ],
27
- "license": "MIT",
28
- "dependencies": {
29
- "@1sat/actions": "0.0.187",
30
- "@1sat/client": "0.0.43",
31
- "@1sat/types": "0.0.34",
32
- "@1sat/wallet-node": "0.0.60",
33
- "@1sat/wallet-server": "0.0.29",
34
- "@bsv/sdk": "^2.1.6",
35
- "@bsv/wallet-toolbox": "2.1.24",
36
- "chalk": "^5.0.0",
37
- "@clack/prompts": "^0.8.0",
38
- "bitcoin-backup": "^0.0.11",
39
- "dotenv": "^17.0.0",
40
- "evlog": "^2.10.0",
41
- "knex": "^3.1.0",
42
- "@bopen-io/messagebox-server": "^1.1.5",
43
- "pg": "^8.11.3"
44
- },
45
- "devDependencies": {
46
- "@types/bun": "^1.3.9",
47
- "typescript": "^5.9.3"
48
- }
2
+ "name": "@1sat/cli",
3
+ "version": "0.0.86",
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
+ "scripts"
13
+ ],
14
+ "scripts": {
15
+ "preinstall": "node scripts/check-bun.cjs",
16
+ "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 --external mariadb --external 'mariadb/*'",
17
+ "dev": "bun run src/cli.ts",
18
+ "lint": "biome check src",
19
+ "test": "bun test"
20
+ },
21
+ "keywords": [
22
+ "1sat",
23
+ "bsv",
24
+ "ordinals",
25
+ "cli"
26
+ ],
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@1sat/actions": "0.0.189",
30
+ "@1sat/client": "0.0.45",
31
+ "@1sat/types": "0.0.36",
32
+ "@1sat/wallet-node": "0.0.62",
33
+ "@1sat/wallet-server": "0.0.32",
34
+ "@bsv/sdk": "^2.1.6",
35
+ "@bsv/wallet-toolbox": "2.1.24",
36
+ "chalk": "^5.0.0",
37
+ "@clack/prompts": "^0.8.0",
38
+ "bitcoin-backup": "^0.0.11",
39
+ "dotenv": "^17.0.0",
40
+ "evlog": "^2.10.0",
41
+ "knex": "^3.1.0",
42
+ "@bopen-io/messagebox-server": "^1.3.2",
43
+ "pg": "^8.11.3",
44
+ "sqlite3": "^6.0.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.3.9",
48
+ "typescript": "^5.9.3"
49
+ }
49
50
  }
@@ -1,10 +1,9 @@
1
1
  /**
2
- * `1sat serve` command — launch wallet server and/or monitor.
2
+ * `1sat serve` command — launch the unified host server and/or monitor.
3
3
  *
4
- * 1sat serve Wallet server + monitor daemon
5
- * 1sat serve wallet Wallet server only
4
+ * 1sat serve Host server (storage + hosting + paymail + messagebox) + monitor
5
+ * 1sat serve wallet Wallet storage server only
6
6
  * 1sat serve monitor Monitor daemon only
7
- * 1sat serve messagebox BSV message-box server
8
7
  *
9
8
  * The server wraps the same wallet instance the CLI uses. Storage, active
10
9
  * remote, and backups all come from `~/.1sat/cli/config.json` via the same
@@ -14,7 +13,9 @@
14
13
  * 1sat config set server.port 8100
15
14
  * 1sat config set server.host 0.0.0.0
16
15
  * 1sat config set server.accounts.enabled true
17
- * 1sat config set server.messagebox.port 8771
16
+ * 1sat config set server.hosting.enabled true
17
+ * 1sat config set server.paymail.baseUrl https://1sat.app
18
+ * 1sat config set server.monitor.enabled false
18
19
  */
19
20
 
20
21
  import { join } from 'node:path'
@@ -23,12 +24,19 @@ import {
23
24
  type NodeWalletStorageConfig,
24
25
  createNodeWallet,
25
26
  } from '@1sat/wallet-node'
26
- import { createWalletServer } from '@1sat/wallet-server'
27
+ import {
28
+ KnexPendingStore,
29
+ createHostServer,
30
+ createWalletServer,
31
+ } from '@1sat/wallet-server'
27
32
  import type { PrivateKey } from '@bsv/sdk'
28
33
  import { initLogger } from 'evlog'
34
+ import knexLib from 'knex'
29
35
  import type { GlobalFlags } from '../args'
30
36
  import {
37
+ type RepricerConfig,
31
38
  type ServerAccountsConfig,
39
+ type ServerMessageboxConfig,
32
40
  type ServerStorageConfig,
33
41
  loadConfig,
34
42
  setConfigPath,
@@ -39,11 +47,11 @@ import { loadKey } from '../keys'
39
47
  import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
40
48
  import { fatal } from '../output'
41
49
  import {
50
+ type RepriceTarget,
42
51
  buildPriceUpdateTask,
43
52
  createAccountsConfigLoader,
44
53
  resolveRateProvider,
45
54
  } from '../repricer'
46
- import { startMessagebox } from './serve-messagebox'
47
55
 
48
56
  const DEFAULT_HOST = '127.0.0.1'
49
57
  const DEFAULT_PORT = 8100
@@ -51,10 +59,11 @@ const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
51
59
  const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
52
60
  const DEFAULT_PURCHASE_UNIT_BYTES = 1_073_741_824 // 1 GB chunks for production
53
61
  const DEFAULT_SATS_PER_UNIT = 1_000_000
62
+ const DEFAULT_HOSTING_PRICE_SATS = 10_000
54
63
  const DEFAULT_DURATION_BLOCKS = 4383
55
64
  const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
56
65
 
57
- type ServeMode = 'all' | 'wallet' | 'monitor' | 'messagebox'
66
+ type ServeMode = 'all' | 'wallet' | 'monitor'
58
67
 
59
68
  interface ResolvedServe {
60
69
  chain: 'main' | 'test'
@@ -68,6 +77,8 @@ interface ResolvedServe {
68
77
  activeRemote?: string
69
78
  backups?: string[]
70
79
  accounts: ResolvedAccounts
80
+ repricer?: RepricerConfig
81
+ monitorEnabled: boolean
71
82
  privateKey: PrivateKey
72
83
  }
73
84
 
@@ -78,14 +89,7 @@ interface ResolvedAccounts {
78
89
  satsPerUnit: number
79
90
  durationBlocks: number
80
91
  freeIdentityKeys: string[]
81
- repricer?: {
82
- enabled?: boolean
83
- targetUsd?: number
84
- intervalMs?: number
85
- provider?: string
86
- maxMovePct?: number
87
- minSats?: number
88
- }
92
+ targetUsd?: number
89
93
  }
90
94
 
91
95
  export async function handleServeCommand(
@@ -103,20 +107,6 @@ export async function handleServeCommand(
103
107
 
104
108
  initLogger({ env: { service: `1sat-cli-serve-${mode}` } })
105
109
 
106
- if (mode === 'messagebox') {
107
- const handle = await startMessagebox(opts)
108
- try {
109
- await waitForShutdown()
110
- } finally {
111
- try {
112
- await handle.stop()
113
- } catch (err) {
114
- console.error(`Error during shutdown: ${(err as Error).message}`)
115
- }
116
- }
117
- return
118
- }
119
-
120
110
  const resolved = await resolveServe(opts)
121
111
  const handles: Stoppable[] = []
122
112
 
@@ -139,7 +129,6 @@ function resolveMode(subcommand: string | undefined): ServeMode | null {
139
129
  switch (subcommand) {
140
130
  case 'wallet':
141
131
  case 'monitor':
142
- case 'messagebox':
143
132
  return subcommand
144
133
  default:
145
134
  return null
@@ -187,6 +176,8 @@ async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
187
176
  activeRemote: config.activeRemote,
188
177
  backups: config.backups,
189
178
  accounts: resolveAccounts(server.accounts),
179
+ repricer: server.repricer,
180
+ monitorEnabled: resolveMonitorEnabled(server.monitor?.enabled),
190
181
  privateKey,
191
182
  }
192
183
  }
@@ -195,6 +186,19 @@ function deriveSqliteFilename(dataDir: string, chain: string): string {
195
186
  return join(dataDir, `wallet-${chain}.db`)
196
187
  }
197
188
 
189
+ /**
190
+ * Env beats config, default on. ONESAT_MONITOR=false is set per-process by
191
+ * clustered deployments (PM2 ecosystem) that run a dedicated `serve monitor`;
192
+ * a plain `1sat serve` runs all-in-one.
193
+ */
194
+ function resolveMonitorEnabled(configured: boolean | undefined): boolean {
195
+ const fromEnv = process.env.ONESAT_MONITOR
196
+ if (fromEnv && fromEnv.trim() !== '') {
197
+ return !['false', '0', 'off'].includes(fromEnv.trim().toLowerCase())
198
+ }
199
+ return configured !== false
200
+ }
201
+
198
202
  function resolvePort(configured: number | undefined): number {
199
203
  const fromEnv = process.env.ONESAT_PORT
200
204
  if (fromEnv && fromEnv.trim() !== '') {
@@ -231,7 +235,7 @@ function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
231
235
  satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
232
236
  durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
233
237
  freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
234
- repricer: accounts?.repricer,
238
+ targetUsd: accounts?.targetUsd,
235
239
  }
236
240
  }
237
241
 
@@ -258,12 +262,17 @@ async function runWithStorage(
258
262
  storage,
259
263
  activeRemote: resolved.activeRemote,
260
264
  backups: resolved.backups,
261
- // Every serve mode manages monitor work explicitly: wallet mode
262
- // does none, monitor/all modes run startTasks. The factory's
263
- // initial runOnce is redundant in all three cases.
265
+ // Each serve mode manages monitor work explicitly (see runMonitor
266
+ // below); the factory's initial runOnce is redundant in every case.
264
267
  skipInitialMonitor: true,
265
268
  })
266
269
 
270
+ // One active monitor per deployment: wallet mode never runs it, monitor
271
+ // mode always does, all mode runs it unless server.monitor.enabled=false
272
+ // (set on redundant host instances that share a dedicated serve monitor).
273
+ const runMonitor =
274
+ mode === 'monitor' || (mode === 'all' && resolved.monitorEnabled)
275
+
267
276
  const accounts =
268
277
  mode === 'monitor'
269
278
  ? undefined
@@ -272,17 +281,35 @@ async function runWithStorage(
272
281
  const serverHandle =
273
282
  mode === 'monitor'
274
283
  ? undefined
275
- : await startWalletServer(resolved, walletResult, accounts)
276
-
277
- if (mode !== 'wallet') {
278
- const accountsCfg = resolved.accounts
279
- const r = accountsCfg.repricer
280
- if (
281
- accountsCfg.enabled &&
282
- r?.enabled &&
283
- typeof r.targetUsd === 'number' &&
284
- r.targetUsd > 0
285
- ) {
284
+ : await startWalletServer(resolved, walletResult, accounts, mode)
285
+
286
+ if (runMonitor) {
287
+ const r = resolved.repricer
288
+ const targets: RepriceTarget[] = []
289
+ if (resolved.accounts.enabled && (resolved.accounts.targetUsd ?? 0) > 0) {
290
+ targets.push({
291
+ name: 'accounts',
292
+ targetUsd: resolved.accounts.targetUsd!,
293
+ readCurrentSats: () =>
294
+ loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
295
+ onPersist: async (sats) => {
296
+ setConfigPath('server.accounts.satsPerUnit', sats)
297
+ },
298
+ })
299
+ }
300
+ const hostingCfg = loadConfig().server?.hosting
301
+ if (hostingCfg?.enabled === true && (hostingCfg.targetUsd ?? 0) > 0) {
302
+ targets.push({
303
+ name: 'hosting',
304
+ targetUsd: hostingCfg.targetUsd!,
305
+ readCurrentSats: () =>
306
+ loadConfig().server?.hosting?.priceSats ?? DEFAULT_HOSTING_PRICE_SATS,
307
+ onPersist: async (sats) => {
308
+ setConfigPath('server.hosting.priceSats', sats)
309
+ },
310
+ })
311
+ }
312
+ if (r?.enabled && targets.length > 0) {
286
313
  walletResult.monitor.addTask(
287
314
  buildPriceUpdateTask({
288
315
  monitor: walletResult.monitor,
@@ -290,27 +317,24 @@ async function runWithStorage(
290
317
  chain: resolved.chain,
291
318
  }),
292
319
  intervalMs: r.intervalMs ?? 15 * 60 * 1000,
293
- targetUsd: r.targetUsd,
294
320
  bounds: {
295
321
  maxMovePct: r.maxMovePct ?? 25,
296
322
  minSats: r.minSats ?? 1,
297
323
  },
298
- readCurrentSats: () =>
299
- loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
300
- onPersist: async (sats) => {
301
- setConfigPath('server.accounts.satsPerUnit', sats)
302
- },
324
+ targets,
303
325
  }),
304
326
  )
305
327
  console.log(
306
- `[repricer] enabled — $${r.targetUsd}/unit every ${Math.round(
328
+ `[repricer] enabled — ${targets
329
+ .map((t) => `${t.name} $${t.targetUsd}`)
330
+ .join(', ')} every ${Math.round(
307
331
  (r.intervalMs ?? 900_000) / 1000,
308
332
  )}s via ${r.provider ?? 'whatsonchain'}`,
309
333
  )
310
334
  }
311
335
  }
312
336
 
313
- if (mode !== 'wallet') {
337
+ if (runMonitor) {
314
338
  // startTasks loops until stopTasks flips its flag. Fire without
315
339
  // awaiting so the caller can install shutdown handlers and write
316
340
  // the monitor pid file.
@@ -323,7 +347,7 @@ async function runWithStorage(
323
347
 
324
348
  return {
325
349
  async stop() {
326
- if (mode !== 'wallet') {
350
+ if (runMonitor) {
327
351
  walletResult.monitor.stopTasks()
328
352
  clearMonitorPid(resolved.dataDir, process.pid)
329
353
  }
@@ -339,20 +363,136 @@ async function startWalletServer(
339
363
  resolved: ResolvedServe,
340
364
  walletResult: NodeWalletResult,
341
365
  accounts: AccountsRuntime | undefined,
366
+ mode: ServeMode,
342
367
  ): Promise<{ stop(): Promise<void> }> {
343
- const handle = createWalletServer({
368
+ const config = loadConfig()
369
+ const hostingCfg = config.server?.hosting
370
+ const hostingEnabled = hostingCfg?.enabled === true
371
+ const hosting = hostingEnabled
372
+ ? {
373
+ getConfig: () => {
374
+ const h = loadConfig().server?.hosting
375
+ return {
376
+ enabled: h?.enabled === true,
377
+ priceSats: h?.priceSats ?? DEFAULT_HOSTING_PRICE_SATS,
378
+ priceUsd: h?.targetUsd,
379
+ periodSeconds: h?.periodSeconds ?? 2_592_000,
380
+ }
381
+ },
382
+ }
383
+ : undefined
384
+
385
+ if (mode === 'wallet') {
386
+ const handle = createWalletServer({
387
+ wallet: walletResult.wallet,
388
+ storage: walletResult.getActiveStorage(),
389
+ serverIdentityKey: walletResult.wallet.identityKey,
390
+ listen: { port: resolved.port, host: resolved.host },
391
+ publicPath: '/',
392
+ internalPath: null,
393
+ accounts: accounts?.walletServerAccounts,
394
+ hosting,
395
+ })
396
+ const port = await handle.start()
397
+ const notes = [
398
+ resolved.accounts.enabled ? 'accounts: on' : '',
399
+ hostingEnabled ? 'hosting: on' : '',
400
+ ]
401
+ .filter(Boolean)
402
+ .join(', ')
403
+ console.log(
404
+ `[wallet] listening on ${resolved.host}:${port}${notes ? ` (${notes})` : ''}`,
405
+ )
406
+ return { stop: () => handle.stop() }
407
+ }
408
+
409
+ // Unified host: storage + messagebox + paymail + hosting
410
+ const serverCfg = config.server ?? {}
411
+ const paymailCfg = serverCfg.paymail
412
+ const messageboxCfg = serverCfg.messagebox
413
+
414
+ const knex = createMessageboxKnex(
415
+ serverCfg.storage ?? { provider: 'bun-sqlite' },
416
+ messageboxCfg,
417
+ resolved,
418
+ )
419
+ const pendingStore = new KnexPendingStore(knex)
420
+ await pendingStore.init()
421
+
422
+ const handle = await createHostServer({
344
423
  wallet: walletResult.wallet,
345
424
  storage: walletResult.getActiveStorage(),
346
425
  serverIdentityKey: walletResult.wallet.identityKey,
347
426
  listen: { port: resolved.port, host: resolved.host },
348
- publicPath: '/',
349
- internalPath: null,
350
427
  accounts: accounts?.walletServerAccounts,
428
+ hosting,
429
+ paymail: {
430
+ baseUrl:
431
+ paymailCfg?.baseUrl ?? `http://${resolved.host}:${resolved.port}`,
432
+ stackUrl: paymailCfg?.stackUrl ?? 'https://api.1sat.app',
433
+ pendingStore,
434
+ hostWallet: hostingEnabled ? walletResult.wallet : undefined,
435
+ requireEntitlement: hostingEnabled,
436
+ messageboxUrl: `http://127.0.0.1:${resolved.port}/messagebox`,
437
+ hostPrivateKey: resolved.privateKey,
438
+ },
439
+ messagebox: {
440
+ knex,
441
+ websockets: messageboxCfg?.websockets !== false,
442
+ },
351
443
  })
352
444
  const port = await handle.start()
353
- const accountsNote = resolved.accounts.enabled ? ' (accounts: on)' : ''
354
- console.log(`[wallet] listening on ${resolved.host}:${port}${accountsNote}`)
355
- return { stop: () => handle.stop() }
445
+ const notes = [
446
+ resolved.accounts.enabled ? 'accounts: on' : '',
447
+ hostingEnabled ? 'hosting: on' : '',
448
+ ]
449
+ .filter(Boolean)
450
+ .join(', ')
451
+ console.log(
452
+ `[host] listening on ${resolved.host}:${port}${notes ? ` (${notes})` : ''}`,
453
+ )
454
+ return {
455
+ stop: async () => {
456
+ await handle.stop()
457
+ if (knex) await knex.destroy()
458
+ },
459
+ }
460
+ }
461
+
462
+ const DEFAULT_PG_SCHEMA = 'messagebox'
463
+
464
+ function createMessageboxKnex(
465
+ storage: ServerStorageConfig,
466
+ messagebox: ServerMessageboxConfig | undefined,
467
+ resolved: ResolvedServe,
468
+ ) {
469
+ if (storage.provider === 'bun-sqlite') {
470
+ const filename =
471
+ messagebox?.dbPath ??
472
+ join(resolved.dataDir, `messagebox-${resolved.chain}.db`)
473
+ return (knexLib as any)({
474
+ client: 'sqlite3',
475
+ connection: { filename },
476
+ useNullAsDefault: true,
477
+ })
478
+ }
479
+ if (storage.provider === 'pg') {
480
+ if (messagebox?.dbUrl) {
481
+ return (knexLib as any)({
482
+ client: 'pg',
483
+ connection: { connectionString: messagebox.dbUrl },
484
+ })
485
+ }
486
+ const schema = messagebox?.pgSchema ?? DEFAULT_PG_SCHEMA
487
+ return (knexLib as any)({
488
+ client: 'pg',
489
+ connection: { connectionString: storage.dbUrl },
490
+ searchPath: [schema],
491
+ })
492
+ }
493
+ fatal(
494
+ `server.storage.provider '${(storage as { provider: string }).provider}' is not supported for messagebox storage.`,
495
+ )
356
496
  }
357
497
 
358
498
  interface AccountsRuntime {
package/src/config.ts CHANGED
@@ -25,18 +25,23 @@ export type ServerStorageConfig =
25
25
  | ServerStorageBunSqliteConfig
26
26
  | ServerStoragePgConfig
27
27
 
28
+ /**
29
+ * Shared repricer engine (`server.repricer`). When enabled, a monitor task
30
+ * fetches the BSV/USD rate on the interval and rewrites each service's sats
31
+ * price toward its own `targetUsd` (`server.accounts.targetUsd` →
32
+ * `satsPerUnit`, `server.hosting.targetUsd` → `priceSats`). Services with no
33
+ * `targetUsd` set are left alone.
34
+ */
28
35
  export interface RepricerConfig {
29
36
  /** Master toggle. Defaults to false. */
30
37
  enabled?: boolean
31
- /** Target price per `purchaseUnitBytes` chunk, in USD. */
32
- targetUsd?: number
33
38
  /** Interval between rate checks in ms. Defaults to 900000 (15 min). */
34
39
  intervalMs?: number
35
40
  /** Rate provider name. Defaults to "whatsonchain". */
36
41
  provider?: string
37
42
  /** Max percent change allowed per update. Larger moves are skipped. Defaults to 25. */
38
43
  maxMovePct?: number
39
- /** Lower bound for `satsPerUnit`. Defaults to 1. */
44
+ /** Lower bound for repriced sats values. Defaults to 1. */
40
45
  minSats?: number
41
46
  }
42
47
 
@@ -49,12 +54,12 @@ export interface ServerAccountsConfig {
49
54
  purchaseUnitBytes?: number
50
55
  /** Sats charged per purchase unit. */
51
56
  satsPerUnit?: number
57
+ /** USD target per purchase unit, maintained by `server.repricer`. */
58
+ targetUsd?: number
52
59
  /** Block window a payment remains valid for. */
53
60
  durationBlocks?: number
54
61
  /** Identity keys that bypass metering (server's own key is auto-added). */
55
62
  freeIdentityKeys?: string[]
56
- /** Optional auto-repricer. When enabled, updates `satsPerUnit` from a live BSV/USD rate. */
57
- repricer?: RepricerConfig
58
63
  }
59
64
 
60
65
  export interface ServerMessageboxConfig {
@@ -85,6 +90,65 @@ export interface ServerMessageboxConfig {
85
90
  walletStorageUrl?: string
86
91
  }
87
92
 
93
+ export interface ServerPaymailConfig {
94
+ /** Port to bind. Defaults to `8466` or ONESAT_PAYMAIL_PORT. */
95
+ port?: number
96
+ /**
97
+ * Public base URL used in /.well-known/bsvalias capability links
98
+ * (e.g. https://1sat.app). Required for serve paymail.
99
+ */
100
+ baseUrl?: string
101
+ /** Stack API root (OpNS, beef, tx). Defaults to https://api.1sat.app */
102
+ stackUrl?: string
103
+ /**
104
+ * SQLite path for in-flight P2P payment references (Go paymail port).
105
+ * Defaults to `<dataDir>/paymail-pending-{chain}.db`.
106
+ */
107
+ pendingDbPath?: string
108
+ /**
109
+ * Messagebox URL for payment_inbox delivery after receive.
110
+ * Defaults to https://messagebox.1sat.app when unset is OK to skip —
111
+ * set explicitly to enable (same as Go messagebox_url).
112
+ */
113
+ messageboxUrl?: string
114
+ /**
115
+ * When true, open host wallet storage and require active HOSTING_BASKET
116
+ * receipt for the bound identity. Default false until subscribe path ships.
117
+ */
118
+ requireEntitlement?: boolean
119
+ /**
120
+ * Wallet storage URL for entitlement listOutputs (and optional later
121
+ * subscribe). Defaults to local wallet serve URL like messagebox.
122
+ */
123
+ walletStorageUrl?: string
124
+ }
125
+
126
+ export interface ServerHostingConfig {
127
+ /** Master toggle. Defaults to false. */
128
+ enabled?: boolean
129
+ /** Sats per subscription period. */
130
+ priceSats?: number
131
+ /**
132
+ * USD price per subscription period. Returned as `priceUsd` on the
133
+ * pricing API for display, and maintained as the repricing target for
134
+ * `priceSats` when `server.repricer` is enabled.
135
+ */
136
+ targetUsd?: number
137
+ /** Period length in seconds (e.g. 2592000 ≈ 30 days). */
138
+ periodSeconds?: number
139
+ }
140
+
141
+ export interface ServerMonitorConfig {
142
+ /**
143
+ * Run the background monitor task loop (post waiting txs, check for
144
+ * proofs, account repricing). Defaults to true. Set false on redundant
145
+ * host instances that run alongside a dedicated `serve monitor`, so a
146
+ * single active monitor runs per deployment. `serve monitor` always runs
147
+ * the loop regardless of this flag.
148
+ */
149
+ enabled?: boolean
150
+ }
151
+
88
152
  export interface ServerConfig {
89
153
  /** Hostname to bind. Defaults to `127.0.0.1`. */
90
154
  host?: string
@@ -96,6 +160,14 @@ export interface ServerConfig {
96
160
  accounts?: ServerAccountsConfig
97
161
  /** Messagebox subcommand settings. */
98
162
  messagebox?: ServerMessageboxConfig
163
+ /** Paymail subcommand settings. */
164
+ paymail?: ServerPaymailConfig
165
+ /** Hosted paymail subscription (wallet serve /hosting/*). */
166
+ hosting?: ServerHostingConfig
167
+ /** Shared BSV/USD repricer engine for accounts + hosting prices. */
168
+ repricer?: RepricerConfig
169
+ /** Background monitor task loop. Defaults to enabled. */
170
+ monitor?: ServerMonitorConfig
99
171
  }
100
172
 
101
173
  export interface OneSatCliConfig {
package/src/help.ts CHANGED
@@ -560,19 +560,14 @@ export const COMMANDS: CommandSpec[] = [
560
560
  group: 'Server',
561
561
  name: 'serve',
562
562
  description:
563
- 'Run wallet HTTP server and/or monitor daemon (config under server.* in config.json)',
563
+ 'Run unified host server (storage + hosting + paymail + messagebox) and/or monitor (config under server.* in config.json)',
564
564
  subcommands: [
565
565
  {
566
566
  name: '(no subcommand)',
567
- description: 'Wallet server + monitor daemon',
567
+ description: 'Host server + monitor daemon',
568
568
  },
569
- { name: 'wallet', description: 'Wallet server only (BRC-100 HTTP)' },
569
+ { name: 'wallet', description: 'Wallet storage server only (BRC-100 HTTP)' },
570
570
  { name: 'monitor', description: 'Monitor daemon only' },
571
- {
572
- name: 'messagebox',
573
- description:
574
- 'BSV message-box server (port 8771 default; uses wallet identity)',
575
- },
576
571
  ],
577
572
  },
578
573
  {
@@ -1,18 +1,25 @@
1
1
  import { computeReprice } from './computeReprice'
2
2
  import type { RateProvider, RepricerBounds } from './types'
3
3
 
4
+ /** One price the repricer maintains from the shared BSV/USD quote. */
5
+ export interface RepriceTarget {
6
+ /** Label for task result logs, e.g. 'accounts' or 'hosting'. */
7
+ name: string
8
+ /** USD target for this price. Targets with 0 or negative are ignored. */
9
+ targetUsd: number
10
+ /** Read the current sats value (typically from disk via the same loader the servers use). */
11
+ readCurrentSats: () => number
12
+ /** Persist the new sats value (wraps `setConfigPath`). */
13
+ onPersist: (newSats: number) => Promise<void>
14
+ }
15
+
4
16
  export interface PriceUpdateTaskOptions {
5
17
  monitor: unknown
6
18
  rateProvider: RateProvider
7
19
  /** Interval between successful tick attempts. */
8
20
  intervalMs: number
9
- /** USD target per purchase unit. Task no-ops when this is 0. */
10
- targetUsd: number
11
21
  bounds: RepricerBounds
12
- /** Read current `satsPerUnit` (typically from disk via the same loader the servers use). */
13
- readCurrentSats: () => number
14
- /** Persist the new `satsPerUnit` value. Wraps `setConfigPath('server.accounts.satsPerUnit', sats)`. */
15
- onPersist: (newSats: number) => Promise<void>
22
+ targets: RepriceTarget[]
16
23
  }
17
24
 
18
25
  export interface PriceUpdateTask {
@@ -28,15 +35,8 @@ export interface PriceUpdateTask {
28
35
  export function buildPriceUpdateTask(
29
36
  options: PriceUpdateTaskOptions,
30
37
  ): PriceUpdateTask {
31
- const {
32
- monitor,
33
- rateProvider,
34
- intervalMs,
35
- targetUsd,
36
- bounds,
37
- readCurrentSats,
38
- onPersist,
39
- } = options
38
+ const { monitor, rateProvider, intervalMs, bounds } = options
39
+ const targets = options.targets.filter((t) => t.targetUsd > 0)
40
40
 
41
41
  const storage = (monitor as { storage?: unknown } | null | undefined)?.storage
42
42
 
@@ -47,7 +47,7 @@ export function buildPriceUpdateTask(
47
47
  lastRunMsecsSinceEpoch: 0,
48
48
  async asyncSetup() {},
49
49
  trigger(now: number): { run: boolean } {
50
- if (!(targetUsd > 0)) return { run: false }
50
+ if (targets.length === 0) return { run: false }
51
51
  if (now - this.lastRunMsecsSinceEpoch < intervalMs) return { run: false }
52
52
  return { run: true }
53
53
  },
@@ -59,22 +59,32 @@ export function buildPriceUpdateTask(
59
59
  return `rate fetch failed: ${(err as Error).message}`
60
60
  }
61
61
 
62
- const currentSats = readCurrentSats()
63
- const result = computeReprice({
64
- targetUsd,
65
- bsvUsd: quote.bsvUsd,
66
- currentSats,
67
- bounds,
68
- })
62
+ const parts: string[] = []
63
+ for (const target of targets) {
64
+ const currentSats = target.readCurrentSats()
65
+ const result = computeReprice({
66
+ targetUsd: target.targetUsd,
67
+ bsvUsd: quote.bsvUsd,
68
+ currentSats,
69
+ bounds,
70
+ })
69
71
 
70
- if (result.status === 'skipped') return `skipped: ${result.reason}`
72
+ if (result.status === 'skipped') {
73
+ parts.push(`${target.name}: skipped: ${result.reason}`)
74
+ continue
75
+ }
71
76
 
72
- try {
73
- await onPersist(result.newSats)
74
- } catch (err) {
75
- return `persist failed: ${(err as Error).message}`
77
+ try {
78
+ await target.onPersist(result.newSats)
79
+ } catch (err) {
80
+ parts.push(
81
+ `${target.name}: persist failed: ${(err as Error).message}`,
82
+ )
83
+ continue
84
+ }
85
+ parts.push(`${target.name}: ${currentSats} → ${result.newSats} sats`)
76
86
  }
77
- return `${currentSats} → ${result.newSats} sats/unit @ $${quote.bsvUsd}/BSV`
87
+ return `${parts.join('; ')} @ $${quote.bsvUsd}/BSV`
78
88
  },
79
89
  }
80
90
  }
@@ -1,5 +1,8 @@
1
1
  export { buildPriceUpdateTask } from './buildPriceUpdateTask'
2
- export type { PriceUpdateTaskOptions } from './buildPriceUpdateTask'
2
+ export type {
3
+ PriceUpdateTaskOptions,
4
+ RepriceTarget,
5
+ } from './buildPriceUpdateTask'
3
6
  export { computeReprice } from './computeReprice'
4
7
  export { createAccountsConfigLoader } from './configLoader'
5
8
  export type { AccountsConfigLoaderOptions } from './configLoader'
@@ -1,221 +0,0 @@
1
- /**
2
- * `1sat serve messagebox` — launch the BSV message-box server using the
3
- * CLI's wallet identity.
4
- *
5
- * messagebox-server boots itself as a side effect of `import` once env vars
6
- * are populated, so this handler:
7
- * 1. Reads CLI config + private key
8
- * 2. Composes env (SERVER_PRIVATE_KEY, PORT, KNEX_DB_*, WALLET_STORAGE_URL, …)
9
- * 3. Switches cwd to the messagebox-server install dir so its knexfile's
10
- * relative `./out/src/migrations` path resolves correctly
11
- * 4. Dynamically imports `@bopen-io/messagebox-server`
12
- * 5. Awaits SIGINT/SIGTERM, then closes the HTTP and WebSocket servers
13
- */
14
-
15
- import { dirname, join } from 'node:path'
16
- import { fileURLToPath } from 'node:url'
17
- import type { PrivateKey } from '@bsv/sdk'
18
- import type { GlobalFlags } from '../args'
19
- import {
20
- type ServerMessageboxConfig,
21
- type ServerStorageConfig,
22
- ensureDataDir,
23
- loadConfig,
24
- } from '../config'
25
- import { loadKey } from '../keys'
26
- import { fatal } from '../output'
27
-
28
- const DEFAULT_MESSAGEBOX_PORT = 8771
29
- const DEFAULT_WALLET_HOST = '127.0.0.1'
30
- const DEFAULT_WALLET_PORT = 8100
31
- const DEFAULT_PG_SCHEMA = 'messagebox'
32
-
33
- interface ResolvedMessagebox {
34
- chain: 'main' | 'test'
35
- port: number
36
- websockets: boolean
37
- walletStorageUrl: string
38
- knexClient: string
39
- knexConnection: Record<string, unknown>
40
- identityHex: string
41
- identityPub: string
42
- }
43
-
44
- interface MessageboxModule {
45
- http: { close(cb?: (err?: Error) => void): void }
46
- io: { close(cb?: () => void): void } | null
47
- }
48
-
49
- export interface MessageboxStoppable {
50
- stop(): Promise<void>
51
- }
52
-
53
- export async function startMessagebox(
54
- opts: GlobalFlags,
55
- ): Promise<MessageboxStoppable> {
56
- const resolved = await resolveMessagebox(opts)
57
- applyEnv(resolved)
58
-
59
- console.log(`[messagebox] starting on 0.0.0.0:${resolved.port}`)
60
- console.log(`[messagebox] identity: ${resolved.identityPub.slice(0, 16)}…`)
61
- console.log(`[messagebox] storage: ${resolved.knexClient}`)
62
- console.log(`[messagebox] wallet: ${resolved.walletStorageUrl}`)
63
- console.log(
64
- `[messagebox] websockets: ${resolved.websockets ? 'enabled' : 'disabled'}`,
65
- )
66
-
67
- // messagebox-server's knexfile uses `./out/src/migrations` relative to
68
- // cwd. Switch cwd to its install dir so that path resolves correctly
69
- // regardless of where the CLI was invoked from.
70
- const packageJsonUrl = import.meta.resolve(
71
- '@bopen-io/messagebox-server/package.json',
72
- )
73
- const messageboxDir = dirname(fileURLToPath(packageJsonUrl))
74
- process.chdir(messageboxDir)
75
-
76
- // messagebox-server boots itself when its index module is loaded.
77
- const mbox = (await import(
78
- '@bopen-io/messagebox-server'
79
- )) as unknown as MessageboxModule
80
-
81
- return {
82
- async stop() {
83
- await new Promise<void>((resolve) => {
84
- try {
85
- mbox.http.close(() => resolve())
86
- } catch {
87
- resolve()
88
- }
89
- })
90
- if (mbox.io) {
91
- await new Promise<void>((resolve) => {
92
- try {
93
- mbox.io?.close(() => resolve())
94
- } catch {
95
- resolve()
96
- }
97
- })
98
- }
99
- },
100
- }
101
- }
102
-
103
- async function resolveMessagebox(
104
- opts: GlobalFlags,
105
- ): Promise<ResolvedMessagebox> {
106
- const config = loadConfig()
107
- const server = config.server ?? {}
108
- const messagebox: ServerMessageboxConfig = server.messagebox ?? {}
109
-
110
- const chain = opts.chain ?? config.chain ?? 'main'
111
- const port = resolvePort(messagebox.port)
112
- const websockets = messagebox.websockets !== false
113
-
114
- let privateKey: PrivateKey
115
- try {
116
- privateKey = await loadKey()
117
- } catch (err) {
118
- fatal((err as Error).message)
119
- }
120
-
121
- const identityHex = privateKey.toString()
122
- const identityPub = privateKey.toPublicKey().toString()
123
-
124
- const walletHost = server.host ?? DEFAULT_WALLET_HOST
125
- const walletPort = server.port ?? DEFAULT_WALLET_PORT
126
- const walletStorageUrl =
127
- messagebox.walletStorageUrl ?? `http://${walletHost}:${walletPort}/`
128
-
129
- const { knexClient, knexConnection } = resolveStorage(
130
- server.storage ?? { provider: 'bun-sqlite' },
131
- messagebox,
132
- chain,
133
- )
134
-
135
- return {
136
- chain,
137
- port,
138
- websockets,
139
- walletStorageUrl,
140
- knexClient,
141
- knexConnection,
142
- identityHex,
143
- identityPub,
144
- }
145
- }
146
-
147
- function resolvePort(configured: number | undefined): number {
148
- const fromEnv = process.env.ONESAT_MESSAGEBOX_PORT
149
- if (fromEnv && fromEnv.trim() !== '') {
150
- const parsed = Number(fromEnv)
151
- if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 65535) {
152
- fatal(
153
- `ONESAT_MESSAGEBOX_PORT must be a valid port number, got: ${fromEnv}`,
154
- )
155
- }
156
- return parsed
157
- }
158
- return configured ?? DEFAULT_MESSAGEBOX_PORT
159
- }
160
-
161
- function resolveStorage(
162
- storage: ServerStorageConfig,
163
- messagebox: ServerMessageboxConfig,
164
- chain: 'main' | 'test',
165
- ): { knexClient: string; knexConnection: Record<string, unknown> } {
166
- if (storage.provider === 'bun-sqlite') {
167
- const dataDir = ensureDataDir()
168
- const filename =
169
- messagebox.dbPath ?? join(dataDir, `messagebox-${chain}.db`)
170
- return {
171
- knexClient: 'sqlite3',
172
- knexConnection: { filename },
173
- }
174
- }
175
-
176
- if (storage.provider === 'pg') {
177
- // Prefer a dedicated messagebox database when configured (recommended:
178
- // avoids `knex_migrations` table collisions with wallet-toolbox).
179
- // Fall back to schema isolation against the wallet's database — note
180
- // the `messagebox` schema must exist in that database for table
181
- // creation to land there instead of `public`.
182
- if (messagebox.dbUrl) {
183
- return {
184
- knexClient: 'pg',
185
- knexConnection: { connectionString: messagebox.dbUrl },
186
- }
187
- }
188
- const schema = messagebox.pgSchema ?? DEFAULT_PG_SCHEMA
189
- return {
190
- knexClient: 'pg',
191
- knexConnection: {
192
- connectionString: storage.dbUrl,
193
- searchPath: [schema, 'public'],
194
- },
195
- }
196
- }
197
-
198
- fatal(
199
- `server.storage.provider '${(storage as { provider: string }).provider}' is not supported by 'serve messagebox'.`,
200
- )
201
- }
202
-
203
- function applyEnv(resolved: ResolvedMessagebox): void {
204
- // messagebox-server reads env at module-load time, so we set everything
205
- // before the dynamic import. dotenv.config() inside messagebox-server
206
- // does not overwrite values already set on process.env.
207
- process.env.SERVER_PRIVATE_KEY = resolved.identityHex
208
- process.env.BSV_NETWORK = resolved.chain === 'main' ? 'mainnet' : 'testnet'
209
- process.env.PORT = String(resolved.port)
210
- // messagebox-server's index.ts forces HTTP_PORT=3000 when NODE_ENV
211
- // is anything other than 'development', ignoring our PORT entirely.
212
- // Use 'development' so PORT is honored.
213
- process.env.NODE_ENV = 'development'
214
- process.env.ENABLE_WEBSOCKETS = resolved.websockets ? 'true' : 'false'
215
- process.env.WALLET_STORAGE_URL = resolved.walletStorageUrl
216
- process.env.KNEX_DB_CLIENT = resolved.knexClient
217
- process.env.KNEX_DB_CONNECTION = JSON.stringify(resolved.knexConnection)
218
- if (process.env.ENABLE_FIREBASE === undefined) {
219
- process.env.ENABLE_FIREBASE = 'false'
220
- }
221
- }