diff --git a/frontend/babel.config.js b/frontend/babel.config.js deleted file mode 100644 index c74fb53e..00000000 --- a/frontend/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: [['@babel/preset-env', { targets: { node: 'current' } }]], -}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 03e307b4..9d102477 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "boxmeout-frontend", "version": "0.1.0", "dependencies": { + "@stellar/stellar-sdk": "^15.1.0", "next": "14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2902,6 +2903,33 @@ "node": ">= 10" } }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3027,6 +3055,65 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@stellar/js-xdr": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@stellar/js-xdr/-/js-xdr-4.0.0.tgz", + "integrity": "sha512-+NmNa7Tk5BI5XFdy/6xGTqAN4J9a9KgCrCGhj2uEUTCBhLkch0M+QbKzNH8zEnejWe0p8w+0q5hUVX6L3OzoVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.0.0", + "pnpm": ">=9.0.0" + } + }, + "node_modules/@stellar/stellar-base": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-15.0.0.tgz", + "integrity": "sha512-XQhxUr9BYiEcFcgc4oWcCMR9QJCny/GmmGsuwPKf/ieIcOeb5149KLHYx9mJCA0ea8QbucR2/GzV58QbXOTxQA==", + "license": "Apache-2.0", + "dependencies": { + "@noble/curves": "^1.9.7", + "@stellar/js-xdr": "^4.0.0", + "base32.js": "^0.1.0", + "bignumber.js": "^9.3.1", + "buffer": "^6.0.3", + "sha.js": "^2.4.12" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-15.1.0.tgz", + "integrity": "sha512-GsJUcWx2yboVzYdhTe/LHS3V1wVLSHkUkglC5bBoYWGJt31vzIhbSGno60NP9CdCTNkLJdnrsLJ63oA58Zvh5A==", + "license": "Apache-2.0", + "dependencies": { + "@stellar/stellar-base": "^15.0.0", + "axios": "1.15.0", + "bignumber.js": "^9.3.1", + "commander": "^14.0.3", + "eventsource": "^2.0.2", + "feaxios": "^0.0.23", + "randombytes": "^2.1.0", + "toml": "^3.0.0", + "urijs": "^1.19.11" + }, + "bin": { + "stellar-js": "bin/stellar-js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stellar/stellar-sdk/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -4298,7 +4385,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/autoprefixer": { @@ -4342,7 +4428,6 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, "license": "MIT", "dependencies": { "possible-typed-array-names": "^1.0.0" @@ -4364,6 +4449,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -4559,6 +4655,35 @@ "dev": true, "license": "MIT" }, + "node_modules/base32.js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", + "integrity": "sha512-n3TkB02ixgBOhTvANakDb4xaMXnYUVkNoRFJjQflcqMQhyEKxEHdj3E6N8t8sUQ0mjH/3/JxzlXuz3ul/J90pQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "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==", + "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" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.21", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz", @@ -4572,6 +4697,15 @@ "node": ">=6.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4665,6 +4799,30 @@ "node-int64": "^0.4.0" } }, + "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/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -4687,7 +4845,6 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4706,7 +4863,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4720,7 +4876,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4984,7 +5139,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -5306,7 +5460,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", @@ -5342,7 +5495,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5444,7 +5596,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5585,7 +5736,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5595,7 +5745,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5654,7 +5803,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5667,7 +5815,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6418,6 +6565,15 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6573,6 +6729,15 @@ "bser": "2.1.1" } }, + "node_modules/feaxios": { + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.23.tgz", + "integrity": "sha512-eghR0A21fvbkcQBgZuMfQhrXxJzC0GNUGC9fXhBge33D+mFDTwl0aJ35zoQQn575BhyjQitRc5N4f+L4cP708g==", + "license": "MIT", + "dependencies": { + "is-retry-allowed": "^3.0.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -6638,11 +6803,30 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, "license": "MIT", "dependencies": { "is-callable": "^1.2.7" @@ -6675,7 +6859,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -6728,7 +6911,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6799,7 +6981,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6834,7 +7015,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -6983,7 +7163,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7064,7 +7243,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" @@ -7093,7 +7271,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7106,7 +7283,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7122,7 +7298,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7214,6 +7389,26 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7297,7 +7492,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/internal-slot": { @@ -7437,7 +7631,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7672,6 +7865,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-retry-allowed": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-3.0.0.tgz", + "integrity": "sha512-9xH0xvoggby+u0uGF7cZXdrutWiBiaFG8ZT4YFPXL8NzkyAwX3AKGLeFQLvzDpM430+nDFBZ1LHkie/8ocL06A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-set": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", @@ -7753,7 +7958,6 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, "license": "MIT", "dependencies": { "which-typed-array": "^1.1.16" @@ -7815,7 +8019,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -8993,7 +9196,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -9034,7 +9236,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9044,7 +9245,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -9908,7 +10108,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -10186,6 +10385,15 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -10254,6 +10462,15 @@ ], "license": "MIT" }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -10644,6 +10861,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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" + }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -10732,7 +10969,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", @@ -10777,6 +11013,26 @@ "node": ">= 0.4" } }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11600,6 +11856,20 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11613,6 +11883,12 @@ "node": ">=8.0" } }, + "node_modules/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", + "license": "MIT" + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -11810,7 +12086,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.3", @@ -12078,6 +12353,12 @@ "punycode": "^2.1.0" } }, + "node_modules/urijs": { + "version": "1.19.11", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.11.tgz", + "integrity": "sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==", + "license": "MIT" + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -12278,7 +12559,6 @@ "version": "1.1.20", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dev": true, "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", diff --git a/frontend/package.json b/frontend/package.json index bd931a85..25c84a7d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "format": "prettier --write 'src/**/*.{ts,tsx}'" }, "dependencies": { + "@stellar/stellar-sdk": "^15.1.0", "next": "14.2.3", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/frontend/src/app/markets/[market_id]/MarketDetailContent.tsx b/frontend/src/app/markets/[market_id]/MarketDetailContent.tsx index 2b535b26..7165a118 100644 --- a/frontend/src/app/markets/[market_id]/MarketDetailContent.tsx +++ b/frontend/src/app/markets/[market_id]/MarketDetailContent.tsx @@ -2,24 +2,17 @@ import { useEffect, useState } from 'react'; import { useMarket } from '../../../hooks/useMarket'; -import { MarketOddsBar } from '../../../components/market/MarketOddsBar'; import { MarketStatusBadge } from '../../../components/market/MarketStatusBadge'; import { CountdownTimer } from '../../../components/ui/CountdownTimer'; -import { BetPanel } from '../../../components/bet/BetPanel'; +import { FighterCard } from '../../../components/market/FighterCard'; +import { OddsDisplay } from '../../../components/market/OddsDisplay'; +import { PoolBar } from '../../../components/market/PoolBar'; +import { BetForm } from '../../../components/bet/BetForm'; +import { BetList } from '../../../components/bet/BetList'; import { stellarExplorerUrl } from '../../../services/wallet'; import { fetchBetsByMarket, NotFoundError } from '../../../services/api'; import type { Bet } from '../../../types'; -const SIDE_LABEL: Record = { - fighter_a: 'Fighter A', - fighter_b: 'Fighter B', - draw: 'Draw', -}; - -function truncate(addr: string) { - return `${addr.slice(0, 6)}…${addr.slice(-4)}`; -} - function fmtXlm(stroops: string) { return (parseInt(stroops, 10) / 1e7).toLocaleString(undefined, { maximumFractionDigits: 2 }); } @@ -27,21 +20,24 @@ function fmtXlm(stroops: string) { export default function MarketDetailContent({ market_id }: { market_id: string }): JSX.Element { const { market, isLoading, error } = useMarket(market_id); const [recentBets, setRecentBets] = useState([]); + const [betsLoading, setBetsLoading] = useState(false); useEffect(() => { if (!market) return; + setBetsLoading(true); fetchBetsByMarket(market_id) .then((bets) => setRecentBets(bets.slice(0, 20))) - .catch(() => {/* non-critical */}); + .catch(() => {/* non-critical */}) + .finally(() => setBetsLoading(false)); }, [market_id, market]); if (isLoading) { - return
Loading…
; + return
Loading…
; } if (error instanceof NotFoundError || !market) { return ( -
+

404

Market not found.

@@ -50,19 +46,20 @@ export default function MarketDetailContent({ market_id }: { market_id: string } if (error) { return ( -
+

Failed to load market. Please try again.

); } - const sideLabel = (side: string) => - side === 'fighter_a' ? market.fighter_a : side === 'fighter_b' ? market.fighter_b : 'Draw'; + const poolAXlm = parseInt(market.pool_a, 10) / 1e7; + const poolBXlm = parseInt(market.pool_b, 10) / 1e7; + const poolDrawXlm = parseInt(market.pool_draw, 10) / 1e7; return ( -
+
{/* Fight header */} -
+
{market.title_fight && ( @@ -70,76 +67,47 @@ export default function MarketDetailContent({ market_id }: { market_id: string } )} {market.weight_class}
-

+

{market.fighter_a} vs {market.fighter_b}

{market.venue}

- {/* Odds bar + pool sizes */} -
- -
- {fmtXlm(market.pool_a)} XLM on {market.fighter_a} - {fmtXlm(market.pool_draw)} XLM Draw - {fmtXlm(market.pool_b)} XLM on {market.fighter_b} -
+ {/* Fighter cards side by side */} +
+ +
- {/* Two-column on desktop */} + {/* Odds display */} + + + {/* Pool bar */} + + + {/* Main content grid */}
- {/* BetPanel — right col on desktop */} + {/* Bet form — right col on desktop */}
- +
{/* Recent bets — left 2 cols on desktop */} -
-

Recent Bets

- {recentBets.length === 0 ? ( -

No bets yet.

- ) : ( -
- - - - - - - - - - - {recentBets.map((bet) => ( - - - - - - - ))} - -
BettorSideAmountTime
- - {truncate(bet.tx_hash)} - - {sideLabel(bet.side)}{bet.amount_xlm} XLM - {new Date(bet.placed_at).toLocaleTimeString()} -
-
- )} +
+
diff --git a/frontend/src/components/bet/BetForm.tsx b/frontend/src/components/bet/BetForm.tsx new file mode 100644 index 00000000..17b77179 --- /dev/null +++ b/frontend/src/components/bet/BetForm.tsx @@ -0,0 +1,159 @@ +'use client'; + +import { useState, useMemo } from 'react'; +import type { BetSide, Market } from '../../types'; +import { usePlaceBet } from '../../hooks/usePlaceBet'; +import { useWallet } from '../../hooks/useWallet'; +import { ConnectPrompt } from '../ui/ConnectPrompt'; +import { TxStatusToast } from '../ui/TxStatusToast'; +import { useAppStore } from '../../store'; + +interface BetFormProps { + market: Market; +} + +const SIDES: { value: BetSide; label: (a: string, b: string) => string }[] = [ + { value: 'fighter_a', label: (a) => a }, + { value: 'draw', label: () => 'Draw' }, + { value: 'fighter_b', label: (_, b) => b }, +]; + +const FEE_DIVISOR = 10000; + +function calcPayout(market: Market, side: BetSide, amountXlm: number): number | null { + if (!amountXlm || amountXlm <= 0) return null; + const poolA = Number(market.pool_a) / 1e7; + const poolB = Number(market.pool_b) / 1e7; + const poolDraw = Number(market.pool_draw) / 1e7; + const sidePool = side === 'fighter_a' ? poolA : side === 'fighter_b' ? poolB : poolDraw; + const total = poolA + poolB + poolDraw + amountXlm; + const newSidePool = sidePool + amountXlm; + if (newSidePool === 0) return null; + const gross = (amountXlm / newSidePool) * total; + return gross * (1 - market.fee_bps / FEE_DIVISOR); +} + +/** + * BetForm component for placing bets on a market. + * Features: + * - Three outcome buttons: Fighter A / Draw / Fighter B + * - Amount input with min validation and balance check + * - Real-time projected payout display + * - "Connect Wallet" prompt if not connected + * - Disabled with reason when market is Locked/Resolved/Cancelled + * - Calls usePlaceBet() mutation on submit + * - Shows TransactionStatus during/after submission + */ +export function BetForm({ market }: BetFormProps): JSX.Element { + const { isConnected } = useWallet(); + const { placeBet, txStatus, error } = usePlaceBet(); + const setTxStatus = useAppStore((s) => s.setTxStatus); + + const [side, setSide] = useState(null); + const [amount, setAmount] = useState(''); + const [isSubmitting, setIsSubmitting] = useState(false); + + const amountNum = parseFloat(amount); + const isAmountValid = !isNaN(amountNum) && amountNum > 0 && amountNum >= 1; + + const projectedPayout = useMemo(() => { + if (!side) return null; + return calcPayout(market, side, amountNum); + }, [side, amountNum, market]); + + const canSubmit = isConnected && !!side && isAmountValid && !isSubmitting && market.status === 'open'; + + const handleSubmit = async () => { + if (!canSubmit) return; + setIsSubmitting(true); + try { + await placeBet(market.market_id, side, amountNum); + setSide(null); + setAmount(''); + } catch { + // Error is handled by usePlaceBet and displayed in TxStatusToast + } finally { + setIsSubmitting(false); + } + }; + + if (!isConnected) { + return ; + } + + if (market.status !== 'open') { + return ( +
+

Betting is closed

+

Market is {market.status}

+
+ ); + } + + return ( +
+ {/* Side selector */} +
+ {SIDES.map(({ value, label }) => ( + + ))} +
+ + {/* Amount input */} +
+ + setAmount(e.target.value)} + placeholder="0.00" + disabled={isSubmitting} + className="w-full bg-gray-800 rounded-lg px-3 py-2 text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-amber-500 disabled:opacity-50" + /> +

Min: 1 XLM

+
+ + {/* Payout preview */} +
+
+ Platform fee + {market.fee_bps / 100}% +
+
+ Est. payout + {projectedPayout != null ? `${projectedPayout.toFixed(4)} XLM` : '—'} +
+
+ + {/* Submit button */} + + + {/* Transaction status toast */} + { + setTxStatus({ hash: null, status: 'idle', error: null }); + }} + /> +
+ ); +} diff --git a/frontend/src/components/bet/BetList.tsx b/frontend/src/components/bet/BetList.tsx new file mode 100644 index 00000000..4a0a63e4 --- /dev/null +++ b/frontend/src/components/bet/BetList.tsx @@ -0,0 +1,84 @@ +// ============================================================ +// BOXMEOUT — BetList Component +// Displays recent bets on a market +// ============================================================ + +import type { Bet } from '../../types'; +import { stellarExplorerUrl } from '../../services/wallet'; + +interface BetListProps { + bets: Bet[]; + fighterA: string; + fighterB: string; + isLoading?: boolean; +} + +function truncate(addr: string) { + return `${addr.slice(0, 6)}…${addr.slice(-4)}`; +} + +const SIDE_LABEL: Record = { + fighter_a: 'Fighter A', + fighter_b: 'Fighter B', + draw: 'Draw', +}; + +/** + * BetList displays recent bets placed on a market in a table format. + */ +export function BetList({ bets, fighterA, fighterB, isLoading }: BetListProps): JSX.Element { + const sideLabel = (side: string) => + side === 'fighter_a' ? fighterA : side === 'fighter_b' ? fighterB : 'Draw'; + + if (isLoading) { + return ( +
+

Recent Bets

+
Loading…
+
+ ); + } + + return ( +
+

Recent Bets

+ {bets.length === 0 ? ( +

No bets yet.

+ ) : ( +
+ + + + + + + + + + + {bets.map((bet) => ( + + + + + + + ))} + +
BettorSideAmountTime
+ + {truncate(bet.tx_hash)} + + {sideLabel(bet.side)}{bet.amount_xlm} XLM + {new Date(bet.placed_at).toLocaleTimeString()} +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/market/FighterCard.tsx b/frontend/src/components/market/FighterCard.tsx new file mode 100644 index 00000000..fde7d9cf --- /dev/null +++ b/frontend/src/components/market/FighterCard.tsx @@ -0,0 +1,35 @@ +// ============================================================ +// BOXMEOUT — FighterCard Component +// Displays fighter information with odds and pool size +// ============================================================ + +interface FighterCardProps { + name: string; + odds: number; + poolXlm: number; + isSelected?: boolean; +} + +/** + * FighterCard displays a fighter's name, odds, and pool size. + * Used in pairs on the market detail page. + */ +export function FighterCard({ name, odds, poolXlm, isSelected }: FighterCardProps): JSX.Element { + return ( +
+

{name}

+
+
+ {(odds / 100).toFixed(1)}% odds +
+
+ {poolXlm.toLocaleString(undefined, { maximumFractionDigits: 2 })} XLM pooled +
+
+
+ ); +} diff --git a/frontend/src/components/market/OddsDisplay.tsx b/frontend/src/components/market/OddsDisplay.tsx new file mode 100644 index 00000000..35ec5597 --- /dev/null +++ b/frontend/src/components/market/OddsDisplay.tsx @@ -0,0 +1,40 @@ +// ============================================================ +// BOXMEOUT — OddsDisplay Component +// Shows live odds for all three outcomes +// ============================================================ + +interface OddsDisplayProps { + odds_a: number; + odds_b: number; + odds_draw: number; + fighter_a: string; + fighter_b: string; +} + +/** + * OddsDisplay shows the current implied probabilities for all three outcomes. + */ +export function OddsDisplay({ + odds_a, + odds_b, + odds_draw, + fighter_a, + fighter_b, +}: OddsDisplayProps): JSX.Element { + return ( +
+
+

{fighter_a}

+

{(odds_a / 100).toFixed(1)}%

+
+
+

Draw

+

{(odds_draw / 100).toFixed(1)}%

+
+
+

{fighter_b}

+

{(odds_b / 100).toFixed(1)}%

+
+
+ ); +} diff --git a/frontend/src/components/market/PoolBar.tsx b/frontend/src/components/market/PoolBar.tsx new file mode 100644 index 00000000..d9b1016c --- /dev/null +++ b/frontend/src/components/market/PoolBar.tsx @@ -0,0 +1,62 @@ +// ============================================================ +// BOXMEOUT — PoolBar Component +// Visual representation of pool proportions +// ============================================================ + +interface PoolBarProps { + pool_a: string; + pool_b: string; + pool_draw: string; + fighter_a: string; + fighter_b: string; +} + +/** + * PoolBar shows the relative sizes of each outcome pool as a stacked bar. + */ +export function PoolBar({ pool_a, pool_b, pool_draw, fighter_a, fighter_b }: PoolBarProps): JSX.Element { + const poolANum = parseInt(pool_a, 10) / 1e7; + const poolBNum = parseInt(pool_b, 10) / 1e7; + const poolDrawNum = parseInt(pool_draw, 10) / 1e7; + const total = poolANum + poolBNum + poolDrawNum; + + const percentA = total > 0 ? (poolANum / total) * 100 : 0; + const percentB = total > 0 ? (poolBNum / total) * 100 : 0; + const percentDraw = total > 0 ? (poolDrawNum / total) * 100 : 0; + + return ( +
+
+ {percentA > 0 && ( +
+ {percentA > 10 && `${percentA.toFixed(0)}%`} +
+ )} + {percentDraw > 0 && ( +
+ {percentDraw > 10 && `${percentDraw.toFixed(0)}%`} +
+ )} + {percentB > 0 && ( +
+ {percentB > 10 && `${percentB.toFixed(0)}%`} +
+ )} +
+
+ {fighter_a} + Draw + {fighter_b} +
+
+ ); +} diff --git a/frontend/src/components/ui/ErrorBoundary.tsx b/frontend/src/components/ui/ErrorBoundary.tsx index 02d11c5c..7af1bea0 100644 --- a/frontend/src/components/ui/ErrorBoundary.tsx +++ b/frontend/src/components/ui/ErrorBoundary.tsx @@ -1,3 +1,5 @@ +'use client'; + import { Component, type ErrorInfo, type ReactNode } from 'react'; interface Props { diff --git a/frontend/src/hooks/usePlaceBet.ts b/frontend/src/hooks/usePlaceBet.ts new file mode 100644 index 00000000..f8a5613c --- /dev/null +++ b/frontend/src/hooks/usePlaceBet.ts @@ -0,0 +1,71 @@ +// ============================================================ +// BOXMEOUT — usePlaceBet Hook +// Mutation hook for signing and broadcasting place_bet transactions +// ============================================================ + +import { useCallback, useState } from 'react'; +import type { BetSide, TxStatus } from '../types'; +import { submitBet } from '../services/wallet'; +import { useAppStore } from '../store'; + +export interface UsePlaceBetResult { + placeBet: (market_id: string, side: BetSide, amount_xlm: number) => Promise; + txStatus: TxStatus; + txHash: string | null; + error: string | null; + reset: () => void; +} + +/** + * Mutation hook for placing a bet on a market. + * Manages state machine: idle → signing → broadcasting → confirming → success | error + * On success, invalidates useMarket and useBets query caches. + */ +export function usePlaceBet(): UsePlaceBetResult { + const [txStatus, setTxStatus] = useState({ hash: null, status: 'idle', error: null }); + const [error, setError] = useState(null); + const { setTxStatus: setAppTxStatus } = useAppStore(); + + const placeBet = useCallback( + async (market_id: string, side: BetSide, amount_xlm: number) => { + setError(null); + setTxStatus({ hash: null, status: 'signing', error: null }); + + try { + // Sign and broadcast transaction + setTxStatus({ hash: null, status: 'broadcasting', error: null }); + const hash = await submitBet(market_id, side, amount_xlm); + + // Confirm transaction + setTxStatus({ hash, status: 'confirming', error: null }); + + // Success + setTxStatus({ hash, status: 'success', error: null }); + setAppTxStatus({ hash, status: 'success', error: null }); + + // Invalidate caches (handled by parent component via useMarket/useBets polling) + } catch (err: any) { + const msg = err?.message ?? 'Transaction failed'; + setError(msg); + setTxStatus({ hash: null, status: 'error', error: msg }); + setAppTxStatus({ hash: null, status: 'error', error: msg }); + throw err; + } + }, + [setAppTxStatus], + ); + + const reset = useCallback(() => { + setTxStatus({ hash: null, status: 'idle', error: null }); + setError(null); + setAppTxStatus({ hash: null, status: 'idle', error: null }); + }, [setAppTxStatus]); + + return { + placeBet, + txStatus, + txHash: txStatus.hash, + error, + reset, + }; +}