From 315e10d7948f7dd7e3590314d27d359fa1039d49 Mon Sep 17 00:00:00 2001 From: Bot Date: Thu, 25 Jun 2026 21:39:16 +0100 Subject: [PATCH] feat: implement indexer foundation Fixes #27 Fixes #28 Fixes #31 Fixes #32 --- bun.lock | 96 +++++++++++++------ indexer/common/package.json | 9 ++ indexer/common/src/config/index.ts | 18 ++++ indexer/common/src/db/entity/IndexedEvent.ts | 23 +++++ indexer/common/src/db/repository.test.ts | 72 ++++++++++++++ indexer/common/src/db/repository.ts | 62 ++++++++++++ indexer/common/src/index.ts | 6 ++ indexer/common/src/poller/index.test.ts | 52 ++++++++++ indexer/common/src/poller/index.ts | 92 ++++++++++++++++++ indexer/common/src/rpc/client.test.ts | 15 +++ indexer/common/src/rpc/client.ts | 12 +++ indexer/streams/package.json | 3 +- indexer/streams/src/db/entity/CancelAction.ts | 36 +++++++ indexer/streams/src/db/entity/Stream.ts | 40 ++++++++ .../streams/src/db/entity/WithdrawalAction.ts | 39 ++++++++ .../migrations/00001_InitialStreamsSchema.ts | 52 ++++++++++ indexer/streams/src/index.ts | 4 + indexer/tsconfig.base.json | 4 +- 18 files changed, 604 insertions(+), 31 deletions(-) create mode 100644 indexer/common/src/config/index.ts create mode 100644 indexer/common/src/db/entity/IndexedEvent.ts create mode 100644 indexer/common/src/db/repository.test.ts create mode 100644 indexer/common/src/db/repository.ts create mode 100644 indexer/common/src/poller/index.test.ts create mode 100644 indexer/common/src/poller/index.ts create mode 100644 indexer/common/src/rpc/client.test.ts create mode 100644 indexer/common/src/rpc/client.ts create mode 100644 indexer/streams/src/db/entity/CancelAction.ts create mode 100644 indexer/streams/src/db/entity/Stream.ts create mode 100644 indexer/streams/src/db/entity/WithdrawalAction.ts create mode 100644 indexer/streams/src/db/migrations/00001_InitialStreamsSchema.ts diff --git a/bun.lock b/bun.lock index 6d3632a..0cc31a2 100644 --- a/bun.lock +++ b/bun.lock @@ -47,6 +47,15 @@ "indexer/common": { "name": "@fundable-indexer/common", "version": "0.1.0", + "dependencies": { + "@stellar/stellar-sdk": "^12.0.0", + "pg": "^8.11.3", + "typeorm": "^0.3.20", + "zod": "^3.22.4", + }, + "devDependencies": { + "@types/pg": "^8.11.0", + }, }, "indexer/distributions": { "name": "@fundable-indexer/distributions", @@ -60,6 +69,7 @@ "version": "0.1.0", "dependencies": { "@fundable-indexer/common": "workspace:*", + "typeorm": "^0.3.20", }, }, }, @@ -276,6 +286,12 @@ "@starknet-io/types-js": ["@starknet-io/types-js@0.7.10", "", {}, "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w=="], + "@stellar/js-xdr": ["@stellar/js-xdr@3.1.2", "", {}, "sha512-VVolPL5goVEIsvuGqDc5uiKxV03lzfWdvYg1KikvwheDmTBO68CKDji3bAZ/kppZrx5iTA8z3Ld5yuytcvhvOQ=="], + + "@stellar/stellar-base": ["@stellar/stellar-base@12.1.1", "", { "dependencies": { "@stellar/js-xdr": "^3.1.2", "base32.js": "^0.1.0", "bignumber.js": "^9.1.2", "buffer": "^6.0.3", "sha.js": "^2.3.6", "tweetnacl": "^1.0.3" }, "optionalDependencies": { "sodium-native": "^4.1.1" } }, "sha512-gOBSOFDepihslcInlqnxKZdIW9dMUO1tpOm3AtJR33K2OvpXG6SaVHCzAmCFArcCqI9zXTEiSoh70T48TmiHJA=="], + + "@stellar/stellar-sdk": ["@stellar/stellar-sdk@12.3.0", "", { "dependencies": { "@stellar/stellar-base": "^12.1.1", "axios": "^1.7.7", "bignumber.js": "^9.1.2", "eventsource": "^2.0.2", "randombytes": "^2.1.0", "toml": "^3.0.0", "urijs": "^1.19.1" } }, "sha512-F2DYFop/M5ffXF0lvV5Ezjk+VWNKg0QDX8gNhwehVU3y5LYA3WAY6VcCarMGPaG9Wdgoeh1IXXzOautpqpsltw=="], + "@turbo/darwin-64": ["@turbo/darwin-64@2.9.18", "", { "os": "darwin", "cpu": "x64" }, "sha512-9f27peFu16ur8c0v9nUFUEyBnbKuuFsUTjHFWfmwGfzySBXbHwzU44QhZon6Mznz0cHsIr3984NQj/bVrnGSRw=="], "@turbo/darwin-arm64": ["@turbo/darwin-arm64@2.9.18", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9A6TMRq/Ib+QnbhLlgkhOm+624wO4pzSQ/yQviQfWHOlFvaYxdnIAYmu2H6TS6y7kSVL0DvzNe04NbESTOzFVQ=="], @@ -320,6 +336,8 @@ "@types/node": ["@types/node@22.15.33", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wzoocdnnpSxZ+6CjW4ADCK1jVmd1S/J3ArNWfn8FDDQtRm8dkDg7TA+mvek2wNrfCgwuZxqEOiB9B1XCJ6+dbw=="], + "@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="], + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], @@ -378,6 +396,8 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -398,12 +418,26 @@ "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "axios": ["axios@1.18.1", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "bare-addon-resolve": ["bare-addon-resolve@1.10.0", "", { "dependencies": { "bare-module-resolve": "^1.10.0", "bare-semver": "^1.0.0" }, "peerDependencies": { "bare-url": "*" }, "optionalPeers": ["bare-url"] }, "sha512-sSd0jieRJlDaODOzj0oe0RjFVC1QI0ZIjGIdPkbrTXsdVVtENg14c+lHHAhHwmWCZ2nQlMhy8jA3Y5LYPc/isA=="], + + "bare-module-resolve": ["bare-module-resolve@1.12.2", "", { "dependencies": { "bare-semver": "^1.0.0" }, "peerDependencies": { "bare-url": "*" }, "optionalPeers": ["bare-url"] }, "sha512-j+hiD5k99qec4KjJvYsI67q5AOBifmy9JG3oeMVxTmvrhn2sIdp8StrUvZu4YNgwTpO+NhniQG16N1ETDe1k5w=="], + + "bare-semver": ["bare-semver@1.1.0", "", {}, "sha512-1Hw5qJ7hXdVt3uPUqjeFTuxyvBUJauvz5A1I2jk8gzjZMHp04n//6nV9MDbG9CMw78JHY2lGV0w6s//LrASm2w=="], + + "base32.js": ["base32.js@0.1.0", "", {}, "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "bcryptjs": ["bcryptjs@2.4.3", "", {}, "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -446,6 +480,8 @@ "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], @@ -474,6 +510,8 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], @@ -502,6 +540,8 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": "bin/esbuild" }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], @@ -536,6 +576,8 @@ "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + "eventsource": ["eventsource@2.0.2", "", {}, "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], @@ -570,8 +612,12 @@ "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], @@ -606,7 +652,9 @@ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="], @@ -614,6 +662,8 @@ "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -812,12 +862,16 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], @@ -828,6 +882,8 @@ "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "require-addon": ["require-addon@1.2.0", "", { "dependencies": { "bare-addon-resolve": "^1.3.0" } }, "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -874,6 +930,8 @@ "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + "sodium-native": ["sodium-native@4.3.3", "", { "dependencies": { "require-addon": "^1.1.0" } }, "sha512-OnxSlN3uyY8D0EsLHpmm2HOFmKddQVvEMmsakCrXUzSd8kjjbzL413t4ZNF3n0UxSwNgwTyUvkmZHTfuCeiYSw=="], + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], @@ -926,6 +984,8 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], @@ -938,6 +998,8 @@ "turbo": ["turbo@2.9.18", "", { "optionalDependencies": { "@turbo/darwin-64": "2.9.18", "@turbo/darwin-arm64": "2.9.18", "@turbo/linux-64": "2.9.18", "@turbo/linux-arm64": "2.9.18", "@turbo/windows-64": "2.9.18", "@turbo/windows-arm64": "2.9.18" }, "bin": { "turbo": "bin/turbo" } }, "sha512-bwabv6PupzeavybzEoArBAkwq5fnzwf8OFnRtpHwnviFWuwJPFxtyH+aVp36TmIqK3aYYgtTJ3J0m2ysxxSzQg=="], + "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], @@ -958,6 +1020,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "urijs": ["urijs@1.19.11", "", {}, "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], @@ -1028,8 +1092,6 @@ "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "color/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], @@ -1038,6 +1100,8 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "get-intrinsic/hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -1048,26 +1112,12 @@ "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "test-exclude/glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], "test-exclude/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "vite/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], - "wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], @@ -1078,8 +1128,6 @@ "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "color/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], @@ -1090,10 +1138,6 @@ "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "test-exclude/glob/path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], @@ -1148,12 +1192,6 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "test-exclude/glob/path-scurry/lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/indexer/common/package.json b/indexer/common/package.json index da8956f..3fc95d4 100644 --- a/indexer/common/package.json +++ b/indexer/common/package.json @@ -13,5 +13,14 @@ "lint": "biome check .", "test": "vitest run src", "type-check": "tsc -p tsconfig.json --noEmit" + }, + "dependencies": { + "@stellar/stellar-sdk": "^12.0.0", + "pg": "^8.11.3", + "typeorm": "^0.3.20", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/pg": "^8.11.0" } } diff --git a/indexer/common/src/config/index.ts b/indexer/common/src/config/index.ts new file mode 100644 index 0000000..54e306d --- /dev/null +++ b/indexer/common/src/config/index.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; + +export const ConfigSchema = z.object({ + RPC_URL: z.string().url().default("https://soroban-testnet.stellar.org"), + NETWORK_PASSPHRASE: z.string().default("Test SDF Network ; September 2015"), +}); + +export type Config = z.infer; + +export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config { + const result = ConfigSchema.safeParse(env); + if (!result.success) { + throw new Error(`Invalid configuration: ${result.error.message}`); + } + return result.data; +} + +export const config = loadConfig(); diff --git a/indexer/common/src/db/entity/IndexedEvent.ts b/indexer/common/src/db/entity/IndexedEvent.ts new file mode 100644 index 0000000..d8d3408 --- /dev/null +++ b/indexer/common/src/db/entity/IndexedEvent.ts @@ -0,0 +1,23 @@ +import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, Unique } from "typeorm"; + +@Entity("indexed_event") +@Unique("uq_indexed_event_identity", ["contractId", "ledgerNumber", "txHash", "eventIndex"]) +export class IndexedEvent { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Column({ type: "varchar" }) + contractId!: string; + + @Column({ type: "int" }) + ledgerNumber!: number; + + @Column({ type: "varchar" }) + txHash!: string; + + @Column({ type: "int" }) + eventIndex!: number; + + @CreateDateColumn() + processedAt!: Date; +} diff --git a/indexer/common/src/db/repository.test.ts b/indexer/common/src/db/repository.test.ts new file mode 100644 index 0000000..c98a824 --- /dev/null +++ b/indexer/common/src/db/repository.test.ts @@ -0,0 +1,72 @@ +import { type DataSource, InsertQueryBuilder, Repository } from "typeorm"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { IndexedEvent } from "./entity/IndexedEvent.js"; +import { EventRepository } from "./repository.js"; + +describe("EventRepository", () => { + // biome-ignore lint/suspicious/noExplicitAny: mock objects + let mockDataSource: any; + // biome-ignore lint/suspicious/noExplicitAny: mock objects + let mockRepo: any; + // biome-ignore lint/suspicious/noExplicitAny: mock objects + let mockQueryBuilder: any; + + beforeEach(() => { + mockQueryBuilder = { + insert: vi.fn().mockReturnThis(), + into: vi.fn().mockReturnThis(), + values: vi.fn().mockReturnThis(), + orIgnore: vi.fn().mockReturnThis(), + execute: vi.fn(), + }; + + mockRepo = { + count: vi.fn(), + createQueryBuilder: vi.fn(() => mockQueryBuilder), + }; + + mockDataSource = { + getRepository: vi.fn(() => mockRepo), + }; + }); + + it("should return true if event is processed", async () => { + mockRepo.count.mockResolvedValue(1); + const repo = new EventRepository(mockDataSource as unknown as DataSource); + const isProcessed = await repo.isEventProcessed("contract1", 100, "txHash1", 0); + + expect(isProcessed).toBe(true); + expect(mockRepo.count).toHaveBeenCalledWith({ + where: { + contractId: "contract1", + ledgerNumber: 100, + txHash: "txHash1", + eventIndex: 0, + }, + }); + }); + + it("should record event successfully", async () => { + const repo = new EventRepository(mockDataSource as unknown as DataSource); + mockQueryBuilder.execute.mockResolvedValue({}); + + const result = await repo.recordEventProcessed("contract1", 100, "txHash1", 0); + + expect(result).toBe(true); + expect(mockQueryBuilder.values).toHaveBeenCalledWith({ + contractId: "contract1", + ledgerNumber: 100, + txHash: "txHash1", + eventIndex: 0, + }); + expect(mockQueryBuilder.orIgnore).toHaveBeenCalledWith("uq_indexed_event_identity"); + }); + + it("should catch unique constraint errors and return false", async () => { + const repo = new EventRepository(mockDataSource as unknown as DataSource); + mockQueryBuilder.execute.mockRejectedValue(new Error("unique constraint violation")); + + const result = await repo.recordEventProcessed("contract1", 100, "txHash1", 0); + expect(result).toBe(false); + }); +}); diff --git a/indexer/common/src/db/repository.ts b/indexer/common/src/db/repository.ts new file mode 100644 index 0000000..b97e5f7 --- /dev/null +++ b/indexer/common/src/db/repository.ts @@ -0,0 +1,62 @@ +import type { DataSource, Repository } from "typeorm"; +import { IndexedEvent } from "./entity/IndexedEvent.js"; + +export class EventRepository { + private repo: Repository; + + constructor(private dataSource: DataSource) { + this.repo = this.dataSource.getRepository(IndexedEvent); + } + + /** + * Checks if an event has already been processed based on its deterministic identity. + */ + async isEventProcessed( + contractId: string, + ledgerNumber: number, + txHash: string, + eventIndex: number, + ): Promise { + const count = await this.repo.count({ + where: { + contractId, + ledgerNumber, + txHash, + eventIndex, + }, + }); + return count > 0; + } + + /** + * Records an event as processed safely handling duplicates without throwing. + */ + async recordEventProcessed( + contractId: string, + ledgerNumber: number, + txHash: string, + eventIndex: number, + ): Promise { + try { + await this.repo + .createQueryBuilder() + .insert() + .into(IndexedEvent) + .values({ + contractId, + ledgerNumber, + txHash, + eventIndex, + }) + .orIgnore("uq_indexed_event_identity") // Safe duplicate insert + .execute(); + return true; + } catch (err) { + // If the driver doesn't support orIgnore, fallback to simple insert and catch unique violation. + if (err instanceof Error && err.message.includes("unique constraint")) { + return false; + } + throw err; + } + } +} diff --git a/indexer/common/src/index.ts b/indexer/common/src/index.ts index a7a318b..cbe5437 100644 --- a/indexer/common/src/index.ts +++ b/indexer/common/src/index.ts @@ -2,3 +2,9 @@ export const commonPackage = { name: "@fundable-indexer/common", role: "shared-infrastructure", } as const; + +export { config, loadConfig } from "./config/index.js"; +export { createSorobanClient, sorobanClient } from "./rpc/client.js"; +export { IndexedEvent } from "./db/entity/IndexedEvent.js"; +export { EventRepository } from "./db/repository.js"; +export { SorobanPoller, type PollerOptions, type PollResult } from "./poller/index.js"; diff --git a/indexer/common/src/poller/index.test.ts b/indexer/common/src/poller/index.test.ts new file mode 100644 index 0000000..eac6318 --- /dev/null +++ b/indexer/common/src/poller/index.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, it, vi } from "vitest"; +import { SorobanPoller } from "./index.js"; + +describe("SorobanPoller", () => { + it("should successfully process events and advance cursor", async () => { + const poller = new SorobanPoller({ retryDelayMs: 1 }); + const fetchEvents = vi.fn().mockResolvedValue([{ id: 1 }]); + const processEvent = vi.fn().mockResolvedValue(undefined); + const updateCursor = vi.fn().mockResolvedValue(undefined); + + const result = await poller.processLedgerRange(10, 20, fetchEvents, processEvent, updateCursor); + + expect(result.success).toBe(true); + expect(result.lastProcessedLedger).toBe(20); + expect(fetchEvents).toHaveBeenCalledWith(10, 20); + expect(processEvent).toHaveBeenCalledWith({ id: 1 }); + expect(updateCursor).toHaveBeenCalledWith(20); + }); + + it("should retry on transient errors when fetching events", async () => { + const poller = new SorobanPoller({ retryDelayMs: 1, maxRetries: 2 }); + let attempts = 0; + const fetchEvents = vi.fn().mockImplementation(() => { + attempts++; + if (attempts === 1) return Promise.reject(new Error("network timeout")); + return Promise.resolve([{ id: 2 }]); + }); + const processEvent = vi.fn().mockResolvedValue(undefined); + const updateCursor = vi.fn().mockResolvedValue(undefined); + + const result = await poller.processLedgerRange(10, 20, fetchEvents, processEvent, updateCursor); + + expect(result.success).toBe(true); + expect(attempts).toBe(2); + expect(fetchEvents).toHaveBeenCalledTimes(2); + expect(processEvent).toHaveBeenCalledWith({ id: 2 }); + expect(updateCursor).toHaveBeenCalledWith(20); + }); + + it("should not advance cursor if handler fails", async () => { + const poller = new SorobanPoller({ retryDelayMs: 1 }); + const fetchEvents = vi.fn().mockResolvedValue([{ id: 3 }]); + const processEvent = vi.fn().mockRejectedValue(new Error("Handler failure")); + const updateCursor = vi.fn(); + + const result = await poller.processLedgerRange(10, 20, fetchEvents, processEvent, updateCursor); + + expect(result.success).toBe(false); + expect(result.error?.message).toBe("Handler failure"); + expect(updateCursor).not.toHaveBeenCalled(); + }); +}); diff --git a/indexer/common/src/poller/index.ts b/indexer/common/src/poller/index.ts new file mode 100644 index 0000000..ae30e14 --- /dev/null +++ b/indexer/common/src/poller/index.ts @@ -0,0 +1,92 @@ +export interface PollerOptions { + maxRetries?: number; + retryDelayMs?: number; +} + +export interface PollResult { + success: boolean; + error?: Error; + lastProcessedLedger?: number; +} + +export class SorobanPoller { + private maxRetries: number; + private retryDelayMs: number; + private isRunning = false; + + constructor(options: PollerOptions = {}) { + this.maxRetries = options.maxRetries ?? 3; + this.retryDelayMs = options.retryDelayMs ?? 1000; + } + + /** + * Utility to wait for a given amount of time. + */ + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + /** + * Executes a block of logic with retry for transient RPC errors. + */ + public async withRetry(operation: () => Promise): Promise { + let attempt = 0; + while (true) { + try { + return await operation(); + } catch (error) { + attempt++; + if (attempt > this.maxRetries || !this.isTransientError(error)) { + throw error; + } + await this.delay(this.retryDelayMs); + } + } + } + + /** + * Determines if an error is transient and should be retried. + */ + private isTransientError(error: unknown): boolean { + // Basic transient error checking - expand based on specific RPC error formats + const message = error instanceof Error ? error.message : String(error); + return ( + message.includes("timeout") || + message.includes("rate limit") || + message.includes("ECONNRESET") || + message.includes("503") || + message.includes("504") || + message.includes("502") + ); + } + + /** + * Processes a ledger range. Returns success=true if processing completes, + * otherwise returns an error and the operation stops without advancing cursor. + */ + public async processLedgerRange( + startLedger: number, + endLedger: number, + fetchEvents: (start: number, end: number) => Promise, + processEvent: (event: TEvent) => Promise, + updateCursor: (ledger: number) => Promise, + ): Promise { + try { + // 1. Fetch events with retry logic for RPC + const events = await this.withRetry(() => fetchEvents(startLedger, endLedger)); + + // 2. Process events sequentially + for (const event of events) { + // If a handler fails, it throws, skipping the updateCursor step + await processEvent(event); + } + + // 3. Update cursor ONLY if all events in the range succeeded + await updateCursor(endLedger); + return { success: true, lastProcessedLedger: endLedger }; + } catch (error) { + // Return the error to surface it. Cursor is intentionally not advanced. + return { success: false, error: error instanceof Error ? error : new Error(String(error)) }; + } + } +} diff --git a/indexer/common/src/rpc/client.test.ts b/indexer/common/src/rpc/client.test.ts new file mode 100644 index 0000000..73ca067 --- /dev/null +++ b/indexer/common/src/rpc/client.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { createSorobanClient } from "./client.js"; + +describe("RPC Client Wrapper", () => { + it("should create a client with the provided URL", () => { + const url = "https://soroban-testnet.stellar.org"; + const client = createSorobanClient(url); + expect(client.serverURL.toString()).toBe("https://soroban-testnet.stellar.org/"); + }); + + it("should use the default configuration if no URL is provided", () => { + const client = createSorobanClient(); + expect(client.serverURL.toString()).toBe("https://soroban-testnet.stellar.org/"); + }); +}); diff --git a/indexer/common/src/rpc/client.ts b/indexer/common/src/rpc/client.ts new file mode 100644 index 0000000..77d97bc --- /dev/null +++ b/indexer/common/src/rpc/client.ts @@ -0,0 +1,12 @@ +import { rpc } from "@stellar/stellar-sdk"; +import { config } from "../config/index.js"; + +/** + * Creates and configures a Soroban Server instance. + * Reads the RPC URL from the validated configuration. + */ +export function createSorobanClient(rpcUrl: string = config.RPC_URL): rpc.Server { + return new rpc.Server(rpcUrl); +} + +export const sorobanClient = createSorobanClient(); diff --git a/indexer/streams/package.json b/indexer/streams/package.json index ed7405e..5d03564 100644 --- a/indexer/streams/package.json +++ b/indexer/streams/package.json @@ -15,6 +15,7 @@ "type-check": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@fundable-indexer/common": "workspace:*" + "@fundable-indexer/common": "workspace:*", + "typeorm": "^0.3.20" } } diff --git a/indexer/streams/src/db/entity/CancelAction.ts b/indexer/streams/src/db/entity/CancelAction.ts new file mode 100644 index 0000000..d2d4310 --- /dev/null +++ b/indexer/streams/src/db/entity/CancelAction.ts @@ -0,0 +1,36 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { Stream } from "./Stream.js"; + +@Entity("stream_cancel_action") +export class CancelAction { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Index() + @Column({ type: "varchar", comment: "The ID of the stream this cancel action belongs to" }) + streamId!: string; + + @ManyToOne(() => Stream, { onDelete: "CASCADE" }) + @JoinColumn({ name: "streamId" }) + stream!: Stream; + + @Column({ type: "varchar", comment: "The address that triggered the cancellation" }) + canceler!: string; + + @Column({ type: "varchar", comment: "Transaction hash where cancellation occurred" }) + txHash!: string; + + @Column({ type: "bigint", comment: "Timestamp of the cancellation" }) + timestamp!: string; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/indexer/streams/src/db/entity/Stream.ts b/indexer/streams/src/db/entity/Stream.ts new file mode 100644 index 0000000..2f12bb0 --- /dev/null +++ b/indexer/streams/src/db/entity/Stream.ts @@ -0,0 +1,40 @@ +import { Column, CreateDateColumn, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm"; + +@Entity("stream") +export class Stream { + @PrimaryColumn({ + type: "varchar", + comment: "The unique ID of the stream (usually contract ID or deterministic stream ID)", + }) + id!: string; + + @Column({ type: "varchar", comment: "The address of the sender" }) + sender!: string; + + @Column({ type: "varchar", comment: "The address of the recipient" }) + recipient!: string; + + @Column({ type: "varchar", comment: "The token asset address" }) + token!: string; + + @Column({ type: "bigint", comment: "The total amount of tokens in the stream" }) + totalAmount!: string; + + @Column({ type: "bigint", comment: "The start time of the stream as unix timestamp" }) + startTime!: string; + + @Column({ type: "bigint", comment: "The end time of the stream as unix timestamp" }) + endTime!: string; + + @Column({ type: "bigint", default: "0", comment: "Total amount withdrawn so far" }) + amountWithdrawn!: string; + + @Column({ type: "boolean", default: false, comment: "Whether the stream was canceled" }) + canceled!: boolean; + + @CreateDateColumn() + createdAt!: Date; + + @UpdateDateColumn() + updatedAt!: Date; +} diff --git a/indexer/streams/src/db/entity/WithdrawalAction.ts b/indexer/streams/src/db/entity/WithdrawalAction.ts new file mode 100644 index 0000000..d1c4c23 --- /dev/null +++ b/indexer/streams/src/db/entity/WithdrawalAction.ts @@ -0,0 +1,39 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, +} from "typeorm"; +import { Stream } from "./Stream.js"; + +@Entity("stream_withdrawal_action") +export class WithdrawalAction { + @PrimaryGeneratedColumn("uuid") + id!: string; + + @Index() + @Column({ type: "varchar", comment: "The ID of the stream this withdrawal belongs to" }) + streamId!: string; + + @ManyToOne(() => Stream, { onDelete: "CASCADE" }) + @JoinColumn({ name: "streamId" }) + stream!: Stream; + + @Column({ type: "varchar" }) + recipient!: string; + + @Column({ type: "bigint", comment: "The amount withdrawn" }) + amount!: string; + + @Column({ type: "varchar", comment: "Transaction hash where withdrawal occurred" }) + txHash!: string; + + @Column({ type: "bigint", comment: "Timestamp of the withdrawal" }) + timestamp!: string; + + @CreateDateColumn() + createdAt!: Date; +} diff --git a/indexer/streams/src/db/migrations/00001_InitialStreamsSchema.ts b/indexer/streams/src/db/migrations/00001_InitialStreamsSchema.ts new file mode 100644 index 0000000..f84a80f --- /dev/null +++ b/indexer/streams/src/db/migrations/00001_InitialStreamsSchema.ts @@ -0,0 +1,52 @@ +import { type MigrationInterface, type QueryRunner, Table, TableIndex } from "typeorm"; + +export class InitialStreamsSchema00001 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + // Note: In a real environment, this can be auto-generated by TypeORM + // These tables match the Stream, WithdrawalAction, and CancelAction entities. + await queryRunner.query(` + CREATE TABLE "stream" ( + "id" varchar PRIMARY KEY NOT NULL, + "sender" varchar NOT NULL, + "recipient" varchar NOT NULL, + "token" varchar NOT NULL, + "totalAmount" bigint NOT NULL, + "startTime" bigint NOT NULL, + "endTime" bigint NOT NULL, + "amountWithdrawn" bigint NOT NULL DEFAULT '0', + "canceled" boolean NOT NULL DEFAULT false, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + "updatedAt" TIMESTAMP NOT NULL DEFAULT now() + ); + + CREATE TABLE "stream_withdrawal_action" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + "streamId" varchar NOT NULL, + "recipient" varchar NOT NULL, + "amount" bigint NOT NULL, + "txHash" varchar NOT NULL, + "timestamp" bigint NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "FK_withdrawal_stream" FOREIGN KEY ("streamId") REFERENCES "stream"("id") ON DELETE CASCADE + ); + CREATE INDEX "IDX_withdrawal_streamId" ON "stream_withdrawal_action" ("streamId"); + + CREATE TABLE "stream_cancel_action" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + "streamId" varchar NOT NULL, + "canceler" varchar NOT NULL, + "txHash" varchar NOT NULL, + "timestamp" bigint NOT NULL, + "createdAt" TIMESTAMP NOT NULL DEFAULT now(), + CONSTRAINT "FK_cancel_stream" FOREIGN KEY ("streamId") REFERENCES "stream"("id") ON DELETE CASCADE + ); + CREATE INDEX "IDX_cancel_streamId" ON "stream_cancel_action" ("streamId"); + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "stream_cancel_action"`); + await queryRunner.query(`DROP TABLE "stream_withdrawal_action"`); + await queryRunner.query(`DROP TABLE "stream"`); + } +} diff --git a/indexer/streams/src/index.ts b/indexer/streams/src/index.ts index a52945c..2da94b9 100644 --- a/indexer/streams/src/index.ts +++ b/indexer/streams/src/index.ts @@ -5,3 +5,7 @@ export const streamsPackage = { role: "payment-stream-indexer", common: commonPackage.name, } as const; + +export { Stream } from "./db/entity/Stream.js"; +export { WithdrawalAction } from "./db/entity/WithdrawalAction.js"; +export { CancelAction } from "./db/entity/CancelAction.js"; diff --git a/indexer/tsconfig.base.json b/indexer/tsconfig.base.json index 66dd4c8..9b7e628 100644 --- a/indexer/tsconfig.base.json +++ b/indexer/tsconfig.base.json @@ -20,6 +20,8 @@ "skipLibCheck": true, "strict": true, "target": "ES2023", - "types": ["vitest/globals"] + "types": ["vitest/globals"], + "experimentalDecorators": true, + "emitDecoratorMetadata": true } }