@1sat/cli 0.0.63 → 0.0.65

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.63",
3
+ "version": "0.0.65",
4
4
  "description": "CLI for 1Sat Ordinals SDK",
5
5
  "type": "module",
6
6
  "main": "./src/cli.ts",
@@ -15,7 +15,8 @@
15
15
  "preinstall": "node scripts/check-bun.cjs",
16
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",
17
17
  "dev": "bun run src/cli.ts",
18
- "lint": "biome check src"
18
+ "lint": "biome check src",
19
+ "test": "bun test"
19
20
  },
20
21
  "keywords": [
21
22
  "1sat",
@@ -25,11 +26,11 @@
25
26
  ],
26
27
  "license": "MIT",
27
28
  "dependencies": {
28
- "@1sat/actions": "0.0.153",
29
- "@1sat/client": "0.0.36",
30
- "@1sat/types": "0.0.28",
31
- "@1sat/wallet-node": "0.0.50",
32
- "@1sat/wallet-server": "0.0.19",
29
+ "@1sat/actions": "0.0.162",
30
+ "@1sat/client": "0.0.38",
31
+ "@1sat/types": "0.0.30",
32
+ "@1sat/wallet-node": "0.0.51",
33
+ "@1sat/wallet-server": "0.0.20",
33
34
  "@bsv/sdk": "^2.0.13",
34
35
  "@bsv/wallet-toolbox": "npm:@bopen-io/wallet-toolbox@2.1.21-parity-fix.2",
35
36
  "chalk": "^5.0.0",
@@ -31,12 +31,18 @@ import {
31
31
  type ServerAccountsConfig,
32
32
  type ServerStorageConfig,
33
33
  loadConfig,
34
+ setConfigPath,
34
35
  } from '../config'
35
36
  import { ensureDataDir } from '../config'
36
37
  import { printCommandHelp } from '../help'
37
38
  import { loadKey } from '../keys'
38
39
  import { clearMonitorPid, writeMonitorPid } from '../monitor-lock'
39
40
  import { fatal } from '../output'
41
+ import {
42
+ buildPriceUpdateTask,
43
+ createAccountsConfigLoader,
44
+ resolveRateProvider,
45
+ } from '../repricer'
40
46
  import { startMessagebox } from './serve-messagebox'
41
47
 
42
48
  const DEFAULT_HOST = '127.0.0.1'
@@ -72,6 +78,14 @@ interface ResolvedAccounts {
72
78
  satsPerUnit: number
73
79
  durationBlocks: number
74
80
  freeIdentityKeys: string[]
81
+ repricer?: {
82
+ enabled?: boolean
83
+ targetUsd?: number
84
+ intervalMs?: number
85
+ provider?: string
86
+ maxMovePct?: number
87
+ minSats?: number
88
+ }
75
89
  }
76
90
 
77
91
  export async function handleServeCommand(
@@ -217,6 +231,7 @@ function resolveAccounts(accounts?: ServerAccountsConfig): ResolvedAccounts {
217
231
  satsPerUnit: accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
218
232
  durationBlocks: accounts?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
219
233
  freeIdentityKeys: accounts?.freeIdentityKeys ?? [],
234
+ repricer: accounts?.repricer,
220
235
  }
221
236
  }
222
237
 
@@ -259,6 +274,42 @@ async function runWithStorage(
259
274
  ? undefined
260
275
  : await startWalletServer(resolved, walletResult, accounts)
261
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
+ ) {
286
+ walletResult.monitor.addTask(
287
+ buildPriceUpdateTask({
288
+ monitor: walletResult.monitor,
289
+ rateProvider: resolveRateProvider(r.provider ?? 'whatsonchain', {
290
+ chain: resolved.chain,
291
+ }),
292
+ intervalMs: r.intervalMs ?? 15 * 60 * 1000,
293
+ targetUsd: r.targetUsd,
294
+ bounds: {
295
+ maxMovePct: r.maxMovePct ?? 25,
296
+ minSats: r.minSats ?? 1,
297
+ },
298
+ readCurrentSats: () =>
299
+ loadConfig().server?.accounts?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
300
+ onPersist: async (sats) => {
301
+ setConfigPath('server.accounts.satsPerUnit', sats)
302
+ },
303
+ }),
304
+ )
305
+ console.log(
306
+ `[repricer] enabled — $${r.targetUsd}/unit every ${Math.round(
307
+ (r.intervalMs ?? 900_000) / 1000,
308
+ )}s via ${r.provider ?? 'whatsonchain'}`,
309
+ )
310
+ }
311
+ }
312
+
262
313
  if (mode !== 'wallet') {
263
314
  // startTasks loops until stopTasks flips its flag. Fire without
264
315
  // awaiting so the caller can install shutdown handlers and write
@@ -318,16 +369,25 @@ async function buildAccountsForServer(
318
369
  // storage; accounts semantics don't apply.
319
370
  if (resolved.activeRemote) return undefined
320
371
 
372
+ const getConfig = createAccountsConfigLoader({
373
+ ttlMs: 60_000,
374
+ read: () => {
375
+ const fresh = loadConfig()
376
+ const a = fresh.server?.accounts
377
+ return {
378
+ enabled: a?.enabled ?? false,
379
+ baselineBytes: a?.baselineBytes ?? DEFAULT_BASELINE_BYTES,
380
+ purchaseUnitBytes: a?.purchaseUnitBytes ?? DEFAULT_PURCHASE_UNIT_BYTES,
381
+ satsPerUnit: a?.satsPerUnit ?? DEFAULT_SATS_PER_UNIT,
382
+ durationBlocks: a?.durationBlocks ?? DEFAULT_DURATION_BLOCKS,
383
+ freeIdentityKeys: a?.freeIdentityKeys ?? [],
384
+ }
385
+ },
386
+ })
387
+
321
388
  return {
322
389
  walletServerAccounts: {
323
- config: {
324
- enabled: resolved.accounts.enabled,
325
- baselineBytes: resolved.accounts.baselineBytes,
326
- purchaseUnitBytes: resolved.accounts.purchaseUnitBytes,
327
- satsPerUnit: resolved.accounts.satsPerUnit,
328
- durationBlocks: resolved.accounts.durationBlocks,
329
- freeIdentityKeys: resolved.accounts.freeIdentityKeys,
330
- },
390
+ getConfig,
331
391
  currentBlock: () => walletResult.services.chaintracks.currentHeight(),
332
392
  },
333
393
  }
package/src/config.ts CHANGED
@@ -25,6 +25,21 @@ export type ServerStorageConfig =
25
25
  | ServerStorageBunSqliteConfig
26
26
  | ServerStoragePgConfig
27
27
 
28
+ export interface RepricerConfig {
29
+ /** Master toggle. Defaults to false. */
30
+ enabled?: boolean
31
+ /** Target price per `purchaseUnitBytes` chunk, in USD. */
32
+ targetUsd?: number
33
+ /** Interval between rate checks in ms. Defaults to 900000 (15 min). */
34
+ intervalMs?: number
35
+ /** Rate provider name. Defaults to "whatsonchain". */
36
+ provider?: string
37
+ /** Max percent change allowed per update. Larger moves are skipped. Defaults to 25. */
38
+ maxMovePct?: number
39
+ /** Lower bound for `satsPerUnit`. Defaults to 1. */
40
+ minSats?: number
41
+ }
42
+
28
43
  export interface ServerAccountsConfig {
29
44
  /** Master toggle. Defaults to false when omitted. */
30
45
  enabled?: boolean
@@ -38,6 +53,8 @@ export interface ServerAccountsConfig {
38
53
  durationBlocks?: number
39
54
  /** Identity keys that bypass metering (server's own key is auto-added). */
40
55
  freeIdentityKeys?: string[]
56
+ /** Optional auto-repricer. When enabled, updates `satsPerUnit` from a live BSV/USD rate. */
57
+ repricer?: RepricerConfig
41
58
  }
42
59
 
43
60
  export interface ServerMessageboxConfig {
@@ -0,0 +1,80 @@
1
+ import { computeReprice } from './computeReprice'
2
+ import type { RateProvider, RepricerBounds } from './types'
3
+
4
+ export interface PriceUpdateTaskOptions {
5
+ monitor: unknown
6
+ rateProvider: RateProvider
7
+ /** Interval between successful tick attempts. */
8
+ intervalMs: number
9
+ /** USD target per purchase unit. Task no-ops when this is 0. */
10
+ targetUsd: number
11
+ 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>
16
+ }
17
+
18
+ export interface PriceUpdateTask {
19
+ monitor: unknown
20
+ storage: unknown
21
+ name: 'PriceUpdate'
22
+ lastRunMsecsSinceEpoch: number
23
+ asyncSetup(): Promise<void>
24
+ trigger(now: number): { run: boolean }
25
+ runTask(): Promise<string>
26
+ }
27
+
28
+ export function buildPriceUpdateTask(
29
+ options: PriceUpdateTaskOptions,
30
+ ): PriceUpdateTask {
31
+ const {
32
+ monitor,
33
+ rateProvider,
34
+ intervalMs,
35
+ targetUsd,
36
+ bounds,
37
+ readCurrentSats,
38
+ onPersist,
39
+ } = options
40
+
41
+ const storage = (monitor as { storage?: unknown } | null | undefined)?.storage
42
+
43
+ return {
44
+ monitor,
45
+ storage,
46
+ name: 'PriceUpdate',
47
+ lastRunMsecsSinceEpoch: 0,
48
+ async asyncSetup() {},
49
+ trigger(now: number): { run: boolean } {
50
+ if (!(targetUsd > 0)) return { run: false }
51
+ if (now - this.lastRunMsecsSinceEpoch < intervalMs) return { run: false }
52
+ return { run: true }
53
+ },
54
+ async runTask(): Promise<string> {
55
+ let quote: Awaited<ReturnType<RateProvider['getBsvUsd']>>
56
+ try {
57
+ quote = await rateProvider.getBsvUsd()
58
+ } catch (err) {
59
+ return `rate fetch failed: ${(err as Error).message}`
60
+ }
61
+
62
+ const currentSats = readCurrentSats()
63
+ const result = computeReprice({
64
+ targetUsd,
65
+ bsvUsd: quote.bsvUsd,
66
+ currentSats,
67
+ bounds,
68
+ })
69
+
70
+ if (result.status === 'skipped') return `skipped: ${result.reason}`
71
+
72
+ try {
73
+ await onPersist(result.newSats)
74
+ } catch (err) {
75
+ return `persist failed: ${(err as Error).message}`
76
+ }
77
+ return `${currentSats} → ${result.newSats} sats/unit @ $${quote.bsvUsd}/BSV`
78
+ },
79
+ }
80
+ }
@@ -0,0 +1,34 @@
1
+ import type { ComputeRepriceInput, ComputeRepriceResult } from './types'
2
+
3
+ const SATS_PER_BSV = 100_000_000
4
+
5
+ export function computeReprice(
6
+ input: ComputeRepriceInput,
7
+ ): ComputeRepriceResult {
8
+ const { targetUsd, bsvUsd, currentSats, bounds } = input
9
+
10
+ if (!(targetUsd > 0))
11
+ return { status: 'skipped', reason: 'targetUsd must be > 0' }
12
+ if (!(bsvUsd > 0)) return { status: 'skipped', reason: 'bsvUsd must be > 0' }
13
+ if (!(currentSats > 0))
14
+ return { status: 'skipped', reason: 'currentSats must be > 0' }
15
+
16
+ const newSats = Math.round((targetUsd / bsvUsd) * SATS_PER_BSV)
17
+
18
+ if (newSats < bounds.minSats) {
19
+ return {
20
+ status: 'skipped',
21
+ reason: `computed ${newSats} is below minSats ${bounds.minSats}`,
22
+ }
23
+ }
24
+
25
+ const movePct = (Math.abs(newSats - currentSats) / currentSats) * 100
26
+ if (movePct > bounds.maxMovePct) {
27
+ return {
28
+ status: 'skipped',
29
+ reason: `move ${movePct.toFixed(2)}% exceeds maxMovePct ${bounds.maxMovePct}`,
30
+ }
31
+ }
32
+
33
+ return { status: 'ok', newSats }
34
+ }
@@ -0,0 +1,37 @@
1
+ import type {
2
+ AccountsConfig,
3
+ AccountsConfigProvider,
4
+ } from '@1sat/wallet-server'
5
+
6
+ export interface AccountsConfigLoaderOptions {
7
+ ttlMs: number
8
+ read: () => AccountsConfig
9
+ /** Override clock (testing). */
10
+ now?: () => number
11
+ }
12
+
13
+ /**
14
+ * Build an `AccountsConfigProvider` that re-reads its source no more often
15
+ * than `ttlMs`. If `read` throws after the first success, the last good
16
+ * value is returned and the failure is silently absorbed.
17
+ */
18
+ export function createAccountsConfigLoader(
19
+ options: AccountsConfigLoaderOptions,
20
+ ): AccountsConfigProvider {
21
+ const now = options.now ?? (() => Date.now())
22
+ let cached: AccountsConfig | undefined
23
+ let cachedAt = 0
24
+
25
+ return () => {
26
+ const t = now()
27
+ if (cached && t - cachedAt < options.ttlMs) return cached
28
+ try {
29
+ cached = options.read()
30
+ cachedAt = t
31
+ return cached
32
+ } catch (err) {
33
+ if (cached) return cached
34
+ throw err
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,14 @@
1
+ export { buildPriceUpdateTask } from './buildPriceUpdateTask'
2
+ export type { PriceUpdateTaskOptions } from './buildPriceUpdateTask'
3
+ export { computeReprice } from './computeReprice'
4
+ export { createAccountsConfigLoader } from './configLoader'
5
+ export type { AccountsConfigLoaderOptions } from './configLoader'
6
+ export { resolveRateProvider } from './providers'
7
+ export { createWhatsOnChainProvider } from './whatsOnChain'
8
+ export type {
9
+ BsvUsdQuote,
10
+ ComputeRepriceInput,
11
+ ComputeRepriceResult,
12
+ RateProvider,
13
+ RepricerBounds,
14
+ } from './types'
@@ -0,0 +1,20 @@
1
+ import type { RateProvider } from './types'
2
+ import { createWhatsOnChainProvider } from './whatsOnChain'
3
+
4
+ export interface ResolveProviderOptions {
5
+ chain: 'main' | 'test'
6
+ }
7
+
8
+ export function resolveRateProvider(
9
+ name: string,
10
+ options: ResolveProviderOptions,
11
+ ): RateProvider {
12
+ switch (name) {
13
+ case 'whatsonchain':
14
+ return createWhatsOnChainProvider({ chain: options.chain })
15
+ default:
16
+ throw new Error(
17
+ `Unknown rate provider "${name}". Supported: whatsonchain.`,
18
+ )
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ export interface BsvUsdQuote {
2
+ bsvUsd: number
3
+ timestamp: number
4
+ source: string
5
+ }
6
+
7
+ export interface RateProvider {
8
+ readonly name: string
9
+ getBsvUsd(): Promise<BsvUsdQuote>
10
+ }
11
+
12
+ export interface RepricerBounds {
13
+ /** Reject moves larger than this percent. */
14
+ maxMovePct: number
15
+ /** Floor for the new sats value. */
16
+ minSats: number
17
+ }
18
+
19
+ export interface ComputeRepriceInput {
20
+ targetUsd: number
21
+ bsvUsd: number
22
+ currentSats: number
23
+ bounds: RepricerBounds
24
+ }
25
+
26
+ export type ComputeRepriceResult =
27
+ | { status: 'ok'; newSats: number }
28
+ | { status: 'skipped'; reason: string }
@@ -0,0 +1,36 @@
1
+ import type { BsvUsdQuote, RateProvider } from './types'
2
+
3
+ export interface WhatsOnChainOptions {
4
+ chain: 'main' | 'test'
5
+ /** Override base URL (testing). */
6
+ baseUrl?: string
7
+ }
8
+
9
+ export function createWhatsOnChainProvider(
10
+ options: WhatsOnChainOptions,
11
+ ): RateProvider {
12
+ const base = options.baseUrl ?? 'https://api.whatsonchain.com/v1/bsv'
13
+ const url = `${base}/${options.chain}/exchangerate`
14
+
15
+ return {
16
+ name: 'whatsonchain',
17
+ async getBsvUsd(): Promise<BsvUsdQuote> {
18
+ const res = await fetch(url)
19
+ if (!res.ok) {
20
+ throw new Error(
21
+ `whatsonchain: ${res.status} ${await res.text().catch(() => '')}`,
22
+ )
23
+ }
24
+ const body = (await res.json()) as { rate?: number; time?: number }
25
+ if (typeof body.rate !== 'number' || !(body.rate > 0)) {
26
+ throw new Error('whatsonchain: missing or invalid "rate" in response')
27
+ }
28
+ return {
29
+ bsvUsd: body.rate,
30
+ timestamp:
31
+ typeof body.time === 'number' ? body.time * 1000 : Date.now(),
32
+ source: 'whatsonchain',
33
+ }
34
+ },
35
+ }
36
+ }