diff --git a/bun.lock b/bun.lock index ea22198..3f35e16 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "Harmonia DAO Management", "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.7.3", + "@epic-web/invariant": "^1.0.0", "@next/swc-wasm-nodejs": "^15.4.4", "@next/third-parties": "^15.3.1", "@react-three/drei": "^10.0.7", @@ -96,12 +97,16 @@ "version": "1.0.0", "dependencies": { "@stellar/stellar-sdk": "^14.0.0-rc.3", + "@types/jsonwebtoken": "^9.0.10", "@types/supertest": "^6.0.3", "cors": "^2.8.5", "dotenv": "^17.2.1", "express": "^5.1.0", + "express-rate-limit": "^8.1.0", + "jsonwebtoken": "^9.0.2", "sqlite3": "^5.1.7", "supertest": "^7.1.4", + "winston": "3.18.3", "zod": "^4.1.1", }, "devDependencies": { @@ -244,6 +249,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], @@ -294,6 +301,8 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.8", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], @@ -796,6 +805,8 @@ "@sinonjs/fake-timers": ["@sinonjs/fake-timers@13.0.5", "", { "dependencies": { "@sinonjs/commons": "^3.0.1" } }, "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw=="], + "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], + "@solana-program/compute-budget": ["@solana-program/compute-budget@0.6.1", "", { "peerDependencies": { "@solana/web3.js": "^2.0.0" } }, "sha512-PWcVmRx2gSQ8jd5va5HzSlKqQmR8Q1sYaPcqpCzhOHcApJ4YsVWY6QhaOD5Nx7z1UXkP12vNq3KDsSCZnT3Hkw=="], "@solana-program/stake": ["@solana-program/stake@0.1.0", "", { "peerDependencies": { "@solana/web3.js": "^2.0.0" } }, "sha512-8U3ax8RFvE7NegZmxn2SKE0927iG6Z9eXwBGgZaocEnZ/V3x7q/r0or1DZOV86RVyl6MQ9cuW8ExrRdorVNAVg=="], @@ -1074,12 +1085,16 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + "@types/jsonwebtoken": ["@types/jsonwebtoken@9.0.10", "", { "dependencies": { "@types/ms": "*", "@types/node": "*" } }, "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA=="], + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], "@types/methods": ["@types/methods@1.1.4", "", {}, "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ=="], "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/node": ["@types/node@24.2.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ=="], "@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], @@ -1110,6 +1125,8 @@ "@types/three": ["@types/three@0.179.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": "*", "@webgpu/types": "*", "fflate": "~0.8.2", "meshoptimizer": "~0.22.0" } }, "sha512-VgbFG2Pgsm84BqdegZzr7w2aKbQxmgzIu4Dy7/75ygiD/0P68LKmp5ie08KMPNqGTQwIge8s6D1guZf1RnZE0A=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], @@ -1414,6 +1431,8 @@ "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bufferutil": ["bufferutil@4.0.9", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw=="], @@ -1690,6 +1709,8 @@ "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], @@ -1702,6 +1723,8 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "encode-utf8": ["encode-utf8@1.0.3", "", {}, "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], @@ -1816,6 +1839,8 @@ "express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="], + "express-rate-limit": ["express-rate-limit@8.1.0", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA=="], + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], @@ -1850,6 +1875,8 @@ "feaxios": ["feaxios@0.0.23", "", { "dependencies": { "is-retry-allowed": "^3.0.0" } }, "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], "fflate": ["fflate@0.6.10", "", {}, "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg=="], @@ -1874,6 +1901,8 @@ "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], @@ -2042,7 +2071,7 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], @@ -2256,12 +2285,20 @@ "jsonschema": ["jsonschema@1.2.2", "", {}, "sha512-iX5OFQ6yx9NgbHCwse51ohhKgLuLL7Z5cNOeZOPIlDUtAMrxlruHLzVZxbltdHE5mEDXN+75oFOwq6Gn0MZwsA=="], + "jsonwebtoken": ["jsonwebtoken@9.0.2", "", { "dependencies": { "jws": "^3.2.2", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ=="], + "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], + "jwa": ["jwa@1.4.2", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw=="], + + "jws": ["jws@3.2.2", "", { "dependencies": { "jwa": "^1.4.1", "safe-buffer": "^5.0.1" } }, "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "keyvaluestorage-interface": ["keyvaluestorage-interface@1.0.0", "", {}, "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g=="], + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + "language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ=="], "language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA=="], @@ -2288,10 +2325,20 @@ "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], "lodash.memoize": ["lodash.memoize@4.1.2", "", {}, "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag=="], @@ -2300,6 +2347,8 @@ "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], @@ -2310,6 +2359,8 @@ "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + "long": ["long@5.2.0", "", {}, "sha512-9RTUNjK60eJbx3uz+TEGF7fUr29ZDxR5QzXcyDpeSfeH28S9ycINflOgOlppit5U+4kNTe83KQnMEerw7GmE8w=="], "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], @@ -2498,6 +2549,8 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -2828,6 +2881,8 @@ "stable-hash": ["stable-hash@0.0.5", "", {}, "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA=="], + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], @@ -2932,6 +2987,8 @@ "text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], @@ -2988,6 +3045,8 @@ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "troika-three-text": ["troika-three-text@0.52.4", "", { "dependencies": { "bidi-js": "^1.0.2", "troika-three-utils": "^0.52.4", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, "peerDependencies": { "three": ">=0.125.0" } }, "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg=="], "troika-three-utils": ["troika-three-utils@0.52.4", "", { "peerDependencies": { "three": ">=0.125.0" } }, "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A=="], @@ -3160,6 +3219,10 @@ "wif": ["wif@5.0.0", "", { "dependencies": { "bs58check": "^4.0.0" } }, "sha512-iFzrC/9ne740qFbNjTZ2FciSRJlHIXoxqk/Y5EnE08QOXu1WjJyCCswwDTYbohAOEnlCtLaAAQBhyaLRFh2hMA=="], + "winston": ["winston@3.18.3", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], @@ -3304,6 +3367,8 @@ "@scure/bip32/@scure/base": ["@scure/base@1.1.9", "", {}, "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg=="], + "@so-ric/colorspace/color": ["color@5.0.2", "", { "dependencies": { "color-convert": "^3.0.1", "color-string": "^2.0.0" } }, "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA=="], + "@solana-program/compute-budget/@solana/web3.js": ["@solana/web3.js@2.0.0", "", { "dependencies": { "@solana/accounts": "2.0.0", "@solana/addresses": "2.0.0", "@solana/codecs": "2.0.0", "@solana/errors": "2.0.0", "@solana/functional": "2.0.0", "@solana/instructions": "2.0.0", "@solana/keys": "2.0.0", "@solana/programs": "2.0.0", "@solana/rpc": "2.0.0", "@solana/rpc-parsed-types": "2.0.0", "@solana/rpc-spec-types": "2.0.0", "@solana/rpc-subscriptions": "2.0.0", "@solana/rpc-types": "2.0.0", "@solana/signers": "2.0.0", "@solana/sysvars": "2.0.0", "@solana/transaction-confirmation": "2.0.0", "@solana/transaction-messages": "2.0.0", "@solana/transactions": "2.0.0" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-x+ZRB2/r5tVK/xw8QRbAfgPcX51G9f2ifEyAQ/J5npOO+6+MPeeCjtr5UxHNDAYs9Ypo0PN+YJATCO4vhzQJGg=="], "@solana-program/stake/@solana/web3.js": ["@solana/web3.js@2.0.0", "", { "dependencies": { "@solana/accounts": "2.0.0", "@solana/addresses": "2.0.0", "@solana/codecs": "2.0.0", "@solana/errors": "2.0.0", "@solana/functional": "2.0.0", "@solana/instructions": "2.0.0", "@solana/keys": "2.0.0", "@solana/programs": "2.0.0", "@solana/rpc": "2.0.0", "@solana/rpc-parsed-types": "2.0.0", "@solana/rpc-spec-types": "2.0.0", "@solana/rpc-subscriptions": "2.0.0", "@solana/rpc-types": "2.0.0", "@solana/signers": "2.0.0", "@solana/sysvars": "2.0.0", "@solana/transaction-confirmation": "2.0.0", "@solana/transaction-messages": "2.0.0", "@solana/transactions": "2.0.0" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-x+ZRB2/r5tVK/xw8QRbAfgPcX51G9f2ifEyAQ/J5npOO+6+MPeeCjtr5UxHNDAYs9Ypo0PN+YJATCO4vhzQJGg=="], @@ -3568,8 +3633,6 @@ "inquirer/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], - "ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], - "isomorphic-unfetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "istanbul-lib-report/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], @@ -3680,6 +3743,8 @@ "simple-swizzle/is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + "socks/ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="], + "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "sqlite3/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], @@ -3820,6 +3885,10 @@ "@oclif/core/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "@so-ric/colorspace/color/color-convert": ["color-convert@3.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg=="], + + "@so-ric/colorspace/color/color-string": ["color-string@2.1.2", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA=="], + "@solana-program/compute-budget/@solana/web3.js/@solana/errors": ["@solana/errors@2.0.0", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-IHlaPFSy4lvYco1oHJ3X8DbchWwAwJaL/4wZKnF1ugwZ0g0re8wbABrqNOe/jyZ84VU9Z14PYM8W9oDAebdJbw=="], "@solana-program/stake/@solana/web3.js/@solana/errors": ["@solana/errors@2.0.0", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-IHlaPFSy4lvYco1oHJ3X8DbchWwAwJaL/4wZKnF1ugwZ0g0re8wbABrqNOe/jyZ84VU9Z14PYM8W9oDAebdJbw=="], @@ -4002,6 +4071,8 @@ "ripple-lib/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "socks/ip-address/sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "sqlite3/tar/chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], "sqlite3/tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], @@ -4032,6 +4103,10 @@ "@near-js/providers/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@so-ric/colorspace/color/color-convert/color-name": ["color-name@2.0.2", "", {}, "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A=="], + + "@so-ric/colorspace/color/color-string/color-name": ["color-name@2.0.2", "", {}, "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A=="], + "@solana-program/compute-budget/@solana/web3.js/@solana/errors/chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], "@solana-program/stake/@solana/web3.js/@solana/errors/chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="], diff --git a/services/stellar-wallet/src/auth/jwt.ts b/services/stellar-wallet/src/auth/jwt.ts index 5492504..d7719bc 100644 --- a/services/stellar-wallet/src/auth/jwt.ts +++ b/services/stellar-wallet/src/auth/jwt.ts @@ -1,5 +1,5 @@ +import type { NextFunction, Request, Response } from 'express' import jwt from 'jsonwebtoken' -import { type Request, type Response, type NextFunction } from 'express' import envs from '../config/envs' export interface JwtPayload { @@ -15,7 +15,7 @@ export interface JwtPayload { * @param role - The user role (defaults to 'user') * @returns JWT token string */ -export const generateToken = (user_id: string, role: string = 'user'): string => { +export const generateToken = (user_id: string, role = 'user'): string => { const payload: JwtPayload = { user_id, role, diff --git a/services/stellar-wallet/src/db/kyc.ts b/services/stellar-wallet/src/db/kyc.ts index 3965991..b70485b 100644 --- a/services/stellar-wallet/src/db/kyc.ts +++ b/services/stellar-wallet/src/db/kyc.ts @@ -28,6 +28,13 @@ export type AccountRow = { private_key: string } +export type TransactionRow = { + id: number + user_id: number + transaction_hash: string + status: string +} + /** * Returns a single shared SQLite database instance (singleton). * Creates the file/directory if missing and applies PRAGMAs once. @@ -170,3 +177,37 @@ export async function findAccountByUserId( ]) return rows.length ? rows[0] : null } + +/** + * Creates the `transactions` table if it doesn't exist (idempotent). + * FK: transactions.user_id → kyc(id) ON DELETE CASCADE + */ +export async function initializeTransactionsTable(db?: sqlite3.Database): Promise { + const conn = db ?? (await connectDB()) + const sql = ` + CREATE TABLE IF NOT EXISTS transactions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + transaction_hash TEXT NOT NULL, + status TEXT NOT NULL, + FOREIGN KEY (user_id) REFERENCES kyc(id) ON DELETE CASCADE + ); + ` + await run(conn, sql) + await run(conn, 'CREATE INDEX IF NOT EXISTS idx_transactions_user_id ON transactions (user_id);') + await run( + conn, + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_transactions_hash ON transactions (transaction_hash);', + ) +} + +/** + * Inserts a new transaction record. + */ +export async function insertTransaction( + db: sqlite3.Database, + args: { user_id: number; transaction_hash: string; status: string }, +): Promise { + const sql = 'INSERT INTO transactions (user_id, transaction_hash, status) VALUES (?, ?, ?);' + await run(db, sql, [args.user_id, args.transaction_hash, args.status]) +} diff --git a/services/stellar-wallet/src/index.ts b/services/stellar-wallet/src/index.ts index e5b276d..c1ef3c1 100644 --- a/services/stellar-wallet/src/index.ts +++ b/services/stellar-wallet/src/index.ts @@ -1,7 +1,7 @@ import cors from 'cors' import express, { type NextFunction, type Request, type Response } from 'express' import envs from './config/envs' -import { logger, loggerMiddleware, logError } from './middlewares/logger' +import { logError, logger, loggerMiddleware } from './middlewares/logger' import { authLimiter, kycLimiter, walletLimiter } from './middlewares/rate-limit' import { authLoginRouter } from './routes/auth-login' import { kycRouter } from './routes/kyc' diff --git a/services/stellar-wallet/src/middlewares/logger.ts b/services/stellar-wallet/src/middlewares/logger.ts index b882242..28c5ee0 100644 --- a/services/stellar-wallet/src/middlewares/logger.ts +++ b/services/stellar-wallet/src/middlewares/logger.ts @@ -1,6 +1,6 @@ -import type { NextFunction, Request, Response } from 'express' import fs from 'node:fs' import path from 'node:path' +import type { NextFunction, Request, Response } from 'express' import winston from 'winston' const logsDir = path.join(process.cwd(), 'services', 'stellar-wallet', 'logs') diff --git a/services/stellar-wallet/src/routes/auth-login.ts b/services/stellar-wallet/src/routes/auth-login.ts index b960597..25b53c1 100644 --- a/services/stellar-wallet/src/routes/auth-login.ts +++ b/services/stellar-wallet/src/routes/auth-login.ts @@ -1,9 +1,9 @@ -import { Router, type Request, type Response } from 'express' +import { type Request, type Response, Router } from 'express' import { generateToken } from '../auth/jwt' import { - verifyWebAuthnAuthentication, - getUserCredentials, type WebAuthnAuthenticationResponse, + getUserCredentials, + verifyWebAuthnAuthentication, } from '../auth/webauthn' export const authLoginRouter = Router() diff --git a/services/stellar-wallet/src/routes/kyc-verify.ts b/services/stellar-wallet/src/routes/kyc-verify.ts index 95069f0..833bd6b 100644 --- a/services/stellar-wallet/src/routes/kyc-verify.ts +++ b/services/stellar-wallet/src/routes/kyc-verify.ts @@ -1,11 +1,11 @@ import { createHash } from 'node:crypto' -import { Router, type Request, type Response } from 'express' import * as StellarSdk from '@stellar/stellar-sdk' +import { type Request, type Response, Router } from 'express' +import envs from '../config/envs' import { connectDB, findKycById, run } from '../db/kyc' import { validateKycData } from '../kyc/validate' +import { logError, logger } from '../middlewares/logger' import { connectSoroban } from '../soroban/client' -import envs from '../config/envs' -import { logger, logError } from '../middlewares/logger' export const kycVerifyRouter = Router() @@ -41,7 +41,7 @@ kycVerifyRouter.post('/verify', async (req: Request, res: Response) => { // Connect to database and verify kyc_id exists const db = await connectDB() - const kycRecord = await findKycById(db, parseInt(kyc_id)) + const kycRecord = await findKycById(db, Number.parseInt(kyc_id)) if (!kycRecord) { return res.status(400).json({ error: 'Invalid kyc_id' }) } @@ -95,7 +95,7 @@ kycVerifyRouter.post('/verify', async (req: Request, res: Response) => { } // Update database status to approved - await run(db, 'UPDATE kyc SET status = ? WHERE id = ?', ['approved', parseInt(kyc_id)]) + await run(db, 'UPDATE kyc SET status = ? WHERE id = ?', ['approved', Number.parseInt(kyc_id)]) // Return success response const verifyResponse: VerifyKycResponse = { diff --git a/services/stellar-wallet/src/routes/kyc.ts b/services/stellar-wallet/src/routes/kyc.ts index a2f8f40..58ba62d 100644 --- a/services/stellar-wallet/src/routes/kyc.ts +++ b/services/stellar-wallet/src/routes/kyc.ts @@ -1,7 +1,7 @@ import { type Request, type Response, Router } from 'express' import { type KycRow, all, connectDB, initializeKycTable, run } from '../db/kyc' import { validateKycData } from '../kyc/validate' -import { logger, logError } from '../middlewares/logger' +import { logError, logger } from '../middlewares/logger' export const kycRouter = Router() diff --git a/services/stellar-wallet/src/routes/wallet.ts b/services/stellar-wallet/src/routes/wallet.ts index f7e3154..76c0e11 100644 --- a/services/stellar-wallet/src/routes/wallet.ts +++ b/services/stellar-wallet/src/routes/wallet.ts @@ -1,17 +1,43 @@ +import { Asset, Memo, Networks, Operation, StrKey, TransactionBuilder } from '@stellar/stellar-sdk' import { type Request, type Response, Router } from 'express' import { z } from 'zod' -import { connectDB, findKycById, initializeAccountsTable, insertAccount } from '../db/kyc' +import { jwtMiddleware } from '../auth/jwt' +import { + connectDB, + findAccountByUserId, + findKycById, + initializeAccountsTable, + initializeTransactionsTable, + insertAccount, + insertTransaction, +} from '../db/kyc' +import { logError, logger } from '../middlewares/logger' +import { connect } from '../stellar/client' import { fundAccount } from '../stellar/fund' import { generateKeyPair } from '../stellar/keys' +import { signTransaction } from '../stellar/sign' import { encryptPrivateKey, getEncryptionKey } from '../utils/encryption' -import { logger, logError } from '../middlewares/logger' export const walletRouter = Router() +// Narrow type to access JWT payload without global augmentation +type AuthRequest = Request & { user?: { user_id: string } } + const CreateWalletBody = z.object({ user_id: z.number().int().positive(), }) +const SendTransactionBody = z.object({ + user_id: z.number().int().positive(), + destination: z.string(), + amount: z.string(), // validated below with regex and range + asset: z.string().optional().default('native'), + memo: z.string().optional(), +}) + +// up to 7 decimals, positive +const AMOUNT_REGEX = /^(?:0|[1-9]\d*)(?:\.\d{1,7})?$/ + /** * POST /wallet/create * Body: { user_id: number } @@ -66,3 +92,139 @@ walletRouter.post('/create', async (req: Request, res: Response) => { return res.status(500).json({ error: 'Failed to create account' }) } }) + +/** + * POST /wallet/send + * Body: { user_id: number, destination: string, amount: string, asset?: string, memo?: string } + * Protection: jwtMiddleware + * Flow: validate -> build payment -> sign -> submit -> persist -> respond + */ +walletRouter.post('/send', jwtMiddleware, async (req: Request, res: Response) => { + // Validate body + const parsed = SendTransactionBody.safeParse(req.body) + if (!parsed.success) { + return res.status(400).json({ error: 'Invalid request body' }) + } + const { user_id, destination, amount, asset, memo } = parsed.data + + const authReq = req as AuthRequest + + // Verify user_id matches JWT + if (Number.parseInt(authReq.user?.user_id || '0') !== user_id) { + return res.status(400).json({ error: 'user_id does not match token' }) + } + + // Validate destination + if (!StrKey.isValidEd25519PublicKey(destination)) { + return res.status(400).json({ error: 'invalid destination' }) + } + + // Validate amount format & range + if (!AMOUNT_REGEX.test(amount)) { + return res + .status(400) + .json({ error: 'amount must be a positive decimal with up to 7 decimals' }) + } + const amountNum = Number(amount) + if (amountNum <= 0 || amountNum > 1000) { + return res.status(400).json({ error: 'amount must be > 0 and ≤ 1000' }) + } + + // Validate asset + if (asset !== 'native') { + return res.status(400).json({ error: 'only native asset supported' }) + } + + // Validate memo length in BYTES (≤ 28) + if (memo && Buffer.byteLength(memo, 'utf8') > 28) { + return res.status(400).json({ error: 'memo must be ≤ 28 bytes' }) + } + + try { + const db = await connectDB() + await initializeTransactionsTable(db) + + // Find user account + const account = await findAccountByUserId(db, user_id) + if (!account) { + return res.status(400).json({ error: 'user account not found' }) + } + + // Connect to Stellar + const server = connect() + + // Load account to get sequence number + const sourceAccount = await server.loadAccount(account.public_key) + + // Base fee from Horizon + const baseFee = String(await server.fetchBaseFee()) + + // Build transaction + const txBuilder = new TransactionBuilder(sourceAccount, { + fee: baseFee, + networkPassphrase: Networks.TESTNET, + }) + + // Add payment operation + txBuilder.addOperation( + Operation.payment({ + destination, + asset: Asset.native(), + amount, + }), + ) + + // Add memo if provided + if (memo) { + txBuilder.addMemo(Memo.text(memo)) + } + + // Set timeout + txBuilder.setTimeout(30) + + // Build transaction + const transaction = txBuilder.build() + + // Sign transaction + const signedTx = await signTransaction(user_id, transaction, db) + + // Submit transaction + const result = await server.submitTransaction(signedTx) + + // Persist success + await insertTransaction(db, { + user_id, + transaction_hash: result.hash, + status: 'success', + }) + + logger.info({ message: 'transaction_sent', user_id, hash: result.hash }) + return res.status(201).json({ + user_id, + transaction_hash: result.hash, + status: 'success', + }) + } catch (err: unknown) { + // Try to persist failure if we can get the hash + try { + const db = await connectDB() + await initializeTransactionsTable(db) + + let hash = 'unknown' + if (err && typeof err === 'object' && 'hash' in err && typeof err.hash === 'string') { + hash = err.hash + } + + await insertTransaction(db, { + user_id, + transaction_hash: hash, + status: 'failed', + }) + } catch { + // Ignore persistence errors + } + + logError(err, { route: '/wallet/send', user_id }) + return res.status(500).json({ error: 'Transaction failed' }) + } +}) diff --git a/services/stellar-wallet/src/stellar/sign.ts b/services/stellar-wallet/src/stellar/sign.ts index 7c88393..e0ea54c 100644 --- a/services/stellar-wallet/src/stellar/sign.ts +++ b/services/stellar-wallet/src/stellar/sign.ts @@ -2,8 +2,8 @@ import { type FeeBumpTransaction, Keypair, StrKey, type Transaction } from '@ste import type sqlite3 from 'sqlite3' import { connectDB } from '../db/kyc' import { findAccountByUserId } from '../db/kyc' +import { logError, logger } from '../middlewares/logger' import { decryptPrivateKey, getEncryptionKey } from '../utils/encryption' -import { logger, logError } from '../middlewares/logger' /** Union type for Stellar base transaction types we can sign. */ export type StellarTx = Transaction | FeeBumpTransaction diff --git a/services/stellar-wallet/tests/auth/jwt.test.ts b/services/stellar-wallet/tests/auth/jwt.test.ts index 1d15d5f..5a3c164 100644 --- a/services/stellar-wallet/tests/auth/jwt.test.ts +++ b/services/stellar-wallet/tests/auth/jwt.test.ts @@ -1,8 +1,8 @@ // Set environment variable for testing process.env.JWT_SECRET = 'test-jwt-secret-key-for-testing-purposes-only-32-chars-long' -import { generateToken, verifyToken, jwtMiddleware } from '../../src/auth/jwt' -import { type Request, type Response, type NextFunction } from 'express' +import type { NextFunction, Request, Response } from 'express' +import { generateToken, jwtMiddleware, verifyToken } from '../../src/auth/jwt' describe('JWT Authentication', () => { describe('generateToken', () => { diff --git a/services/stellar-wallet/tests/db/kyc.test.ts b/services/stellar-wallet/tests/db/kyc.test.ts index 1d18565..d61cf11 100644 --- a/services/stellar-wallet/tests/db/kyc.test.ts +++ b/services/stellar-wallet/tests/db/kyc.test.ts @@ -3,13 +3,16 @@ import path from 'node:path' import type sqlite3 from 'sqlite3' import { type KycRow, + type TransactionRow, all, closeDB, connectDB, findKycById, initializeAccountsTable, initializeKycTable, + initializeTransactionsTable, insertAccount, + insertTransaction, run, } from '../../src/db/kyc' @@ -223,4 +226,159 @@ describe('KYC SQLite module', () => { ]) expect(accounts.length).toBe(0) }) + + test('initializeTransactionsTable creates transactions schema with FK/indexes and is idempotent', async () => { + await expect(initializeTransactionsTable(db)).resolves.toBeUndefined() + // call again to verify idempotency + await expect(initializeTransactionsTable(db)).resolves.toBeUndefined() + + type PragmaCol = { + cid: number + name: string + type: string + notnull: number + dflt_value: string | null + pk: number + } + const cols = await all(db, "PRAGMA table_info('transactions');") + const byName = Object.fromEntries(cols.map((c) => [c.name, c])) + + // id + expect(byName.id).toBeDefined() + expect(byName.id.type.toUpperCase()).toBe('INTEGER') + expect(byName.id.pk).toBe(1) + + // user_id + expect(byName.user_id).toBeDefined() + expect(byName.user_id.type.toUpperCase()).toBe('INTEGER') + expect(byName.user_id.notnull).toBe(1) + + // transaction_hash + expect(byName.transaction_hash).toBeDefined() + expect(byName.transaction_hash.type.toUpperCase()).toBe('TEXT') + expect(byName.transaction_hash.notnull).toBe(1) + + // status + expect(byName.status).toBeDefined() + expect(byName.status.type.toUpperCase()).toBe('TEXT') + expect(byName.status.notnull).toBe(1) + + // verify FK user_id -> kyc(id) with ON DELETE CASCADE + type FK = { + id: number + seq: number + table: string + from: string + to: string + on_update: string + on_delete: string + match: string + } + const fks = await all(db, "PRAGMA foreign_key_list('transactions');") + const fkUser = fks.find((f) => f.from === 'user_id') + expect(fkUser).toBeDefined() + expect(fkUser?.table.toLowerCase()).toBe('kyc') + expect(fkUser?.to.toLowerCase()).toBe('id') + expect((fkUser?.on_delete ?? '').toUpperCase()).toBe('CASCADE') + + // index metadata assertions + type Idx = { seq: number; name: string; unique: number; origin: string; partial: number } + const idx = await all(db, "PRAGMA index_list('transactions');") + const names = idx.map((i) => i.name) + expect(names).toEqual( + expect.arrayContaining(['idx_transactions_user_id', 'idx_transactions_hash']), + ) + const userIdx = idx.find((i) => i.name === 'idx_transactions_user_id') + expect(userIdx?.unique).toBe(0) + const hashIdx = idx.find((i) => i.name === 'idx_transactions_hash') + expect(hashIdx?.unique).toBe(1) + + type IdxInfo = { seqno: number; cid: number; name: string } + const userCols = await all(db, "PRAGMA index_info('idx_transactions_user_id');") + expect(userCols.map((c) => c.name)).toEqual(['user_id']) + const hashCols = await all(db, "PRAGMA index_info('idx_transactions_hash');") + expect(hashCols.map((c) => c.name)).toEqual(['transaction_hash']) + }) + + test('insertTransaction inserts, enforces UNIQUE(transaction_hash) and FK CASCADE', async () => { + // create a KYC row to associate transactions with + await run(db, 'INSERT INTO kyc (name, document, status) VALUES (?, ?, ?)', [ + 'Eve', + 'DOC-TX-UNIQ', + 'approved', + ]) + const kycRows = await all(db, 'SELECT * FROM kyc WHERE document = ?', ['DOC-TX-UNIQ']) + const userId = kycRows[0].id + + const txHash = 'TX_HASH_UNIQUE_TEST_12345678901234567890' + const status = 'success' + + // first insert should succeed + await expect( + insertTransaction(db, { user_id: userId, transaction_hash: txHash, status }), + ).resolves.toBeUndefined() + + // verify the transaction was inserted correctly + const transactions = await all( + db, + 'SELECT * FROM transactions WHERE transaction_hash = ?', + [txHash], + ) + expect(transactions.length).toBe(1) + expect(transactions[0].user_id).toBe(userId) + expect(transactions[0].transaction_hash).toBe(txHash) + expect(transactions[0].status).toBe(status) + + // inserting the same transaction_hash should violate the UNIQUE constraint + await expect( + insertTransaction(db, { user_id: userId, transaction_hash: txHash, status: 'failed' }), + ).rejects.toThrow() + + // deleting the KYC row should cascade and remove dependent transactions + await run(db, 'DELETE FROM kyc WHERE id = ?', [userId]) + const remainingTransactions = await all<{ id: number }>( + db, + 'SELECT id FROM transactions WHERE user_id = ?', + [userId], + ) + expect(remainingTransactions.length).toBe(0) + }) + + test('can INSERT multiple transactions for same user with different hashes', async () => { + // create a KYC row + await run(db, 'INSERT INTO kyc (name, document, status) VALUES (?, ?, ?)', [ + 'Frank', + 'DOC-TX-MULTI', + 'approved', + ]) + const kycRows = await all(db, 'SELECT * FROM kyc WHERE document = ?', ['DOC-TX-MULTI']) + const userId = kycRows[0].id + + // insert multiple transactions for the same user + const transactions = [ + { hash: 'TX_HASH_1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', status: 'success' }, + { hash: 'TX_HASH_2_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', status: 'failed' }, + { hash: 'TX_HASH_3_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', status: 'pending' }, + ] + + for (const tx of transactions) { + await expect( + insertTransaction(db, { user_id: userId, transaction_hash: tx.hash, status: tx.status }), + ).resolves.toBeUndefined() + } + + // verify all transactions were inserted + const allUserTransactions = await all( + db, + 'SELECT * FROM transactions WHERE user_id = ? ORDER BY transaction_hash', + [userId], + ) + expect(allUserTransactions.length).toBe(3) + expect(allUserTransactions.map((t) => t.status)).toEqual(['success', 'failed', 'pending']) + expect(allUserTransactions.map((t) => t.transaction_hash)).toEqual([ + 'TX_HASH_1_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + 'TX_HASH_2_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB', + 'TX_HASH_3_CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC', + ]) + }) }) diff --git a/services/stellar-wallet/tests/middlewares/logger.test.ts b/services/stellar-wallet/tests/middlewares/logger.test.ts index c647bd9..eec55d9 100644 --- a/services/stellar-wallet/tests/middlewares/logger.test.ts +++ b/services/stellar-wallet/tests/middlewares/logger.test.ts @@ -13,7 +13,7 @@ jest.mock('winston', () => { } }) -import { loggerMiddleware, logError } from '../../src/middlewares/logger' +import { logError, loggerMiddleware } from '../../src/middlewares/logger' describe('logger middleware', () => { let app: express.Application diff --git a/services/stellar-wallet/tests/routes/auth-login.test.ts b/services/stellar-wallet/tests/routes/auth-login.test.ts index e94d259..f0a7394 100644 --- a/services/stellar-wallet/tests/routes/auth-login.test.ts +++ b/services/stellar-wallet/tests/routes/auth-login.test.ts @@ -1,3 +1,4 @@ +import type { NextFunction, Request, Response } from 'express' import request from 'supertest' import { app } from '../../src/index' @@ -10,10 +11,11 @@ jest.mock('../../src/auth/webauthn', () => ({ // Mock the JWT module jest.mock('../../src/auth/jwt', () => ({ generateToken: jest.fn(), + jwtMiddleware: jest.fn((req: Request, res: Response, next: NextFunction) => next()), })) -import { verifyWebAuthnAuthentication, getUserCredentials } from '../../src/auth/webauthn' import { generateToken } from '../../src/auth/jwt' +import { getUserCredentials, verifyWebAuthnAuthentication } from '../../src/auth/webauthn' const mockVerifyWebAuthnAuthentication = verifyWebAuthnAuthentication as jest.MockedFunction< typeof verifyWebAuthnAuthentication diff --git a/services/stellar-wallet/tests/routes/kyc-verify.test.ts b/services/stellar-wallet/tests/routes/kyc-verify.test.ts index 732a323..6f4beb6 100644 --- a/services/stellar-wallet/tests/routes/kyc-verify.test.ts +++ b/services/stellar-wallet/tests/routes/kyc-verify.test.ts @@ -1,6 +1,6 @@ import type sqlite3 from 'sqlite3' import request from 'supertest' -import { closeDB, connectDB, initializeKycTable, run, all } from '../../src/db/kyc' +import { all, closeDB, connectDB, initializeKycTable, run } from '../../src/db/kyc' // Create a clean app instance for testing without rate limiting import express from 'express' diff --git a/services/stellar-wallet/tests/routes/wallet.test.ts b/services/stellar-wallet/tests/routes/wallet.test.ts index 2ae467a..03c1a88 100644 --- a/services/stellar-wallet/tests/routes/wallet.test.ts +++ b/services/stellar-wallet/tests/routes/wallet.test.ts @@ -1,11 +1,9 @@ import request from 'supertest' +import type { KycRow } from '../../src/db/kyc' // Deterministic 32-byte key (Base64) process.env.ENCRYPTION_KEY = Buffer.alloc(32, 7).toString('base64') -import type { KycRow } from '../../src/db/kyc' -import { app } from '../../src/index' - // Types for mocks type InsertAccountArgs = { user_id: number @@ -13,6 +11,12 @@ type InsertAccountArgs = { private_key_encrypted: string } +type InsertTransactionArgs = { + user_id: number + transaction_hash: string + status: string +} + // Helper to take the second argument typed from a mock function secondArg(m: jest.Mock): T2 { const call = m.mock.calls[0] @@ -23,12 +27,112 @@ function secondArg(m: jest.Mock): T2 { return call[1] } -// ---- Mocks ---- - -// Mock keypair generation +// Constants const VALID_PUBLIC_KEY = 'GCCGMBN46TNVH2WL732DYB5WWBEJG5S4UDXAJGB7O3GPQJVVHVQOP5E7' const MOCK_SECRET = 'SA2XMOCKSECRETPRIVATEKEYFORTESTS01234567' +// Test control variables +let JWT_BEHAVIOR: 'success' | 'fail' = 'success' +let AUTH_USER_ID = '1' + +// Mock rate limiting middleware +jest.mock('../../src/middlewares/rate-limit', () => ({ + walletLimiter: jest.fn((req: Request, res: Response, next: NextFunction) => next()), + authLimiter: jest.fn((req: Request, res: Response, next: NextFunction) => next()), + kycLimiter: jest.fn((req: Request, res: Response, next: NextFunction) => next()), +})) + +// Mock auth JWT +import type { NextFunction, Request, Response } from 'express' + +interface JwtPayload { + user_id: string + role: string + iat?: number + exp?: number +} + +interface AuthenticatedRequest extends Request { + user?: JwtPayload +} + +jest.mock('../../src/auth/jwt', () => ({ + jwtMiddleware: jest.fn((req: Request, res: Response, next: NextFunction) => { + if (JWT_BEHAVIOR === 'fail') { + return res.status(401).json({ error: 'unauthorized' }) + } + const authReq = req as AuthenticatedRequest + authReq.user = { user_id: AUTH_USER_ID, role: 'user' } + next() + }), +})) + +// Mock stellar-sdk +const mockTransaction = { __signed: false } + +const StrKeyMock = { + isValidEd25519PublicKey: jest.fn().mockReturnValue(true), +} + +const TransactionBuilderMock = jest.fn().mockImplementation(() => ({ + addOperation: jest.fn().mockReturnThis(), + addMemo: jest.fn().mockReturnThis(), + setTimeout: jest.fn().mockReturnThis(), + build: jest.fn().mockReturnValue(mockTransaction), +})) + +const OperationMock = { + payment: jest.fn().mockReturnValue({ type: 'payment' }), +} + +const AssetMock = { + native: jest.fn().mockReturnValue({ type: 'native' }), +} + +const MemoMock = { + text: jest.fn().mockReturnValue({ type: 'text' }), +} + +const NetworksMock = { + TESTNET: 'Test SDF Network ; September 2015', +} + +jest.mock('@stellar/stellar-sdk', () => ({ + StrKey: StrKeyMock, + TransactionBuilder: TransactionBuilderMock, + Operation: OperationMock, + Asset: AssetMock, + Memo: MemoMock, + Networks: NetworksMock, +})) + +// Mock stellar client +const loadAccountMock = jest.fn().mockResolvedValue({ accountId: 'SOURCE', sequence: '1' }) +const fetchBaseFeeMock = jest.fn().mockResolvedValue(100) +const submitTransactionMock = jest.fn().mockResolvedValue({ hash: 'TXHASH_SUCCESS' }) + +const connectMock = jest.fn().mockReturnValue({ + loadAccount: loadAccountMock, + fetchBaseFee: fetchBaseFeeMock, + submitTransaction: submitTransactionMock, +}) + +jest.mock('../../src/stellar/client', () => ({ + connect: connectMock, +})) + +// Mock stellar signing +import type { Transaction } from '@stellar/stellar-sdk' + +const signTransactionMock = jest.fn().mockImplementation((userId: number, tx: Transaction) => { + return Promise.resolve(tx) +}) + +jest.mock('../../src/stellar/sign', () => ({ + signTransaction: signTransactionMock, +})) + +// Mock stellar keys jest.mock('../../src/stellar/keys', () => ({ generateKeyPair: jest.fn((): { publicKey: string; privateKey: string } => ({ publicKey: VALID_PUBLIC_KEY, @@ -42,7 +146,8 @@ jest.mock('../../src/stellar/fund', () => ({ fundAccount: (publicKey: string): Promise => fundMock(publicKey), })) -// Mock DB helpers used by the route +// Mock DB helpers +const connectDBMock = jest.fn().mockResolvedValue({}) const findKycByIdMock: jest.Mock, [unknown, number]> = jest.fn() const insertAccountMock: jest.Mock, [unknown, InsertAccountArgs]> = jest .fn, [unknown, InsertAccountArgs]>() @@ -50,18 +155,35 @@ const insertAccountMock: jest.Mock, [unknown, InsertAccountArgs]> const initializeAccountsTableMock: jest.Mock, [unknown?]> = jest .fn, [unknown?]>() .mockResolvedValue(undefined) +const initializeTransactionsTableMock = jest.fn().mockResolvedValue(undefined) +const findAccountByUserIdMock = jest.fn().mockResolvedValue({ + id: 10, + user_id: 1, + public_key: VALID_PUBLIC_KEY, + private_key: 'iv:tag:cipher', +}) +const insertTransactionMock: jest.Mock, [unknown, InsertTransactionArgs]> = jest + .fn, [unknown, InsertTransactionArgs]>() + .mockResolvedValue(undefined) jest.mock('../../src/db/kyc', () => { const actual = jest.requireActual('../../src/db/kyc') return { ...actual, + connectDB: connectDBMock, findKycById: (db: unknown, id: number): Promise => findKycByIdMock(db, id), insertAccount: (db: unknown, args: InsertAccountArgs): Promise => insertAccountMock(db, args), initializeAccountsTable: (db?: unknown): Promise => initializeAccountsTableMock(db), + initializeTransactionsTable: initializeTransactionsTableMock, + findAccountByUserId: findAccountByUserIdMock, + insertTransaction: (db: unknown, args: InsertTransactionArgs): Promise => + insertTransactionMock(db, args), } }) +import { app } from '../../src/index' + describe('POST /wallet/create', () => { beforeEach(() => { jest.clearAllMocks() @@ -137,3 +259,190 @@ describe('POST /wallet/create', () => { expect(res.body).toHaveProperty('error') }) }) + +describe('POST /wallet/send', () => { + beforeEach(() => { + jest.clearAllMocks() + JWT_BEHAVIOR = 'success' + AUTH_USER_ID = '1' + + // Reset default mocks + StrKeyMock.isValidEd25519PublicKey.mockReturnValue(true) + findAccountByUserIdMock.mockResolvedValue({ + id: 10, + user_id: 1, + public_key: VALID_PUBLIC_KEY, + private_key: 'iv:tag:cipher', + }) + loadAccountMock.mockResolvedValue({ accountId: 'SOURCE', sequence: '1' }) + fetchBaseFeeMock.mockResolvedValue(100) + submitTransactionMock.mockResolvedValue({ hash: 'TXHASH_SUCCESS' }) + }) + + it('should reject requests without JWT', async () => { + JWT_BEHAVIOR = 'fail' + + const res = await request(app).post('/wallet/send').send({ + user_id: 123, + destination: VALID_PUBLIC_KEY, + amount: '10', + }) + + expect(res.status).toBe(401) + expect(res.body).toEqual({ error: 'unauthorized' }) + }) + + it('returns 201 on successful transaction', async () => { + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '12.3456', + asset: 'native', + memo: 'hello', + }) + + expect(res.status).toBe(201) + expect(res.body).toEqual({ + user_id: 1, + transaction_hash: 'TXHASH_SUCCESS', + status: 'success', + }) + + // Verify stellar calls + expect(loadAccountMock).toHaveBeenCalledWith(VALID_PUBLIC_KEY) + expect(fetchBaseFeeMock).toHaveBeenCalled() + expect(submitTransactionMock).toHaveBeenCalled() + + // Verify transaction persistence + expect(insertTransactionMock).toHaveBeenCalledWith(expect.anything(), { + user_id: 1, + transaction_hash: 'TXHASH_SUCCESS', + status: 'success', + }) + }) + + it('returns 400 when user_id does not match token', async () => { + AUTH_USER_ID = '99' + + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '10', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'user_id does not match token' }) + }) + + it('returns 400 for invalid destination', async () => { + StrKeyMock.isValidEd25519PublicKey.mockReturnValue(false) + + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: 'invalid-destination', + amount: '10', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'invalid destination' }) + }) + + it('returns 400 for amount with >7 decimals', async () => { + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '1.23456789', // 8 decimals + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'amount must be a positive decimal with up to 7 decimals' }) + }) + + it('returns 400 for amount = 0', async () => { + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '0', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'amount must be > 0 and ≤ 1000' }) + }) + + it('returns 400 for amount > 1000', async () => { + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '1000.0000001', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'amount must be > 0 and ≤ 1000' }) + }) + + it('returns 400 for unsupported asset', async () => { + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '10', + asset: 'USDC', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'only native asset supported' }) + }) + + it('returns 400 for memo > 28 bytes', async () => { + const res = await request(app) + .post('/wallet/send') + .send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '10', + memo: 'x'.repeat(29), // 29 bytes + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'memo must be ≤ 28 bytes' }) + }) + + it('returns 400 when user account not found', async () => { + findAccountByUserIdMock.mockResolvedValue(null) + + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '10', + }) + + expect(res.status).toBe(400) + expect(res.body).toEqual({ error: 'user account not found' }) + }) + + it('returns 500 when Horizon rejects transaction with hash', async () => { + const horizonError = { + response: { + data: { + hash: 'FAILHASH_123', + }, + }, + } + submitTransactionMock.mockRejectedValue(horizonError) + + const res = await request(app).post('/wallet/send').send({ + user_id: 1, + destination: VALID_PUBLIC_KEY, + amount: '10', + }) + + expect(res.status).toBe(500) + expect(res.body).toEqual({ error: 'Transaction failed' }) + + // Verify failed transaction persistence + expect(insertTransactionMock).toHaveBeenCalledWith(expect.anything(), { + user_id: 1, + transaction_hash: 'unknown', // Since error doesn't have direct hash property + status: 'failed', + }) + }) +})