@cogcoin/client 0.5.14 → 1.0.0

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.
Files changed (172) hide show
  1. package/README.md +80 -25
  2. package/dist/app-paths.d.ts +5 -6
  3. package/dist/app-paths.js +8 -16
  4. package/dist/art/balance.txt +10 -0
  5. package/dist/art/welcome.txt +16 -0
  6. package/dist/bitcoind/bootstrap/controller.d.ts +1 -0
  7. package/dist/bitcoind/bootstrap/controller.js +53 -1
  8. package/dist/bitcoind/client/follow-block-times.d.ts +1 -0
  9. package/dist/bitcoind/client/follow-block-times.js +1 -1
  10. package/dist/bitcoind/client/internal-types.d.ts +7 -3
  11. package/dist/bitcoind/client/managed-client.d.ts +4 -2
  12. package/dist/bitcoind/client/managed-client.js +14 -0
  13. package/dist/bitcoind/client/sync-engine.js +72 -11
  14. package/dist/bitcoind/hash-order.d.ts +4 -0
  15. package/dist/bitcoind/hash-order.js +13 -0
  16. package/dist/bitcoind/indexer-daemon-main.js +11 -3
  17. package/dist/bitcoind/normalize.js +3 -2
  18. package/dist/bitcoind/processing-start-height.d.ts +5 -0
  19. package/dist/bitcoind/processing-start-height.js +7 -0
  20. package/dist/bitcoind/progress/constants.d.ts +4 -0
  21. package/dist/bitcoind/progress/constants.js +4 -0
  22. package/dist/bitcoind/progress/controller.d.ts +2 -1
  23. package/dist/bitcoind/progress/controller.js +3 -3
  24. package/dist/bitcoind/progress/follow-scene.d.ts +6 -2
  25. package/dist/bitcoind/progress/follow-scene.js +29 -6
  26. package/dist/bitcoind/progress/formatting.d.ts +1 -0
  27. package/dist/bitcoind/progress/formatting.js +6 -0
  28. package/dist/bitcoind/progress/train-scene.js +37 -18
  29. package/dist/bitcoind/progress/tty-renderer.d.ts +6 -1
  30. package/dist/bitcoind/progress/tty-renderer.js +8 -4
  31. package/dist/bitcoind/rpc.d.ts +2 -1
  32. package/dist/bitcoind/rpc.js +3 -0
  33. package/dist/bitcoind/types.d.ts +16 -0
  34. package/dist/bytes.d.ts +1 -0
  35. package/dist/bytes.js +3 -0
  36. package/dist/cli/art.d.ts +2 -0
  37. package/dist/cli/art.js +37 -0
  38. package/dist/cli/commands/client-admin.d.ts +2 -0
  39. package/dist/cli/commands/client-admin.js +91 -0
  40. package/dist/cli/commands/follow.js +0 -2
  41. package/dist/cli/commands/mining-admin.js +6 -47
  42. package/dist/cli/commands/mining-read.js +11 -50
  43. package/dist/cli/commands/mining-runtime.js +38 -3
  44. package/dist/cli/commands/service-runtime.js +0 -2
  45. package/dist/cli/commands/status.js +8 -2
  46. package/dist/cli/commands/sync.js +51 -4
  47. package/dist/cli/commands/wallet-admin.js +142 -136
  48. package/dist/cli/commands/wallet-mutation.js +91 -79
  49. package/dist/cli/commands/wallet-read.js +15 -18
  50. package/dist/cli/context.js +4 -14
  51. package/dist/cli/mining-format.d.ts +0 -1
  52. package/dist/cli/mining-format.js +5 -37
  53. package/dist/cli/mining-json.d.ts +0 -18
  54. package/dist/cli/mining-json.js +0 -35
  55. package/dist/cli/mutation-command-groups.d.ts +1 -2
  56. package/dist/cli/mutation-command-groups.js +0 -5
  57. package/dist/cli/mutation-json.d.ts +24 -145
  58. package/dist/cli/mutation-json.js +30 -136
  59. package/dist/cli/mutation-resolved-json.d.ts +0 -7
  60. package/dist/cli/mutation-resolved-json.js +4 -10
  61. package/dist/cli/mutation-success.d.ts +2 -0
  62. package/dist/cli/mutation-success.js +11 -1
  63. package/dist/cli/mutation-text-format.js +1 -3
  64. package/dist/cli/output.d.ts +1 -1
  65. package/dist/cli/output.js +254 -231
  66. package/dist/cli/parse.d.ts +1 -1
  67. package/dist/cli/parse.js +93 -122
  68. package/dist/cli/preview-json.d.ts +17 -120
  69. package/dist/cli/preview-json.js +14 -97
  70. package/dist/cli/prompt.js +8 -13
  71. package/dist/cli/read-json.d.ts +15 -37
  72. package/dist/cli/read-json.js +44 -140
  73. package/dist/cli/runner.js +10 -13
  74. package/dist/cli/types.d.ts +8 -17
  75. package/dist/cli/types.js +0 -2
  76. package/dist/cli/wallet-format.d.ts +1 -0
  77. package/dist/cli/wallet-format.js +205 -144
  78. package/dist/cli/workflow-hints.d.ts +3 -3
  79. package/dist/cli/workflow-hints.js +11 -8
  80. package/dist/client/default-client.d.ts +3 -1
  81. package/dist/client/default-client.js +45 -2
  82. package/dist/client/factory.js +1 -1
  83. package/dist/client/initialization.js +23 -0
  84. package/dist/client/persistence.js +5 -5
  85. package/dist/client/store-adapter.js +1 -0
  86. package/dist/sqlite/checkpoints.d.ts +1 -0
  87. package/dist/sqlite/checkpoints.js +7 -0
  88. package/dist/sqlite/store.js +14 -1
  89. package/dist/types.d.ts +1 -0
  90. package/dist/wallet/coin-control.d.ts +41 -11
  91. package/dist/wallet/coin-control.js +100 -357
  92. package/dist/wallet/descriptor-normalization.d.ts +1 -3
  93. package/dist/wallet/descriptor-normalization.js +0 -16
  94. package/dist/wallet/lifecycle.d.ts +7 -99
  95. package/dist/wallet/lifecycle.js +513 -968
  96. package/dist/wallet/managed-core-wallet.d.ts +13 -0
  97. package/dist/wallet/managed-core-wallet.js +20 -0
  98. package/dist/wallet/mining/constants.d.ts +5 -12
  99. package/dist/wallet/mining/constants.js +5 -12
  100. package/dist/wallet/mining/control.d.ts +1 -13
  101. package/dist/wallet/mining/control.js +45 -349
  102. package/dist/wallet/mining/index.d.ts +3 -4
  103. package/dist/wallet/mining/index.js +1 -2
  104. package/dist/wallet/mining/runner.d.ts +179 -6
  105. package/dist/wallet/mining/runner.js +891 -501
  106. package/dist/wallet/mining/runtime-artifacts.js +23 -3
  107. package/dist/wallet/mining/sentence-protocol.d.ts +44 -0
  108. package/dist/wallet/mining/sentence-protocol.js +123 -0
  109. package/dist/wallet/mining/sentences.d.ts +4 -8
  110. package/dist/wallet/mining/sentences.js +3 -52
  111. package/dist/wallet/mining/state.d.ts +11 -6
  112. package/dist/wallet/mining/state.js +7 -6
  113. package/dist/wallet/mining/types.d.ts +2 -30
  114. package/dist/wallet/mining/visualizer.d.ts +31 -3
  115. package/dist/wallet/mining/visualizer.js +135 -13
  116. package/dist/wallet/read/context.d.ts +0 -2
  117. package/dist/wallet/read/context.js +119 -140
  118. package/dist/wallet/read/filter.js +2 -11
  119. package/dist/wallet/read/index.d.ts +1 -1
  120. package/dist/wallet/read/project.js +24 -77
  121. package/dist/wallet/read/types.d.ts +10 -25
  122. package/dist/wallet/reset.d.ts +0 -1
  123. package/dist/wallet/reset.js +60 -138
  124. package/dist/wallet/root-resolution.d.ts +1 -5
  125. package/dist/wallet/root-resolution.js +0 -18
  126. package/dist/wallet/runtime.d.ts +0 -6
  127. package/dist/wallet/runtime.js +0 -8
  128. package/dist/wallet/state/client-password-agent.js +208 -0
  129. package/dist/wallet/state/client-password.d.ts +65 -0
  130. package/dist/wallet/state/client-password.js +952 -0
  131. package/dist/wallet/state/crypto.d.ts +1 -20
  132. package/dist/wallet/state/crypto.js +0 -63
  133. package/dist/wallet/state/provider.d.ts +23 -11
  134. package/dist/wallet/state/provider.js +248 -290
  135. package/dist/wallet/state/storage.d.ts +2 -2
  136. package/dist/wallet/state/storage.js +48 -16
  137. package/dist/wallet/tx/anchor.d.ts +3 -28
  138. package/dist/wallet/tx/anchor.js +349 -1240
  139. package/dist/wallet/tx/bitcoin-transfer.d.ts +35 -0
  140. package/dist/wallet/tx/bitcoin-transfer.js +200 -0
  141. package/dist/wallet/tx/cog.d.ts +5 -1
  142. package/dist/wallet/tx/cog.js +149 -185
  143. package/dist/wallet/tx/common.d.ts +74 -10
  144. package/dist/wallet/tx/common.js +315 -138
  145. package/dist/wallet/tx/domain-admin.d.ts +3 -1
  146. package/dist/wallet/tx/domain-admin.js +61 -99
  147. package/dist/wallet/tx/domain-market.d.ts +5 -1
  148. package/dist/wallet/tx/domain-market.js +221 -228
  149. package/dist/wallet/tx/field.d.ts +4 -10
  150. package/dist/wallet/tx/field.js +84 -914
  151. package/dist/wallet/tx/identity-selector.d.ts +9 -3
  152. package/dist/wallet/tx/identity-selector.js +17 -35
  153. package/dist/wallet/tx/index.d.ts +3 -1
  154. package/dist/wallet/tx/index.js +2 -1
  155. package/dist/wallet/tx/register.d.ts +3 -1
  156. package/dist/wallet/tx/register.js +62 -220
  157. package/dist/wallet/tx/reputation.d.ts +3 -1
  158. package/dist/wallet/tx/reputation.js +58 -95
  159. package/dist/wallet/types.d.ts +8 -122
  160. package/package.json +5 -5
  161. package/dist/wallet/archive.d.ts +0 -4
  162. package/dist/wallet/archive.js +0 -41
  163. package/dist/wallet/mining/hook-protocol.d.ts +0 -47
  164. package/dist/wallet/mining/hook-protocol.js +0 -161
  165. package/dist/wallet/mining/hook-runner.js +0 -52
  166. package/dist/wallet/mining/hooks.d.ts +0 -38
  167. package/dist/wallet/mining/hooks.js +0 -520
  168. package/dist/wallet/state/explicit-lock.d.ts +0 -4
  169. package/dist/wallet/state/explicit-lock.js +0 -19
  170. package/dist/wallet/state/session.d.ts +0 -12
  171. package/dist/wallet/state/session.js +0 -23
  172. /package/dist/wallet/{mining/hook-runner.d.ts → state/client-password-agent.d.ts} +0 -0
@@ -54,6 +54,19 @@ async function setBitcoinSyncProgress(dependencies, info, targetHeightCap) {
54
54
  : "Reading blocks from the managed Bitcoin node.",
55
55
  });
56
56
  }
57
+ function isMissingBlockRecordError(error) {
58
+ return error instanceof Error && error.message.startsWith("client_store_missing_block_record_");
59
+ }
60
+ async function setDeepRecoveryProgress(dependencies, blocks, bestHeight, message) {
61
+ await dependencies.progress.setPhase("cogcoin_sync", {
62
+ blocks,
63
+ headers: bestHeight,
64
+ targetHeight: bestHeight,
65
+ etaSeconds: estimateEtaSeconds(dependencies.cogcoinRateTracker, blocks, bestHeight),
66
+ lastError: null,
67
+ message,
68
+ });
69
+ }
57
70
  function resolveIndexedHeightForReplayWindow(tip, startHeight) {
58
71
  return tip?.height ?? (startHeight - 1);
59
72
  }
@@ -100,20 +113,50 @@ async function runWithManagedRpcRetry(dependencies, retryState, operation) {
100
113
  }
101
114
  }
102
115
  async function findCommonAncestor(dependencies, tip, bestHeight, runRpc) {
103
- const startHeight = Math.min(tip.height, bestHeight);
104
- for (let height = startHeight; height >= dependencies.startHeight; height -= 1) {
116
+ const searchStartHeight = Math.min(tip.height, bestHeight);
117
+ for (let height = searchStartHeight; height >= dependencies.startHeight; height -= 1) {
105
118
  const localHashHex = height === tip.height
106
119
  ? tip.blockHashHex
107
120
  : (await dependencies.store.loadBlockRecord(height))?.blockHashHex ?? null;
108
121
  if (localHashHex === null) {
109
- continue;
122
+ return {
123
+ kind: "checkpoint_recovery",
124
+ };
110
125
  }
111
126
  const chainHashHex = await runRpc(() => dependencies.rpc.getBlockHash(height));
112
127
  if (chainHashHex === localHashHex) {
113
- return height;
128
+ return {
129
+ kind: "rewind",
130
+ rewindTarget: height,
131
+ };
114
132
  }
115
133
  }
116
- return dependencies.startHeight - 1;
134
+ return {
135
+ kind: "rewind",
136
+ rewindTarget: dependencies.startHeight - 1,
137
+ };
138
+ }
139
+ async function recoverFromCheckpoint(dependencies, startTip, bestHeight, runRpc) {
140
+ let checkpoint = await dependencies.store.loadLatestCheckpointAtOrBelow(Math.min(startTip.height, bestHeight));
141
+ while (checkpoint !== null) {
142
+ const currentCheckpoint = checkpoint;
143
+ const chainHashHex = await runRpc(() => dependencies.rpc.getBlockHash(currentCheckpoint.height));
144
+ if (chainHashHex === currentCheckpoint.blockHashHex) {
145
+ await setDeepRecoveryProgress(dependencies, currentCheckpoint.height, bestHeight, `Retained rewind window exhausted; restoring checkpoint at height ${currentCheckpoint.height.toLocaleString()} and replaying.`);
146
+ await dependencies.client.restoreCheckpoint(currentCheckpoint);
147
+ return {
148
+ rewoundBlocks: startTip.height - currentCheckpoint.height,
149
+ commonAncestorHeight: currentCheckpoint.height,
150
+ };
151
+ }
152
+ checkpoint = await dependencies.store.loadLatestCheckpointAtOrBelow(currentCheckpoint.height - 1);
153
+ }
154
+ await setDeepRecoveryProgress(dependencies, Math.max(0, dependencies.startHeight - 1), bestHeight, "Retained rewind window exhausted; resetting to the processing start and replaying.");
155
+ await dependencies.client.resetToInitialState();
156
+ return {
157
+ rewoundBlocks: startTip.height - dependencies.startHeight + 1,
158
+ commonAncestorHeight: null,
159
+ };
117
160
  }
118
161
  async function syncAgainstBestHeight(dependencies, bestHeight, runRpc) {
119
162
  if (bestHeight < dependencies.startHeight) {
@@ -127,11 +170,26 @@ async function syncAgainstBestHeight(dependencies, bestHeight, runRpc) {
127
170
  let rewoundBlocks = 0;
128
171
  let commonAncestorHeight = null;
129
172
  if (startTip !== null) {
130
- const rewindTarget = await findCommonAncestor(dependencies, startTip, bestHeight, runRpc);
131
- if (rewindTarget < startTip.height) {
132
- commonAncestorHeight = rewindTarget < dependencies.startHeight ? null : rewindTarget;
133
- await dependencies.client.rewindToHeight(rewindTarget);
134
- rewoundBlocks = startTip.height - rewindTarget;
173
+ const ancestor = await findCommonAncestor(dependencies, startTip, bestHeight, runRpc);
174
+ if (ancestor.kind === "checkpoint_recovery") {
175
+ const recovered = await recoverFromCheckpoint(dependencies, startTip, bestHeight, runRpc);
176
+ rewoundBlocks = recovered.rewoundBlocks;
177
+ commonAncestorHeight = recovered.commonAncestorHeight;
178
+ }
179
+ else if (ancestor.rewindTarget < startTip.height) {
180
+ try {
181
+ commonAncestorHeight = ancestor.rewindTarget < dependencies.startHeight ? null : ancestor.rewindTarget;
182
+ await dependencies.client.rewindToHeight(ancestor.rewindTarget);
183
+ rewoundBlocks = startTip.height - ancestor.rewindTarget;
184
+ }
185
+ catch (error) {
186
+ if (!isMissingBlockRecordError(error)) {
187
+ throw error;
188
+ }
189
+ const recovered = await recoverFromCheckpoint(dependencies, startTip, bestHeight, runRpc);
190
+ rewoundBlocks = recovered.rewoundBlocks;
191
+ commonAncestorHeight = recovered.commonAncestorHeight;
192
+ }
135
193
  }
136
194
  }
137
195
  const tipAfterRewind = await dependencies.client.getTip();
@@ -165,6 +223,7 @@ export async function syncToTip(dependencies) {
165
223
  const runRpc = (operation) => runWithManagedRpcRetry(dependencies, retryState, operation);
166
224
  throwIfAborted(dependencies.abortSignal);
167
225
  await runRpc(() => dependencies.node.validate());
226
+ await dependencies.bootstrap.cleanupObsoleteSnapshotFilesIfNeeded().catch(() => false);
168
227
  const indexedTipBeforeBootstrap = await dependencies.client.getTip();
169
228
  await runRpc(() => dependencies.bootstrap.ensureReady(indexedTipBeforeBootstrap, dependencies.node.expectedChain, {
170
229
  signal: dependencies.abortSignal,
@@ -219,6 +278,7 @@ export async function syncToTip(dependencies) {
219
278
  && dependencies.targetHeightCap !== undefined
220
279
  && endBestHeight >= dependencies.targetHeightCap;
221
280
  if (reachedTargetHeightCap && caughtUpCogcoin) {
281
+ await dependencies.bootstrap.cleanupObsoleteSnapshotFilesIfNeeded().catch(() => false);
222
282
  return aggregate;
223
283
  }
224
284
  if (dependencies.targetHeightCap === null
@@ -234,8 +294,9 @@ export async function syncToTip(dependencies) {
234
294
  lastError: null,
235
295
  message: dependencies.isFollowing()
236
296
  ? "Following the live Bitcoin tip."
237
- : "Managed sync fully caught up to the live tip.",
297
+ : "Sync complete.",
238
298
  });
299
+ await dependencies.bootstrap.cleanupObsoleteSnapshotFilesIfNeeded().catch(() => false);
239
300
  return aggregate;
240
301
  }
241
302
  if (endBestHeight >= dependencies.startHeight && finalTip?.height !== endBestHeight) {
@@ -0,0 +1,4 @@
1
+ export declare function displayHashHexToInternalBytes(hashHex: string): Uint8Array;
2
+ export declare function displayHashHexToInternalHex(hashHex: string): string;
3
+ export declare function internalBytesToDisplayHashHex(hash: Uint8Array): string;
4
+ export declare function internalHashHexToDisplayHashHex(hashHex: string): string;
@@ -0,0 +1,13 @@
1
+ import { bytesToHex, hexToBytes, reverseBytes } from "../bytes.js";
2
+ export function displayHashHexToInternalBytes(hashHex) {
3
+ return reverseBytes(hexToBytes(hashHex));
4
+ }
5
+ export function displayHashHexToInternalHex(hashHex) {
6
+ return bytesToHex(displayHashHexToInternalBytes(hashHex));
7
+ }
8
+ export function internalBytesToDisplayHashHex(hash) {
9
+ return bytesToHex(reverseBytes(hash));
10
+ }
11
+ export function internalHashHexToDisplayHashHex(hashHex) {
12
+ return internalBytesToDisplayHashHex(hexToBytes(hashHex));
13
+ }
@@ -1,12 +1,13 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import net from "node:net";
3
3
  import { access, constants, mkdir, readFile, rm } from "node:fs/promises";
4
- import { serializeIndexerState } from "@cogcoin/indexer";
4
+ import { loadBundledGenesisParameters, serializeIndexerState } from "@cogcoin/indexer";
5
5
  import { openManagedBitcoindClientInternal } from "./client.js";
6
6
  import { openClient } from "../client.js";
7
7
  import { openSqliteStore } from "../sqlite/index.js";
8
8
  import { writeRuntimeStatusFile } from "../wallet/fs/status-file.js";
9
9
  import { createRpcClient } from "./node.js";
10
+ import { normalizeCogcoinProcessingStartHeight } from "./processing-start-height.js";
10
11
  import { resolveManagedServicePaths, UNINITIALIZED_WALLET_ROOT_ID } from "./service-paths.js";
11
12
  import { INDEXER_DAEMON_SCHEMA_VERSION, INDEXER_DAEMON_SERVICE_API_VERSION, } from "./types.js";
12
13
  const SNAPSHOT_TTL_MS = 30_000;
@@ -156,6 +157,7 @@ async function main() {
156
157
  const paths = resolveManagedServicePaths(dataDir, walletRootId);
157
158
  const daemonInstanceId = randomUUID();
158
159
  const binaryVersion = await readPackageVersionFromDisk();
160
+ const genesisParameters = await loadBundledGenesisParameters();
159
161
  const startedAtUnixMs = Date.now();
160
162
  const snapshots = new Map();
161
163
  let state = "starting";
@@ -292,12 +294,18 @@ async function main() {
292
294
  backgroundResumePromise = (async () => {
293
295
  const bitcoindStatus = await readManagedBitcoindStatus(paths);
294
296
  const store = await openSqliteStore({ filename: databasePath });
297
+ const chain = bitcoindStatus?.chain ?? "main";
298
+ const startHeight = normalizeCogcoinProcessingStartHeight({
299
+ chain,
300
+ startHeight: bitcoindStatus?.startHeight,
301
+ genesisParameters,
302
+ });
295
303
  try {
296
304
  const client = await openManagedBitcoindClientInternal({
297
305
  store,
298
306
  dataDir,
299
- chain: bitcoindStatus?.chain ?? "main",
300
- startHeight: bitcoindStatus?.startHeight ?? 0,
307
+ chain,
308
+ startHeight,
301
309
  walletRootId,
302
310
  progressOutput: "none",
303
311
  });
@@ -1,4 +1,5 @@
1
1
  import { hexToBytes } from "../bytes.js";
2
+ import { displayHashHexToInternalBytes } from "./hash-order.js";
2
3
  function btcValueToSats(value) {
3
4
  const source = typeof value === "number"
4
5
  ? value.toFixed(8)
@@ -40,8 +41,8 @@ function normalizeTransaction(transaction) {
40
41
  export function normalizeRpcBlock(block) {
41
42
  return {
42
43
  height: block.height,
43
- hash: hexToBytes(block.hash),
44
- previousHash: block.previousblockhash ? hexToBytes(block.previousblockhash) : null,
44
+ hash: displayHashHexToInternalBytes(block.hash),
45
+ previousHash: block.previousblockhash ? displayHashHexToInternalBytes(block.previousblockhash) : null,
45
46
  transactions: block.tx.map(normalizeTransaction),
46
47
  };
47
48
  }
@@ -1,5 +1,10 @@
1
1
  import type { GenesisParameters } from "@cogcoin/indexer/types";
2
2
  export declare function resolveCogcoinProcessingStartHeight(genesisParameters: GenesisParameters): number;
3
+ export declare function normalizeCogcoinProcessingStartHeight(options: {
4
+ chain: "main" | "regtest";
5
+ startHeight: number | null | undefined;
6
+ genesisParameters: GenesisParameters;
7
+ }): number;
3
8
  export declare function assertCogcoinProcessingStartHeight(options: {
4
9
  chain: "main" | "regtest";
5
10
  startHeight: number;
@@ -1,6 +1,13 @@
1
1
  export function resolveCogcoinProcessingStartHeight(genesisParameters) {
2
2
  return genesisParameters.genesisBlock;
3
3
  }
4
+ export function normalizeCogcoinProcessingStartHeight(options) {
5
+ if (options.chain !== "main") {
6
+ return options.startHeight ?? 0;
7
+ }
8
+ const processingStartHeight = resolveCogcoinProcessingStartHeight(options.genesisParameters);
9
+ return Math.max(options.startHeight ?? processingStartHeight, processingStartHeight);
10
+ }
4
11
  export function assertCogcoinProcessingStartHeight(options) {
5
12
  const processingStartHeight = resolveCogcoinProcessingStartHeight(options.genesisParameters);
6
13
  if (options.chain === "main" && options.startHeight < processingStartHeight) {
@@ -7,6 +7,10 @@ export declare const INTRO_ENTRY_MS = 5000;
7
7
  export declare const INTRO_PAUSE_MS = 5000;
8
8
  export declare const INTRO_EXIT_MS = 5000;
9
9
  export declare const INTRO_TOTAL_MS: number;
10
+ export declare const COMPLETION_ENTRY_MS = 5000;
11
+ export declare const COMPLETION_PAUSE_MS = 5000;
12
+ export declare const COMPLETION_EXIT_MS = 0;
13
+ export declare const COMPLETION_TOTAL_MS: number;
10
14
  export declare const STATUS_ELLIPSIS_WIDTH = 3;
11
15
  export declare const STATUS_ELLIPSIS_TICK_MS = 500;
12
16
  export declare const NEUTRAL_MESSAGE_TITLE = "\u26ED C O G C O I N \u26ED";
@@ -7,6 +7,10 @@ export const INTRO_ENTRY_MS = 5_000;
7
7
  export const INTRO_PAUSE_MS = 5_000;
8
8
  export const INTRO_EXIT_MS = 5_000;
9
9
  export const INTRO_TOTAL_MS = INTRO_ENTRY_MS + INTRO_PAUSE_MS + INTRO_EXIT_MS;
10
+ export const COMPLETION_ENTRY_MS = INTRO_ENTRY_MS;
11
+ export const COMPLETION_PAUSE_MS = INTRO_PAUSE_MS;
12
+ export const COMPLETION_EXIT_MS = 0;
13
+ export const COMPLETION_TOTAL_MS = COMPLETION_ENTRY_MS + COMPLETION_PAUSE_MS + COMPLETION_EXIT_MS;
10
14
  export const STATUS_ELLIPSIS_WIDTH = 3;
11
15
  export const STATUS_ELLIPSIS_TICK_MS = 500;
12
16
  export const NEUTRAL_MESSAGE_TITLE = "⛭ C O G C O I N ⛭";
@@ -2,6 +2,7 @@ import type { QuoteDisplayPhase } from "../quotes.js";
2
2
  import type { BootstrapPhase, BootstrapProgress, ManagedBitcoindProgressEvent, ProgressOutputMode, SnapshotMetadata, WritingQuote } from "../types.js";
3
3
  import { type FollowSceneStateForTesting } from "./follow-scene.js";
4
4
  import { type RenderClock, type TtyRenderStream } from "./render-policy.js";
5
+ import { type FollowSceneRenderOptions } from "./tty-renderer.js";
5
6
  interface QuoteRotatorLike {
6
7
  current(now?: number): Promise<{
7
8
  displayPhase: QuoteDisplayPhase;
@@ -12,7 +13,7 @@ interface QuoteRotatorLike {
12
13
  interface ProgressRendererLike {
13
14
  render(displayPhase: QuoteDisplayPhase, quote: WritingQuote | null, progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, introElapsedMs?: number, statusFieldText?: string): void;
14
15
  renderTrainScene(kind: "intro" | "completion", progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, elapsedMs: number, statusFieldText?: string): void;
15
- renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string): void;
16
+ renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string, renderOptions?: FollowSceneRenderOptions): void;
16
17
  close(): void;
17
18
  }
18
19
  interface ProgressControllerOptions {
@@ -1,5 +1,5 @@
1
1
  import { WritingQuoteRotator as QuoteRotator } from "../quotes.js";
2
- import { INTRO_TOTAL_MS } from "./constants.js";
2
+ import { COMPLETION_TOTAL_MS } from "./constants.js";
3
3
  import { advanceFollowSceneState, createFollowSceneState, replaceFollowBlockTimes, setFollowBlockTime, syncFollowSceneState, } from "./follow-scene.js";
4
4
  import { createBootstrapProgress, createDefaultMessage, resolveStatusFieldText, } from "./formatting.js";
5
5
  import { DEFAULT_RENDER_CLOCK, resolveTtyRenderPolicy, TtyRenderThrottle, } from "./render-policy.js";
@@ -105,9 +105,9 @@ export class ManagedProgressController {
105
105
  }
106
106
  const startedAt = this.#clock.now();
107
107
  while (true) {
108
- const elapsedMs = Math.min(INTRO_TOTAL_MS, this.#clock.now() - startedAt);
108
+ const elapsedMs = Math.min(COMPLETION_TOTAL_MS, this.#clock.now() - startedAt);
109
109
  this.#renderer.renderTrainScene("completion", this.#progress, this.#cogcoinSyncHeight, this.#cogcoinSyncTargetHeight, elapsedMs);
110
- if (elapsedMs >= INTRO_TOTAL_MS) {
110
+ if (elapsedMs >= COMPLETION_TOTAL_MS) {
111
111
  break;
112
112
  }
113
113
  await new Promise((resolve) => {
@@ -16,6 +16,10 @@ export interface FollowSceneStateForTesting {
16
16
  pendingStaticX: number | null;
17
17
  animation: FollowAnimation | null;
18
18
  }
19
+ export interface FollowFrameRenderOptions {
20
+ artworkCogText?: string | null;
21
+ artworkSatText?: string | null;
22
+ }
19
23
  export declare function createFollowSceneState(indexedHeight?: number | null, blockTimesByHeight?: Record<number, number>): FollowSceneStateForTesting;
20
24
  export declare function setFollowBlockTime(state: FollowSceneStateForTesting, height: number, blockTime: number): void;
21
25
  export declare function replaceFollowBlockTimes(state: FollowSceneStateForTesting, blockTimesByHeight: Record<number, number>): void;
@@ -27,7 +31,7 @@ export declare function syncFollowSceneState(state: FollowSceneStateForTesting,
27
31
  liveActivated?: boolean;
28
32
  }): void;
29
33
  export declare function advanceFollowSceneState(state: FollowSceneStateForTesting, now: number): void;
30
- export declare function renderFollowFrame(state: FollowSceneStateForTesting, statusFieldText: string, now: number): string[];
34
+ export declare function renderFollowFrame(state: FollowSceneStateForTesting, statusFieldText: string, now: number, options?: FollowFrameRenderOptions): string[];
31
35
  export declare function createFollowSceneStateForTesting(indexedHeight?: number | null, blockTimesByHeight?: Record<number, number>): FollowSceneStateForTesting;
32
36
  export declare function setFollowBlockTimesForTesting(state: FollowSceneStateForTesting, blockTimesByHeight: Record<number, number>): FollowSceneStateForTesting;
33
37
  export declare function setFollowBlockTimeForTesting(state: FollowSceneStateForTesting, height: number, blockTime: number): FollowSceneStateForTesting;
@@ -37,4 +41,4 @@ export declare function syncFollowSceneStateForTesting(state: FollowSceneStateFo
37
41
  liveActivated?: boolean;
38
42
  }): FollowSceneStateForTesting;
39
43
  export declare function advanceFollowSceneStateForTesting(state: FollowSceneStateForTesting, now: number): FollowSceneStateForTesting;
40
- export declare function renderFollowFrameForTesting(state: FollowSceneStateForTesting, statusFieldText?: string, now?: number): string[];
44
+ export declare function renderFollowFrameForTesting(state: FollowSceneStateForTesting, statusFieldText?: string, now?: number, options?: FollowFrameRenderOptions): string[];
@@ -1,6 +1,12 @@
1
1
  import { loadArtTemplate, loadFollowCarTemplate } from "./assets.js";
2
- import { FOLLOW_AGE_ROW, FOLLOW_APPROACH_MS, FOLLOW_CAR_HEIGHT, FOLLOW_CAR_PITCH, FOLLOW_CAR_TOP, FOLLOW_CAR_WIDTH, FOLLOW_CENTER_SLOT_X, FOLLOW_CLIP_MAX_COLUMN, FOLLOW_CLIP_MIN_COLUMN, FOLLOW_CONNECTION_SLOT_X, FOLLOW_FAST_APPROACH_MS, FOLLOW_FAST_SHIFT_MS, FOLLOW_PENDING_ENTER_MS, FOLLOW_PENDING_LABEL, FOLLOW_PENDING_OFFSCREEN_LEFT_X, FOLLOW_PENDING_SLOT_X, FOLLOW_RIGHT_SLOT_XS, FOLLOW_SHIFT_MS, FOLLOW_WINDOW_LEFT, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, STATUS_FIELD_ROW, } from "./constants.js";
3
- import { centerLine, overlayCenteredField } from "./formatting.js";
2
+ import { FIELD_LEFT, FIELD_WIDTH, FOLLOW_AGE_ROW, FOLLOW_APPROACH_MS, FOLLOW_CAR_HEIGHT, FOLLOW_CAR_PITCH, FOLLOW_CAR_TOP, FOLLOW_CAR_WIDTH, FOLLOW_CENTER_SLOT_X, FOLLOW_CLIP_MAX_COLUMN, FOLLOW_CLIP_MIN_COLUMN, FOLLOW_CONNECTION_SLOT_X, FOLLOW_FAST_APPROACH_MS, FOLLOW_FAST_SHIFT_MS, FOLLOW_PENDING_ENTER_MS, FOLLOW_PENDING_LABEL, FOLLOW_PENDING_OFFSCREEN_LEFT_X, FOLLOW_PENDING_SLOT_X, FOLLOW_RIGHT_SLOT_XS, FOLLOW_SHIFT_MS, FOLLOW_WINDOW_LEFT, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, STATUS_FIELD_ROW, } from "./constants.js";
3
+ import { centerLine, computeCenteredLeftPadding, overlayCenteredField, replaceSegment, rightAlignLine, truncateLine, } from "./formatting.js";
4
+ const FOLLOW_TITLE_LEFT = computeCenteredLeftPadding(NEUTRAL_MESSAGE_TITLE, FIELD_WIDTH);
5
+ const FOLLOW_TITLE_WIDTH = NEUTRAL_MESSAGE_TITLE.length;
6
+ const FOLLOW_COG_LEFT = 0;
7
+ const FOLLOW_COG_WIDTH = FOLLOW_TITLE_LEFT;
8
+ const FOLLOW_SAT_LEFT = FOLLOW_TITLE_LEFT + FOLLOW_TITLE_WIDTH;
9
+ const FOLLOW_SAT_WIDTH = FIELD_WIDTH - FOLLOW_SAT_LEFT;
4
10
  export function createFollowSceneState(indexedHeight = null, blockTimesByHeight = {}) {
5
11
  return {
6
12
  liveActivated: false,
@@ -59,6 +65,20 @@ export function formatCompactFollowAgeLabel(blockTime, now) {
59
65
  return `${Math.floor(elapsedHours / 24)}d`;
60
66
  }
61
67
  export const formatCompactFollowAgeLabelForTesting = formatCompactFollowAgeLabel;
68
+ function leftAlignLane(line, width) {
69
+ const aligned = truncateLine(line, width);
70
+ return aligned.padEnd(width, " ");
71
+ }
72
+ function renderFollowHeaderField(options) {
73
+ let field = centerLine(NEUTRAL_MESSAGE_TITLE, FIELD_WIDTH);
74
+ if (options.artworkCogText !== null && options.artworkCogText !== undefined && options.artworkCogText.length > 0) {
75
+ field = replaceSegment(field, FOLLOW_COG_LEFT, FOLLOW_COG_WIDTH, leftAlignLane(options.artworkCogText, FOLLOW_COG_WIDTH));
76
+ }
77
+ if (options.artworkSatText !== null && options.artworkSatText !== undefined && options.artworkSatText.length > 0) {
78
+ field = replaceSegment(field, FOLLOW_SAT_LEFT, FOLLOW_SAT_WIDTH, rightAlignLine(options.artworkSatText, FOLLOW_SAT_WIDTH));
79
+ }
80
+ return field;
81
+ }
62
82
  function highestTrackedFollowHeight(state) {
63
83
  return Math.max(state.indexedHeight ?? Number.NEGATIVE_INFINITY, state.displayedCenterHeight ?? Number.NEGATIVE_INFINITY, state.animation?.height ?? Number.NEGATIVE_INFINITY, ...state.queuedHeights);
64
84
  }
@@ -327,7 +347,7 @@ function resolveFollowCarPlacements(state, now) {
327
347
  }
328
348
  return placements;
329
349
  }
330
- export function renderFollowFrame(state, statusFieldText, now) {
350
+ export function renderFollowFrame(state, statusFieldText, now, options = {}) {
331
351
  let frame = createFollowBaseFrame();
332
352
  const placements = resolveFollowCarPlacements(state, now);
333
353
  for (const car of placements) {
@@ -339,7 +359,10 @@ export function renderFollowFrame(state, statusFieldText, now) {
339
359
  frame = overlayFollowAgeLabel(frame, car, ageLabel);
340
360
  }
341
361
  }
342
- overlayCenteredField(frame, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE);
362
+ const headerRow = frame[MESSAGE_FIELD_ROW];
363
+ if (headerRow !== undefined) {
364
+ frame[MESSAGE_FIELD_ROW] = replaceSegment(headerRow, FIELD_LEFT, FIELD_WIDTH, renderFollowHeaderField(options));
365
+ }
343
366
  overlayCenteredField(frame, STATUS_FIELD_ROW, statusFieldText);
344
367
  return frame;
345
368
  }
@@ -362,6 +385,6 @@ export function advanceFollowSceneStateForTesting(state, now) {
362
385
  advanceFollowSceneState(state, now);
363
386
  return cloneFollowSceneState(state);
364
387
  }
365
- export function renderFollowFrameForTesting(state, statusFieldText = "", now = 0) {
366
- return renderFollowFrame(state, statusFieldText, now);
388
+ export function renderFollowFrameForTesting(state, statusFieldText = "", now = 0, options = {}) {
389
+ return renderFollowFrame(state, statusFieldText, now, options);
367
390
  }
@@ -9,6 +9,7 @@ export declare function renderIndeterminateBar(width: number, now: number): stri
9
9
  export declare function truncateLine(line: string, width: number): string;
10
10
  export declare function normalizeInlineText(value: string): string;
11
11
  export declare function centerLine(line: string, width: number): string;
12
+ export declare function rightAlignLine(line: string, width: number): string;
12
13
  export declare function positionLine(line: string, width: number, leftPadding: number): string;
13
14
  export declare function computeCenteredLeftPadding(line: string, width: number): number;
14
15
  export declare function centerFieldText(text: string): string;
@@ -102,6 +102,11 @@ export function centerLine(line, width) {
102
102
  const leftPadding = Math.max(0, Math.floor((width - centered.length) / 2));
103
103
  return `${" ".repeat(leftPadding)}${centered}`.padEnd(width, " ");
104
104
  }
105
+ export function rightAlignLine(line, width) {
106
+ const aligned = truncateLine(line, width);
107
+ const leftPadding = Math.max(0, width - aligned.length);
108
+ return `${" ".repeat(leftPadding)}${aligned}`.padEnd(width, " ");
109
+ }
105
110
  export function positionLine(line, width, leftPadding) {
106
111
  const positioned = truncateLine(line, width);
107
112
  const safePadding = Math.max(0, Math.min(leftPadding, Math.max(0, width - positioned.length)));
@@ -130,6 +135,7 @@ export function resolveStatusFieldText(progress, snapshotHeight, now = 0) {
130
135
  case "getblock_archive_import":
131
136
  return `Importing getblock range${animateStatusEllipsis(now)}`;
132
137
  case "paused":
138
+ return `Waiting to start managed sync${animateStatusEllipsis(now)}`;
133
139
  case "snapshot_download":
134
140
  return `Downloading snapshot to ${snapshotHeight}${animateStatusEllipsis(now)}`;
135
141
  case "wait_headers_for_snapshot":
@@ -1,18 +1,34 @@
1
- import { INTRO_ENTRY_MS, INTRO_EXIT_MS, INTRO_PAUSE_MS, INTRO_TOTAL_MS, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, SCROLL_WINDOW_LEFT, STATUS_FIELD_ROW, TRAIN_CENTER_X, TRAIN_CLIP_MAX_COLUMN, TRAIN_CLIP_MIN_COLUMN, TRAIN_OFFSCREEN_LEFT_X, TRAIN_OFFSCREEN_RIGHT_X, TRAIN_SPRITE_TOP, } from "./constants.js";
1
+ import { COMPLETION_ENTRY_MS, COMPLETION_EXIT_MS, COMPLETION_PAUSE_MS, COMPLETION_TOTAL_MS, INTRO_ENTRY_MS, INTRO_EXIT_MS, INTRO_PAUSE_MS, INTRO_TOTAL_MS, MESSAGE_FIELD_ROW, NEUTRAL_MESSAGE_TITLE, SCROLL_WINDOW_LEFT, STATUS_FIELD_ROW, TRAIN_CENTER_X, TRAIN_CLIP_MAX_COLUMN, TRAIN_CLIP_MIN_COLUMN, TRAIN_OFFSCREEN_LEFT_X, TRAIN_OFFSCREEN_RIGHT_X, TRAIN_SPRITE_TOP, } from "./constants.js";
2
2
  import { loadArtTemplate, loadSprite } from "./assets.js";
3
3
  import { overlayCenteredField } from "./formatting.js";
4
+ function resolveTrainSceneTimings(kind) {
5
+ return kind === "intro"
6
+ ? {
7
+ entryMs: INTRO_ENTRY_MS,
8
+ pauseMs: INTRO_PAUSE_MS,
9
+ exitMs: INTRO_EXIT_MS,
10
+ totalMs: INTRO_TOTAL_MS,
11
+ }
12
+ : {
13
+ entryMs: COMPLETION_ENTRY_MS,
14
+ pauseMs: COMPLETION_PAUSE_MS,
15
+ exitMs: COMPLETION_EXIT_MS,
16
+ totalMs: COMPLETION_TOTAL_MS,
17
+ };
18
+ }
4
19
  export function resolveTrainSceneMessage(kind, elapsedMs) {
5
- if (elapsedMs < INTRO_ENTRY_MS) {
20
+ const timings = resolveTrainSceneTimings(kind);
21
+ if (elapsedMs < timings.entryMs) {
6
22
  return kind === "intro"
7
23
  ? "Here comes the mining train!"
8
24
  : "Congratuations, you are synced!";
9
25
  }
10
- if (elapsedMs < INTRO_ENTRY_MS + INTRO_PAUSE_MS) {
26
+ if (elapsedMs < timings.entryMs + timings.pauseMs || kind === "completion") {
11
27
  return kind === "intro"
12
28
  ? "Welcome to Cogcoin!"
13
29
  : "You shape your own future.";
14
30
  }
15
- if (elapsedMs < INTRO_TOTAL_MS) {
31
+ if (elapsedMs < timings.totalMs) {
16
32
  return kind === "intro"
17
33
  ? "How many sentences will you mine?"
18
34
  : "Your Cogcoin story begins...";
@@ -25,25 +41,27 @@ export function resolveIntroMessageForTesting(introElapsedMs) {
25
41
  export function resolveCompletionMessageForTesting(completionElapsedMs) {
26
42
  return resolveTrainSceneMessage("completion", completionElapsedMs);
27
43
  }
28
- function resolveIntroSpriteName(introElapsedMs) {
29
- if (introElapsedMs >= INTRO_ENTRY_MS && introElapsedMs < INTRO_ENTRY_MS + INTRO_PAUSE_MS) {
44
+ function resolveTrainSpriteName(kind, elapsedMs) {
45
+ const timings = resolveTrainSceneTimings(kind);
46
+ if (elapsedMs >= timings.entryMs && (kind === "completion" || elapsedMs < timings.entryMs + timings.pauseMs)) {
30
47
  return "train";
31
48
  }
32
49
  return "train-smoke";
33
50
  }
34
- function resolveIntroSpriteX(introElapsedMs) {
35
- if (introElapsedMs <= 0) {
51
+ function resolveTrainSpriteX(kind, elapsedMs) {
52
+ const timings = resolveTrainSceneTimings(kind);
53
+ if (elapsedMs <= 0) {
36
54
  return TRAIN_OFFSCREEN_RIGHT_X;
37
55
  }
38
- if (introElapsedMs < INTRO_ENTRY_MS) {
39
- const progress = introElapsedMs / INTRO_ENTRY_MS;
56
+ if (elapsedMs < timings.entryMs) {
57
+ const progress = elapsedMs / timings.entryMs;
40
58
  return Math.round(TRAIN_OFFSCREEN_RIGHT_X + ((TRAIN_CENTER_X - TRAIN_OFFSCREEN_RIGHT_X) * progress));
41
59
  }
42
- if (introElapsedMs < INTRO_ENTRY_MS + INTRO_PAUSE_MS) {
60
+ if (kind === "completion" || elapsedMs < timings.entryMs + timings.pauseMs) {
43
61
  return TRAIN_CENTER_X;
44
62
  }
45
- if (introElapsedMs < INTRO_TOTAL_MS) {
46
- const progress = (introElapsedMs - INTRO_ENTRY_MS - INTRO_PAUSE_MS) / INTRO_EXIT_MS;
63
+ if (elapsedMs < timings.totalMs) {
64
+ const progress = (elapsedMs - timings.entryMs - timings.pauseMs) / timings.exitMs;
47
65
  return Math.round(TRAIN_CENTER_X + ((TRAIN_OFFSCREEN_LEFT_X - TRAIN_CENTER_X) * progress));
48
66
  }
49
67
  return TRAIN_OFFSCREEN_LEFT_X;
@@ -70,13 +88,14 @@ function overlaySpriteOnFrame(frame, sprite, spriteX) {
70
88
  }
71
89
  export function renderTrainSceneFrame(kind, elapsedMs, statusFieldText) {
72
90
  const frame = [...loadArtTemplate("scroll")];
73
- const introFrame = elapsedMs >= INTRO_TOTAL_MS
91
+ const timings = resolveTrainSceneTimings(kind);
92
+ const renderedFrame = kind === "intro" && elapsedMs >= timings.totalMs
74
93
  ? frame
75
- : overlaySpriteOnFrame(frame, loadSprite(resolveIntroSpriteName(elapsedMs)), resolveIntroSpriteX(elapsedMs));
94
+ : overlaySpriteOnFrame(frame, loadSprite(resolveTrainSpriteName(kind, elapsedMs)), resolveTrainSpriteX(kind, elapsedMs));
76
95
  const message = resolveTrainSceneMessage(kind, elapsedMs);
77
- overlayCenteredField(introFrame, MESSAGE_FIELD_ROW, message.length > 0 ? message : NEUTRAL_MESSAGE_TITLE);
78
- overlayCenteredField(introFrame, STATUS_FIELD_ROW, statusFieldText);
79
- return introFrame;
96
+ overlayCenteredField(renderedFrame, MESSAGE_FIELD_ROW, message.length > 0 ? message : NEUTRAL_MESSAGE_TITLE);
97
+ overlayCenteredField(renderedFrame, STATUS_FIELD_ROW, statusFieldText);
98
+ return renderedFrame;
80
99
  }
81
100
  export function renderIntroFrame(introElapsedMs, statusFieldText) {
82
101
  return renderTrainSceneFrame("intro", introElapsedMs, statusFieldText);
@@ -7,12 +7,17 @@ interface RenderStream {
7
7
  columns?: number;
8
8
  write(chunk: string): boolean | void;
9
9
  }
10
+ export interface FollowSceneRenderOptions {
11
+ artworkCogText?: string | null;
12
+ artworkSatText?: string | null;
13
+ extraLines?: string[];
14
+ }
10
15
  export declare class TtyProgressRenderer {
11
16
  #private;
12
17
  constructor(stream?: RenderStream);
13
18
  render(displayPhase: QuoteDisplayPhase, quote: WritingQuote | null, progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, introElapsedMs?: number, statusFieldText?: string): void;
14
19
  renderTrainScene(kind: TrainSceneKind, progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, elapsedMs: number, statusFieldText?: string): void;
15
- renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string): void;
20
+ renderFollowScene(progress: BootstrapProgress, cogcoinSyncHeight: number | null, cogcoinSyncTargetHeight: number | null, followScene: FollowSceneStateForTesting, statusFieldText?: string, renderOptions?: FollowSceneRenderOptions): void;
16
21
  close(): void;
17
22
  }
18
23
  export {};
@@ -1,4 +1,4 @@
1
- import { ART_WIDTH, INTRO_TOTAL_MS, NEUTRAL_MESSAGE_TITLE } from "./constants.js";
1
+ import { ART_WIDTH, NEUTRAL_MESSAGE_TITLE } from "./constants.js";
2
2
  import { renderFollowFrame } from "./follow-scene.js";
3
3
  import { formatProgressLine, formatQuoteLine, truncateLine } from "./formatting.js";
4
4
  import { renderArtFrame } from "./quote-scene.js";
@@ -85,13 +85,17 @@ export class TtyProgressRenderer {
85
85
  this.#writeChunk(frame);
86
86
  this.#previousFrameHeight = lines.length;
87
87
  }
88
- renderFollowScene(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, followScene, statusFieldText = "") {
88
+ renderFollowScene(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, followScene, statusFieldText = "", renderOptions = {}) {
89
89
  const now = Date.now();
90
90
  const width = Math.max(20, this.#stream.columns ?? 120);
91
91
  const progressLine = formatProgressLine(progress, cogcoinSyncHeight, cogcoinSyncTargetHeight, width, now);
92
+ const extraLines = (renderOptions.extraLines ?? []).map((line) => truncateLine(line, width));
92
93
  const lines = width >= ART_WIDTH
93
- ? [...renderFollowFrame(followScene, statusFieldText, now), "", progressLine, ""]
94
- : [truncateLine(NEUTRAL_MESSAGE_TITLE, width), progressLine, ""];
94
+ ? [...renderFollowFrame(followScene, statusFieldText, now, {
95
+ artworkCogText: renderOptions.artworkCogText ?? null,
96
+ artworkSatText: renderOptions.artworkSatText ?? null,
97
+ }), "", progressLine, "", ...extraLines]
98
+ : [truncateLine(NEUTRAL_MESSAGE_TITLE, width), progressLine, "", ...extraLines];
95
99
  const frame = lines.join("\n");
96
100
  this.#resetFrameIfExternalWritesDetected();
97
101
  if (!this.#rendered) {
@@ -1,4 +1,4 @@
1
- import type { RpcBlock, RpcBlockchainInfo, RpcChainStatesResponse, RpcCreateWalletResult, RpcDecodedPsbt, RpcDescriptorInfo, RpcFinalizePsbtResult, RpcImportDescriptorRequest, RpcImportDescriptorResult, RpcListUnspentEntry, RpcMempoolEntry, RpcMempoolInfo, RpcRawMempoolVerbose, RpcListDescriptorsResult, RpcLockedUnspent, RpcLoadTxOutSetResult, RpcLoadWalletResult, RpcNetworkInfo, RpcTestMempoolAcceptResult, RpcWalletInfo, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult, RpcTransaction, RpcWalletTransaction, RpcZmqNotification } from "./types.js";
1
+ import type { RpcBlock, RpcBlockchainInfo, RpcChainStatesResponse, RpcCreateWalletResult, RpcDecodedPsbt, RpcDescriptorInfo, RpcEstimateSmartFeeResult, RpcFinalizePsbtResult, RpcImportDescriptorRequest, RpcImportDescriptorResult, RpcListUnspentEntry, RpcMempoolEntry, RpcMempoolInfo, RpcRawMempoolVerbose, RpcListDescriptorsResult, RpcLockedUnspent, RpcLoadTxOutSetResult, RpcLoadWalletResult, RpcNetworkInfo, RpcTestMempoolAcceptResult, RpcWalletInfo, RpcWalletCreateFundedPsbtResult, RpcWalletProcessPsbtResult, RpcTransaction, RpcWalletTransaction, RpcZmqNotification } from "./types.js";
2
2
  interface RpcRequestPayload {
3
3
  readonly body: string;
4
4
  readonly headers: Record<string, string>;
@@ -63,6 +63,7 @@ export declare class BitcoinRpcClient {
63
63
  getRawMempoolVerbose(): Promise<RpcRawMempoolVerbose>;
64
64
  getMempoolInfo(): Promise<RpcMempoolInfo>;
65
65
  getMempoolEntry(txid: string): Promise<RpcMempoolEntry>;
66
+ estimateSmartFee(confirmTarget: number, mode: "conservative" | "economical"): Promise<RpcEstimateSmartFeeResult>;
66
67
  saveMempool(): Promise<null>;
67
68
  getRawTransaction(txid: string, verbose?: boolean): Promise<RpcTransaction>;
68
69
  getTransaction(walletName: string, txid: string): Promise<RpcWalletTransaction>;
@@ -307,6 +307,9 @@ export class BitcoinRpcClient {
307
307
  getMempoolEntry(txid) {
308
308
  return this.call("getmempoolentry", [txid]);
309
309
  }
310
+ estimateSmartFee(confirmTarget, mode) {
311
+ return this.call("estimatesmartfee", [confirmTarget, mode]);
312
+ }
310
313
  saveMempool() {
311
314
  return this.call("savemempool");
312
315
  }