diff --git a/api/.env.example b/api/.env.example index c5557eda..4d8e8dba 100644 --- a/api/.env.example +++ b/api/.env.example @@ -35,3 +35,9 @@ COMMUNITY_WALLETS_URL="https://raw.githubusercontent.com/soaresa/test_data/refs/ # Path to the json file containing well known addresses info KNOWN_ADDRESSES_URL="https://raw.githubusercontent.com/soaresa/test_data/refs/heads/main/known-addresses.json" + +# Neo4j Configuration +NEO4J_URL=bolt://localhost:7687 +NEO4J_USERNAME=neo4j +NEO4J_PASSWORD=password +NEO4J_DATABASE=neo4j diff --git a/api/package-lock.json b/api/package-lock.json index a4f37f04..b6ee69b0 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -20,6 +20,7 @@ "@nestjs/core": "^10.3.9", "@nestjs/graphql": "^12.1.1", "@nestjs/platform-express": "^10.3.9", + "@nestjs/typeorm": "^11.0.0", "@parse/node-apn": "^6.0.1", "@prisma/client": "^5.19.0", "@repeaterjs/repeater": "^3.0.6", @@ -31,6 +32,8 @@ "bluebird": "^3.7.2", "bn.js": "^5.2.1", "bullmq": "^5.8.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "csv-stringify": "^6.5.0", "d3-array": "^3.2.4", "decimal.js": "^10.4.3", @@ -41,6 +44,7 @@ "lodash": "^4.17.21", "maxmind": "^4.3.20", "nats": "^2.26.0", + "neo4j-driver": "^5.17.0", "path": "^0.12.7", "qs": "^6.12.1", "reflect-metadata": "^0.2.2", @@ -1954,7 +1958,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -1966,7 +1970,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, + "devOptional": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -2383,7 +2387,6 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -2400,7 +2403,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, "engines": { "node": ">=12" }, @@ -2412,7 +2414,6 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "engines": { "node": ">=12" }, @@ -2423,14 +2424,12 @@ "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -2447,7 +2446,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "dependencies": { "ansi-regex": "^6.0.1" }, @@ -2462,7 +2460,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -2922,7 +2919,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -2950,7 +2947,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true + "devOptional": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -3441,6 +3438,19 @@ } } }, + "node_modules/@nestjs/typeorm": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-11.0.0.tgz", + "integrity": "sha512-SOeUQl70Lb2OfhGkvnh4KXWlsd+zA08RuuQgT7kKbzivngxzSo1Oc7Usu5VxCxACQC9wc2l9esOHILSJeK7rJA==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "@nestjs/core": "^10.0.0 || ^11.0.0", + "reflect-metadata": "^0.1.13 || ^0.2.0", + "rxjs": "^7.2.0", + "typeorm": "^0.3.0" + } + }, "node_modules/@noble/curves": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", @@ -3530,7 +3540,6 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, "optional": true, "engines": { "node": ">=14" @@ -3668,7 +3677,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" @@ -3678,7 +3687,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.0.tgz", "integrity": "sha512-aR0uffYI700OEEH4gYnitAnv3vzVGXCFvYfdpu/CJKvk4pHfLPEy/JSZyrpQ+15WhXe1yJRXLtfQ84s4mEXnPg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2", @@ -3693,14 +3702,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/@redis/graph": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz", "integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" @@ -3710,7 +3719,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz", "integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" @@ -3720,7 +3729,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz", "integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" @@ -3730,7 +3739,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz", "integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@redis/client": "^1.0.0" @@ -4447,6 +4456,13 @@ "node": ">=16.0.0" } }, + "node_modules/@sqltools/formatter": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz", + "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==", + "license": "MIT", + "peer": true + }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -4471,25 +4487,25 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true + "devOptional": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true + "devOptional": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4852,6 +4868,12 @@ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "optional": true }, + "node_modules/@types/validator": { + "version": "13.15.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.1.tgz", + "integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -5295,7 +5317,7 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true, + "devOptional": true, "bin": { "acorn": "bin/acorn" }, @@ -5325,7 +5347,7 @@ "version": "8.3.3", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", - "dev": true, + "devOptional": true, "dependencies": { "acorn": "^8.11.0" }, @@ -5435,7 +5457,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "devOptional": true, "engines": { "node": ">=8" } @@ -5454,6 +5475,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -5477,6 +5508,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/append-field": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", @@ -5525,7 +5566,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true + "devOptional": true }, "node_modules/argparse": { "version": "2.0.1", @@ -5729,14 +5770,12 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "devOptional": true, "funding": [ { "type": "github", @@ -5852,7 +5891,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -6152,6 +6190,23 @@ "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6204,7 +6259,6 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "devOptional": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -6218,7 +6272,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "devOptional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -6500,7 +6553,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true + "devOptional": true }, "node_modules/cron-parser": { "version": "4.9.0", @@ -6528,7 +6581,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -6559,6 +6611,13 @@ "node": ">=12" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "peer": true + }, "node_modules/debug": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", @@ -6606,10 +6665,10 @@ } }, "node_modules/dedent": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", - "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", - "dev": true, + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -6735,7 +6794,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.3.1" } @@ -6815,8 +6874,7 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", @@ -6867,8 +6925,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "devOptional": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -6937,7 +6994,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", - "devOptional": true, "engines": { "node": ">=6" } @@ -7703,7 +7759,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" @@ -7912,7 +7967,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 4" @@ -7931,7 +7986,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "devOptional": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -8380,7 +8434,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -8590,7 +8643,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "devOptional": true, "engines": { "node": ">=8" } @@ -8673,8 +8725,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", @@ -8768,7 +8819,6 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" }, @@ -9730,6 +9780,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.9", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.9.tgz", + "integrity": "sha512-VWwAdNeJgN7jFOD+wN4qx83DTPMVPPAUyx9/TUkBXKLiNkuWWk6anV0439tgdtwaJDrEdqkvdN22iA6J4bUCZg==", + "license": "MIT" + }, "node_modules/limiter": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", @@ -9926,7 +9982,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true + "devOptional": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -10072,7 +10128,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -10095,7 +10150,6 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -10208,6 +10262,58 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/neo4j-driver": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-5.28.1.tgz", + "integrity": "sha512-jbyBwyM0a3RLGcP43q3hIxPUPxA+1bE04RovOKdNAS42EtBMVCKcPSeOvWiHxgXp1ZFd0a8XqK+7LtguInOLUg==", + "license": "Apache-2.0", + "dependencies": { + "neo4j-driver-bolt-connection": "5.28.1", + "neo4j-driver-core": "5.28.1", + "rxjs": "^7.8.1" + } + }, + "node_modules/neo4j-driver-bolt-connection": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.28.1.tgz", + "integrity": "sha512-nY8GBhjOW7J0rDtpiyJn6kFdk2OiNVZZhZrO8//mwNXnf5VQJ6HqZQTDthH/9pEaX0Jvbastz1xU7ZL8xzqY0w==", + "license": "Apache-2.0", + "dependencies": { + "buffer": "^6.0.3", + "neo4j-driver-core": "5.28.1", + "string_decoder": "^1.3.0" + } + }, + "node_modules/neo4j-driver-bolt-connection/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/neo4j-driver-core": { + "version": "5.28.1", + "resolved": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-5.28.1.tgz", + "integrity": "sha512-14vN8TlxC0JvJYfjWic5PwjsZ38loQLOKFTXwk4fWLTbCk6VhrhubB2Jsy9Rz+gM6PtTor4+6ClBEFDp1q/c8g==", + "license": "Apache-2.0" + }, "node_modules/nkeys.js": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz", @@ -10478,8 +10584,7 @@ "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", - "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", - "dev": true + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==" }, "node_modules/parent-module": { "version": "1.0.1", @@ -10550,7 +10655,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -10565,7 +10669,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -10580,8 +10683,7 @@ "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" }, "node_modules/path-to-regexp": { "version": "3.2.0", @@ -11023,7 +11125,7 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", - "dev": true, + "devOptional": true, "license": "MIT", "workspaces": [ "./packages/*" @@ -11074,7 +11176,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -11504,7 +11605,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -11516,7 +11616,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -11542,7 +11641,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { "node": ">=14" }, @@ -11599,6 +11697,23 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sql-highlight": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sql-highlight/-/sql-highlight-6.1.0.tgz", + "integrity": "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA==", + "funding": [ + "https://github.com/scriptcoded/sql-highlight?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/scriptcoded" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -11660,7 +11775,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "devOptional": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -11682,7 +11796,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "devOptional": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11697,7 +11810,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -11711,7 +11823,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "devOptional": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -11724,7 +11835,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -12290,7 +12400,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, + "devOptional": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -12426,11 +12536,223 @@ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, + "node_modules/typeorm": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.3.24.tgz", + "integrity": "sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@sqltools/formatter": "^1.2.5", + "ansis": "^3.17.0", + "app-root-path": "^3.1.0", + "buffer": "^6.0.3", + "dayjs": "^1.11.13", + "debug": "^4.4.0", + "dedent": "^1.6.0", + "dotenv": "^16.4.7", + "glob": "^10.4.5", + "sha.js": "^2.4.11", + "sql-highlight": "^6.0.0", + "tslib": "^2.8.1", + "uuid": "^11.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "typeorm": "cli.js", + "typeorm-ts-node-commonjs": "cli-ts-node-commonjs.js", + "typeorm-ts-node-esm": "cli-ts-node-esm.js" + }, + "engines": { + "node": ">=16.13.0" + }, + "funding": { + "url": "https://opencollective.com/typeorm" + }, + "peerDependencies": { + "@google-cloud/spanner": "^5.18.0 || ^6.0.0 || ^7.0.0", + "@sap/hana-client": "^2.12.25", + "better-sqlite3": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "hdb-pool": "^0.1.6", + "ioredis": "^5.0.4", + "mongodb": "^5.8.0 || ^6.0.0", + "mssql": "^9.1.1 || ^10.0.1 || ^11.0.1", + "mysql2": "^2.2.5 || ^3.0.1", + "oracledb": "^6.3.0", + "pg": "^8.5.1", + "pg-native": "^3.0.0", + "pg-query-stream": "^4.0.0", + "redis": "^3.1.1 || ^4.0.0", + "reflect-metadata": "^0.1.14 || ^0.2.0", + "sql.js": "^1.4.0", + "sqlite3": "^5.0.3", + "ts-node": "^10.7.0", + "typeorm-aurora-data-api-driver": "^2.0.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@google-cloud/spanner": { + "optional": true + }, + "@sap/hana-client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "hdb-pool": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "mongodb": { + "optional": true + }, + "mssql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "pg-query-stream": { + "optional": true + }, + "redis": { + "optional": true + }, + "sql.js": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "ts-node": { + "optional": true + }, + "typeorm-aurora-data-api-driver": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/typeorm/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/typeorm/node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/typeorm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typeorm/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT", + "peer": true + }, + "node_modules/typeorm/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "peer": true + }, + "node_modules/typeorm/node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12553,7 +12875,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true + "devOptional": true }, "node_modules/v8-to-istanbul": { "version": "9.3.0", @@ -12569,6 +12891,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/value-or-promise": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.12.tgz", @@ -12767,7 +13098,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -12806,7 +13136,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -12895,7 +13224,6 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "devOptional": true, "engines": { "node": ">=10" } @@ -12910,7 +13238,6 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "devOptional": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -12928,7 +13255,6 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "devOptional": true, "engines": { "node": ">=12" } @@ -12937,7 +13263,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } diff --git a/api/package.json b/api/package.json index 5511c9f1..a09f772a 100644 --- a/api/package.json +++ b/api/package.json @@ -38,6 +38,7 @@ "@nestjs/core": "^10.3.9", "@nestjs/graphql": "^12.1.1", "@nestjs/platform-express": "^10.3.9", + "@nestjs/typeorm": "^11.0.0", "@parse/node-apn": "^6.0.1", "@prisma/client": "^5.19.0", "@repeaterjs/repeater": "^3.0.6", @@ -49,6 +50,8 @@ "bluebird": "^3.7.2", "bn.js": "^5.2.1", "bullmq": "^5.8.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "csv-stringify": "^6.5.0", "d3-array": "^3.2.4", "decimal.js": "^10.4.3", @@ -59,6 +62,7 @@ "lodash": "^4.17.21", "maxmind": "^4.3.20", "nats": "^2.26.0", + "neo4j-driver": "^5.17.0", "path": "^0.12.7", "qs": "^6.12.1", "reflect-metadata": "^0.2.2", diff --git a/api/src/app/app.module.ts b/api/src/app/app.module.ts index 74661f86..c3fea5ba 100644 --- a/api/src/app/app.module.ts +++ b/api/src/app/app.module.ts @@ -7,6 +7,7 @@ import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo'; import config from '../config/config.js'; import { AppService } from './app.service.js'; import { ClickhouseModule } from '../clickhouse/clickhouse.module.js'; +import { Neo4jModule } from '../neo4j/neo4j.module.js'; import { OlModule } from '../ol/ol.module.js'; import { S3Module } from '../s3/s3.module.js'; import { NodeWatcherModule } from '../node-watcher/node-watcher.module.js'; @@ -40,6 +41,7 @@ import { MultiSigModule } from '../multi-sig/multi-sig.module.js'; S3Module, ClickhouseModule, + Neo4jModule, OlModule, NodeWatcherModule, StatsModule, diff --git a/api/src/clickhouse/clickhouse.service.ts b/api/src/clickhouse/clickhouse.service.ts index a591b19d..90736469 100644 --- a/api/src/clickhouse/clickhouse.service.ts +++ b/api/src/clickhouse/clickhouse.service.ts @@ -16,6 +16,9 @@ export interface ClickhouseQueryResponse { statistics: { elapsed: number; rows_read: number; bytes_read: number }; } +// Version 0's timestamp is calculated by subtracting the interval between epoch 2 and 3 from epoch 2's timestamp +const V0_TIMESTAMP = 1701203279; + @Injectable() export class ClickhouseService implements OnModuleInit, OnApplicationShutdown { private insertQueries = new Map(); @@ -150,4 +153,117 @@ export class ClickhouseService implements OnModuleInit, OnApplicationShutdown { throw error; } } + + /** + * Convert transaction versions to timestamps using the same method as ol.controller.ts + * @param versions Array of transaction versions to convert + * @returns Mapping of versions to timestamps (in seconds) + */ + public async convertVersionsToTimestamps(versions: number[]): Promise> { + if (!versions || versions.length === 0) { + return new Map(); + } + + try { + // Using the same query approach as in ol.controller.ts + const timestampQuery = ` + WITH + "gen_txs" AS ( + SELECT ("version" + 1) as "version" + FROM "genesis_transaction" + WHERE + "genesis_transaction"."version" IN (${versions.join(',')}) + ), + + "txs" AS ( + SELECT "timestamp", "version" + FROM "block_metadata_transaction" + WHERE "version" IN (SELECT "version" FROM "gen_txs") + + UNION ALL + + SELECT "timestamp", "version" + FROM "state_checkpoint_transaction" + WHERE "version" IN (SELECT "version" FROM "gen_txs") + + UNION ALL + + SELECT "timestamp", "version" + FROM "user_transaction" + WHERE "version" IN (SELECT "version" FROM "gen_txs") + + UNION ALL + + SELECT "timestamp", "version" + FROM "script" + WHERE "version" IN (SELECT "version" FROM "gen_txs") + ), + + "tx_timestamps" AS ( + SELECT + toUInt64("txs"."timestamp" - 1) as "timestamp", + toUInt64("version" - 1) as "version" + FROM "txs" + + UNION ALL + + SELECT "timestamp", "version" + FROM "block_metadata_transaction" + WHERE "version" IN (${versions.join(',')}) + + UNION ALL + + SELECT "timestamp", "version" + FROM "state_checkpoint_transaction" + WHERE "version" IN (${versions.join(',')}) + + UNION ALL + + SELECT "timestamp", "version" + FROM "user_transaction" + WHERE "version" IN (${versions.join(',')}) + + UNION ALL + + SELECT "timestamp", "version" + FROM "script" + WHERE "version" IN (${versions.join(',')}) + ) + + SELECT "timestamp", "version" + FROM "tx_timestamps" + ORDER BY "version" ASC + `; + + const resultSet = await this.client.query({ + query: timestampQuery, + format: 'JSONEachRow', + }); + + const rows = await resultSet.json<{ + timestamp: string; + version: string; + }>(); + + // Create a map of version to timestamp (converted to seconds) + const versionToTimestampMap = new Map(); + + rows.forEach(row => { + const version = parseInt(row.version, 10); + // Convert from microseconds to seconds + const timestamp = Math.floor(parseInt(row.timestamp, 10) / 1_000_000); + versionToTimestampMap.set(version, timestamp); + }); + + // Handle version 0 with hardcoded timestamp if needed + if (versions.includes(0) && !versionToTimestampMap.has(0)) { + versionToTimestampMap.set(0, V0_TIMESTAMP); + } + + return versionToTimestampMap; + } catch (error) { + this.logger.error(`Error converting versions to timestamps: ${error.message}`); + throw error; + } + } } diff --git a/api/src/config/config.interface.ts b/api/src/config/config.interface.ts index 7bf4b11a..df377a9f 100644 --- a/api/src/config/config.interface.ts +++ b/api/src/config/config.interface.ts @@ -10,6 +10,7 @@ export interface Config { apn?: ApnConfig; firebase?: FirebaseConfig; nats: NatsConfig; + neo4j: Neo4jConfig; } export interface InfoConfig { @@ -56,3 +57,9 @@ export interface FirebaseConfig { export interface NatsConfig { servers: string; } + +export interface Neo4jConfig { + url: string; + username: string; + password: string; +} diff --git a/api/src/config/config.ts b/api/src/config/config.ts index 57d92736..8874effb 100644 --- a/api/src/config/config.ts +++ b/api/src/config/config.ts @@ -57,6 +57,12 @@ export default (): Config => { nats: { servers: ENV.NATS_SERVERS!, }, + + neo4j: { + url: ENV.NEO4J_URL || 'bolt://localhost:7687', + username: ENV.NEO4J_USERNAME || 'neo4j', + password: ENV.NEO4J_PASSWORD || 'password', + }, }; return config; diff --git a/api/src/neo4j/dto/query.dto.ts b/api/src/neo4j/dto/query.dto.ts new file mode 100644 index 00000000..16b13c66 --- /dev/null +++ b/api/src/neo4j/dto/query.dto.ts @@ -0,0 +1,10 @@ +import { IsObject, IsOptional, IsString } from 'class-validator'; + +export class CypherQueryDto { + @IsString() + query: string; + + @IsObject() + @IsOptional() + params?: Record; +} diff --git a/api/src/neo4j/neo4j.controller.ts b/api/src/neo4j/neo4j.controller.ts new file mode 100644 index 00000000..73504c52 --- /dev/null +++ b/api/src/neo4j/neo4j.controller.ts @@ -0,0 +1,28 @@ +import { Body, Controller, Get, Logger, Param, Post } from '@nestjs/common'; +import { Neo4jService } from './neo4j.service.js'; +import { CypherQueryDto } from './dto/query.dto.js'; + +@Controller('neo4j') +export class Neo4jController { + private readonly logger = new Logger(Neo4jController.name); + + constructor(private readonly neo4jService: Neo4jService) {} + + // @Post('query') + // async executeQuery(@Body() queryDto: CypherQueryDto) { + // this.logger.debug(`Executing Neo4j query: ${queryDto.query}`); + // try { + // const results = await this.neo4jService.runQuery(queryDto.query, queryDto.params || {}); + // return { results }; + // } catch (error) { + // this.logger.error(`Error executing Neo4j query: ${error.message}`, error.stack); + // throw error; + // } + // } + + @Get('wallet/:address/connections') + async getWalletConnections(@Param('address') address: string) { + this.logger.debug(`Fetching connections for wallet: ${address}`); + return this.neo4jService.getAllWalletFirstDegree(address); + } +} diff --git a/api/src/neo4j/neo4j.module.ts b/api/src/neo4j/neo4j.module.ts new file mode 100644 index 00000000..27d4df6e --- /dev/null +++ b/api/src/neo4j/neo4j.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { Neo4jService } from './neo4j.service.js'; +import { Neo4jController } from './neo4j.controller.js'; + +@Module({ + providers: [Neo4jService], + exports: [Neo4jService], + controllers: [Neo4jController], +}) +export class Neo4jModule {} diff --git a/api/src/neo4j/neo4j.service.ts b/api/src/neo4j/neo4j.service.ts new file mode 100644 index 00000000..e2d6e1db --- /dev/null +++ b/api/src/neo4j/neo4j.service.ts @@ -0,0 +1,275 @@ +import { Injectable, OnModuleDestroy, OnModuleInit, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import * as neo4j from 'neo4j-driver'; + +@Injectable() +export class Neo4jService implements OnModuleInit, OnModuleDestroy { + private driver: neo4j.Driver; + private readonly logger = new Logger(Neo4jService.name); + + constructor(private configService: ConfigService) {} + + async onModuleInit() { + try { + this.driver = neo4j.driver( + this.configService.get('neo4j.url') || 'bolt://localhost:7687', + neo4j.auth.basic( + this.configService.get('neo4j.username') || 'neo4j', + this.configService.get('neo4j.password') || 'password' + ), + { + disableLosslessIntegers: true, + } + ); + + await this.driver.verifyConnectivity(); + this.logger.log('Successfully connected to Neo4j'); + } catch (error) { + this.logger.error(`Failed to connect to Neo4j: ${error.message}`); + throw error; + } + } + + async onModuleDestroy() { + if (this.driver) { + await this.driver.close(); + this.logger.log('Neo4j connection closed'); + } + } + + /** + * Get all first-degree connections of a wallet + * @param address The wallet address to query + * @returns All nodes and relationships connected to the given address + */ + async getAllWalletFirstDegree(address: string) { + const sanitizedAddress = address.startsWith('0x') + ? address.substring(2).toUpperCase() + : address.toUpperCase(); + + const session = this.driver.session({ + database: this.configService.get('neo4j.database'), + }); + + try { + const result = await session.run( + ` + MATCH (w1:Wallet {address: $address})-[r]->(w2:Wallet) + RETURN w1, r, w2 + UNION + MATCH (w1:Wallet)-[r]->(w2:Wallet {address: $address}) + RETURN w1, r, w2 + `, + { address: sanitizedAddress } + ); + + // Get all versions that need timestamp conversion + const versionsToConvert: number[] = []; + result.records.forEach(record => { + const relProps = record.get('r').properties; + if (relProps.version && !relProps.timestamp) { + const version = this.parseNeo4jValue(relProps.version); + if (typeof version === 'number') { + versionsToConvert.push(version); + } + } + }); + + // Create a mapping of version to timestamp if we have versions to convert + const versionToTimestampMap = new Map(); + if (versionsToConvert.length > 0) { + try { + // Import the ClickhouseService to use its conversion method + const { ClickhouseService } = await import('../clickhouse/clickhouse.service.js'); + const clickhouseService = new ClickhouseService(this.configService); + + // Process versions in chunks to avoid overwhelming the database + const chunkSize = 100; + for (let i = 0; i < versionsToConvert.length; i += chunkSize) { + const chunk = versionsToConvert.slice(i, i + chunkSize); + + // Use the new method from ClickhouseService + const chunkTimestampMap = await clickhouseService.convertVersionsToTimestamps(chunk); + + // Merge the results into our main map + chunkTimestampMap.forEach((timestamp, version) => { + versionToTimestampMap.set(version, timestamp); + }); + } + } catch (error) { + this.logger.error('Error mapping versions to timestamps:', error); + } + } + + // Process the results + const nodes = new Map(); + const relationships: Array> = []; + + result.records.forEach(record => { + const w1 = this.processNodeSimplified(record.get('w1')); + const w2 = this.processNodeSimplified(record.get('w2')); + + // Process relationship with timestamp mapping + const rel = this.processRelationshipSimplified( + record.get('r'), + versionToTimestampMap + ); + + // Add nodes to map to deduplicate + nodes.set(w1.address, w1); + nodes.set(w2.address, w2); + + // Add relationship + relationships.push(rel); + }); + + return { + nodes: Array.from(nodes.values()), + relationships: relationships + }; + } catch (error) { + this.logger.error(`Error in getAllWalletFirstDegree: ${error.message}`, error.stack); + throw error; + } finally { + await session.close(); + } + } + + /** + * Process a Neo4j node into a simplified object with only necessary properties + */ + private processNodeSimplified(node: neo4j.Node) { + const properties = node.properties; + + // Calculate total inputs and outputs + const v5_in = this.parseNeo4jValue(properties.v5_in) || 0; + const v6_in = this.parseNeo4jValue(properties.v6_in) || 0; + const v6_total_in = this.parseNeo4jValue(properties.v6_total_in) || 0; + const v7_in = this.parseNeo4jValue(properties.v7_in) || 0; + + const v5_out = this.parseNeo4jValue(properties.v5_out) || 0; + const v6_out = this.parseNeo4jValue(properties.v6_out) || 0; + const v6_total_out = this.parseNeo4jValue(properties.v6_total_out) || 0; + const v7_out = this.parseNeo4jValue(properties.v7_out) || 0; + + return { + id: node.identity.toString(), + address: properties.address, + balance: this.parseNeo4jValue(properties.balance) || 0, + locked: this.parseNeo4jValue(properties.locked) || 0, + totalIn: v5_in + v6_in + v6_total_in + v7_in, + totalOut: v5_out + v6_out + v6_total_out + v7_out, + }; + } + + /** + * Process a Neo4j relationship into a simplified object with only necessary properties + * @param rel The Neo4j relationship to process + * @param versionToTimestampMap Optional map to convert versions to timestamps + * @returns A simplified relationship object + */ + private processRelationshipSimplified(rel: neo4j.Relationship, versionToTimestampMap?: Map) { + const properties = this.parseNeo4jProperties(rel.properties); + + // Add timestamp if we have version but no timestamp + if (properties.version && !properties.timestamp && versionToTimestampMap) { + const version = properties.version; + const timestamp = versionToTimestampMap.get(version); + if (timestamp) { + properties.timestamp = timestamp; + } + } + + return { + type: rel.type, + startNodeId: rel.start.toString(), + endNodeId: rel.end.toString(), + ...properties, + }; + } + + /** + * Parse Neo4j properties to handle special types like integers, dates, etc. + */ + private parseNeo4jProperties(properties: any): any { + const result = {}; + Object.keys(properties).forEach(key => { + result[key] = this.parseNeo4jValue(properties[key]); + }); + return result; + } + + /** + * Parse Neo4j values to handle special Neo4j types + */ + private parseNeo4jValue(value: any): any { + if (value === null || value === undefined) { + return value; + } + + // Handle Neo4j integers + if (neo4j.isInt(value)) { + return value.toNumber(); + } + + // Handle Neo4j dates + if (neo4j.isDate(value)) { + return new Date(value.toString()); + } + + // Handle Neo4j datetime + if (neo4j.isDateTime(value)) { + return new Date(value.toString()); + } + + // Handle Neo4j duration + if (neo4j.isDuration(value)) { + return { + months: value.months.toNumber(), + days: value.days.toNumber(), + seconds: value.seconds.toNumber(), + nanoseconds: value.nanoseconds.toNumber(), + }; + } + + // Handle Neo4j point + if (neo4j.isPoint(value)) { + return { + srid: value.srid.toNumber(), + x: value.x, + y: value.y, + z: value.z, + }; + } + + // Handle Neo4j path + if (neo4j.isPath(value)) { + return { + start: this.parseNeo4jValue(value.start), + end: this.parseNeo4jValue(value.end), + segments: value.segments.map(segment => ({ + start: this.parseNeo4jValue(segment.start), + relationship: this.parseNeo4jValue(segment.relationship), + end: this.parseNeo4jValue(segment.end), + })), + }; + } + + // Handle arrays + if (Array.isArray(value)) { + return value.map(item => this.parseNeo4jValue(item)); + } + + // Handle plain objects + if (typeof value === 'object') { + const result = {}; + Object.keys(value).forEach(key => { + result[key] = this.parseNeo4jValue(value[key]); + }); + return result; + } + + // Return primitive values as-is + return value; + } +} diff --git a/web-app/src/modules/core/router.tsx b/web-app/src/modules/core/router.tsx index 5be65954..13c5adeb 100644 --- a/web-app/src/modules/core/router.tsx +++ b/web-app/src/modules/core/router.tsx @@ -8,6 +8,7 @@ import AccountOverview from './routes/Account/Overview'; import AccountTransactions from './routes/Account/UserTransactions'; import AccountResources from './routes/Account/Resources'; import AccountModules from './routes/Account/Modules'; +import AccountConnections from './routes/Account/Connections'; import Block from './routes/Block'; import Root from './Root'; import Validators from './routes/Validators'; @@ -55,6 +56,10 @@ const router = createBrowserRouter([ }, ], }, + { + path: 'connections', + element: , + }, ], }, { diff --git a/web-app/src/modules/core/routes/Account/Account.tsx b/web-app/src/modules/core/routes/Account/Account.tsx index a9faa830..df9924ec 100644 --- a/web-app/src/modules/core/routes/Account/Account.tsx +++ b/web-app/src/modules/core/routes/Account/Account.tsx @@ -59,6 +59,7 @@ const Account: FC = ({ accountAddress }) => { { name: 'Transactions', to: `/accounts/${accountAddress}/transactions` }, { name: 'Resources', to: `/accounts/${accountAddress}/resources` }, { name: 'Modules', to: `/accounts/${accountAddress}/modules` }, + { name: 'Connections', to: `/accounts/${accountAddress}/connections` }, // Add new Connections tab ]; const { data, error, loading } = useQuery(GET_ACCOUNT, { diff --git a/web-app/src/modules/core/routes/Account/AccountDoesntExist.tsx b/web-app/src/modules/core/routes/Account/AccountDoesntExist.tsx index b7a006f2..260d6fdf 100644 --- a/web-app/src/modules/core/routes/Account/AccountDoesntExist.tsx +++ b/web-app/src/modules/core/routes/Account/AccountDoesntExist.tsx @@ -20,7 +20,7 @@ const AccountDoesntExist: FC = ({ address }) => { The crypto account associated with the specified address ( {address}) could not be found.
- This may be because the account was recently created and our system is still updating. + This may be because the account was recently created and our system is still indexing it.

diff --git a/web-app/src/modules/core/routes/Account/Connections/Connections.tsx b/web-app/src/modules/core/routes/Account/Connections/Connections.tsx new file mode 100644 index 00000000..bf338e71 --- /dev/null +++ b/web-app/src/modules/core/routes/Account/Connections/Connections.tsx @@ -0,0 +1,310 @@ +import { FC, useEffect, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import clsx from 'clsx'; +import ReactECharts from 'echarts-for-react'; + +import neo4jService from '../../../../../services/neo4j.service'; + +interface Link { + source: string; + target: string; + lineStyle: { + width: number; + color: string; + curveness: number; + type?: 'solid' | 'dashed'; + }; + tooltip: { + formatter: () => string; + }; +} + +interface RelationColors { + CREATED: string; + AUTOPAY: string; + TRANSFERRED: string; + VOUCHED: string; + DEFAULT: string; + [key: string]: string; // Add index signature +} + +const Connections: FC = () => { + const { accountAddress } = useParams(); + const [options, setOptions] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const load = async () => { + if (!accountAddress) return; + + try { + setLoading(true); + + const sanitizedAddress = accountAddress.startsWith('0x') + ? accountAddress.substring(2) + : accountAddress; + + const connectionData = await neo4jService.getWalletConnections(sanitizedAddress); + + if (!connectionData || connectionData.nodes.length === 0) { + setLoading(false); + return; + } + + // Find the current account node + const currentAccountNode = connectionData.nodes.find(node => + node.address && node.address.toUpperCase() === sanitizedAddress.toUpperCase() + ); + + if (!currentAccountNode) { + setError("Current account not found in returned data"); + setLoading(false); + return; + } + + const relationColors: RelationColors = { + 'CREATED': '#6B7280', // Gray for CREATED + 'AUTOPAY': '#10B981', // Green for AUTOPAY + 'TRANSFERRED': '#EF4444', // Red for TRANSFERRED + 'VOUCHED': '#3B82F6', // Blue for VOUCHED + 'DEFAULT': '#6B7280', // Default gray for unknown types + }; + + // Calculate min/max balance for node size scaling + let minBalance = Number.MAX_VALUE; + let maxBalance = 0; + + connectionData.nodes.forEach(node => { + if (node.balance > maxBalance) maxBalance = node.balance; + if (node.balance < minBalance) minBalance = node.balance; + }); + + const balanceRange = maxBalance - minBalance > 0 ? maxBalance - minBalance : 1; + + // List of commswap addresses (colored green) + const commswapAddresses = [ + '7153A13691E832EC5C5E2F0503FB7D228FBB7C87DD0C285C29D3F1D9F320CD5C', + '8D57A33412C4625289E35F2843E1D36EA19FA6BDE7816B1E3607C694926F01AE', + 'C6E97E7EF03A9162BEF775C9A77848DF83AFAF68350F0AFECB237BED495FBED7' + ]; + + // Process nodes + const nodes = connectionData.nodes.map(node => { + // Skip nodes without an address + if (!node.address) { + return null; + } + + const isCurrentAccount = node.address.toUpperCase() === sanitizedAddress.toUpperCase(); + const isSpecialNode = commswapAddresses.includes(node.address.toUpperCase()); + + const normalizedSize = isCurrentAccount + ? 25 + : 10 + ((node.balance - minBalance) / balanceRange) * 15; + + return { + id: node.id, + name: node.address.slice(0, 8) + '...', + symbolSize: normalizedSize, + itemStyle: { + color: isSpecialNode ? '#10B981' : (isCurrentAccount ? '#5A68FF' : '#64748B'), + }, + tooltip: { + formatter: () => { + return `Address: ${node.address}
` + + `Balance: ${node.balance.toLocaleString()} Ƚ
` + + `Locked: ${node.locked.toLocaleString()} Ƚ
` + + `Total In: ${node.totalIn.toLocaleString()} Ƚ
` + + `Total Out: ${node.totalOut.toLocaleString()} Ƚ
` + + (isCurrentAccount ? '' : 'Click to view account'); + } + }, + value: node.address, + fixed: false + }; + }).filter(Boolean); // Remove null nodes + + // Process edges and handle multiple relationships + const links: Link[] = []; + const edgeCounts: Record = {}; + + // Calculate max transfer amount for scaling edge width + let maxTransferAmount = 0; + connectionData.relationships.forEach(rel => { + if (rel.type === 'TRANSFERRED' && typeof rel.amount === 'number' && rel.amount > maxTransferAmount) { + maxTransferAmount = rel.amount; + } + }); + + // Create edges with properly styled relationships + connectionData.relationships.forEach(rel => { + const key = `${rel.startNodeId}-${rel.endNodeId}`; + edgeCounts[key] = (edgeCounts[key] || 0) + 1; + + // Calculate line width for TRANSFERRED relationships + const lineWidth = rel.type === 'TRANSFERRED' && typeof rel.amount === 'number' && maxTransferAmount > 0 + ? 1 + (rel.amount / maxTransferAmount) * 4 + : 1; + + // Calculate curveness based on number of edges between these nodes + const curveness = edgeCounts[key] > 1 ? 0.2 + ((edgeCounts[key] - 1) * 0.1) : 0.2; + + // Set edge color based on relationship type + // Safely access the relationColors object + const color = rel.type in relationColors ? relationColors[rel.type] : relationColors.DEFAULT; + + links.push({ + source: rel.startNodeId, + target: rel.endNodeId, + lineStyle: { + width: lineWidth, + color: color, + curveness: curveness, + type: rel.type === 'CREATED' ? 'dashed' : 'solid', + }, + tooltip: { + formatter: () => { + let tooltip = `Type: ${rel.type}
`; + + if (rel.amount) { + tooltip += `Amount: ${rel.amount.toLocaleString()} Ƚ
`; + } + + if (rel.version) { + tooltip += `Version: ${rel.version.toLocaleString()}
`; + } + + if (rel.timestamp) { + const date = new Date(rel.timestamp * 1000); + tooltip += `Date: ${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + } + + return tooltip; + } + } + }); + }); + + // Create the chart options + setOptions({ + tooltip: { + trigger: 'item', + backgroundColor: 'rgba(50, 50, 50, 0.95)', + borderColor: 'transparent', + borderRadius: 12, + textStyle: { + color: '#ffffff', + fontSize: 12, + }, + padding: [8, 12], + confine: true, + }, + series: [{ + type: 'graph', + layout: 'force', + data: nodes, + links: links, + roam: true, + draggable: true, + label: { + show: false, + }, + force: { + repulsion: 200, + edgeLength: [80, 150], + gravity: 0.1, + layoutAnimation: true, + friction: 0.6 + }, + edgeSymbol: ['none', 'arrow'], + edgeSymbolSize: [0, 6], + emphasis: { + focus: 'adjacency', + } + }] + }); + } catch (error) { + console.error('Failed to load connection data:', error); + setError(error instanceof Error ? error.message : 'Failed to load connection data'); + } finally { + setLoading(false); + } + }; + + load(); + }, [accountAddress]); + + // Handle clicks on nodes (for navigation to that account) + const onChartEvents = { + 'click': (eventParams: any) => { + if (eventParams.dataType === 'node' && eventParams.data && eventParams.data.value) { + const currentAddress = accountAddress?.startsWith('0x') + ? accountAddress.substring(2).toUpperCase() + : accountAddress?.toUpperCase(); + + // Don't navigate when clicking on current account node + if (eventParams.data.value.toUpperCase() !== currentAddress) { + const baseUrl = `${window.location.protocol}//${window.location.hostname}${window.location.port ? ':' + window.location.port : ''}`; + const accountUrl = `${baseUrl}/accounts/${eventParams.data.value}`; + window.open(accountUrl, '_blank'); + } + } + }, + 'dragstart': (eventParams: any) => { + if (eventParams.data) { + eventParams.data.fixed = true; + } + } + }; + + // Display states + if (loading) { + return ( +
+
+
Loading connection network...
+
+
+ ); + } + + if (error) { + return ( +
+
+
Error: {error}
+
+
+ ); + } + + if (!options) { + return ( +
+
+
No connection data available
+
+
+ ); + } + + return ( +
+ +
+ ); +}; + +export default Connections; diff --git a/web-app/src/modules/core/routes/Account/Connections/index.ts b/web-app/src/modules/core/routes/Account/Connections/index.ts new file mode 100644 index 00000000..96d29924 --- /dev/null +++ b/web-app/src/modules/core/routes/Account/Connections/index.ts @@ -0,0 +1 @@ +export { default } from './Connections'; diff --git a/web-app/src/modules/core/routes/Account/Overview/Overview.tsx b/web-app/src/modules/core/routes/Account/Overview/Overview.tsx index bb86b28b..250a483c 100644 --- a/web-app/src/modules/core/routes/Account/Overview/Overview.tsx +++ b/web-app/src/modules/core/routes/Account/Overview/Overview.tsx @@ -19,7 +19,7 @@ const Overview: FC = () => { setAccount(account); }; load(); - }, []); + }, [accountAddress]); return (
diff --git a/web-app/src/modules/core/routes/Transaction/BlockMetadataTransactionDetails.tsx b/web-app/src/modules/core/routes/Transaction/BlockMetadataTransactionDetails.tsx index 699712a7..12317d5f 100644 --- a/web-app/src/modules/core/routes/Transaction/BlockMetadataTransactionDetails.tsx +++ b/web-app/src/modules/core/routes/Transaction/BlockMetadataTransactionDetails.tsx @@ -14,6 +14,7 @@ const BlockMetadataTransactionDetails: FC = ({ transaction }) => { return ( + diff --git a/web-app/src/modules/core/routes/Transaction/StateCheckpointTransactionDetails.tsx b/web-app/src/modules/core/routes/Transaction/StateCheckpointTransactionDetails.tsx index 28776af5..37c87226 100644 --- a/web-app/src/modules/core/routes/Transaction/StateCheckpointTransactionDetails.tsx +++ b/web-app/src/modules/core/routes/Transaction/StateCheckpointTransactionDetails.tsx @@ -13,6 +13,7 @@ const StateCheckpointTransactionDetails: FC = ({ transaction }) => { return ( + diff --git a/web-app/src/modules/core/routes/Transaction/Transaction.tsx b/web-app/src/modules/core/routes/Transaction/Transaction.tsx index f5092cdb..6ac6a4ef 100644 --- a/web-app/src/modules/core/routes/Transaction/Transaction.tsx +++ b/web-app/src/modules/core/routes/Transaction/Transaction.tsx @@ -1,5 +1,5 @@ import { FC, useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate } from 'react-router-dom'; import Page from '../../../ui/Page/Page'; import useAptos from '../../../aptos'; import { Types } from 'aptos'; @@ -10,20 +10,78 @@ import StateCheckpointTransactionDetails from './StateCheckpointTransactionDetai const Transaction: FC = () => { const { version } = useParams(); const [transaction, setTransaction] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); const aptos = useAptos(); + const navigate = useNavigate(); useEffect(() => { const load = async () => { - const transaction = await aptos.getTransactionByVersion(parseInt(version!, 10)); - setTransaction(transaction); + if (!version) return; + + try { + setLoading(true); + setError(null); + + let txn: Types.Transaction; + + // Check if it's a hash (0x + 64 hex chars) + if (version.startsWith('0x') && version.length === 66 && /^0x[0-9a-fA-F]{64}$/.test(version)) { + try { + txn = await aptos.getTransactionByHash(version); + setTransaction(txn); + } catch (txError) { + console.error('Error loading transaction by hash:', txError); + + // If transaction not found, check if it's a valid account address (v6 onward format) + try { + await aptos.getAccount(version); + // If account exists, redirect to account page + navigate(`/accounts/${encodeURIComponent(version)}/resources`); + return; + } catch (acctError) { + // Neither a valid transaction nor account + throw txError; // Re-throw the original error + } + } + } + // Otherwise treat as version number + else if (/^\d+$/.test(version)) { + try { + txn = await aptos.getTransactionByVersion(parseInt(version, 10)); + setTransaction(txn); + } catch (txError) { + console.error('Error loading transaction by version:', txError); + throw txError; + } + } + // Invalid format + else { + throw new Error('Invalid transaction identifier format'); + } + } catch (err) { + console.error('Error loading transaction:', err); + setError(`Failed to load transaction: ${err instanceof Error ? err.message : 'Unknown error'}`); + } finally { + setLoading(false); + } }; + load(); - }, [version]); + }, [version, navigate]); return ( - {transaction && ( + {loading &&
Loading transaction...
} + + {error && ( +
+ {error} +
+ )} + + {transaction && !loading && !error && (
{transaction.type === 'block_metadata_transaction' && ( = ({ transaction }) => { return ( + diff --git a/web-app/src/modules/ui/Layout/Header/Header.tsx b/web-app/src/modules/ui/Layout/Header/Header.tsx index eeee45bb..7320de9b 100644 --- a/web-app/src/modules/ui/Layout/Header/Header.tsx +++ b/web-app/src/modules/ui/Layout/Header/Header.tsx @@ -34,15 +34,39 @@ const Header: React.FC = () => { const input = searchAddress.trim(); + // Don't process empty searches + if (!input) return; + try { + // Check if it's a transaction hash (0x + 64 hex chars) + if (input.startsWith('0x') && input.length === 66 && /^0x[0-9a-fA-F]{64}$/.test(input)) { + navigate(`/transactions/${encodeURIComponent(input)}`); + setSearchAddress(''); + if (searchInput.current) { + searchInput.current.blur(); + } + return; + } + + // Check if it's a transaction version (numeric only) + if (/^\d+$/.test(input)) { + navigate(`/transactions/${encodeURIComponent(input)}`); + setSearchAddress(''); + if (searchInput.current) { + searchInput.current.blur(); + } + return; + } + + // Default case: treat as an account address const addr = normalizeAddress(input); - navigate(`/accounts/${encodeURIComponent(addr)}/resources`); + navigate(`/accounts/${encodeURIComponent(addr)}`); setSearchAddress(''); if (searchInput.current) { searchInput.current.blur(); } } catch (error) { - console.warn(error); + console.warn('Search error:', error); } }; @@ -75,7 +99,30 @@ const Header: React.FC = () => { ))}
-
+
+
+
+
+
+ setSearchAddress(event.target.value)} + /> +
+
+ {localStorage.getItem('postero_enabled') === 'true' && (
@@ -109,29 +156,6 @@ const Header: React.FC = () => {
)} - -
-
-
-
- setSearchAddress(event.target.value)} - /> -
-
@@ -179,11 +203,11 @@ const Header: React.FC = () => { id="search" className={clsx( 'ring-1', - 'block w-full rounded-md border-0 bg-white py-1 pl-10 pr-3', + 'block w-full rounded-md border-0 bg-white py-1.5 pl-10 pr-3', 'text-gray-900 text-sm', 'focus:ring-2 ring-white ring-offset-2 ring-offset-primary-600', )} - placeholder="Search Address" + placeholder="Search Address / Hash / Version" type="search" name="search" ref={searchInput} diff --git a/web-app/src/services/neo4j.service.ts b/web-app/src/services/neo4j.service.ts new file mode 100644 index 00000000..546b7ef2 --- /dev/null +++ b/web-app/src/services/neo4j.service.ts @@ -0,0 +1,52 @@ +import { config } from '../config'; + +export interface Neo4jNode { + id: string; + labels: string[]; + address?: string; + [key: string]: any; +} + +export interface Neo4jRelationship { + id: string; + type: string; + startNodeId: string; + endNodeId: string; + [key: string]: any; +} + +export interface Neo4jGraphData { + nodes: Neo4jNode[]; + relationships: Neo4jRelationship[]; +} + +class Neo4jService { + private readonly apiUrl: string; + + constructor() { + this.apiUrl = `${config.apiHost}/neo4j`; + } + + /** + * Get all first-degree connections for a wallet + * @param address The wallet address to query + */ + async getWalletConnections(address: string): Promise { + try { + const response = await fetch(`${this.apiUrl}/wallet/${encodeURIComponent(address)}/connections`); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Failed to get wallet connections: ${response.status} - ${errorText}`); + } + + return await response.json(); + } catch (error) { + console.error('Error fetching wallet connections:', error); + throw error; + } + } +} + +// Export a singleton instance +export default new Neo4jService();