import type { LoggingLevelArg } from "@lightsparkdev/core"; import { type MayHaveSspClientOptions, type SspClientOptions, } from "../graphql/client.js"; import type { SparkWalletEvents } from "../spark-wallet/types.js"; import { isBare } from "@lightsparkdev/core"; import { isHermeticTest } from "../tests/isHermeticTest.js"; import { type NetworkType } from "../utils/network.js"; const SSP_IDENTITY_PUBLIC_KEYS = { LOCAL: "028c094a432d46a0ac95349d792c2e3730bd60c29188db716f56a99e39b95338b4", REGTEST: { PROD: "022bf283544b16c0622daecb79422007d167eca6ce9f0c98c0c49833b1f7170bfe", }, MAINNET: { PROD: "023e33e2920326f64ea31058d44777442d97d7d5cbfcf54e3060bc1695e5261c93", }, }; const URL_CONFIG = { LOCAL: { SSP: "http://127.0.0.1:5000", ELECTRS: "http://127.0.0.1:30000", }, REGTEST: { PROD: { SSP: "https://api.lightspark.com", ELECTRS: "https://regtest-mempool.us-west-2.sparkinfra.net/api", }, }, MAINNET: { PROD: { SSP: "https://api.lightspark.com", ELECTRS: "https://mempool.space/api", }, }, } as const; export const ELECTRS_CREDENTIALS = { username: "spark-sdk", password: "mCMk1JqlBNtetUNy", }; export function getElectrsUrl(network: NetworkType): string { switch (network) { case "LOCAL": return isHermeticTest ? "http://mempool.minikube.local/api" : URL_CONFIG.LOCAL.ELECTRS; case "REGTEST": return URL_CONFIG.REGTEST.PROD.ELECTRS; case "MAINNET": return URL_CONFIG.MAINNET.PROD.ELECTRS; default: return URL_CONFIG.LOCAL.ELECTRS; } } export function getSspIdentityPublicKey(network: NetworkType): string { switch (network) { case "LOCAL": return SSP_IDENTITY_PUBLIC_KEYS.LOCAL; case "REGTEST": return SSP_IDENTITY_PUBLIC_KEYS.REGTEST.PROD; case "MAINNET": return SSP_IDENTITY_PUBLIC_KEYS.MAINNET.PROD; default: return SSP_IDENTITY_PUBLIC_KEYS.LOCAL; } } export function getSspUrl(network: NetworkType): string { switch (network) { case "LOCAL": return isHermeticTest ? "http://app.minikube.local" : URL_CONFIG.LOCAL.SSP; case "REGTEST": return URL_CONFIG.REGTEST.PROD.SSP; case "MAINNET": return URL_CONFIG.MAINNET.PROD.SSP; default: return URL_CONFIG.LOCAL.SSP; } } export function getSspSchemaEndpoint(network: NetworkType): string | undefined { switch (network) { case "LOCAL": return "graphql/spark/rc"; } return; } export type SigningOperator = { readonly id: number; readonly identifier: string; readonly address: string; readonly identityPublicKey: string; }; export type MethodLoggingConfig = { enabled: boolean; collapseConsecutive: boolean; excludedMethods: readonly string[]; exitOnly: boolean; }; export type MethodLoggingOptions = boolean | Partial; export type ServiceLogOptions = | boolean | { enabled?: boolean; level?: LoggingLevelArg; methods?: MethodLoggingOptions; }; export type ServiceLoggingConfig = { enabled: boolean; level: LoggingLevelArg; methods: MethodLoggingConfig; }; export const LOG_SERVICE_NAMES = [ "sparkWallet", "sparkReadonlyClient", "connectionManager", "serverTimeSync", "sspClient", "sparkAuthProvider", "signingService", "transferService", "lightningService", "depositService", "tokenTransactionService", "tokenOutputManager", "coopExitService", "swapService", "leafManager", "bareHttpTransport", "xhrTransport", ] as const; export type LogServiceName = (typeof LOG_SERVICE_NAMES)[number]; export type LogConfig = { level: LoggingLevelArg; timestamps: boolean; console: boolean; file?: string; services: Record; }; export type LogServicesOptions = | "all" | Partial>; export type LogOptionsObject = { level?: LoggingLevelArg; timestamps?: boolean; console?: boolean; file?: string; services?: LogServicesOptions; }; export type LogOptions = boolean | LoggingLevelArg | LogOptionsObject; export type OptimizationOptions = { readonly auto?: boolean; readonly multiplicity?: number; }; export type TokenOptimizationOptions = { readonly enabled?: boolean; readonly intervalMs?: number; readonly minOutputsThreshold?: number; }; export type TlsOptions = { /** Only for local development with self-signed certificates. */ readonly dangerouslyDisableCertificateVerification?: boolean; }; export type ResolvedTlsOptions = TlsOptions & { readonly rootCaPath?: string; }; export type ConfigOptions = MayHaveSspClientOptions & { readonly log?: LogOptions; readonly network?: NetworkType; readonly signingOperators?: Readonly>; readonly tls?: TlsOptions; readonly coordinatorIdentifier?: string; readonly frostSignerAddress?: string; readonly threshold?: number; readonly tokenSignatures?: "ECDSA" | "SCHNORR"; readonly tokenValidityDurationSeconds?: number; readonly electrsUrl?: string; readonly sspClientOptions?: SspClientOptions; readonly expectedWithdrawBondSats?: number; readonly expectedWithdrawRelativeBlockLocktime?: number; readonly signerWithPreExistingKeys?: boolean; readonly events?: Partial; readonly optimizationOptions?: OptimizationOptions; readonly tokenOptimizationOptions?: TokenOptimizationOptions; readonly tokenOutputLockExpiryMs?: number; readonly tokenTransactionVersion?: "V2" | "V3"; readonly useTokenPrimitivesBindings?: boolean; }; const VALID_NETWORK_TYPES = new Set([ "LOCAL", "MAINNET", "REGTEST", "SIGNET", "TESTNET", ]); const PROD_PUBKEYS = [ "03dfbdff4b6332c220f8fa2ba8ed496c698ceada563fa01b67d9983bfc5c95e763", "03e625e9768651c9be268e287245cc33f96a68ce9141b0b4769205db027ee8ed77", "022eda13465a59205413086130a65dc0ed1b8f8e51937043161f8be0c369b1a410", ]; const LOCAL_OPERATOR_PUBLIC_KEYS = [ "0322ca18fc489ae25418a0e768273c2c61cabb823edfb14feb891e9bec62016510", "0341727a6c41b168f07eb50865ab8c397a53c7eef628ac1020956b705e43b6cb27", "0305ab8d485cc752394de4981f8a5ae004f2becfea6f432c9a59d5022d8764f0a6", "0352aef4d49439dedd798ac4aef1e7ebef95f569545b647a25338398c1247ffdea", "02c05c88cc8fc181b1ba30006df6a4b0597de6490e24514fbdd0266d2b9cd3d0ba", ] as const; const MINIKUBE_ROOT_CA_PATH = "/tmp/minikube-ca.pem"; function getLocalFrostSignerAddress(): string { return isHermeticTest ? "localhost:9999" : "unix:///tmp/frost_0.sock"; } export function getTlsOptionsFromEnv( network?: NetworkType, ): ResolvedTlsOptions { if (typeof process === "undefined") { return {}; } const rootCaPath = network === "LOCAL" && process.env.SPARK_LOCAL_INGRESS_HOST ? MINIKUBE_ROOT_CA_PATH : undefined; const dangerouslyDisableCertificateVerification = process.env.SPARK_DANGEROUSLY_DISABLE_TLS_VERIFICATION; return { rootCaPath, dangerouslyDisableCertificateVerification: dangerouslyDisableCertificateVerification == null ? undefined : ["1", "true", "yes"].includes( dangerouslyDisableCertificateVerification.toLowerCase(), ), }; } const BASE_CONFIG: Required = { log: false, network: "LOCAL", coordinatorIdentifier: "0000000000000000000000000000000000000000000000000000000000000001", frostSignerAddress: getLocalFrostSignerAddress(), threshold: 2, signingOperators: getLocalSigningOperators(), tls: {}, tokenSignatures: "SCHNORR", tokenValidityDurationSeconds: 180, electrsUrl: getElectrsUrl("LOCAL"), expectedWithdrawBondSats: 10000, expectedWithdrawRelativeBlockLocktime: 1000, sspClientOptions: { baseUrl: getSspUrl("LOCAL"), identityPublicKey: getSspIdentityPublicKey("LOCAL"), schemaEndpoint: getSspSchemaEndpoint("LOCAL"), }, signerWithPreExistingKeys: false, events: {}, optimizationOptions: { auto: true, multiplicity: 1, }, tokenOptimizationOptions: { enabled: true, intervalMs: 300000, // 5 minutes minOutputsThreshold: 50, }, tokenOutputLockExpiryMs: 20000, // 20 seconds tokenTransactionVersion: "V3", useTokenPrimitivesBindings: true, }; const LOCAL_WALLET_CONFIG: Required = { ...BASE_CONFIG, threshold: getLocalSigningThreshold(), }; const LOCAL_WALLET_CONFIG_SCHNORR: Required = { ...LOCAL_WALLET_CONFIG, threshold: getLocalSigningThreshold(), // 2 for issuance tests. }; const LOCAL_WALLET_CONFIG_ECDSA: Required = { ...LOCAL_WALLET_CONFIG, tokenSignatures: "ECDSA", threshold: getLocalSigningThreshold(), // 2 for issuance tests. }; const REGTEST_WALLET_CONFIG: Required = { ...BASE_CONFIG, network: "REGTEST", signingOperators: getSigningOperators(), electrsUrl: getElectrsUrl("REGTEST"), expectedWithdrawBondSats: 10000, expectedWithdrawRelativeBlockLocktime: 1000, sspClientOptions: { baseUrl: getSspUrl("REGTEST"), identityPublicKey: getSspIdentityPublicKey("REGTEST"), }, }; const MAINNET_WALLET_CONFIG: Required = { ...BASE_CONFIG, network: "MAINNET", signingOperators: getSigningOperators(), electrsUrl: getElectrsUrl("MAINNET"), expectedWithdrawBondSats: 10000, expectedWithdrawRelativeBlockLocktime: 1000, sspClientOptions: { baseUrl: getSspUrl("MAINNET"), identityPublicKey: getSspIdentityPublicKey("MAINNET"), }, }; export const WalletConfig = { LOCAL: LOCAL_WALLET_CONFIG, LOCAL_SCHNORR: LOCAL_WALLET_CONFIG_SCHNORR, LOCAL_ECDSA: LOCAL_WALLET_CONFIG_ECDSA, REGTEST: REGTEST_WALLET_CONFIG, MAINNET: MAINNET_WALLET_CONFIG, }; export function createLocalSigningOperators( addresses: readonly string[], ): Record { if (addresses.length > LOCAL_OPERATOR_PUBLIC_KEYS.length) { throw new Error( `Expected at most ${LOCAL_OPERATOR_PUBLIC_KEYS.length} local signing operators, got ${addresses.length}`, ); } const operators: Record = {}; for (let i = 0; i < addresses.length; i++) { const identifier = `000000000000000000000000000000000000000000000000000000000000000${i + 1}`; operators[identifier] = { id: i, identifier, address: addresses[i]!, identityPublicKey: LOCAL_OPERATOR_PUBLIC_KEYS[i]!, }; } return operators; } function getSigningOperators(): Record { return { "0000000000000000000000000000000000000000000000000000000000000001": { id: 0, identifier: "0000000000000000000000000000000000000000000000000000000000000001", address: "https://0.spark.lightspark.com", identityPublicKey: PROD_PUBKEYS[0]!, }, "0000000000000000000000000000000000000000000000000000000000000002": { id: 1, identifier: "0000000000000000000000000000000000000000000000000000000000000002", address: "https://spark-operator.breez.technology", identityPublicKey: PROD_PUBKEYS[1]!, }, "0000000000000000000000000000000000000000000000000000000000000003": { id: 2, identifier: "0000000000000000000000000000000000000000000000000000000000000003", address: "https://2.spark.flashnet.xyz", identityPublicKey: PROD_PUBKEYS[2]!, }, }; } export function getLocalSigningOperators(): Record { const numOperators = typeof process !== "undefined" && process.env.NUM_SPARK_OPERATORS !== undefined ? parseInt(process.env.NUM_SPARK_OPERATORS, 10) : 3; const hermeticDomain = isBare ? "spark-web.minikube.local" : "spark.minikube.local"; const addresses = Array.from({ length: numOperators }, (_, i) => isHermeticTest ? `https://${i}.${hermeticDomain}` : `https://localhost:${8535 + i}`, ); return createLocalSigningOperators(addresses); } export function getLocalSigningThreshold( signingOperators: Readonly< Record > = getLocalSigningOperators(), ): number { const numOperators = Object.keys(signingOperators).length; return Math.floor((numOperators + 2) / 2); } export function normalizeNetworkType( value: string | undefined, defaultNetwork: NetworkType = "REGTEST", ): NetworkType { const normalizedValue = value?.toUpperCase(); return VALID_NETWORK_TYPES.has(normalizedValue as NetworkType) ? (normalizedValue as NetworkType) : defaultNetwork; } export function mergeConfigOptionsForNetwork( network: NetworkType, configOverride?: ConfigOptions, ): ConfigOptions { if (!configOverride) { return { network }; } if (configOverride.network && configOverride.network !== network) { throw new Error( `Config file network ${configOverride.network} does not match ${network}`, ); } return { ...configOverride, network, }; } export function rewriteSigningOperatorAddresses( signingOperators: Readonly>, rewriteAddress: (signingOperator: SigningOperator) => string, ): Record { return Object.fromEntries( Object.values(signingOperators) .sort((left, right) => left.id - right.id) .map((signingOperator) => [ signingOperator.identifier, { ...signingOperator, address: rewriteAddress(signingOperator), }, ]), ); }