@1sat/cli 0.0.85 → 0.0.87

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@1sat/cli",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -30,7 +30,7 @@
30
30
  "@1sat/client": "0.0.45",
31
31
  "@1sat/types": "0.0.36",
32
32
  "@1sat/wallet-node": "0.0.62",
33
- "@1sat/wallet-server": "0.0.31",
33
+ "@1sat/wallet-server": "0.0.33",
34
34
  "@bsv/sdk": "^2.1.6",
35
35
  "@bsv/wallet-toolbox": "2.1.24",
36
36
  "chalk": "^5.0.0",
@@ -34,6 +34,7 @@ import { initLogger } from 'evlog'
34
34
  import knexLib from 'knex'
35
35
  import type { GlobalFlags } from '../args'
36
36
  import {
37
+ type RepricerConfig,
37
38
  type ServerAccountsConfig,
38
39
  type ServerMessageboxConfig,
39
40
  type ServerStorageConfig,
@@ -46,6 +47,7 @@ import { loadKey } from '../keys'
46
47
  import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
47
48
  import { fatal } from '../output'
48
49
  import {
50
+ type RepriceTarget,
49
51
  buildPriceUpdateTask,
50
52
  createAccountsConfigLoader,
51
53
  resolveRateProvider,
@@ -57,6 +59,7 @@ const DEFAULT_ONESAT_URL = 'https://api.1sat.app/1sat'
57
59
  const DEFAULT_BASELINE_BYTES = 1024 * 1024 * 1024 // 1 GB
58
60
  const DEFAULT_PURCHASE_UNIT_BYTES = 1_073_741_824 // 1 GB chunks for production
59
61
  const DEFAULT_SATS_PER_UNIT = 1_000_000
62
+ const DEFAULT_HOSTING_PRICE_SATS = 10_000
60
63
  const DEFAULT_DURATION_BLOCKS = 4383
61
64
  const DEFAULT_STORAGE_IDENTITY_KEY = '1sat-cli-default'
62
65
 
@@ -74,6 +77,7 @@ interface ResolvedServe {
74
77
  activeRemote?: string
75
78
  backups?: string[]
76
79
  accounts: ResolvedAccounts
80
+ repricer?: RepricerConfig
77
81
  monitorEnabled: boolean
78
82
  privateKey: PrivateKey
79
83
  }
@@ -85,14 +89,7 @@ interface ResolvedAccounts {
85
89
  satsPerUnit: number
86
90
  durationBlocks: number
87
91
  freeIdentityKeys: string[]
88
- repricer?: {
89
- enabled?: boolean
90
- targetUsd?: number
91
- intervalMs?: number
92
- provider?: string
93
- maxMovePct?: number
94
- minSats?: number
95
- }
92
+ targetUsd?: number
96
93
  }
97
94
 
98
95
  export async function handleServeCommand(
@@ -179,7 +176,8 @@ async function resolveServe(opts: GlobalFlags): Promise<ResolvedServe> {
179
176
  activeRemote: config.activeRemote,
180
177
  backups: config.backups,
181
178
  accounts: resolveAccounts(server.accounts),
182
- monitorEnabled: server.monitor?.enabled !== false,
179
+ repricer: server.repricer,
180
+ monitorEnabled: resolveMonitorEnabled(server.monitor?.enabled),
183
181
  privateKey,
184
182
  }
185
183
  }
@@ -188,6 +186,19 @@ function deriveSqliteFilename(dataDir: string, chain: string): string {
188
186
  return join(dataDir, `wallet-${chain}.db`)
189
187
  }
190
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
+
191
202
  function resolvePort(configured: number | undefined): number {
192
203
  const fromEnv = process.env.ONESAT_PORT
193
204
  if (fromEnv && fromEnv.trim() !== '') {
@@ -224,7 +235,7 @@ function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
224
235
  satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
225
236
  durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
226
237
  freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
227
- repricer: accounts?.repricer,
238
+ targetUsd: accounts?.targetUsd,
228
239
  }
229
240
  }
230
241
 
@@ -273,14 +284,32 @@ async function runWithStorage(
273
284
  : await startWalletServer(resolved, walletResult, accounts, mode)
274
285
 
275
286
  if (runMonitor) {
276
- const accountsCfg = resolved.accounts
277
- const r = accountsCfg.repricer
278
- if (
279
- accountsCfg.enabled &&
280
- r?.enabled &&
281
- typeof r.targetUsd === 'number' &&
282
- r.targetUsd > 0
283
- ) {
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) {
284
313
  walletResult.monitor.addTask(
285
314
  buildPriceUpdateTask({
286
315
  monitor: walletResult.monitor,
@@ -288,20 +317,17 @@ async function runWithStorage(
288
317
  chain: resolved.chain,
289
318
  }),
290
319
  intervalMs: r.intervalMs ?? 15 * 60 * 1000,
291
- targetUsd: r.targetUsd,
292
320
  bounds: {
293
321
  maxMovePct: r.maxMovePct ?? 25,
294
322
  minSats: r.minSats ?? 1,
295
323
  },
296
- readCurrentSats: () =>
297
- loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
298
- onPersist: async (sats) => {
299
- setConfigPath('server.accounts.satsPerUnit', sats)
300
- },
324
+ targets,
301
325
  }),
302
326
  )
303
327
  console.log(
304
- `[repricer] enabled — $${r.targetUsd}/unit every ${Math.round(
328
+ `[repricer] enabled — ${targets
329
+ .map((t) => `${t.name} $${t.targetUsd}`)
330
+ .join(', ')} every ${Math.round(
305
331
  (r.intervalMs ?? 900_000) / 1000,
306
332
  )}s via ${r.provider ?? 'whatsonchain'}`,
307
333
  )
@@ -348,13 +374,22 @@ async function startWalletServer(
348
374
  const h = loadConfig().server?.hosting
349
375
  return {
350
376
  enabled: h?.enabled === true,
351
- priceSats: h?.priceSats ?? 10_000,
377
+ priceSats: h?.priceSats ?? DEFAULT_HOSTING_PRICE_SATS,
378
+ priceUsd: h?.targetUsd,
352
379
  periodSeconds: h?.periodSeconds ?? 2_592_000,
353
380
  }
354
381
  },
355
382
  }
356
383
  : undefined
357
384
 
385
+ const sessionStoreCfg = config.server?.sessionStore
386
+ const sessionStore = sessionStoreCfg?.redisUrl
387
+ ? {
388
+ redisUrl: sessionStoreCfg.redisUrl,
389
+ ttlSeconds: sessionStoreCfg.ttlSeconds,
390
+ }
391
+ : undefined
392
+
358
393
  if (mode === 'wallet') {
359
394
  const handle = createWalletServer({
360
395
  wallet: walletResult.wallet,
@@ -365,6 +400,7 @@ async function startWalletServer(
365
400
  internalPath: null,
366
401
  accounts: accounts?.walletServerAccounts,
367
402
  hosting,
403
+ sessionStore,
368
404
  })
369
405
  const port = await handle.start()
370
406
  const notes = [
@@ -394,18 +430,20 @@ async function startWalletServer(
394
430
 
395
431
  const handle = await createHostServer({
396
432
  wallet: walletResult.wallet,
433
+ sessionStore,
397
434
  storage: walletResult.getActiveStorage(),
398
435
  serverIdentityKey: walletResult.wallet.identityKey,
399
436
  listen: { port: resolved.port, host: resolved.host },
400
437
  accounts: accounts?.walletServerAccounts,
401
438
  hosting,
402
439
  paymail: {
403
- baseUrl: paymailCfg?.baseUrl ?? `http://${resolved.host}:${resolved.port}`,
440
+ baseUrl:
441
+ paymailCfg?.baseUrl ?? `http://${resolved.host}:${resolved.port}`,
404
442
  stackUrl: paymailCfg?.stackUrl ?? 'https://api.1sat.app',
405
443
  pendingStore,
406
444
  hostWallet: hostingEnabled ? walletResult.wallet : undefined,
407
445
  requireEntitlement: hostingEnabled,
408
- messageboxUrl: `http://127.0.0.1:${resolved.port}/`,
446
+ messageboxUrl: `http://127.0.0.1:${resolved.port}/messagebox`,
409
447
  hostPrivateKey: resolved.privateKey,
410
448
  },
411
449
  messagebox: {
@@ -440,7 +478,8 @@ function createMessageboxKnex(
440
478
  ) {
441
479
  if (storage.provider === 'bun-sqlite') {
442
480
  const filename =
443
- messagebox?.dbPath ?? join(resolved.dataDir, `messagebox-${resolved.chain}.db`)
481
+ messagebox?.dbPath ??
482
+ join(resolved.dataDir, `messagebox-${resolved.chain}.db`)
444
483
  return (knexLib as any)({
445
484
  client: 'sqlite3',
446
485
  connection: { filename },
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 {
@@ -123,10 +128,23 @@ export interface ServerHostingConfig {
123
128
  enabled?: boolean
124
129
  /** Sats per subscription period. */
125
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
126
137
  /** Period length in seconds (e.g. 2592000 ≈ 30 days). */
127
138
  periodSeconds?: number
128
139
  }
129
140
 
141
+ export interface ServerSessionStoreConfig {
142
+ /** e.g. redis://127.0.0.1:6379 */
143
+ redisUrl?: string
144
+ /** Session TTL in Redis, refreshed on writes. Defaults to 86400 (24h). */
145
+ ttlSeconds?: number
146
+ }
147
+
130
148
  export interface ServerMonitorConfig {
131
149
  /**
132
150
  * Run the background monitor task loop (post waiting txs, check for
@@ -153,6 +171,13 @@ export interface ServerConfig {
153
171
  paymail?: ServerPaymailConfig
154
172
  /** Hosted paymail subscription (wallet serve /hosting/*). */
155
173
  hosting?: ServerHostingConfig
174
+ /** Shared BSV/USD repricer engine for accounts + hosting prices. */
175
+ repricer?: RepricerConfig
176
+ /**
177
+ * Redis-shared BRC-104 sessions. Required when multiple serve instances
178
+ * run behind one load balancer; unset keeps sessions in-memory.
179
+ */
180
+ sessionStore?: ServerSessionStoreConfig
156
181
  /** Background monitor task loop. Defaults to enabled. */
157
182
  monitor?: ServerMonitorConfig
158
183
  }
@@ -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'