diff --git a/.gitmodules b/.gitmodules index f18645c2..a8ef7faf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "regtest"] path = regtest url = https://github.com/BoltzExchange/regtest.git +[submodule "dnssec-prover"] + path = dnssec-prover + url = https://git.bitcoin.ninja/dnssec-prover diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..008427ea --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +src/utils/dnssec/dnssec* diff --git a/babel.config.js b/babel.config.js index a6920cc1..c8eed905 100644 --- a/babel.config.js +++ b/babel.config.js @@ -5,6 +5,7 @@ module.exports = { "@babel/preset-typescript", ], plugins: [ + "babel-plugin-transform-import-meta", "babel-plugin-transform-vite-meta-env", [ "@babel/plugin-transform-modules-commonjs", diff --git a/build.py b/build.py index 39851474..c7574788 100755 --- a/build.py +++ b/build.py @@ -21,7 +21,7 @@ def handle_coop_disabled(): data = json.load(f) network = data["network"] - + if data.get("cooperativeDisabled", False): handle_coop_disabled() diff --git a/build_dnssec_prover.py b/build_dnssec_prover.py new file mode 100755 index 00000000..69fda8d6 --- /dev/null +++ b/build_dnssec_prover.py @@ -0,0 +1,27 @@ +#! /bin/python3 + +import os +import shutil +from pathlib import Path + +def build(): + target_dir = Path(os.path.dirname(__file__)).joinpath("src/utils/dnssec") + + build_dir = Path(os.path.dirname(__file__)).joinpath("dnssec-prover/wasmpack") + os.chdir(build_dir) + os.system("wasm-pack build --target web --release && rm Cargo.lock && rm -r target/") + + os.chdir(os.path.dirname(__file__)) + os.makedirs(target_dir, exist_ok=True) + + for base, _, files in os.walk(build_dir.joinpath("pkg")): + for file in files: + if file in ["package.json", ".gitignore"]: + continue + + shutil.copy2( + Path(base).joinpath(file), + Path(target_dir).joinpath(file) + ) + +build() diff --git a/dnssec-prover b/dnssec-prover new file mode 160000 index 00000000..c4e06c7d --- /dev/null +++ b/dnssec-prover @@ -0,0 +1 @@ +Subproject commit c4e06c7d6099683b080d2f0c3b8b15d2d9e63824 diff --git a/package-lock.json b/package-lock.json index 9a40e8e9..af2ba9b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,6 +47,7 @@ "@types/jest": "^29.5.12", "@webbtc/webln-types": "^3.0.0", "babel-jest": "^29.7.0", + "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-vite-meta-env": "^1.0.3", "babel-preset-jest": "^29.6.3", "babel-preset-solid": "^1.8.19", @@ -5281,6 +5282,20 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/babel-plugin-transform-import-meta": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-import-meta/-/babel-plugin-transform-import-meta-2.2.1.tgz", + "integrity": "sha512-AxNh27Pcg8Kt112RGa3Vod2QS2YXKKJ6+nSvRtv7qQTJAdx0MZa4UHZ4lnxHUWA2MNbLuZQv5FVab4P1CoLOWw==", + "dev": true, + "license": "BSD", + "dependencies": { + "@babel/template": "^7.4.4", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.0" + } + }, "node_modules/babel-plugin-transform-vite-meta-env": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/babel-plugin-transform-vite-meta-env/-/babel-plugin-transform-vite-meta-env-1.0.3.tgz", diff --git a/package.json b/package.json index 666de3da..ea3cf9d3 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/jest": "^29.5.12", "@webbtc/webln-types": "^3.0.0", "babel-jest": "^29.7.0", + "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-vite-meta-env": "^1.0.3", "babel-preset-jest": "^29.6.3", "babel-preset-solid": "^1.8.19", diff --git a/src/utils/dnssec/README.md b/src/utils/dnssec/README.md new file mode 100644 index 00000000..19fef312 --- /dev/null +++ b/src/utils/dnssec/README.md @@ -0,0 +1,7 @@ +# DNSSEC prover + +## Copyright + +Compiled artifacts of https://git.bitcoin.ninja/dnssec-prover which was created +by Matt Corallo and is licensed under the MIT or Apache 2.0 open source +licenses. diff --git a/src/utils/dnssec/dnssec_prover_wasm.d.ts b/src/utils/dnssec/dnssec_prover_wasm.d.ts new file mode 100644 index 00000000..beb07a00 --- /dev/null +++ b/src/utils/dnssec/dnssec_prover_wasm.d.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Builds a proof builder which can generate a proof for records of the given `ty`pe at the given + * `name`. + * + * After calling this [`get_next_query`] should be called to fetch the initial query. + * @param {string} name + * @param {number} ty + * @returns {WASMProofBuilder | undefined} + */ +export function init_proof_builder(name: string, ty: number): WASMProofBuilder | undefined; +/** + * Processes a response to a query previously fetched from [`get_next_query`]. + * + * After calling this, [`get_next_query`] should be called until pending queries are exhausted and + * no more pending queries exist, at which point [`get_unverified_proof`] should be called. + * @param {WASMProofBuilder} proof_builder + * @param {Uint8Array} response + */ +export function process_query_response(proof_builder: WASMProofBuilder, response: Uint8Array): void; +/** + * Gets the next query (if any) that should be sent to the resolver for the given proof builder. + * + * Once the resolver responds [`process_query_response`] should be called with the response. + * @param {WASMProofBuilder} proof_builder + * @returns {Uint8Array | undefined} + */ +export function get_next_query(proof_builder: WASMProofBuilder): Uint8Array | undefined; +/** + * Gets the final, unverified, proof once all queries fetched via [`get_next_query`] have + * completed and their responses passed to [`process_query_response`]. + * @param {WASMProofBuilder} proof_builder + * @returns {Uint8Array} + */ +export function get_unverified_proof(proof_builder: WASMProofBuilder): Uint8Array; +/** + * Verifies an RFC 9102-formatted proof and returns verified records matching the given name + * (resolving any C/DNAMEs as required). + * @param {Uint8Array} stream + * @param {string} name_to_resolve + * @returns {string} + */ +export function verify_byte_stream(stream: Uint8Array, name_to_resolve: string): string; +export class WASMProofBuilder { + free(): void; +} + +export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module; + +export interface InitOutput { + readonly memory: WebAssembly.Memory; + readonly __wbg_wasmproofbuilder_free: (a: number, b: number) => void; + readonly init_proof_builder: (a: number, b: number, c: number) => number; + readonly process_query_response: (a: number, b: number, c: number) => void; + readonly get_next_query: (a: number) => Array; + readonly get_unverified_proof: (a: number) => Array; + readonly verify_byte_stream: (a: number, b: number, c: number, d: number) => Array; + readonly __wbindgen_export_0: WebAssembly.Table; + readonly __wbindgen_malloc: (a: number, b: number) => number; + readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number; + readonly __wbindgen_free: (a: number, b: number, c: number) => void; + readonly __externref_table_dealloc: (a: number) => void; + readonly __wbindgen_start: () => void; +} + +export type SyncInitInput = BufferSource | WebAssembly.Module; +/** +* Instantiates the given `module`, which can either be bytes or +* a precompiled `WebAssembly.Module`. +* +* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated. +* +* @returns {InitOutput} +*/ +export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput; + +/** +* If `module_or_path` is {RequestInfo} or {URL}, makes a request and +* for everything else, calls `WebAssembly.instantiate` directly. +* +* @param {{ module_or_path: InitInput | Promise }} module_or_path - Passing `InitInput` directly is deprecated. +* +* @returns {Promise} +*/ +export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise } | InitInput | Promise): Promise; diff --git a/src/utils/dnssec/dnssec_prover_wasm.js b/src/utils/dnssec/dnssec_prover_wasm.js new file mode 100644 index 00000000..0a0e655e --- /dev/null +++ b/src/utils/dnssec/dnssec_prover_wasm.js @@ -0,0 +1,339 @@ +let wasm; + +const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } ); + +if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); }; + +let cachedUint8ArrayMemory0 = null; + +function getUint8ArrayMemory0() { + if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) { + cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer); + } + return cachedUint8ArrayMemory0; +} + +function getStringFromWasm0(ptr, len) { + ptr = ptr >>> 0; + return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len)); +} + +let WASM_VECTOR_LEN = 0; + +const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } ); + +const encodeString = (typeof cachedTextEncoder.encodeInto === 'function' + ? function (arg, view) { + return cachedTextEncoder.encodeInto(arg, view); +} + : function (arg, view) { + const buf = cachedTextEncoder.encode(arg); + view.set(buf); + return { + read: arg.length, + written: buf.length + }; +}); + +function passStringToWasm0(arg, malloc, realloc) { + + if (realloc === undefined) { + const buf = cachedTextEncoder.encode(arg); + const ptr = malloc(buf.length, 1) >>> 0; + getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf); + WASM_VECTOR_LEN = buf.length; + return ptr; + } + + let len = arg.length; + let ptr = malloc(len, 1) >>> 0; + + const mem = getUint8ArrayMemory0(); + + let offset = 0; + + for (; offset < len; offset++) { + const code = arg.charCodeAt(offset); + if (code > 0x7F) break; + mem[ptr + offset] = code; + } + + if (offset !== len) { + if (offset !== 0) { + arg = arg.slice(offset); + } + ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0; + const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len); + const ret = encodeString(arg, view); + + offset += ret.written; + ptr = realloc(ptr, len, offset, 1) >>> 0; + } + + WASM_VECTOR_LEN = offset; + return ptr; +} +/** + * Builds a proof builder which can generate a proof for records of the given `ty`pe at the given + * `name`. + * + * After calling this [`get_next_query`] should be called to fetch the initial query. + * @param {string} name + * @param {number} ty + * @returns {WASMProofBuilder | undefined} + */ +export function init_proof_builder(name, ty) { + const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len0 = WASM_VECTOR_LEN; + const ret = wasm.init_proof_builder(ptr0, len0, ty); + return ret === 0 ? undefined : WASMProofBuilder.__wrap(ret); +} + +function _assertClass(instance, klass) { + if (!(instance instanceof klass)) { + throw new Error(`expected instance of ${klass.name}`); + } + return instance.ptr; +} + +function passArray8ToWasm0(arg, malloc) { + const ptr = malloc(arg.length * 1, 1) >>> 0; + getUint8ArrayMemory0().set(arg, ptr / 1); + WASM_VECTOR_LEN = arg.length; + return ptr; +} +/** + * Processes a response to a query previously fetched from [`get_next_query`]. + * + * After calling this, [`get_next_query`] should be called until pending queries are exhausted and + * no more pending queries exist, at which point [`get_unverified_proof`] should be called. + * @param {WASMProofBuilder} proof_builder + * @param {Uint8Array} response + */ +export function process_query_response(proof_builder, response) { + _assertClass(proof_builder, WASMProofBuilder); + const ptr0 = passArray8ToWasm0(response, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + wasm.process_query_response(proof_builder.__wbg_ptr, ptr0, len0); +} + +function getArrayU8FromWasm0(ptr, len) { + ptr = ptr >>> 0; + return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len); +} +/** + * Gets the next query (if any) that should be sent to the resolver for the given proof builder. + * + * Once the resolver responds [`process_query_response`] should be called with the response. + * @param {WASMProofBuilder} proof_builder + * @returns {Uint8Array | undefined} + */ +export function get_next_query(proof_builder) { + _assertClass(proof_builder, WASMProofBuilder); + const ret = wasm.get_next_query(proof_builder.__wbg_ptr); + let v1; + if (ret[0] !== 0) { + v1 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + } + return v1; +} + +function takeFromExternrefTable0(idx) { + const value = wasm.__wbindgen_export_0.get(idx); + wasm.__externref_table_dealloc(idx); + return value; +} +/** + * Gets the final, unverified, proof once all queries fetched via [`get_next_query`] have + * completed and their responses passed to [`process_query_response`]. + * @param {WASMProofBuilder} proof_builder + * @returns {Uint8Array} + */ +export function get_unverified_proof(proof_builder) { + _assertClass(proof_builder, WASMProofBuilder); + var ptr0 = proof_builder.__destroy_into_raw(); + const ret = wasm.get_unverified_proof(ptr0); + if (ret[3]) { + throw takeFromExternrefTable0(ret[2]); + } + var v2 = getArrayU8FromWasm0(ret[0], ret[1]).slice(); + wasm.__wbindgen_free(ret[0], ret[1] * 1, 1); + return v2; +} + +/** + * Verifies an RFC 9102-formatted proof and returns verified records matching the given name + * (resolving any C/DNAMEs as required). + * @param {Uint8Array} stream + * @param {string} name_to_resolve + * @returns {string} + */ +export function verify_byte_stream(stream, name_to_resolve) { + let deferred3_0; + let deferred3_1; + try { + const ptr0 = passArray8ToWasm0(stream, wasm.__wbindgen_malloc); + const len0 = WASM_VECTOR_LEN; + const ptr1 = passStringToWasm0(name_to_resolve, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + const len1 = WASM_VECTOR_LEN; + const ret = wasm.verify_byte_stream(ptr0, len0, ptr1, len1); + deferred3_0 = ret[0]; + deferred3_1 = ret[1]; + return getStringFromWasm0(ret[0], ret[1]); + } finally { + wasm.__wbindgen_free(deferred3_0, deferred3_1, 1); + } +} + +const WASMProofBuilderFinalization = (typeof FinalizationRegistry === 'undefined') + ? { register: () => {}, unregister: () => {} } + : new FinalizationRegistry(ptr => wasm.__wbg_wasmproofbuilder_free(ptr >>> 0, 1)); + +export class WASMProofBuilder { + + static __wrap(ptr) { + ptr = ptr >>> 0; + const obj = Object.create(WASMProofBuilder.prototype); + obj.__wbg_ptr = ptr; + WASMProofBuilderFinalization.register(obj, obj.__wbg_ptr, obj); + return obj; + } + + __destroy_into_raw() { + const ptr = this.__wbg_ptr; + this.__wbg_ptr = 0; + WASMProofBuilderFinalization.unregister(this); + return ptr; + } + + free() { + const ptr = this.__destroy_into_raw(); + wasm.__wbg_wasmproofbuilder_free(ptr, 0); + } +} + +async function __wbg_load(module, imports) { + if (typeof Response === 'function' && module instanceof Response) { + if (typeof WebAssembly.instantiateStreaming === 'function') { + try { + return await WebAssembly.instantiateStreaming(module, imports); + + } catch (e) { + if (module.headers.get('Content-Type') != 'application/wasm') { + console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e); + + } else { + throw e; + } + } + } + + const bytes = await module.arrayBuffer(); + return await WebAssembly.instantiate(bytes, imports); + + } else { + const instance = await WebAssembly.instantiate(module, imports); + + if (instance instanceof WebAssembly.Instance) { + return { instance, module }; + + } else { + return instance; + } + } +} + +function __wbg_get_imports() { + const imports = {}; + imports.wbg = {}; + imports.wbg.__wbindgen_string_new = function(arg0, arg1) { + const ret = getStringFromWasm0(arg0, arg1); + return ret; + }; + imports.wbg.__wbindgen_throw = function(arg0, arg1) { + throw new Error(getStringFromWasm0(arg0, arg1)); + }; + imports.wbg.__wbindgen_init_externref_table = function() { + const table = wasm.__wbindgen_export_0; + const offset = table.grow(4); + table.set(0, undefined); + table.set(offset + 0, undefined); + table.set(offset + 1, null); + table.set(offset + 2, true); + table.set(offset + 3, false); + ; + }; + + return imports; +} + +function __wbg_init_memory(imports, memory) { + +} + +function __wbg_finalize_init(instance, module) { + wasm = instance.exports; + __wbg_init.__wbindgen_wasm_module = module; + cachedUint8ArrayMemory0 = null; + + + wasm.__wbindgen_start(); + return wasm; +} + +function initSync(module) { + if (wasm !== undefined) return wasm; + + + if (typeof module !== 'undefined') { + if (Object.getPrototypeOf(module) === Object.prototype) { + ({module} = module) + } else { + console.warn('using deprecated parameters for `initSync()`; pass a single object instead') + } + } + + const imports = __wbg_get_imports(); + + __wbg_init_memory(imports); + + if (!(module instanceof WebAssembly.Module)) { + module = new WebAssembly.Module(module); + } + + const instance = new WebAssembly.Instance(module, imports); + + return __wbg_finalize_init(instance, module); +} + +async function __wbg_init(module_or_path) { + if (wasm !== undefined) return wasm; + + + if (typeof module_or_path !== 'undefined') { + if (Object.getPrototypeOf(module_or_path) === Object.prototype) { + ({module_or_path} = module_or_path) + } else { + console.warn('using deprecated parameters for the initialization function; pass a single object instead') + } + } + + if (typeof module_or_path === 'undefined') { + module_or_path = new URL('dnssec_prover_wasm_bg.wasm', import.meta.url); + } + const imports = __wbg_get_imports(); + + if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) { + module_or_path = fetch(module_or_path); + } + + __wbg_init_memory(imports); + + const { instance, module } = await __wbg_load(await module_or_path, imports); + + return __wbg_finalize_init(instance, module); +} + +export { initSync }; +export default __wbg_init; diff --git a/src/utils/dnssec/dnssec_prover_wasm_bg.wasm b/src/utils/dnssec/dnssec_prover_wasm_bg.wasm new file mode 100644 index 00000000..790bae35 Binary files /dev/null and b/src/utils/dnssec/dnssec_prover_wasm_bg.wasm differ diff --git a/src/utils/dnssec/dnssec_prover_wasm_bg.wasm.d.ts b/src/utils/dnssec/dnssec_prover_wasm_bg.wasm.d.ts new file mode 100644 index 00000000..9dd3f0a4 --- /dev/null +++ b/src/utils/dnssec/dnssec_prover_wasm_bg.wasm.d.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +/* eslint-disable */ +export const memory: WebAssembly.Memory; +export function __wbg_wasmproofbuilder_free(a: number, b: number): void; +export function init_proof_builder(a: number, b: number, c: number): number; +export function process_query_response(a: number, b: number, c: number): void; +export function get_next_query(a: number): Array; +export function get_unverified_proof(a: number): Array; +export function verify_byte_stream(a: number, b: number, c: number, d: number): Array; +export const __wbindgen_export_0: WebAssembly.Table; +export function __wbindgen_malloc(a: number, b: number): number; +export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number; +export function __wbindgen_free(a: number, b: number, c: number): void; +export function __externref_table_dealloc(a: number): void; +export function __wbindgen_start(): void; diff --git a/src/utils/dnssec/dohLookup.ts b/src/utils/dnssec/dohLookup.ts new file mode 100644 index 00000000..826a0231 --- /dev/null +++ b/src/utils/dnssec/dohLookup.ts @@ -0,0 +1,94 @@ +import init, { WASMProofBuilder } from "./dnssec_prover_wasm.js"; +import * as wasm from "./dnssec_prover_wasm.js"; + +/* + * Based on: https://github.com/TheBlueMatt/satsto.me/blob/6fe1d5c027db0cbf928e961277b19addfa955577/static/doh_lookup.js + * Copyright: Matt Corallo 2024 + */ + +type DnsType = "txt" | "tsla" | "a" | "aaaa"; + +type RRS = { + type: DnsType; + name: string; + contents: string; +}; + +type LookupResult = { + expires: number; + valid_from: number; + max_cache_ttl: number; + verified_rrs: RRS[]; +}; + +const sendQuery = async ( + builder: WASMProofBuilder, + domain: string, + dohEndpoint: string, +) => { + const query = wasm.get_next_query(builder); + if (query === null || query === undefined) { + const proof = wasm.get_unverified_proof(builder); + if (proof === null) { + throw "failed to build proof"; + } + + return JSON.parse(wasm.verify_byte_stream(proof, domain)); + } + + const b64url = btoa(String.fromCodePoint(...query)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); + + const resp = await fetch(dohEndpoint + "?dns=" + b64url, { + headers: { accept: "application/dns-message" }, + }); + if (!resp.ok) { + throw "DoH query failed"; + } + + const buf = new Uint8Array(await resp.arrayBuffer()); + wasm.process_query_response(builder, buf); + + return await sendQuery(builder, domain, dohEndpoint); +}; + +export const lookup = async ( + domain: string, + dnsType: DnsType, + dohEndpoint: string, +): Promise => { + await init(); + + if (!domain.endsWith(".")) domain += "."; + + let ty: number; + switch (dnsType) { + case "txt": + ty = 16; + break; + + case "tsla": + ty = 52; + break; + + case "a": + ty = 1; + break; + + case "aaaa": + ty = 28; + break; + + default: + throw "invalid DNS lookup type"; + } + + const builder = wasm.init_proof_builder(domain, ty); + if (builder == null) { + throw "bad domain"; + } + + return await sendQuery(builder, domain, dohEndpoint); +}; diff --git a/src/utils/invoice.ts b/src/utils/invoice.ts index 46a938df..043527ce 100644 --- a/src/utils/invoice.ts +++ b/src/utils/invoice.ts @@ -6,6 +6,7 @@ import log from "loglevel"; import { config } from "../config"; import { fetchBolt12Invoice } from "./boltzClient"; +import { lookup } from "./dnssec/dohLookup"; import { checkResponse } from "./http"; type LnurlResponse = { @@ -104,10 +105,7 @@ export const fetchLnurl = async ( return await fetchLnurlInvoice(amount, res); }; -export const fetchBip353 = async ( - bip353: string, - amountSat: number, -): Promise => { +export const resolveBip353 = async (bip353: string): Promise => { const split = bip353.split("@"); if (split.length !== 2) { throw "invalid BIP-353"; @@ -119,26 +117,45 @@ export const fetchBip353 = async ( log.debug(`Fetching BIP-353: ${bip353}`); - const params = new URLSearchParams({ - type: "TXT", - name: `${split[0]}.user._bitcoin-payment.${split[1]}`, - }); - const res = await fetch(`${config.dnsOverHttps}?${params.toString()}`, { - headers: { - Accept: "application/dns-json", - }, - }); - const resBody = await res.json(); - if (resBody.Answer === undefined || resBody.Answer.length === 0) { + const res = await lookup( + `${split[0]}.user._bitcoin-payment.${split[1]}`, + "txt", + config.dnsOverHttps, + ); + + const nowUnix = Date.now() / 1_000; + if (nowUnix < res.valid_from) { + throw "proof is not valid yet"; + } + if (nowUnix > res.expires) { + throw "proof has expired"; + } + + if (res.verified_rrs === undefined || res.verified_rrs.length === 0) { throw "no TXT record"; } - const paymentRequest = resBody.Answer[0].data; - const offer = new URLSearchParams(paymentRequest.split("?")[1]).get("lno"); - const invoice = ( - await fetchBolt12Invoice(offer.replaceAll('"', ""), amountSat) - ).invoice; - log.debug(`Resolved invoice for BIP-353:`, invoice); + if (res.verified_rrs[0].type !== "txt") { + throw "invalid proof"; + } + + const paymentRequest = res.verified_rrs[0].contents; + const offer = new URLSearchParams(paymentRequest.split("?")[1]) + .get("lno") + .replaceAll('"', ""); + + log.debug("Resolved offer for BIP-353:", offer); + return offer; +}; + +export const fetchBip353 = async ( + bip353: string, + amountSat: number, +): Promise => { + const offer = await resolveBip353(bip353); + const invoice = (await fetchBolt12Invoice(offer, amountSat)).invoice; + log.debug(`Resolved invoice for offer:`, invoice); + return invoice; };