diff --git a/apps/randomness/package.json b/apps/randomness/package.json index de6e74b348..235fb4c725 100644 --- a/apps/randomness/package.json +++ b/apps/randomness/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@happy.tech/common": "workspace:0.1.0", - "@happy.tech/txm": "workspace:0.1.0", "@happy.tech/contracts": "workspace:0.1.0", + "@happy.tech/txm": "workspace:0.1.0", "better-sqlite3": "^11.7.0", "kysely": "^0.27.5", "neverthrow": "^8.1.0", diff --git a/bun.lock b/bun.lock index 15ebe8ffef..c5c8975605 100644 --- a/bun.lock +++ b/bun.lock @@ -275,6 +275,11 @@ "@happy.tech/configs": "workspace:0.1.0", "@happy.tech/contracts": "workspace:0.1.0", "@hono/node-server": "^1.13.8", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-prometheus": "^0.57.2", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/sdk-node": "^0.57.2", + "@opentelemetry/sdk-trace-node": "^1.30.1", "better-sqlite3": "^11.5.0", "eventemitter3": "^5.0.1", "hono": "^4.7.2", @@ -311,6 +316,9 @@ "support/common": { "name": "@happy.tech/common", "version": "0.1.0", + "dependencies": { + "@opentelemetry/api": "^1.9.0", + }, "devDependencies": { "@happy.tech/configs": "workspace:0.1.0", "@happy.tech/happybuild": "workspace:0.1.1", @@ -956,6 +964,62 @@ "@okikio/sharedworker": ["@okikio/sharedworker@1.1.0", "", {}, "sha512-Xj9TUWll9mhARsKu5DtlQCjRekfJfQ2E291ow6gmXIz+WuF6uJMH8ZmGhdRTx/ndOippHnm1j/vxXNjmR6JuXw=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], + + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.30.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA=="], + + "@opentelemetry/core": ["@opentelemetry/core@1.30.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ=="], + + "@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.57.2", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/sdk-logs": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eovEy10n3umjKJl2Ey6TLzikPE+W4cUQ4gCwgGP1RqzTGtgDra0WjIqdy29ohiUKfvmbiL3MndZww58xfIvyFw=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/sdk-logs": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-0rygmvLcehBRp56NQVLSleJ5ITTduq/QfU7obOkyWgPpFHulwpw2LYTqNIz5TczKZuy5YY+5D3SDnXZL1tXImg=="], + + "@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-trace-base": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ta0ithCin0F8lu9eOf4lEz9YAScecezCHkMMyDkvd9S7AnZNX5ikUmC5EQOQADU+oCcgo/qkQIaKcZvQ0TYKDw=="], + + "@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.57.2", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-r70B8yKR41F0EC443b5CGB4rUaOMm99I5N75QQt6sHKxYDzSEc6gm48Diz1CI1biwa5tDPznpylTrywO/pT7qw=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ttb9+4iKw04IMubjm3t0EZsYRNWr3kg44uUuzfo9CaccYlOh8cDooe4QObDUkvx9d5qQUrbEckhrWKfJnKhemA=="], + + "@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HX068Q2eNs38uf7RIkNN9Hl4Ynl+3lP0++KELkXMCpsCbFO03+0XNNZ1SkwxPlP9jrhQahsMPMkzNXpq3fKsnw=="], + + "@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-metrics": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-VqIqXnuxWMWE/1NatAGtB1PvsQipwxDcdG4RwA/umdBcW3/iOHp0uejvFHTRN2O78ZPged87ErJajyUBPUhlDQ=="], + + "@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.57.2", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-grpc-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gHU1vA3JnHbNxEXg5iysqCWxN9j83d7/epTYBZflqQnTyCC4N7yZXn/dMM+bEmyhQPGjhCkNZLx4vZuChH1PYw=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-sB/gkSYFu+0w2dVQ0PWY9fAMl172PKMZ/JrHkkW8dmjCL0CYkmXeE+ssqIL/yBUTPOvpLIpenX5T9RwXRBW/3g=="], + + "@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-awDdNRMIwDvUtoRYxRhja5QYH6+McBLtoz1q9BeEsskhZcrGmH/V1fWpGx8n+Rc+542e8pJA6y+aullbIzQmlw=="], + + "@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-6S2QIMJahIquvFaaxmcwpvQQRD/YFaMTNoIxrfPIPOeITN+a8lfEcPDxNxn8JDAaxkg+4EnXhz8upVDYenoQjA=="], + + "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.57.2", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-transformer": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XdxEzL23Urhidyebg5E6jZoaiW5ygP/mRjxLHixogbqwDy2Faduzb5N0o/Oi+XTIJu+iyxXdVORjXax+Qgfxag=="], + + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.57.2", "", { "dependencies": { "@grpc/grpc-js": "^1.7.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/otlp-exporter-base": "0.57.2", "@opentelemetry/otlp-transformer": "0.57.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-USn173KTWy0saqqRB5yU9xUZ2xdgb1Rdu5IosJnm9aV4hMTuFFRTUsQxbgc24QxpCHeoKzzCSnS/JzdV0oM2iQ=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-48IIRj49gbQVK52jYsw70+Jv+JbahT8BqT2Th7C4H7RCM9d0gZ5sgNPoMpWldmfjvIsSgiGJtjfk9MeZvjhoig=="], + + "@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ=="], + + "@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-TXFHJ5c+BKggWbdEQ/inpgIzEmS2BGQowLE9UhsMd7YYlUfBQJ4uax0VF/B5NYigdM/75OoJGhAV3upEhK+3gg=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog=="], + + "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@opentelemetry/core": "1.30.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.57.2", "@opentelemetry/exporter-logs-otlp-http": "0.57.2", "@opentelemetry/exporter-logs-otlp-proto": "0.57.2", "@opentelemetry/exporter-metrics-otlp-grpc": "0.57.2", "@opentelemetry/exporter-metrics-otlp-http": "0.57.2", "@opentelemetry/exporter-metrics-otlp-proto": "0.57.2", "@opentelemetry/exporter-prometheus": "0.57.2", "@opentelemetry/exporter-trace-otlp-grpc": "0.57.2", "@opentelemetry/exporter-trace-otlp-http": "0.57.2", "@opentelemetry/exporter-trace-otlp-proto": "0.57.2", "@opentelemetry/exporter-zipkin": "1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/resources": "1.30.1", "@opentelemetry/sdk-logs": "0.57.2", "@opentelemetry/sdk-metrics": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "@opentelemetry/sdk-trace-node": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-8BaeqZyN5sTuPBtAoY+UtKwXBdqyuRKmekN5bFzAO40CgbGzAxfTpiL3PBerT7rhZ7p2nBdq7FaMv/tBQgHE4A=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1", "@opentelemetry/resources": "1.30.1", "@opentelemetry/semantic-conventions": "1.28.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@1.30.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "1.30.1", "@opentelemetry/core": "1.30.1", "@opentelemetry/propagator-b3": "1.30.1", "@opentelemetry/propagator-jaeger": "1.30.1", "@opentelemetry/sdk-trace-base": "1.30.1", "semver": "^7.5.2" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="], + "@openzeppelin/contracts": ["@openzeppelin/contracts@5.2.0", "", {}, "sha512-bxjNie5z89W1Ea0NZLZluFh8PrFNn9DH8DQlujEok2yjsOlraUPKID5p1Wk3qdNbf6XkQ1Os2RvfiHrrXLHWKA=="], "@openzeppelin/contracts-upgradeable": ["@openzeppelin/contracts-upgradeable@5.2.0", "", { "peerDependencies": { "@openzeppelin/contracts": "5.2.0" } }, "sha512-mZIu9oa4tQTlGiOJHk6D3LdJlqFqF6oNOSn6S6UVJtzfs9UsY9/dhMEbAVTwElxUtJnjpf6yA062+oBp+eOyPg=="], @@ -1426,6 +1490,8 @@ "@types/secp256k1": ["@types/secp256k1@4.0.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ=="], + "@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -1724,6 +1790,8 @@ "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], + "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="], @@ -2592,6 +2660,8 @@ "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "import-in-the-middle": ["import-in-the-middle@1.13.1", "", { "dependencies": { "acorn": "^8.14.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^1.2.2", "module-details-from-path": "^1.0.3" } }, "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA=="], + "import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="], "imul": ["imul@1.0.1", "", {}, "sha512-WFAgfwPLAjU66EKt6vRdTlKj4nAgIDQzh29JonLa4Bqtl6D8JrIMvWjCnx7xEjVNmP3U0fM5o8ZObk7d0f62bA=="], @@ -3020,6 +3090,8 @@ "modern-ahocorasick": ["modern-ahocorasick@1.1.0", "", {}, "sha512-sEKPVl2rM+MNVkGQt3ChdmD8YsigmXdn5NifZn6jiwn9LRJpWm8F3guhaqrJT/JOat6pwpbXEk6kv+b9DMIjsQ=="], + "module-details-from-path": ["module-details-from-path@1.0.3", "", {}, "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A=="], + "motion": ["motion@10.18.0", "", { "dependencies": { "@motionone/animation": "^10.18.0", "@motionone/dom": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0" } }, "sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ=="], "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], @@ -3420,6 +3492,8 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "require-in-the-middle": ["require-in-the-middle@7.5.2", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3", "resolve": "^1.22.8" } }, "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ=="], + "require-like": ["require-like@0.1.2", "", {}, "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A=="], "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], @@ -3508,6 +3582,8 @@ "shiki": ["shiki@1.29.2", "", { "dependencies": { "@shikijs/core": "1.29.2", "@shikijs/engine-javascript": "1.29.2", "@shikijs/engine-oniguruma": "1.29.2", "@shikijs/langs": "1.29.2", "@shikijs/themes": "1.29.2", "@shikijs/types": "1.29.2", "@shikijs/vscode-textmate": "^10.0.1", "@types/hast": "^3.0.4" } }, "sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg=="], + "shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="], + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], @@ -4090,6 +4166,8 @@ "@firebase/vertexai-preview/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@happy.tech/happybuild/@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], + "@happy.tech/txm/vitest": ["vitest@3.0.9", "", { "dependencies": { "@vitest/expect": "3.0.9", "@vitest/mocker": "3.0.9", "@vitest/pretty-format": "^3.0.9", "@vitest/runner": "3.0.9", "@vitest/snapshot": "3.0.9", "@vitest/spy": "3.0.9", "@vitest/utils": "3.0.9", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.9", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.9", "@vitest/ui": "3.0.9", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ=="], "@humanwhocodes/config-array/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], @@ -4764,6 +4842,8 @@ "@ethereumjs/util/ethereum-cryptography/@scure/bip39": ["@scure/bip39@1.3.0", "", { "dependencies": { "@noble/hashes": "~1.4.0", "@scure/base": "~1.1.6" } }, "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ=="], + "@happy.tech/happybuild/@types/bun/bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], + "@happy.tech/txm/vitest/@vitest/expect": ["@vitest/expect@3.0.9", "", { "dependencies": { "@vitest/spy": "3.0.9", "@vitest/utils": "3.0.9", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig=="], "@happy.tech/txm/vitest/@vitest/mocker": ["@vitest/mocker@3.0.9", "", { "dependencies": { "@vitest/spy": "3.0.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA=="], diff --git a/packages/txm/lib/BlockMonitor.ts b/packages/txm/lib/BlockMonitor.ts index 3cfd27329f..282b101028 100644 --- a/packages/txm/lib/BlockMonitor.ts +++ b/packages/txm/lib/BlockMonitor.ts @@ -2,6 +2,8 @@ import { LogTag, Logger } from "@happy.tech/common" import type { Block } from "viem" import { Topics, eventBus } from "./EventBus.js" import type { TransactionManager } from "./TransactionManager.js" +import { TxmMetrics } from "./telemetry/metrics" + /** * A type alias for {@link Block} with the `blockTag` set to `"latest"`, ensuring type definitions correspond to the latest block. */ @@ -35,6 +37,10 @@ export class BlockMonitor { private onNewBlock(block: LatestBlock) { if (this.blockTimeout) clearTimeout(this.blockTimeout) eventBus.emit(Topics.NewBlock, block) + + TxmMetrics.getInstance().currentBlockGauge.record(Number(block.number)) + TxmMetrics.getInstance().newBlockDelayHistogram.record(Date.now() - Number(block.timestamp) * 1000) + this.scheduleTimeout() } @@ -46,6 +52,7 @@ export class BlockMonitor { } private resetBlockSubscription() { + TxmMetrics.getInstance().resetBlockMonitorCounter.add(1) if (this.unwatch) { this.unwatch() } diff --git a/packages/txm/lib/NonceManager.ts b/packages/txm/lib/NonceManager.ts index c381a87bbf..55c5a6c0ba 100644 --- a/packages/txm/lib/NonceManager.ts +++ b/packages/txm/lib/NonceManager.ts @@ -1,5 +1,8 @@ +import { LogTag, Logger } from "@happy.tech/common" import type { TransactionManager } from "./TransactionManager" +import { TxmMetrics } from "./telemetry/metrics" +/* /* * This class manages the nonce of the account that the transaction manager is using. * @@ -41,10 +44,19 @@ export class NonceManager { public async start() { const address = this.txmgr.viemWallet.account.address - const blockchainNonce = await this.txmgr.viemClient.getTransactionCount({ + const blockchainNonceResult = await this.txmgr.viemClient.safeGetTransactionCount({ address: address, }) + if (blockchainNonceResult.isErr()) { + Logger.instance.error(LogTag.TXM, `Failed to get transaction count for address ${address}`, { + error: blockchainNonceResult.error, + }) + throw new Error("Failed to get transaction count for address") + } + + const blockchainNonce = blockchainNonceResult.value + this.maxExecutedNonce = blockchainNonce const highestDbNonce = this.txmgr.transactionRepository.getHighestNonce() @@ -62,11 +74,14 @@ export class NonceManager { public requestNonce(): number { if (this.returnedNonceQueue.length > 0) { - return this.returnedNonceQueue.shift()! + const nonce = this.returnedNonceQueue.shift()! + TxmMetrics.getInstance().returnedNonceQueueGauge.record(this.returnedNonceQueue.length) + return nonce } const requestedNonce = this.nonce this.nonce = this.nonce + 1 + TxmMetrics.getInstance().nonceManagerGauge.record(this.nonce) return requestedNonce } @@ -79,15 +94,25 @@ export class NonceManager { } else { this.returnedNonceQueue.splice(index, 0, nonce) } + + TxmMetrics.getInstance().returnedNonceCounter.add(1) + TxmMetrics.getInstance().returnedNonceQueueGauge.record(this.returnedNonceQueue.length) } public async resync() { const address = this.txmgr.viemWallet.account.address - const blockchainNonce = await this.txmgr.viemClient.getTransactionCount({ + const blockchainNonceResult = await this.txmgr.viemClient.safeGetTransactionCount({ address: address, }) - this.maxExecutedNonce = blockchainNonce + if (blockchainNonceResult.isErr()) { + Logger.instance.error(LogTag.TXM, `Failed to get transaction count for address ${address}`, { + error: blockchainNonceResult.error, + }) + return + } + + this.maxExecutedNonce = blockchainNonceResult.value } } diff --git a/packages/txm/lib/Transaction.ts b/packages/txm/lib/Transaction.ts index 0dda4ea2c6..48ae0824f7 100644 --- a/packages/txm/lib/Transaction.ts +++ b/packages/txm/lib/Transaction.ts @@ -4,6 +4,7 @@ import type { Address, ContractFunctionArgs, Hash } from "viem" import type { LatestBlock } from "./BlockMonitor" import { Topics, eventBus } from "./EventBus.js" import type { TransactionTable } from "./db/types.js" +import { TxmMetrics } from "./telemetry/metrics" export enum TransactionStatus { /** @@ -204,6 +205,15 @@ export class Transaction { changeStatus(status: TransactionStatus): void { this.status = status this.markUpdated() + + TxmMetrics.getInstance().transactionStatusChangeCounter.add(1, { + status: this.status, + }) + + if (!NotFinalizedStatuses.includes(status)) { + TxmMetrics.getInstance().attemptsUntilFinalization.record(this.attempts.length) + } + eventBus.emit(Topics.TransactionStatusChanged, { transaction: this, }) diff --git a/packages/txm/lib/TransactionCollector.ts b/packages/txm/lib/TransactionCollector.ts index 36cd245aef..a7afaeec98 100644 --- a/packages/txm/lib/TransactionCollector.ts +++ b/packages/txm/lib/TransactionCollector.ts @@ -3,6 +3,7 @@ import type { LatestBlock } from "./BlockMonitor.js" import { Topics, eventBus } from "./EventBus.js" import { AttemptType, TransactionStatus } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" +import { TxmMetrics } from "./telemetry/metrics" /** * This module is responsible for retrieving transactions from the originators when a new block is received. @@ -41,6 +42,8 @@ export class TransactionCollector { return } + TxmMetrics.getInstance().transactionCollectedCounter.add(transactionsBatch.length) + await Promise.all( transactionsBatch.map(async (transaction) => { const nonce = this.txmgr.nonceManager.requestNonce() diff --git a/packages/txm/lib/TransactionManager.ts b/packages/txm/lib/TransactionManager.ts index bdb3f71ad6..20c627ab43 100644 --- a/packages/txm/lib/TransactionManager.ts +++ b/packages/txm/lib/TransactionManager.ts @@ -1,4 +1,6 @@ import type { UUID } from "@happy.tech/common" +import type { MetricReader } from "@opentelemetry/sdk-metrics" +import type { Result } from "neverthrow" import { type Abi, type Hex, @@ -23,6 +25,8 @@ import { TransactionRepository } from "./TransactionRepository.js" import { TransactionSubmitter } from "./TransactionSubmitter.js" import { TxMonitor } from "./TxMonitor.js" import { type EIP1559Parameters, opStackDefaultEIP1559Parameters } from "./eip1559.js" +import { initializeTelemetry } from "./telemetry/instrumentation" +import { TxmMetrics } from "./telemetry/metrics" import { getUrlProtocol } from "./utils/getUrlProtocol" import type { SafeViemPublicClient, SafeViemWalletClient } from "./utils/safeViemClients" import { convertToSafeViemPublicClient, convertToSafeViemWalletClient } from "./utils/safeViemClients" @@ -133,6 +137,30 @@ export type TransactionManagerConfig = { * Default: {@link DefaultRetryPolicyManager} */ retryPolicyManager?: RetryPolicyManager + + /** + * Transaction Manager metrics configuration. + */ + metrics?: { + /** + * Whether to enable metrics collection. + * Defaults to true. + */ + active?: boolean + /** + * Port number for the default Prometheus metrics endpoint. + * The default metric reader is a Prometheus reader that exposes metrics via this endpoint. + * This setting is only used when custom metricReaders are not provided. + * Defaults to 9090. + */ + port?: number + /** + * Custom metric readers to use instead of the default Prometheus reader. + * If provided, these readers will be used and the port setting will be ignored. + * If not provided, a default Prometheus reader will be configured using the specified port. + */ + metricReaders?: MetricReader[] + } } export type TransactionOriginator = (block: LatestBlock) => Promise @@ -172,6 +200,12 @@ export class TransactionManager { public readonly blockInactivityTimeout: number constructor(_config: TransactionManagerConfig) { + initializeTelemetry({ + active: _config.metrics?.active ?? true, + port: _config.metrics?.port ?? 9090, + metricReaders: _config.metrics?.metricReaders, + }) + this.collectors = [] const protocol = getUrlProtocol(_config.rpc.url) @@ -230,6 +264,11 @@ export class TransactionManager { transport, chain, }), + { + rpcCounter: TxmMetrics.getInstance().rpcCounter, + rpcErrorCounter: TxmMetrics.getInstance().rpcErrorCounter, + rpcResponseTimeHistogram: TxmMetrics.getInstance().blockchainRpcResponseTimeHistogram, + }, ) this.viemClient = convertToSafeViemPublicClient( @@ -237,6 +276,11 @@ export class TransactionManager { transport, chain, }), + { + rpcCounter: TxmMetrics.getInstance().rpcCounter, + rpcErrorCounter: TxmMetrics.getInstance().rpcErrorCounter, + rpcResponseTimeHistogram: TxmMetrics.getInstance().blockchainRpcResponseTimeHistogram, + }, ) this.nonceManager = new NonceManager(this) @@ -284,7 +328,7 @@ export class TransactionManager { return this.hookManager.addHook(type, handler) } - public async getTransaction(txIntentId: UUID): Promise { + public async getTransaction(txIntentId: UUID): Promise> { return this.transactionRepository.getTransaction(txIntentId) } diff --git a/packages/txm/lib/TransactionRepository.ts b/packages/txm/lib/TransactionRepository.ts index eb4df0f2b5..8fa8af5cba 100644 --- a/packages/txm/lib/TransactionRepository.ts +++ b/packages/txm/lib/TransactionRepository.ts @@ -1,10 +1,11 @@ import { unknownToError } from "@happy.tech/common" import type { UUID } from "@happy.tech/common" -import { type Result, ResultAsync } from "neverthrow" +import { type Result, ResultAsync, err, ok } from "neverthrow" import { Topics, eventBus } from "./EventBus.js" import { NotFinalizedStatuses, Transaction } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" import { db } from "./db/driver.js" +import { TxmMetrics } from "./telemetry/metrics" /** * This module acts as intermediate layer between the library and the database. @@ -41,21 +42,42 @@ export class TransactionRepository { return this.notFinalizedTransactions.filter((t) => t.collectionBlock && t.collectionBlock < blockNumber) } - async getTransaction(intentId: UUID): Promise { + async getTransaction(intentId: UUID): Promise> { const cachedTransaction = this.notFinalizedTransactions.find((t) => t.intentId === intentId) if (cachedTransaction) { - return cachedTransaction + return ok(cachedTransaction) } - const persistedTransaction = await db - .selectFrom("transaction") - .where("intentId", "=", intentId) - .where("from", "=", this.transactionManager.viemWallet.account.address) - .selectAll() - .executeTakeFirst() + TxmMetrics.getInstance().databaseOperationsCounter.add(1, { + operation: "getTransaction", + }) + const start = Date.now() + + const persistedTransactionResult = await ResultAsync.fromPromise( + db + .selectFrom("transaction") + .where("intentId", "=", intentId) + .where("from", "=", this.transactionManager.viemWallet.account.address) + .selectAll() + .executeTakeFirst(), + unknownToError, + ) + + TxmMetrics.getInstance().databaseOperationDurationHistogram.record(Date.now() - start, { + operation: "getTransaction", + }) - return persistedTransaction ? Transaction.fromDbRow(persistedTransaction) : undefined + if (persistedTransactionResult.isErr()) { + TxmMetrics.getInstance().databaseErrorsCounter.add(1, { + operation: "getTransaction", + }) + return err(persistedTransactionResult.error) + } + + const persistedTransaction = persistedTransactionResult.value + + return persistedTransaction ? ok(Transaction.fromDbRow(persistedTransaction)) : ok(undefined) } async saveTransactions(transactions: Transaction[]): Promise> { @@ -63,6 +85,11 @@ export class TransactionRepository { const notPersistedTransactions = transactions.filter((t) => t.notPersisted) + TxmMetrics.getInstance().databaseOperationsCounter.add(1, { + operation: "saveTransactions", + }) + const start = Date.now() + const result = await ResultAsync.fromPromise( db.transaction().execute(async (dbTransaction) => { const promises = transactionsToFlush.map((t) => { @@ -81,12 +108,22 @@ export class TransactionRepository { unknownToError, ) + TxmMetrics.getInstance().databaseOperationDurationHistogram.record(Date.now() - start, { + operation: "saveTransactions", + }) + if (result.isOk()) { this.notFinalizedTransactions = this.notFinalizedTransactions.filter((transaction) => NotFinalizedStatuses.includes(transaction.status), ) this.notFinalizedTransactions.push(...notPersistedTransactions) transactions.forEach((t) => t.markFlushed()) + + TxmMetrics.getInstance().notFinalizedTransactionsGauge.record(this.notFinalizedTransactions.length) + } else { + TxmMetrics.getInstance().databaseErrorsCounter.add(1, { + operation: "saveTransactions", + }) } return result @@ -105,11 +142,31 @@ export class TransactionRepository { } async purgeFinalizedTransactions() { - await db - .deleteFrom("transaction") - .where("status", "not in", NotFinalizedStatuses) - .where("updatedAt", "<", Date.now() - this.transactionManager.finalizedTransactionPurgeTime) - .where("from", "=", this.transactionManager.viemWallet.account.address) - .execute() + TxmMetrics.getInstance().databaseOperationsCounter.add(1, { + operation: "purgeFinalizedTransactions", + }) + const start = Date.now() + + const result = await ResultAsync.fromPromise( + db + .deleteFrom("transaction") + .where("status", "not in", NotFinalizedStatuses) + .where("updatedAt", "<", Date.now() - this.transactionManager.finalizedTransactionPurgeTime) + .where("from", "=", this.transactionManager.viemWallet.account.address) + .execute(), + unknownToError, + ) + + TxmMetrics.getInstance().databaseOperationDurationHistogram.record(Date.now() - start, { + operation: "purgeFinalizedTransactions", + }) + + if (result.isErr()) { + TxmMetrics.getInstance().databaseErrorsCounter.add(1, { + operation: "purgeFinalizedTransactions", + }) + } + + return result } } diff --git a/packages/txm/lib/TxMonitor.ts b/packages/txm/lib/TxMonitor.ts index f2a1b4d6ab..7265c13da9 100644 --- a/packages/txm/lib/TxMonitor.ts +++ b/packages/txm/lib/TxMonitor.ts @@ -6,6 +6,7 @@ import { Topics, eventBus } from "./EventBus.js" import type { RevertedTransactionReceipt } from "./RetryPolicyManager" import { type Attempt, AttemptType, type Transaction, TransactionStatus } from "./Transaction.js" import type { TransactionManager } from "./TransactionManager.js" +import { TxmMetrics } from "./telemetry/metrics" type AttemptWithReceipt = { attempt: Attempt; receipt: TransactionReceipt } @@ -139,6 +140,10 @@ export class TxMonitor { const { attempt, receipt } = attemptOrResults + TxmMetrics.getInstance().transactionInclusionBlockHistogram.record( + Number(block.number - transaction.collectionBlock!), + ) + if (receipt.status === "success") { if (attempt.type === AttemptType.Cancellation) { Logger.instance.error(LogTag.TXM, `Transaction ${transaction.intentId} was cancelled`) @@ -159,6 +164,8 @@ export class TxMonitor { return transaction.changeStatus(TransactionStatus.Failed) } + TxmMetrics.getInstance().transactionsRetriedCounter.add(1) + return this.handleRetryTransaction(transaction) }) diff --git a/packages/txm/lib/index.ts b/packages/txm/lib/index.ts index 1211ec2110..14af7a2c37 100644 --- a/packages/txm/lib/index.ts +++ b/packages/txm/lib/index.ts @@ -1,3 +1,4 @@ +import "./telemetry/instrumentation.js" export { Transaction, TransactionStatus, type TransactionConstructorConfig } from "./Transaction.js" export { TransactionManager, type TransactionManagerConfig, type TransactionOriginator } from "./TransactionManager.js" export type { Abi } from "viem" diff --git a/packages/txm/lib/telemetry/instrumentation.ts b/packages/txm/lib/telemetry/instrumentation.ts new file mode 100644 index 0000000000..4a80549ffb --- /dev/null +++ b/packages/txm/lib/telemetry/instrumentation.ts @@ -0,0 +1,29 @@ +import opentelemetry from "@opentelemetry/api" +import { PrometheusExporter } from "@opentelemetry/exporter-prometheus" +import { Resource } from "@opentelemetry/resources" +import { MeterProvider, type MetricReader } from "@opentelemetry/sdk-metrics" +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions" + +const resource = Resource.default().merge( + new Resource({ + [ATTR_SERVICE_NAME]: "txm", + [ATTR_SERVICE_VERSION]: "0.1.0", + }), +) + +export function initializeTelemetry({ + active, + port, + metricReaders, +}: { active: boolean; port: number; metricReaders?: MetricReader[] }): void { + if (!active) { + return + } + + const meterProvider = new MeterProvider({ + resource: resource, + readers: metricReaders || [new PrometheusExporter({ port })], + }) + + opentelemetry.metrics.setGlobalMeterProvider(meterProvider) +} diff --git a/packages/txm/lib/telemetry/metrics.ts b/packages/txm/lib/telemetry/metrics.ts new file mode 100644 index 0000000000..54056692c0 --- /dev/null +++ b/packages/txm/lib/telemetry/metrics.ts @@ -0,0 +1,297 @@ +import { ValueType, metrics } from "@opentelemetry/api" + +export class TxmMetrics { + /* General metrics */ + private readonly generalMeter = metrics.getMeter("txm.general") + + public readonly blockchainRpcResponseTimeHistogram = this.generalMeter.createHistogram( + "txm.general.blockchain-rpc-response-time", + { + description: "Elapsed time between sending a request to the blockchain and receiving its response", + unit: "ms", + valueType: ValueType.INT, + advice: { + explicitBucketBoundaries: [ + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1250, + 1500, + 1750, + 2000, + 4000, + Number.POSITIVE_INFINITY, + ], + }, + }, + ) + + public readonly rpcCounter = this.generalMeter.createCounter("txm.general.rpc", { + description: "Number of requests sent to the blockchain", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly rpcErrorCounter = this.generalMeter.createCounter("txm.general.rpc-error", { + description: "Number of errors that occurred while sending requests to the blockchain", + unit: "count", + valueType: ValueType.INT, + }) + + /* NONCE MANAGER METRICS */ + private readonly nonceManagerMeter = metrics.getMeter("txm.nonce-manager") + + public readonly nonceManagerGauge = this.nonceManagerMeter.createGauge("txm.nonce-manager.nonce", { + description: "Current nonce", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly returnedNonceCounter = this.nonceManagerMeter.createCounter("txm.nonce-manager.returned-nonce", { + description: "Number of transaction nonces that were reserved but returned to the queue", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly returnedNonceQueueGauge = this.nonceManagerMeter.createGauge( + "txm.nonce-manager.returned-nonce-queue", + { + description: "Quantity of returned nonces in the queue", + unit: "count", + valueType: ValueType.INT, + }, + ) + + /* TRANSACTION COLLECTOR METRICS */ + private readonly transactionCollectorMeter = metrics.getMeter("txm.transaction-collector") + + public readonly transactionCollectedCounter = this.transactionCollectorMeter.createCounter("txm.collector.count", { + description: "Number of transactions collected", + unit: "count", + valueType: ValueType.INT, + }) + + /* TRANSACTION REPOSITORY METRICS */ + private readonly transactionRepositoryMeter = metrics.getMeter("txm.transaction-repository") + + public readonly notFinalizedTransactionsGauge = this.transactionRepositoryMeter.createGauge( + "txm.transaction-repository.not-finalized-transactions", + { + description: "Quantity of transactions in the repository that are not finalized", + unit: "count", + valueType: ValueType.INT, + }, + ) + + /* TRANSACTION METRICS */ + private readonly transactionMeter = metrics.getMeter("txm.transaction") + + public readonly transactionStatusChangeCounter = this.transactionMeter.createCounter( + "txm.transaction.status-change", + { + description: "Count of transactions transitioning to a different status", + unit: "count", + valueType: ValueType.INT, + }, + ) + + public readonly attemptsUntilFinalization = this.transactionMeter.createHistogram( + "txm.transaction.attempts-until-finalization", + { + description: "Count of attempts until a transaction is finalized", + unit: "count", + valueType: ValueType.INT, + advice: { + explicitBucketBoundaries: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 20, + 40, + 60, + 80, + 100, + 200, + 300, + Number.POSITIVE_INFINITY, + ], + }, + }, + ) + + /* Block Monitor Metrics */ + private readonly blockMonitorMeter = metrics.getMeter("txm.block-monitor") + + public readonly currentBlockGauge = this.blockMonitorMeter.createGauge("txm.block-monitor.current-block", { + description: "Current block number", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly newBlockDelayHistogram = this.blockMonitorMeter.createHistogram( + "txm.block-monitor.new-block-delay", + { + description: "Time delay between when a block is generated and when it is received", + unit: "ms", + valueType: ValueType.INT, + advice: { + explicitBucketBoundaries: [ + 100, + 200, + 300, + 400, + 500, + 600, + 700, + 800, + 900, + 1000, + 1250, + 1500, + 1750, + 2000, + 4000, + Number.POSITIVE_INFINITY, + ], + }, + }, + ) + + public readonly resetBlockMonitorCounter = this.blockMonitorMeter.createCounter("txm.block-monitor.reset", { + description: "Number of times the block monitor has been reset", + unit: "count", + valueType: ValueType.INT, + }) + + /* TX MONITOR METRICS */ + private readonly txMonitorMeter = metrics.getMeter("txm.tx-monitor") + + public readonly transactionsRetriedCounter = this.txMonitorMeter.createCounter( + "txm.tx-monitor.transactions-retried", + { + description: "Number of transactions retried", + unit: "count", + valueType: ValueType.INT, + }, + ) + + public readonly transactionInclusionBlockHistogram = this.txMonitorMeter.createHistogram( + "txm.tx-monitor.transaction-inclusion-block", + { + description: "Number of blocks it takes for a transaction to be included", + unit: "count", + valueType: ValueType.INT, + advice: { + explicitBucketBoundaries: [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 20, + 40, + 60, + 80, + 100, + 200, + 300, + Number.POSITIVE_INFINITY, + ], + }, + }, + ) + + /* Database Metrics */ + private readonly databaseMeter = metrics.getMeter("txm.database") + + public readonly databaseOperationsCounter = this.databaseMeter.createCounter("txm.database.operations", { + description: "Number of database operations", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly databaseErrorsCounter = this.databaseMeter.createCounter("txm.database.errors", { + description: "Number of database errors", + unit: "count", + valueType: ValueType.INT, + }) + + public readonly databaseOperationDurationHistogram = this.databaseMeter.createHistogram( + "txm.database.operation-duration", + { + description: "Duration of database operations", + unit: "ms", + valueType: ValueType.INT, + }, + ) + + /* OS Metrics */ + private readonly osMeter = metrics.getMeter("txm.os") + + public readonly heapUsedGauge = this.osMeter.createObservableGauge("txm.os.heap-used", { + description: "Heap used in bytes", + unit: "bytes", + valueType: ValueType.INT, + }) + + public readonly heapTotalGauge = this.osMeter.createObservableGauge("txm.os.heap-total", { + description: "Heap total in bytes", + unit: "bytes", + valueType: ValueType.INT, + }) + + public readonly processUptimeGauge = this.osMeter.createObservableGauge("txm.os.process-uptime", { + description: "Process uptime in seconds", + unit: "seconds", + valueType: ValueType.INT, + }) + + public readonly processCpuUsageGauge = this.osMeter.createObservableGauge("txm.os.process-cpu-usage", { + description: "Process CPU usage in percentage", + unit: "%", + valueType: ValueType.INT, + }) + + // Singleton instance + private static instance: TxmMetrics + + private constructor() { + this.heapUsedGauge.addCallback((result) => { + result.observe(process.memoryUsage().heapUsed) + }) + + this.heapTotalGauge.addCallback((result) => { + result.observe(process.memoryUsage().heapTotal) + }) + + this.processUptimeGauge.addCallback((result) => { + result.observe(process.uptime()) + }) + } + + public static getInstance(): TxmMetrics { + if (!TxmMetrics.instance) { + TxmMetrics.instance = new TxmMetrics() + } + return TxmMetrics.instance + } +} diff --git a/packages/txm/lib/utils/safeViemClients.ts b/packages/txm/lib/utils/safeViemClients.ts index f0c6572dcb..e75e6876aa 100644 --- a/packages/txm/lib/utils/safeViemClients.ts +++ b/packages/txm/lib/utils/safeViemClients.ts @@ -1,10 +1,12 @@ import { unknownToError } from "@happy.tech/common" +import type { Counter, Histogram } from "@opentelemetry/api" import { ResultAsync } from "neverthrow" import type { Account, Chain, EstimateGasErrorType, GetChainIdErrorType, + GetTransactionCountErrorType, GetTransactionReceiptErrorType, Hash, PublicClient, @@ -82,58 +84,223 @@ export type DebugTransactionSchema = { ReturnType: Call } -export type SafeViemPublicClient = ReturnType - -export function convertToSafeViemPublicClient(client: ViemPublicClient) { - return client.extend((client) => ({ - safeEstimateGas: async (...args: Parameters) => - ResultAsync.fromPromise>, EstimateGasErrorType>( - client.estimateGas(...args), - unknownToError as (u: unknown) => EstimateGasErrorType, - ), - safeGetTransactionReceipt: async (...args: Parameters) => - ResultAsync.fromPromise< - Awaited>, - GetTransactionReceiptErrorType - >(client.getTransactionReceipt(...args), unknownToError as (u: unknown) => GetTransactionReceiptErrorType), - safeDebugTransaction: async (...args: DebugTransactionSchema["Parameters"]) => - ResultAsync.fromPromise( - client.request({ - method: "debug_traceTransaction", - params: args, - }), - unknownToError as (u: unknown) => RpcErrorType, - ), - safeGetChainId: async () => - ResultAsync.fromPromise>, GetChainIdErrorType>( - client.getChainId(), - unknownToError as (u: unknown) => GetChainIdErrorType, - ), - })) +export interface SafeViemPublicClient extends ViemPublicClient { + rpcCounter: Counter | undefined + rpcErrorCounter: Counter | undefined + rpcResponseTimeHistogram: Histogram | undefined + + safeEstimateGas: ( + ...args: Parameters + ) => ResultAsync>, EstimateGasErrorType> + safeGetTransactionReceipt: ( + ...args: Parameters + ) => ResultAsync>, GetTransactionReceiptErrorType> + safeDebugTransaction: ( + ...args: DebugTransactionSchema["Parameters"] + ) => ResultAsync + safeGetChainId: () => ResultAsync>, GetChainIdErrorType> + safeGetTransactionCount: ( + ...args: Parameters + ) => ResultAsync>, GetTransactionCountErrorType> +} + +export interface MetricsHandlers { + rpcCounter?: Counter + rpcErrorCounter?: Counter + rpcResponseTimeHistogram?: Histogram } -export type SafeViemWalletClient = ReturnType - -export function convertToSafeViemWalletClient(client: ViemWalletClient) { - return client.extend((client) => ({ - safeSendRawTransaction: async (...args: Parameters) => - ResultAsync.fromPromise< - Awaited>, - SendRawTransactionErrorType - >(client.sendRawTransaction(...args), unknownToError as (u: unknown) => SendRawTransactionErrorType), - safeSignTransaction: async (args: TransactionRequestEIP1559 & { gas: bigint }) => - ResultAsync.fromThrowable(() => { - if (client.account.signTransaction) { - return client.account.signTransaction({ - ...args, - chainId: client.chain.id, - }) +export function convertToSafeViemPublicClient( + client: ViemPublicClient, + metrics?: MetricsHandlers, +): SafeViemPublicClient { + const safeClient = client as SafeViemPublicClient + + safeClient.rpcCounter = metrics?.rpcCounter + safeClient.rpcErrorCounter = metrics?.rpcErrorCounter + safeClient.rpcResponseTimeHistogram = metrics?.rpcResponseTimeHistogram + + safeClient.safeEstimateGas = (...args: Parameters) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "estimateGas" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.estimateGas(...args), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "estimateGas" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "estimateGas" }) + } + return error as EstimateGasErrorType + }) + } + + safeClient.safeGetTransactionReceipt = (...args: Parameters) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "getTransactionReceipt" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.getTransactionReceipt(...args), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "getTransactionReceipt" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "getTransactionReceipt" }) } - // biome-ignore format: tidy - console.warn( - "No signTransaction method found on the account, using signMessage instead. " + - "A viem update probably change the internal signing API.") - return client.signTransaction(args) - })() as ResultAsync>, SignTransactionErrorType>, - })) + return error as GetTransactionReceiptErrorType + }) + } + + safeClient.safeDebugTransaction = (...args: DebugTransactionSchema["Parameters"]) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "debug_traceTransaction" }) + const startTime = Date.now() + + return ResultAsync.fromPromise( + client.request({ + method: "debug_traceTransaction", + params: args, + }), + unknownToError, + ) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "debug_traceTransaction" }) + return result as Call + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "debug_traceTransaction" }) + } + return error as RpcErrorType + }) + } + + safeClient.safeGetChainId = () => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "getChainId" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.getChainId(), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "getChainId" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "getChainId" }) + } + return error as GetChainIdErrorType + }) + } + + safeClient.safeGetTransactionCount = (...args: Parameters) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "getTransactionCount" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.getTransactionCount(...args), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "getTransactionCount" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "getTransactionCount" }) + } + return error as GetTransactionCountErrorType + }) + } + + return safeClient +} + +export interface SafeViemWalletClient extends ViemWalletClient { + rpcCounter?: Counter + rpcErrorCounter?: Counter + rpcResponseTimeHistogram?: Histogram + + safeSendRawTransaction: ( + ...args: Parameters + ) => ResultAsync>, SendRawTransactionErrorType> + safeSignTransaction: ( + args: TransactionRequestEIP1559 & { gas: bigint }, + ) => ResultAsync>, SignTransactionErrorType> +} + +export function convertToSafeViemWalletClient( + client: ViemWalletClient, + metrics?: MetricsHandlers, +): SafeViemWalletClient { + const safeClient = client as SafeViemWalletClient + + safeClient.rpcCounter = metrics?.rpcCounter + safeClient.rpcErrorCounter = metrics?.rpcErrorCounter + safeClient.rpcResponseTimeHistogram = metrics?.rpcResponseTimeHistogram + + safeClient.safeSendRawTransaction = (...args: Parameters) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "sendRawTransaction" }) + const startTime = Date.now() + + return ResultAsync.fromPromise(client.sendRawTransaction(...args), unknownToError) + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "sendRawTransaction" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "sendRawTransaction" }) + } + return error as SendRawTransactionErrorType + }) + } + + safeClient.safeSignTransaction = (args: TransactionRequestEIP1559 & { gas: bigint }) => { + if (safeClient.rpcCounter) safeClient.rpcCounter.add(1, { method: "signTransaction" }) + const startTime = Date.now() + + return ResultAsync.fromThrowable(() => { + // We first attempt to use the account's signTransaction method since it's more efficient: + // it doesn't make an additional getChainId RPC call when chainId is provided. + // If the account's signTransaction is not available, we fallback to the client's + // signTransaction method, which will make a getChainId call even if chainId is provided. + + if (client.account.signTransaction) { + return client.account.signTransaction({ + ...args, + chainId: client.chain.id, + }) + } + // biome-ignore format: tidy + console.warn( + "No signTransaction method found on the account, using signMessage instead. " + + "A viem update probably change the internal signing API."); + return client.signTransaction(args) + })() + .map((result) => { + const duration = Date.now() - startTime + if (safeClient.rpcResponseTimeHistogram) + safeClient.rpcResponseTimeHistogram.record(duration, { method: "signTransaction" }) + return result + }) + .mapErr((error) => { + if (safeClient.rpcErrorCounter) { + safeClient.rpcErrorCounter.add(1, { method: "signTransaction" }) + } + return error as SignTransactionErrorType + }) + } + + return safeClient } diff --git a/packages/txm/package.json b/packages/txm/package.json index 1f4c4af6b5..2d5c73bf48 100644 --- a/packages/txm/package.json +++ b/packages/txm/package.json @@ -21,6 +21,11 @@ "@happy.tech/configs": "workspace:0.1.0", "@happy.tech/contracts": "workspace:0.1.0", "@hono/node-server": "^1.13.8", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-prometheus": "^0.57.2", + "@opentelemetry/sdk-metrics": "^1.30.1", + "@opentelemetry/sdk-node": "^0.57.2", + "@opentelemetry/sdk-trace-node": "^1.30.1", "better-sqlite3": "^11.5.0", "eventemitter3": "^5.0.1", "hono": "^4.7.2", diff --git a/packages/txm/test/txm.test.ts b/packages/txm/test/txm.test.ts index b2b1f30e86..18b07a27dd 100644 --- a/packages/txm/test/txm.test.ts +++ b/packages/txm/test/txm.test.ts @@ -26,7 +26,7 @@ import { RPC_URL, } from "./utils/constants" import { deployMockContracts } from "./utils/contracts" -import { assertIsDefined, assertReceiptReverted, assertReceiptSuccess } from "./utils/customAsserts" +import { assertIsDefined, assertIsOk, assertReceiptReverted, assertReceiptSuccess } from "./utils/customAsserts" import { cleanDB, getPersistedTransaction } from "./utils/db" const retryManager = new TestRetryManager() @@ -44,6 +44,9 @@ const txm = new TransactionManager({ retryPolicyManager: retryManager, baseFeePercentageMargin: BASE_FEE_PERCENTAGE_MARGIN, eip1559: ethereumDefaultEIP1559Parameters, + metrics: { + active: false, + }, }) const fromAddress = privateKeyToAddress(PRIVATE_KEY) @@ -193,9 +196,13 @@ test("TransactionSubmissionFailed hook works correctly", async () => { // and to establish a clean starting point for subsequent test cases await mineBlock() - const retrievedTransaction = await txm.getTransaction(transaction.intentId) + const retrievedTransactionResult = await txm.getTransaction(transaction.intentId) const persistedTransaction = await getPersistedTransaction(transaction.intentId) + if (!assertIsOk(retrievedTransactionResult)) return + + const retrievedTransaction = retrievedTransactionResult.value + if (!assertIsDefined(retrievedTransaction)) return expect(retrievedTransaction.status).toBe(TransactionStatus.Success) @@ -248,7 +255,11 @@ test("Simple transaction executed", async () => { await mineBlock(2) - const retrievedTransaction = await txm.getTransaction(transaction.intentId) + const retrievedTransactionResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(retrievedTransactionResult)) return + + const retrievedTransaction = retrievedTransactionResult.value if (!assertIsDefined(retrievedTransaction)) return @@ -281,7 +292,11 @@ test("Transaction retried", async () => { await mineBlock(2) - const transactionPending = await txm.getTransaction(transaction.intentId) + const transactionPendingResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionPendingResult)) return + + const transactionPending = transactionPendingResult.value if (!assertIsDefined(transactionPending)) return @@ -299,7 +314,11 @@ test("Transaction retried", async () => { await mineBlock() - const transactionSuccess = await txm.getTransaction(transaction.intentId) + const transactionSuccessResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionSuccessResult)) return + + const transactionSuccess = transactionSuccessResult.value if (!assertIsDefined(transactionSuccess)) return @@ -336,7 +355,11 @@ test("Transaction failed", async () => { await mineBlock(2) - const transactionReverted = await txm.getTransaction(transaction.intentId) + const transactionRevertedResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionRevertedResult)) return + + const transactionReverted = transactionRevertedResult.value if (!assertIsDefined(transactionReverted)) return @@ -385,7 +408,11 @@ test("Transaction failed for out of gas", async () => { await mineBlock(2) - const transactionReverted = await txm.getTransaction(transaction.intentId) + const transactionRevertedResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionRevertedResult)) return + + const transactionReverted = transactionRevertedResult.value if (!assertIsDefined(transactionReverted)) return @@ -442,7 +469,11 @@ test("Transaction cancelled due to deadline passing", async () => { await mineBlock() - const transactionCancelling = await txm.getTransaction(transaction.intentId) + const transactionCancellingResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionCancellingResult)) return + + const transactionCancelling = transactionCancellingResult.value if (!assertIsDefined(transactionCancelling)) return @@ -450,7 +481,11 @@ test("Transaction cancelled due to deadline passing", async () => { await mineBlock() - const transactionCancelled = await txm.getTransaction(transaction.intentId) + const transactionCancelledResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(transactionCancelledResult)) return + + const transactionCancelled = transactionCancelledResult.value if (!assertIsDefined(transactionCancelled)) return @@ -495,7 +530,11 @@ test("Correctly calculates baseFeePerGas after a block with high gas usage", asy await mineBlock(2) - const transactionBurnerExecuted = await txm.getTransaction(transactionBurner.intentId) + const transactionBurnerExecutedResult = await txm.getTransaction(transactionBurner.intentId) + + if (!assertIsOk(transactionBurnerExecutedResult)) return + + const transactionBurnerExecuted = transactionBurnerExecutedResult.value if (!assertIsDefined(transactionBurnerExecuted)) return @@ -515,7 +554,11 @@ test("Correctly calculates baseFeePerGas after a block with high gas usage", asy }) ).baseFeePerGas - const incrementerExecuted = await txm.getTransaction(incrementerTransaction.intentId) + const incrementerExecutedResult = await txm.getTransaction(incrementerTransaction.intentId) + + if (!assertIsOk(incrementerExecutedResult)) return + + const incrementerExecuted = incrementerExecutedResult.value if (!assertIsDefined(incrementerExecuted)) return @@ -554,9 +597,15 @@ test("Transaction manager successfully processes transactions despite random RPC let successfulTransactions = 0 for (const [index, transaction] of emittedTransactions.entries()) { - const executedTransaction = await txm.getTransaction(transaction.intentId) + const executedTransactionResult = await txm.getTransaction(transaction.intentId) + + if (!assertIsOk(executedTransactionResult)) return - if (executedTransaction?.status === TransactionStatus.Success) { + const executedTransaction = executedTransactionResult.value + + if (!assertIsDefined(executedTransaction)) return + + if (executedTransaction.status === TransactionStatus.Success) { successfulTransactions++ } @@ -589,16 +638,26 @@ test("Transaction succeeds in congested blocks", async () => { await sendBurnGasTransactionWithSecondWallet(2) - const executedIncrementerTransaction = await txm.getTransaction(incrementerTransaction.intentId) + const executedIncrementerTransactionResult = await txm.getTransaction(incrementerTransaction.intentId) + + if (!assertIsOk(executedIncrementerTransactionResult)) return + + const executedIncrementerTransaction = executedIncrementerTransactionResult.value - if (executedIncrementerTransaction?.status === TransactionStatus.Success) { + if (!assertIsDefined(executedIncrementerTransaction)) return + + if (executedIncrementerTransaction.status === TransactionStatus.Success) { break } iterations++ } - const executedIncrementerTransaction = await txm.getTransaction(incrementerTransaction.intentId) + const executedIncrementerTransactionResult = await txm.getTransaction(incrementerTransaction.intentId) + + if (!assertIsOk(executedIncrementerTransactionResult)) return + + const executedIncrementerTransaction = executedIncrementerTransactionResult.value if (!assertIsDefined(executedIncrementerTransaction)) return diff --git a/packages/txm/test/utils/customAsserts.ts b/packages/txm/test/utils/customAsserts.ts index 2a1e55cca6..1c168a52e4 100644 --- a/packages/txm/test/utils/customAsserts.ts +++ b/packages/txm/test/utils/customAsserts.ts @@ -1,3 +1,4 @@ +import type { Ok, Result } from "neverthrow" import type { Address, TransactionReceipt } from "viem" import { expect } from "vitest" @@ -21,3 +22,8 @@ export function assertIsDefined(value: T): value is NonNullable { expect(isDefined).toBe(true) return isDefined } + +export function assertIsOk(result: Result): result is Ok { + expect(result.isOk()).toBe(true) + return result.isOk() +} diff --git a/packages/txm/vitest.setup.ts b/packages/txm/vitest.setup.ts index 248050839b..3db7ff801e 100644 --- a/packages/txm/vitest.setup.ts +++ b/packages/txm/vitest.setup.ts @@ -1 +1,3 @@ +import "./lib/telemetry/instrumentation" + process.env.TXM_DB_PATH = "./test/txm.sqlite" diff --git a/support/common/package.json b/support/common/package.json index 1b0bf5244e..7b065c4386 100644 --- a/support/common/package.json +++ b/support/common/package.json @@ -28,5 +28,8 @@ "@happy.tech/happybuild": "workspace:0.1.1", "@types/react": "^18.3.4", "vite-plugin-node-polyfills": "^0.22.0" + }, + "dependencies": { + "@opentelemetry/api": "^1.9.0" } }