diff --git a/constants/index.ts b/constants/index.ts index c89a7c59..de1daea1 100644 --- a/constants/index.ts +++ b/constants/index.ts @@ -166,7 +166,7 @@ export const HUMAN_READABLE_DATE_FORMAT = 'YYYY-MM-DD' export const PRICE_API_DATE_FORMAT = 'YYYY-MM-DD' export const PRICE_API_TIMEOUT = 40 * 1000 // 40 seconds -export const PRICE_API_MAX_RETRIES = 3 +export const PRICE_API_MAX_RETRIES = 1 export const SYNC_TXS_JOBS_MAX_RETRIES = 3 export const SYNC_TXS_JOBS_RETRY_DELAY = 2000 @@ -277,9 +277,9 @@ export const MEMPOOL_PROCESS_DELAY = 100 // On chronik, the max allowed is 200 export const CHRONIK_FETCH_N_TXS_PER_PAGE = 200 -export const INITIAL_ADDRESS_SYNC_FETCH_CONCURRENTLY = 512 -export const TX_EMIT_BATCH_SIZE = 10_000 // for our generator, not chronik -export const DB_COMMIT_BATCH_SIZE = 10_000 // tamanho dos lotes para commit no DB +export const INITIAL_ADDRESS_SYNC_FETCH_CONCURRENTLY = 128 +export const TX_EMIT_BATCH_SIZE = 2_000 // for our generator, not chronik +export const DB_COMMIT_BATCH_SIZE = 2_000 // tamanho dos lotes para commit no DB export const TRIGGER_POST_CONCURRENCY = 100 export const TRIGGER_EMAIL_CONCURRENCY = 100 diff --git a/jobs/initJobs.ts b/jobs/initJobs.ts index 07291159..0a25b755 100644 --- a/jobs/initJobs.ts +++ b/jobs/initJobs.ts @@ -7,7 +7,15 @@ import { syncCurrentPricesWorker, syncBlockchainAndPricesWorker, cleanupClientPa EventEmitter.defaultMaxListeners = 20 const main = async (): Promise => { + // --- force fresh start --- const pricesQueue = new Queue('pricesSync', { connection: redisBullMQ }) + const blockchainQueue = new Queue('blockchainSync', { connection: redisBullMQ }) + const cleanupQueue = new Queue('clientPaymentCleanup', { connection: redisBullMQ }) + + await pricesQueue.obliterate({ force: true }) + await blockchainQueue.obliterate({ force: true }) + await cleanupQueue.obliterate({ force: true }) + await pricesQueue.add('syncCurrentPrices', {}, { @@ -21,18 +29,23 @@ const main = async (): Promise => { await syncCurrentPricesWorker(pricesQueue.name) - const blockchainQueue = new Queue('blockchainSync', { connection: redisBullMQ }) - await blockchainQueue.add('syncBlockchainAndPrices', {}, { jobId: 'syncBlockchainAndPrices' }) + await blockchainQueue.add('syncBlockchainAndPrices', + {}, + { + jobId: 'syncBlockchainAndPrices', + removeOnComplete: true, + removeOnFail: true + } + ) await syncBlockchainAndPricesWorker(blockchainQueue.name) - const cleanupQueue = new Queue('clientPaymentCleanup', { connection: redisBullMQ }) - await cleanupQueue.add( 'cleanupClientPayments', {}, { jobId: 'cleanupClientPayments', - removeOnFail: false, + removeOnComplete: true, + removeOnFail: true, repeat: { every: CLIENT_PAYMENT_EXPIRATION_TIME } diff --git a/jobs/workers.ts b/jobs/workers.ts index 0f0efefe..eaa5bbb6 100644 --- a/jobs/workers.ts +++ b/jobs/workers.ts @@ -56,9 +56,12 @@ export const syncBlockchainAndPricesWorker = async (queueName: string): Promise< }) worker.on('failed', (job, err) => { - if (job != null) { - console.error(`job ${job.id as string}: FAILED — ${err.message}`) - } + void (async () => { + if (job != null) { + console.error(`job ${job.id as string}: FAILED — ${err.message}`) + } + await multiBlockchainClient.destroy() + })() }) } diff --git a/package.json b/package.json index 132489b1..9dfc0d30 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "xecaddrjs": "^0.0.1" }, "devDependencies": { - "@types/jest": "^27.5.1", + "@types/jest": "^30.0.0", "@types/node": "^12.0.12", "@types/nodemailer": "^6.4.16", "@types/react": "^16.9.44", diff --git a/prisma-local/seeds/prices.csv b/prisma-local/seeds/prices.csv index fb1f9ff2..de995fff 100644 --- a/prisma-local/seeds/prices.csv +++ b/prisma-local/seeds/prices.csv @@ -2763,7 +2763,246 @@ BCH,2025-02-21,452.72380844148000,318.02452210423000 BCH,2025-02-22,469.26416887939000,329.61065729593000 BCH,2025-02-23,452.85358468831000,319.34622774228000 BCH,2025-02-24,408.32744565971000,286.26032000372000 -BCH,2025-02-25,402.10936872711000,281.13834867552000 +BCH,2025-02-25,424.48244044519000,296.44864515101000 +BCH,2025-02-26,430.63415218554000,300.12130196425000 +BCH,2025-02-27,397.39351676903000,275.15505716037000 +BCH,2025-02-28,453.46007799857000,313.65902051269000 +BCH,2025-03-01,460.08091937277000,318.03194924327000 +BCH,2025-03-02,456.26502258532000,315.98748196933000 +BCH,2025-03-03,443.11371293929000,306.10287581429000 +BCH,2025-03-04,510.92301946951000,354.45492053360000 +BCH,2025-03-05,580.96399507220000,405.43129821578000 +BCH,2025-03-06,576.33801357141000,402.84680149792000 +BCH,2025-03-07,550.00069363765000,382.64910678516000 +BCH,2025-03-08,545.27504675121000,379.36135718594000 +BCH,2025-03-09,517.59553405345000,360.26570106838000 +BCH,2025-03-10,484.69966325050000,335.50335997831000 +BCH,2025-03-11,481.91021776635000,333.59653449515000 +BCH,2025-03-12,485.62983419505000,337.25816387202000 +BCH,2025-03-13,472.14959358637000,327.22289889061000 +BCH,2025-03-14,478.52217956931000,332.54955319456000 +BCH,2025-03-15,487.37387925947000,338.70105233640000 +BCH,2025-03-16,480.44600775834000,334.19378445059000 +BCH,2025-03-17,475.33984255853000,332.30207561879000 +BCH,2025-03-18,476.79127725258000,333.14207864937000 +BCH,2025-03-19,494.90309760135000,345.28685183341000 +BCH,2025-03-20,476.52405554455000,332.56730783748000 +BCH,2025-03-21,469.52328836192000,325.52659781739000 +BCH,2025-03-22,466.43063516980000,323.40600865905000 +BCH,2025-03-23,471.55639573989000,328.84791172720000 +BCH,2025-03-24,471.18074860901000,328.98137148629000 +BCH,2025-03-25,475.59034533182000,333.18108600732000 +BCH,2025-03-26,480.70530313722000,336.89967630600000 +BCH,2025-03-27,451.77672942837000,315.64999284215000 +BCH,2025-03-28,441.29669547564000,306.85025586736000 +BCH,2025-03-29,439.94453556747000,305.91004802522000 +BCH,2025-03-30,427.53261733054000,298.62824306872000 +BCH,2025-03-31,442.34650486980000,307.34315538339000 +BCH,2025-04-01,433.67853569149000,303.03372498060000 +BCH,2025-04-02,432.53190211300000,304.29499319555000 +BCH,2025-04-03,434.45966740052000,308.25689848738000 +BCH,2025-04-04,427.94390511353000,300.76529860036000 +BCH,2025-04-05,423.47377102863000,297.62362232746000 +BCH,2025-04-06,377.26436655375000,265.19744313144000 +BCH,2025-04-07,393.90906532192000,278.12662153614000 +BCH,2025-04-08,388.47686726700000,273.15302561674000 +BCH,2025-04-09,422.08193327942000,300.03765962932000 +BCH,2025-04-10,415.31211440851000,297.10092785089000 +BCH,2025-04-11,412.82908760996310,296.99934360429000 +BCH,2025-04-12,499.19264965757000,360.18266960298000 +BCH,2025-04-13,475.69196752541000,342.90605536430000 +BCH,2025-04-14,460.74424031813000,332.11914509202000 +BCH,2025-04-15,446.87396719533000,320.78378762943000 +BCH,2025-04-16,465.80902421792000,335.30738858186000 +BCH,2025-04-17,454.00433306674000,328.00558114718000 +BCH,2025-04-18,466.28085786331000,336.87503096755000 +BCH,2025-04-19,468.19876328015000,338.26066461687000 +BCH,2025-04-20,468.20882374239000,339.36536809242000 +BCH,2025-04-21,478.79405645893000,347.04576629274000 +BCH,2025-04-22,499.63752386339000,361.40878358721000 +BCH,2025-04-23,497.19367663927000,358.46437779054000 +BCH,2025-04-24,501.90570342071000,361.42639644119000 +BCH,2025-04-25,518.74201944229000,373.62704599181000 +BCH,2025-04-26,491.63556679194000,354.03850271266000 +BCH,2025-04-27,488.03816538161000,351.89747705030000 +BCH,2025-04-28,512.88129112431000,369.89343530546000 +BCH,2025-04-29,503.29399437915000,363.94546348400000 +BCH,2025-04-30,507.08915490926000,367.43727123208000 +BCH,2025-05-01,508.97471598053000,368.31382474232000 +BCH,2025-05-02,510.14189698905000,369.11971129051000 +BCH,2025-05-03,503.95791026042000,364.62907678858000 +BCH,2025-05-04,497.18122995333000,360.20943212950000 +BCH,2025-05-05,496.69901589463000,359.26036909402000 +BCH,2025-05-06,511.17579150798000,370.68450912649000 +BCH,2025-05-07,576.11561643753000,416.14072010945000 +BCH,2025-05-08,580.76358934299000,417.31565379814000 +BCH,2025-05-09,580.25457609438000,416.17685213870000 +BCH,2025-05-10,579.23451490612000,415.44523213636000 +BCH,2025-05-11,568.00383916506000,408.00711361705000 +BCH,2025-05-12,555.69611419609000,397.79953411678000 +BCH,2025-05-13,571.72268349387000,410.32973892944000 +BCH,2025-05-14,550.69860438263000,394.14386187146000 +BCH,2025-05-15,556.14060668423000,399.01606184924000 +BCH,2025-05-16,557.04514950570000,398.72837125104000 +BCH,2025-05-17,558.93958448587000,400.08559785682000 +BCH,2025-05-18,536.21527159025000,383.97001051215000 +BCH,2025-05-19,548.21933868869000,392.97581491126000 +BCH,2025-05-20,554.40649956411000,399.12436967245000 +BCH,2025-05-21,575.83094991448000,415.78613503187000 +BCH,2025-05-22,619.05485098978000,447.75822546483000 +BCH,2025-05-23,591.29861528015000,430.45798804655000 +BCH,2025-05-24,582.87055930543000,424.37456210030000 +BCH,2025-05-25,582.11002969814000,424.99244700327000 +BCH,2025-05-26,573.99859975598000,417.88904345837000 +BCH,2025-05-27,576.79859378245000,416.94413586022000 +BCH,2025-05-28,583.19566321211000,421.05710031017000 +BCH,2025-05-29,566.02832319460000,409.59619131424000 +BCH,2025-05-30,552.13021600526000,401.77119928451000 +BCH,2025-05-31,557.49016756689000,405.72771556121000 +BCH,2025-06-01,553.46086264303000,403.35009229466000 +BCH,2025-06-02,556.82671797678000,405.41658587769000 +BCH,2025-06-03,553.95446744085000,403.55600487574000 +BCH,2025-06-04,548.98467973700000,401.33710211096000 +BCH,2025-06-05,528.59419369872000,386.75265681267000 +BCH,2025-06-06,544.75108819700000,397.78003713714000 +BCH,2025-06-07,557.25634375736000,406.86039773472000 +BCH,2025-06-08,566.19876556735000,413.65296276247000 +BCH,2025-06-09,577.45492770024000,421.07334153445000 +BCH,2025-06-10,603.84975743490000,441.46817630026000 +BCH,2025-06-11,589.30256833277000,431.44614101308000 +BCH,2025-06-12,558.39983172455000,409.55308659260000 +BCH,2025-06-13,591.39876482008000,435.15600222220000 +BCH,2025-06-14,609.02889144140000,448.12839221618000 +BCH,2025-06-15,619.06449678297000,455.79271086166000 +BCH,2025-06-16,649.33698310311000,477.84892627672000 +BCH,2025-06-17,650.14141566675000,475.91021137291000 +BCH,2025-06-18,636.71234304669000,463.97877956371000 +BCH,2025-06-19,674.52098850381000,492.51354951927000 +BCH,2025-06-20,649.74410721946000,473.14335133404000 +BCH,2025-06-21,635.38580108189000,462.68763960087000 +BCH,2025-06-22,626.57071329232000,455.60329778282000 +BCH,2025-06-23,631.91219721563000,460.51060904113000 +BCH,2025-06-24,651.72560517787000,474.65713445508000 +BCH,2025-06-25,685.05140796485000,499.07461212656000 +BCH,2025-06-26,673.80939429545000,493.76239375093000 +BCH,2025-06-27,684.27680723317000,500.03357582642000 +BCH,2025-06-28,675.88034216801000,492.96549518107000 +BCH,2025-06-29,682.83541682761000,499.84548403923000 +BCH,2025-06-30,713.96542897376000,524.66518198368000 +BCH,2025-07-01,690.43879202683000,505.76594404716000 +BCH,2025-07-02,685.46452805380000,504.35489427911000 +BCH,2025-07-03,659.56252998278000,485.89672412360000 +BCH,2025-07-04,660.64169712189000,485.31988769285000 +BCH,2025-07-05,663.45244667057000,487.37635095686000 +BCH,2025-07-06,678.30012403709000,497.44977213026000 +BCH,2025-07-07,679.18881262818000,497.39967632470000 +BCH,2025-07-08,696.37645852461000,509.14388591736000 +BCH,2025-07-09,700.14516540256000,512.02659456089000 +BCH,2025-07-10,718.08808449432000,524.22076221278000 +BCH,2025-07-11,706.59528278893000,515.90265421754000 +BCH,2025-07-12,692.62731092225000,505.58583227290000 +BCH,2025-07-13,702.01279728981000,512.54455280924000 +BCH,2025-07-14,673.45617939763000,491.81831914711000 +BCH,2025-07-15,682.52237406034000,497.74281874394000 +BCH,2025-07-16,680.14258072398000,495.86772200483000 +BCH,2025-07-17,734.71249625207000,534.69801120181000 +BCH,2025-07-18,708.89105591683000,516.17654342799000 +BCH,2025-07-19,719.83007503219000,524.39030606726000 +BCH,2025-07-20,723.97976631262000,527.71435445948000 +BCH,2025-07-21,709.82790324814000,518.48739057858000 +BCH,2025-07-22,719.16631855159000,528.57843501018000 +BCH,2025-07-23,677.51520733175000,498.04237135515000 +BCH,2025-07-24,712.85108629372000,522.19505920330000 +BCH,2025-07-25,758.50574277499000,553.75487700309000 +BCH,2025-07-26,783.15567949186000,571.60201969054000 +BCH,2025-07-27,810.65071887209000,591.72525135884000 +BCH,2025-07-28,794.26368296771000,578.24053588722000 +BCH,2025-07-29,780.08213863416000,566.42001331249000 +BCH,2025-07-30,805.48655218316000,582.85217535435000 +BCH,2025-07-31,781.09550326609000,563.78469325208000 +BCH,2025-08-01,748.90087520986000,542.33890789464000 +BCH,2025-08-02,732.64565998121000,530.84495162208000 +BCH,2025-08-03,761.41806397221000,552.59392492929000 +BCH,2025-08-04,780.16726902719000,565.79106733004000 +BCH,2025-08-05,770.06905020405000,559.24206270942000 +BCH,2025-08-06,787.92325846797000,573.78572391796000 +BCH,2025-08-07,789.65067660961000,574.52455368041000 +BCH,2025-08-08,810.04877058990000,588.89082228193000 +BCH,2025-08-09,787.52518441322000,572.51658203135000 +BCH,2025-08-10,793.84215528414000,577.30406141012000 +BCH,2025-08-11,814.97411136679000,591.64133879750000 +BCH,2025-08-12,831.81437559972000,603.71335767091000 +BCH,2025-08-13,848.72981083638000,616.78126735277000 +BCH,2025-08-14,825.85438306680000,598.33074184851000 +BCH,2025-08-15,823.73243552722000,595.97904390060000 +BCH,2025-08-16,814.19429331275000,589.07809811724000 +BCH,2025-08-17,786.82769284449000,569.99564828166000 +BCH,2025-08-18,783.41685507406000,567.27079760443000 +BCH,2025-08-19,768.06992099550000,553.68841209375000 +BCH,2025-08-20,778.91820171752000,561.40248345755000 +BCH,2025-08-21,778.80398434607000,559.66815638582000 +BCH,2025-08-22,818.64099347955000,591.55260858404000 +BCH,2025-08-23,822.16223815391000,594.11225071642000 +BCH,2025-08-24,791.94841881000000,572.68111983295000 +BCH,2025-08-25,749.09489532681000,540.71984971972000 +BCH,2025-08-26,769.08864543483000,555.67178353323000 +BCH,2025-08-27,769.31065674928000,558.29072134783000 +BCH,2025-08-28,761.84918387042000,554.00041585192000 +BCH,2025-08-29,731.80021906599000,532.17920665302000 +BCH,2025-08-30,754.21667207175000,548.46138390120000 +BCH,2025-08-31,733.66546880811000,533.95350798428000 +BCH,2025-09-01,782.24870911674000,568.76089471871000 +BCH,2025-09-02,822.86537108742000,596.47375672315000 +BCH,2025-09-03,810.37416178422000,586.77476047093000 +BCH,2025-09-04,819.86571947092000,593.92483408740000 +BCH,2025-09-05,846.84978389199000,611.99623045492000 +BCH,2025-09-06,827.59209465730000,598.04359532590000 +BCH,2025-09-07,830.09197420836000,600.20287082705000 +BCH,2025-09-08,803.30277413791000,582.27476644427000 +BCH,2025-09-09,800.65773068327000,578.43383383289000 +BCH,2025-09-10,819.95164773690000,590.84074226721000 +BCH,2025-09-11,825.58511859292000,596.45951813527000 +BCH,2025-09-12,833.88154970310000,601.84154285526000 +BCH,2025-09-13,822.13994271757000,593.37010970374000 +BCH,2025-09-14,835.35423043289000,603.69523820159000 +BCH,2025-09-15,819.30201198097000,595.08650776013000 +BCH,2025-09-16,824.83738888146000,599.69201662126000 +BCH,2025-09-17,878.12002210799000,636.59932934799000 +BCH,2025-09-18,843.64030883911000,610.91090169481000 +BCH,2025-09-19,832.81580391122000,604.34367687038000 +BCH,2025-09-20,826.88536070373000,600.04017321849000 +BCH,2025-09-21,787.41649178815000,570.66237322752000 +BCH,2025-09-22,776.67310675231000,561.67307770355000 +BCH,2025-09-23,776.80308228828000,561.04226510838000 +BCH,2025-09-24,765.29777947607000,550.74658665923000 +BCH,2025-09-25,749.32913346352000,537.56351329151000 +BCH,2025-09-26,757.72699204790000,543.38770988411000 +BCH,2025-09-27,751.76173276627000,539.10985174532000 +BCH,2025-09-28,778.77168903045000,559.25024763614000 +BCH,2025-09-29,779.62041601866000,560.18884395360000 +BCH,2025-09-30,782.11217766028000,562.12468297717000 +BCH,2025-10-01,815.02621603883000,584.68281872534000 +BCH,2025-10-02,840.55371378422000,601.93043244861000 +BCH,2025-10-03,829.27534685678000,593.76031708500000 +BCH,2025-10-04,840.44455518170000,601.71837582792000 +BCH,2025-10-05,834.78971307195000,598.26546248034000 +BCH,2025-10-06,833.88559815422000,597.39788084806000 +BCH,2025-10-07,802.75249191153000,574.63091444703000 +BCH,2025-10-08,808.11001442035000,579.19691406070000 +BCH,2025-10-09,807.69940907034000,576.52417061355000 +BCH,2025-10-10,743.33505925839000,530.29075031809000 +BCH,2025-10-11,730.09020804056000,520.84195330163000 +BCH,2025-10-12,755.64784652217000,539.83522103350000 +BCH,2025-10-13,739.98732943257000,526.65453098416000 +BCH,2025-10-14,752.52269821033000,536.30365340516000 +BCH,2025-10-15,732.36704845060000,522.08556402582000 +BCH,2025-10-16,705.40787171557000,502.41294529754000 +BCH,2025-10-17,669.27614449872000,477.18523011566000 +BCH,2025-10-18,650.08177771596000,463.49989498838000 +BCH,2025-10-19,673.55323575795000,480.44506721986000 +BCH,2025-10-20,655.44309302580000,466.20193668478000 +BCH,2025-10-21,669.40316392844000,478.23051539806000 +BCH,2025-10-22,661.48359871167000,473.01476862792000 XEC,2020-11-14,0.00001943000000,0.00001479000000 XEC,2020-11-15,0.00002010000000,0.00001530000000 XEC,2020-11-16,0.00001483000000,0.00001130000000 @@ -4327,4 +4566,243 @@ XEC,2025-02-21,0.00003721690746,0.00002614373043 XEC,2025-02-22,0.00003762775120,0.00002643047884 XEC,2025-02-23,0.00003612205057,0.00002546347346 XEC,2025-02-24,0.00003264817116,0.00002288818942 -XEC,2025-02-25,0.00003309567416,0.00002313913554 \ No newline at end of file +XEC,2025-02-25,0.00003369806769,0.00002353299186 +XEC,2025-02-26,0.00003478346603,0.00002424159593 +XEC,2025-02-27,0.00003189196431,0.00002208197893 +XEC,2025-02-28,0.00003319543842,0.00002296023291 +XEC,2025-03-01,0.00003294819651,0.00002277551343 +XEC,2025-03-02,0.00003421958575,0.00002368465237 +XEC,2025-03-03,0.00003015258060,0.00002082246935 +XEC,2025-03-04,0.00003195259482,0.00002216724247 +XEC,2025-03-05,0.00003427443731,0.00002391874494 +XEC,2025-03-06,0.00003328318092,0.00002326416558 +XEC,2025-03-07,0.00003214885965,0.00002236675803 +XEC,2025-03-08,0.00003193051533,0.00002221485047 +XEC,2025-03-09,0.00002951089932,0.00002054068115 +XEC,2025-03-10,0.00002872534744,0.00001989145349 +XEC,2025-03-11,0.00002893216814,0.00002002794436 +XEC,2025-03-12,0.00002959694350,0.00002055436079 +XEC,2025-03-13,0.00003003684608,0.00002081701220 +XEC,2025-03-14,0.00003041231950,0.00002113507732 +XEC,2025-03-15,0.00003076838668,0.00002138252662 +XEC,2025-03-16,0.00003062504230,0.00002130249522 +XEC,2025-03-17,0.00003182432685,0.00002224785074 +XEC,2025-03-18,0.00003150362834,0.00002201211459 +XEC,2025-03-19,0.00003187664353,0.00002223988079 +XEC,2025-03-20,0.00003130820961,0.00002185007632 +XEC,2025-03-21,0.00003168200599,0.00002196554650 +XEC,2025-03-22,0.00003151171538,0.00002184748181 +XEC,2025-03-23,0.00003170480990,0.00002210989104 +XEC,2025-03-24,0.00003241238551,0.00002262973298 +XEC,2025-03-25,0.00003290830519,0.00002305434702 +XEC,2025-03-26,0.00003255159011,0.00002281360347 +XEC,2025-03-27,0.00003016628589,0.00002107723441 +XEC,2025-03-28,0.00002901929856,0.00002017821407 +XEC,2025-03-29,0.00002886727055,0.00002007250325 +XEC,2025-03-30,0.00002846541424,0.00001988287279 +XEC,2025-03-31,0.00002869984762,0.00001994021213 +XEC,2025-04-01,0.00002790220212,0.00001950001581 +XEC,2025-04-02,0.00002767901400,0.00001947274949 +XEC,2025-04-03,0.00002741461307,0.00001945086491 +XEC,2025-04-04,0.00002773095145,0.00001948972235 +XEC,2025-04-05,0.00002751000277,0.00001933443636 +XEC,2025-04-06,0.00002400095666,0.00001686962594 +XEC,2025-04-07,0.00002490230109,0.00001758272018 +XEC,2025-04-08,0.00002460906824,0.00001730358230 +XEC,2025-04-09,0.00002615856952,0.00001859435053 +XEC,2025-04-10,0.00002645511325,0.00001892535712 +XEC,2025-04-11,0.00002646515984,0.00001903968334 +XEC,2025-04-12,0.00002761359874,0.00001992405080 +XEC,2025-04-13,0.00002710895736,0.00001954169140 +XEC,2025-04-14,0.00002753797135,0.00001985024815 +XEC,2025-04-15,0.00002688998028,0.00001930374401 +XEC,2025-04-16,0.00002656576114,0.00001912306445 +XEC,2025-04-17,0.00002770654112,0.00002001721010 +XEC,2025-04-18,0.00002788638171,0.00002014713996 +XEC,2025-04-19,0.00002845196416,0.00002055575764 +XEC,2025-04-20,0.00002883570963,0.00002090059118 +XEC,2025-04-21,0.00002853532294,0.00002068334576 +XEC,2025-04-22,0.00003057155016,0.00002211368488 +XEC,2025-04-23,0.00002998284453,0.00002161689139 +XEC,2025-04-24,0.00003251847893,0.00002343672468 +XEC,2025-04-25,0.00003473728158,0.00002501514534 +XEC,2025-04-26,0.00003279113993,0.00002361368230 +XEC,2025-04-27,0.00003103782320,0.00002237966711 +XEC,2025-04-28,0.00003139284776,0.00002264073286 +XEC,2025-04-29,0.00003068589727,0.00002218980005 +XEC,2025-04-30,0.00003043037784,0.00002204987996 +XEC,2025-05-01,0.00003159988090,0.00002286689816 +XEC,2025-05-02,0.00003088782829,0.00002234928425 +XEC,2025-05-03,0.00002963887951,0.00002144403973 +XEC,2025-05-04,0.00002975938552,0.00002156077244 +XEC,2025-05-05,0.00002972083431,0.00002149695804 +XEC,2025-05-06,0.00002880496544,0.00002088822408 +XEC,2025-05-07,0.00003060576719,0.00002210720494 +XEC,2025-05-08,0.00003221295725,0.00002314706287 +XEC,2025-05-09,0.00003442315907,0.00002468937355 +XEC,2025-05-10,0.00003456674579,0.00002479235846 +XEC,2025-05-11,0.00003471407325,0.00002490436721 +XEC,2025-05-12,0.00003387042658,0.00002424641737 +XEC,2025-05-13,0.00003537408568,0.00002538825162 +XEC,2025-05-14,0.00003342364883,0.00002390938661 +XEC,2025-05-15,0.00003311164370,0.00002375672179 +XEC,2025-05-16,0.00003180961544,0.00002276913170 +XEC,2025-05-17,0.00003183865423,0.00002278991749 +XEC,2025-05-18,0.00003080746828,0.00002206332953 +XEC,2025-05-19,0.00003179070094,0.00002278828149 +XEC,2025-05-20,0.00003177231463,0.00002287329795 +XEC,2025-05-21,0.00003289313535,0.00002375091095 +XEC,2025-05-22,0.00003329775257,0.00002408404131 +XEC,2025-05-23,0.00003120443617,0.00002271643881 +XEC,2025-05-24,0.00003042008319,0.00002214543966 +XEC,2025-05-25,0.00003052321567,0.00002228468065 +XEC,2025-05-26,0.00003055039725,0.00002224165057 +XEC,2025-05-27,0.00003062263594,0.00002213304264 +XEC,2025-05-28,0.00003051240420,0.00002202942382 +XEC,2025-05-29,0.00002926292988,0.00002117188305 +XEC,2025-05-30,0.00002761955402,0.00002010083623 +XEC,2025-05-31,0.00002952455637,0.00002148725037 +XEC,2025-06-01,0.00002928527582,0.00002134246431 +XEC,2025-06-02,0.00003012370384,0.00002193317600 +XEC,2025-06-03,0.00003045288104,0.00002218493348 +XEC,2025-06-04,0.00002938307749,0.00002148146448 +XEC,2025-06-05,0.00002838158238,0.00002076574529 +XEC,2025-06-06,0.00002910981553,0.00002125347025 +XEC,2025-06-07,0.00002907554342,0.00002122844772 +XEC,2025-06-08,0.00002925169985,0.00002137157333 +XEC,2025-06-09,0.00003034891944,0.00002213007511 +XEC,2025-06-10,0.00003126581325,0.00002285890305 +XEC,2025-06-11,0.00002997974839,0.00002196133541 +XEC,2025-06-12,0.00002758068716,0.00002022879470 +XEC,2025-06-13,0.00002824735854,0.00002078463525 +XEC,2025-06-14,0.00002806393011,0.00002064966713 +XEC,2025-06-15,0.00002769506432,0.00002039078078 +XEC,2025-06-16,0.00002670388607,0.00001966672048 +XEC,2025-06-17,0.00002602093020,0.00001904758887 +XEC,2025-06-18,0.00002576799774,0.00001877408708 +XEC,2025-06-19,0.00002648990312,0.00001934111686 +XEC,2025-06-20,0.00002590162786,0.00001886155315 +XEC,2025-06-21,0.00002480881086,0.00001806576432 +XEC,2025-06-22,0.00002424175294,0.00001762709675 +XEC,2025-06-23,0.00002589251588,0.00001886935924 +XEC,2025-06-24,0.00002626694477,0.00001913043256 +XEC,2025-06-25,0.00002621233028,0.00001910477926 +XEC,2025-06-26,0.00002558556782,0.00001874890929 +XEC,2025-06-27,0.00002537445448,0.00001854231953 +XEC,2025-06-28,0.00002566368557,0.00001871827108 +XEC,2025-06-29,0.00002587112021,0.00001893803731 +XEC,2025-06-30,0.00002493271928,0.00001832207718 +XEC,2025-07-01,0.00002482759167,0.00001818691314 +XEC,2025-07-02,0.00002588721364,0.00001904549903 +XEC,2025-07-03,0.00002562060036,0.00001887458007 +XEC,2025-07-04,0.00002521219799,0.00001852135757 +XEC,2025-07-05,0.00002481520433,0.00001822971851 +XEC,2025-07-06,0.00002541920583,0.00001864186324 +XEC,2025-07-07,0.00002499073115,0.00001830180556 +XEC,2025-07-08,0.00002562853466,0.00001873787025 +XEC,2025-07-09,0.00002654597452,0.00001941226011 +XEC,2025-07-10,0.00002890997152,0.00002110494191 +XEC,2025-07-11,0.00002880973145,0.00002102976857 +XEC,2025-07-12,0.00002875791671,0.00002099194621 +XEC,2025-07-13,0.00002994177628,0.00002186070453 +XEC,2025-07-14,0.00002831310759,0.00002067955869 +XEC,2025-07-15,0.00003033331203,0.00002212116233 +XEC,2025-07-16,0.00003038586846,0.00002215325404 +XEC,2025-07-17,0.00003180697100,0.00002314799901 +XEC,2025-07-18,0.00003068131522,0.00002234049239 +XEC,2025-07-19,0.00003326971169,0.00002423605320 +XEC,2025-07-20,0.00003348868335,0.00002441015583 +XEC,2025-07-21,0.00003225808120,0.00002356262450 +XEC,2025-07-22,0.00003268872177,0.00002402663818 +XEC,2025-07-23,0.00002945846887,0.00002165371287 +XEC,2025-07-24,0.00002939361219,0.00002153212551 +XEC,2025-07-25,0.00003110813441,0.00002271081176 +XEC,2025-07-26,0.00003145731348,0.00002296573351 +XEC,2025-07-27,0.00003245189538,0.00002368656396 +XEC,2025-07-28,0.00003063706215,0.00002230442058 +XEC,2025-07-29,0.00002990535657,0.00002171437036 +XEC,2025-07-30,0.00003033581464,0.00002195107480 +XEC,2025-07-31,0.00002830679957,0.00002043148405 +XEC,2025-08-01,0.00002810064263,0.00002036057141 +XEC,2025-08-02,0.00002754085836,0.00001995497472 +XEC,2025-08-03,0.00002800216330,0.00002032237749 +XEC,2025-08-04,0.00002844533598,0.00002062975377 +XEC,2025-08-05,0.00002765962885,0.00002008706607 +XEC,2025-08-06,0.00002780889918,0.00002025129729 +XEC,2025-08-07,0.00002879659704,0.00002095148216 +XEC,2025-08-08,0.00002992243729,0.00002175307135 +XEC,2025-08-09,0.00003014336816,0.00002191368410 +XEC,2025-08-10,0.00003065690519,0.00002229455284 +XEC,2025-08-11,0.00002922445209,0.00002121588124 +XEC,2025-08-12,0.00003001765765,0.00002178618382 +XEC,2025-08-13,0.00003043392814,0.00002211957722 +XEC,2025-08-14,0.00002897031444,0.00002098896620 +XEC,2025-08-15,0.00002875542977,0.00002080485459 +XEC,2025-08-16,0.00002919159924,0.00002112042777 +XEC,2025-08-17,0.00002792357882,0.00002022846750 +XEC,2025-08-18,0.00002756056798,0.00001995655988 +XEC,2025-08-19,0.00002746232892,0.00001979712117 +XEC,2025-08-20,0.00002790803949,0.00002011361241 +XEC,2025-08-21,0.00002751866600,0.00001977560641 +XEC,2025-08-22,0.00002918687622,0.00002109106928 +XEC,2025-08-23,0.00002875606758,0.00002077975762 +XEC,2025-08-24,0.00002859757866,0.00002067701906 +XEC,2025-08-25,0.00002747499938,0.00001983230380 +XEC,2025-08-26,0.00002823973228,0.00002040339888 +XEC,2025-08-27,0.00002824004431,0.00002049387275 +XEC,2025-08-28,0.00002816197758,0.00002047878717 +XEC,2025-08-29,0.00002768658380,0.00002013350092 +XEC,2025-08-30,0.00002770872689,0.00002014960324 +XEC,2025-08-31,0.00002685952943,0.00001954806458 +XEC,2025-09-01,0.00002717272707,0.00001975686809 +XEC,2025-09-02,0.00002722212401,0.00001973261136 +XEC,2025-09-03,0.00002657406501,0.00001924234624 +XEC,2025-09-04,0.00002677015231,0.00001939275895 +XEC,2025-09-05,0.00002676286642,0.00001934082488 +XEC,2025-09-06,0.00002708938098,0.00001957678842 +XEC,2025-09-07,0.00002708931297,0.00001958708664 +XEC,2025-09-08,0.00002755682348,0.00001997458925 +XEC,2025-09-09,0.00002789221614,0.00002015008864 +XEC,2025-09-10,0.00002812413488,0.00002026568856 +XEC,2025-09-11,0.00002832768908,0.00002046687433 +XEC,2025-09-12,0.00002880069953,0.00002078647435 +XEC,2025-09-13,0.00002904031811,0.00002095941547 +XEC,2025-09-14,0.00002839165125,0.00002051812757 +XEC,2025-09-15,0.00002731080025,0.00001983674946 +XEC,2025-09-16,0.00002763074114,0.00002008872912 +XEC,2025-09-17,0.00002814824170,0.00002040585296 +XEC,2025-09-18,0.00002804958866,0.00002031427746 +XEC,2025-09-19,0.00002707820760,0.00001964965538 +XEC,2025-09-20,0.00002719255377,0.00001973263217 +XEC,2025-09-21,0.00002502811153,0.00001813856031 +XEC,2025-09-22,0.00002482588645,0.00001795354046 +XEC,2025-09-23,0.00002506419033,0.00001810320157 +XEC,2025-09-24,0.00002451500681,0.00001763944933 +XEC,2025-09-25,0.00002416463267,0.00001733553956 +XEC,2025-09-26,0.00002446383101,0.00001754371330 +XEC,2025-09-27,0.00002417801013,0.00001733874297 +XEC,2025-09-28,0.00002462817145,0.00001768502905 +XEC,2025-09-29,0.00002431007082,0.00001746777045 +XEC,2025-09-30,0.00002436205424,0.00001750965195 +XEC,2025-10-01,0.00002586288235,0.00001855349270 +XEC,2025-10-02,0.00002601766586,0.00001863289184 +XEC,2025-10-03,0.00002649395277,0.00001896964363 +XEC,2025-10-04,0.00002649373797,0.00001896948983 +XEC,2025-10-05,0.00002609420595,0.00001870083201 +XEC,2025-10-06,0.00002652790082,0.00001900465936 +XEC,2025-10-07,0.00002495387105,0.00001786262377 +XEC,2025-10-08,0.00002504975041,0.00001795249916 +XEC,2025-10-09,0.00002441307767,0.00001742570218 +XEC,2025-10-10,0.00002020850733,0.00001441662731 +XEC,2025-10-11,0.00002043735041,0.00001457988258 +XEC,2025-10-12,0.00002193763638,0.00001567225902 +XEC,2025-10-13,0.00002194453456,0.00001561675170 +XEC,2025-10-14,0.00002191642971,0.00001561627700 +XEC,2025-10-15,0.00002104541567,0.00001499863926 +XEC,2025-10-16,0.00002039465210,0.00001452569164 +XEC,2025-10-17,0.00001996062879,0.00001423167002 +XEC,2025-10-18,0.00002012376993,0.00001434798754 +XEC,2025-10-19,0.00002106827322,0.00001502799575 +XEC,2025-10-20,0.00002011117279,0.00001430462507 +XEC,2025-10-21,0.00002004905008,0.00001432330779 +XEC,2025-10-22,0.00001974686104,0.00001412228124 \ No newline at end of file diff --git a/prisma-local/seeds/prices.ts b/prisma-local/seeds/prices.ts index 10d9a41d..a58966e6 100644 --- a/prisma-local/seeds/prices.ts +++ b/prisma-local/seeds/prices.ts @@ -33,7 +33,7 @@ export async function createPricesFile (): Promise { const prices: PriceFileData[] = [] await Promise.all(Object.values(NETWORK_TICKERS).map(async (networkTicker) => { - const pricesByNetworkTicker = await getAllPricesByNetworkTicker(networkTicker) + const pricesByNetworkTicker = await getAllPricesByNetworkTicker(networkTicker, true) pricesByNetworkTicker?.forEach(price => { if (isEmpty(price.Price_in_CAD) || isEmpty(price.Price_in_USD)) { throw new Error(`Price came back with at least one quote empty from API: ${JSON.stringify(price)}`) } diff --git a/prisma-local/seeds/productionTxs.csv b/prisma-local/seeds/productionTxs.csv index 4d4259be..712d91eb 100644 --- a/prisma-local/seeds/productionTxs.csv +++ b/prisma-local/seeds/productionTxs.csv @@ -131466,4 +131466,8 @@ fe95acd641f4ced15eb81dbfeb508124c322f8ca6cbb8e1868ea6024fcba3574,1000119.19,1754 baba23425aa5e5bb5c3b2b1b3e9a7f30dd4fe333f58be276700be85ff2cf49f0,1000023.22,1754861586,d1266537-9c17-4ec5-b09f-b05a81811c60,true 39c84db94eb3c7d9e9b9307060a840ba36dbefeb4add2713f81f911d7bcc3a3f,1000152.16,1754861467,d1266537-9c17-4ec5-b09f-b05a81811c60,truefb2dce1149b2a9a1722954c931964a061704a06910682e6ddd6f676e1c01e0af,1000043.31,1754947609,d1266537-9c17-4ec5-b09f-b05a81811c60,true 2db424bd1ae48962d0a2eea6bdf6d21fa02af129e7d54d8ecda02511d7d3760a,1000131.92,1754946651,d1266537-9c17-4ec5-b09f-b05a81811c60,true -eedd73ccf42e75872329a68f2cc0a98513bf58ad0fe8c2314c5a7ea5bb3430a3,1000035.45,1754946080,d1266537-9c17-4ec5-b09f-b05a81811c60,true \ No newline at end of file +eedd73ccf42e75872329a68f2cc0a98513bf58ad0fe8c2314c5a7ea5bb3430a3,1000035.45,1754946080,d1266537-9c17-4ec5-b09f-b05a81811c60,true924125cf4bb5cbe292d7a1f9a78ea2b03daeac23af8d5b51e8f122ca6dc21ebb,1000030.75,1761165795,d1266537-9c17-4ec5-b09f-b05a81811c60,true +298aa89e9d0e352492386d4e35f1f53135e6e79d6abdd2043cfcb73ef4e73fec,1000000,1761165132,d1266537-9c17-4ec5-b09f-b05a81811c60,true +746ad0dde27297c0c29f0e844b488fd06abdc5091e678a2a0e1b0b5c1d5efbd1,1000036.53,1761165079,d1266537-9c17-4ec5-b09f-b05a81811c60,true +d0180fa8ffb28760f64bdadb2fd7289196f003cd27a2adcee6e6419e848c1597,1000000,1761164173,d1266537-9c17-4ec5-b09f-b05a81811c60,true +f9f64587e7f7b11f771e130455b59fd84e5a3422215ac1f08505418fd395dc07,1000068.28,1761164139,d1266537-9c17-4ec5-b09f-b05a81811c60,true7e6312169483006fec8d6d1d9d4311c82f3334be4c58fe4a188cbc9e088de75c,1000716.35,1761169691,d1266537-9c17-4ec5-b09f-b05a81811c60,true \ No newline at end of file diff --git a/scripts/updateAllPriceConnections.ts b/scripts/updateAllPriceConnections.ts index e5a0b727..25d7647f 100644 --- a/scripts/updateAllPriceConnections.ts +++ b/scripts/updateAllPriceConnections.ts @@ -58,7 +58,8 @@ async function fixMisconnectedTxs (): Promise { select: { price: { select: { - timestamp: true + timestamp: true, + networkId: true } } } diff --git a/services/chronikService.ts b/services/chronikService.ts index 64330f95..56e3d3fa 100644 --- a/services/chronikService.ts +++ b/services/chronikService.ts @@ -181,19 +181,21 @@ export class ChronikBlockchainClient { } private clearOldMessages (): void { - const now = moment() + const now = moment().unix() + for (const key of Object.keys(this.lastProcessedMessages.unconfirmed)) { - const diff = moment.unix(this.lastProcessedMessages.unconfirmed[key]).diff(now) - if (diff > CHRONIK_MESSAGE_CACHE_DELAY) { - const { [key]: _, ...newConfirmed } = this.lastProcessedMessages.confirmed - this.lastProcessedMessages.confirmed = newConfirmed + const ageDiffMs = (now - Number(this.lastProcessedMessages.unconfirmed[key])) * 1000 + if (ageDiffMs > CHRONIK_MESSAGE_CACHE_DELAY) { + const { [key]: _, ...rest } = this.lastProcessedMessages.unconfirmed + this.lastProcessedMessages.unconfirmed = rest } } + for (const key of Object.keys(this.lastProcessedMessages.confirmed)) { - const diff = moment.unix(this.lastProcessedMessages.confirmed[key]).diff(now) - if (diff > CHRONIK_MESSAGE_CACHE_DELAY) { - const { [key]: _, ...newConfirmed } = this.lastProcessedMessages.confirmed - this.lastProcessedMessages.confirmed = newConfirmed + const ageDiffMs = (now - Number(this.lastProcessedMessages.confirmed[key])) * 1000 + if (ageDiffMs > CHRONIK_MESSAGE_CACHE_DELAY) { + const { [key]: _, ...rest } = this.lastProcessedMessages.confirmed + this.lastProcessedMessages.confirmed = rest } } } @@ -298,7 +300,7 @@ export class ChronikBlockchainClient { } /* - * For each address, fetch PAGE_CONCURRENCY pages in parallel (“burst”), + * For each address, fetch pages in parallel (“burst”), * then use the burst’s newest/oldest timestamps to decide whether to continue. * Yields happen only in the generator body (after each slice finishes, and at final flush). */ @@ -308,18 +310,20 @@ export class ChronikBlockchainClient { const logPrefix = `${this.CHRONIK_MSG_PREFIX}[PARALLEL FETCHING]` console.log( - `${logPrefix}: Will fetch latest txs for ${addresses.length} addresses ` + + `${logPrefix} >>> Will fetch latest txs for ${addresses.length} addresses ` + `(addressConcurrency=${INITIAL_ADDRESS_SYNC_FETCH_CONCURRENTLY}, pageConcurrency=1).` ) let chronikTxs: ChronikTxWithAddress[] = [] let lastBatchAddresses: string[] = [] + const totalCount = addresses.length + let syncedAlready = 0 for (let i = 0; i < addresses.length; i += INITIAL_ADDRESS_SYNC_FETCH_CONCURRENTLY) { const addressBatchSlice = addresses.slice(i, i + INITIAL_ADDRESS_SYNC_FETCH_CONCURRENTLY) lastBatchAddresses = addressBatchSlice.map(a => a.address) - console.log(`${logPrefix}: starting chronik fetching for ${addressBatchSlice.length} addresses...`) + console.log(`${logPrefix} >>> starting chronik fetching for ${addressBatchSlice.length} addresses... (${syncedAlready}/${totalCount} synced)`) const perAddressWorkers = addressBatchSlice.map(async (address) => { const addrLogPrefix = `${logPrefix} > ${address.address}:` @@ -329,6 +333,7 @@ export class ChronikBlockchainClient { let nextBurstBasePageIndex = 0 let hasReachedStoppingCondition = false + let newTxs = 0 while (!hasReachedStoppingCondition) { const pageIndex = nextBurstBasePageIndex let pageTxs: Tx[] = [] @@ -346,18 +351,20 @@ export class ChronikBlockchainClient { } const newestTs = Number(pageTxs[0].block?.timestamp ?? pageTxs[0].timeFirstSeen) - const oldestTs = Number(pageTxs[pageTxs.length - 1].block?.timestamp ?? pageTxs[pageTxs.length - 1].timeFirstSeen) if (newestTs < lastSyncedTimestampSeconds) { console.log(`${addrLogPrefix} NO NEW TXS`) break } + const oldestTs = Number(pageTxs[pageTxs.length - 1].block?.timestamp ?? pageTxs[pageTxs.length - 1].timeFirstSeen) + pageTxs = pageTxs .filter(txThresholdFilter) .filter(t => t.block === undefined || t.block.timestamp >= lastSyncedTimestampSeconds) - if (pageTxs.length > 0) { + const newTxsInThisPage = pageTxs.length + if (newTxsInThisPage > 0) { chronikTxs.push(...pageTxs.map(tx => ({ tx, address }))) } @@ -366,11 +373,16 @@ export class ChronikBlockchainClient { } nextBurstBasePageIndex += 1 - if (pageTxs.length === 0 && oldestTs < lastSyncedTimestampSeconds) { + if (newTxsInThisPage === 0 && oldestTs < lastSyncedTimestampSeconds) { hasReachedStoppingCondition = true } + newTxs += newTxsInThisPage + } + if (newTxs > 0) { + console.log(`${addrLogPrefix} ${newTxs} new txs.`) } }) + syncedAlready += addressBatchSlice.length await Promise.all( perAddressWorkers.map(async worker => @@ -378,18 +390,18 @@ export class ChronikBlockchainClient { ) ) - // Emit full chunks of chronik txs (addressesSynced vazio) + // Yield full TX batches when buffer reaches TX_EMIT_BATCH_SIZE while (chronikTxs.length >= TX_EMIT_BATCH_SIZE) { const chronikTxsSlice = chronikTxs.slice(0, TX_EMIT_BATCH_SIZE) chronikTxs = chronikTxs.slice(TX_EMIT_BATCH_SIZE) yield { chronikTxs: chronikTxsSlice, addressesSynced: [] } } - // Emit marcador para este slice (sem txs, só addressesSynced) + // Yield batch marker for completed address group yield { chronikTxs: [], addressesSynced: lastBatchAddresses } } - // Final flush de txs (addressesSynced vazio) + // Final TX flush after all addresses processed if (chronikTxs.length > 0) { const remaining = chronikTxs chronikTxs = [] @@ -579,13 +591,26 @@ export class ChronikBlockchainClient { } } + private async fetchTxWithRetry (txid: string, tries = 3, delayMs = 1000): Promise { + for (let i = 0; i < tries; i++) { + try { + return await this.chronik.tx(txid) + } catch (e: any) { + const msg = String(e?.message ?? e) + const is404 = /not found in the index|404/.test(msg) + if (!is404 || i === tries - 1) throw e + const delay = delayMs * Math.pow(2, i) + console.error(`Got a 404 Error trying to fetch tx ${txid} on the attempt number ${i + 1}, waiting ${(delay / 1000).toFixed(1)}s...`) + await new Promise(resolve => setTimeout(resolve, delay)) + } + } + throw new Error('unreachable') + } + private async processWsMessage (msg: WsMsgClient): Promise { - // delete unconfirmed transaction from our database - // if they were cancelled and not confirmed if (msg.type === 'Tx') { - const transaction = await this.chronik.tx(msg.txid) - const addressesWithTransactions = await this.getAddressesForTransaction(transaction) - + // delete unconfirmed transaction from our database + // if they were cancelled and not confirmed if (msg.msgType === 'TX_REMOVED_FROM_MEMPOOL') { console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`) const transactionsToDelete = await fetchUnconfirmedTransactions(msg.txid) @@ -598,36 +623,48 @@ export class ChronikBlockchainClient { } } } else if (msg.msgType === 'TX_CONFIRMED') { - console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`) - this.confirmedTxsHashesFromLastBlock = [...this.confirmedTxsHashesFromLastBlock, msg.txid] - for (const addressWithTransaction of addressesWithTransactions) { - const { amount, opReturn } = addressWithTransaction.transaction - await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address) + try { + const transaction = await this.fetchTxWithRetry(msg.txid) + const addressesWithTransactions = await this.getAddressesForTransaction(transaction) + console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`) + this.confirmedTxsHashesFromLastBlock = [...this.confirmedTxsHashesFromLastBlock, msg.txid] + for (const addressWithTransaction of addressesWithTransactions) { + const { amount, opReturn } = addressWithTransaction.transaction + await this.handleUpdateClientPaymentStatus(amount, opReturn, 'CONFIRMED' as ClientPaymentStatus, addressWithTransaction.address.address) + } + } catch (e) { + console.error(`${this.CHRONIK_MSG_PREFIX}: confirmed tx handler failed for ${msg.txid}`, e) } } else if (msg.msgType === 'TX_ADDED_TO_MEMPOOL') { - if (this.isAlreadyBeingProcessed(msg.txid, false)) { - return - } + if (this.isAlreadyBeingProcessed(msg.txid, false)) return + while (this.mempoolTxsBeingProcessed >= MAX_MEMPOOL_TXS_TO_PROCESS_AT_A_TIME) { await new Promise(resolve => setTimeout(resolve, MEMPOOL_PROCESS_DELAY)) } + this.mempoolTxsBeingProcessed += 1 - console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`) - const transaction = await this.chronik.tx(msg.txid) - const addressesWithTransactions = await this.getAddressesForTransaction(transaction) - await this.waitForSyncing(msg.txid, addressesWithTransactions.map(obj => obj.address.address)) - for (const addressWithTransaction of addressesWithTransactions) { - const { created, tx } = await upsertTransaction(addressWithTransaction.transaction) - if (tx !== undefined) { - const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, transaction, tx) - if (created) { // only execute trigger for newly added txs - await executeAddressTriggers(broadcastTxData, tx.address.networkId) + try { + console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] ${msg.txid}`) + const transaction = await this.fetchTxWithRetry(msg.txid) + const addressesWithTransactions = await this.getAddressesForTransaction(transaction) + await this.waitForSyncing(msg.txid, addressesWithTransactions.map(obj => obj.address.address)) + + for (const addressWithTransaction of addressesWithTransactions) { + const { created, tx } = await upsertTransaction(addressWithTransaction.transaction) + if (tx !== undefined) { + const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, transaction, tx) + if (created) { // only execute trigger for newly added txs + await executeAddressTriggers(broadcastTxData, tx.address.networkId) + } + const { amount, opReturn } = addressWithTransaction.transaction + await this.handleUpdateClientPaymentStatus(amount, opReturn, 'ADDED_TO_MEMPOOL' as ClientPaymentStatus, addressWithTransaction.address.address) } - const { amount, opReturn } = addressWithTransaction.transaction - await this.handleUpdateClientPaymentStatus(amount, opReturn, 'ADDED_TO_MEMPOOL' as ClientPaymentStatus, addressWithTransaction.address.address) } + } catch (e) { + console.error(`${this.CHRONIK_MSG_PREFIX}: mempool handler failed for ${msg.txid}`, e) + } finally { + this.mempoolTxsBeingProcessed = Math.max(0, this.mempoolTxsBeingProcessed - 1) } - this.mempoolTxsBeingProcessed -= 1 } } else if (msg.type === 'Block') { console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] Height: ${msg.blockHeight} Hash: ${msg.blockHash}`) @@ -638,6 +675,8 @@ export class ChronikBlockchainClient { } await this.syncBlockTransactions(msg.blockHash) console.log(`${this.CHRONIK_MSG_PREFIX}: [${msg.msgType}] Syncing done.`) + const subsCount = this.chronikWSEndpoint.subs.scripts.length + console.log(`${this.CHRONIK_MSG_PREFIX}: [INFO] *Currently Subscribed to ${subsCount} addresses*`) this.confirmedTxsHashesFromLastBlock = [] } } else if (msg.type === 'Error') { @@ -764,55 +803,69 @@ export class ChronikBlockchainClient { continue } - // são txs de fato - const pairsFromBatch: RowWithRaw[] = await Promise.all( - batch.chronikTxs.map(async ({ tx, address }) => { - const row = await this.getTransactionFromChronikTransaction(tx, address) - return { row, raw: tx } - }) - ) + const involvedAddrIds = new Set(batch.chronikTxs.map(({ address }) => address.id)) - for (const { row } of pairsFromBatch) { - perAddrCount.set(row.addressId, (perAddrCount.get(row.addressId) ?? 0) + 1) - } + try { + const pairsFromBatch: RowWithRaw[] = await Promise.all( + batch.chronikTxs.map(async ({ tx, address }) => { + const row = await this.getTransactionFromChronikTransaction(tx, address) + return { row, raw: tx } + }) + ) + + for (const { row } of pairsFromBatch) { + perAddrCount.set(row.addressId, (perAddrCount.get(row.addressId) ?? 0) + 1) + } - toCommit.push(...pairsFromBatch) + toCommit.push(...pairsFromBatch) - if (toCommit.length >= DB_COMMIT_BATCH_SIZE) { - const commitPairs = toCommit.slice(0, DB_COMMIT_BATCH_SIZE) - toCommit = toCommit.slice(DB_COMMIT_BATCH_SIZE) + if (toCommit.length >= DB_COMMIT_BATCH_SIZE) { + const commitPairs = toCommit.slice(0, DB_COMMIT_BATCH_SIZE) + toCommit = toCommit.slice(DB_COMMIT_BATCH_SIZE) - const rows = commitPairs.map(p => p.row) - const createdTxs = await createManyTransactions(rows) - console.log(`${this.CHRONIK_MSG_PREFIX} committed — created=${createdTxs.length}`) + const rows = commitPairs.map(p => p.row) + const createdTxs = await createManyTransactions(rows) + console.log(`${this.CHRONIK_MSG_PREFIX} committed — created=${createdTxs.length}`) - const createdForProd = createdTxs.filter(t => productionAddressesIds.includes(t.addressId)) - if (createdForProd.length > 0) { - await appendTxsToFile(createdForProd as unknown as Prisma.TransactionCreateManyInput[]) - } + const createdForProd = createdTxs.filter(t => productionAddressesIds.includes(t.addressId)) + if (createdForProd.length > 0) { + await appendTxsToFile(createdForProd as unknown as Prisma.TransactionCreateManyInput[]) + } - if (createdTxs.length > 0) { - const rawByHash = new Map(commitPairs.map(p => [p.raw.txid, p.raw])) - const triggerBatch: BroadcastTxData[] = [] - for (const createdTx of createdTxs) { - const raw = rawByHash.get(createdTx.hash) - if (raw == null) continue - const bd = this.broadcastIncomingTx(createdTx.address.address, raw, createdTx) - triggerBatch.push(bd) + if (createdTxs.length > 0) { + const rawByHash = new Map(commitPairs.map(p => [p.raw.txid, p.raw])) + const triggerBatch: BroadcastTxData[] = [] + for (const createdTx of createdTxs) { + const raw = rawByHash.get(createdTx.hash) + if (raw == null) continue + const bd = this.broadcastIncomingTx(createdTx.address.address, raw, createdTx) + triggerBatch.push(bd) + } + if (runTriggers && triggerBatch.length > 0) { + await executeTriggersBatch(triggerBatch, this.networkId) + } } - if (runTriggers && triggerBatch.length > 0) { - await executeTriggersBatch(triggerBatch, this.networkId) + } + } catch (err: any) { + console.error(`${this.CHRONIK_MSG_PREFIX}: ERROR in batch (scoped): ${err.message as string}`) + // Only mark addresses that were actually in this batch + for (const a of addresses) { + if (involvedAddrIds.has(a.id)) { + failedAddressesWithErrors[a.address] = err.stack ?? String(err) } } + continue } } // final DB flush (se sobrou menos que DB_COMMIT_BATCH_SIZE) if (toCommit.length > 0) { - const rows = toCommit.map(p => p.row) + const commitPairs = toCommit.slice() + toCommit = [] + + const rows = commitPairs.map(p => p.row) const createdTxs = await createManyTransactions(rows) console.log(`${this.CHRONIK_MSG_PREFIX} committed FINAL — created=${createdTxs.length}`) - toCommit = [] const createdForProd = createdTxs.filter(t => productionAddressesIds.includes(t.addressId)) if (createdForProd.length > 0) { @@ -820,7 +873,7 @@ export class ChronikBlockchainClient { } if (createdTxs.length > 0) { - const rawByHash = new Map(toCommit.map(p => [p.raw.txid, p.raw])) + const rawByHash = new Map(commitPairs.map(p => [p.raw.txid, p.raw])) const triggerBatch: BroadcastTxData[] = [] for (const createdTx of createdTxs) { const raw = rawByHash.get(createdTx.hash) @@ -841,7 +894,7 @@ export class ChronikBlockchainClient { const okAddresses = addresses.filter(a => !(a.address in failedAddressesWithErrors)) await updateManyLastSynced(okAddresses.map(a => a.address)) } catch (err: any) { - console.error(`${this.CHRONIK_MSG_PREFIX}: ERROR in parallel sync: ${err.message as string}`) + console.error(`${this.CHRONIK_MSG_PREFIX}: FATAL ERROR in parallel sync: ${err.message as string}`) addresses.forEach(a => { if ((perAddrCount.get(a.id) ?? 0) === 0) { failedAddressesWithErrors[a.address] = err.stack ?? String(err) @@ -851,7 +904,7 @@ export class ChronikBlockchainClient { const failed = Object.keys(failedAddressesWithErrors) const total = Object.values(successfulAddressesWithCount).reduce((p, c) => p + c, 0) - console.log(`${this.CHRONIK_MSG_PREFIX} (PARALLEL) Finished syncing ${total} txs for ${addresses.length} addresses with ${failed.length} errors.`) + console.log(`${this.CHRONIK_MSG_PREFIX} Finished syncing ${total} txs for ${addresses.length} addresses with ${failed.length} errors.`) console.timeEnd(`${this.CHRONIK_MSG_PREFIX} syncAddresses`) return { failedAddressesWithErrors, successfulAddressesWithCount } @@ -1011,7 +1064,7 @@ class MultiBlockchainClient { await newClient.subscribeInitialAddresses() })() ) - } else if (process.env.NODE_ENV === 'test') { + } else if (process.env.NODE_ENV === 'test' || process.env.JOBS_ENV !== undefined) { asyncOperations.push( (async () => { await newClient.waitForLatencyTest() diff --git a/services/priceService.ts b/services/priceService.ts index 2e70920c..2c75153c 100644 --- a/services/priceService.ts +++ b/services/priceService.ts @@ -2,12 +2,12 @@ import axios from 'axios' import { Prisma, Price } from '@prisma/client' import config from 'config' import prisma from 'prisma-local/clientInstance' -import { HUMAN_READABLE_DATE_FORMAT, PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES } from 'constants/index' +import { PRICE_API_TIMEOUT, PRICE_API_MAX_RETRIES, PRICE_API_DATE_FORMAT, RESPONSE_MESSAGES, NETWORK_TICKERS, XEC_NETWORK_ID, BCH_NETWORK_ID, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES } from 'constants/index' import { validatePriceAPIUrlAndToken, validateNetworkTicker } from 'utils/validators' import moment from 'moment' export function flattenTimestamp (timestamp: number): number { - const date = moment.utc((timestamp * 1000)) + const date = moment.utc(timestamp * 1000) const dateStart = date.startOf('day') return dateStart.unix() } @@ -47,6 +47,7 @@ export async function upsertPricesForNetworkId (responseData: IResponseData, net value: new Prisma.Decimal(responseData.Price_in_USD) } }) + await prisma.price.upsert({ where: { Price_timestamp_quoteId_networkId_unique_constraint: { @@ -89,70 +90,67 @@ function getAllPricesURLForNetworkTicker (networkTicker: string): string { return `${config.priceAPIURL}/dailyprices/${process.env.PRICE_API_TOKEN!}/${networkTicker}` } -export async function getPriceForDayAndNetworkTicker (day: moment.Moment, networkTicker: string, attempt: number = 1): Promise { - try { - const res = await axios.get(getPriceURLForDayAndNetworkTicker(day, networkTicker), { - timeout: PRICE_API_TIMEOUT - }) - - if (res.data.success !== false) { - const data = res.data - if (isResponseAsExpected(data)) return data - } - throw new Error(RESPONSE_MESSAGES.FAILED_TO_FETCH_PRICE_FROM_API_500(day.format(PRICE_API_DATE_FORMAT), networkTicker).message) - } catch (error: any) { - console.error(`Problem getting price of ${networkTicker} ${day.format(HUMAN_READABLE_DATE_FORMAT)} (attempt ${attempt}) ->\n${error as string}: ${error.message as string} `) - - if (attempt < PRICE_API_MAX_RETRIES) { - return await getPriceForDayAndNetworkTicker(day, networkTicker, attempt + 1) - } else { - throw error +async function withRetries ( + fn: () => Promise, + { maxRetries = PRICE_API_MAX_RETRIES, throwOnFailure = true, context = {} } = {} +): Promise { + let lastError: any + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn() + } catch (error) { + lastError = error + console.error(`[Retry ${attempt}/${maxRetries}] ${String(error)}`, { ...context }) + if (attempt < maxRetries) continue + if (throwOnFailure) throw lastError + console.error(`[Retry ${attempt}/${maxRetries}] skipping error:`) + console.error(String(lastError)) + return null } } + return null +} + +export async function getPriceForDayAndNetworkTicker (day: moment.Moment, networkTicker: string): Promise { + return await withRetries(async () => { + const res = await axios.get(getPriceURLForDayAndNetworkTicker(day, networkTicker), { timeout: PRICE_API_TIMEOUT }) + if (res.data.success !== false && isResponseAsExpected(res.data)) return res.data + throw new Error(RESPONSE_MESSAGES.FAILED_TO_FETCH_PRICE_FROM_API_500(day.format(PRICE_API_DATE_FORMAT), networkTicker).message) + }, { context: { day } }) } function isResponseAsExpected (data: any): boolean { - const isExpectedObj = data.Price_in_CAD !== undefined && data.Price_in_USD !== undefined + const isExpectedObj = data?.Price_in_CAD !== undefined && data?.Price_in_USD !== undefined if (isExpectedObj) return true - const values = Object.values(data) as unknown as any[] + const values = Object.values(data ?? {}) if (values.length > 0) { - const firstValueIsExpectedObj = values[0].Price_in_CAD !== undefined && values[0].Price_in_USD !== undefined - if (firstValueIsExpectedObj) return true + const first = values[0] as any + return first?.Price_in_CAD !== undefined && first?.Price_in_USD !== undefined } return false } -export async function getAllPricesByNetworkTicker (networkTicker: string, attempt: number = 1): Promise { - let lastError: any - try { - const res = await axios.get(getAllPricesURLForNetworkTicker(networkTicker), { - timeout: PRICE_API_TIMEOUT - }) +export async function getAllPricesByNetworkTicker ( + networkTicker: string, + throwOnFailure: true +): Promise - if (res.data.success !== false) { - const data = res.data - if (isResponseAsExpected(data)) { - const dailyPrices: IResponseDataDaily[] = Object.entries(res.data).map(([day, priceData]) => { - return { - day, - ...priceData - } - }) - return dailyPrices - } +export async function getAllPricesByNetworkTicker ( + networkTicker: string, + throwOnFailure: false +): Promise + +export async function getAllPricesByNetworkTicker ( + networkTicker: string, + throwOnFailure = true +): Promise { + return await withRetries(async () => { + const res = await axios.get(getAllPricesURLForNetworkTicker(networkTicker), { timeout: PRICE_API_TIMEOUT }) + if (res.data.success !== false && isResponseAsExpected(res.data)) { + return Object.entries(res.data).map(([day, priceData]) => ({ day, ...priceData })) } throw new Error(RESPONSE_MESSAGES.FAILED_TO_FETCH_PRICE_FROM_API_500('ALL_DAYS', networkTicker).message) - } catch (error: any) { - lastError = error - console.error(`Problem getting price of ${networkTicker} (attempt ${attempt}) ->\n${error as string}: ${error.response.statusText as string} `) - } - - if (attempt < PRICE_API_MAX_RETRIES) { - return await getAllPricesByNetworkTicker(networkTicker, attempt + 1) - } else { - console.error(`Price file could not be created after ${PRICE_API_MAX_RETRIES} retries`) - throw new Error(lastError.response.statusText) - } + }, { throwOnFailure }) } export async function syncPastDaysNewerPrices (): Promise { @@ -161,7 +159,6 @@ export async function syncPastDaysNewerPrices (): Promise { orderBy: [{ timestamp: 'desc' }], select: { timestamp: true } }) - if (lastPrice === null) throw new Error('No prices found, initial database seed did not complete successfully') const lastDateInDB = moment.unix(lastPrice.timestamp) @@ -173,26 +170,30 @@ export async function syncPastDaysNewerPrices (): Promise { date.add(-1, 'day') } - const allXECPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.ecash) - const allBCHPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.bitcoincash) - await Promise.all( - allXECPrices.filter(p => daysToRetrieve.includes(p.day)).map(async price => { - return await upsertPricesForNetworkId(price, XEC_NETWORK_ID, moment(price.day).unix()) - } + const allXECPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.ecash, false) + const allBCHPrices = await getAllPricesByNetworkTicker(NETWORK_TICKERS.bitcoincash, false) + + if (allXECPrices !== null) { + await Promise.all( + allXECPrices + .filter(p => daysToRetrieve.includes(p.day)) + .map(async price => await upsertPricesForNetworkId(price, XEC_NETWORK_ID, moment(price.day).unix())) ) - ) - await Promise.all( - allBCHPrices.filter(p => daysToRetrieve.includes(p.day)).map(async price => { - return await upsertPricesForNetworkId(price, BCH_NETWORK_ID, moment(price.day).unix()) - } + } + + if (allBCHPrices !== null) { + await Promise.all( + allBCHPrices + .filter(p => daysToRetrieve.includes(p.day)) + .map(async price => await upsertPricesForNetworkId(price, BCH_NETWORK_ID, moment(price.day).unix())) ) - ) + } + console.log('[PRICES] All past prices have been synced.') } export async function syncCurrentPrices (): Promise { const today = moment() - const bchPrice = await getPriceForDayAndNetworkTicker(today, NETWORK_TICKERS.bitcoincash) void upsertCurrentPricesForNetworkId(bchPrice, BCH_NETWORK_ID) @@ -202,32 +203,28 @@ export async function syncCurrentPrices (): Promise { export async function getCurrentPrices (): Promise { return await prisma.price.findMany({ - where: { - timestamp: 0 - } + where: { timestamp: 0 } }) } export async function getCurrentPricesForNetworkId (networkId: number): Promise { const currentPrices = await prisma.price.findMany({ - where: { - timestamp: 0, - networkId - } + where: { timestamp: 0, networkId } }) if (currentPrices.length !== N_OF_QUOTES) { throw new Error(RESPONSE_MESSAGES.NO_CURRENT_PRICES_FOUND_404.message) } return { - usd: currentPrices.filter((price) => price.quoteId === USD_QUOTE_ID)[0].value, - cad: currentPrices.filter((price) => price.quoteId === CAD_QUOTE_ID)[0].value + usd: currentPrices.filter(p => p.quoteId === USD_QUOTE_ID)[0].value, + cad: currentPrices.filter(p => p.quoteId === CAD_QUOTE_ID)[0].value } } export interface QuoteValues { - 'usd': Prisma.Decimal - 'cad': Prisma.Decimal + usd: Prisma.Decimal + cad: Prisma.Decimal } + export interface CreatePricesFromTransactionInput { timestamp: number networkId: number @@ -241,43 +238,43 @@ export interface SyncTransactionPricesInput { transactionId: string } -export async function fetchPricesForNetworkAndTimestamp (networkId: number, timestamp: number, prisma: Prisma.TransactionClient, attempt: number = 1): Promise { - const flattenedTimestamp = flattenTimestamp(timestamp) - const cadPrice = await prisma.price.findUnique({ - where: { - Price_timestamp_quoteId_networkId_unique_constraint: { - quoteId: CAD_QUOTE_ID, - networkId, - timestamp: flattenedTimestamp - } - } - }) - const usdPrice = await prisma.price.findUnique({ - where: { - Price_timestamp_quoteId_networkId_unique_constraint: { - quoteId: USD_QUOTE_ID, - networkId, - timestamp: flattenedTimestamp +export async function fetchPricesForNetworkAndTimestamp ( + networkId: number, + timestamp: number, + prismaTx: Prisma.TransactionClient, + tryRenewing = true +): Promise { + const getPrices = async (firstRun = true): Promise => { + const flattenedTimestamp = flattenTimestamp(timestamp) + try { + const cad = await prismaTx.price.findUniqueOrThrow({ + where: { Price_timestamp_quoteId_networkId_unique_constraint: { quoteId: CAD_QUOTE_ID, networkId, timestamp: flattenedTimestamp } } + }) + const usd = await prismaTx.price.findUniqueOrThrow({ + where: { Price_timestamp_quoteId_networkId_unique_constraint: { quoteId: USD_QUOTE_ID, networkId, timestamp: flattenedTimestamp } } + }) + return { cad, usd } + } catch (err) { + if (tryRenewing && firstRun) { + const ok = await renewPricesForTimestamp(flattenedTimestamp) + if (ok) return await getPrices(false) } + throw err } - }) - if (cadPrice === null || usdPrice === null) { - await renewPricesForTimestamp(flattenedTimestamp) - if (attempt < PRICE_API_MAX_RETRIES) { - return await fetchPricesForNetworkAndTimestamp(networkId, flattenedTimestamp, prisma, attempt + 1) - } - throw new Error(RESPONSE_MESSAGES.NO_PRICES_FOUND_404(networkId, flattenedTimestamp).message) - } - return { - cad: cadPrice, - usd: usdPrice } + return await getPrices() } -async function renewPricesForTimestamp (timestamp: number): Promise { - const xecPrice = await getPriceForDayAndNetworkTicker(moment(timestamp * 1000), NETWORK_TICKERS.ecash) - await upsertPricesForNetworkId(xecPrice, XEC_NETWORK_ID, timestamp) +async function renewPricesForTimestamp (timestamp: number): Promise { + try { + const xecPrice = await getPriceForDayAndNetworkTicker(moment(timestamp * 1000), NETWORK_TICKERS.ecash) + await upsertPricesForNetworkId(xecPrice, XEC_NETWORK_ID, timestamp) - const bchPrice = await getPriceForDayAndNetworkTicker(moment(timestamp * 1000), NETWORK_TICKERS.bitcoincash) - await upsertPricesForNetworkId(bchPrice, BCH_NETWORK_ID, timestamp) + const bchPrice = await getPriceForDayAndNetworkTicker(moment(timestamp * 1000), NETWORK_TICKERS.bitcoincash) + await upsertPricesForNetworkId(bchPrice, BCH_NETWORK_ID, timestamp) + + return true + } catch { + return false + } } diff --git a/services/transactionService.ts b/services/transactionService.ts index 044287eb..f17fc54d 100644 --- a/services/transactionService.ts +++ b/services/transactionService.ts @@ -1,6 +1,6 @@ import prisma from 'prisma-local/clientInstance' import { Prisma, Transaction } from '@prisma/client' -import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT } from 'constants/index' +import { RESPONSE_MESSAGES, USD_QUOTE_ID, CAD_QUOTE_ID, N_OF_QUOTES, UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT, SupportedQuotesType, NETWORK_IDS, PRICES_CONNECTION_BATCH_SIZE, PRICES_CONNECTION_TIMEOUT, HUMAN_READABLE_DATE_FORMAT } from 'constants/index' import { fetchAddressBySubstring, fetchAddressById, fetchAddressesByPaybuttonId, addressExists } from 'services/addressService' import { AllPrices, QuoteValues, fetchPricesForNetworkAndTimestamp, flattenTimestamp } from 'services/priceService' import _ from 'lodash' @@ -9,6 +9,7 @@ import { SimplifiedTransaction } from 'ws-service/types' import { OpReturnData, parseAddress } from 'utils/validators' import { generatePaymentFromTxWithInvoices } from 'redis/paymentCache' import { ButtonDisplayData, Payment } from 'redis/types' +import moment from 'moment' export function getTransactionValue (transaction: TransactionWithPrices | TransactionsWithPaybuttonsAndPrices | SimplifiedTransaction): QuoteValues { const ret: QuoteValues = { @@ -78,6 +79,7 @@ const resolveOpReturn = (opReturn: string): OpReturnData | null => { return null } } + const includePrices = { prices: { include: { @@ -95,7 +97,21 @@ const transactionWithPrices = Prisma.validator()( { include: includePrices } ) -export type TransactionWithPrices = Prisma.TransactionGetPayload +type TransactionWithPrices = Prisma.TransactionGetPayload + +const includeNetwork = { + address: { + select: { + networkId: true + } + } +} + +const transactionWithNetwork = Prisma.validator()( + { include: includeNetwork } +) + +type TransactionWithNetwork = Prisma.TransactionGetPayload const transactionWithAddressAndPrices = Prisma.validator()( { include: includeAddressAndPrices } @@ -379,7 +395,7 @@ export async function upsertTransaction ( const created = createdTx.createdAt.getTime() === createdTx.updatedAt.getTime() const networkId = await getTransactionNetworkId(createdTx) const ts = flattenTimestamp(createdTx.timestamp) - const allPrices = await fetchPricesForNetworkAndTimestamp(Number(networkId), ts, prisma) + const allPrices = await fetchPricesForNetworkAndTimestamp(Number(networkId), ts, prisma, true) await connectTransactionToPrices(createdTx, prisma, allPrices) const txWithPaybuttonsAndPrices = await fetchTransactionWithPaybuttonsAndPrices(createdTx.id) await CacheSet.txCreation(txWithPaybuttonsAndPrices) @@ -400,15 +416,17 @@ async function createPriceTxConnectionInChunks ( client: Prisma.TransactionClient | typeof prisma, rows: Prisma.PricesOnTransactionsCreateManyInput[] ): Promise { - console.log(`[PRICES] Inserting links in chunks of ${PRICES_CONNECTION_BATCH_SIZE}...`) + let pricesLinkedCount = 0 + console.log(`[PRICES] Inserting ${rows.length} price links...`) for (let i = 0; i < rows.length; i += PRICES_CONNECTION_BATCH_SIZE) { const slice = rows.slice(i, i + PRICES_CONNECTION_BATCH_SIZE) - await client.pricesOnTransactions.createMany({ + const result = await client.pricesOnTransactions.createMany({ data: slice, skipDuplicates: true }) + pricesLinkedCount += result.count } - console.log('[PRICES] Inserted all price links.') + console.log(`[PRICES] Inserted ${pricesLinkedCount} price links.`) } export async function connectTransactionToPrices ( @@ -424,51 +442,96 @@ export async function connectTransactionToPrices ( await createPriceTxConnectionInChunks(txClient, rows) } -export async function connectTransactionsListToPrices (txList: Transaction[]): Promise { +export async function connectTransactionsListToPrices ( + txList: TransactionWithNetwork[] +): Promise { if (txList.length === 0) return console.log(`[PRICES] Preparing to connect ${txList.length} txs to prices...`) // collect UNIQUE (networkId, timestamp) pairs const networkIdToTimestamps = new Map>() - await Promise.all(txList.map(async (t) => { - const networkId = await getTransactionNetworkId(t) + for (const t of txList) { + const networkId = t.address.networkId const ts = flattenTimestamp(t.timestamp) const set = networkIdToTimestamps.get(networkId) ?? new Set() set.add(ts) networkIdToTimestamps.set(networkId, set) - })) + } - // fetch AllPrices for each unique (networkId, timestamp) - const timestampToPrice: Record = {} + // ---- efficient fetch ---- + const priceByNetworkTs = new Map() let pairs = 0 + for (const [networkId, timestamps] of networkIdToTimestamps.entries()) { - for (const ts of timestamps) { - pairs++ - // not parallel - const allPrices = await fetchPricesForNetworkAndTimestamp(networkId, ts, prisma) - timestampToPrice[ts] = allPrices + const tsArray = [...timestamps] + pairs += tsArray.length + + // Bulk fetch all (CAD + USD) for this networkId + const prices = await prisma.price.findMany({ + where: { + networkId, + timestamp: { in: tsArray }, + quoteId: { in: [CAD_QUOTE_ID, USD_QUOTE_ID] } + } + }) + + // Group prices by timestamp + const grouped = new Map>() + for (const p of prices) { + const g = grouped.get(p.timestamp) ?? {} + if (p.quoteId === CAD_QUOTE_ID) g.cad = p + else if (p.quoteId === USD_QUOTE_ID) g.usd = p + grouped.set(p.timestamp, g) + } + + // Throw on missing price pairs + for (const ts of tsArray) { + const allPrices = grouped.get(ts) + const formattedDate = moment.unix(ts).format(HUMAN_READABLE_DATE_FORMAT) + + if (allPrices == null) { + throw new Error( + `[PRICES] No price record found for networkId=${networkId} at ${formattedDate}.` + ) + } + + if ((allPrices.cad == null) || (allPrices.usd == null)) { + throw new Error( + `[PRICES] Incomplete price data for networkId=${networkId} at ${formattedDate}. Partial data: ${JSON.stringify(allPrices, null, 2)}` + ) + } + + priceByNetworkTs.set(`${networkId}:${ts}`, allPrices as AllPrices) } } + console.log(`[PRICES] Loaded prices for ${pairs} (network,timestamp) pairs.`) // Build all join rows (2 per tx: USD + CAD) const rows: Prisma.PricesOnTransactionsCreateManyInput[] = [] for (const t of txList) { const ts = flattenTimestamp(t.timestamp) - const allPrices = timestampToPrice[ts] + const allPrices = priceByNetworkTs.get(`${t.address.networkId}:${ts}`) + if (allPrices == null) { + throw new Error(`[PRICES] Missing price pair for networkId ${t.address.networkId} at ${moment.unix(ts).format(HUMAN_READABLE_DATE_FORMAT)}.`) + } rows.push(...buildPriceTxConnectionInput(t, allPrices)) } console.log(`[PRICES] Built ${rows.length} price links (2 per tx).`) - await prisma.$transaction(async (tx) => { - console.log(`[PRICES] Disconnecting existing price links for ${txList.length} txs...`) - await tx.pricesOnTransactions.deleteMany({ - where: { transactionId: { in: txList.map(t => t.id) } } - }) - - await createPriceTxConnectionInChunks(tx, rows) - }, { timeout: PRICES_CONNECTION_TIMEOUT }) + await prisma.$transaction( + async (tx) => { + console.log( + `[PRICES] Disconnecting existing price links for ${txList.length} txs...` + ) + await tx.pricesOnTransactions.deleteMany({ + where: { transactionId: { in: txList.map((t) => t.id) } } + }) + await createPriceTxConnectionInChunks(tx, rows) + }, + { timeout: PRICES_CONNECTION_TIMEOUT } + ) } export async function connectAllTransactionsToPrices (): Promise { @@ -487,47 +550,62 @@ export async function connectAllTransactionsToPrices (): Promise { } interface TxDistinguished { - tx: Transaction + tx: TransactionWithNetwork isCreated: boolean } export async function createManyTransactions ( transactionsData: Prisma.TransactionUncheckedCreateInput[] ): Promise { - // we don't use `createMany` to ignore conflicts between the sync and the subscription - // and we don't return transactions that were updated, only ones that were created const insertedTransactionsDistinguished: TxDistinguished[] = [] - await prisma.$transaction(async (prisma) => { - for (const tx of transactionsData) { - const upsertedTx = await prisma.transaction.upsert({ - create: tx, - where: { - Transaction_hash_addressId_unique_constraint: { - hash: tx.hash, - addressId: tx.addressId - } - }, - update: { - confirmed: tx.confirmed, - timestamp: tx.timestamp + + await prisma.$transaction( + async (prisma) => { + const BATCH_SIZE = 50 + for (let i = 0; i < transactionsData.length; i += BATCH_SIZE) { + const batch = transactionsData.slice(i, i + BATCH_SIZE) + + const results = await Promise.all( + batch.map(async (tx) => + await prisma.transaction.upsert({ + create: tx, + where: { + Transaction_hash_addressId_unique_constraint: { + hash: tx.hash, + addressId: tx.addressId + } + }, + update: { + confirmed: tx.confirmed, + timestamp: tx.timestamp + }, + include: includeNetwork + }) + ) + ) + + for (const upsertedTx of results) { + insertedTransactionsDistinguished.push({ + tx: upsertedTx, + isCreated: upsertedTx.createdAt.getTime() === upsertedTx.updatedAt.getTime() + }) } - }) - insertedTransactionsDistinguished.push({ - tx: upsertedTx, - isCreated: upsertedTx.createdAt.getTime() === upsertedTx.updatedAt.getTime() - }) + } + }, + { + timeout: UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT } - }, - { - timeout: UPSERT_TRANSACTION_PRICES_ON_DB_TIMEOUT - } ) + const insertedTransactions = insertedTransactionsDistinguished - .filter(txD => txD.isCreated) - .map(txD => txD.tx) - void await connectTransactionsListToPrices(insertedTransactions) - const txsWithPaybuttonsAndPrices = await fetchTransactionsWithPaybuttonsAndPricesForIdList(insertedTransactions.map(tx => tx.id)) - void await CacheSet.txsCreation(txsWithPaybuttonsAndPrices) + .filter((txD) => txD.isCreated) + .map((txD) => txD.tx) + + await connectTransactionsListToPrices(insertedTransactions) + const txsWithPaybuttonsAndPrices = await fetchTransactionsWithPaybuttonsAndPricesForIdList(insertedTransactions.map((tx) => tx.id)) + + void CacheSet.txsCreation(txsWithPaybuttonsAndPrices) + return txsWithPaybuttonsAndPrices } @@ -551,27 +629,31 @@ export async function deleteTransactions (transactions: TransactionWithAddressAn )) } -export async function fetchAllTransactionsWithNoPrices (): Promise { +async function fetchAllTransactionsWithNoPrices (): Promise { const x = await prisma.transaction.findMany({ where: { prices: { none: {} } - } + }, + include: includeNetwork }) return x } -export async function fetchAllTransactionsWithIrregularPrices (): Promise { - return await prisma.$queryRaw` - SELECT t.* - FROM Transaction t - WHERE ( - SELECT COUNT(*) - FROM PricesOnTransactions pot - WHERE pot.transactionId = t.id - ) = 1; -` +export async function fetchAllTransactionsWithIrregularPrices (): Promise { + const grouped = await prisma.pricesOnTransactions.groupBy({ + by: ['transactionId'], + _count: { transactionId: true }, + having: { transactionId: { _count: { equals: 1 } } } + }) + + const ids = grouped.map(g => g.transactionId) + + return await prisma.transaction.findMany({ + where: { id: { in: ids } }, + include: includeNetwork + }) } export async function fetchTransactionsByPaybuttonId (paybuttonId: string, networkIds?: number[]): Promise { diff --git a/services/triggerService.ts b/services/triggerService.ts index d5b43cf1..862dc34a 100644 --- a/services/triggerService.ts +++ b/services/triggerService.ts @@ -564,7 +564,7 @@ export async function executeTriggersBatch (broadcasts: BroadcastTxData[], netwo const logs: Prisma.TriggerLogCreateManyInput[] = [] // Build queues - console.log(`[TRIGGER ${currency}]: will get triggers for ${txItems.length} txs and ${uniqueAddresses.length} addresses...`) + console.log(`[TRIGGER ${currency}]: preparing triggers for ${txItems.length} txs belonging to ${uniqueAddresses.length} addresses`) for (const { address, tx } of txItems) { const triggers = triggersByAddress.get(address) ?? [] @@ -602,7 +602,6 @@ export async function executeTriggersBatch (broadcasts: BroadcastTxData[], netwo ...Object.keys(emailTaskQueueByUser) ]).size} users` ) - const postUserRunners = Object.entries(postTaskQueueByUser).map(([userId, queue]) => async () => { const limit = userPostCredits[userId] ?? 0 const { accepted, attempted } = await runTasksUpToCredits(queue, limit) diff --git a/services/userService.ts b/services/userService.ts index 9e817b5b..e9d6e70f 100644 --- a/services/userService.ts +++ b/services/userService.ts @@ -74,8 +74,12 @@ function getUserSeedHash (userId: string): Buffer { export function getUserPrivateKey (userId: string): crypto.KeyObject { const seed = getUserSeedHash(userId) const prefixPrivateEd25519 = Buffer.from('302e020100300506032b657004220420', 'hex') - const der = Buffer.concat([prefixPrivateEd25519, seed]) - return crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' }) + + const der = new Uint8Array(prefixPrivateEd25519.length + seed.length) + der.set(prefixPrivateEd25519 instanceof Uint8Array ? prefixPrivateEd25519 : new Uint8Array(prefixPrivateEd25519)) + der.set(seed instanceof Uint8Array ? seed : new Uint8Array(seed), prefixPrivateEd25519.length) + const derBuf = Buffer.from(der) + return crypto.createPrivateKey({ key: derBuf, format: 'der', type: 'pkcs8' }) } export async function getUserPublicKeyHex (id: string): Promise { diff --git a/tests/unittests/chronikService.test.ts b/tests/unittests/chronikService.test.ts index 9aabd1d8..016e2901 100644 --- a/tests/unittests/chronikService.test.ts +++ b/tests/unittests/chronikService.test.ts @@ -20,7 +20,8 @@ jest.mock('chronik-client-cashtokens', () => ({ waitForOpen: jest.fn(), subscribeToBlocks: jest.fn(), subs: { scripts: [] } - }) + }), + tx: jest.fn(), // <-- needed }) }, ConnectionStrategy: { @@ -329,7 +330,7 @@ const originalEnv = process.env describe('ChronikBlockchainClient tests', () => { beforeEach(() => { - jest.resetModules() + jest.clearAllMocks() process.env = { ...originalEnv } process.env.WS_AUTH_KEY = 'test-auth-key' }) @@ -1269,3 +1270,251 @@ describe('Additional behavior and integration tests', () => { expect(p2shResult.hash160).toHaveLength(40) }) }) + +describe('Regression: mempool + retries + onMessage + cache TTL', () => { + it('TX_ADDED_TO_MEMPOOL never leaks in-flight counter on error', async () => { + process.env.WS_AUTH_KEY = 'test-auth-key' + const client = new ChronikBlockchainClient('ecash') + + // make internal state writable + Object.defineProperty(client as any, 'mempoolTxsBeingProcessed', { value: 0, writable: true }) + + // simulate mempool path with a failing chronik.tx + ;(client as any).chronik = { tx: jest.fn().mockRejectedValue(new Error('boom')) } + jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(false) + + const msg = { type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'tx123' } as any + await (client as any).processWsMessage(msg).catch(() => {}) + + // counter must be decremented in finally + expect((client as any).mempoolTxsBeingProcessed).toBe(0) + + // next run should still be accepted and complete + ;((client as any).chronik.tx as jest.Mock).mockResolvedValue({ txid: 'tx123', inputs: [], outputs: [] }) + jest.spyOn(client as any, 'getAddressesForTransaction').mockResolvedValue([]) + await (client as any).processWsMessage(msg) + expect((client as any).mempoolTxsBeingProcessed).toBe(0) + }) + + it('fetchTxWithRetry retries on 404 then succeeds', async () => { + process.env.WS_AUTH_KEY = 'test-auth-key' + const client = new ChronikBlockchainClient('ecash') + + // wait for async constructor to assign `chronik` + await new Promise(resolve => setImmediate(resolve)) + + const txMock = (client as any).chronik.tx as jest.Mock + txMock.mockReset() + + const err404 = new Error('Transaction not found in the index') + + // first call => reject with a 404-ish error + txMock.mockImplementationOnce(async () => { throw err404 }) + // second call => succeed + txMock.mockImplementationOnce(async () => ({ txid: 'txABC', inputs: [], outputs: [] })) + + const tx = await (client as any).fetchTxWithRetry('txABC', 3, 1) + + expect(tx).toBeDefined() + expect(tx.txid).toBe('txABC') + expect(txMock).toHaveBeenCalledTimes(2) + }) + + + it('clearOldMessages expires entries by TTL and from the correct maps', () => { + process.env.WS_AUTH_KEY = 'test-auth-key' + const client = new ChronikBlockchainClient('ecash') + + const nowSec = Math.floor(Date.now() / 1000) + // build state: one old and one fresh in each map + ;(client as any).lastProcessedMessages = { + unconfirmed: { u_old: nowSec - 999999, u_new: nowSec - 1 }, + confirmed: { c_old: nowSec - 999999, c_new: nowSec - 1 } + } + + // run cleanup + ;(client as any).clearOldMessages() + + // old entries should be gone, fresh ones should remain + expect((client as any).lastProcessedMessages.unconfirmed.u_old).toBeUndefined() + expect((client as any).lastProcessedMessages.confirmed.c_old).toBeUndefined() + expect((client as any).lastProcessedMessages.unconfirmed.u_new).toBeDefined() + expect((client as any).lastProcessedMessages.confirmed.c_new).toBeDefined() + }) +}) + +describe('WS onMessage matrix (no re-mocks)', () => { + beforeAll(() => { + process.env.WS_AUTH_KEY = 'test-auth-key' + }) + + let client: any + let fetchAddressesArray: jest.Mock + + beforeEach(() => { + jest.clearAllMocks() + + // fresh client + client = new ChronikBlockchainClient('ecash') + + // avoid any wait-paths that depend on async ctor + client.setInitialized() // initializing=false + + // ensure ws endpoint exists for BLK_FINALIZED logs + client.chronikWSEndpoint = { + subs: { scripts: [] }, + subscribeToBlocks: jest.fn(), + waitForOpen: jest.fn() + } as any + + // addressService mocks used by getAddressesForTransaction / waitForSyncing + ;({ fetchAddressesArray } = require('../../services/addressService')) + fetchAddressesArray.mockResolvedValue([ + { + id: 'addr-1', + address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h', + networkId: 1, + syncing: false, + lastSynced: new Date().toISOString() + } + ]) + + // never hit real payment layer in these tests + jest.spyOn(client as any, 'handleUpdateClientPaymentStatus').mockResolvedValue(undefined) + }) + + it('handles TX_REMOVED_FROM_MEMPOOL → deletes unconfirmed txs', async () => { + const { fetchUnconfirmedTransactions, deleteTransactions } = require('../../services/transactionService') + fetchUnconfirmedTransactions.mockResolvedValueOnce(['tx-to-del']) + deleteTransactions.mockResolvedValueOnce(undefined) + + await client.processWsMessage({ type: 'Tx', msgType: 'TX_REMOVED_FROM_MEMPOOL', txid: 'deadbeef' }) + + expect(fetchUnconfirmedTransactions).toHaveBeenCalledWith('deadbeef') + expect(deleteTransactions).toHaveBeenCalledWith(['tx-to-del']) + }) + + it('handles TX_CONFIRMED → uses fetchTxWithRetry and updates payments for related addresses', async () => { + const fetchSpy = jest.spyOn(client, 'fetchTxWithRetry').mockResolvedValue({ + txid: 'txCONF', + inputs: [], + outputs: [{ sats: 10n, outputScript: '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac' }] + }) + + // deterministic related addresses + jest.spyOn(client as any, 'getRelatedAddressesForTransaction') + .mockReturnValue(['ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h']) + + // minimal transaction shape for downstream + jest.spyOn(client as any, 'getTransactionFromChronikTransaction') + .mockResolvedValue({ + hash: 'txCONF', + amount: '0.01', + timestamp: Math.floor(Date.now() / 1000), + addressId: 'addr-1', + confirmed: false, + opReturn: JSON.stringify({ message: { type: 'PAY', paymentId: 'pid-1' } }) + }) + + const paySpy = jest.spyOn(client as any, 'handleUpdateClientPaymentStatus') + + await client.processWsMessage({ type: 'Tx', msgType: 'TX_CONFIRMED', txid: 'txCONF' }) + + expect(fetchSpy).toHaveBeenCalledWith('txCONF') + expect(paySpy).toHaveBeenCalled() + expect(client.confirmedTxsHashesFromLastBlock).toContain('txCONF') + }) + + it('handles TX_ADDED_TO_MEMPOOL → calls fetchTxWithRetry and upserts, triggers once', async () => { + const { upsertTransaction } = require('../../services/transactionService') + const { executeAddressTriggers } = require('../../services/triggerService') + + jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(false) + + jest.spyOn(client, 'fetchTxWithRetry').mockResolvedValue({ + txid: 'txMEM', + inputs: [], + outputs: [{ sats: 10n, outputScript: '76a914c5d2460186f7233c927e7db2dcc703c0e500b65388ac' }] + }) + + jest.spyOn(client as any, 'getAddressesForTransaction').mockResolvedValue([ + { + address: { address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h', networkId: 1 }, + transaction: { + hash: 'txMEM', + amount: '0.01', + timestamp: Math.floor(Date.now() / 1000), + addressId: 'addr-1', + confirmed: false, + opReturn: JSON.stringify({ message: { type: 'PAY', paymentId: 'pid-2' } }) + } + } + ]) + + upsertTransaction.mockResolvedValue({ + created: true, + tx: { address: { networkId: 1, address: 'ecash:qqkv9wr69ry2p9l53lxp635va4h86wv435995w8p2h' } } + }) + + await client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'txMEM' }) + + expect(client.fetchTxWithRetry).toHaveBeenCalledWith('txMEM') + expect(upsertTransaction).toHaveBeenCalledTimes(1) + expect(executeAddressTriggers).toHaveBeenCalledTimes(1) + expect(client.mempoolTxsBeingProcessed).toBe(0) + }) + + it('TX_ADDED_TO_MEMPOOL → short-circuits when already being processed', async () => { + const fetchSpy = jest.spyOn(client, 'fetchTxWithRetry') + jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(true) + + await client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'dup' }) + + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it('TX_ADDED_TO_MEMPOOL → retries on 404-ish twice then succeeds (uses fake timers)', async () => { + jest.useFakeTimers() + + let attempts = 0 + // drive underlying chronik.tx via the real fetchTxWithRetry + ;(client.chronik as any) = { tx: jest.fn(async () => { + attempts += 1 + if (attempts < 3) throw new Error('Transaction not found in the index') + return { txid: 'tx404', inputs: [], outputs: [] } + })} + + jest.spyOn(client as any, 'isAlreadyBeingProcessed').mockReturnValue(false) + jest.spyOn(client as any, 'getAddressesForTransaction').mockResolvedValue([]) + + const p = client.processWsMessage({ type: 'Tx', msgType: 'TX_ADDED_TO_MEMPOOL', txid: 'tx404' }) + + // advance 1s + 2s exponential backoffs + await jest.advanceTimersByTimeAsync(1000) + await jest.advanceTimersByTimeAsync(2000) + + await p + expect(attempts).toBe(3) + expect(client.mempoolTxsBeingProcessed).toBe(0) + + jest.useRealTimers() + }) + + it('handles Block → BLK_FINALIZED triggers sync and clears cache', async () => { + client.initializing = false + client.confirmedTxsHashesFromLastBlock = ['A', 'B'] + const syncSpy = jest.spyOn(client as any, 'syncBlockTransactions').mockResolvedValue(undefined) + + await client.processWsMessage({ type: 'Block', msgType: 'BLK_FINALIZED', blockHash: 'bh', blockHeight: 123 }) + + expect(syncSpy).toHaveBeenCalledWith('bh') + expect(client.confirmedTxsHashesFromLastBlock).toEqual([]) + }) + + it('handles type=Error → logs JSON payload', async () => { + const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}) + await client.processWsMessage({ type: 'Error', msg: { code: 42, reason: 'nope' } } as any) + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('[CHRONIK — ecash]: [Error]')) + logSpy.mockRestore() + }) +}) diff --git a/tests/unittests/transactionService.test.ts b/tests/unittests/transactionService.test.ts index 95efcfdd..cc3580e4 100644 --- a/tests/unittests/transactionService.test.ts +++ b/tests/unittests/transactionService.test.ts @@ -48,8 +48,8 @@ describe('Create services', () => { prismaMock.userProfile.findMany.mockResolvedValue([mockedUserProfile]) prisma.userProfile.findMany = prismaMock.userProfile.findMany - prismaMock.price.findUnique.mockResolvedValue(mockedUSDPriceOnTransaction.price) - prisma.price.findUnique = prismaMock.price.findUnique + prismaMock.price.findUniqueOrThrow.mockResolvedValue(mockedUSDPriceOnTransaction.price) + prisma.price.findUniqueOrThrow = prismaMock.price.findUniqueOrThrow prismaMock.pricesOnTransactions.upsert.mockResolvedValue(mockedUSDPriceOnTransaction) prisma.pricesOnTransactions.upsert = prismaMock.pricesOnTransactions.upsert diff --git a/utils/validators.ts b/utils/validators.ts index 86b9e1f9..a802add9 100644 --- a/utils/validators.ts +++ b/utils/validators.ts @@ -248,9 +248,14 @@ function getSignaturePayload (postData: string, postDataParameters: PostDataPara export function signPostData ({ userId, postData, postDataParameters }: PaybuttonTriggerParseParameters): TriggerSignature { const payload = getSignaturePayload(postData, postDataParameters) const pk = getUserPrivateKey(userId) + + const data: Uint8Array = typeof payload === 'string' + ? new TextEncoder().encode(payload) + : payload as unknown as Uint8Array + const signature = crypto.sign( null, - Buffer.from(payload), + data, pk ) return { diff --git a/yarn.lock b/yarn.lock index 666d3ef1..6af26e03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -660,6 +660,11 @@ slash "^3.0.0" strip-ansi "^6.0.0" +"@jest/diff-sequences@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz#0ededeae4d071f5c8ffe3678d15f3a1be09156be" + integrity sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw== + "@jest/environment@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" @@ -670,6 +675,13 @@ "@types/node" "*" jest-mock "^29.7.0" +"@jest/expect-utils@30.2.0": + version "30.2.0" + resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-30.2.0.tgz#4f95413d4748454fdb17404bf1141827d15e6011" + integrity sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA== + dependencies: + "@jest/get-type" "30.1.0" + "@jest/expect-utils@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" @@ -697,6 +709,11 @@ jest-mock "^29.7.0" jest-util "^29.7.0" +"@jest/get-type@30.1.0": + version "30.1.0" + resolved "https://registry.yarnpkg.com/@jest/get-type/-/get-type-30.1.0.tgz#4fcb4dc2ebcf0811be1c04fd1cb79c2dba431cbc" + integrity sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA== + "@jest/globals@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" @@ -707,6 +724,14 @@ "@jest/types" "^29.6.3" jest-mock "^29.7.0" +"@jest/pattern@30.0.1": + version "30.0.1" + resolved "https://registry.yarnpkg.com/@jest/pattern/-/pattern-30.0.1.tgz#d5304147f49a052900b4b853dedb111d080e199f" + integrity sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA== + dependencies: + "@types/node" "*" + jest-regex-util "30.0.1" + "@jest/reporters@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" @@ -737,6 +762,13 @@ strip-ansi "^6.0.0" v8-to-istanbul "^9.0.1" +"@jest/schemas@30.0.5": + version "30.0.5" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-30.0.5.tgz#7bdf69fc5a368a5abdb49fd91036c55225846473" + integrity sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA== + dependencies: + "@sinclair/typebox" "^0.34.0" + "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -794,6 +826,19 @@ slash "^3.0.0" write-file-atomic "^4.0.2" +"@jest/types@30.2.0": + version "30.2.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-30.2.0.tgz#1c678a7924b8f59eafd4c77d56b6d0ba976d62b8" + integrity sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg== + dependencies: + "@jest/pattern" "30.0.1" + "@jest/schemas" "30.0.5" + "@types/istanbul-lib-coverage" "^2.0.6" + "@types/istanbul-reports" "^3.0.4" + "@types/node" "*" + "@types/yargs" "^17.0.33" + chalk "^4.1.2" + "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" @@ -1184,6 +1229,11 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== +"@sinclair/typebox@^0.34.0": + version "0.34.41" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.34.41.tgz#aa51a6c1946df2c5a11494a2cdb9318e026db16c" + integrity sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g== + "@sinonjs/commons@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" @@ -1321,7 +1371,7 @@ resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472" integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1", "@types/istanbul-lib-coverage@^2.0.6": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== @@ -1333,21 +1383,13 @@ dependencies: "@types/istanbul-lib-coverage" "*" -"@types/istanbul-reports@^3.0.0": +"@types/istanbul-reports@^3.0.0", "@types/istanbul-reports@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz#0f03e3d2f670fbdac586e34b433783070cc16f54" integrity sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ== dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^27.5.1": - version "27.5.2" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.2.tgz#ec49d29d926500ffb9fd22b84262e862049c026c" - integrity sha512-mpT8LJJ4CMeeahobofYWIjFo0xonRS/HfxnVEPMPFSQdGUt1uHCnoPT7Zhb+sjDU2wz0oKV0OLUR0WzrHNgfeA== - dependencies: - jest-matcher-utils "^27.0.0" - pretty-format "^27.0.0" - "@types/jest@^29.5.11": version "29.5.14" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" @@ -1356,6 +1398,14 @@ expect "^29.0.0" pretty-format "^29.0.0" +"@types/jest@^30.0.0": + version "30.0.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-30.0.0.tgz#5e85ae568006712e4ad66f25433e9bdac8801f1d" + integrity sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA== + dependencies: + expect "^30.0.0" + pretty-format "^30.0.0" + "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" @@ -1482,7 +1532,7 @@ "@types/node" "*" "@types/socket.io-parser" "*" -"@types/stack-utils@^2.0.0": +"@types/stack-utils@^2.0.0", "@types/stack-utils@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.3.tgz#6209321eb2c1712a7e7466422b8cb1fc0d9dd5d8" integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw== @@ -1504,6 +1554,13 @@ resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== +"@types/yargs@^17.0.33": + version "17.0.34" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.34.tgz#1c2f9635b71d5401827373a01ce2e8a7670ea839" + integrity sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A== + dependencies: + "@types/yargs-parser" "*" + "@types/yargs@^17.0.8": version "17.0.33" resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" @@ -1667,7 +1724,7 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^5.0.0: +ansi-styles@^5.0.0, ansi-styles@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== @@ -2190,7 +2247,7 @@ caniuse-lite@^1.0.30001406, caniuse-lite@^1.0.30001733: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz#f97e08599e2d75664543ae4b6ef25dc2183c5cc6" integrity sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A== -chalk@^4.0.0: +chalk@^4.0.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -2237,7 +2294,7 @@ chronik-client-cashtokens@^3.1.1-rc0: dependencies: "@types/ws" "^8.2.1" axios "^1.6.3" - ecashaddrjs "file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs" + ecashaddrjs "file:../../../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs" isomorphic-ws "^4.0.1" protobufjs "^6.8.8" ws "^8.3.0" @@ -2247,6 +2304,11 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +ci-info@^4.2.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa" + integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA== + cipher-base@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.6.tgz#8fe672437d01cd6c4561af5334e0cc50ff1955f7" @@ -2682,11 +2744,6 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -diff-sequences@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" - integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -2773,7 +2830,7 @@ ecashaddrjs@^1.0.7: big-integer "1.6.36" bs58check "^3.0.1" -ecashaddrjs@^2.0.0, "ecashaddrjs@file:../../../../AppData/Local/Yarn/Cache/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": +ecashaddrjs@^2.0.0, "ecashaddrjs@file:../.cache/yarn/v6/npm-chronik-client-cashtokens-3.1.1-rc0-e5e4a3538e8010b70623974a731bf2712506c5e3-integrity/node_modules/ecashaddrjs": version "2.0.0" resolved "https://registry.yarnpkg.com/ecashaddrjs/-/ecashaddrjs-2.0.0.tgz#d45ede7fb6168815dbcf664b8e0a6872e485d874" integrity sha512-EvK1V4D3+nIEoD0ggy/b0F4lW39/72R9aOs/scm6kxMVuXu16btc+H74eQv7okNfXaQWKgolEekZkQ6wfcMMLw== @@ -3282,6 +3339,18 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +expect@^30.0.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-30.2.0.tgz#d4013bed267013c14bc1199cec8aa57cee9b5869" + integrity sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw== + dependencies: + "@jest/expect-utils" "30.2.0" + "@jest/get-type" "30.1.0" + jest-matcher-utils "30.2.0" + jest-message-util "30.2.0" + jest-mock "30.2.0" + jest-util "30.2.0" + express@^4.17.1: version "4.21.2" resolved "https://registry.yarnpkg.com/express/-/express-4.21.2.tgz#cf250e48362174ead6cea4a566abef0162c1ec32" @@ -3656,7 +3725,7 @@ gopd@^1.0.1, gopd@^1.2.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== -graceful-fs@^4.1.2, graceful-fs@^4.2.9: +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -4276,15 +4345,15 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" - integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== +jest-diff@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-30.2.0.tgz#e3ec3a6ea5c5747f605c9e874f83d756cba36825" + integrity sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A== dependencies: - chalk "^4.0.0" - diff-sequences "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" + "@jest/diff-sequences" "30.0.1" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + pretty-format "30.2.0" jest-diff@^29.7.0: version "29.7.0" @@ -4326,11 +4395,6 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" -jest-get-type@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" - integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== - jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" @@ -4363,15 +4427,15 @@ jest-leak-detector@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" -jest-matcher-utils@^27.0.0: - version "27.5.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" - integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== +jest-matcher-utils@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz#69a0d4c271066559ec8b0d8174829adc3f23a783" + integrity sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg== dependencies: - chalk "^4.0.0" - jest-diff "^27.5.1" - jest-get-type "^27.5.1" - pretty-format "^27.5.1" + "@jest/get-type" "30.1.0" + chalk "^4.1.2" + jest-diff "30.2.0" + pretty-format "30.2.0" jest-matcher-utils@^29.7.0: version "29.7.0" @@ -4383,6 +4447,21 @@ jest-matcher-utils@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" +jest-message-util@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-30.2.0.tgz#fc97bf90d11f118b31e6131e2b67fc4f39f92152" + integrity sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@jest/types" "30.2.0" + "@types/stack-utils" "^2.0.3" + chalk "^4.1.2" + graceful-fs "^4.2.11" + micromatch "^4.0.8" + pretty-format "30.2.0" + slash "^3.0.0" + stack-utils "^2.0.6" + jest-message-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.7.0.tgz#8bc392e204e95dfe7564abbe72a404e28e51f7f3" @@ -4405,6 +4484,15 @@ jest-mock-extended@^2.0.6: dependencies: ts-essentials "^7.0.3" +jest-mock@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-30.2.0.tgz#69f991614eeb4060189459d3584f710845bff45e" + integrity sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw== + dependencies: + "@jest/types" "30.2.0" + "@types/node" "*" + jest-util "30.2.0" + jest-mock@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" @@ -4419,6 +4507,11 @@ jest-pnp-resolver@^1.2.2: resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== +jest-regex-util@30.0.1: + version "30.0.1" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-30.0.1.tgz#f17c1de3958b67dfe485354f5a10093298f2a49b" + integrity sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA== + jest-regex-util@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" @@ -4528,6 +4621,18 @@ jest-snapshot@^29.7.0: pretty-format "^29.7.0" semver "^7.5.3" +jest-util@30.2.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-30.2.0.tgz#5142adbcad6f4e53c2776c067a4db3c14f913705" + integrity sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA== + dependencies: + "@jest/types" "30.2.0" + "@types/node" "*" + chalk "^4.1.2" + ci-info "^4.2.0" + graceful-fs "^4.2.11" + picomatch "^4.0.2" + jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" @@ -5579,6 +5684,11 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042" + integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q== + pidtree@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.5.0.tgz#ad5fbc1de78b8a5f99d6fbdd4f6e4eee21d1aca1" @@ -5636,14 +5746,14 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -pretty-format@^27.0.0, pretty-format@^27.5.1: - version "27.5.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" - integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== +pretty-format@30.2.0, pretty-format@^30.0.0: + version "30.2.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-30.2.0.tgz#2d44fe6134529aed18506f6d11509d8a62775ebe" + integrity sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA== dependencies: - ansi-regex "^5.0.1" - ansi-styles "^5.0.0" - react-is "^17.0.1" + "@jest/schemas" "30.0.5" + ansi-styles "^5.2.0" + react-is "^18.3.1" pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" @@ -5856,12 +5966,12 @@ react-is@^16.13.1, react-is@^16.7.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1: +"react-is@^16.8.0 || ^17.0.0": version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-is@^18.0.0: +react-is@^18.0.0, react-is@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== @@ -6487,7 +6597,7 @@ sqlstring@^2.3.2: resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== -stack-utils@^2.0.3: +stack-utils@^2.0.3, stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==