diff --git a/.gitignore b/.gitignore index 1711fa779..d8530956d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ coverage/ logs/ node_modules/ -data .env .env.aws .docker.config @@ -13,8 +12,8 @@ error.log bcn.config.json zmqlog.log packages/**/node_modules/ -packages/**/data/blockchain-data/ -packages/**/data/db-data/ +packages/**/chain-setup/**/blockchain-data +packages/**/chain-setup/**/db-data # Logs logs *.log diff --git a/packages/node/dist/bcn.es.mjs b/packages/node/dist/bcn.es.mjs index 708b4808a..db7832b6b 100644 --- a/packages/node/dist/bcn.es.mjs +++ b/packages/node/dist/bcn.es.mjs @@ -1 +1 @@ -import t from"body-parser";import e from"cors";import s from"express";import a from"http";import*as r from"zeromq";import n from"express-rate-limit";import*as o from"@bitcoin-computer/secp256k1";import{crypto as i,networks as c,bufferUtils as u,Transaction as l,address as p,payments as d,Psbt as m,initEccLib as h}from"@bitcoin-computer/nakamotojs";import y from"dotenv";import g from"winston";import w from"winston-daily-rotate-file";import f from"pg-promise";import E from"pg-monitor";import{backOff as v}from"exponential-backoff";import T from"fs";import{ECPairFactory as $}from"ecpair";import{Computer as O}from"@bitcoin-computer/lib";import R from"bitcoind-rpc";import S from"util";import I from"elliptic";import b from"hash.js";import x,{dirname as N}from"path";import{fileURLToPath as B}from"url";y.config();const M=process.env.BCN_CHAIN;const C=process.env.BCN_NETWORK;const{BCN_PORT:H}=process.env;const{BCN_ZMQ_URL:k}=process.env;const{BCN_ALLOWED_RPC_METHODS:P}=process.env;const{BCN_DEBUG_MODE:L}=process.env;const{BCN_LOG_MAX_FILES:A}=process.env;const{BCN_LOG_MAX_SIZE:_}=process.env;const{BCN_LOG_ZIP:j}=process.env;const{BCN_SHOW_CONSOLE_LOGS:U}=process.env;const{BCN_SHOW_DB_LOGS:F}=process.env;const{BCN_RATE_LIMIT_ENABLED:D}=process.env;const{BCN_RATE_LIMIT_WINDOW:W}=process.env;const{BCN_RATE_LIMIT_MAX:K}=process.env;const{BCN_RATE_LIMIT_STANDARD_HEADERS:Y}=process.env;const{BCN_RATE_LIMIT_LEGACY_HEADERS:G}=process.env;process.env,process.env;const{BCN_OFFCHAIN_PROTOCOL:q}=process.env;const J=process.env.BCN_QUERY_LIMIT||"1000";const V=process.env.BCN_URL||`http://127.0.0.1:${H}`;const z=process.env.BCN_ENV||"dev";const{BITCOIN_RPC_USER:Z}=process.env;const{BITCOIN_RPC_PASSWORD:X}=process.env;const{BITCOIN_RPC_HOST:Q}=process.env;const{BITCOIN_RPC_PORT:tt}=process.env;const{BITCOIN_RPC_PROTOCOL:et}=process.env;const{BITCOIN_DEFAULT_WALLET:st}=process.env;const{POSTGRES_USER:at}=process.env;const{POSTGRES_PASSWORD:rt}=process.env;const{POSTGRES_DB:nt}=process.env;const{POSTGRES_HOST:ot}=process.env;const{POSTGRES_PORT:it}=process.env;g.addColors({error:"red",warn:"yellow",info:"green",http:"magenta",debug:"white"});const ct=g.format.combine(g.format.colorize(),g.format.timestamp({format:"YYYY-MM-DD HH:mm:ss:ms"}),g.format.json(),g.format.printf((t=>`${t.timestamp} [${t.level.slice(5).slice(0,-5)}] ${t.message}`)));const ut={zippedArchive:"true"===j,maxSize:_,maxFiles:A,dirname:"logs"};const lt=[];"true"===U&<.push(new g.transports.Console({format:g.format.combine(g.format.colorize(),g.format.timestamp({format:"MM-DD-YYYY HH:mm:ss"}),g.format.printf((t=>`${t.timestamp} ${t.level} ${t.message}`)))}));const pt=parseInt(L,10);pt>=0&<.push(new w({filename:"error-%DATE%.log",datePattern:"YYYY-MM-DD",level:"error",...ut})),pt>=1&<.push(new w({filename:"warn-%DATE%.log",datePattern:"YYYY-MM-DD",level:"warn",...ut})),pt>=2&<.push(new w({filename:"info-%DATE%.log",datePattern:"YYYY-MM-DD",level:"info",...ut})),pt>=3&<.push(new w({filename:"http-%DATE%.log",datePattern:"YYYY-MM-DD",level:"http",...ut})),pt>=4&<.push(new w({filename:"debug-%DATE%.log",datePattern:"YYYY-MM-DD",level:"debug",...ut}));const dt=g.createLogger({levels:{error:0,warn:1,info:2,http:3,debug:4},format:ct,transports:lt,exceptionHandlers:[new g.transports.File({filename:"logs/exceptions.log"})],rejectionHandlers:[new g.transports.File({filename:"logs/rejections.log"})]});y.config();const{version:mt}=JSON.parse(T.readFileSync("package.json","utf8"));const ht=mt||process.env.BCN_SERVER_VERSION;const yt=parseInt(process.env.MWEB_HEIGHT||"",10)||432;const gt={error:(t,e)=>{if(e.cn){const{host:s,port:a,database:r,user:n,password:o}=e.cn;dt.debug(`Waiting for db to start { message:${t.message} host:${s}, port:${a}, database:${r}, user:${n}, password: ${o}`)}},noWarnings:!0};"true"===F&&(E.isAttached()?E.detach():(E.attach(gt),E.setTheme("matrix")));const wt=f(gt)({host:ot,port:parseInt(it,10),database:nt,user:at,password:rt,allowExitOnIdle:!0,idleTimeoutMillis:100});const{PreparedStatement:ft}=f;class Et{static async select(t){const e=new ft({name:`OffChain.select.${Math.random()}`,text:'SELECT "data" FROM "OffChain" WHERE "id" = $1',values:[t]});return wt.oneOrNone(e)}static async insert({id:t,data:e}){const s=new ft({name:`OffChain.insert.${Math.random()}`,text:'INSERT INTO "OffChain" ("id", "data") VALUES ($1, $2) ON CONFLICT DO NOTHING',values:[t,e]});return wt.none(s)}static async delete(t){const e=new ft({name:`OffChain.delete.${Math.random()}`,text:'WITH deleted AS (DELETE FROM "OffChain" WHERE "id" = $1 RETURNING *) SELECT count(*) FROM deleted;',values:[t]});return(await wt.any(e))[0].count>0}}class vt{static async select(t){const e=await Et.select(t);return e?.data||null}static async insert(t){return Et.insert(t)}static async delete(t){return Et.delete(t)}}const Tt=s.Router();Tt.get("/:id",(async({params:{id:t},url:e},s)=>{try{const e=await vt.select(t);e?s.status(200).json(e):s.status(403).json({error:"No entry found."})}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),Tt.post("/",(async(t,e)=>{const{body:{data:s},url:a}=t;try{const a=i.sha256(Buffer.from(s)).toString("hex");await vt.insert({id:a,data:s});const r=`${q||t.protocol}://${t.get("host")}/store/${a}`;e.status(201).json({_url:r})}catch(t){dt.error(`POST ${a} failed with error '${t.message}'`),e.status(500).json({error:t.message})}})),Tt.delete("/:id",(async(t,e)=>{e.status(500).json({error:"Deletions are not supported yet."})}));const{PreparedStatement:$t}=f;class Ot{static async getBalance(t){const e=new $t({name:`Utxos.getBalance.${Math.random()}`,text:'SELECT sum("satoshis") as "satoshis" FROM "Utxos" WHERE "address" = $1 and "blockHash" is not null',values:[t]});const s=await wt.oneOrNone(e);const a=new $t({name:`Utxos.getBalance.${Math.random()}`,text:'SELECT sum("satoshis") as "satoshis" FROM "Utxos" WHERE "address" = $1 and "blockHash" is null',values:[t]});const r=await wt.oneOrNone(a);return{confirmed:parseInt(s.satoshis,10)||0,unconfirmed:parseInt(r.satoshis,10)||0,balance:(parseInt(s.satoshis,10)||0)+(parseInt(r.satoshis,10)||0)}}static async select(t){const e=new $t({name:`Utxos.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout" FROM "Utxos" WHERE "address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)||0})))}static async selectByScriptHex(t){const e=new $t({name:`Utxos.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout" FROM "Utxos" WHERE "scriptPubKey" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)||0})))}static async selectByPk(t){const e=new $t({name:`Utxos.selectByPk.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout", "publicKeys" FROM "Utxos" WHERE $1 = ANY ("publicKeys")',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)})))}}class Rt{static async getBalance(t){return Ot.getBalance(t)}static async select(t){return Ot.select(t)}static async selectByScriptHex(t){return Ot.selectByScriptHex(t)}static async selectByPk(t){return Ot.selectByPk(t)}}class St{static getBalance=async t=>Rt.getBalance(t);static selectByAddress=async t=>Rt.select(t);static selectByScriptHex=async t=>Rt.selectByScriptHex(t);static selectByPk=async t=>Rt.selectByPk(t)}const It={protocol:et,user:Z,pass:X,host:Q,port:parseInt(tt,10)};const bt=new R(It);const xt=S.promisify(R.prototype.createwallet.bind(bt));const Nt=S.promisify(R.prototype.generateToAddress.bind(bt));const Bt=S.promisify(R.prototype.getaddressinfo.bind(bt));const Mt=S.promisify(R.prototype.getBlock.bind(bt));const Ct=S.promisify(R.prototype.getBlockchainInfo.bind(bt));const Ht=S.promisify(R.prototype.getBlockHash.bind(bt));const kt=S.promisify(R.prototype.getRawTransaction.bind(bt));const Pt=S.promisify(R.prototype.getRawTransaction.bind(bt));const Lt=S.promisify(R.prototype.getTransaction.bind(bt));const At=S.promisify(R.prototype.getNewAddress.bind(bt));const _t={createwallet:xt,generateToAddress:Nt,getaddressinfo:Bt,getBlock:Mt,getBlockchainInfo:Ct,getBlockHash:Ht,getRawTransaction:kt,getTransaction:Lt,importaddress:S.promisify(R.prototype.importaddress.bind(bt)),invalidateBlock:S.promisify(R.prototype.invalidateBlock.bind(bt)),listunspent:S.promisify(R.prototype.listunspent.bind(bt)),sendRawTransaction:S.promisify(R.prototype.sendRawTransaction.bind(bt)),getNewAddress:At,sendToAddress:S.promisify(R.prototype.sendToAddress.bind(bt)),getRawTransactionJSON:Pt};const jt=(t,e)=>{const s=[];for(let a=0;a{const e=[];for(let s=1;s<=t;s+=3){const t=`($${s},$${s+1},$${s+2})`;e.push(t)}return e.join(",")};const Ft=t=>{const e=[];for(let s=1;s<=t;s+=10){const t=`($${s},$${s+1},$${s+2},$${s+3},$${s+4},$${s+5},$${s+6},$${s+7},$${s+8},$${s+9})`;e.push(t)}return e.join(",")};const Dt=t=>{try{return t()}catch{return null}};class Wt{static async getTransaction(t){const{result:e}=await _t.getTransaction(t);return e}static async getBulkTransactions(t){return(await Promise.all(t.map((t=>_t.getRawTransaction(t,0))))).map((t=>t.result))}static async getRawTransaction(t,e){const{result:s}=await _t.getRawTransaction(t,e);return s}static async getRawTransactionsJSON(t){return{txId:(e=(await _t.getRawTransactionJSON(t,1)).result).txid,txHex:e.hex,vsize:e.vsize,version:e.version,locktime:e.locktime,ins:e.vin.map((t=>t.coinbase?{coinbase:t.coinbase,sequence:t.sequence}:{txId:t.txid,vout:t.vout,script:t.scriptSig.hex,sequence:t.sequence})),outs:e.vout.map((t=>{let e;return t.scriptPubKey.addresses?[e]=t.scriptPubKey.addresses:e=t.scriptPubKey.address?t.scriptPubKey.address:void 0,{address:e,script:t.scriptPubKey.hex,value:Math.round(1e8*t.value)}}))};var e}static async sendRawTransaction(t){const{result:e,error:s}=await _t.sendRawTransaction(t);if(s)throw dt.error(s),new Error("Error sending transaction");return e}static getUtxos=async t=>(void 0===(await _t.getaddressinfo(t)).result.timestamp&&(dt.info(`Importing address: ${t}`),await _t.importaddress(t,!1)),(await _t.listunspent(0,999999,[t])).result);static waitForRpcBlockHash=async(t,e)=>(await v((async()=>{let s;try{s=await _t.getBlockHash(t)}catch(s){throw dt.info(`[wid ${e} pid: ${process.pid}]: waiting for RPC to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).result;static getBlock=async(t,e)=>_t.getBlock(t,e);static walletSetup=async()=>{if("regtest"===C){if(dt.info(`Node is starting for chain ${M} and network ${C}, \n\n. Starting Wallet setup.`),"LTC"===M){const{result:t}=await _t.getBlockchainInfo();const e=t.blocks;if(e{try{await _t.createwallet(st,!1,!1,"",!1,!1)}catch(t){dt.error(`Wallet creation failed with error '${t.message}'`)}};static checkBlockchainProgress=async()=>{const t=await v((async()=>{const t=await _t.getBlockchainInfo();const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const{blocks:s}=t.result;if(dt.info(`Zmq. Bitcoind { percentage:${e}%, blocks:${s} }`),parseFloat(t.result.verificationprogress)<=.7)throw new Error("Node not ready yet");return t}),{startingDelay:6e4,timeMultiple:1,numOfAttempts:8760});const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const s=t.result.blocks;dt.info(`BCN reaches sync end...at { bitcoind.progress:${e}%, bitcoindSyncedHeight:${s} }`)}}const{PreparedStatement:Kt}=f;class Yt{static async select(t){const e=new Kt({name:`Input.select.${Math.random()}`,text:'SELECT "outputSpent", "spendingInput", "blockHash" FROM "Input" WHERE "outputSpent" = $1',values:[t]});return wt.any(e)}static async insert(t){await Promise.all(jt(t,3333).map((t=>{const e=t.flatMap((({outputSpent:t,spendingInput:e,blockHash:s})=>[t,e,s]));return wt.none(new Kt({name:`Input.insert.${Math.random()}`,text:`INSERT INTO "Input"("outputSpent", "spendingInput", "blockHash") VALUES ${Ut(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async updateBlockHash(t,e){await Promise.all(jt(t,1e4).map((t=>{const s=t.join("','");return wt.none(new Kt({name:`Input.updateBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = $1 WHERE "spendingInput" IN ('${s}')`,values:[e]}))})))}static async eraseBlockHash(t){await Promise.all(jt(t,1e4).map((t=>{const e=t.join("','");return wt.none(new Kt({name:`Input.eraseBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async count(t){const e=t.map((t=>t.outputSpent));const s=new Kt({name:`Input.belong.${Math.random()}`,text:'SELECT count(*) FROM "Input" WHERE "outputSpent" LIKE ANY ($1)',values:[[e]]});const a=await wt.oneOrNone(s);return parseInt(a?.count,10)||0}}class Gt{static async select(t){return Yt.select(t)}static async insert(t){return Yt.insert(t)}static async updateBlockHash(t,e){return Yt.updateBlockHash(t,e)}static async eraseBlockHash(t){return Yt.eraseBlockHash(t)}}class qt{static insert=async(t,e=null)=>{const s=t.flatMap((t=>t.tx.ins.map(((e,s)=>({input:e,index:s,txId:t.txId}))))).filter((({input:t})=>!l.isCoinbaseHash(t.hash))).map((({input:t,index:e,txId:s})=>{return{outputSpent:`${a=t.hash,u.reverseBuffer(Buffer.from(a)).toString("hex")}:${t.index}`,spendingInput:`${s}:${e}`,blockHash:null};var a}));if(await Gt.insert(s),e){const t=s.map((({spendingInput:t})=>t));await Gt.updateBlockHash(t,e)}};static select=async t=>Gt.select(t);static updateBlockHash=async(t,e)=>{await Gt.updateBlockHash(t,e)};static eraseBlockHash=async t=>{await Gt.eraseBlockHash(t)}}function Jt(t){return/^[0-9A-Fa-f]{64}:\d+$/.test(t)}function Vt(t){if(!Jt(t))throw new Error("Invalid rev")}const{PreparedStatement:zt}=f;class Zt{static async listSentOutputs(t){const e=new zt({name:`Output.listSentTxs.${Math.random()}`,text:'SELECT "Input"."spendingInput" AS "output", "Output"."satoshis" AS "amount"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output"."address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listReceivedOutputs(t){const e=new zt({name:`Output.listReceivedTxs.${Math.random()}`,text:'SELECT "Output"."rev" as "output", "Output"."satoshis" as "amount" FROM "Output" WHERE "address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listTxs(t){const e=new zt({name:`Output.listTxs.${Math.random()}`,text:'WITH\n -- List all txs sent from a given address\n SENT AS (\n SELECT split_part("Input"."spendingInput",\':\',1) as "txId", SUM("Output".satoshis) as "satoshis"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output".address = $1\n GROUP BY split_part("Input"."spendingInput",\':\',1)\n ),\n -- List all tx received from a given address\n RECEIVED AS (\n SELECT SPLIT_PART("Output"."rev",\':\',1) as "txId", SUM("Output"."satoshis") as "satoshis" \n FROM "Output" \n WHERE "address" = $1\n GROUP BY "txId"\n )\n\n SELECT\n RECEIVED."txId", \n coalesce(SENT."satoshis", 0) as "inputsSatoshis", \n coalesce(RECEIVED."satoshis", 0) as "outputsSatoshis", \n coalesce(RECEIVED."satoshis",0) - coalesce(SENT."satoshis",0) as "satoshis"\n FROM\n SENT RIGHT JOIN RECEIVED ON SENT."txId" = RECEIVED."txId";',values:[t]});const s=(await wt.any(e)).map((t=>({...t,inputsSatoshis:parseInt(t.inputsSatoshis,10)||0,outputsSatoshis:parseInt(t.outputsSatoshis,10)||0,satoshis:parseInt(t.satoshis,10)||0})));return{sentTxs:s.filter((t=>t.satoshis<0)).map((t=>({...t,satoshis:Math.abs(t.satoshis)}))),receivedTxs:s.filter((t=>t.satoshis>=0))}}static async select(t){const e=new zt({name:`Output.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", "publicKeys", "hash", "mod", "isTbcOutput", "previous", "blockHash" FROM "Output" WHERE "address" = $1',values:[t]});return wt.any(e)}static async insert(t){await Promise.all(jt(t,1e4).map((t=>{const e=t.flatMap((({rev:t,address:e,satoshis:s,scriptPubKey:a,isTbcOutput:r,publicKeys:n,mod:o,previous:i,hash:c,blockHash:u})=>[t,e,s,a,r,n,o,i,c,u]));return wt.none(new zt({name:`Output.insert.${Math.random()}`,text:`INSERT INTO "Output"("rev", "address", "satoshis", "scriptPubKey", "isTbcOutput", "publicKeys", "mod", "previous", "hash", "blockHash") VALUES ${Ft(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async eraseBlockHash(t){await Promise.all(jt(t,1e4).map((t=>{const e=t.join("','");return wt.none(new zt({name:`Output.eraseBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async updateBlockHash(t,e){await Promise.all(jt(t,1e4).map((t=>{const s=t.join("','");return wt.none(new zt({name:`Output.updateBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = $1 WHERE "rev" IN ('${s}')`,values:[e]}))})))}static async getIdByRev(t){const e=new zt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON r."previous" = o."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=(await wt.any(e)).filter((t=>null===t.previous));return s[0]?.rev}static async getIdsByRevs(t){return Promise.all(t.map((t=>this.getIdByRev(t))))}static async getLatestRev(t){const e=new zt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON o."previous" = r."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=await wt.any(e);const a=Object.fromEntries(s.map((t=>[t.previous,t.rev])));let r=t;for(;a[r];)r=a[r];return r}static async getLatestRevs(t){return Promise.all(t.map(this.getLatestRev))}static async getIdsByMod(t){const e=new zt({name:`Output.getIdsByMod.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1',values:[t]});return(await wt.any(e)).map((t=>t.rev))}static sqlSuffix(t,e,s){let a="";return s&&(a+=` order by "timestamp" ${s}`),a+=` limit ${t||J}`,e&&(a+=` offset ${e}`),a}static async getRevsByPublicKey(t){const e=new zt({name:`Output.getRevsByPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys")',values:[t]});return(await wt.any(e)).map((t=>t.rev))}static async getUnspentRevsByMod(t,e,s,a){const r=await this.getIdsByMod(t);const n=await this.getLatestRevs(r);const o=new zt({name:`Output.getUnspentRevsByMod.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(e,s,a)}`,values:[n]});return(await wt.any(o)).map((t=>t.rev))}static async getUnspentRevsByPublicKey(t,e,s,a){const r=new zt({name:`Output.getUnspentRevsByPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys") AND "isTbcOutput" = true \n AND NOT EXISTS (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") \n ${this.sqlSuffix(e,s,a)}`,values:[t]});return(await wt.any(r)).map((t=>t.rev))}static async getUnspentRevsByModAndPublicKey(t,e,s,a,r){const n=await this.getUnspentRevsByPublicKey(e,s,a,r);const o=await this.getIdsByRevs(n);const i=new zt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1 AND "rev" = ANY($2)',values:[t,o]});const c=(await wt.any(i)).map((t=>t.rev));const u=await this.getLatestRevs(c);const l=new zt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(s,a,r)}`,values:[u]});return(await wt.any(l)).map((t=>t.rev))}static async getUnspentTbcOutputs(t,e,s){const a=new zt({name:`Output.getUnspentTbcOutputs.${Math.random()}`,text:`SELECT "rev", "address", "satoshis", "scriptPubKey", "publicKeys", "timestamp"\n FROM "Output" WHERE "isTbcOutput" = true AND NOT EXISTS\n (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") ${this.sqlSuffix(t,e,s)}`});return(await wt.any(a)).map((t=>t.rev))}static async query(t){const{publicKey:e,limit:s,offset:a,ids:r,mod:n,order:o}=t;const i=parseInt(J||"",10);if(s&&parseInt(s||"",10)>i||r&&r.length>i)throw new Error(`Can't fetch more than ${J} revs.`);if(o&&"ASC"!==o&&"DESC"!==o)throw new Error("Invalid order. Should be ASC or DESC.");return r?(r.map(Vt),this.getLatestRevs(r)):n&&!e?this.getUnspentRevsByMod(n,s,a,o):!n&&e?this.getUnspentRevsByPublicKey(e,s,a,o):n&&e?this.getUnspentRevsByModAndPublicKey(n,e,s,a,o):this.getUnspentTbcOutputs(s,a,o)}}class Xt{static async select(t){return Zt.select(t)}static async insert(t){return Zt.insert(t)}static async eraseBlockHash(t){return Zt.eraseBlockHash(t)}static async updateBlockHash(t,e){return Zt.updateBlockHash(t,e)}static async listSentOutputs(t){return Zt.listSentOutputs(t)}static async listReceivedOutputs(t){return Zt.listReceivedOutputs(t)}static async listTxs(t){return Zt.listTxs(t)}static async getLatestRev(t){return Zt.getLatestRev(t)}static async getLatestRevs(t){return Zt.getLatestRevs(t)}static async getIdByRev(t){return Zt.getIdByRev(t)}static async query(t){return Zt.query(t)}}class Qt{static insert=async(t,e=null)=>{const s=function(t=M,e=C){switch(t){case"BTC":switch(e){case"mainnet":return c.bitcoin;case"testnet":return c.testnet;case"regtest":return c.regtest;default:throw new Error(`Invalid network ${e}`)}case"LTC":switch(e){case"mainnet":return c.litecoin;case"testnet":return c.litecointestnet;case"regtest":return c.litecoinregtest;default:throw new Error(`Invalid network ${e}`)}case"PEPE":switch(e){case"mainnet":return c.pepecoin;case"testnet":return c.pepecointestnet;case"regtest":return c.pepecoinregtest;default:throw new Error(`Invalid network ${e}`)}default:throw new Error(`Invalid chain ${t}`)}}(M,C);const a=t.flatMap((t=>{const{zip:e,ownerData:a,onChainMetaData:r}=t;const{exp:n="",mod:o=""}=r;return t.tx.outs.map((({script:r,value:c},u)=>{const l=up.fromOutputScript(r,s))),satoshis:Math.round(c),scriptPubKey:r.toString("hex"),isTbcOutput:l,publicKeys:l?a[u]._owners:[],mod:l?o:"",previous:l?e[u][0]:null,hash:l?i.sha256(Buffer.from(n||"")).toString("hex"):null,blockHash:null}}))}));if(await Xt.insert(a),e){const t=a.map((({rev:t})=>t));await Xt.updateBlockHash(t,e)}};static eraseBlockHash=async t=>{await Xt.eraseBlockHash(t)};static listSentOutputs=async t=>Xt.listSentOutputs(t);static listReceivedOutputs=async t=>Xt.listReceivedOutputs(t);static listTxs=async t=>Xt.listTxs(t);static getLatestRev=async t=>Xt.getLatestRev(t);static getLatestRevs=async t=>Xt.getLatestRevs(t);static getIdByRev=async t=>Xt.getIdByRev(t);static query=async t=>Xt.query(t)}class te{static get=async t=>Wt.getTransaction(t);static getRaw=async t=>Wt.getBulkTransactions(t);static getRawJSON=async t=>Wt.getRawTransactionsJSON(t);static sendRaw=async t=>Wt.sendRawTransaction(t);static getUtxos=async t=>Wt.getUtxos(t);static waitForRpcBlockHash=async(t,e)=>Wt.waitForRpcBlockHash(t,e);static insertRpcBlock=async(t,e,s="LTC")=>{const{result:a}=await Wt.getBlock(t,2);const{tx:r}=a;let n=r;"LTC"===s&&(n=r.filter((t=>"08"!==t.hex.slice(10,12))));const o=`[wid ${e} pid: ${process.pid}: backfilling height ${a.height} - backfilling ${n.length} txs `;"LTC"===s&&o.concat(`(${r.length-n.length} mweb tx's filtered)...`),dt.info(o);const i=[];for(const t of n)try{let{hex:e}=t;e||(e=(await Wt.getRawTransaction(t.txid,1)).hex);const s=O.txFromHex({hex:e});s&&i.push(s)}catch(s){dt.error(`[wid ${e} pid: ${process.pid}: failed to parse transaction in block ${a.height}\n error message: ${s.message}\n transaction: ${JSON.stringify(t)}`)}try{await Qt.insert(i,t),await qt.insert(i,t)}catch(t){dt.error(`[wid ${e} pid: ${process.pid}: inserting inputs and outputs for block ${a.height} failed with error '${t.message}'`)}};static walletSetup=async()=>Wt.walletSetup()}const ee={protocol:et,user:Z,pass:X,host:Q,port:parseInt(tt,10)};const se=new R(ee);const ae={};const re=JSON.parse(JSON.stringify(R.callspec));Object.keys(re).forEach((t=>{re[t.toLowerCase()]=re[t]}));const ne={str:t=>t.toString(),string:t=>t.toString(),int:t=>parseFloat(t),float:t=>parseFloat(t),bool:t=>!0===t||"1"===t||1===t||"true"===t||"true"===t.toString().toLowerCase(),obj:t=>"string"==typeof t?JSON.parse(t):t};try{Object.keys(R.prototype).forEach((t=>{if(t&&"function"==typeof R.prototype[t]){const e=t.toLowerCase();ae[t]=S.promisify(R.prototype[t].bind(se)),ae[e]=S.promisify(R.prototype[e].bind(se))}}))}catch(t){dt.error(`Error occurred while binding RPC methods: ${t.message}`)}const oe=t=>new Promise((e=>setTimeout(e,t)));const ie=$(o);const ce=c.regtest;class ue{static rawTxSubscriber=async t=>{const e=t.toString("hex");if(dt.info(`ZMQ message { rawTx:${e} }`),"08"!==e.slice(10,12))try{const t=O.txFromHex({hex:e});await Qt.insert([t]),await qt.insert([t])}catch(t){dt.error(`Error parsing transaction ${e} ${t.message} ${t.stack}`)}};static sub=async t=>{try{await Wt.createWallet(),"regtest"!==C&&await Wt.checkBlockchainProgress(),await Wt.walletSetup(),dt.info(`Bitcoin Computer Node ${ht} is ready. MAX_BLOCKCHAIN_HEIGHT: 2538171`);for await(const[,e]of t)await this.rawTxSubscriber(e)}catch(t){dt.error(`ZMQ subscription failed with error '${t.message}'`)}}}const{PreparedStatement:le}=f;class pe{static async select(t){const e=new le({name:`User.select.${Math.random()}`,text:'SELECT "publicKey", "clientTimestamp" FROM "User" WHERE "publicKey" = $1',values:[t]});const s=await wt.oneOrNone(e);return s?{publicKey:s.publicKey,clientTimestamp:parseInt(s.clientTimestamp,10)||0}:null}static async insert({publicKey:t,clientTimestamp:e}){const s=new le({name:`User.insert.${Math.random()}`,text:'INSERT INTO "User"("publicKey", "clientTimestamp") VALUES ($1, $2)',values:[t,e]});await wt.none(s)}static async update({publicKey:t,clientTimestamp:e}){const s=new le({name:`User.update.${Math.random()}`,text:'UPDATE "User" SET "clientTimestamp"=$1 WHERE "publicKey"=$2',values:[e,t]});await wt.none(s)}}class de{static async select(t){return pe.select(t)}static async insert(t){return pe.insert(t)}static async update(t){return pe.update(t)}}const{ec:me}=I;const he=new me("secp256k1");const ye=s();const ge=new class{configFile;loaded=!1;load=()=>{try{const t="dev"===z?"bcn.test.config.json":"bcn.config.json";const e=N(B(import.meta.url));this.configFile=T.readFileSync(x.join(e,"..","..",t)),this.loaded=!0}catch(t){if(t.message.includes("ENOENT: no such file or directory"))return void(this.loaded=!0);throw dt.error(`Access-list failed with error '${t.message}'`),t}};middleware=({url:t},e,s)=>{if(void 0!==e.locals.authToken)if(this.loaded||(dt.warn("Access-list failed with error 'AccessList not loaded.'. Loading now."),this.load()),void 0!==this.configFile)try{const{blacklist:t,whitelist:a}=JSON.parse(this.configFile.toString());if(t&&a)return void e.status(403).json({error:"Cannot enforce blacklist and whitelist at the same time."});const{publicKey:r}=e.locals.authToken;if(a&&!a.includes(r)||t&&t.includes(r))return void e.status(403).json({error:`Public key ${r} is not allowed.`});s()}catch(s){dt.error(`Authorization failed at ${t} with error: '${s.message}'`),e.status(403).json({error:s.message})}else s();else s()}};let we;h(o);try{we=a.createServer(ye)}catch(t){throw dt.error(`Starting server failed with error '${t.message}'`),t}if(dt.info(`Server listening on port ${H}`),ye.use(e()),"true"===D){const t=n({windowMs:parseInt(W,10),max:parseInt(K,10),standardHeaders:"true"===Y,legacyHeaders:"true"===G});ye.use(t)}ye.use(t.json({limit:"100mb"})),ye.use(t.urlencoded({limit:"100mb",extended:!0})),ye.get("/",((t,e)=>e.status(200).send(`\n

Bitcoin Computer Node

\n Status: Healthy
\n Version: ${ht}
\n Chain: ${M}
\n Network: ${C}\n `))),ge.loaded&&(ye.use((async(t,e,s)=>{try{const a=t.get("Authentication");if(!a){const{method:s,url:a}=t;const r=`Auth failed with error 'no Authentication key provided' ${s} ${t.get("Host")} ${a}`;return dt.error(r),void e.status(401).json({error:r})}const r=(t=>{const e=t.split(" ");if(2!==e.length||"Bearer"!==e[0])throw new Error("Authentication header is invalid.");const s=Buffer.from(e[1],"base64").toString().split(":");if(3!==s.length)throw new Error;return{signature:s[0],publicKey:s[1],timestamp:parseInt(s[2],10)}})(a);const{signature:n,publicKey:o,timestamp:i}=r;if(Date.now()-i>18e4)return void e.status(401).json({error:"Signature is too old."});const c=b.sha256().update(V+i).digest("hex");if(!he.keyFromPublic(o,"hex").verify(c,n)){const t="The origin and public key pair doesn't match the signature.";return void e.status(401).json({error:t})}const u=await de.select(o);if(u){if(u.clientTimestamp>=i)return void e.status(401).json({error:"Please use a fresh authentication token."});await de.update({publicKey:o,clientTimestamp:i})}else await de.insert({publicKey:o,clientTimestamp:i});e.locals.authToken=r,s()}catch(t){dt.error(`Auth failed with error '${t.message}'`),e.status(401).json({error:t.message})}})),ye.use(ge.middleware));const fe=(()=>{const t=s.Router();return t.get("/wallet/:address/utxos",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await St.selectByAddress(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/sent-outputs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listSentOutputs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/received-outputs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listReceivedOutputs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/list-txs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listTxs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/non-standard-utxos",(async(t,e)=>{try{const s=new URLSearchParams(t.url.split("?")[1]);const a={mod:s.get("mod"),publicKey:s.get("publicKey"),limit:s.get("limit"),order:s.get("order"),offset:s.get("offset"),ids:JSON.parse(s.get("ids"))};const r=await Qt.query(a);e.status(200).json(r)}catch(s){dt.error(`GET ${t.url} failed with error '${s.messages}'`),e.status(500).json({error:s.message})}})),t.get("/address/:address/balance",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await St.getBalance(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message||t}'`),s.status(500).json({error:t.message})}})),t.post("/tx/bulk",(async({body:{txIds:t},url:e},s)=>{try{if(void 0===t||0===t.length)return void s.status(400).json({error:"Missing input txIds."});const e=await te.getRaw(t);e?s.status(200).json(e):s.status(404).json({error:"Not found"})}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/tx/post",(async({body:{hex:t},url:e},s)=>{try{if(!t)return void s.status(400).json({error:"Missing input hex."});const e=await te.sendRaw(t);e?s.status(200).json(e):s.status(404).json({error:"Error Occured"})}catch(a){dt.error(`POST ${e} failed with error '${a.message}\ntxHex: ${t}`),s.status(500).json({error:a.message})}})),t.get("/mine",(async({query:{count:t},url:e},s)=>{try{const{result:e}=await ae.getnewaddress();if("string"!=typeof t)throw new Error("Please provide appropriate count");return await ae.generatetoaddress(parseInt(t,10)||1,e),s.status(200).json({success:!0})}catch(t){return dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/:id/height",(async({params:{id:t},url:e},s)=>{try{let e=t;if("best"===t){const{result:t}=await ae.getbestblockhash();e=t}const{result:a}=await ae.getblockheader(e,!0);return s.status(200).json({height:a.height})}catch(t){return dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/faucet",(async({body:{address:t,value:e},url:s},a)=>{try{const s=parseInt(e,10)/1e8;const{result:r}=await ae.sendtoaddress(t,s);await ae.generateToAddress(1,"mvFeNF9DAR7WMuCpBPbKuTtheihLyxzj8i");const{result:n}=await ae.getrawtransaction(r,1);const o=n.vout.findIndex((t=>1e8*t.value===parseInt(e,10)));return a.status(200).json({txId:r,vout:o,height:-1,satoshis:e})}catch(t){return dt.error(`POST ${s} failed with error '${t.message}'`),a.status(500).json({error:t.message})}})),t.post("/faucetScript",(async({body:{script:t,value:e},url:s},a)=>{try{const s=ie.makeRandom({network:ce});const r=d.p2pkh({pubkey:s.publicKey,network:ce});const{address:n}=r;const o=(await ae.sendtoaddress(n,2*parseInt(e,10)/1e8,"","")).result;let i;let c=10;for(;!i;)if(i=(await St.selectByAddress(n)).filter((t=>t.txId===o))[0],!i){if(c-=1,c<=0)throw new Error("No outputs");await oe(10)}const u=(await ae.getrawtransaction(i.txId,1)).result;const l=new m({network:ce});l.addInput({hash:i.txId,index:i.vout,nonWitnessUtxo:Buffer.from(u.hex,"hex")}),l.addOutput({script:Buffer.from(t,"hex"),value:parseInt(e,10)}),l.signInput(0,s),l.finalizeAllInputs();const p=l.extractTransaction();let h;for(await ae.sendrawtransaction(p.toHex()),c=5;!h;)if(h=(await St.selectByScriptHex(t)).filter((t=>t.txId===p.getId()))[0],!h){if(c-=1,c<=0)throw new Error("No outputs");await oe(10)}return a.status(200).json({txId:p.getId(),vout:h.vout,height:-1,satoshis:h.satoshis})}catch(t){return dt.error(`POST ${s} failed with error '${t.message}'`),a.status(500).json({error:t.message})}})),t.get("/tx/:txId/json",(async({params:{txId:t},url:e},s)=>{try{if(!t)return void s.status(400).json({error:"Missing input txId."});const e=await te.getRawJSON(t);e?s.status(200).json(e):s.status(404).json({error:"Not found"})}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/revs",(async({body:{ids:t},url:e},s)=>{try{if(void 0===t||0===t.length)return void s.status(400).json({error:"Missing input object ids."});const e=await Qt.getLatestRevs(t);s.status(200).json(e)}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/revToId",(async({body:{rev:t},url:e},s)=>{try{if(!Jt(t))return void s.status(400).json({error:"Invalid rev id"});const e=await Qt.getIdByRev(t);e&&s.status(200).json(e),s.status(404).json()}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/rpc",(async({body:t,url:e},s)=>{try{if(!t||!t.method)throw new Error("Please provide appropriate RPC method name");if(!new RegExp(P).test(t.method))throw new Error("Method is not allowed");const e=function(t,e){if(void 0===re[t]||null===re[t])throw new Error("This RPC method does not exist, or not supported");const s=e.trim().split(" ");const a=re[t].trim().split(" ");if(0===e.trim().length&&0!==re[t].trim().length)throw new Error(`Too few params provided. Expected ${a.length} Provided 0`);if(0!==e.trim().length&&0===re[t].trim().length)throw new Error(`Too many params provided. Expected 0 Provided ${s.length}`);if(s.lengtha.length)throw new Error(`Too many params provided. Expected ${a.length} Provided ${s.length}`);return 0===e.length?[]:s.map(((t,e)=>ne[a[e]](t)))}(t.method,t.params);const a=e.length?await ae[t.method](...e):await ae[t.method]();s.status(200).json({result:a})}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/non-standard-utxo",(async(t,e)=>{e.status(500).json({error:"Please upgrade to @bitcoin-computer/lib to the latest version."})})),t})();ye.use(`/v1/${M}/${C}`,fe),ye.use("/v1/store",Tt),we.listen(H,(()=>{dt.info(`\nStarted Bitcoin Computer Node Version ${ht}\nPORT ${H} \n`)})).on("error",(t=>{dt.error(t.message),process.exit(1)}));const Ee=new r.Subscriber;Ee.connect(k),Ee.subscribe("rawtx"),dt.info(`ZMQ Subscriber connected to ${k}`),(async()=>{await(async()=>{await v((()=>wt.connect()),{startingDelay:500})})(),await ue.sub(Ee)})(); +import t from"body-parser";import e from"cors";import s from"express";import a from"http";import*as r from"zeromq";import n from"express-rate-limit";import*as o from"@bitcoin-computer/secp256k1";import{crypto as i,networks as c,bufferUtils as u,Transaction as l,address as p,payments as d,Psbt as m,initEccLib as h}from"@bitcoin-computer/nakamotojs";import y from"dotenv";import g from"winston";import w from"winston-daily-rotate-file";import f from"pg-promise";import E from"pg-monitor";import{backOff as v}from"exponential-backoff";import $ from"fs";import{ECPairFactory as T}from"ecpair";import{Computer as O}from"@bitcoin-computer/lib";import R from"bitcoind-rpc";import S from"util";import I from"elliptic";import b from"hash.js";import x,{dirname as N}from"path";import{fileURLToPath as B}from"url";y.config();const M=process.env.BCN_CHAIN;const C=process.env.BCN_NETWORK;const{BCN_PORT:H}=process.env;const{BCN_ZMQ_URL:k}=process.env;const{BCN_ALLOWED_RPC_METHODS:P}=process.env;const{BCN_DEBUG_MODE:L}=process.env;const{BCN_LOG_MAX_FILES:A}=process.env;const{BCN_LOG_MAX_SIZE:_}=process.env;const{BCN_LOG_ZIP:j}=process.env;const{BCN_SHOW_CONSOLE_LOGS:U}=process.env;const{BCN_SHOW_DB_LOGS:F}=process.env;const{BCN_RATE_LIMIT_ENABLED:D}=process.env;const{BCN_RATE_LIMIT_WINDOW:W}=process.env;const{BCN_RATE_LIMIT_MAX:K}=process.env;const{BCN_RATE_LIMIT_STANDARD_HEADERS:Y}=process.env;const{BCN_RATE_LIMIT_LEGACY_HEADERS:G}=process.env;process.env,process.env;const{BCN_OFFCHAIN_PROTOCOL:q}=process.env;const J=process.env.BCN_QUERY_LIMIT||"1000";const V=process.env.BCN_URL||`http://127.0.0.1:${H}`;const z=process.env.BCN_ENV||"dev";const{BITCOIN_RPC_USER:Z}=process.env;const{BITCOIN_RPC_PASSWORD:X}=process.env;const{BITCOIN_RPC_HOST:Q}=process.env;const{BITCOIN_RPC_PORT:tt}=process.env;const{BITCOIN_RPC_PROTOCOL:et}=process.env;const{BITCOIN_DEFAULT_WALLET:st}=process.env;const{POSTGRES_USER:at}=process.env;const{POSTGRES_PASSWORD:rt}=process.env;const{POSTGRES_DB:nt}=process.env;const{POSTGRES_HOST:ot}=process.env;const{POSTGRES_PORT:it}=process.env;g.addColors({error:"red",warn:"yellow",info:"green",http:"magenta",debug:"white"});const ct=g.format.combine(g.format.colorize(),g.format.timestamp({format:"YYYY-MM-DD HH:mm:ss:ms"}),g.format.json(),g.format.printf((t=>`${t.timestamp} [${t.level.slice(5).slice(0,-5)}] ${t.message}`)));const ut={zippedArchive:"true"===j,maxSize:_,maxFiles:A,dirname:"logs"};const lt=[];"true"===U&<.push(new g.transports.Console({format:g.format.combine(g.format.colorize(),g.format.timestamp({format:"MM-DD-YYYY HH:mm:ss"}),g.format.printf((t=>`${t.timestamp} ${t.level} ${t.message}`)))}));const pt=parseInt(L,10);pt>=0&<.push(new w({filename:"error-%DATE%.log",datePattern:"YYYY-MM-DD",level:"error",...ut})),pt>=1&<.push(new w({filename:"warn-%DATE%.log",datePattern:"YYYY-MM-DD",level:"warn",...ut})),pt>=2&<.push(new w({filename:"info-%DATE%.log",datePattern:"YYYY-MM-DD",level:"info",...ut})),pt>=3&<.push(new w({filename:"http-%DATE%.log",datePattern:"YYYY-MM-DD",level:"http",...ut})),pt>=4&<.push(new w({filename:"debug-%DATE%.log",datePattern:"YYYY-MM-DD",level:"debug",...ut}));const dt=g.createLogger({levels:{error:0,warn:1,info:2,http:3,debug:4},format:ct,transports:lt,exceptionHandlers:[new g.transports.File({filename:"logs/exceptions.log"})],rejectionHandlers:[new g.transports.File({filename:"logs/rejections.log"})]});y.config();const{version:mt}=JSON.parse($.readFileSync("package.json","utf8"));const ht=mt||process.env.BCN_SERVER_VERSION;const yt=parseInt(process.env.MWEB_HEIGHT||"",10)||432;const gt={error:(t,e)=>{if(e.cn){const{host:s,port:a,database:r,user:n,password:o}=e.cn;dt.debug(`Waiting for db to start { message:${t.message} host:${s}, port:${a}, database:${r}, user:${n}, password: ${o}`)}},noWarnings:!0};"true"===F&&(E.isAttached()?E.detach():(E.attach(gt),E.setTheme("matrix")));const wt=f(gt)({host:ot,port:parseInt(it,10),database:nt,user:at,password:rt,allowExitOnIdle:!0,idleTimeoutMillis:100});const{PreparedStatement:ft}=f;class Et{static async select(t){const e=new ft({name:`OffChain.select.${Math.random()}`,text:'SELECT "data" FROM "OffChain" WHERE "id" = $1',values:[t]});return wt.oneOrNone(e)}static async insert({id:t,data:e}){const s=new ft({name:`OffChain.insert.${Math.random()}`,text:'INSERT INTO "OffChain" ("id", "data") VALUES ($1, $2) ON CONFLICT DO NOTHING',values:[t,e]});return wt.none(s)}static async delete(t){const e=new ft({name:`OffChain.delete.${Math.random()}`,text:'WITH deleted AS (DELETE FROM "OffChain" WHERE "id" = $1 RETURNING *) SELECT count(*) FROM deleted;',values:[t]});return(await wt.any(e))[0].count>0}}class vt{static async select(t){const e=await Et.select(t);return e?.data||null}static async insert(t){return Et.insert(t)}static async delete(t){return Et.delete(t)}}const $t=s.Router();$t.get("/:id",(async({params:{id:t},url:e},s)=>{try{const e=await vt.select(t);e?s.status(200).json(e):s.status(403).json({error:"No entry found."})}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),$t.post("/",(async(t,e)=>{const{body:{data:s},url:a}=t;try{const a=i.sha256(Buffer.from(s)).toString("hex");await vt.insert({id:a,data:s});const r=`${q||t.protocol}://${t.get("host")}/store/${a}`;e.status(201).json({_url:r})}catch(t){dt.error(`POST ${a} failed with error '${t.message}'`),e.status(500).json({error:t.message})}})),$t.delete("/:id",(async(t,e)=>{e.status(500).json({error:"Deletions are not supported yet."})}));const{PreparedStatement:Tt}=f;class Ot{static async getBalance(t){const e=new Tt({name:`Utxos.getBalance.${Math.random()}`,text:'SELECT sum("satoshis") as "satoshis" FROM "Utxos" WHERE "address" = $1 and "blockHash" is not null',values:[t]});const s=await wt.oneOrNone(e);const a=new Tt({name:`Utxos.getBalance.${Math.random()}`,text:'SELECT sum("satoshis") as "satoshis" FROM "Utxos" WHERE "address" = $1 and "blockHash" is null',values:[t]});const r=await wt.oneOrNone(a);return{confirmed:parseInt(s.satoshis,10)||0,unconfirmed:parseInt(r.satoshis,10)||0,balance:(parseInt(s.satoshis,10)||0)+(parseInt(r.satoshis,10)||0)}}static async select(t){const e=new Tt({name:`Utxos.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout" FROM "Utxos" WHERE "address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)||0})))}static async selectByScriptHex(t){const e=new Tt({name:`Utxos.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout" FROM "Utxos" WHERE "scriptPubKey" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)||0})))}static async selectByPk(t){const e=new Tt({name:`Utxos.selectByPk.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", split_part(rev, \':\', 1) AS "txId", cast(split_part(rev, \':\', 2) as INTEGER) AS "vout", "publicKeys" FROM "Utxos" WHERE $1 = ANY ("publicKeys")',values:[t]});return(await wt.any(e)).map((t=>({...t,satoshis:parseInt(t.satoshis,10)})))}}class Rt{static async getBalance(t){return Ot.getBalance(t)}static async select(t){return Ot.select(t)}static async selectByScriptHex(t){return Ot.selectByScriptHex(t)}static async selectByPk(t){return Ot.selectByPk(t)}}class St{static getBalance=async t=>Rt.getBalance(t);static selectByAddress=async t=>Rt.select(t);static selectByScriptHex=async t=>Rt.selectByScriptHex(t);static selectByPk=async t=>Rt.selectByPk(t)}const It={protocol:et,user:Z,pass:X,host:Q,port:parseInt(tt,10)};const bt=new R(It);const xt=S.promisify(R.prototype.createwallet.bind(bt));const Nt=S.promisify(R.prototype.generateToAddress.bind(bt));const Bt=S.promisify(R.prototype.getaddressinfo.bind(bt));const Mt=S.promisify(R.prototype.getBlock.bind(bt));const Ct=S.promisify(R.prototype.getBlockchainInfo.bind(bt));const Ht=S.promisify(R.prototype.getBlockHash.bind(bt));const kt=S.promisify(R.prototype.getRawTransaction.bind(bt));const Pt=S.promisify(R.prototype.getRawTransaction.bind(bt));const Lt=S.promisify(R.prototype.getTransaction.bind(bt));const At=S.promisify(R.prototype.getNewAddress.bind(bt));const _t={createwallet:xt,generateToAddress:Nt,getaddressinfo:Bt,getBlock:Mt,getBlockchainInfo:Ct,getBlockHash:Ht,getRawTransaction:kt,getTransaction:Lt,importaddress:S.promisify(R.prototype.importaddress.bind(bt)),invalidateBlock:S.promisify(R.prototype.invalidateBlock.bind(bt)),listunspent:S.promisify(R.prototype.listunspent.bind(bt)),sendRawTransaction:S.promisify(R.prototype.sendRawTransaction.bind(bt)),getNewAddress:At,sendToAddress:S.promisify(R.prototype.sendToAddress.bind(bt)),getRawTransactionJSON:Pt};const jt=(t,e)=>{const s=[];for(let a=0;a{const e=[];for(let s=1;s<=t;s+=3){const t=`($${s},$${s+1},$${s+2})`;e.push(t)}return e.join(",")};const Ft=t=>{const e=[];for(let s=1;s<=t;s+=10){const t=`($${s},$${s+1},$${s+2},$${s+3},$${s+4},$${s+5},$${s+6},$${s+7},$${s+8},$${s+9})`;e.push(t)}return e.join(",")};const Dt=t=>{try{return t()}catch{return null}};class Wt{static async getTransaction(t){const{result:e}=await _t.getTransaction(t);return e}static async getBulkTransactions(t){return(await Promise.all(t.map((t=>_t.getRawTransaction(t,0))))).map((t=>t.result))}static async getRawTransaction(t,e){const{result:s}=await _t.getRawTransaction(t,e);return s}static async getRawTransactionsJSON(t){return{txId:(e=(await _t.getRawTransactionJSON(t,1)).result).txid,txHex:e.hex,vsize:e.vsize,version:e.version,locktime:e.locktime,ins:e.vin.map((t=>t.coinbase?{coinbase:t.coinbase,sequence:t.sequence}:{txId:t.txid,vout:t.vout,script:t.scriptSig.hex,sequence:t.sequence})),outs:e.vout.map((t=>{let e;return t.scriptPubKey.addresses?[e]=t.scriptPubKey.addresses:e=t.scriptPubKey.address?t.scriptPubKey.address:void 0,{address:e,script:t.scriptPubKey.hex,value:Math.round(1e8*t.value)}}))};var e}static async sendRawTransaction(t){const{result:e,error:s}=await _t.sendRawTransaction(t);if(s)throw dt.error(s),new Error("Error sending transaction");return e}static getUtxos=async t=>(void 0===(await _t.getaddressinfo(t)).result.timestamp&&(dt.info(`Importing address: ${t}`),await _t.importaddress(t,!1)),(await _t.listunspent(0,999999,[t])).result);static waitForRpcBlockHash=async(t,e)=>(await v((async()=>{let s;try{s=await _t.getBlockHash(t)}catch(s){throw dt.info(`[wid ${e} pid: ${process.pid}]: waiting for RPC to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).result;static getBlock=async(t,e)=>_t.getBlock(t,e);static walletSetup=async()=>{if("regtest"===C){if(dt.info(`Node is starting for chain ${M} and network ${C}, \n\n. Starting Wallet setup.`),"LTC"===M){const{result:t}=await _t.getBlockchainInfo();const e=t.blocks;if(e{try{await _t.createwallet(st,!1,!1,"",!1,!1)}catch(t){if(t.message.includes("already exists"))return void dt.info(`Wallet ${st} already exists`);dt.error(`Wallet creation failed with error '${t.message}'`)}};static checkBlockchainProgress=async()=>{const t=await v((async()=>{const t=await _t.getBlockchainInfo();const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const{blocks:s}=t.result;if(dt.info(`Zmq. Bitcoind { percentage:${e}%, blocks:${s} }`),parseFloat(t.result.verificationprogress)<=.7)throw new Error("Node not ready yet");return t}),{startingDelay:6e4,timeMultiple:1,numOfAttempts:8760});const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const s=t.result.blocks;dt.info(`BCN reaches sync end...at { bitcoind.progress:${e}%, bitcoindSyncedHeight:${s} }`)}}const{PreparedStatement:Kt}=f;class Yt{static async select(t){const e=new Kt({name:`Input.select.${Math.random()}`,text:'SELECT "outputSpent", "spendingInput", "blockHash" FROM "Input" WHERE "outputSpent" = $1',values:[t]});return wt.any(e)}static async insert(t){await Promise.all(jt(t,3333).map((t=>{const e=t.flatMap((({outputSpent:t,spendingInput:e,blockHash:s})=>[t,e,s]));return wt.none(new Kt({name:`Input.insert.${Math.random()}`,text:`INSERT INTO "Input"("outputSpent", "spendingInput", "blockHash") VALUES ${Ut(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async updateBlockHash(t,e){await Promise.all(jt(t,1e4).map((t=>{const s=t.join("','");return wt.none(new Kt({name:`Input.updateBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = $1 WHERE "spendingInput" IN ('${s}')`,values:[e]}))})))}static async eraseBlockHash(t){await Promise.all(jt(t,1e4).map((t=>{const e=t.join("','");return wt.none(new Kt({name:`Input.eraseBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async count(t){const e=t.map((t=>t.outputSpent));const s=new Kt({name:`Input.belong.${Math.random()}`,text:'SELECT count(*) FROM "Input" WHERE "outputSpent" LIKE ANY ($1)',values:[[e]]});const a=await wt.oneOrNone(s);return parseInt(a?.count,10)||0}}class Gt{static async select(t){return Yt.select(t)}static async insert(t){return Yt.insert(t)}static async updateBlockHash(t,e){return Yt.updateBlockHash(t,e)}static async eraseBlockHash(t){return Yt.eraseBlockHash(t)}}class qt{static insert=async(t,e=null)=>{const s=t.flatMap((t=>t.tx.ins.map(((e,s)=>({input:e,index:s,txId:t.txId}))))).filter((({input:t})=>!l.isCoinbaseHash(t.hash))).map((({input:t,index:e,txId:s})=>{return{outputSpent:`${a=t.hash,u.reverseBuffer(Buffer.from(a)).toString("hex")}:${t.index}`,spendingInput:`${s}:${e}`,blockHash:null};var a}));if(await Gt.insert(s),e){const t=s.map((({spendingInput:t})=>t));await Gt.updateBlockHash(t,e)}};static select=async t=>Gt.select(t);static updateBlockHash=async(t,e)=>{await Gt.updateBlockHash(t,e)};static eraseBlockHash=async t=>{await Gt.eraseBlockHash(t)}}function Jt(t){return/^[0-9A-Fa-f]{64}:\d+$/.test(t)}function Vt(t){if(!Jt(t))throw new Error("Invalid rev")}const{PreparedStatement:zt}=f;class Zt{static async listSentOutputs(t){const e=new zt({name:`Output.listSentTxs.${Math.random()}`,text:'SELECT "Input"."spendingInput" AS "output", "Output"."satoshis" AS "amount"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output"."address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listReceivedOutputs(t){const e=new zt({name:`Output.listReceivedTxs.${Math.random()}`,text:'SELECT "Output"."rev" as "output", "Output"."satoshis" as "amount" FROM "Output" WHERE "address" = $1',values:[t]});return(await wt.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listTxs(t){const e=new zt({name:`Output.listTxs.${Math.random()}`,text:'WITH\n -- List all txs sent from a given address\n SENT AS (\n SELECT split_part("Input"."spendingInput",\':\',1) as "txId", SUM("Output".satoshis) as "satoshis"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output".address = $1\n GROUP BY split_part("Input"."spendingInput",\':\',1)\n ),\n -- List all tx received from a given address\n RECEIVED AS (\n SELECT SPLIT_PART("Output"."rev",\':\',1) as "txId", SUM("Output"."satoshis") as "satoshis" \n FROM "Output" \n WHERE "address" = $1\n GROUP BY "txId"\n )\n\n SELECT\n RECEIVED."txId", \n coalesce(SENT."satoshis", 0) as "inputsSatoshis", \n coalesce(RECEIVED."satoshis", 0) as "outputsSatoshis", \n coalesce(RECEIVED."satoshis",0) - coalesce(SENT."satoshis",0) as "satoshis"\n FROM\n SENT RIGHT JOIN RECEIVED ON SENT."txId" = RECEIVED."txId";',values:[t]});const s=(await wt.any(e)).map((t=>({...t,inputsSatoshis:parseInt(t.inputsSatoshis,10)||0,outputsSatoshis:parseInt(t.outputsSatoshis,10)||0,satoshis:parseInt(t.satoshis,10)||0})));return{sentTxs:s.filter((t=>t.satoshis<0)).map((t=>({...t,satoshis:Math.abs(t.satoshis)}))),receivedTxs:s.filter((t=>t.satoshis>=0))}}static async select(t){const e=new zt({name:`Output.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", "publicKeys", "hash", "mod", "isTbcOutput", "previous", "blockHash" FROM "Output" WHERE "address" = $1',values:[t]});return wt.any(e)}static async insert(t){await Promise.all(jt(t,1e3).map((t=>{const e=t.flatMap((({rev:t,address:e,satoshis:s,scriptPubKey:a,isTbcOutput:r,publicKeys:n,mod:o,previous:i,hash:c,blockHash:u})=>[t,e,s,a,r,n,o,i,c,u]));return wt.none(new zt({name:`Output.insert.${Math.random()}`,text:`INSERT INTO "Output"("rev", "address", "satoshis", "scriptPubKey", "isTbcOutput",\n "publicKeys", "mod", "previous", "hash", "blockHash") VALUES ${Ft(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async eraseBlockHash(t){await Promise.all(jt(t,1e4).map((t=>{const e=t.join("','");return wt.none(new zt({name:`Output.eraseBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async updateBlockHash(t,e){await Promise.all(jt(t,1e4).map((t=>{const s=t.join("','");return wt.none(new zt({name:`Output.updateBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = $1 WHERE "rev" IN ('${s}')`,values:[e]}))})))}static async getIdByRev(t){const e=new zt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON r."previous" = o."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=(await wt.any(e)).filter((t=>null===t.previous));return s[0]?.rev}static async getIdsByRevs(t){return Promise.all(t.map((t=>this.getIdByRev(t))))}static async getLatestRev(t){const e=new zt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON o."previous" = r."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=await wt.any(e);const a=Object.fromEntries(s.map((t=>[t.previous,t.rev])));let r=t;for(;a[r];)r=a[r];return r}static async getLatestRevs(t){return Promise.all(t.map(this.getLatestRev))}static async getIdsByMod(t){const e=new zt({name:`Output.getIdsByMod.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1',values:[t]});return(await wt.any(e)).map((t=>t.rev))}static sqlSuffix(t,e,s){let a="";return s&&(a+=` order by "timestamp" ${s}`),a+=` limit ${t||J}`,e&&(a+=` offset ${e}`),a}static async getRevsByPublicKey(t){const e=new zt({name:`Output.getRevsByPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys")',values:[t]});return(await wt.any(e)).map((t=>t.rev))}static async getUnspentRevsByMod(t,e,s,a){const r=await this.getIdsByMod(t);const n=await this.getLatestRevs(r);const o=new zt({name:`Output.getUnspentRevsByMod.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(e,s,a)}`,values:[n]});return(await wt.any(o)).map((t=>t.rev))}static async getUnspentRevsByPublicKey(t,e,s,a){const r=new zt({name:`Output.getUnspentRevsByPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys") AND "isTbcOutput" = true \n AND NOT EXISTS (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") \n ${this.sqlSuffix(e,s,a)}`,values:[t]});return(await wt.any(r)).map((t=>t.rev))}static async getUnspentRevsByModAndPublicKey(t,e,s,a,r){const n=await this.getUnspentRevsByPublicKey(e,s,a,r);const o=await this.getIdsByRevs(n);const i=new zt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1 AND "rev" = ANY($2)',values:[t,o]});const c=(await wt.any(i)).map((t=>t.rev));const u=await this.getLatestRevs(c);const l=new zt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(s,a,r)}`,values:[u]});return(await wt.any(l)).map((t=>t.rev))}static async getUnspentTbcOutputs(t,e,s){const a=new zt({name:`Output.getUnspentTbcOutputs.${Math.random()}`,text:`SELECT "rev", "address", "satoshis", "scriptPubKey", "publicKeys", "timestamp"\n FROM "Output" WHERE "isTbcOutput" = true AND NOT EXISTS\n (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") ${this.sqlSuffix(t,e,s)}`});return(await wt.any(a)).map((t=>t.rev))}static async query(t){const{publicKey:e,limit:s,offset:a,ids:r,mod:n,order:o}=t;const i=parseInt(J||"",10);if(s&&parseInt(s||"",10)>i||r&&r.length>i)throw new Error(`Can't fetch more than ${J} revs.`);if(o&&"ASC"!==o&&"DESC"!==o)throw new Error("Invalid order. Should be ASC or DESC.");return r?(r.map(Vt),this.getLatestRevs(r)):n&&!e?this.getUnspentRevsByMod(n,s,a,o):!n&&e?this.getUnspentRevsByPublicKey(e,s,a,o):n&&e?this.getUnspentRevsByModAndPublicKey(n,e,s,a,o):this.getUnspentTbcOutputs(s,a,o)}}class Xt{static async select(t){return Zt.select(t)}static async insert(t){return Zt.insert(t)}static async eraseBlockHash(t){return Zt.eraseBlockHash(t)}static async updateBlockHash(t,e){return Zt.updateBlockHash(t,e)}static async listSentOutputs(t){return Zt.listSentOutputs(t)}static async listReceivedOutputs(t){return Zt.listReceivedOutputs(t)}static async listTxs(t){return Zt.listTxs(t)}static async getLatestRev(t){return Zt.getLatestRev(t)}static async getLatestRevs(t){return Zt.getLatestRevs(t)}static async getIdByRev(t){return Zt.getIdByRev(t)}static async query(t){return Zt.query(t)}}class Qt{static insert=async(t,e=null)=>{const s=function(t=M,e=C){switch(t){case"BTC":switch(e){case"mainnet":return c.bitcoin;case"testnet":return c.testnet;case"regtest":return c.regtest;default:throw new Error(`Invalid network ${e}`)}case"LTC":switch(e){case"mainnet":return c.litecoin;case"testnet":return c.litecointestnet;case"regtest":return c.litecoinregtest;default:throw new Error(`Invalid network ${e}`)}case"PEPE":switch(e){case"mainnet":return c.pepecoin;case"testnet":return c.pepecointestnet;case"regtest":return c.pepecoinregtest;default:throw new Error(`Invalid network ${e}`)}default:throw new Error(`Invalid chain ${t}`)}}(M,C);const a=t.flatMap((t=>{const{zip:e,ownerData:a,onChainMetaData:r}=t;const{exp:n="",mod:o=""}=r;return t.tx.outs.map((({script:r,value:c},u)=>{const l=up.fromOutputScript(r,s))),satoshis:Math.round(c),scriptPubKey:r.toString("hex"),isTbcOutput:l,publicKeys:l?a[u]._owners:[],mod:l?o:"",previous:l?e[u][0]:null,hash:l?i.sha256(Buffer.from(n||"")).toString("hex"):null,blockHash:null}}))}));if(await Xt.insert(a),e){const t=a.map((({rev:t})=>t));await Xt.updateBlockHash(t,e)}};static eraseBlockHash=async t=>{await Xt.eraseBlockHash(t)};static listSentOutputs=async t=>Xt.listSentOutputs(t);static listReceivedOutputs=async t=>Xt.listReceivedOutputs(t);static listTxs=async t=>Xt.listTxs(t);static getLatestRev=async t=>Xt.getLatestRev(t);static getLatestRevs=async t=>Xt.getLatestRevs(t);static getIdByRev=async t=>Xt.getIdByRev(t);static query=async t=>Xt.query(t)}class te{static get=async t=>Wt.getTransaction(t);static getRaw=async t=>Wt.getBulkTransactions(t);static getRawJSON=async t=>Wt.getRawTransactionsJSON(t);static sendRaw=async t=>Wt.sendRawTransaction(t);static getUtxos=async t=>Wt.getUtxos(t);static waitForRpcBlockHash=async(t,e)=>Wt.waitForRpcBlockHash(t,e);static insertRpcBlock=async(t,e,s="LTC")=>{const{result:a}=await Wt.getBlock(t,2);const{tx:r}=a;let n=r;"LTC"===s&&(n=r.filter((t=>"08"!==t.hex.slice(10,12))));const o=`[wid ${e} pid: ${process.pid}: backfilling height ${a.height} - backfilling ${n.length} txs `;"LTC"===s&&o.concat(`(${r.length-n.length} mweb tx's filtered)...`),dt.info(o);const i=[];for(const t of n)try{let{hex:e}=t;e||(e=(await Wt.getRawTransaction(t.txid,1)).hex);const s=O.txFromHex({hex:e});s&&i.push(s)}catch(s){dt.error(`[wid ${e} pid: ${process.pid}: failed to parse transaction in block ${a.height}\n error message: ${s.message}\n transaction: ${JSON.stringify(t)}`)}try{await Qt.insert(i,t),await qt.insert(i,t)}catch(t){dt.error(`[wid ${e} pid: ${process.pid}: inserting inputs and outputs for block ${a.height} failed with error '${t.message}'`)}};static walletSetup=async()=>Wt.walletSetup()}const ee={protocol:et,user:Z,pass:X,host:Q,port:parseInt(tt,10)};const se=new R(ee);const ae={};const re=JSON.parse(JSON.stringify(R.callspec));Object.keys(re).forEach((t=>{re[t.toLowerCase()]=re[t]}));const ne={str:t=>t.toString(),string:t=>t.toString(),int:t=>parseFloat(t),float:t=>parseFloat(t),bool:t=>!0===t||"1"===t||1===t||"true"===t||"true"===t.toString().toLowerCase(),obj:t=>"string"==typeof t?JSON.parse(t):t};try{Object.keys(R.prototype).forEach((t=>{if(t&&"function"==typeof R.prototype[t]){const e=t.toLowerCase();ae[t]=S.promisify(R.prototype[t].bind(se)),ae[e]=S.promisify(R.prototype[e].bind(se))}}))}catch(t){dt.error(`Error occurred while binding RPC methods: ${t.message}`)}const oe=t=>new Promise((e=>setTimeout(e,t)));const ie=T(o);const ce=c.regtest;class ue{static rawTxSubscriber=async t=>{const e=t.toString("hex");if(dt.info(`ZMQ message { rawTx:${e} }`),"08"!==e.slice(10,12))try{const t=O.txFromHex({hex:e});await Qt.insert([t]),await qt.insert([t])}catch(t){dt.error(`Error parsing transaction ${e} ${t.message} ${t.stack}`)}};static sub=async t=>{try{dt.info(`Bitcoin Computer Node ${ht} is starting at chain ${M} and network ${C}`),await Wt.createWallet(),"regtest"!==C&&await Wt.checkBlockchainProgress(),await Wt.walletSetup(),dt.info(`Bitcoin Computer Node ${ht} is ready. MAX_BLOCKCHAIN_HEIGHT: 2538171`);for await(const[,e]of t)await this.rawTxSubscriber(e)}catch(t){dt.error(`ZMQ subscription failed with error '${t.message}'`)}}}const{PreparedStatement:le}=f;class pe{static async select(t){const e=new le({name:`User.select.${Math.random()}`,text:'SELECT "publicKey", "clientTimestamp" FROM "User" WHERE "publicKey" = $1',values:[t]});const s=await wt.oneOrNone(e);return s?{publicKey:s.publicKey,clientTimestamp:parseInt(s.clientTimestamp,10)||0}:null}static async insert({publicKey:t,clientTimestamp:e}){const s=new le({name:`User.insert.${Math.random()}`,text:'INSERT INTO "User"("publicKey", "clientTimestamp") VALUES ($1, $2)',values:[t,e]});await wt.none(s)}static async update({publicKey:t,clientTimestamp:e}){const s=new le({name:`User.update.${Math.random()}`,text:'UPDATE "User" SET "clientTimestamp"=$1 WHERE "publicKey"=$2',values:[e,t]});await wt.none(s)}}class de{static async select(t){return pe.select(t)}static async insert(t){return pe.insert(t)}static async update(t){return pe.update(t)}}const{ec:me}=I;const he=new me("secp256k1");const ye=s();const ge=new class{configFile;loaded=!1;load=()=>{try{const t="dev"===z?"bcn.test.config.json":"bcn.config.json";const e=N(B(import.meta.url));this.configFile=$.readFileSync(x.join(e,"..","..",t)),this.loaded=!0}catch(t){if(t.message.includes("ENOENT: no such file or directory"))return void(this.loaded=!0);throw dt.error(`Access-list failed with error '${t.message}'`),t}};middleware=({url:t},e,s)=>{if(void 0!==e.locals.authToken)if(this.loaded||(dt.warn("Access-list failed with error 'AccessList not loaded.'. Loading now."),this.load()),void 0!==this.configFile)try{const{blacklist:t,whitelist:a}=JSON.parse(this.configFile.toString());if(t&&a)return void e.status(403).json({error:"Cannot enforce blacklist and whitelist at the same time."});const{publicKey:r}=e.locals.authToken;if(a&&!a.includes(r)||t&&t.includes(r))return void e.status(403).json({error:`Public key ${r} is not allowed.`});s()}catch(s){dt.error(`Authorization failed at ${t} with error: '${s.message}'`),e.status(403).json({error:s.message})}else s();else s()}};let we;h(o);try{we=a.createServer(ye)}catch(t){throw dt.error(`Starting server failed with error '${t.message}'`),t}if(dt.info(`Server listening on port ${H}`),ye.use(e()),"true"===D){const t=n({windowMs:parseInt(W,10),max:parseInt(K,10),standardHeaders:"true"===Y,legacyHeaders:"true"===G});ye.use(t)}ye.use(t.json({limit:"100mb"})),ye.use(t.urlencoded({limit:"100mb",extended:!0})),ye.get("/",((t,e)=>e.status(200).send(`\n

Bitcoin Computer Node

\n Status: Healthy
\n Version: ${ht}
\n Chain: ${M}
\n Network: ${C}\n `))),ge.loaded&&(ye.use((async(t,e,s)=>{try{const a=t.get("Authentication");if(!a){const{method:s,url:a}=t;const r=`Auth failed with error 'no Authentication key provided' ${s} ${t.get("Host")} ${a}`;return dt.error(r),void e.status(401).json({error:r})}const r=(t=>{const e=t.split(" ");if(2!==e.length||"Bearer"!==e[0])throw new Error("Authentication header is invalid.");const s=Buffer.from(e[1],"base64").toString().split(":");if(3!==s.length)throw new Error;return{signature:s[0],publicKey:s[1],timestamp:parseInt(s[2],10)}})(a);const{signature:n,publicKey:o,timestamp:i}=r;if(Date.now()-i>18e4)return void e.status(401).json({error:"Signature is too old."});const c=b.sha256().update(V+i).digest("hex");if(!he.keyFromPublic(o,"hex").verify(c,n)){const t="The origin and public key pair doesn't match the signature.";return void e.status(401).json({error:t})}const u=await de.select(o);if(u){if(u.clientTimestamp>=i)return void e.status(401).json({error:"Please use a fresh authentication token."});await de.update({publicKey:o,clientTimestamp:i})}else await de.insert({publicKey:o,clientTimestamp:i});e.locals.authToken=r,s()}catch(t){dt.error(`Auth failed with error '${t.message}'`),e.status(401).json({error:t.message})}})),ye.use(ge.middleware));const fe=(()=>{const t=s.Router();return t.get("/wallet/:address/utxos",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await St.selectByAddress(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/sent-outputs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listSentOutputs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/received-outputs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listReceivedOutputs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/wallet/:address/list-txs",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await Qt.listTxs(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/non-standard-utxos",(async(t,e)=>{try{const s=new URLSearchParams(t.url.split("?")[1]);const a={mod:s.get("mod"),publicKey:s.get("publicKey"),limit:s.get("limit"),order:s.get("order"),offset:s.get("offset"),ids:JSON.parse(s.get("ids"))};const r=await Qt.query(a);e.status(200).json(r)}catch(s){dt.error(`GET ${t.url} failed with error '${s.messages}'`),e.status(500).json({error:s.message})}})),t.get("/address/:address/balance",(async({params:t,url:e},s)=>{try{const{address:e}=t;s.status(200).json(await St.getBalance(e))}catch(t){dt.error(`GET ${e} failed with error '${t.message||t}'`),s.status(500).json({error:t.message})}})),t.post("/tx/bulk",(async({body:{txIds:t},url:e},s)=>{try{if(void 0===t||0===t.length)return void s.status(400).json({error:"Missing input txIds."});const e=await te.getRaw(t);e?s.status(200).json(e):s.status(404).json({error:"Not found"})}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/tx/post",(async({body:{hex:t},url:e},s)=>{try{if(!t)return void s.status(400).json({error:"Missing input hex."});const e=await te.sendRaw(t);e?s.status(200).json(e):s.status(404).json({error:"Error Occured"})}catch(a){dt.error(`POST ${e} failed with error '${a.message}\ntxHex: ${t}`),s.status(500).json({error:a.message})}})),t.get("/mine",(async({query:{count:t},url:e},s)=>{try{const{result:e}=await ae.getnewaddress();if("string"!=typeof t)throw new Error("Please provide appropriate count");return await ae.generatetoaddress(parseInt(t,10)||1,e),s.status(200).json({success:!0})}catch(t){return dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.get("/:id/height",(async({params:{id:t},url:e},s)=>{try{let e=t;if("best"===t){const{result:t}=await ae.getbestblockhash();e=t}const{result:a}=await ae.getblockheader(e,!0);return s.status(200).json({height:a.height})}catch(t){return dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/faucet",(async({body:{address:t,value:e},url:s},a)=>{try{const s=parseInt(e,10)/1e8;const{result:r}=await ae.sendtoaddress(t,s);await ae.generateToAddress(1,"mvFeNF9DAR7WMuCpBPbKuTtheihLyxzj8i");const{result:n}=await ae.getrawtransaction(r,1);const o=n.vout.findIndex((t=>1e8*t.value===parseInt(e,10)));return a.status(200).json({txId:r,vout:o,height:-1,satoshis:e})}catch(t){return dt.error(`POST ${s} failed with error '${t.message}'`),a.status(500).json({error:t.message})}})),t.post("/faucetScript",(async({body:{script:t,value:e},url:s},a)=>{try{const s=ie.makeRandom({network:ce});const r=d.p2pkh({pubkey:s.publicKey,network:ce});const{address:n}=r;const o=(await ae.sendtoaddress(n,2*parseInt(e,10)/1e8,"","")).result;let i;let c=10;for(;!i;)if(i=(await St.selectByAddress(n)).filter((t=>t.txId===o))[0],!i){if(c-=1,c<=0)throw new Error("No outputs");await oe(10)}const u=(await ae.getrawtransaction(i.txId,1)).result;const l=new m({network:ce});l.addInput({hash:i.txId,index:i.vout,nonWitnessUtxo:Buffer.from(u.hex,"hex")}),l.addOutput({script:Buffer.from(t,"hex"),value:parseInt(e,10)}),l.signInput(0,s),l.finalizeAllInputs();const p=l.extractTransaction();let h;for(await ae.sendrawtransaction(p.toHex()),c=5;!h;)if(h=(await St.selectByScriptHex(t)).filter((t=>t.txId===p.getId()))[0],!h){if(c-=1,c<=0)throw new Error("No outputs");await oe(10)}return a.status(200).json({txId:p.getId(),vout:h.vout,height:-1,satoshis:h.satoshis})}catch(t){return dt.error(`POST ${s} failed with error '${t.message}'`),a.status(500).json({error:t.message})}})),t.get("/tx/:txId/json",(async({params:{txId:t},url:e},s)=>{try{if(!t)return void s.status(400).json({error:"Missing input txId."});const e=await te.getRawJSON(t);e?s.status(200).json(e):s.status(404).json({error:"Not found"})}catch(t){dt.error(`GET ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/revs",(async({body:{ids:t},url:e},s)=>{try{if(void 0===t||0===t.length)return void s.status(400).json({error:"Missing input object ids."});const e=await Qt.getLatestRevs(t);s.status(200).json(e)}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/revToId",(async({body:{rev:t},url:e},s)=>{try{if(!Jt(t))return void s.status(400).json({error:"Invalid rev id"});const e=await Qt.getIdByRev(t);e&&s.status(200).json(e),s.status(404).json()}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/rpc",(async({body:t,url:e},s)=>{try{if(!t||!t.method)throw new Error("Please provide appropriate RPC method name");if(!new RegExp(P).test(t.method))throw new Error("Method is not allowed");const e=function(t,e){if(void 0===re[t]||null===re[t])throw new Error("This RPC method does not exist, or not supported");const s=e.trim().split(" ");const a=re[t].trim().split(" ");if(0===e.trim().length&&0!==re[t].trim().length)throw new Error(`Too few params provided. Expected ${a.length} Provided 0`);if(0!==e.trim().length&&0===re[t].trim().length)throw new Error(`Too many params provided. Expected 0 Provided ${s.length}`);if(s.lengtha.length)throw new Error(`Too many params provided. Expected ${a.length} Provided ${s.length}`);return 0===e.length?[]:s.map(((t,e)=>ne[a[e]](t)))}(t.method,t.params);const a=e.length?await ae[t.method](...e):await ae[t.method]();s.status(200).json({result:a})}catch(t){dt.error(`POST ${e} failed with error '${t.message}'`),s.status(500).json({error:t.message})}})),t.post("/non-standard-utxo",(async(t,e)=>{e.status(500).json({error:"Please upgrade to @bitcoin-computer/lib to the latest version."})})),t})();ye.use(`/v1/${M}/${C}`,fe),ye.use("/v1/store",$t),we.listen(H,(()=>{dt.info(`\nStarted Bitcoin Computer Node Version ${ht}\nPORT ${H} \n`)})).on("error",(t=>{dt.error(t.message),process.exit(1)}));const Ee=new r.Subscriber;Ee.connect(k),Ee.subscribe("rawtx"),dt.info(`ZMQ Subscriber connected to ${k}`),(async()=>{await(async()=>{await v((()=>wt.connect()),{startingDelay:500})})(),await ue.sub(Ee)})(); diff --git a/packages/node/dist/bcn.sync.es.mjs b/packages/node/dist/bcn.sync.es.mjs index 8e94df19f..fbd4424b2 100644 --- a/packages/node/dist/bcn.sync.es.mjs +++ b/packages/node/dist/bcn.sync.es.mjs @@ -1 +1 @@ -import*as t from"@bitcoin-computer/secp256k1";import{bufferUtils as e,networks as s,Transaction as a,crypto as n,address as r,initEccLib as o}from"@bitcoin-computer/nakamotojs";import i from"node:cluster";import{availableParallelism as c}from"node:os";import l from"dotenv";import p from"winston";import u from"winston-daily-rotate-file";import d from"bitcoind-rpc";import h from"util";import y from"pg-promise";import m from"pg-monitor";import{backOff as w}from"exponential-backoff";import g from"fs";import{Computer as E}from"@bitcoin-computer/lib";l.config();const $=process.env.BCN_CHAIN;const v=process.env.BCN_NETWORK;const{BCN_PORT:f}=process.env;process.env,process.env;const{BCN_DEBUG_MODE:O}=process.env;const{BCN_LOG_MAX_FILES:T}=process.env;const{BCN_LOG_MAX_SIZE:S}=process.env;const{BCN_LOG_ZIP:R}=process.env;const{BCN_SHOW_CONSOLE_LOGS:I}=process.env;const{BCN_SHOW_DB_LOGS:B}=process.env;process.env,process.env,process.env,process.env,process.env;const{BCN_THREADS:k}=process.env;process.env,process.env;const b=process.env.BCN_QUERY_LIMIT||"1000";const x=process.env.BCN_URL||`http://127.0.0.1:${f}`;process.env.BCN_ENV;const{BITCOIN_RPC_USER:M}=process.env;const{BITCOIN_RPC_PASSWORD:H}=process.env;const{BITCOIN_RPC_HOST:N}=process.env;const{BITCOIN_RPC_PORT:A}=process.env;const{BITCOIN_RPC_PROTOCOL:C}=process.env;const{BITCOIN_DEFAULT_WALLET:L}=process.env;const{POSTGRES_USER:P}=process.env;const{POSTGRES_PASSWORD:D}=process.env;const{POSTGRES_DB:F}=process.env;const{POSTGRES_HOST:U}=process.env;const{POSTGRES_PORT:W}=process.env;p.addColors({error:"red",warn:"yellow",info:"green",http:"magenta",debug:"white"});const _=p.format.combine(p.format.colorize(),p.format.timestamp({format:"YYYY-MM-DD HH:mm:ss:ms"}),p.format.json(),p.format.printf((t=>`${t.timestamp} [${t.level.slice(5).slice(0,-5)}] ${t.message}`)));const Y={zippedArchive:"true"===R,maxSize:S,maxFiles:T,dirname:"logs"};const K=[];"true"===I&&K.push(new p.transports.Console({format:p.format.combine(p.format.colorize(),p.format.timestamp({format:"MM-DD-YYYY HH:mm:ss"}),p.format.printf((t=>`${t.timestamp} ${t.level} ${t.message}`)))}));const G=parseInt(O,10);G>=0&&K.push(new u({filename:"error-%DATE%.log",datePattern:"YYYY-MM-DD",level:"error",...Y})),G>=1&&K.push(new u({filename:"warn-%DATE%.log",datePattern:"YYYY-MM-DD",level:"warn",...Y})),G>=2&&K.push(new u({filename:"info-%DATE%.log",datePattern:"YYYY-MM-DD",level:"info",...Y})),G>=3&&K.push(new u({filename:"http-%DATE%.log",datePattern:"YYYY-MM-DD",level:"http",...Y})),G>=4&&K.push(new u({filename:"debug-%DATE%.log",datePattern:"YYYY-MM-DD",level:"debug",...Y}));const V=p.createLogger({levels:{error:0,warn:1,info:2,http:3,debug:4},format:_,transports:K,exceptionHandlers:[new p.transports.File({filename:"logs/exceptions.log"})],rejectionHandlers:[new p.transports.File({filename:"logs/rejections.log"})]});const j=new d({protocol:C,user:M,pass:H,host:N,port:parseInt(A,10)});const q=h.promisify(d.prototype.createwallet.bind(j));const J=h.promisify(d.prototype.generateToAddress.bind(j));const z=h.promisify(d.prototype.getaddressinfo.bind(j));const X=h.promisify(d.prototype.getBlock.bind(j));const Z=h.promisify(d.prototype.getBlockchainInfo.bind(j));const Q=h.promisify(d.prototype.getBlockHash.bind(j));const tt=h.promisify(d.prototype.getRawTransaction.bind(j));const et=h.promisify(d.prototype.getRawTransaction.bind(j));const st=h.promisify(d.prototype.getTransaction.bind(j));const at=h.promisify(d.prototype.getNewAddress.bind(j));const nt={createwallet:q,generateToAddress:J,getaddressinfo:z,getBlock:X,getBlockchainInfo:Z,getBlockHash:Q,getRawTransaction:tt,getTransaction:st,importaddress:h.promisify(d.prototype.importaddress.bind(j)),invalidateBlock:h.promisify(d.prototype.invalidateBlock.bind(j)),listunspent:h.promisify(d.prototype.listunspent.bind(j)),sendRawTransaction:h.promisify(d.prototype.sendRawTransaction.bind(j)),getNewAddress:at,sendToAddress:h.promisify(d.prototype.sendToAddress.bind(j)),getRawTransactionJSON:et};l.config();const{version:rt}=JSON.parse(g.readFileSync("package.json","utf8"));rt||process.env.BCN_SERVER_VERSION;const ot=parseInt(process.env.MWEB_HEIGHT||"",10)||432;const it={error:(t,e)=>{if(e.cn){const{host:s,port:a,database:n,user:r,password:o}=e.cn;V.debug(`Waiting for db to start { message:${t.message} host:${s}, port:${a}, database:${n}, user:${r}, password: ${o}`)}},noWarnings:!0};"true"===B&&(m.isAttached()?m.detach():(m.attach(it),m.setTheme("matrix")));const ct=y(it)({host:U,port:parseInt(W,10),database:F,user:P,password:D,allowExitOnIdle:!0,idleTimeoutMillis:100});const{PreparedStatement:lt}=y;class pt{static async selectByWorkerId(t){const e=new lt({name:`TxStatus.select.${Math.random()}`,text:'SELECT "blockToSync", "workerId" FROM "TxStatus" WHERE "workerId" = $1',values:[t]});return ct.oneOrNone(e)}static async update({blockToSync:t,workerId:e}){const s=new lt({name:`TxStatus.update.${Math.random()}`,text:'UPDATE "TxStatus" SET "blockToSync" = $1 WHERE "workerId" = $2',values:[t,e]});await ct.any(s)}static async count(){const t=new lt({name:`TxStatus.count.${Math.random()}`,text:'SELECT COUNT(*) FROM "TxStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.count,10)||0}static async min(){const t=new lt({name:`TxStatus.min.${Math.random()}`,text:'SELECT MIN("blockToSync") FROM "TxStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.min,10)||0}static async deleteAll(){const t=new lt({name:`TxStatus.delete.${Math.random()}`,text:'DELETE FROM "TxStatus"'});await ct.any(t)}static async insertBatch(t){const e=[];for(let s=1;s<=t.length;s+=2)e.push(`($${s}, $${s+1})`);const s=e.join(",");const a=new lt({name:`TxStatus.reorg.${Math.random()}`,text:`INSERT INTO "TxStatus"("workerId", "blockToSync") VALUES ${s}`,values:t});await ct.any(a)}}class ut{static async selectByWorkerId(t){return pt.selectByWorkerId(t)}static async update(t){await pt.update(t)}static async count(){return pt.count()}static async insertBatch(t){await pt.insertBatch(t)}static async min(){return pt.min()}static async deleteAll(){await pt.deleteAll()}}const{PreparedStatement:dt}=y;class ht{static async select(){const t=new dt({name:`BlockStatus.select.${Math.random()}`,text:'SELECT "blockToSync" FROM "BlockStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.blockToSync,10)||null}static async update(t){const e=new dt({name:`BlockStatus.update.${Math.random()}`,text:'UPDATE "BlockStatus" SET "blockToSync" = $1',values:[t]});await ct.any(e)}static async insert(t){const e=new dt({name:`BlockStatus.insert.${Math.random()}`,text:'INSERT INTO "BlockStatus"("blockToSync") VALUES ($1)',values:[t]});await ct.any(e)}static async count(){const t=new dt({name:`BlockStatus.count.${Math.random()}`,text:'SELECT COUNT(*) FROM "BlockStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.count,10)||0}static async delete(){const t=new dt({name:`BlockStatus.delete.${Math.random()}`,text:'DELETE FROM "BlockStatus"'});await ct.any(t)}}class yt{static async select(){return ht.select()}static async update(t){await ht.update(t)}static async insert(t){await ht.insert(t)}static async count(){return ht.count()}static async delete(){await ht.delete()}}class mt{static update=async t=>yt.update(t);static select=async()=>yt.select();static insert=async t=>yt.insert(t);static count=async()=>yt.count();static delete=async()=>yt.delete()}const{PreparedStatement:wt}=y;class gt{static async selectByHeight(t){const e=new wt({name:`Block.select.${Math.random()}`,text:'SELECT "hash", "height", "previousHash" FROM "Block" WHERE "height" = $1',values:[t]});return ct.one(e)}static async insert(t){const e=new wt({name:`Block.insert.${Math.random()}`,text:'INSERT INTO "Block" ("hash", "height", "previousHash") VALUES ($1, $2, $3)',values:[t.hash,t.height,t.previousHash]});await ct.none(e)}static async deleteAll(){const t=new wt({name:`Block.delete.${Math.random()}`,text:'DELETE FROM "Block"',values:[]});await ct.none(t)}static async deleteByHash(t){const e=t.map((t=>t)).join("', '");const s=new wt({name:`Block.deleteByHash.${Math.random()}`,text:`DELETE FROM "Block" WHERE "hash" IN ('${e}')`});await ct.none(s)}}class Et{static selectByHeight=async t=>gt.selectByHeight(t);static insert=async t=>gt.insert(t);static deleteAll=async()=>gt.deleteAll();static deleteByHash=async t=>gt.deleteByHash(t);static waitForDbBlockHash=async(t,e)=>(await w((async()=>{let s;try{s=await gt.selectByHeight(t)}catch(s){throw V.info(`[wid ${e} pid: ${process.pid}]: waiting for DB to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).hash}class $t{static selectByHeight=async t=>Et.selectByHeight(t);static insert=async t=>Et.insert(t);static deleteAll=async()=>Et.deleteAll();static deleteByHash=async t=>Et.deleteByHash(t);static waitForDbBlockHash=async(t,e)=>Et.waitForDbBlockHash(t,e)}const{PreparedStatement:vt}=y;class ft{static async selectAll(){const t=new vt({name:`Orphan.select.${Math.random()}`,text:'SELECT * FROM "Orphan"'});return(await ct.any(t)).map((t=>t.hash))}static async insertAll(t){const e=t.map((t=>`('${t}')`)).join(",");const s=new vt({name:`Orphan.insert.${Math.random()}`,text:`INSERT INTO "Orphan" (hash) VALUES ${e}`});await ct.none(s)}static async deleteAll(){const t=new vt({name:`Orphan.delete.${Math.random()}`,text:'DELETE FROM "Orphan"',values:[]});await ct.none(t)}}class Ot{static selectAll=async()=>ft.selectAll();static insertAll=async t=>ft.insertAll(t);static deleteAll=async()=>ft.deleteAll()}class Tt{static selectAll=async()=>Ot.selectAll();static insertAll=async t=>Ot.insertAll(t);static deleteAll=async()=>Ot.deleteAll()}const St=(t,e)=>{const s=[];for(let a=0;a{const e=[];for(let s=1;s<=t;s+=3){const t=`($${s},$${s+1},$${s+2})`;e.push(t)}return e.join(",")};const It=t=>{const e=[];for(let s=1;s<=t;s+=10){const t=`($${s},$${s+1},$${s+2},$${s+3},$${s+4},$${s+5},$${s+6},$${s+7},$${s+8},$${s+9})`;e.push(t)}return e.join(",")};const Bt=t=>{try{return t()}catch{return null}};const{PreparedStatement:kt}=y;class bt{static async select(t){const e=new kt({name:`Input.select.${Math.random()}`,text:'SELECT "outputSpent", "spendingInput", "blockHash" FROM "Input" WHERE "outputSpent" = $1',values:[t]});return ct.any(e)}static async insert(t){await Promise.all(St(t,3333).map((t=>{const e=t.flatMap((({outputSpent:t,spendingInput:e,blockHash:s})=>[t,e,s]));return ct.none(new kt({name:`Input.insert.${Math.random()}`,text:`INSERT INTO "Input"("outputSpent", "spendingInput", "blockHash") VALUES ${Rt(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async updateBlockHash(t,e){await Promise.all(St(t,1e4).map((t=>{const s=t.join("','");return ct.none(new kt({name:`Input.updateBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = $1 WHERE "spendingInput" IN ('${s}')`,values:[e]}))})))}static async eraseBlockHash(t){await Promise.all(St(t,1e4).map((t=>{const e=t.join("','");return ct.none(new kt({name:`Input.eraseBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async count(t){const e=t.map((t=>t.outputSpent));const s=new kt({name:`Input.belong.${Math.random()}`,text:'SELECT count(*) FROM "Input" WHERE "outputSpent" LIKE ANY ($1)',values:[[e]]});const a=await ct.oneOrNone(s);return parseInt(a?.count,10)||0}}class xt{static async select(t){return bt.select(t)}static async insert(t){return bt.insert(t)}static async updateBlockHash(t,e){return bt.updateBlockHash(t,e)}static async eraseBlockHash(t){return bt.eraseBlockHash(t)}}class Mt{static insert=async(t,s=null)=>{const n=t.flatMap((t=>t.tx.ins.map(((e,s)=>({input:e,index:s,txId:t.txId}))))).filter((({input:t})=>!a.isCoinbaseHash(t.hash))).map((({input:t,index:s,txId:a})=>{return{outputSpent:`${n=t.hash,e.reverseBuffer(Buffer.from(n)).toString("hex")}:${t.index}`,spendingInput:`${a}:${s}`,blockHash:null};var n}));if(await xt.insert(n),s){const t=n.map((({spendingInput:t})=>t));await xt.updateBlockHash(t,s)}};static select=async t=>xt.select(t);static updateBlockHash=async(t,e)=>{await xt.updateBlockHash(t,e)};static eraseBlockHash=async t=>{await xt.eraseBlockHash(t)}}function Ht(t){if(!function(t){return/^[0-9A-Fa-f]{64}:\d+$/.test(t)}(t))throw new Error("Invalid rev")}const{PreparedStatement:Nt}=y;class At{static async listSentOutputs(t){const e=new Nt({name:`Output.listSentTxs.${Math.random()}`,text:'SELECT "Input"."spendingInput" AS "output", "Output"."satoshis" AS "amount"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output"."address" = $1',values:[t]});return(await ct.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listReceivedOutputs(t){const e=new Nt({name:`Output.listReceivedTxs.${Math.random()}`,text:'SELECT "Output"."rev" as "output", "Output"."satoshis" as "amount" FROM "Output" WHERE "address" = $1',values:[t]});return(await ct.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listTxs(t){const e=new Nt({name:`Output.listTxs.${Math.random()}`,text:'WITH\n -- List all txs sent from a given address\n SENT AS (\n SELECT split_part("Input"."spendingInput",\':\',1) as "txId", SUM("Output".satoshis) as "satoshis"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output".address = $1\n GROUP BY split_part("Input"."spendingInput",\':\',1)\n ),\n -- List all tx received from a given address\n RECEIVED AS (\n SELECT SPLIT_PART("Output"."rev",\':\',1) as "txId", SUM("Output"."satoshis") as "satoshis" \n FROM "Output" \n WHERE "address" = $1\n GROUP BY "txId"\n )\n\n SELECT\n RECEIVED."txId", \n coalesce(SENT."satoshis", 0) as "inputsSatoshis", \n coalesce(RECEIVED."satoshis", 0) as "outputsSatoshis", \n coalesce(RECEIVED."satoshis",0) - coalesce(SENT."satoshis",0) as "satoshis"\n FROM\n SENT RIGHT JOIN RECEIVED ON SENT."txId" = RECEIVED."txId";',values:[t]});const s=(await ct.any(e)).map((t=>({...t,inputsSatoshis:parseInt(t.inputsSatoshis,10)||0,outputsSatoshis:parseInt(t.outputsSatoshis,10)||0,satoshis:parseInt(t.satoshis,10)||0})));return{sentTxs:s.filter((t=>t.satoshis<0)).map((t=>({...t,satoshis:Math.abs(t.satoshis)}))),receivedTxs:s.filter((t=>t.satoshis>=0))}}static async select(t){const e=new Nt({name:`Output.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", "publicKeys", "hash", "mod", "isTbcOutput", "previous", "blockHash" FROM "Output" WHERE "address" = $1',values:[t]});return ct.any(e)}static async insert(t){await Promise.all(St(t,1e4).map((t=>{const e=t.flatMap((({rev:t,address:e,satoshis:s,scriptPubKey:a,isTbcOutput:n,publicKeys:r,mod:o,previous:i,hash:c,blockHash:l})=>[t,e,s,a,n,r,o,i,c,l]));return ct.none(new Nt({name:`Output.insert.${Math.random()}`,text:`INSERT INTO "Output"("rev", "address", "satoshis", "scriptPubKey", "isTbcOutput", "publicKeys", "mod", "previous", "hash", "blockHash") VALUES ${It(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async eraseBlockHash(t){await Promise.all(St(t,1e4).map((t=>{const e=t.join("','");return ct.none(new Nt({name:`Output.eraseBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async updateBlockHash(t,e){await Promise.all(St(t,1e4).map((t=>{const s=t.join("','");return ct.none(new Nt({name:`Output.updateBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = $1 WHERE "rev" IN ('${s}')`,values:[e]}))})))}static async getIdByRev(t){const e=new Nt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON r."previous" = o."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=(await ct.any(e)).filter((t=>null===t.previous));return s[0]?.rev}static async getIdsByRevs(t){return Promise.all(t.map((t=>this.getIdByRev(t))))}static async getLatestRev(t){const e=new Nt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON o."previous" = r."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=await ct.any(e);const a=Object.fromEntries(s.map((t=>[t.previous,t.rev])));let n=t;for(;a[n];)n=a[n];return n}static async getLatestRevs(t){return Promise.all(t.map(this.getLatestRev))}static async getIdsByMod(t){const e=new Nt({name:`Output.getIdsByMod.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1',values:[t]});return(await ct.any(e)).map((t=>t.rev))}static sqlSuffix(t,e,s){let a="";return s&&(a+=` order by "timestamp" ${s}`),a+=` limit ${t||b}`,e&&(a+=` offset ${e}`),a}static async getRevsByPublicKey(t){const e=new Nt({name:`Output.getRevsByPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys")',values:[t]});return(await ct.any(e)).map((t=>t.rev))}static async getUnspentRevsByMod(t,e,s,a){const n=await this.getIdsByMod(t);const r=await this.getLatestRevs(n);const o=new Nt({name:`Output.getUnspentRevsByMod.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(e,s,a)}`,values:[r]});return(await ct.any(o)).map((t=>t.rev))}static async getUnspentRevsByPublicKey(t,e,s,a){const n=new Nt({name:`Output.getUnspentRevsByPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys") AND "isTbcOutput" = true \n AND NOT EXISTS (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") \n ${this.sqlSuffix(e,s,a)}`,values:[t]});return(await ct.any(n)).map((t=>t.rev))}static async getUnspentRevsByModAndPublicKey(t,e,s,a,n){const r=await this.getUnspentRevsByPublicKey(e,s,a,n);const o=await this.getIdsByRevs(r);const i=new Nt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1 AND "rev" = ANY($2)',values:[t,o]});const c=(await ct.any(i)).map((t=>t.rev));const l=await this.getLatestRevs(c);const p=new Nt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(s,a,n)}`,values:[l]});return(await ct.any(p)).map((t=>t.rev))}static async getUnspentTbcOutputs(t,e,s){const a=new Nt({name:`Output.getUnspentTbcOutputs.${Math.random()}`,text:`SELECT "rev", "address", "satoshis", "scriptPubKey", "publicKeys", "timestamp"\n FROM "Output" WHERE "isTbcOutput" = true AND NOT EXISTS\n (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") ${this.sqlSuffix(t,e,s)}`});return(await ct.any(a)).map((t=>t.rev))}static async query(t){const{publicKey:e,limit:s,offset:a,ids:n,mod:r,order:o}=t;const i=parseInt(b||"",10);if(s&&parseInt(s||"",10)>i||n&&n.length>i)throw new Error(`Can't fetch more than ${b} revs.`);if(o&&"ASC"!==o&&"DESC"!==o)throw new Error("Invalid order. Should be ASC or DESC.");return n?(n.map(Ht),this.getLatestRevs(n)):r&&!e?this.getUnspentRevsByMod(r,s,a,o):!r&&e?this.getUnspentRevsByPublicKey(e,s,a,o):r&&e?this.getUnspentRevsByModAndPublicKey(r,e,s,a,o):this.getUnspentTbcOutputs(s,a,o)}}class Ct{static async select(t){return At.select(t)}static async insert(t){return At.insert(t)}static async eraseBlockHash(t){return At.eraseBlockHash(t)}static async updateBlockHash(t,e){return At.updateBlockHash(t,e)}static async listSentOutputs(t){return At.listSentOutputs(t)}static async listReceivedOutputs(t){return At.listReceivedOutputs(t)}static async listTxs(t){return At.listTxs(t)}static async getLatestRev(t){return At.getLatestRev(t)}static async getLatestRevs(t){return At.getLatestRevs(t)}static async getIdByRev(t){return At.getIdByRev(t)}static async query(t){return At.query(t)}}class Lt{static insert=async(t,e=null)=>{const a=function(t=$,e=v){switch(t){case"BTC":switch(e){case"mainnet":return s.bitcoin;case"testnet":return s.testnet;case"regtest":return s.regtest;default:throw new Error(`Invalid network ${e}`)}case"LTC":switch(e){case"mainnet":return s.litecoin;case"testnet":return s.litecointestnet;case"regtest":return s.litecoinregtest;default:throw new Error(`Invalid network ${e}`)}case"PEPE":switch(e){case"mainnet":return s.pepecoin;case"testnet":return s.pepecointestnet;case"regtest":return s.pepecoinregtest;default:throw new Error(`Invalid network ${e}`)}default:throw new Error(`Invalid chain ${t}`)}}($,v);const o=t.flatMap((t=>{const{zip:e,ownerData:s,onChainMetaData:o}=t;const{exp:i="",mod:c=""}=o;return t.tx.outs.map((({script:o,value:l},p)=>{const u=pr.fromOutputScript(o,a))),satoshis:Math.round(l),scriptPubKey:o.toString("hex"),isTbcOutput:u,publicKeys:u?s[p]._owners:[],mod:u?c:"",previous:u?e[p][0]:null,hash:u?n.sha256(Buffer.from(i||"")).toString("hex"):null,blockHash:null}}))}));if(await Ct.insert(o),e){const t=o.map((({rev:t})=>t));await Ct.updateBlockHash(t,e)}};static eraseBlockHash=async t=>{await Ct.eraseBlockHash(t)};static listSentOutputs=async t=>Ct.listSentOutputs(t);static listReceivedOutputs=async t=>Ct.listReceivedOutputs(t);static listTxs=async t=>Ct.listTxs(t);static getLatestRev=async t=>Ct.getLatestRev(t);static getLatestRevs=async t=>Ct.getLatestRevs(t);static getIdByRev=async t=>Ct.getIdByRev(t);static query=async t=>Ct.query(t)}class Pt{static update=async t=>ut.update(t);static selectByWorkerId=async t=>ut.selectByWorkerId(t);static deleteAll=async()=>ut.deleteAll();static setup=async t=>{0===await yt.count()&&(await mt.insert(1),V.info(`[wid 0 pid: ${process.pid}: registering block sync status on block 1`)),await ut.count()===t?V.info(`[wid 1 pid: ${process.pid}: all ${t} workers have already registered`):await Pt.register(t,await ut.min());const e=await Tt.selectAll();e.length>0&&(V.info(`[wid 0 pid: ${process.pid}: found ${e.length} orphans`),await $t.deleteByHash(e),await Mt.eraseBlockHash(e),await Lt.eraseBlockHash(e),await Tt.deleteAll())};static register=async(t,e)=>{const s=[];let a=Math.max(1,e);for(let e=1;e<=t;e+=1,a+=1)s.push(e,a);V.info(`[wid 0 pid: ${process.pid}: reorging sync status for ${t} workers...${s}`),await ut.deleteAll(),await ut.insertBatch(s)}}class Dt{static async getTransaction(t){const{result:e}=await nt.getTransaction(t);return e}static async getBulkTransactions(t){return(await Promise.all(t.map((t=>nt.getRawTransaction(t,0))))).map((t=>t.result))}static async getRawTransaction(t,e){const{result:s}=await nt.getRawTransaction(t,e);return s}static async getRawTransactionsJSON(t){return{txId:(e=(await nt.getRawTransactionJSON(t,1)).result).txid,txHex:e.hex,vsize:e.vsize,version:e.version,locktime:e.locktime,ins:e.vin.map((t=>t.coinbase?{coinbase:t.coinbase,sequence:t.sequence}:{txId:t.txid,vout:t.vout,script:t.scriptSig.hex,sequence:t.sequence})),outs:e.vout.map((t=>{let e;return t.scriptPubKey.addresses?[e]=t.scriptPubKey.addresses:e=t.scriptPubKey.address?t.scriptPubKey.address:void 0,{address:e,script:t.scriptPubKey.hex,value:Math.round(1e8*t.value)}}))};var e}static async sendRawTransaction(t){const{result:e,error:s}=await nt.sendRawTransaction(t);if(s)throw V.error(s),new Error("Error sending transaction");return e}static getUtxos=async t=>(void 0===(await nt.getaddressinfo(t)).result.timestamp&&(V.info(`Importing address: ${t}`),await nt.importaddress(t,!1)),(await nt.listunspent(0,999999,[t])).result);static waitForRpcBlockHash=async(t,e)=>(await w((async()=>{let s;try{s=await nt.getBlockHash(t)}catch(s){throw V.info(`[wid ${e} pid: ${process.pid}]: waiting for RPC to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).result;static getBlock=async(t,e)=>nt.getBlock(t,e);static walletSetup=async()=>{if("regtest"===v){if(V.info(`Node is starting for chain ${$} and network ${v}, \n\n. Starting Wallet setup.`),"LTC"===$){const{result:t}=await nt.getBlockchainInfo();const e=t.blocks;if(e{try{await nt.createwallet(L,!1,!1,"",!1,!1)}catch(t){V.error(`Wallet creation failed with error '${t.message}'`)}};static checkBlockchainProgress=async()=>{const t=await w((async()=>{const t=await nt.getBlockchainInfo();const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const{blocks:s}=t.result;if(V.info(`Zmq. Bitcoind { percentage:${e}%, blocks:${s} }`),parseFloat(t.result.verificationprogress)<=.7)throw new Error("Node not ready yet");return t}),{startingDelay:6e4,timeMultiple:1,numOfAttempts:8760});const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const s=t.result.blocks;V.info(`BCN reaches sync end...at { bitcoind.progress:${e}%, bitcoindSyncedHeight:${s} }`)}}class Ft{static get=async t=>Dt.getTransaction(t);static getRaw=async t=>Dt.getBulkTransactions(t);static getRawJSON=async t=>Dt.getRawTransactionsJSON(t);static sendRaw=async t=>Dt.sendRawTransaction(t);static getUtxos=async t=>Dt.getUtxos(t);static waitForRpcBlockHash=async(t,e)=>Dt.waitForRpcBlockHash(t,e);static insertRpcBlock=async(t,e,s="LTC")=>{const{result:a}=await Dt.getBlock(t,2);const{tx:n}=a;let r=n;"LTC"===s&&(r=n.filter((t=>"08"!==t.hex.slice(10,12))));const o=`[wid ${e} pid: ${process.pid}: backfilling height ${a.height} - backfilling ${r.length} txs `;"LTC"===s&&o.concat(`(${n.length-r.length} mweb tx's filtered)...`),V.info(o);const i=[];for(const t of r)try{let{hex:e}=t;e||(e=(await Dt.getRawTransaction(t.txid,1)).hex);const s=E.txFromHex({hex:e});s&&i.push(s)}catch(s){V.error(`[wid ${e} pid: ${process.pid}: failed to parse transaction in block ${a.height}\n error message: ${s.message}\n transaction: ${JSON.stringify(t)}`)}try{await Lt.insert(i,t),await Mt.insert(i,t)}catch(t){V.error(`[wid ${e} pid: ${process.pid}: inserting inputs and outputs for block ${a.height} failed with error '${t.message}'`)}};static walletSetup=async()=>Dt.walletSetup()}class Ut{static syncTxs=async(t,e,s)=>{for(V.info(`[wid ${e} pid: ${process.pid}]: starting to sync txs from block: ${t} - numWorkers: ${s}`);;){try{const s=await $t.waitForDbBlockHash(t,e);await Ft.insertRpcBlock(s,e,$)}catch(s){V.error(`[wid ${e} pid: ${process.pid}: syncing block num ${t} failed with error '${s.message}'`)}t+=s,await Pt.update({blockToSync:t,workerId:e})}};static findOrphans=async t=>{const e=await nt.getBlock(t,2);if(1===e?.result.height)return[];const s=await $t.selectByHeight(e.result.height-1);return e?.result.previousblockhash===s.hash?[]:[...await Ut.findOrphans(e.result.previousblockhash),s.hash]};static updateStatus=async(t,e)=>{await mt.update(t);const s=await ut.min();V.info(`[wid 0 pid: ${process.pid}: min: ${s}, height to resume: ${t}`);const a=Math.min(s,t);V.info(`[wid 0 pid: ${process.pid}: reorg detected, resuming at block ${a}`),await Pt.register(e,a)};static registerOrphans=async(t,e,s)=>{try{V.info(`[wid 0 pid: ${process.pid}: block reorg detected at height ${e} [!] orphans ${t}`),await Tt.insertAll(t),V.info(`[wid 0 pid: ${process.pid}: detected ${t.length} orphaned blocks`),await this.updateStatus(e,s),V.info(`[wid 0 pid: ${process.pid}: resuming at height ${e} [!] exiting ...`)}catch(t){V.error(`[wid 0 pid: ${process.pid}: failed to register orphans with error '${t.message}'`)}};static syncBlocks=async(t,e,s)=>{let a="";let n="";for(V.info(`[wid ${e} pid: ${process.pid}]: starting to sync block: ${t}`);;){try{n=await Ft.waitForRpcBlockHash(t,e),V.info(`[wip ${e} pid: ${process.pid}: synchronizing block num ${t} hash ${n}`);const r=await this.findOrphans(n);r.length&&(await this.registerOrphans(r,t-r.length,s),process.exit(0)),await $t.insert({hash:n,height:t,previousHash:a})}catch(s){V.error(`[wid ${e} pid: ${process.pid}: error block num ${t} failed to sync with error '${s.message}'`)}t+=1,a=n,await mt.update(t)}}}o(t);let Wt=c();k&&parseInt(k,10)>0&&(Wt=parseInt(k,10));const _t=i.worker?i.worker.id:0;V.info(`[wid ${_t} pid: ${process.pid}]: starting with ${Wt} threads`);try{if(await(async()=>{await w((()=>ct.connect()),{startingDelay:500})})(),V.info(`[wid ${_t} pid: ${process.pid}]: connected to the database successfully`),i.isPrimary){V.info(`[wid ${_t} pid: ${process.pid}]: parameters { url: ${x}, chain:${$} network:${v} numWorkers: ${Wt}}`),await Pt.setup(Wt);for(let t=1;t<=Wt;t+=1)V.info(`[wid ${_t} pid: ${process.pid}: launching worker ${t}`),i.fork();i.on("exit",((t,e,s)=>{V.info(`[wid ${_t} pid: ${process.pid}]: worker ${t.process.pid} died with code ${e} and signal ${s}`),V.error(`[wid ${_t} pid: ${process.pid}]: aborting`),process.exit(0)}));const t=await mt.select();await Ut.syncBlocks(t,_t,Wt)}else{const t=await Pt.selectByWorkerId(_t);await Ut.syncTxs(t.blockToSync,t.workerId,Wt)}}catch(t){V.error(`[wid ${_t} pid: ${process.pid}]: synchronizing failed with error '${t.message}'`)} +import*as t from"@bitcoin-computer/secp256k1";import{bufferUtils as e,networks as s,Transaction as a,crypto as n,address as r,initEccLib as o}from"@bitcoin-computer/nakamotojs";import i from"node:cluster";import{availableParallelism as c}from"node:os";import l from"dotenv";import p from"winston";import u from"winston-daily-rotate-file";import d from"bitcoind-rpc";import h from"util";import y from"pg-promise";import m from"pg-monitor";import{backOff as w}from"exponential-backoff";import g from"fs";import{Computer as E}from"@bitcoin-computer/lib";l.config();const $=process.env.BCN_CHAIN;const v=process.env.BCN_NETWORK;const{BCN_PORT:f}=process.env;process.env,process.env;const{BCN_DEBUG_MODE:O}=process.env;const{BCN_LOG_MAX_FILES:T}=process.env;const{BCN_LOG_MAX_SIZE:S}=process.env;const{BCN_LOG_ZIP:R}=process.env;const{BCN_SHOW_CONSOLE_LOGS:I}=process.env;const{BCN_SHOW_DB_LOGS:B}=process.env;process.env,process.env,process.env,process.env,process.env;const{BCN_THREADS:k}=process.env;process.env,process.env;const b=process.env.BCN_QUERY_LIMIT||"1000";const x=process.env.BCN_URL||`http://127.0.0.1:${f}`;process.env.BCN_ENV;const{BITCOIN_RPC_USER:M}=process.env;const{BITCOIN_RPC_PASSWORD:H}=process.env;const{BITCOIN_RPC_HOST:N}=process.env;const{BITCOIN_RPC_PORT:A}=process.env;const{BITCOIN_RPC_PROTOCOL:C}=process.env;const{BITCOIN_DEFAULT_WALLET:L}=process.env;const{POSTGRES_USER:P}=process.env;const{POSTGRES_PASSWORD:D}=process.env;const{POSTGRES_DB:F}=process.env;const{POSTGRES_HOST:U}=process.env;const{POSTGRES_PORT:W}=process.env;p.addColors({error:"red",warn:"yellow",info:"green",http:"magenta",debug:"white"});const _=p.format.combine(p.format.colorize(),p.format.timestamp({format:"YYYY-MM-DD HH:mm:ss:ms"}),p.format.json(),p.format.printf((t=>`${t.timestamp} [${t.level.slice(5).slice(0,-5)}] ${t.message}`)));const Y={zippedArchive:"true"===R,maxSize:S,maxFiles:T,dirname:"logs"};const K=[];"true"===I&&K.push(new p.transports.Console({format:p.format.combine(p.format.colorize(),p.format.timestamp({format:"MM-DD-YYYY HH:mm:ss"}),p.format.printf((t=>`${t.timestamp} ${t.level} ${t.message}`)))}));const G=parseInt(O,10);G>=0&&K.push(new u({filename:"error-%DATE%.log",datePattern:"YYYY-MM-DD",level:"error",...Y})),G>=1&&K.push(new u({filename:"warn-%DATE%.log",datePattern:"YYYY-MM-DD",level:"warn",...Y})),G>=2&&K.push(new u({filename:"info-%DATE%.log",datePattern:"YYYY-MM-DD",level:"info",...Y})),G>=3&&K.push(new u({filename:"http-%DATE%.log",datePattern:"YYYY-MM-DD",level:"http",...Y})),G>=4&&K.push(new u({filename:"debug-%DATE%.log",datePattern:"YYYY-MM-DD",level:"debug",...Y}));const V=p.createLogger({levels:{error:0,warn:1,info:2,http:3,debug:4},format:_,transports:K,exceptionHandlers:[new p.transports.File({filename:"logs/exceptions.log"})],rejectionHandlers:[new p.transports.File({filename:"logs/rejections.log"})]});const j=new d({protocol:C,user:M,pass:H,host:N,port:parseInt(A,10)});const q=h.promisify(d.prototype.createwallet.bind(j));const J=h.promisify(d.prototype.generateToAddress.bind(j));const z=h.promisify(d.prototype.getaddressinfo.bind(j));const X=h.promisify(d.prototype.getBlock.bind(j));const Z=h.promisify(d.prototype.getBlockchainInfo.bind(j));const Q=h.promisify(d.prototype.getBlockHash.bind(j));const tt=h.promisify(d.prototype.getRawTransaction.bind(j));const et=h.promisify(d.prototype.getRawTransaction.bind(j));const st=h.promisify(d.prototype.getTransaction.bind(j));const at=h.promisify(d.prototype.getNewAddress.bind(j));const nt={createwallet:q,generateToAddress:J,getaddressinfo:z,getBlock:X,getBlockchainInfo:Z,getBlockHash:Q,getRawTransaction:tt,getTransaction:st,importaddress:h.promisify(d.prototype.importaddress.bind(j)),invalidateBlock:h.promisify(d.prototype.invalidateBlock.bind(j)),listunspent:h.promisify(d.prototype.listunspent.bind(j)),sendRawTransaction:h.promisify(d.prototype.sendRawTransaction.bind(j)),getNewAddress:at,sendToAddress:h.promisify(d.prototype.sendToAddress.bind(j)),getRawTransactionJSON:et};l.config();const{version:rt}=JSON.parse(g.readFileSync("package.json","utf8"));rt||process.env.BCN_SERVER_VERSION;const ot=parseInt(process.env.MWEB_HEIGHT||"",10)||432;const it={error:(t,e)=>{if(e.cn){const{host:s,port:a,database:n,user:r,password:o}=e.cn;V.debug(`Waiting for db to start { message:${t.message} host:${s}, port:${a}, database:${n}, user:${r}, password: ${o}`)}},noWarnings:!0};"true"===B&&(m.isAttached()?m.detach():(m.attach(it),m.setTheme("matrix")));const ct=y(it)({host:U,port:parseInt(W,10),database:F,user:P,password:D,allowExitOnIdle:!0,idleTimeoutMillis:100});const{PreparedStatement:lt}=y;class pt{static async selectByWorkerId(t){const e=new lt({name:`TxStatus.select.${Math.random()}`,text:'SELECT "blockToSync", "workerId" FROM "TxStatus" WHERE "workerId" = $1',values:[t]});return ct.oneOrNone(e)}static async update({blockToSync:t,workerId:e}){const s=new lt({name:`TxStatus.update.${Math.random()}`,text:'UPDATE "TxStatus" SET "blockToSync" = $1 WHERE "workerId" = $2',values:[t,e]});await ct.any(s)}static async count(){const t=new lt({name:`TxStatus.count.${Math.random()}`,text:'SELECT COUNT(*) FROM "TxStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.count,10)||0}static async min(){const t=new lt({name:`TxStatus.min.${Math.random()}`,text:'SELECT MIN("blockToSync") FROM "TxStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.min,10)||0}static async deleteAll(){const t=new lt({name:`TxStatus.delete.${Math.random()}`,text:'DELETE FROM "TxStatus"'});await ct.any(t)}static async insertBatch(t){const e=[];for(let s=1;s<=t.length;s+=2)e.push(`($${s}, $${s+1})`);const s=e.join(",");const a=new lt({name:`TxStatus.reorg.${Math.random()}`,text:`INSERT INTO "TxStatus"("workerId", "blockToSync") VALUES ${s}`,values:t});await ct.any(a)}}class ut{static async selectByWorkerId(t){return pt.selectByWorkerId(t)}static async update(t){await pt.update(t)}static async count(){return pt.count()}static async insertBatch(t){await pt.insertBatch(t)}static async min(){return pt.min()}static async deleteAll(){await pt.deleteAll()}}const{PreparedStatement:dt}=y;class ht{static async select(){const t=new dt({name:`BlockStatus.select.${Math.random()}`,text:'SELECT "blockToSync" FROM "BlockStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.blockToSync,10)||null}static async update(t){const e=new dt({name:`BlockStatus.update.${Math.random()}`,text:'UPDATE "BlockStatus" SET "blockToSync" = $1',values:[t]});await ct.any(e)}static async insert(t){const e=new dt({name:`BlockStatus.insert.${Math.random()}`,text:'INSERT INTO "BlockStatus"("blockToSync") VALUES ($1)',values:[t]});await ct.any(e)}static async count(){const t=new dt({name:`BlockStatus.count.${Math.random()}`,text:'SELECT COUNT(*) FROM "BlockStatus"'});const e=await ct.oneOrNone(t);return parseInt(e?.count,10)||0}static async delete(){const t=new dt({name:`BlockStatus.delete.${Math.random()}`,text:'DELETE FROM "BlockStatus"'});await ct.any(t)}}class yt{static async select(){return ht.select()}static async update(t){await ht.update(t)}static async insert(t){await ht.insert(t)}static async count(){return ht.count()}static async delete(){await ht.delete()}}class mt{static update=async t=>yt.update(t);static select=async()=>yt.select();static insert=async t=>yt.insert(t);static count=async()=>yt.count();static delete=async()=>yt.delete()}const{PreparedStatement:wt}=y;class gt{static async selectByHeight(t){const e=new wt({name:`Block.select.${Math.random()}`,text:'SELECT "hash", "height", "previousHash" FROM "Block" WHERE "height" = $1',values:[t]});return ct.one(e)}static async insert(t){const e=new wt({name:`Block.insert.${Math.random()}`,text:'INSERT INTO "Block" ("hash", "height", "previousHash") VALUES ($1, $2, $3)',values:[t.hash,t.height,t.previousHash]});await ct.none(e)}static async deleteAll(){const t=new wt({name:`Block.delete.${Math.random()}`,text:'DELETE FROM "Block"',values:[]});await ct.none(t)}static async deleteByHash(t){const e=t.map((t=>t)).join("', '");const s=new wt({name:`Block.deleteByHash.${Math.random()}`,text:`DELETE FROM "Block" WHERE "hash" IN ('${e}')`});await ct.none(s)}}class Et{static selectByHeight=async t=>gt.selectByHeight(t);static insert=async t=>gt.insert(t);static deleteAll=async()=>gt.deleteAll();static deleteByHash=async t=>gt.deleteByHash(t);static waitForDbBlockHash=async(t,e)=>(await w((async()=>{let s;try{s=await gt.selectByHeight(t)}catch(s){throw V.info(`[wid ${e} pid: ${process.pid}]: waiting for DB to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).hash}class $t{static selectByHeight=async t=>Et.selectByHeight(t);static insert=async t=>Et.insert(t);static deleteAll=async()=>Et.deleteAll();static deleteByHash=async t=>Et.deleteByHash(t);static waitForDbBlockHash=async(t,e)=>Et.waitForDbBlockHash(t,e)}const{PreparedStatement:vt}=y;class ft{static async selectAll(){const t=new vt({name:`Orphan.select.${Math.random()}`,text:'SELECT * FROM "Orphan"'});return(await ct.any(t)).map((t=>t.hash))}static async insertAll(t){const e=t.map((t=>`('${t}')`)).join(",");const s=new vt({name:`Orphan.insert.${Math.random()}`,text:`INSERT INTO "Orphan" (hash) VALUES ${e}`});await ct.none(s)}static async deleteAll(){const t=new vt({name:`Orphan.delete.${Math.random()}`,text:'DELETE FROM "Orphan"',values:[]});await ct.none(t)}}class Ot{static selectAll=async()=>ft.selectAll();static insertAll=async t=>ft.insertAll(t);static deleteAll=async()=>ft.deleteAll()}class Tt{static selectAll=async()=>Ot.selectAll();static insertAll=async t=>Ot.insertAll(t);static deleteAll=async()=>Ot.deleteAll()}const St=(t,e)=>{const s=[];for(let a=0;a{const e=[];for(let s=1;s<=t;s+=3){const t=`($${s},$${s+1},$${s+2})`;e.push(t)}return e.join(",")};const It=t=>{const e=[];for(let s=1;s<=t;s+=10){const t=`($${s},$${s+1},$${s+2},$${s+3},$${s+4},$${s+5},$${s+6},$${s+7},$${s+8},$${s+9})`;e.push(t)}return e.join(",")};const Bt=t=>{try{return t()}catch{return null}};const{PreparedStatement:kt}=y;class bt{static async select(t){const e=new kt({name:`Input.select.${Math.random()}`,text:'SELECT "outputSpent", "spendingInput", "blockHash" FROM "Input" WHERE "outputSpent" = $1',values:[t]});return ct.any(e)}static async insert(t){await Promise.all(St(t,3333).map((t=>{const e=t.flatMap((({outputSpent:t,spendingInput:e,blockHash:s})=>[t,e,s]));return ct.none(new kt({name:`Input.insert.${Math.random()}`,text:`INSERT INTO "Input"("outputSpent", "spendingInput", "blockHash") VALUES ${Rt(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async updateBlockHash(t,e){await Promise.all(St(t,1e4).map((t=>{const s=t.join("','");return ct.none(new kt({name:`Input.updateBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = $1 WHERE "spendingInput" IN ('${s}')`,values:[e]}))})))}static async eraseBlockHash(t){await Promise.all(St(t,1e4).map((t=>{const e=t.join("','");return ct.none(new kt({name:`Input.eraseBlockHash.${Math.random()}`,text:`UPDATE "Input" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async count(t){const e=t.map((t=>t.outputSpent));const s=new kt({name:`Input.belong.${Math.random()}`,text:'SELECT count(*) FROM "Input" WHERE "outputSpent" LIKE ANY ($1)',values:[[e]]});const a=await ct.oneOrNone(s);return parseInt(a?.count,10)||0}}class xt{static async select(t){return bt.select(t)}static async insert(t){return bt.insert(t)}static async updateBlockHash(t,e){return bt.updateBlockHash(t,e)}static async eraseBlockHash(t){return bt.eraseBlockHash(t)}}class Mt{static insert=async(t,s=null)=>{const n=t.flatMap((t=>t.tx.ins.map(((e,s)=>({input:e,index:s,txId:t.txId}))))).filter((({input:t})=>!a.isCoinbaseHash(t.hash))).map((({input:t,index:s,txId:a})=>{return{outputSpent:`${n=t.hash,e.reverseBuffer(Buffer.from(n)).toString("hex")}:${t.index}`,spendingInput:`${a}:${s}`,blockHash:null};var n}));if(await xt.insert(n),s){const t=n.map((({spendingInput:t})=>t));await xt.updateBlockHash(t,s)}};static select=async t=>xt.select(t);static updateBlockHash=async(t,e)=>{await xt.updateBlockHash(t,e)};static eraseBlockHash=async t=>{await xt.eraseBlockHash(t)}}function Ht(t){if(!function(t){return/^[0-9A-Fa-f]{64}:\d+$/.test(t)}(t))throw new Error("Invalid rev")}const{PreparedStatement:Nt}=y;class At{static async listSentOutputs(t){const e=new Nt({name:`Output.listSentTxs.${Math.random()}`,text:'SELECT "Input"."spendingInput" AS "output", "Output"."satoshis" AS "amount"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output"."address" = $1',values:[t]});return(await ct.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listReceivedOutputs(t){const e=new Nt({name:`Output.listReceivedTxs.${Math.random()}`,text:'SELECT "Output"."rev" as "output", "Output"."satoshis" as "amount" FROM "Output" WHERE "address" = $1',values:[t]});return(await ct.any(e)).map((t=>({...t,amount:parseInt(t.amount,10)||0})))}static async listTxs(t){const e=new Nt({name:`Output.listTxs.${Math.random()}`,text:'WITH\n -- List all txs sent from a given address\n SENT AS (\n SELECT split_part("Input"."spendingInput",\':\',1) as "txId", SUM("Output".satoshis) as "satoshis"\n FROM "Output" INNER JOIN "Input" ON "Output".rev = "Input"."outputSpent" \n WHERE "Output".address = $1\n GROUP BY split_part("Input"."spendingInput",\':\',1)\n ),\n -- List all tx received from a given address\n RECEIVED AS (\n SELECT SPLIT_PART("Output"."rev",\':\',1) as "txId", SUM("Output"."satoshis") as "satoshis" \n FROM "Output" \n WHERE "address" = $1\n GROUP BY "txId"\n )\n\n SELECT\n RECEIVED."txId", \n coalesce(SENT."satoshis", 0) as "inputsSatoshis", \n coalesce(RECEIVED."satoshis", 0) as "outputsSatoshis", \n coalesce(RECEIVED."satoshis",0) - coalesce(SENT."satoshis",0) as "satoshis"\n FROM\n SENT RIGHT JOIN RECEIVED ON SENT."txId" = RECEIVED."txId";',values:[t]});const s=(await ct.any(e)).map((t=>({...t,inputsSatoshis:parseInt(t.inputsSatoshis,10)||0,outputsSatoshis:parseInt(t.outputsSatoshis,10)||0,satoshis:parseInt(t.satoshis,10)||0})));return{sentTxs:s.filter((t=>t.satoshis<0)).map((t=>({...t,satoshis:Math.abs(t.satoshis)}))),receivedTxs:s.filter((t=>t.satoshis>=0))}}static async select(t){const e=new Nt({name:`Output.select.${Math.random()}`,text:'SELECT "address", "satoshis", "scriptPubKey", "rev", "publicKeys", "hash", "mod", "isTbcOutput", "previous", "blockHash" FROM "Output" WHERE "address" = $1',values:[t]});return ct.any(e)}static async insert(t){await Promise.all(St(t,1e3).map((t=>{const e=t.flatMap((({rev:t,address:e,satoshis:s,scriptPubKey:a,isTbcOutput:n,publicKeys:r,mod:o,previous:i,hash:c,blockHash:l})=>[t,e,s,a,n,r,o,i,c,l]));return ct.none(new Nt({name:`Output.insert.${Math.random()}`,text:`INSERT INTO "Output"("rev", "address", "satoshis", "scriptPubKey", "isTbcOutput",\n "publicKeys", "mod", "previous", "hash", "blockHash") VALUES ${It(e.length)} ON CONFLICT DO NOTHING`,values:e}))})))}static async eraseBlockHash(t){await Promise.all(St(t,1e4).map((t=>{const e=t.join("','");return ct.none(new Nt({name:`Output.eraseBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = NULL WHERE "blockHash" IN ('${e}')`}))})))}static async updateBlockHash(t,e){await Promise.all(St(t,1e4).map((t=>{const s=t.join("','");return ct.none(new Nt({name:`Output.updateBlockHash.${Math.random()}`,text:`UPDATE "Output" SET "blockHash" = $1 WHERE "rev" IN ('${s}')`,values:[e]}))})))}static async getIdByRev(t){const e=new Nt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON r."previous" = o."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=(await ct.any(e)).filter((t=>null===t.previous));return s[0]?.rev}static async getIdsByRevs(t){return Promise.all(t.map((t=>this.getIdByRev(t))))}static async getLatestRev(t){const e=new Nt({name:`NonStandard.recursiveUpdates.${Math.random()}`,text:'WITH RECURSIVE revUpdates AS (\n SELECT "rev", "previous" FROM "Output" WHERE "isTbcOutput" = true and "rev" = $1\n UNION ALL\n SELECT o."rev", o."previous" FROM "Output" o\n INNER JOIN revUpdates r ON o."previous" = r."rev"\n )\n SELECT * FROM revUpdates',values:[t]});const s=await ct.any(e);const a=Object.fromEntries(s.map((t=>[t.previous,t.rev])));let n=t;for(;a[n];)n=a[n];return n}static async getLatestRevs(t){return Promise.all(t.map(this.getLatestRev))}static async getIdsByMod(t){const e=new Nt({name:`Output.getIdsByMod.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1',values:[t]});return(await ct.any(e)).map((t=>t.rev))}static sqlSuffix(t,e,s){let a="";return s&&(a+=` order by "timestamp" ${s}`),a+=` limit ${t||b}`,e&&(a+=` offset ${e}`),a}static async getRevsByPublicKey(t){const e=new Nt({name:`Output.getRevsByPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys")',values:[t]});return(await ct.any(e)).map((t=>t.rev))}static async getUnspentRevsByMod(t,e,s,a){const n=await this.getIdsByMod(t);const r=await this.getLatestRevs(n);const o=new Nt({name:`Output.getUnspentRevsByMod.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(e,s,a)}`,values:[r]});return(await ct.any(o)).map((t=>t.rev))}static async getUnspentRevsByPublicKey(t,e,s,a){const n=new Nt({name:`Output.getUnspentRevsByPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE $1 = ANY("publicKeys") AND "isTbcOutput" = true \n AND NOT EXISTS (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") \n ${this.sqlSuffix(e,s,a)}`,values:[t]});return(await ct.any(n)).map((t=>t.rev))}static async getUnspentRevsByModAndPublicKey(t,e,s,a,n){const r=await this.getUnspentRevsByPublicKey(e,s,a,n);const o=await this.getIdsByRevs(r);const i=new Nt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:'SELECT "rev" FROM "Output" WHERE "mod" = $1 AND "rev" = ANY($2)',values:[t,o]});const c=(await ct.any(i)).map((t=>t.rev));const l=await this.getLatestRevs(c);const p=new Nt({name:`Output.getLatestRevsByModAndPublicKey.${Math.random()}`,text:`SELECT "rev" FROM "Output" WHERE "rev" = ANY($1) ${this.sqlSuffix(s,a,n)}`,values:[l]});return(await ct.any(p)).map((t=>t.rev))}static async getUnspentTbcOutputs(t,e,s){const a=new Nt({name:`Output.getUnspentTbcOutputs.${Math.random()}`,text:`SELECT "rev", "address", "satoshis", "scriptPubKey", "publicKeys", "timestamp"\n FROM "Output" WHERE "isTbcOutput" = true AND NOT EXISTS\n (SELECT 1 FROM "Input" ip WHERE "ip"."outputSpent" = "Output"."rev") ${this.sqlSuffix(t,e,s)}`});return(await ct.any(a)).map((t=>t.rev))}static async query(t){const{publicKey:e,limit:s,offset:a,ids:n,mod:r,order:o}=t;const i=parseInt(b||"",10);if(s&&parseInt(s||"",10)>i||n&&n.length>i)throw new Error(`Can't fetch more than ${b} revs.`);if(o&&"ASC"!==o&&"DESC"!==o)throw new Error("Invalid order. Should be ASC or DESC.");return n?(n.map(Ht),this.getLatestRevs(n)):r&&!e?this.getUnspentRevsByMod(r,s,a,o):!r&&e?this.getUnspentRevsByPublicKey(e,s,a,o):r&&e?this.getUnspentRevsByModAndPublicKey(r,e,s,a,o):this.getUnspentTbcOutputs(s,a,o)}}class Ct{static async select(t){return At.select(t)}static async insert(t){return At.insert(t)}static async eraseBlockHash(t){return At.eraseBlockHash(t)}static async updateBlockHash(t,e){return At.updateBlockHash(t,e)}static async listSentOutputs(t){return At.listSentOutputs(t)}static async listReceivedOutputs(t){return At.listReceivedOutputs(t)}static async listTxs(t){return At.listTxs(t)}static async getLatestRev(t){return At.getLatestRev(t)}static async getLatestRevs(t){return At.getLatestRevs(t)}static async getIdByRev(t){return At.getIdByRev(t)}static async query(t){return At.query(t)}}class Lt{static insert=async(t,e=null)=>{const a=function(t=$,e=v){switch(t){case"BTC":switch(e){case"mainnet":return s.bitcoin;case"testnet":return s.testnet;case"regtest":return s.regtest;default:throw new Error(`Invalid network ${e}`)}case"LTC":switch(e){case"mainnet":return s.litecoin;case"testnet":return s.litecointestnet;case"regtest":return s.litecoinregtest;default:throw new Error(`Invalid network ${e}`)}case"PEPE":switch(e){case"mainnet":return s.pepecoin;case"testnet":return s.pepecointestnet;case"regtest":return s.pepecoinregtest;default:throw new Error(`Invalid network ${e}`)}default:throw new Error(`Invalid chain ${t}`)}}($,v);const o=t.flatMap((t=>{const{zip:e,ownerData:s,onChainMetaData:o}=t;const{exp:i="",mod:c=""}=o;return t.tx.outs.map((({script:o,value:l},p)=>{const u=pr.fromOutputScript(o,a))),satoshis:Math.round(l),scriptPubKey:o.toString("hex"),isTbcOutput:u,publicKeys:u?s[p]._owners:[],mod:u?c:"",previous:u?e[p][0]:null,hash:u?n.sha256(Buffer.from(i||"")).toString("hex"):null,blockHash:null}}))}));if(await Ct.insert(o),e){const t=o.map((({rev:t})=>t));await Ct.updateBlockHash(t,e)}};static eraseBlockHash=async t=>{await Ct.eraseBlockHash(t)};static listSentOutputs=async t=>Ct.listSentOutputs(t);static listReceivedOutputs=async t=>Ct.listReceivedOutputs(t);static listTxs=async t=>Ct.listTxs(t);static getLatestRev=async t=>Ct.getLatestRev(t);static getLatestRevs=async t=>Ct.getLatestRevs(t);static getIdByRev=async t=>Ct.getIdByRev(t);static query=async t=>Ct.query(t)}class Pt{static update=async t=>ut.update(t);static selectByWorkerId=async t=>ut.selectByWorkerId(t);static deleteAll=async()=>ut.deleteAll();static setup=async t=>{0===await yt.count()&&(await mt.insert(1),V.info(`[wid 0 pid: ${process.pid}: registering block sync status on block 1`)),await ut.count()===t?V.info(`[wid 1 pid: ${process.pid}: all ${t} workers have already registered`):await Pt.register(t,await ut.min());const e=await Tt.selectAll();e.length>0&&(V.info(`[wid 0 pid: ${process.pid}: found ${e.length} orphans`),await $t.deleteByHash(e),await Mt.eraseBlockHash(e),await Lt.eraseBlockHash(e),await Tt.deleteAll())};static register=async(t,e)=>{const s=[];let a=Math.max(1,e);for(let e=1;e<=t;e+=1,a+=1)s.push(e,a);V.info(`[wid 0 pid: ${process.pid}: reorging sync status for ${t} workers...${s}`),await ut.deleteAll(),await ut.insertBatch(s)}}class Dt{static async getTransaction(t){const{result:e}=await nt.getTransaction(t);return e}static async getBulkTransactions(t){return(await Promise.all(t.map((t=>nt.getRawTransaction(t,0))))).map((t=>t.result))}static async getRawTransaction(t,e){const{result:s}=await nt.getRawTransaction(t,e);return s}static async getRawTransactionsJSON(t){return{txId:(e=(await nt.getRawTransactionJSON(t,1)).result).txid,txHex:e.hex,vsize:e.vsize,version:e.version,locktime:e.locktime,ins:e.vin.map((t=>t.coinbase?{coinbase:t.coinbase,sequence:t.sequence}:{txId:t.txid,vout:t.vout,script:t.scriptSig.hex,sequence:t.sequence})),outs:e.vout.map((t=>{let e;return t.scriptPubKey.addresses?[e]=t.scriptPubKey.addresses:e=t.scriptPubKey.address?t.scriptPubKey.address:void 0,{address:e,script:t.scriptPubKey.hex,value:Math.round(1e8*t.value)}}))};var e}static async sendRawTransaction(t){const{result:e,error:s}=await nt.sendRawTransaction(t);if(s)throw V.error(s),new Error("Error sending transaction");return e}static getUtxos=async t=>(void 0===(await nt.getaddressinfo(t)).result.timestamp&&(V.info(`Importing address: ${t}`),await nt.importaddress(t,!1)),(await nt.listunspent(0,999999,[t])).result);static waitForRpcBlockHash=async(t,e)=>(await w((async()=>{let s;try{s=await nt.getBlockHash(t)}catch(s){throw V.info(`[wid ${e} pid: ${process.pid}]: waiting for RPC to get block ${t} ...`),s}return s}),{startingDelay:1e4,timeMultiple:1,numOfAttempts:720})).result;static getBlock=async(t,e)=>nt.getBlock(t,e);static walletSetup=async()=>{if("regtest"===v){if(V.info(`Node is starting for chain ${$} and network ${v}, \n\n. Starting Wallet setup.`),"LTC"===$){const{result:t}=await nt.getBlockchainInfo();const e=t.blocks;if(e{try{await nt.createwallet(L,!1,!1,"",!1,!1)}catch(t){if(t.message.includes("already exists"))return void V.info(`Wallet ${L} already exists`);V.error(`Wallet creation failed with error '${t.message}'`)}};static checkBlockchainProgress=async()=>{const t=await w((async()=>{const t=await nt.getBlockchainInfo();const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const{blocks:s}=t.result;if(V.info(`Zmq. Bitcoind { percentage:${e}%, blocks:${s} }`),parseFloat(t.result.verificationprogress)<=.7)throw new Error("Node not ready yet");return t}),{startingDelay:6e4,timeMultiple:1,numOfAttempts:8760});const e=(100*parseFloat(t.result.verificationprogress)).toFixed(4);const s=t.result.blocks;V.info(`BCN reaches sync end...at { bitcoind.progress:${e}%, bitcoindSyncedHeight:${s} }`)}}class Ft{static get=async t=>Dt.getTransaction(t);static getRaw=async t=>Dt.getBulkTransactions(t);static getRawJSON=async t=>Dt.getRawTransactionsJSON(t);static sendRaw=async t=>Dt.sendRawTransaction(t);static getUtxos=async t=>Dt.getUtxos(t);static waitForRpcBlockHash=async(t,e)=>Dt.waitForRpcBlockHash(t,e);static insertRpcBlock=async(t,e,s="LTC")=>{const{result:a}=await Dt.getBlock(t,2);const{tx:n}=a;let r=n;"LTC"===s&&(r=n.filter((t=>"08"!==t.hex.slice(10,12))));const o=`[wid ${e} pid: ${process.pid}: backfilling height ${a.height} - backfilling ${r.length} txs `;"LTC"===s&&o.concat(`(${n.length-r.length} mweb tx's filtered)...`),V.info(o);const i=[];for(const t of r)try{let{hex:e}=t;e||(e=(await Dt.getRawTransaction(t.txid,1)).hex);const s=E.txFromHex({hex:e});s&&i.push(s)}catch(s){V.error(`[wid ${e} pid: ${process.pid}: failed to parse transaction in block ${a.height}\n error message: ${s.message}\n transaction: ${JSON.stringify(t)}`)}try{await Lt.insert(i,t),await Mt.insert(i,t)}catch(t){V.error(`[wid ${e} pid: ${process.pid}: inserting inputs and outputs for block ${a.height} failed with error '${t.message}'`)}};static walletSetup=async()=>Dt.walletSetup()}class Ut{static syncTxs=async(t,e,s)=>{for(V.info(`[wid ${e} pid: ${process.pid}]: starting to sync txs from block: ${t} - numWorkers: ${s}`);;){try{const s=await $t.waitForDbBlockHash(t,e);await Ft.insertRpcBlock(s,e,$)}catch(s){V.error(`[wid ${e} pid: ${process.pid}: syncing block num ${t} failed with error '${s.message}'`)}t+=s,await Pt.update({blockToSync:t,workerId:e})}};static findOrphans=async t=>{const e=await nt.getBlock(t,2);if(1===e?.result.height)return[];const s=await $t.selectByHeight(e.result.height-1);return e?.result.previousblockhash===s.hash?[]:[...await Ut.findOrphans(e.result.previousblockhash),s.hash]};static updateStatus=async(t,e)=>{await mt.update(t);const s=await ut.min();V.info(`[wid 0 pid: ${process.pid}: min: ${s}, height to resume: ${t}`);const a=Math.min(s,t);V.info(`[wid 0 pid: ${process.pid}: reorg detected, resuming at block ${a}`),await Pt.register(e,a)};static registerOrphans=async(t,e,s)=>{try{V.info(`[wid 0 pid: ${process.pid}: block reorg detected at height ${e} [!] orphans ${t}`),await Tt.insertAll(t),V.info(`[wid 0 pid: ${process.pid}: detected ${t.length} orphaned blocks`),await this.updateStatus(e,s),V.info(`[wid 0 pid: ${process.pid}: resuming at height ${e} [!] exiting ...`)}catch(t){V.error(`[wid 0 pid: ${process.pid}: failed to register orphans with error '${t.message}'`)}};static syncBlocks=async(t,e,s)=>{let a="";let n="";for(V.info(`[wid ${e} pid: ${process.pid}]: starting to sync block: ${t}`);;){try{n=await Ft.waitForRpcBlockHash(t,e),V.info(`[wip ${e} pid: ${process.pid}: synchronizing block num ${t} hash ${n}`);const r=await this.findOrphans(n);r.length&&(await this.registerOrphans(r,t-r.length,s),process.exit(0)),await $t.insert({hash:n,height:t,previousHash:a})}catch(s){V.error(`[wid ${e} pid: ${process.pid}: error block num ${t} failed to sync with error '${s.message}'`)}t+=1,a=n,await mt.update(t)}}}o(t);let Wt=c();k&&parseInt(k,10)>0&&(Wt=parseInt(k,10));const _t=i.worker?i.worker.id:0;V.info(`[wid ${_t} pid: ${process.pid}]: starting with ${Wt} threads`);try{if(await(async()=>{await w((()=>ct.connect()),{startingDelay:500})})(),V.info(`[wid ${_t} pid: ${process.pid}]: connected to the database successfully`),i.isPrimary){V.info(`[wid ${_t} pid: ${process.pid}]: parameters { url: ${x}, chain:${$} network:${v} numWorkers: ${Wt}}`),await Pt.setup(Wt);for(let t=1;t<=Wt;t+=1)V.info(`[wid ${_t} pid: ${process.pid}: launching worker ${t}`),i.fork();i.on("exit",((t,e,s)=>{V.info(`[wid ${_t} pid: ${process.pid}]: worker ${t.process.pid} died with code ${e} and signal ${s}`),V.error(`[wid ${_t} pid: ${process.pid}]: aborting`),process.exit(0)}));const t=await mt.select();await Ut.syncBlocks(t,_t,Wt)}else{const t=await Pt.selectByWorkerId(_t);await Ut.syncTxs(t.blockToSync,t.workerId,Wt)}}catch(t){V.error(`[wid ${_t} pid: ${process.pid}]: synchronizing failed with error '${t.message}'`)} diff --git a/packages/node/docker-compose.yml b/packages/node/docker-compose.yml index fe6dbaab8..8cc88a27c 100644 --- a/packages/node/docker-compose.yml +++ b/packages/node/docker-compose.yml @@ -14,7 +14,7 @@ services: - POSTGRES_PORT=${POSTGRES_PORT} - POSTGRES_HOST=${POSTGRES_HOST} volumes: - - ./data/db-data:/var/lib/postgresql/data + - ./chain-setup/${BCN_CHAIN}/${BCN_NETWORK}/db-data:/var/lib/postgresql/data - ./db/db_schema.sql:/docker-entrypoint-initdb.d/db_schema.sql node: image: ${BITCOIN_IMAGE} @@ -25,7 +25,7 @@ services: - ${BITCOIN_RPC_PORT}:${BITCOIN_RPC_PORT} - ${BCN_ZMQ_PORT}:${BCN_ZMQ_PORT} volumes: - - ./data/blockchain-data:${BITCOIN_DATA_DIR} + - ./chain-setup/${BCN_CHAIN}/${BCN_NETWORK}/blockchain-data:${BITCOIN_DATA_DIR} - ./${BITCOIN_CONF_FILE}:${BITCOIN_DATA_DIR}/${BITCOIN_CONF_FILE} bcn: image: bitcoin-computer-node diff --git a/packages/node/package.json b/packages/node/package.json index 01cc76f58..c8903b73e 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -38,7 +38,7 @@ "start": "node dist/bcn.es.mjs", "sync": "node dist/bcn.sync.es.mjs", "setup": "./scripts/setup.py", - "reset": "yes | ./scripts/reset.sh ; rm -rf data", + "reset": "yes | ./scripts/reset.sh", "test": "mocha --config .mocharc.json ", "test-and-show": "npm run test 2>&1 | tee node-test.log && open node-test.log", "test-unit": "mocha --config .mocharc.json ", diff --git a/packages/node/scripts/reset.sh b/packages/node/scripts/reset.sh index d44ae6e98..7dc571b33 100755 --- a/packages/node/scripts/reset.sh +++ b/packages/node/scripts/reset.sh @@ -26,22 +26,19 @@ EOF # docker ps -a --format="{{.ID}}" | xargs docker update --restart=no | xargs docker stop docker stop $(docker ps -a -q) & docker update --restart=no $(docker ps -a -q) & systemctl restart docker -# Delete all volumes: -docker volume rm $(docker volume ls -q) - # Delete all containers: docker rm -f $(docker ps -a -q) -# delete all dangling containers, images, volumes, networks +# delete all dangling containers, images, networks docker system prune docker network prune -docker volume prune # uncomment to delete all stopped containers and unused images # docker system prune -a # delete data files -rm -rf ./data/ +rm -rf ./chain-setup/**/**/db-data/ + # delete the logs yes | rm -r logs rm error.log