2023-08-24 17:49:27 +09:00
import fetch from 'node-fetch-commonjs' ;
import config from './config' ;
2023-08-25 16:40:03 +09:00
import http from 'node:http' ;
import https from 'node:https' ;
const httpAgent = new http . Agent ( { keepAlive : true } ) ;
const httpsAgent = new https . Agent ( { keepAlive : true } ) ;
const agentSelector = function ( _parsedURL : any ) {
if ( _parsedURL . protocol == 'http:' ) {
return httpAgent ;
} else {
return httpsAgent ;
}
}
2023-08-24 17:49:27 +09:00
2022-08-29 01:23:20 +00:00
interface Match {
render : boolean ;
title : string ;
2024-05-09 20:01:13 +00:00
description : string ;
2022-08-29 01:23:20 +00:00
fallbackImg : string ;
staticImg? : string ;
networkMode : string ;
2023-08-24 17:49:27 +09:00
params? : string [ ] ;
sip? : SipTemplate ;
}
interface SipTemplate {
template : string ;
getData : Function ;
2022-08-29 01:23:20 +00:00
}
2023-08-25 16:40:03 +09:00
async function sipFetchJSON ( url , defaultVal = null ) {
try {
const response = await fetch ( url , { agent : agentSelector } ) ;
return response . ok ? response . json ( ) : defaultVal ;
} catch ( error ) {
return defaultVal ;
}
}
2022-08-29 01:23:20 +00:00
const routes = {
2024-03-11 15:38:09 +09:00
about : {
title : "About" ,
fallbackImg : '/resources/previews/about.jpg' ,
} ,
acceleration : {
title : "Acceleration" ,
fallbackImg : '/resources/previews/accelerator.jpg' ,
} ,
accelerator : {
title : "Mempool Accelerator" ,
fallbackImg : '/resources/previews/accelerator.jpg' ,
} ,
2023-08-24 17:49:27 +09:00
block : {
2024-03-12 10:22:42 +09:00
render : true ,
params : 1 ,
getTitle ( path ) {
2023-08-24 17:49:27 +09:00
return ` Block: ${ path [ 0 ] } ` ;
} ,
sip : {
template : 'block' ,
async getData ( params : string [ ] ) {
if ( params ? . length ) {
let blockId = params [ 0 ] ;
if ( blockId . length !== 64 ) {
2023-08-25 16:40:03 +09:00
blockId = await ( await fetch ( config . API . ESPLORA + ` /block-height/ ${ blockId } ` , { agent : agentSelector } ) ) . text ( ) ;
2023-08-24 17:49:27 +09:00
}
const [ block , transactions ] = await Promise . all ( [
2023-08-25 16:40:03 +09:00
sipFetchJSON ( config . API . MEMPOOL + ` /block/ ${ blockId } ` ) ,
sipFetchJSON ( config . API . ESPLORA + ` /block/ ${ blockId } /txids ` ) ,
2023-08-24 17:49:27 +09:00
] )
return {
block ,
transactions ,
2023-08-24 20:34:36 +09:00
canonicalPath : ` /block/ ${ blockId } ` ,
2023-08-24 17:49:27 +09:00
} ;
}
}
2024-03-12 10:22:42 +09:00
}
} ,
2023-08-24 17:49:27 +09:00
address : {
2022-08-29 01:23:20 +00:00
render : true ,
params : 1 ,
getTitle ( path ) {
2023-08-24 17:49:27 +09:00
return ` Address: ${ path [ 0 ] } ` ;
2022-08-29 01:23:20 +00:00
}
} ,
2024-03-11 17:54:51 +09:00
blocks : {
title : "Blocks" ,
fallbackImg : '/resources/previews/blocks.jpg' ,
} ,
2024-03-11 15:38:09 +09:00
docs : {
title : "Docs" ,
fallbackImg : '/resources/previews/faq.jpg' ,
routes : {
faq : {
title : "FAQ" ,
fallbackImg : '/resources/previews/faq.jpg' ,
2024-03-18 03:16:00 -04:00
} ,
api : {
title : "API Docs" ,
fallbackImg : '/resources/previews/docs-api.jpg' ,
2024-03-11 15:38:09 +09:00
}
}
} ,
2023-08-25 16:40:57 +09:00
tx : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Transaction: ${ path [ 0 ] } ` ;
} ,
sip : {
template : 'tx' ,
async getData ( params : string [ ] ) {
if ( params ? . length ) {
let txid = params [ 0 ] ;
const [ transaction , times , cpfp , rbf , outspends ] : any [ ] = await Promise . all ( [
sipFetchJSON ( config . API . ESPLORA + ` /tx/ ${ txid } ` ) ,
sipFetchJSON ( config . API . MEMPOOL + ` /transaction-times?txId[]= ${ txid } ` ) ,
sipFetchJSON ( config . API . MEMPOOL + ` /cpfp/ ${ txid } ` ) ,
sipFetchJSON ( config . API . MEMPOOL + ` /tx/ ${ txid } /rbf ` ) ,
sipFetchJSON ( config . API . MEMPOOL + ` /outspends?txId[]= ${ txid } ` ) ,
] )
const features = transaction ? {
segwit : transaction.vin.some ( ( v ) = > v . prevout && [ 'v0_p2wsh' , 'v0_p2wpkh' ] . includes ( v . prevout . scriptpubkey_type ) ) ,
taproot : transaction.vin.some ( ( v ) = > v . prevout && v . prevout . scriptpubkey_type === 'v1_p2tr' ) ,
rbf : transaction.vin.some ( ( v ) = > v . sequence < 0xfffffffe ) ,
} : { } ;
return {
transaction ,
times ,
cpfp ,
rbf ,
outspends ,
features ,
hex2ascii : function ( hex ) {
const opPush = hex . split ( ' ' ) . filter ( ( _ , i , a ) = > i > 0 && /^OP_PUSH/ . test ( a [ i - 1 ] ) ) ;
if ( opPush [ 0 ] ) {
hex = opPush [ 0 ] ;
}
if ( ! hex ) {
return '' ;
}
const bytes : number [ ] = [ ] ;
for ( let i = 0 ; i < hex . length ; i += 2 ) {
bytes . push ( parseInt ( hex . substr ( i , 2 ) , 16 ) ) ;
}
return new TextDecoder ( 'utf8' ) . decode ( Uint8Array . from ( bytes ) ) . replace ( /\uFFFD/g , '' ) . replace ( /\\0/g , '' ) ;
} ,
}
}
}
} ,
routes : {
push : {
title : "Push Transaction" ,
fallbackImg : '/resources/previews/tx-push.jpg' ,
}
}
} ,
2024-03-12 10:24:04 +09:00
enterprise : {
title : "Mempool Enterprise" ,
fallbackImg : '/resources/previews/enterprise.jpg' ,
} ,
2022-08-29 01:23:20 +00:00
lightning : {
title : "Lightning" ,
2024-03-10 10:33:15 +09:00
fallbackImg : '/resources/previews/lightning.jpg' ,
2022-08-29 01:23:20 +00:00
routes : {
node : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Lightning Node: ${ path [ 0 ] } ` ;
}
} ,
channel : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Lightning Channel: ${ path [ 0 ] } ` ;
}
} ,
2022-09-10 14:53:52 +00:00
nodes : {
routes : {
isp : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Lightning ISP: ${ path [ 0 ] } ` ;
}
}
}
2022-09-29 15:29:59 +00:00
} ,
group : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Lightning Node Group: ${ path [ 0 ] } ` ;
}
2022-09-10 14:53:52 +00:00
}
2022-08-29 01:23:20 +00:00
}
} ,
mining : {
title : "Mining" ,
2024-03-10 10:33:15 +09:00
fallbackImg : '/resources/previews/mining.jpg' ,
2022-09-17 22:27:37 +00:00
routes : {
pool : {
render : true ,
params : 1 ,
getTitle ( path ) {
return ` Mining Pool: ${ path [ 0 ] } ` ;
}
}
}
2024-03-11 15:38:09 +09:00
} ,
2024-03-11 15:55:32 +09:00
"privacy-policy" : {
2024-03-11 15:38:09 +09:00
title : "Privacy Policy" ,
fallbackImg : '/resources/previews/privacy-policy.jpg' ,
} ,
rbf : {
title : "RBF" ,
fallbackImg : '/resources/previews/rbf.jpg' ,
} ,
2024-03-12 10:24:04 +09:00
sponsor : {
title : "Community Sponsors" ,
fallbackImg : '/resources/previews/sponsor.jpg' ,
} ,
2024-03-11 15:55:32 +09:00
"terms-of-service" : {
2024-03-11 15:38:09 +09:00
title : "Terms of Service" ,
fallbackImg : '/resources/previews/terms-of-service.jpg' ,
} ,
2024-03-11 15:55:32 +09:00
"trademark-policy" : {
2024-03-11 15:38:09 +09:00
title : "Trademark Policy" ,
fallbackImg : '/resources/previews/trademark-policy.jpg' ,
} ,
2022-08-29 01:23:20 +00:00
} ;
2024-05-09 20:01:13 +00:00
export const networks = {
2022-08-29 01:23:20 +00:00
bitcoin : {
2024-05-09 20:01:13 +00:00
title : 'The Mempool Open Source Project®' ,
description : 'Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See the real-time status of your transactions, get network info, and more.' ,
2024-03-10 10:33:15 +09:00
fallbackImg : '/resources/previews/mempool-space-preview.jpg' ,
2022-08-29 01:23:20 +00:00
routes : {
. . . routes // all routes supported
}
} ,
liquid : {
2024-05-09 20:01:13 +00:00
title : 'The Mempool Open Source Project®' ,
description : 'Explore the full Bitcoin ecosystem with The Mempool Open Source Project®. See Liquid transactions & assets, get network info, and more.' ,
2023-08-23 00:11:24 +09:00
fallbackImg : '/resources/liquid/liquid-network-preview.png' ,
2022-08-29 01:23:20 +00:00
routes : { // only block, address & tx routes supported
block : routes.block ,
address : routes.address ,
tx : routes.tx
}
} ,
bisq : {
2023-08-23 00:11:24 +09:00
fallbackImg : '/resources/bisq/bisq-markets-preview.png' ,
2022-08-29 01:23:20 +00:00
routes : { } // no routes supported
2024-04-28 16:01:14 +00:00
} ,
onbtc : {
2024-05-09 20:01:13 +00:00
title : 'National Bitcoin Office of El Salvador' ,
description : 'The National Bitcoin Office (ONBTC) of El Salvador under President @nayibbukele' ,
2024-05-09 15:50:43 +00:00
fallbackImg : '/resources/onbtc/onbtc-preview.jpg' ,
2024-04-28 16:01:14 +00:00
routes : { // only dynamic routes supported
block : routes.block ,
address : routes.address ,
tx : routes.tx ,
mining : {
title : "Mining" ,
routes : {
pool : routes.mining.routes.pool ,
}
} ,
lightning : {
title : "Lightning" ,
routes : routes.lightning.routes ,
}
}
2022-08-29 01:23:20 +00:00
}
} ;
2023-08-24 17:49:27 +09:00
export function matchRoute ( network : string , path : string , matchFor : string = 'render' ) : Match {
2022-08-29 01:23:20 +00:00
const match : Match = {
render : false ,
title : '' ,
2024-05-09 20:01:13 +00:00
description : '' ,
2022-08-29 01:23:20 +00:00
fallbackImg : '' ,
networkMode : 'mainnet'
}
const parts = path . slice ( 1 ) . split ( '/' ) . filter ( p = > p . length ) ;
if ( parts [ 0 ] === 'preview' ) {
parts . shift ( ) ;
}
2024-05-08 19:58:57 +00:00
if ( [ 'testnet' , 'testnet4' , 'signet' ] . includes ( parts [ 0 ] ) ) {
2022-08-29 01:23:20 +00:00
match . networkMode = parts . shift ( ) || 'mainnet' ;
}
let route = networks [ network ] || networks . bitcoin ;
match . fallbackImg = route . fallbackImg ;
2024-05-09 20:01:13 +00:00
match . title = route . title ;
match . description = route . description ;
2022-08-29 01:23:20 +00:00
// traverse the route tree until we run out of route or tree, or hit a renderable match
2023-08-24 17:49:27 +09:00
while ( ! route [ matchFor ] && route . routes && parts . length && route . routes [ parts [ 0 ] ] ) {
2022-08-29 01:23:20 +00:00
route = route . routes [ parts [ 0 ] ] ;
parts . shift ( ) ;
if ( route . fallbackImg ) {
match . fallbackImg = route . fallbackImg ;
}
2024-05-09 20:01:13 +00:00
if ( route . description ) {
match . description = route . description ;
}
2022-08-29 01:23:20 +00:00
}
// enough route parts left for title & rendering
2023-08-24 17:49:27 +09:00
if ( route [ matchFor ] && parts . length >= route . params ) {
match . render = route . render ;
match . sip = route . sip ;
match . params = parts ;
2022-08-29 01:23:20 +00:00
}
// only use set a static image for exact matches
if ( ! parts . length && route . staticImg ) {
match . staticImg = route . staticImg ;
}
// apply the title function if present
if ( route . getTitle && typeof route . getTitle === 'function' ) {
match . title = route . getTitle ( parts ) ;
2024-05-09 20:01:13 +00:00
} else if ( route . title ) {
2022-08-29 01:23:20 +00:00
match . title = route . title ;
}
return match ;
2024-03-10 08:21:00 +09:00
}