@did-btcr2/cli 0.25.0 → 0.25.2

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/src/config.ts CHANGED
@@ -74,7 +74,7 @@ export type ConnectionOverrides = {
74
74
  * },
75
75
  * "bitcoin": {
76
76
  * "btc": { "rest": "https://my-mempool/api" },
77
- * "cas": { "gateway": "https://ipfs.io", "rpcUrl": "http://127.0.0.1:5001" }
77
+ * "cas": { "gateway": "https://trustless-gateway.link", "rpcUrl": "http://127.0.0.1:5001" }
78
78
  * }
79
79
  * }
80
80
  * }
@@ -958,7 +958,7 @@ export function resolveEffectiveConfig(network: NetworkOption, overrides?: Conne
958
958
  };
959
959
  }
960
960
 
961
- /** One endpoint reachability check produced by `config doctor`. */
961
+ /** One endpoint check produced by `config doctor`. */
962
962
  export interface DoctorCheck {
963
963
  endpoint : 'btc-rest' | 'btc-rpc' | 'cas';
964
964
  target : string;
@@ -966,81 +966,82 @@ export interface DoctorCheck {
966
966
  detail? : string;
967
967
  }
968
968
 
969
- /** Result of `config doctor`: per-endpoint reachability and any coherence warning. */
969
+ /** Result of `config doctor`: one check per endpoint, and any coherence warning. */
970
970
  export interface DoctorReport {
971
971
  checks : DoctorCheck[];
972
972
  coherence? : { profile: string; declared: NetworkOption; encoding: NetworkOption };
973
973
  }
974
974
 
975
- /** Default per-probe timeout (ms) for `config doctor`. */
975
+ /** The timeout (ms) of each `config doctor` check. */
976
976
  const DOCTOR_PROBE_TIMEOUT_MS = 5000;
977
977
 
978
- /** Fetches a URL with a bounded timeout, reporting reachability rather than throwing. */
979
- async function probeEndpoint(
980
- endpoint : DoctorCheck['endpoint'],
981
- target : string,
982
- url : string,
983
- opts? : { method?: 'GET' | 'POST'; headers?: Record<string, string> },
984
- ): Promise<DoctorCheck> {
978
+ /**
979
+ * A block that only the chain of each network has: the genesis block, or block 1 on
980
+ * signet and mutinynet, because all signets share one genesis block. A `config doctor`
981
+ * check reads the hash at this height, to prove that an endpoint serves the chain of
982
+ * the network.
983
+ */
984
+ const CHAIN_MARKER: Record<NetworkOption, { height: number; hash: string }> = {
985
+ bitcoin : { height: 0, hash: '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f' },
986
+ testnet3 : { height: 0, hash: '000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943' },
987
+ testnet4 : { height: 0, hash: '00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043' },
988
+ signet : { height: 1, hash: '00000086d6b2636cb2a392d45edc4ec544a10024d30141c9adf4bfd9de533b53' },
989
+ mutinynet : { height: 1, hash: '000002855893a0a9b24eaffc5efc770558a326fee4fc10c9da22fc19cd2954f9' },
990
+ regtest : { height: 0, hash: '0f9188f13cb7b2c71f2a335e3a4fc328bf5beb436012afca590b1a11466e2206' },
991
+ };
992
+
993
+ /** Runs one check. A rejection gives a failed check with the error message as the detail. */
994
+ async function runCheck(endpoint: DoctorCheck['endpoint'], target: string, check: () => Promise<void>): Promise<DoctorCheck> {
985
995
  try {
986
- const res = await fetch(url, {
987
- method : opts?.method ?? 'GET',
988
- headers : opts?.headers,
989
- signal : AbortSignal.timeout(DOCTOR_PROBE_TIMEOUT_MS),
990
- });
991
- return res.ok
992
- ? { endpoint, target, ok: true }
993
- : { endpoint, target, ok: false, detail: `HTTP ${res.status}` };
996
+ await check();
997
+ return { endpoint, target, ok: true };
994
998
  } catch (error) {
995
999
  return { endpoint, target, ok: false, detail: (error as Error).message };
996
1000
  }
997
1001
  }
998
1002
 
999
- /** Races a promise against a timeout so a stalled RPC call cannot hang `doctor`. */
1000
- function withProbeTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
1001
- let timer: ReturnType<typeof setTimeout>;
1002
- const timeout = new Promise<never>((_, reject) => {
1003
- timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
1004
- });
1005
- return Promise.race([ promise, timeout ]).finally(() => clearTimeout(timer));
1003
+ /** Throws if a block hash is not the hash of the chain marker of the network. */
1004
+ function assertChainMarker(network: NetworkOption, hash: unknown): void {
1005
+ const marker = CHAIN_MARKER[network];
1006
+ if (hash === marker.hash) return;
1007
+ const got = (typeof hash === 'string' ? hash : JSON.stringify(hash) ?? String(hash)).slice(0, 80);
1008
+ throw new Error(`block ${marker.height} is ${got}, not the ${network} block ${marker.hash}: the endpoint serves another chain`);
1006
1009
  }
1007
1010
 
1008
1011
  /**
1009
- * Probes reachability of the resolved endpoints for `config doctor`: a
1010
- * lightweight REST call against btc-rest, a `getblockchaininfo` against btc-rpc
1011
- * when configured, and a reachability check against the resolved CAS. Also
1012
- * surfaces the profile/network coherence warning. Reads and touches the network;
1013
- * never writes.
1012
+ * Checks the resolved endpoints of one network for `config doctor`. Each check uses
1013
+ * the api client of the commands (with a 5-second abort), so it tests the request
1014
+ * path of the commands, not only a connection:
1015
+ * - `btc-rest` and `btc-rpc`: read the hash of the {@link CHAIN_MARKER} block. A pass
1016
+ * proves an Esplora or a Bitcoin Core endpoint for the chain of the network.
1017
+ * - `cas`: `CasApi.probe` reads a fixed identity block through the resolved backend
1018
+ * (the RPC endpoint if one is set, else the gateway) and compares the bytes.
1019
+ * Also surfaces the profile/network coherence warning. Reads the network; never writes.
1014
1020
  */
1015
1021
  export async function runDoctor(network: NetworkOption, overrides?: ConnectionOverrides): Promise<DoctorReport> {
1016
- const api = defaultApiFactory(network, overrides);
1022
+ const conn = resolveConnectionConfig(network, overrides);
1023
+ const api = createApi({
1024
+ btc : { ...conn.btc, network, timeoutMs: DOCTOR_PROBE_TIMEOUT_MS },
1025
+ cas : { ...(conn.cas ?? { gateway: DEFAULT_CAS_GATEWAY }), timeoutMs: DOCTOR_PROBE_TIMEOUT_MS },
1026
+ });
1027
+ const height = CHAIN_MARKER[network].height;
1017
1028
  const checks: DoctorCheck[] = [];
1018
1029
 
1019
- const restHost = api.btc.connection.rest.config.host.replace(/\/+$/, '');
1020
- checks.push(await probeEndpoint('btc-rest', restHost, `${restHost}/blocks/tip/height`, { headers: api.btc.connection.rest.config.headers }));
1030
+ const rest = api.btc.connection.rest;
1031
+ checks.push(await runCheck('btc-rest', rest.config.host.replace(/\/+$/, ''), async () => {
1032
+ assertChainMarker(network, await rest.block.getHash(height));
1033
+ }));
1021
1034
 
1022
1035
  const rpc = api.btc.connection.rpc;
1023
1036
  if (rpc) {
1024
- const target = rpc.config.host ?? '(default rpc)';
1025
- try {
1026
- await withProbeTimeout(rpc.getBlockchainInfo(), DOCTOR_PROBE_TIMEOUT_MS);
1027
- checks.push({ endpoint: 'btc-rpc', target, ok: true });
1028
- } catch (error) {
1029
- checks.push({ endpoint: 'btc-rpc', target, ok: false, detail: (error as Error).message });
1030
- }
1037
+ checks.push(await runCheck('btc-rpc', rpc.config.host ?? '(default rpc)', async () => {
1038
+ assertChainMarker(network, await rpc.getBlockHash(height));
1039
+ }));
1031
1040
  }
1032
1041
 
1033
- // A writable IPFS RPC (Kubo) answers only POST, so a bare GET would falsely
1034
- // report a healthy node as down; probe its version endpoint with POST. A
1035
- // read-only gateway answers a plain GET on its base URL.
1036
- const conn = resolveConnectionConfig(network, overrides);
1037
- if (conn.cas?.rpcUrl) {
1038
- const base = conn.cas.rpcUrl.replace(/\/+$/, '');
1039
- checks.push(await probeEndpoint('cas', conn.cas.rpcUrl, `${base}/api/v0/version`, { method: 'POST' }));
1040
- } else {
1041
- const gateway = (conn.cas?.gateway ?? DEFAULT_CAS_GATEWAY).replace(/\/+$/, '');
1042
- checks.push(await probeEndpoint('cas', gateway, gateway));
1043
- }
1042
+ // The api selects the RPC endpoint over the gateway, so the check names that target.
1043
+ const casTarget = (conn.cas?.rpcUrl ?? conn.cas?.gateway ?? DEFAULT_CAS_GATEWAY).replace(/\/+$/, '');
1044
+ checks.push(await runCheck('cas', casTarget, () => api.cas.probe()));
1044
1045
 
1045
1046
  const mismatch = profileNetworkMismatch(network, overrides);
1046
1047
  return {