diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png index 4ad073a91a..9987878c48 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-input-price.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-animation.png b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-animation.png index 8590f7307a..539eebe661 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-animation.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-animation.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-summary.png b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-summary.png index e0afd07819..fb2e637288 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-summary.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_limit/simulator-results-summary.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-input-price.png b/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-input-price.png index 90f82bc2dc..cfa4b4be8f 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-input-price.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-input-price.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-results-summary.png b/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-results-summary.png index 9e44b15df4..470a8d3427 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-results-summary.png and b/e2e/screenshots/simulator/recurring/Recurring_limit_range/simulator-results-summary.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-input-price.png b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-input-price.png index cb10f60852..f1c8a5b733 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-input-price.png and b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-input-price.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-animation.png b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-animation.png index 04cb0b9a8f..6d2ed3bc84 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-animation.png and b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-animation.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-summary.png b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-summary.png index 4dda10f536..ba4cb6b623 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-summary.png and b/e2e/screenshots/simulator/recurring/Recurring_range_limit/simulator-results-summary.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-animation.png b/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-animation.png index 9bc9e437a2..0a70b465a6 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-animation.png and b/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-animation.png differ diff --git a/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-summary.png b/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-summary.png index 4bff6aa54c..5f06f9fee7 100644 Binary files a/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-summary.png and b/e2e/screenshots/simulator/recurring/Recurring_range_range/simulator-results-summary.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png index d09ac833c4..68cccd839b 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png index e02da450c2..f3f621b35e 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/duplicate/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/duplicate/form.png index 12d5540a7c..7a38ad3d8d 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/duplicate/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/editPrices/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/editPrices/form.png index 1177e38916..4abdfeada0 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/editPrices/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/undercut/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/undercut/form.png index 1413074027..bfb9aba23a 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_limit/undercut/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_limit/undercut/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/form.png index fde4b2309d..337830962d 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png index 20b30d6ce3..c3a48443a3 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/duplicate/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/duplicate/form.png index 5c0d0a86cb..6448c826bf 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/duplicate/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/editPrices/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/editPrices/form.png index d78a646ddc..2e181619d4 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/editPrices/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_buy_range/undercut/form.png b/e2e/screenshots/strategy/disposable/Disposable_buy_range/undercut/form.png index b0d8c3b0ec..f8f1ddf2e5 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_buy_range/undercut/form.png and b/e2e/screenshots/strategy/disposable/Disposable_buy_range/undercut/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/form.png index abad9900fe..2f9e2e922d 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png index 007e581240..634ce770dd 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/duplicate/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/duplicate/form.png index 7a43d20227..2c1f07cc91 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/duplicate/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/editPrices/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/editPrices/form.png index 77f67fb5b3..0ccb97869c 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/editPrices/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/undercut/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/undercut/form.png index 9b339c9882..2bfd15abec 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_limit/undercut/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_limit/undercut/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/form.png index 80da97ef91..151eb4d8fc 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png index 454a424fd3..99398dc087 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/duplicate/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/duplicate/form.png index dd83de1bce..d398eb205c 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/duplicate/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/editPrices/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/editPrices/form.png index 77da5bd10d..528f955110 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/editPrices/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/disposable/Disposable_sell_range/undercut/form.png b/e2e/screenshots/strategy/disposable/Disposable_sell_range/undercut/form.png index 294d0749d5..bed14bc775 100644 Binary files a/e2e/screenshots/strategy/disposable/Disposable_sell_range/undercut/form.png and b/e2e/screenshots/strategy/disposable/Disposable_sell_range/undercut/form.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png b/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png index 3aa969a0b6..0e61b1845c 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png and b/e2e/screenshots/strategy/overlapping/Overlapping/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/deposit/form.png b/e2e/screenshots/strategy/overlapping/Overlapping/deposit/form.png index 3e560f61e2..f353b0c79e 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/deposit/form.png and b/e2e/screenshots/strategy/overlapping/Overlapping/deposit/form.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/duplicate/form.png b/e2e/screenshots/strategy/overlapping/Overlapping/duplicate/form.png index 41b9507332..6599e7bd74 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/duplicate/form.png and b/e2e/screenshots/strategy/overlapping/Overlapping/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/editPrices/form.png b/e2e/screenshots/strategy/overlapping/Overlapping/editPrices/form.png index e3c1e37df2..bac574b51f 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/editPrices/form.png and b/e2e/screenshots/strategy/overlapping/Overlapping/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/overlapping/Overlapping/undercut/form.png b/e2e/screenshots/strategy/overlapping/Overlapping/undercut/form.png index 75c7f59d39..ebeca327f3 100644 Binary files a/e2e/screenshots/strategy/overlapping/Overlapping/undercut/form.png and b/e2e/screenshots/strategy/overlapping/Overlapping/undercut/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png index 7daa4c070e..cc12e6f9c7 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/duplicate/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/duplicate/form.png index 5bec29d510..fcac3ee91e 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/duplicate/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/withdraw/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/withdraw/form.png index 40478b9c6a..973af896c4 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_limit/withdraw/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_limit/withdraw/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png index bb21796ae7..71bac1138d 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/duplicate/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/duplicate/form.png index 95e09d2465..bf8a1df973 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/duplicate/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/undercut/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/undercut/form.png index 5bbb099f46..715d782b77 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/undercut/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/undercut/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_limit_range/withdraw/form.png b/e2e/screenshots/strategy/recurring/Recurring_limit_range/withdraw/form.png index cbecd72ee4..d0a3a43239 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_limit_range/withdraw/form.png and b/e2e/screenshots/strategy/recurring/Recurring_limit_range/withdraw/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png index 2f86337ab6..5fdface1d7 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_range_limit/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_limit/duplicate/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_limit/duplicate/form.png index 5cdf69aadb..7258c03be1 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_limit/duplicate/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_limit/duplicate/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_limit/undercut/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_limit/undercut/form.png index d42a6cc822..dcf4871d41 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_limit/undercut/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_limit/undercut/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png index c669037dad..b469a968b5 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/create/my-strategy.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png index 8f541b0a1d..634439f27c 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/deposit/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/editPrices/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/editPrices/form.png index b0d8a09fbe..d70b2cee25 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/editPrices/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/editPrices/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/renew/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/renew/form.png index 4f5b144bde..7fb1116101 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/renew/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/renew/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/undercut/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/undercut/form.png index fa08904e73..0d2bb7bb80 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/undercut/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/undercut/form.png differ diff --git a/e2e/screenshots/strategy/recurring/Recurring_range_range/withdraw/form.png b/e2e/screenshots/strategy/recurring/Recurring_range_range/withdraw/form.png index 7b59b6ac4d..2fbe4e21c4 100644 Binary files a/e2e/screenshots/strategy/recurring/Recurring_range_range/withdraw/form.png and b/e2e/screenshots/strategy/recurring/Recurring_range_range/withdraw/form.png differ diff --git a/package.json b/package.json index cac9922051..68d90a08fb 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "dependencies": { - "@bancor/carbon-sdk": "0.0.129-DEV", + "@bancor/carbon-sdk": "0.0.139-DEV", "@cloudflare/workers-types": "4.20230717.0", "@eslint/js": "^9.39.2", "@floating-ui/core": "^1.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2da00ede0c..3bc55feb64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,13 +4,16 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + carbon-sdk: link:../../../Users/franc/AppData/Local/pnpm/global/5/node_modules/@bancor/carbon-sdk + importers: .: dependencies: '@bancor/carbon-sdk': - specifier: 0.0.129-DEV - version: 0.0.129-DEV(ethers@6.14.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + specifier: 0.0.139-DEV + version: 0.0.139-DEV(ethers@6.14.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@cloudflare/workers-types': specifier: 4.20230717.0 version: 4.20230717.0 @@ -493,8 +496,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@bancor/carbon-sdk@0.0.129-DEV': - resolution: {integrity: sha512-gO2h4zeKgW8o8cQUS+zrTQ2qZJJ+vI/5uBcJz5E3PCwJl7//Vkt9/ouXZeV1gTDY2hS9eAnsvPxveygxwAwFnQ==} + '@bancor/carbon-sdk@0.0.139-DEV': + resolution: {integrity: sha512-zilHLZdY/34iszTcQ8nNsdO+wku4JWRpbm7Ryt0AjrjNFZJa1hAwVnXIRC34OsAO8WcltAUvX1680EiEE9ub9A==} engines: {node: '>=18'} peerDependencies: ethers: ^6.15.0 @@ -7261,7 +7264,7 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@bancor/carbon-sdk@0.0.129-DEV(ethers@6.14.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + '@bancor/carbon-sdk@0.0.139-DEV(ethers@6.14.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: decimal.js: 10.6.0 ethers: 6.14.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..628f176cad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +overrides: + carbon-sdk: link:../../../Users/franc/AppData/Local/pnpm/global/5/node_modules/@bancor/carbon-sdk diff --git a/src/assets/icons/auction.svg b/src/assets/icons/auction.svg new file mode 100644 index 0000000000..f56e52770c --- /dev/null +++ b/src/assets/icons/auction.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/assets/icons/channel.svg b/src/assets/icons/channel.svg new file mode 100644 index 0000000000..3539958d16 --- /dev/null +++ b/src/assets/icons/channel.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/assets/icons/fast.svg b/src/assets/icons/fast.svg index e6351b0023..c25ad013ce 100644 --- a/src/assets/icons/fast.svg +++ b/src/assets/icons/fast.svg @@ -1,9 +1,3 @@ - - - - - - - - + + \ No newline at end of file diff --git a/src/assets/icons/hourglass.svg b/src/assets/icons/hourglass.svg index dc1f59243c..fc8980ba83 100644 --- a/src/assets/icons/hourglass.svg +++ b/src/assets/icons/hourglass.svg @@ -1,3 +1,3 @@ - - + + diff --git a/src/assets/icons/slow.svg b/src/assets/icons/slow.svg index 993d6dba5e..feb415ad5c 100644 --- a/src/assets/icons/slow.svg +++ b/src/assets/icons/slow.svg @@ -1,9 +1,3 @@ - - - - - - - - + + \ No newline at end of file diff --git a/src/components/activity/ActivityExport.tsx b/src/components/activity/ActivityExport.tsx index 0661fb43ac..f04c1d5fef 100644 --- a/src/components/activity/ActivityExport.tsx +++ b/src/components/activity/ActivityExport.tsx @@ -35,16 +35,16 @@ const getActivityCSV = (activities: Activity[]) => { 'Block Number', 'Transaction Hash', 'Date', - // @todo(gradient) - // 'Strategy Type', - // 'Buy _S P_', - // 'Buy _E P_', - // 'Buy _S D_', - // 'Buy _E D_', - // 'Sell _S P_', - // 'Sell _E P_', - // 'Sell _S D_', - // 'Sell _E D_', + // Gradient + 'Strategy Type', + 'Buy Start Price', + 'Buy End Price', + 'Buy Start Date', + 'Buy End Date', + 'Sell Start Price', + 'Sell End Price', + 'Sell Start Date', + 'Sell End Date', ].join(','); const body = activities.map((activity) => { @@ -53,19 +53,19 @@ const getActivityCSV = (activities: Activity[]) => { const date = new Date(activity.date).toLocaleDateString(); const min = (order: Order) => { if ('min' in order) return order.min; - return SafeDecimal.min(order._sP_, order._eP_).toString(); + return SafeDecimal.min(order.startPrice, order.endPrice).toString(); }; const max = (order: Order) => { if ('max' in order) return order.max; - return SafeDecimal.max(order._sP_, order._eP_).toString(); + return SafeDecimal.max(order.startPrice, order.endPrice).toString(); }; const gradientPrices = (order: Order) => { if ('min' in order) return ['', '', '', '']; return [ - order._sP_, - order._eP_, - fromUnixUTC(order._sD_).toLocaleDateString(), - fromUnixUTC(order._eD_).toLocaleDateString(), + order.startPrice, + order.endPrice, + fromUnixUTC(order.startDate).toLocaleDateString(), + fromUnixUTC(order.endDate).toLocaleDateString(), ]; }; return [ @@ -89,10 +89,9 @@ const getActivityCSV = (activities: Activity[]) => { blockNumber, txHash, date, - // @todo(gradient) - // strategy.type === 'gradient' ? 'Gradient' : 'Static', - // ...gradientPrices(strategy.buy), - // ...gradientPrices(strategy.sell), + strategy.type === 'gradient' ? 'Gradient' : 'Static', + ...gradientPrices(strategy.buy), + ...gradientPrices(strategy.sell), ] .map((v) => `"${v}"`) // Ignore inner comma .join(','); diff --git a/src/components/activity/ActivityList.tsx b/src/components/activity/ActivityList.tsx index 9712b01937..a5e1300a27 100644 --- a/src/components/activity/ActivityList.tsx +++ b/src/components/activity/ActivityList.tsx @@ -14,6 +14,7 @@ import { } from './ActivityTable'; import { shortenString, tokenAmount } from 'utils/helpers'; import { useActivity, useActivityPagination } from './context'; +import { StrategyTypeIcon } from 'components/strategies/overview/StrategyTypeIcon'; export interface ActivityListProps { activities: Activity[]; @@ -67,9 +68,10 @@ const ActivityItem: FC = ({ activity, hideIds }) => { }; return (
  • -
    +
    + {!hideIds && } -
    +

    {activityDateFormatter.format(activity.date)}

    diff --git a/src/components/activity/ActivityTable.tsx b/src/components/activity/ActivityTable.tsx index 93c406d039..be5e66e22c 100644 --- a/src/components/activity/ActivityTable.tsx +++ b/src/components/activity/ActivityTable.tsx @@ -26,6 +26,7 @@ import { Token } from 'libs/tokens'; import { ActivityListProps } from './ActivityList'; import { NotFound } from 'components/common/NotFound'; import { useActivity, useActivityPagination } from './context'; +import { StrategyTypeIcon } from 'components/strategies/overview/StrategyTypeIcon'; import style from './ActivityTable.module.css'; export const ActivityTable: FC = (props) => { @@ -35,11 +36,10 @@ export const ActivityTable: FC = (props) => { {!hideIds && ID} - {/* @todo(gradient) */} - {/* Type */} + Type Action - Buy Budget - Sell Budget + Buy Budget + Sell Budget Date @@ -99,13 +99,12 @@ const ActivityRow: FC = ({ activity, hideIds, index }) => { )} - {/* @todo(gradient) */} - {/* + - */} + - +
    diff --git a/src/components/strategies/common/gradient/GradientFullOutcome.tsx b/src/components/strategies/common/gradient/GradientFullOutcome.tsx index a02d92c371..cfffba6842 100644 --- a/src/components/strategies/common/gradient/GradientFullOutcome.tsx +++ b/src/components/strategies/common/gradient/GradientFullOutcome.tsx @@ -14,10 +14,10 @@ interface Props { export const GradientFullOutcome: FC = (props) => { const { base, quote, order } = props; - const { _sP_, _eP_, budget, marginalPrice, direction } = order; + const { startPrice, endPrice, budget, marginalPrice, direction } = order; if (!marginalPrice || isZero(budget)) return; - const min = SafeDecimal.min(_sP_, _eP_, marginalPrice).mul(budget); - const max = SafeDecimal.max(_sP_, _eP_, marginalPrice).mul(budget); + const min = SafeDecimal.min(startPrice, endPrice, marginalPrice).mul(budget); + const max = SafeDecimal.max(startPrice, endPrice, marginalPrice).mul(budget); const token = direction === 'buy' ? base : quote; return ( diff --git a/src/components/strategies/common/gradient/GradientPriceRange.tsx b/src/components/strategies/common/gradient/GradientPriceRange.tsx index 4c04567f61..2a322a9129 100644 --- a/src/components/strategies/common/gradient/GradientPriceRange.tsx +++ b/src/components/strategies/common/gradient/GradientPriceRange.tsx @@ -12,7 +12,7 @@ import { Token } from 'libs/tokens'; import { formatNumber, roundSearchParam, sanitizeNumber } from 'utils/helpers'; import { decimalNumberValidationRegex } from 'utils/inputsValidations'; import { Presets } from 'components/common/preset/Preset'; -import { buyPresets, sellPresets } from '../price-presets'; +import { limitPreset } from '../price-presets'; import { StrategyDirection } from 'libs/routing'; import { SafeDecimal } from 'libs/safedecimal'; import { MarketPriceIndication } from 'components/strategies/marketPriceIndication/MarketPriceIndication'; @@ -26,6 +26,7 @@ interface Props { end: string; setEnd: (value: string) => void; direction: StrategyDirection; + disabled?: boolean; } export const GradientPriceRange: FC = (props) => { @@ -41,12 +42,12 @@ export const GradientPriceRange: FC = (props) => { const startPercent = useMemo(() => { if (!marketPrice) return ''; - return new SafeDecimal(start).div(marketPrice).sub(1).mul(100).toString(); + return new SafeDecimal(start).div(marketPrice).sub(1).abs().toString(); }, [marketPrice, start]); const endPercent = useMemo(() => { if (!marketPrice) return ''; - return new SafeDecimal(end).div(marketPrice).sub(1).mul(100).toString(); + return new SafeDecimal(end).div(marketPrice).sub(1).abs().toString(); }, [marketPrice, end]); useEffect(() => { @@ -63,7 +64,10 @@ export const GradientPriceRange: FC = (props) => { const setStartPreset = (preset: string) => { if (!marketPrice) return; - const percent = new SafeDecimal(1).add(new SafeDecimal(preset).div(100)); + const percent = + direction === 'buy' + ? new SafeDecimal(1).sub(new SafeDecimal(preset)) + : new SafeDecimal(1).add(new SafeDecimal(preset)); const next = new SafeDecimal(marketPrice).mul(percent).toString(); setLocalStart(roundSearchParam(next)); setStart(next); @@ -117,7 +121,10 @@ export const GradientPriceRange: FC = (props) => { const setEndPreset = (preset: string) => { if (!marketPrice) return; - const percent = new SafeDecimal(1).add(new SafeDecimal(preset).div(100)); + const percent = + direction === 'buy' + ? new SafeDecimal(1).sub(new SafeDecimal(preset)) + : new SafeDecimal(1).add(new SafeDecimal(preset)); const next = new SafeDecimal(marketPrice).mul(percent).toString(); setLocalEnd(roundSearchParam(next)); setEnd(next); @@ -127,7 +134,7 @@ export const GradientPriceRange: FC = (props) => { <>
    document.getElementById(inputStartId)?.focus()} >
    @@ -135,9 +142,9 @@ export const GradientPriceRange: FC = (props) => { htmlFor={inputStartId} className="text-12 flex justify-between text-main-0/60" > - _S P_ + Start Price - {!!marketPrice && ( + {!!marketPrice && !props.disabled && (
    document.getElementById(inputEndId)?.focus()} >
    @@ -178,9 +186,9 @@ export const GradientPriceRange: FC = (props) => { htmlFor={inputEndId} className="text-12 flex justify-between text-main-0/60" > - _E P_ + End Price - {!!marketPrice && ( + {!!marketPrice && !props.disabled && (
    - {!!marketPrice && ( + {!!marketPrice && !props.disabled && ( <> diff --git a/src/components/strategies/common/gradient/GradientShape.tsx b/src/components/strategies/common/gradient/GradientShape.tsx new file mode 100644 index 0000000000..13fb94d95a --- /dev/null +++ b/src/components/strategies/common/gradient/GradientShape.tsx @@ -0,0 +1,63 @@ +import { FC } from 'react'; +import { DropdownMenu } from 'components/common/dropdownMenu'; +import { Link } from '@tanstack/react-router'; +import IconSlow from 'assets/icons/slow.svg?react'; +import IconFast from 'assets/icons/fast.svg?react'; +import KeyboardArrowDownIcon from 'assets/icons/keyboard_arrow_down.svg?react'; + +interface Props { + shape: 'triangle' | 'channel'; +} + +const items = { + triangle: { + Icon: IconFast, + to: '/trade/triangle' as const, + label: 'Triangle', + }, + channel: { + Icon: IconSlow, + to: '/trade/channel' as const, + label: 'Channel', + }, +}; + +export const GradientShape: FC = ({ shape }) => { + const item = items[shape]; + return ( + ( + + )} + > + {Object.entries(items).map(([id, item]) => ( + ({ + base: s.base, + quote: s.quote, + marketPrice: s.marketPrice, + chartStart: s.chartStart, + chartEnd: s.chartEnd, + })} + > + + {item.label} + + ))} + + ); +}; diff --git a/src/components/strategies/common/gradient/useGradientOrder.tsx b/src/components/strategies/common/gradient/useGradientOrder.tsx index 125e2c014c..fcadbdd17c 100644 --- a/src/components/strategies/common/gradient/useGradientOrder.tsx +++ b/src/components/strategies/common/gradient/useGradientOrder.tsx @@ -7,12 +7,13 @@ import { useState, } from 'react'; import { GradientOrderBlock, QuickGradientOrderBlock } from '../types'; -import { defaultGradientOrder } from './utils'; +import { defaultGradientMultipliers, defaultGradientOrder } from './utils'; import { defaultQuickGradientOrder, quickToGradientOrder, } from '../quick/utils'; import { ChartPoint, Drawing } from '../d3Chart/D3ChartContext'; +import { useStrategyFormCtx } from '../StrategyFormContext'; export const useGradientOrder = ( initOrder: GradientOrderBlock, @@ -20,15 +21,25 @@ export const useGradientOrder = ( ) => { const id = useId(); const timeout = useRef(null); + const { base, quote } = useStrategyFormCtx(); + + const direction = initOrder.direction; + + const multi = useMemo(() => { + return defaultGradientMultipliers(base.address, quote.address, direction); + }, [base.address, quote.address, direction]); + const [order, setOrder] = useState(initOrder); const set = useCallback( (next: Partial) => { - setOrder((current) => defaultGradientOrder({ ...current, ...next })); + setOrder((current) => + defaultGradientOrder({ ...current, ...next }, multi), + ); if (timeout.current) clearTimeout(timeout.current); timeout.current = setTimeout(() => saveOrder(next), 200); }, - [saveOrder], + [multi, saveOrder], ); useEffect(() => { @@ -41,16 +52,16 @@ export const useGradientOrder = ( mode: 'line', points: [ { - x: order._sD_, - y: Number(order._sP_), + x: order.startDate, + y: Number(order.startPrice), }, { - x: order._eD_, - y: Number(order._eP_), + x: order.endDate, + y: Number(order.endPrice), }, ], }), - [id, order._eD_, order._eP_, order._sD_, order._sP_], + [id, order.endDate, order.endPrice, order.startDate, order.startPrice], ); const onDrawingUpdate = useCallback( @@ -59,10 +70,10 @@ export const useGradientOrder = ( const copy = structuredClone(points); const [start, end] = copy.sort((a, b) => Number(a.x) - Number(b.x)); set({ - _sP_: start.y.toString(), - _eP_: end.y.toString(), - _sD_: start.x, - _eD_: end.x, + startPrice: start.y.toString(), + endPrice: end.y.toString(), + startDate: start.x, + endDate: end.x, }); }, [set], @@ -82,15 +93,23 @@ export const useQuickGradientOrder = ( ) => { const id = useId(); const timeout = useRef(null); + const { base, quote } = useStrategyFormCtx(); const [order, setOrder] = useState(initOrder); + const direction = initOrder.direction; + const multi = useMemo(() => { + return defaultGradientMultipliers(base.address, quote.address, direction); + }, [base.address, quote.address, direction]); + const set = useCallback( (next: Partial) => { - setOrder((current) => defaultQuickGradientOrder({ ...current, ...next })); + setOrder((current) => { + return defaultQuickGradientOrder({ ...current, ...next }, multi); + }); if (timeout.current) clearTimeout(timeout.current); timeout.current = setTimeout(() => saveOrder(next), 200); }, - [saveOrder], + [multi, saveOrder], ); useEffect(() => { @@ -104,15 +123,15 @@ export const useQuickGradientOrder = ( points: [ { x: '0', - y: Number(order._sP_), + y: Number(order.startPrice), }, { x: order.deltaTime, - y: Number(order._eP_), + y: Number(order.endPrice), }, ], }), - [id, order._sP_, order.deltaTime, order._eP_], + [id, order.startPrice, order.deltaTime, order.endPrice], ); const onDrawingUpdate = useCallback( @@ -121,8 +140,8 @@ export const useQuickGradientOrder = ( const copy = structuredClone(points); const [start, end] = copy.sort((a, b) => Number(a.x) - Number(b.x)); set({ - _sP_: start.y.toString(), - _eP_: end.y.toString(), + startPrice: start.y.toString(), + endPrice: end.y.toString(), deltaTime: end.x, }); }, diff --git a/src/components/strategies/common/gradient/utils.sdk.ts b/src/components/strategies/common/gradient/utils.sdk.ts new file mode 100644 index 0000000000..dbb99cb590 --- /dev/null +++ b/src/components/strategies/common/gradient/utils.sdk.ts @@ -0,0 +1,62 @@ +import { GradientType } from '@bancor/carbon-sdk'; +import { SafeDecimal } from 'libs/safedecimal'; +import { FormGradientOrder } from '../types'; +import { StrategyDirection } from 'libs/routing'; +import { Token } from 'libs/tokens'; +import { isZero } from '../utils'; + +export const STRATEGY_TYPE_SHIFT = 248n; +export const GRADIENT_STRATEGY_TYPE_MASK = 1n << 255n; +export const STRATEGY_TYPE_VALUE_MASK = (1n << STRATEGY_TYPE_SHIFT) - 1n; + +export function isGradientStrategyId(id: bigint) { + return (id & GRADIENT_STRATEGY_TYPE_MASK) !== 0n; +} + +export const getGradientType = ( + order: FormGradientOrder, + direction: StrategyDirection, +) => { + const goUp = new SafeDecimal(order.startPrice).lt(order.endPrice); + const increase = direction === 'buy' ? goUp : !goUp; + return increase ? GradientType.LINEAR_INCREASE : GradientType.LINEAR_DECREASE; +}; + +interface GradientStrategyEdit { + base: Token; + quote: Token; + buy: FormGradientOrder; + sell: FormGradientOrder; +} + +export const createGradientStrategyParams = (strategy: GradientStrategyEdit) => + [ + strategy.base.address, + strategy.quote.address, + ...createGradientOrderParams(strategy.buy, 'buy'), + ...createGradientOrderParams(strategy.sell, 'sell'), + ] as const; + +const createGradientOrderParams = ( + order: FormGradientOrder, + direction: StrategyDirection, +) => { + const empty = isZero(order.startPrice) && isZero(order.endPrice); + if (empty) + return [ + '0', + '0', + order.budget, + 0, + 0, + GradientType.LINEAR_INCREASE, + ] as const; + return [ + order.startPrice || '0', + order.endPrice || '0', + order.budget || '0', + Number(order.startDate), + Number(order.endDate), + getGradientType(order, direction), + ] as const; +}; diff --git a/src/components/strategies/common/gradient/utils.test.ts b/src/components/strategies/common/gradient/utils.test.ts index 2a092792eb..cb4b5d6dd2 100644 --- a/src/components/strategies/common/gradient/utils.test.ts +++ b/src/components/strategies/common/gradient/utils.test.ts @@ -5,48 +5,48 @@ describe('isReverseGradientOrders', () => { // @todo(gradient) // it('should return true if buy line is above sell line', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '2', - // _eP_: '8', + // startDate: '0', + // endDate: '10', + // startPrice: '2', + // endPrice: '8', // }; // const sell = { - // _sD_: '2', - // _eD_: '12', - // _sP_: '5', - // _eP_: '3', + // startDate: '2', + // endDate: '12', + // startPrice: '5', + // endPrice: '3', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); it('should return false if buy line is below sell line', () => { const buy = { - _sD_: '0', - _eD_: '10', - _sP_: '2', - _eP_: '4', + startDate: '0', + endDate: '10', + startPrice: '2', + endPrice: '4', }; const sell = { - _sD_: '2', - _eD_: '12', - _sP_: '5', - _eP_: '7', + startDate: '2', + endDate: '12', + startPrice: '5', + endPrice: '7', }; expect(isReverseGradientOrders(buy, sell)).toBe(false); }); it('should return false if lines do not overlap', () => { const buy = { - _sD_: '0', - _eD_: '10', - _sP_: '2', - _eP_: '4', + startDate: '0', + endDate: '10', + startPrice: '2', + endPrice: '4', }; const sell = { - _sD_: '12', - _eD_: '20', - _sP_: '5', - _eP_: '7', + startDate: '12', + endDate: '20', + startPrice: '5', + endPrice: '7', }; expect(isReverseGradientOrders(buy, sell)).toBe(false); }); @@ -54,48 +54,48 @@ describe('isReverseGradientOrders', () => { // @todo(gradient) // it('should return true if buy line slopes downwards and overlaps', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '8', - // _eP_: '2', + // startDate: '0', + // endDate: '10', + // startPrice: '8', + // endPrice: '2', // }; // const sell = { - // _sD_: '2', - // _eD_: '12', - // _sP_: '5', - // _eP_: '7', + // startDate: '2', + // endDate: '12', + // startPrice: '5', + // endPrice: '7', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); it('should return false if overlap only occurs at one point and buy is not above sell', () => { const buy = { - _sD_: '0', - _eD_: '10', - _sP_: '2', - _eP_: '4', + startDate: '0', + endDate: '10', + startPrice: '2', + endPrice: '4', }; const sell = { - _sD_: '10', - _eD_: '20', - _sP_: '5', - _eP_: '7', + startDate: '10', + endDate: '20', + startPrice: '5', + endPrice: '7', }; expect(isReverseGradientOrders(buy, sell)).toBe(false); }); it('should return false if overlap only occurs at one point and buy is equal to sell', () => { const buy = { - _sD_: '0', - _eD_: '10', - _sP_: '2', - _eP_: '4', + startDate: '0', + endDate: '10', + startPrice: '2', + endPrice: '4', }; const sell = { - _sD_: '10', - _eD_: '20', - _sP_: '4', - _eP_: '7', + startDate: '10', + endDate: '20', + startPrice: '4', + endPrice: '7', }; expect(isReverseGradientOrders(buy, sell)).toBe(false); }); @@ -103,64 +103,64 @@ describe('isReverseGradientOrders', () => { // @todo(gradient) // it('should return true if buy line is above sell line in the middle of overlap', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '2', - // _eP_: '7', + // startDate: '0', + // endDate: '10', + // startPrice: '2', + // endPrice: '7', // }; // const sell = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '4', - // _eP_: '5', + // startDate: '0', + // endDate: '10', + // startPrice: '4', + // endPrice: '5', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); // it('should return true if lines cross', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '4', - // _eP_: '2', + // startDate: '0', + // endDate: '10', + // startPrice: '4', + // endPrice: '2', // }; // const sell = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '2', - // _eP_: '4', + // startDate: '0', + // endDate: '10', + // startPrice: '2', + // endPrice: '4', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); // it('should handle decimal values correctly', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '2.5', - // _eP_: '7.5', + // startDate: '0', + // endDate: '10', + // startPrice: '2.5', + // endPrice: '7.5', // }; // const sell = { - // _sD_: '2', - // _eD_: '12', - // _sP_: '4.5', - // _eP_: '3.5', + // startDate: '2', + // endDate: '12', + // startPrice: '4.5', + // endPrice: '3.5', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); // it('should handle string number values correctly', () => { // const buy = { - // _sD_: '0', - // _eD_: '10', - // _sP_: '2.5', - // _eP_: '7.5', + // startDate: '0', + // endDate: '10', + // startPrice: '2.5', + // endPrice: '7.5', // }; // const sell = { - // _sD_: '2', - // _eD_: '12', - // _sP_: '4.5', - // _eP_: '3.5', + // startDate: '2', + // endDate: '12', + // startPrice: '4.5', + // endPrice: '3.5', // }; // expect(isReverseGradientOrders(buy, sell)).toBe(true); // }); diff --git a/src/components/strategies/common/gradient/utils.tsx b/src/components/strategies/common/gradient/utils.tsx index 75d1e26da1..86a75c8aa4 100644 --- a/src/components/strategies/common/gradient/utils.tsx +++ b/src/components/strategies/common/gradient/utils.tsx @@ -1,4 +1,8 @@ -import { fromUnixUTC, toUnixUTCDay } from 'components/simulator/utils'; +import { + fromUnixUTC, + toUnixUTC, + toUnixUTCDay, +} from 'components/simulator/utils'; import { FormGradientOrder, QuickGradientOrderBlock, @@ -8,27 +12,69 @@ import { addDays, endOfDay, isToday, startOfDay } from 'date-fns'; import { SafeDecimal } from 'libs/safedecimal'; import { StrategyDirection } from 'libs/routing'; import { Token } from 'libs/tokens'; -import { isEmptyGradientOrder } from '../utils'; +import { isEmptyGradientOrder, isOrderInFuture, isOrderInPast } from '../utils'; +import config from 'config'; -export const gradientMarginalPrice = (order: FormGradientOrder) => { +export const gradientMarginalPrice = ( + order: FormGradientOrder, + date = new Date(), +) => { if (isEmptyGradientOrder(order)) return '0'; - return '0'; // @todo(gradient) we should use the sdk if possible + // Set the marginal price to 0 to hide the line in the graph + if (isOrderInPast(order)) return '0'; + if (isOrderInFuture(order)) return '0'; + const now = toUnixUTC(date); + const { startPrice, endPrice, startDate, endDate } = order; + const totalDuration = new SafeDecimal(endDate).minus(startDate); + const elapsed = new SafeDecimal(now).minus(startDate); + const tRaw = elapsed.div(totalDuration); + const t = SafeDecimal.clamp(tRaw, 0, 1); + const delta = new SafeDecimal(endPrice).minus(startPrice); + // startPrice + t*delta + const marginal = new SafeDecimal(startPrice).add(t.mul(delta)); + return marginal.toString(); +}; + +const today = new Date(); +export const defaultGradientStartDate = toUnixUTCDay(addDays(today, 5)); +export const defaultGradientEndDate = toUnixUTCDay(addDays(today, 50)); + +export interface GradientMultipliers { + start: number; + end: number; +} +export const defaultGradientMultipliers = ( + base: string, + quote: string, + direction: StrategyDirection = 'sell', +): GradientMultipliers => { + const isStable = (token: string) => config.stableTokens.includes(token); + const stablePair = isStable(base) && isStable(quote); + if (stablePair) { + return { + start: direction === 'buy' ? 0.8 : 1.2, + end: direction === 'buy' ? 0.98 : 1.02, + }; + } else { + return { + start: direction === 'buy' ? 0.9 : 1.1, + end: direction === 'buy' ? 0.99 : 1.01, + }; + } }; export const defaultGradientOrder = ( baseOrder: Partial, + multipliers: GradientMultipliers, marketPrice: number = 0, ): GradientOrderBlock => { const direction = baseOrder.direction ?? 'sell'; - const today = new Date(); - const startMultiplier = direction === 'buy' ? 0.9 : 1.1; - const endMultiplier = direction === 'buy' ? 0.99 : 1.01; const price = new SafeDecimal(marketPrice); const order = { - _sP_: baseOrder._sP_ ?? price.mul(startMultiplier).toString(), - _eP_: baseOrder._eP_ ?? price.mul(endMultiplier).toString(), - _sD_: baseOrder._sD_ ?? toUnixUTCDay(addDays(today, 1)), - _eD_: baseOrder._eD_ ?? toUnixUTCDay(addDays(today, 21)), + startPrice: baseOrder.startPrice ?? price.mul(multipliers.start).toString(), + endPrice: baseOrder.endPrice ?? price.mul(multipliers.end).toString(), + startDate: baseOrder.startDate ?? defaultGradientStartDate, + endDate: baseOrder.endDate ?? defaultGradientEndDate, budget: baseOrder.budget ?? '', direction: direction, }; @@ -38,34 +84,85 @@ export const defaultGradientOrder = ( }; }; -export const order_SD_ = (timestamp: string) => { +export const orderStartDate = (timestamp: string) => { const date = fromUnixUTC(timestamp); return isToday(date) ? new Date() : startOfDay(date); }; -export const order_ED_ = (timestamp: string) => { +export const orderEndDate = (timestamp: string) => { return endOfDay(fromUnixUTC(timestamp)); }; -type Line = Pick; +type Line = Pick< + GradientOrderBlock, + 'startDate' | 'endDate' | 'startPrice' | 'endPrice' +>; /** Checks if the buy and sell lines overlap, and if the buy line is above the sell line at any point within the overlapping range */ export const isReverseGradientOrders = (buy: Line, sell: Line): boolean => { - // @todo(gradient) + const startX = SafeDecimal.max(buy.startDate, sell.startDate); + const endX = SafeDecimal.min(buy.endDate, sell.endDate); + + if (startX.gt(endX)) return false; // No overlap + + const buystartDate = new SafeDecimal(buy.startDate); + const buyendDate = new SafeDecimal(buy.endDate); + const buyStartPrice = new SafeDecimal(buy.startPrice); + const buyEndPrice = new SafeDecimal(buy.endPrice); + + const sellstartDate = new SafeDecimal(sell.startDate); + const sellendDate = new SafeDecimal(sell.endDate); + const sellStartPrice = new SafeDecimal(sell.startPrice); + const sellEndPrice = new SafeDecimal(sell.endPrice); + + const startXDecimal = new SafeDecimal(startX); + const endXDecimal = new SafeDecimal(endX); + + const buyYStart = buyStartPrice.add( + startXDecimal + .sub(buystartDate) + .mul(buyEndPrice.sub(buyStartPrice)) + .div(buyendDate.sub(buystartDate)), + ); + const sellYStart = sellStartPrice.add( + startXDecimal + .sub(sellstartDate) + .mul(sellEndPrice.sub(sellStartPrice)) + .div(sellendDate.sub(sellstartDate)), + ); + const buyYEnd = buyStartPrice.add( + endXDecimal + .sub(buystartDate) + .mul(buyEndPrice.sub(buyStartPrice)) + .div(buyendDate.sub(buystartDate)), + ); + const sellYEnd = sellStartPrice.add( + endXDecimal + .sub(sellstartDate) + .mul(sellEndPrice.sub(sellStartPrice)) + .div(sellendDate.sub(sellstartDate)), + ); + + if (buyYStart.greaterThan(sellYStart) || buyYEnd.greaterThan(sellYEnd)) { + return true; + } + + if (buyYStart.sub(sellYStart).mul(buyYEnd.sub(sellYEnd)).lessThan(0)) { + return true; + } + return false; }; export const gradientDateWarning = (order: FormGradientOrder) => { - const _sD_ = order_SD_(order._sD_); - const _eD_ = order_ED_(order._eD_); - if (_eD_ < new Date()) return; - if (_sD_ >= new Date()) return; - if (new SafeDecimal(order._sP_).gt(order._eP_)) { - // @todo(gradient) - return ''; + const startDate = orderStartDate(order.startDate); + const endDate = orderEndDate(order.endDate); + if (endDate < new Date()) return; + if (startDate >= new Date()) return; + if (new SafeDecimal(order.startPrice).gt(order.endPrice)) { + return 'Your strategy is set to begin in the past, so the actual starting price will be lower than the specified starting price.'; } - if (new SafeDecimal(order._sP_).lt(order._eP_)) { - // @todo(gradient) - return ''; + if (new SafeDecimal(order.startPrice).lt(order.endPrice)) { + return 'Your strategy is set to begin in the past, so the actual starting price will be higher than the specified starting price'; } }; export const gradientPriceWarning = ( @@ -74,8 +171,18 @@ export const gradientPriceWarning = ( base: Token, marketPrice?: number, ) => { - // @todo(gradient) - return ''; + const startDate = fromUnixUTC(order.startDate); + if (startDate > new Date()) return ''; + if (!marketPrice || !order.marginalPrice) return ''; + if (direction === 'buy') { + if (new SafeDecimal(order.marginalPrice).gt(marketPrice)) { + return `Notice: you offer to buy ${base.symbol} above current market price`; + } + } else { + if (new SafeDecimal(order.marginalPrice).lt(marketPrice)) { + return `Notice: you offer to sell ${base.symbol} below current market price`; + } + } }; export const quickGradientPriceWarning = ( direction: StrategyDirection, @@ -96,8 +203,20 @@ export const quickGradientPriceWarning = ( }; export const gradientDateError = (order: FormGradientOrder) => { - if (new Date() > order_ED_(order._eD_)) { - // @todo(gradient) - return ''; + if (new Date() > orderEndDate(order.endDate)) { + return 'Your order is set in the past and will never be active.'; } }; + +export const gradientDateFormatter = new Intl.DateTimeFormat(undefined, { + month: '2-digit', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, +}); +export const formatGradientDate = (timestamp: number | string) => { + const date = fromUnixUTC(timestamp); + return gradientDateFormatter.format(date); +}; diff --git a/src/components/strategies/common/quick/CreateQuickGradientOrder.tsx b/src/components/strategies/common/quick/CreateQuickGradientOrder.tsx index 9487b537e1..815292f685 100644 --- a/src/components/strategies/common/quick/CreateQuickGradientOrder.tsx +++ b/src/components/strategies/common/quick/CreateQuickGradientOrder.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo, ReactNode, useId, useState, useEffect } from 'react'; +import { FC, useMemo, ReactNode, useId } from 'react'; import { QuickGradientOrderBlock } from '../types'; import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; import { GradientPriceRange } from '../gradient/GradientPriceRange'; @@ -8,109 +8,50 @@ import { SafeDecimal } from 'libs/safedecimal'; import { Warning } from 'components/common/WarningMessageWithIcon'; import { useMarketPrice } from 'hooks/useMarketPrice'; import { quickGradientPriceWarning } from '../gradient/utils'; -import { formatQuickTime } from './utils'; +import { OrderTitle } from '../OrderTitle'; +import { QuickGradientDateRange } from './QuickGradientDateRange'; interface Props { order: QuickGradientOrderBlock; setOrder: (order: Partial) => any; priceWarning?: ReactNode; + action?: ReactNode; } export const CreateQuickGradientOrder: FC = (props) => { const { order, setOrder } = props; const { base, quote } = useStrategyFormCtx(); const { marketPrice } = useMarketPrice({ base, quote }); - const [localDelta, setLocalDelta] = useState(order.deltaTime); const budgetId = useId(); - const endTimeId = useId(); + const titleId = useId(); const budgetToken = order.direction === 'buy' ? quote : base; const balance = useGetTokenBalance(budgetToken); - useEffect(() => { - setLocalDelta(order.deltaTime); - }, [order.deltaTime]); - const insufficientBalance = (() => { if (!balance.data) return; if (new SafeDecimal(balance.data).gte(order.budget || '0')) return; return 'Insufficient balance'; })(); - const dateError = useMemo(() => { - const delta = Number(localDelta); - if (delta < 1) return 'End time should be above 1min'; - if (delta > 60) return 'End time should be below 60min'; - }, [localDelta]); - const priceWarning = useMemo(() => { - if (dateError) return; + if (props.priceWarning) return; return quickGradientPriceWarning(order.direction, order, base, marketPrice); - }, [base, dateError, marketPrice, order]); - - const setDeltaTime = (value: string | number) => { - if (!value) return setLocalDelta(''); - const time = new SafeDecimal(value); - if (time.lt(1) || time.gt(60)) { - setLocalDelta(time.toString()); - } else { - setOrder({ deltaTime: value.toString() }); - setLocalDelta(value.toString()); - } - }; + }, [base, marketPrice, order, props.priceWarning]); return ( -
    -

    - {order.direction} Overview -

    +
    +
    + + {props.action} +

    Duration

    -
    -
    - Start Time - On Execution -
    -
    - - - setDeltaTime(e.currentTarget.value)} - type="number" - min="1" - max="60" - step="1" - autoComplete="off" - /> - min - - ({formatQuickTime(order.deltaTime)}) - - -
    -
    - {dateError && } + setOrder({ deltaTime })} + />

    @@ -119,16 +60,14 @@ export const CreateQuickGradientOrder: FC = (props) => { setOrder({ _sP_ })} - setEnd={(_eP_) => setOrder({ _eP_ })} + start={order.startPrice} + end={order.endPrice} + setStart={(startPrice) => setOrder({ startPrice })} + setEnd={(endPrice) => setOrder({ endPrice })} direction={order.direction} /> {props.priceWarning} - {!props.priceWarning && priceWarning && ( - - )} + {priceWarning && }

    -
    + ); }; diff --git a/src/components/strategies/common/quick/QuickGradientChart.tsx b/src/components/strategies/common/quick/QuickGradientChart.tsx index 65a2eff5e1..95bae8bd17 100644 --- a/src/components/strategies/common/quick/QuickGradientChart.tsx +++ b/src/components/strategies/common/quick/QuickGradientChart.tsx @@ -35,7 +35,7 @@ interface Props { const yDomain = (orders: QuickGradientOrderBlock[], marketPrice?: string) => { if (!marketPrice) return [0, 1000]; const price = Number(marketPrice); - const orderPrices = orders.map((o) => [+o._sP_, +o._eP_]).flat(); + const orderPrices = orders.map((o) => [+o.startPrice, +o.endPrice]).flat(); return [ Math.min(price / 1.5, ...orderPrices), Math.max(price * 1.5, ...orderPrices), @@ -103,7 +103,10 @@ const QuickGradientChartContent: FC = (props) => { @@ -121,7 +124,7 @@ const QuickGradientChartContent: FC = (props) => { y="0" width={dms.marginRight} height={dms.height} - className="fill-background-black" + className="fill-main-800" /> = (props) => { y={dms.boundedHeight} width={dms.width} height={dms.marginBottom} - className="fill-background-black" + className="fill-main-800" /> ; +} + +const minutes = new Array(60).fill(null).map((_, i) => (i + 1).toString()); + +export const QuickGradientDateRange: FC = (props) => { + const { deltaTime, setDeltaTime } = props; + const endTimeId = useId(); + + const [open, setOpen] = useState(false); + + const dateError = useMemo(() => { + const delta = Number(deltaTime); + if (delta < 1) return 'End time should be above 1min'; + if (delta > 60) return 'End time should be below 60min'; + }, [deltaTime]); + + const setDelta = (value: string) => { + setDeltaTime(value); + setOpen(false); + }; + + return ( + <> +
    +
    + Start Time + On Execution +
    + ( + + )} + > + {minutes.map((delta) => ( + + ))} + +
    + {dateError && } + + ); +}; diff --git a/src/components/strategies/common/quick/utils.ts b/src/components/strategies/common/quick/utils.ts index 45a46de1b5..ed42528055 100644 --- a/src/components/strategies/common/quick/utils.ts +++ b/src/components/strategies/common/quick/utils.ts @@ -1,24 +1,28 @@ import { hourFormatter, toUnixUTC } from 'components/simulator/utils'; import { GradientOrderBlock, QuickGradientOrderBlock } from '../types'; import { addMinutes } from 'date-fns'; +import { GradientMultipliers } from '../gradient/utils'; export const defaultQuickGradientOrder = ( baseOrder: Partial, + multiplier: GradientMultipliers, marketPrice: number = 0, ): QuickGradientOrderBlock => { const direction = baseOrder.direction ?? 'sell'; - const startMultiplier = direction === 'buy' ? 0.95 : 1.05; - const endMultiplier = direction === 'buy' ? 0.99 : 1.01; + const startMultiplier = multiplier.start; + const endMultiplier = multiplier.end; + const defaultStartPrice = (marketPrice * startMultiplier).toString(); + const defaultEndPrice = (marketPrice * endMultiplier).toString(); const order: QuickGradientOrderBlock = { - _sP_: baseOrder._sP_ ?? (marketPrice * startMultiplier).toString(), - _eP_: baseOrder._eP_ ?? (marketPrice * endMultiplier).toString(), + startPrice: baseOrder.startPrice ?? defaultStartPrice, + endPrice: baseOrder.endPrice ?? defaultEndPrice, deltaTime: baseOrder.deltaTime ?? '30', budget: baseOrder.budget ?? '', direction: direction, }; return { ...order, - marginalPrice: order._sP_, + marginalPrice: order.startPrice, }; }; @@ -29,8 +33,8 @@ export const quickToGradientOrder = ( const today = new Date(); return { ...baseOrder, - _sD_: toUnixUTC(today), - _eD_: toUnixUTC(addMinutes(today, Number(deltaTime))), + startDate: toUnixUTC(today), + endDate: toUnixUTC(addMinutes(today, Number(deltaTime))), }; }; diff --git a/src/components/strategies/common/types.ts b/src/components/strategies/common/types.ts index 3850d822c9..0b8251c77b 100644 --- a/src/components/strategies/common/types.ts +++ b/src/components/strategies/common/types.ts @@ -1,6 +1,10 @@ import { StrategyDirection, StrategySettings } from 'libs/routing'; import { Token } from 'libs/tokens'; -import { EncodedStrategyBNStr } from '@bancor/carbon-sdk'; +import { + EncodedStrategyBNStr, + EncodedGradientOrder, + RetypeProps, +} from '@bancor/carbon-sdk'; import { SafeDecimal } from 'libs/safedecimal'; import { MarginalPriceOptions } from '@bancor/carbon-sdk/strategy-management'; @@ -39,10 +43,13 @@ export interface FormStaticOrder { marginalPrice?: string | MarginalPriceOptions; } -export interface EditOrders { - buy: FormStaticOrder; - sell: FormStaticOrder; +export interface EditOrders { + buy: T; + sell: T; } +export type AnyEditOrders = + | EditOrders + | EditOrders; export interface OrderBlock extends FormStaticOrder { settings: StrategySettings; @@ -60,19 +67,19 @@ export interface CreateOverlappingOrder extends FormStaticOrder { } export interface GradientOrder { - _sP_: string; - _eP_: string; - _sD_: string; - _eD_: string; + startPrice: string; + endPrice: string; + startDate: string; + endDate: string; budget: string; marginalPrice: string; } export interface FormGradientOrder { - _sP_: string; - _eP_: string; - _sD_: string; - _eD_: string; + startPrice: string; + endPrice: string; + startDate: string; + endDate: string; budget: string; marginalPrice?: string; } @@ -82,8 +89,8 @@ export interface GradientOrderBlock extends FormGradientOrder { } export interface QuickGradientOrderBlock { - _sP_: string; - _eP_: string; + startPrice: string; + endPrice: string; deltaTime: string; budget: string; marginalPrice?: string; @@ -110,14 +117,34 @@ export interface BaseStrategy export type StaticBaseStrategy = BaseStrategy; export type GradientBaseStrategy = BaseStrategy; +export type GradientEncodedOrderBNStr = RetypeProps< + EncodedGradientOrder, + bigint, + string +>; +export interface EncodedGradientStrategyBnStr { + id: string; + token0: string; + token1: string; + order0: GradientEncodedOrderBNStr; + order1: GradientEncodedOrderBNStr; +} + +export type AnyEncodedStrategy = + | EncodedStrategyBNStr + | EncodedGradientStrategyBnStr; + export interface Strategy extends BaseStrategy { - type: T extends StaticOrder ? 'static' : 'gradient'; + type: T extends StaticOrder ? 'regular' : 'gradient'; id: string; idDisplay: string; + createdAt: number; status: StrategyStatus; - encoded?: EncodedStrategyBNStr; owner?: string; + encoded?: T extends StaticOrder + ? EncodedStrategyBNStr + : EncodedGradientStrategyBnStr; } export interface StrategyWithFiat diff --git a/src/components/strategies/common/utils.ts b/src/components/strategies/common/utils.ts index 511d323c34..b3b5d47d23 100644 --- a/src/components/strategies/common/utils.ts +++ b/src/components/strategies/common/utils.ts @@ -9,6 +9,7 @@ import { FormOrder, FormGradientOrder, BuySellOrders, + AnyEncodedStrategy, } from './types'; import { fromUnixUTC } from 'components/simulator/utils'; import { startOfDay, sub } from 'date-fns'; @@ -16,6 +17,7 @@ import { toUnixUTC } from 'components/simulator/utils'; import { ChartPrices } from './d3Chart'; import { getMinMaxPricesByDecimals } from '@bancor/carbon-sdk/strategy-management'; import { geoMean } from 'utils/fullOutcome'; +import { EncodedStrategyBNStr } from '@bancor/carbon-sdk'; export interface StrategyInput { buy: { min: string; max: string }; @@ -26,10 +28,21 @@ export type OrdersInput = | BuySellOrders | BuySellOrders; +const GRADIENT_STRATEGY_TYPE_MASK = 1n << 255n; +export function isGradientStrategyId(id: bigint) { + return (id & GRADIENT_STRATEGY_TYPE_MASK) !== 0n; +} + +export const isStaticEncoded = ( + encoded?: AnyEncodedStrategy, +): encoded is EncodedStrategyBNStr => { + return !!encoded && 'y' in encoded.order0; +}; + export const isGradientStrategy = ( strategy: OrdersInput, ): strategy is BuySellOrders => { - return '_sD_' in strategy.buy; + return 'startDate' in strategy.buy; }; export const isAuctionStrategy = (strategy: BaseStrategy) => { @@ -156,13 +169,12 @@ export const getStrategyType = (strategy: OrdersInput) => { export const isPaused = (strategy: OrdersInput) => { if (isGradientStrategy(strategy)) { - return ( - (isZero(strategy.buy._sP_) && - isZero(strategy.buy._eP_) && - isZero(strategy.sell._sP_) && - isZero(strategy.sell._eP_)) || - isInPast(strategy) - ); + const { buy, sell } = strategy; + const orders = [buy, sell].filter((o) => !isEmptyGradientOrder(o)); + if (!orders.length) return true; + if (orders.every(isOrderInPast)) return true; + if (orders.every(isOrderInFuture)) return true; + return false; } else { return ( isZero(strategy.buy.min) && @@ -179,14 +191,16 @@ export const isNoBudget = (strategy: OrdersInput) => { return !Number(strategy.buy.budget) && !Number(strategy.sell.budget); }; -export const isInPast = (strategy: BuySellOrders) => { - if (!isEmptyGradientOrder(strategy.buy)) { - if (fromUnixUTC(strategy.buy._eD_) < new Date()) return true; - } - if (!isEmptyGradientOrder(strategy.sell)) { - if (fromUnixUTC(strategy.sell._eD_) < new Date()) return true; - } - return false; +export const isOrderInPast = (order: FormGradientOrder) => { + if (isEmptyGradientOrder(order)) return false; + return fromUnixUTC(order.endDate) < new Date(); +}; +export const isOrderInFuture = (order: FormGradientOrder) => { + if (isEmptyGradientOrder(order)) return false; + return fromUnixUTC(order.startDate) > new Date(); +}; +export const isActiveOrder = (order: FormGradientOrder) => { + return !isOrderInFuture(order) && !isOrderInPast(order); }; export const getStrategyStatus = (orders: OrdersInput) => { @@ -204,7 +218,7 @@ export const isStaticOrder = (order: FormOrder): order is FormStaticOrder => { export const isGradientOrder = ( order: FormOrder, ): order is FormGradientOrder => { - return '_sD_' in order; + return 'startDate' in order; }; export const emptyOrder = (): FormStaticOrder => ({ min: '0', @@ -212,17 +226,17 @@ export const emptyOrder = (): FormStaticOrder => ({ budget: '0', }); export const emptyGradientOrder = () => ({ - _sP_: '0', - _eP_: '0', - _sD_: '0', - _eD_: '0', + startPrice: '0', + endPrice: '0', + startDate: '0', + endDate: '0', budget: '0', }); export const isEmptyOrder = (order: FormStaticOrder) => { return !Number(order.min) && !Number(order.max); }; export const isEmptyGradientOrder = (order: FormGradientOrder) => { - return !Number(order._sP_) && !Number(order._eP_); + return !Number(order.startPrice) && !Number(order.endPrice); }; export const isLimitOrder = (order: StaticOrder) => { return order.min === order.max; @@ -271,11 +285,11 @@ export const resetPrice = (price?: string) => { return isZero(price) ? '' : price; }; -export const default_SD_ = (now = new Date()) => +export const defaultStartDate = (now = new Date()) => startOfDay(sub(now, { months: 3 })); -export const default_ED_ = (now = new Date()) => startOfDay(now); -export const defaultStart = (now?: Date) => toUnixUTC(default_SD_(now)); -export const defaultEnd = (now?: Date) => toUnixUTC(default_ED_(now)); +export const defaultEndDate = (now = new Date()) => startOfDay(now); +export const defaultStart = (now?: Date) => toUnixUTC(defaultStartDate(now)); +export const defaultEnd = (now?: Date) => toUnixUTC(defaultEndDate(now)); export const oneYearAgo = (now = new Date()) => { // Add extra days because last day is sometime today sometime yesterday return toUnixUTC(startOfDay(sub(now, { years: 1, days: 2 }))); @@ -292,8 +306,8 @@ export const getBounds = ( if (!order) return; if (!isGradientOrder(order)) return order; return { - min: SafeDecimal.min(order._sP_, order._eP_).toString(), - max: SafeDecimal.max(order._sP_, order._eP_).toString(), + min: SafeDecimal.min(order.startPrice, order.endPrice).toString(), + max: SafeDecimal.max(order.startPrice, order.endPrice).toString(), }; }; const buy = getMinMax(buyOrder); diff --git a/src/components/strategies/create/CreateLayout.tsx b/src/components/strategies/create/CreateLayout.tsx index f0011cc160..804d497e25 100644 --- a/src/components/strategies/create/CreateLayout.tsx +++ b/src/components/strategies/create/CreateLayout.tsx @@ -11,10 +11,12 @@ interface Props { | '/trade/disposable' | '/trade/recurring' | '/trade/overlapping' - | '/trade/custom' + | '/trade/channel' | '/trade/auction' + | '/trade/triangle' | '/trade/quick-auction' - | '/trade/quick-custom'; + | '/trade/quick-channel' + | '/trade/quick-triangle'; children: ReactNode; } diff --git a/src/components/strategies/create/useCreateStrategy.ts b/src/components/strategies/create/useCreateStrategy.ts index 40fbf1b38b..df49163402 100644 --- a/src/components/strategies/create/useCreateStrategy.ts +++ b/src/components/strategies/create/useCreateStrategy.ts @@ -72,7 +72,7 @@ export const useCreateStrategy = (props: Props) => { token_pair: `${base.symbol}/${quote.symbol}`, strategy_base_token: base.symbol, strategy_quote_token: quote.symbol, - strategy_category: 'static', + strategy_category: 'regular', strategy_type: getStrategyType({ buy, sell }), }); await tx.wait(); diff --git a/src/components/strategies/edit/EditBudgetForm.tsx b/src/components/strategies/edit/EditBudgetForm.tsx index b3f612fcfa..18981116de 100644 --- a/src/components/strategies/edit/EditBudgetForm.tsx +++ b/src/components/strategies/edit/EditBudgetForm.tsx @@ -13,10 +13,10 @@ import { useWagmi } from 'libs/wagmi'; import { useEditStrategyCtx } from './EditStrategyContext'; import { useDeleteStrategy } from '../useDeleteStrategy'; import { hasNoBudget } from '../overlapping/utils'; -import { EditOrders } from '../common/types'; +import { EditOrders, FormStaticOrder } from '../common/types'; interface Props { - orders: EditOrders; + orders: EditOrders; editType: EditTypes; hasChanged: boolean; children: ReactNode; diff --git a/src/components/strategies/overview/StrategyTypeIcon.tsx b/src/components/strategies/overview/StrategyTypeIcon.tsx index 3bbdc40108..6068c6e186 100644 --- a/src/components/strategies/overview/StrategyTypeIcon.tsx +++ b/src/components/strategies/overview/StrategyTypeIcon.tsx @@ -8,6 +8,6 @@ interface Props { } export const StrategyTypeIcon = ({ isGradient, className }: Props) => { - if (isGradient) return ; - return ; + if (isGradient) return ; + return ; }; diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx index bf1ab09d25..b74925f127 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockBuySell.tsx @@ -5,12 +5,21 @@ import { TokenLogo } from 'components/common/imager/Imager'; import { Tooltip } from 'components/common/tooltip/Tooltip'; import WarningIcon from 'assets/icons/warning.svg?react'; import { cn, getUsdPrice, prettifyNumber } from 'utils/helpers'; +import { ReactNode } from '@tanstack/react-router'; -export const StrategyBlockBuySell: FC<{ +interface Props { strategy: CartStrategy; isBuy?: boolean; className?: string; -}> = ({ strategy, isBuy = false, className }) => { + children?: ReactNode; +} + +export const StrategyBlockBuySell: FC = ({ + strategy, + isBuy = false, + className, + children, +}) => { const token = isBuy ? strategy.base : strategy.quote; const otherToken = isBuy ? strategy.quote : strategy.base; const order = isBuy ? strategy.buy : strategy.sell; @@ -88,6 +97,7 @@ export const StrategyBlockBuySell: FC<{ > {hasFiatValue ? fiatBudgetValue : '...'}

    + {children} ); }; diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockDates.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockDates.tsx new file mode 100644 index 0000000000..71f82a21fb --- /dev/null +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockDates.tsx @@ -0,0 +1,29 @@ +import { formatGradientDate } from 'components/strategies/common/gradient/utils'; +import { Order } from 'components/strategies/common/types'; +import { isGradientOrder } from 'components/strategies/common/utils'; +import { FC } from 'react'; + +interface Props { + isBuy?: boolean; + order: Order; +} +export const StrategyBlockDates: FC = ({ order, isBuy }) => { + if (!isGradientOrder(order)) return; + const textColor = isBuy ? 'text-buy' : 'text-sell'; + return ( + + + + + + + + + + + +
    Start DateEnd Date
    + {formatGradientDate(order.startDate)} + {formatGradientDate(order.endDate)}
    + ); +}; diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockHeader.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockHeader.tsx index 40f4da6dab..d4a1ec4247 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockHeader.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockHeader.tsx @@ -14,6 +14,7 @@ import { TokensOverlap } from 'components/common/tokensOverlap'; import { PairName } from 'components/common/DisplayPair'; import { isGradientStrategy } from 'components/strategies/common/utils'; import { Link } from '@tanstack/react-router'; +import { StrategyTypeIcon } from '../StrategyTypeIcon'; interface Props { strategy: AnyStrategy; @@ -65,18 +66,17 @@ interface StrategySubtitleProps { isExplorer?: boolean; } export const StrategySubtitle: FC = (props) => { - const { id, status, isExplorer } = props; + const { id, status, isExplorer, isGradient } = props; return (

    {id} - {/* @todo(gradient) */} - {/* + - */} +

    ); diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx index b56359a6bc..909a7011f7 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockInfo.tsx @@ -1,21 +1,26 @@ import { FC } from 'react'; import { StrategyBlockTradeCount } from 'components/strategies/overview/strategyBlock/StrategyBlockTradeCount'; import { StrategyBlockBudget } from 'components/strategies/overview/strategyBlock/StrategyBlockBudget'; -import { cn } from 'utils/helpers'; -import { Order, StrategyWithFiat } from 'components/strategies/common/types'; +import { AnyStrategyWithFiat } from 'components/strategies/common/types'; +import { isGradientStrategy } from 'components/strategies/common/utils'; +import { StrategyCountDown } from './StrategyCountDown'; interface Props { - strategy: StrategyWithFiat; + strategy: AnyStrategyWithFiat; } export const StrategyBlockInfo: FC = ({ strategy }) => { + const isGradient = isGradientStrategy(strategy); return (
    - + {isGradient ? ( + + ) : ( + + )}
    ); diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx index bc13e6f4f8..4f0a0de3b6 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockManage.tsx @@ -203,9 +203,11 @@ export const StrategyBlockManage: FC = (props) => { } // separator - items.push(1); + if (items.length) { + items.push(1); + } - if (strategy.status === 'active') { + if (strategy.status === 'active' && !isGradient) { items.push({ id: 'pauseStrategy', name: 'Pause strategy', diff --git a/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx b/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx index 9c29b90138..2c7d615e6e 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyBlockTradeCount.tsx @@ -1,9 +1,9 @@ -import { Order, StrategyWithFiat } from 'components/strategies/common/types'; +import { AnyStrategyWithFiat } from 'components/strategies/common/types'; import { FC } from 'react'; import { prettifyNumber } from 'utils/helpers'; interface Props { - strategy: StrategyWithFiat; + strategy: AnyStrategyWithFiat; } export const StrategyBlockTradeCount: FC = ({ strategy }) => { diff --git a/src/components/strategies/overview/strategyBlock/StrategyCountDown.tsx b/src/components/strategies/overview/strategyBlock/StrategyCountDown.tsx new file mode 100644 index 0000000000..525f3f1947 --- /dev/null +++ b/src/components/strategies/overview/strategyBlock/StrategyCountDown.tsx @@ -0,0 +1,158 @@ +import { toUnixUTC } from 'components/simulator/utils'; +import { GradientOrder, Strategy } from 'components/strategies/common/types'; +import { isEmptyGradientOrder } from 'components/strategies/common/utils'; +import { QueryKey, useQueryClient } from 'libs/queries'; +import { FC, RefObject, useEffect, useMemo, useRef, useState } from 'react'; + +interface Props { + strategy: Strategy; +} + +const M = 60; +const H = 60 * M; +const D = 24 * H; +const SECOND = 1000; + +const getRemaining = (min: number, max: number, now: number) => { + if (now < min) return 0; + if (now > max) return 0; + return max - now; +}; +const getNow = () => Number(toUnixUTC(new Date())); + +export const StrategyCountDown: FC = ({ strategy }) => { + const cache = useQueryClient(); + const { min, max } = useMemo(() => { + const buy = strategy.buy; + const sell = strategy.sell; + const orders = [buy, sell].filter((o) => !isEmptyGradientOrder(o)); + const starts = orders.map((o) => Number(o.startDate)); + const ends = orders.map((o) => Number(o.endDate)); + return { + min: Math.min(...starts), + max: Math.max(...ends), + }; + }, [strategy.buy, strategy.sell]); + const containerRef = useRef(null); + const [now, setNow] = useState(getNow); + const remaining = useMemo(() => getRemaining(min, max, now), [max, min, now]); + const hasCountdown = remaining > 0; + const wasActive = useRef(hasCountdown); + + // Refresh query when a strategy expires + useEffect(() => { + if (wasActive.current && !hasCountdown) { + wasActive.current = false; + cache.invalidateQueries({ + queryKey: QueryKey.strategyAll(), + }); + } + }, [cache, hasCountdown]); + + useEffect(() => { + let timeoutId: ReturnType | undefined; + + const update = () => { + setNow(getNow()); + const delay = SECOND - (Date.now() % SECOND || SECOND); + timeoutId = setTimeout(update, delay); + }; + + update(); + return () => { + if (timeoutId) clearTimeout(timeoutId); + }; + }, []); + + const textClassName = + 'flex items-center gap-4 text-18 font-medium truncate @xs/strategy:text-24'; + + return ( +
    +

    + Countdown +

    +

    + {now < min && Starting Soon} + {now > max && Expired} + {hasCountdown && ( + + )} +

    +
    + ); +}; + +const pad = (value: number) => value.toString().padStart(2, '0'); +const cancelAnimations = (el: Element | null) => { + el?.getAnimations().forEach((a) => a.cancel()); +}; + +interface CountDownProps { + remaining: number; + container: RefObject; +} +const CountDown: FC = ({ remaining, container }) => { + const days = Math.floor(remaining / D); + const hours = Math.floor((remaining % D) / H); + const minutes = Math.floor((remaining % H) / M); + const seconds = remaining % M; + const belowTwo = useMemo(() => minutes < 2, [minutes]); + + useEffect(() => { + const reduce = matchMedia('(prefers-reduced-motion: reduce)'); + if (reduce.matches) return; + const keyframes = [{ opacity: 1 }, { opacity: 0.2 }, { opacity: 1 }]; + const syncOptions: KeyframeAnimationOptions = { + duration: SECOND, + iterations: Infinity, + easing: 'steps(2, end)', + delay: -(Date.now() % SECOND), + }; + + if (belowTwo) { + const p = container.current; + cancelAnimations(p); + p?.animate(keyframes, syncOptions); + return () => cancelAnimations(p); + } else { + cancelAnimations(container.current); + const colons = container.current?.querySelectorAll('.colon') ?? []; + for (const colon of colons) { + cancelAnimations(colon); + colon.animate(keyframes, syncOptions); + } + return () => colons.forEach(cancelAnimations); + } + }, [container, belowTwo]); + + if (days) { + return ( + <> + {pad(days)}d + + : + + {pad(hours)}h + + : + + {minutes}m + + ); + } else { + return ( + <> + {pad(hours)}h + + : + + {pad(minutes)}m + + : + + {pad(seconds)}s + + ); + } +}; diff --git a/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx b/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx index 189259b34b..013bf5f249 100644 --- a/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx +++ b/src/components/strategies/overview/strategyBlock/StrategyGraph.tsx @@ -1,5 +1,5 @@ import { FC, useId, useMemo } from 'react'; -import { cn, prettifyNumber, sanitizeNumber } from 'utils/helpers'; +import { cn, prettifyNumber, sanitizeNumber, tokenAmount } from 'utils/helpers'; import { FloatTooltip, FloatTooltipContent, @@ -13,6 +13,9 @@ import { isGradientStrategy, isZero, isFullRangeStrategy, + isOrderInPast, + isOrderInFuture, + isEmptyGradientOrder, } from 'components/strategies/common/utils'; import { isOverlappingStrategy } from 'components/strategies/common/utils'; import { getRoundedSpread } from 'components/strategies/overlapping/utils'; @@ -49,7 +52,7 @@ const isSmallRange = (strategy: AnyBaseStrategy) => { const strategyPrices = (() => { if (isGradientStrategy(strategy)) { const { buy, sell } = strategy; - return [buy._sP_, buy._eP_, sell._sP_, sell._eP_]; + return [buy.startPrice, buy.endPrice, sell.startPrice, sell.endPrice]; } else { const { buy, sell } = strategy; return [buy.min, buy.max, sell.min, sell.max]; @@ -88,6 +91,7 @@ export const StrategyGraph: FC = ({ strategy, className }) => { const buyOrder = toMinMax(strategy.buy); const sellOrder = toMinMax(strategy.sell); const staticStrategy = { ...strategy, buy: buyOrder, sell: sellOrder }; + const isGradient = isGradientStrategy(strategy); const buy = { from: Number(sanitizeNumber(buyOrder.min)), @@ -105,7 +109,6 @@ export const StrategyGraph: FC = ({ strategy, className }) => { const sellOrderExists = sell.from !== 0 && sell.to !== 0; const buyOrderIsLimit = buy.from === buy.to; const sellOrderIsLimit = sell.from === sell.to; - const max = Math.max(buy.to, sell.to); const min = buy.from && sell.from @@ -166,6 +169,13 @@ export const StrategyGraph: FC = ({ strategy, className }) => { ]); } }; + const buyArea = () => { + const { marginalPrice, from, to } = buy; + const buyTo = + marginalPrice >= from && marginalPrice < to ? marginalPrice : to; + const points = getBuyPoints(buy.from, buyTo); + return [...points].join(' '); + }; const getSellPoints = (sellFrom: number, sellTo: number) => { if (buyOrderExists) { @@ -197,6 +207,14 @@ export const StrategyGraph: FC = ({ strategy, className }) => { } }; + const sellArea = () => { + const { marginalPrice, from, to } = sell; + const sellFrom = + marginalPrice > from && marginalPrice <= to ? marginalPrice : from; + const points = getSellPoints(sellFrom, sell.to); + return [...points].join(' '); + }; + return ( = ({ strategy, className }) => { /> + + + + + {isGradient && } + @@ -240,15 +264,7 @@ export const StrategyGraph: FC = ({ strategy, className }) => { className={style.buyArea} fill="url(#svg-buy-gradient)" fillOpacity="0.5" - points={Array.from( - getBuyPoints( - buy.from, - buy.marginalPrice >= buy.from && - buy.marginalPrice < buy.to - ? buy.marginalPrice - : buy.to, - ), - ).join(' ')} + points={buyArea()} /> {buy.marginalPrice < buy.to && buy.marginalPrice >= buy.from && ( @@ -302,7 +318,7 @@ export const StrategyGraph: FC = ({ strategy, className }) => { - {isGradientStrategy(strategy) ? ( + {isGradient ? ( ) : ( @@ -322,15 +338,7 @@ export const StrategyGraph: FC = ({ strategy, className }) => { className={style.sellArea} fill="url(#svg-sell-gradient)" fillOpacity="0.5" - points={Array.from( - getSellPoints( - sell.marginalPrice > sell.from && - sell.marginalPrice <= sell.to - ? sell.marginalPrice - : sell.from, - sell.to, - ), - ).join(' ')} + points={sellArea()} /> {sell.marginalPrice <= sell.to && sell.marginalPrice > sell.from && ( @@ -632,10 +640,10 @@ const StaticOrderTooltip: FC> = ({ strategy.buy, strategy.sell, ); - const _sP_ = useMemo(() => { + const startPrice = useMemo(() => { return fullRange ? '0' : prettifyNumber(min, priceOption); }, [fullRange, min, priceOption]); - const _eP_ = useMemo(() => { + const endPrice = useMemo(() => { return fullRange ? '∞' : prettifyNumber(max, priceOption); }, [fullRange, max, priceOption]); const marginalPrice = useMemo( @@ -659,7 +667,7 @@ const StaticOrderTooltip: FC> = ({ Price - {_sP_} {quote.symbol} + {startPrice} {quote.symbol} @@ -673,7 +681,7 @@ const StaticOrderTooltip: FC> = ({ Min Price - {_sP_} {quote.symbol} + {startPrice} {quote.symbol}
    @@ -681,7 +689,7 @@ const StaticOrderTooltip: FC> = ({ Max Price - {_eP_} {quote.symbol} + {endPrice} {quote.symbol}
    {!!spread && ( @@ -724,7 +732,7 @@ const GradientOrderTooltip: FC> = ({ }) => { const location = useLocation(); const order = isBuy ? strategy.buy : strategy.sell; - const { _sD_, _eD_ } = order; + const { startDate, endDate } = order; const smallRange = isSmallRange(strategy); const priceOption = useMemo( @@ -735,22 +743,22 @@ const GradientOrderTooltip: FC> = ({ }), [smallRange], ); - const _sP_ = useMemo(() => { - return prettifyNumber(order._sP_, priceOption); - }, [order._sP_, priceOption]); - const _eP_ = useMemo(() => { - return prettifyNumber(order._eP_, priceOption); - }, [order._eP_, priceOption]); - const marginalPrice = useMemo( - () => prettifyNumber(order.marginalPrice, priceOption), - [order.marginalPrice, priceOption], - ); + const startPrice = useMemo(() => { + return prettifyNumber(order.startPrice, priceOption); + }, [order.startPrice, priceOption]); + const endPrice = useMemo(() => { + return prettifyNumber(order.endPrice, priceOption); + }, [order.endPrice, priceOption]); + const marginalPrice = useMemo(() => { + const price = gradientMarginalPrice(order); + return prettifyNumber(price, priceOption); + }, [order, priceOption]); const { quote, base } = strategy; const color = isBuy ? 'text-buy' : 'text-sell'; - const _sD_Text = - location.pathname.includes('cart') && isToday(fromUnixUTC(_sD_)) + const startDateText = + location.pathname.includes('cart') && isToday(fromUnixUTC(startDate)) ? 'Now' - : fromUnixUTC(_sD_).toLocaleString(); + : fromUnixUTC(startDate).toLocaleString(); return (
    > = ({
    - + - + - + - + @@ -808,3 +820,96 @@ const GradientOrderTooltip: FC> = ({ ); }; + +interface GradientTimeProps { + strategy: BaseStrategy; +} +const timeFormatter = new Intl.DateTimeFormat('en', { + month: 'short', + day: 'numeric', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + hour12: false, +}); +const GradientTime: FC = ({ strategy }) => { + const { base, quote, buy, sell } = strategy; + const buyExist = !isEmptyGradientOrder(buy) && !isOrderInPast(buy); + const sellExist = !isEmptyGradientOrder(sell) && !isOrderInPast(sell); + const buyIsFuture = isOrderInFuture(buy); + const sellIsFuture = isOrderInFuture(sell); + const buyStart = useMemo(() => { + const date = fromUnixUTC(buy.startDate); + return timeFormatter.format(date); + }, [buy.startDate]); + const sellStart = useMemo(() => { + const date = fromUnixUTC(sell.startDate); + return timeFormatter.format(date); + }, [sell.startDate]); + const buyStartPrice = useMemo(() => { + return `${tokenAmount(buy.startPrice, quote)} per 1 ${base.symbol}`; + }, [base.symbol, buy.startPrice, quote]); + const sellStartPrice = useMemo(() => { + return `${tokenAmount(sell.startPrice, quote)} per 1 ${base.symbol}`; + }, [base.symbol, sell.startPrice, quote]); + return ( + <> + {buyExist && buyIsFuture && ( + + + + + +

    Future Auction

    +

    + Buy order will begin on {buyStart} at the price of {buyStartPrice} + . +

    + + Learn more about marginal price + + +
    +
    + )} + {sellExist && sellIsFuture && ( + + + + + +

    Future Auction

    +

    + Sell order will begin on {sellStart} at the price of{' '} + {sellStartPrice}. +

    + + Learn more about marginal price + + +
    +
    + )} + + ); +}; diff --git a/src/components/strategies/useDeleteStrategy.ts b/src/components/strategies/useDeleteStrategy.ts index 590fe63f27..0a54350892 100644 --- a/src/components/strategies/useDeleteStrategy.ts +++ b/src/components/strategies/useDeleteStrategy.ts @@ -5,7 +5,6 @@ import { useState } from 'react'; import { ONE_AND_A_HALF_SECONDS_IN_MS } from 'utils/time'; import { AnyStrategy } from './common/types'; -import { isGradientStrategy } from './common/utils'; export const useDeleteStrategy = () => { const { user } = useWagmi(); @@ -25,11 +24,6 @@ export const useDeleteStrategy = () => { throw new Error('error in delete strategy: missing data '); } - // TODO: support gradient - if (isGradientStrategy(strategy)) { - throw new Error('Cannot pause gradient strategy for now'); - } - deleteMutation.mutate( { id, diff --git a/src/components/strategies/usePauseStrategy.ts b/src/components/strategies/usePauseStrategy.ts index b569219ae0..63e33def76 100644 --- a/src/components/strategies/usePauseStrategy.ts +++ b/src/components/strategies/usePauseStrategy.ts @@ -4,7 +4,6 @@ import { useWagmi } from 'libs/wagmi'; import { useState } from 'react'; import { ONE_AND_A_HALF_SECONDS_IN_MS } from 'utils/time'; import { AnyStrategy } from './common/types'; -import { isGradientStrategy } from './common/utils'; export const usePauseStrategy = () => { const { user } = useWagmi(); @@ -24,11 +23,6 @@ export const usePauseStrategy = () => { throw new Error('error in update strategy: missing data '); } - // TODO: support gradient - if (isGradientStrategy(strategy)) { - throw new Error('Cannot pause gradient strategy for now'); - } - updateMutation.mutate(strategy, { onSuccess: async (tx) => { setIsProcessing(true); diff --git a/src/components/trade/TradeLayout.tsx b/src/components/trade/TradeLayout.tsx index 47e79e8201..74cdfe8e83 100644 --- a/src/components/trade/TradeLayout.tsx +++ b/src/components/trade/TradeLayout.tsx @@ -7,8 +7,6 @@ interface Props { export const TradeLayout: FC = ({ children }) => { return (
    - {/* @todo(gradient) */} - {/* */} {children}
    ); diff --git a/src/components/trade/TradeNav.tsx b/src/components/trade/TradeNav.tsx index 1328839c0e..bc53d760f3 100644 --- a/src/components/trade/TradeNav.tsx +++ b/src/components/trade/TradeNav.tsx @@ -3,132 +3,203 @@ import IconRecurring from 'assets/icons/recurring.svg?react'; import IconOverlapping from 'assets/icons/overlapping.svg?react'; import IconMarket from 'assets/icons/market.svg?react'; import IconRange from 'assets/icons/range.svg?react'; +import IconAuction from 'assets/icons/auction.svg?react'; +import IconChannel from 'assets/icons/channel.svg?react'; import KeyboardArrowDownIcon from 'assets/icons/keyboard_arrow_down.svg?react'; import { Link, useRouterState } from 'libs/routing'; import { DropdownMenu } from 'components/common/dropdownMenu'; import { useBreakpoints } from 'hooks/useBreakpoints'; -import { FC, useMemo } from 'react'; +import { FC, Fragment, useMemo } from 'react'; import { useMenuCtx } from 'components/common/dropdownMenu/utils'; import { cn } from 'utils/helpers'; +import config from 'config'; import style from './TradeNav.module.css'; -type StrategyLink = (typeof types)[number]['strategies'][number]; +type StrategyLink = + (typeof types)[number]['groups'][number]['strategies'][number]; type ActivePage = { type: string; strategy: string; }; +const hasGradient = !!config.addresses.carbon.gradientController; + const types = [ { id: 'essentials', title: 'Essentials', - strategies: [ - { - id: 'swap', - name: 'Swap', - to: '/trade/market' as const, - search: undefined, - icon: , - }, - { - id: 'fullRange', - name: 'Full range', - to: '/trade/overlapping' as const, - search: { preset: 'Infinity' }, - icon: , - }, + groups: [ { - id: 'overlapping', - name: 'Concentrated', - to: '/trade/overlapping' as const, - search: undefined, - icon: , + type: 'regular' as const, + label: '', + strategies: [ + { + id: 'swap', + name: 'Swap', + to: '/trade/market' as const, + search: undefined, + icon: , + isNew: false, + }, + { + id: 'fullRange', + name: 'Full range', + to: '/trade/overlapping' as const, + search: { preset: 'Infinity' }, + icon: , + isNew: false, + }, + { + id: 'overlapping', + name: 'Concentrated', + to: '/trade/overlapping' as const, + search: undefined, + icon: , + isNew: false, + }, + ], }, ], }, { id: 'intermediate', title: 'Intermediate', - strategies: [ + groups: [ { - id: 'limitBuy', - name: 'Limit buy', - to: '/trade/disposable' as const, - search: { settings: 'limit' as const, direction: 'buy' as const }, - icon: , + type: 'regular' as const, + label: 'Price based strategies', + strategies: [ + { + id: 'limitBuy', + name: 'Limit buy', + to: '/trade/disposable' as const, + search: { settings: 'limit' as const, direction: 'buy' as const }, + icon: , + isNew: false, + }, + { + id: 'limitSell', + name: 'Limit sell', + to: '/trade/disposable' as const, + search: { settings: 'limit' as const }, + icon: , + isNew: false, + }, + { + id: 'recurringLimit', + name: 'Recurring limit', + to: '/trade/recurring' as const, + search: { buySettings: 'limit', sellSettings: 'limit' } as const, + icon: , + isNew: false, + }, + ], }, { - id: 'limitSell', - name: 'Limit sell', - to: '/trade/disposable' as const, - search: { settings: 'limit' as const }, - icon: , - }, - { - id: 'recurringLimit', - name: 'Recurring limit', - to: '/trade/recurring' as const, - search: { buySettings: 'limit', sellSettings: 'limit' } as const, - icon: , + type: 'gradient' as const, + label: 'Time based strategies', + strategies: [ + { + id: 'auctionBuy', + name: 'Auction Buy', + to: '/trade/auction' as const, + search: { direction: 'buy' } as const, + icon: , + isNew: true, + }, + { + id: 'auctionSell', + name: 'Auction Sell', + to: '/trade/auction' as const, + search: { direction: 'sell' } as const, + icon: , + isNew: true, + }, + { + id: 'quickAuction', + name: 'Fast auction', + to: '/trade/quick-auction' as const, + search: {} as const, + icon: , + isNew: true, + }, + ], }, ], }, { id: 'advanced', title: 'Advanced', - strategies: [ + groups: [ { - id: 'rangeBuy', - name: 'Range buy', - to: '/trade/disposable' as const, - search: { settings: 'range' as const, direction: 'buy' as const }, - icon: , + type: 'regular' as const, + label: 'Price based strategies', + strategies: [ + { + id: 'rangeBuy', + name: 'Range buy', + to: '/trade/disposable' as const, + search: { settings: 'range' as const, direction: 'buy' as const }, + icon: , + isNew: false, + }, + { + id: 'rangeSell', + name: 'Range sell', + to: '/trade/disposable' as const, + search: { settings: 'range' as const }, + icon: , + isNew: false, + }, + { + id: 'recurringRange', + name: 'Recurring range', + to: '/trade/recurring' as const, + search: { buySettings: 'range', sellSettings: 'range' } as const, + icon: , + isNew: false, + }, + ], }, { - id: 'rangeSell', - name: 'Range sell', - to: '/trade/disposable' as const, - search: { settings: 'range' as const }, - icon: , + type: 'gradient' as const, + label: 'Time based strategies', + strategies: [ + { + id: 'channel', + name: 'Channel', + to: '/trade/channel' as const, + search: {} as const, + icon: , + isNew: true, + }, + { + id: 'quickChannel', + name: 'Fast channel', + to: '/trade/quick-channel' as const, + search: {} as const, + icon: , + isNew: true, + }, + { + id: 'triangle', + name: 'Triangle', + to: '/trade/triangle' as const, + search: {} as const, + icon: , + isNew: true, + }, + { + id: 'quickTriangle', + name: 'Fast triangle', + to: '/trade/quick-triangle' as const, + search: {} as const, + icon: , + isNew: true, + }, + ], }, - { - id: 'recurringRange', - name: 'Recurring range', - to: '/trade/recurring' as const, - search: { buySettings: 'range', sellSettings: 'range' } as const, - icon: , - }, - // { - // label: 'Auction', - // svg: , - // to: '/trade/auction' as const, - // text: '', - // id: 'auction', - // }, - // { - // label: 'Quick Auction', - // svg: , - // to: '/trade/quick-auction' as const, - // text: '', - // testId: 'strategy-gradient-regular', - // id: 'quick-auction', - // }, - // { - // label: 'Custom', - // svg: , - // to: '/trade/custom' as const, - // text: '', - // id: 'custom', - // }, - // { - // label: 'Quick Custom', - // svg: , - // to: '/trade/quick-custom' as const, - // text: '', - // testId: 'strategy-gradient-quick', - // id: 'quick-custom', - // }, ], }, ]; @@ -139,15 +210,18 @@ export const TradeNav = () => { const active = useMemo((): ActivePage | undefined => { for (const type of types) { - for (const strategy of type.strategies) { - if (strategy.to === location.pathname) { - if (!strategy.search) return { type: type.id, strategy: strategy.id }; - const sameSearch = Object.entries(strategy.search).every( - ([key, value]) => { - return (location.search as any)[key] === value; - }, - ); - if (sameSearch) return { type: type.id, strategy: strategy.id }; + for (const group of type.groups) { + for (const strategy of group.strategies) { + if (strategy.to === location.pathname) { + if (!strategy.search) + return { type: type.id, strategy: strategy.id }; + const sameSearch = Object.entries(strategy.search).every( + ([key, value]) => { + return (location.search as any)[key] === value; + }, + ); + if (sameSearch) return { type: type.id, strategy: strategy.id }; + } } } } @@ -164,14 +238,15 @@ export const TradeNav = () => { className="surface flex rounded-full overflow-clip animate-slide-up flex-1 sm:gap-8 2xl:grid 2xl:rounded-2xl tab-list p-4" style={{ animationDelay: '100ms' }} > - {types.map(({ id, title, strategies }) => ( + {types.map(({ id, title, groups }) => ( ( )} > - {strategies.map((strategy) => ( - - ))} + {groups.map(({ type, label, strategies }) => { + if (type === 'gradient' && !hasGradient) return; + return ( + + {label && ( +

    {label}

    + )} + {strategies.map((strategy) => ( + + ))} +
    + ); + })}
    ))} ); }; -const StrategyLink: FC<{ strategy: StrategyLink; selected: boolean }> = ( - props, -) => { +interface Props { + strategy: StrategyLink; + selected: boolean; +} + +const StrategyLink: FC = (props) => { const menu = useMenuCtx(); - const { id, name, to, search, icon } = props.strategy; + const { id, name, to, search, icon, isNew } = props.strategy; return ( = ( onClick={() => menu.setMenuOpen(false)} > {icon} - {name} + {name} + {isNew && ( + + New! + + )} ); }; diff --git a/src/components/trade/TradeType.tsx b/src/components/trade/TradeType.tsx deleted file mode 100644 index 00f81ba663..0000000000 --- a/src/components/trade/TradeType.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { useMemo } from 'react'; -import IconPriceBased from 'assets/icons/price-based.svg?react'; -import HourglassIcon from 'assets/icons/hourglass.svg?react'; -import { Tooltip } from 'components/common/tooltip/Tooltip'; -import { Link, TradeSearch, useRouterState } from 'libs/routing'; -import { DropdownMenu } from 'components/common/dropdownMenu'; -import { gradientTypePages, staticTypePages } from './utils'; -import KeyboardArrowDownIcon from 'assets/icons/keyboard_arrow_down.svg?react'; - -const links = [ - { - label: 'Price Based', - svg: , - to: '/trade/overlapping' as const, - text: '', - testId: 'strategy-type-static', - pages: staticTypePages, - }, - { - label: 'Gradient', - svg: , - to: '/trade/auction' as const, - text: '', - testId: 'strategy-type-gradient', - pages: gradientTypePages, - }, -]; - -export const TradeType = () => { - const { location } = useRouterState(); - const current = location.pathname; - - const selected = useMemo(() => { - return links.find((link) => link.pages.includes(current)); - }, [current]); - - return ( - ( - - )} - > - {links.map((link, i) => ( - ({ - base: search.base, - quote: search.quote, - marketPrice: search.marketPrice, - })} - aria-current={link.pages.includes(current) ? 'page' : 'false'} - data-testid={link.testId} - className="rounded-md hover:surface flex items-center justify-center gap-8 py-8 px-16 text-main-0/60 aria-[current=page]:bg-main-900 aria-[current=page]:text-main-0" - > - {link.svg} - {link.label} - - - ))} - - ); -}; diff --git a/src/components/trade/preview/all/auction-buy.tsx b/src/components/trade/preview/all/auction-buy.tsx new file mode 100644 index 0000000000..75164d9a36 --- /dev/null +++ b/src/components/trade/preview/all/auction-buy.tsx @@ -0,0 +1,13 @@ +import { SVGProps } from 'react'; + +export const AuctionBuyPreview = (props: SVGProps) => { + return ( + + + + + + + + ); +}; diff --git a/src/components/trade/preview/all/auction-sell.tsx b/src/components/trade/preview/all/auction-sell.tsx new file mode 100644 index 0000000000..aa038359b2 --- /dev/null +++ b/src/components/trade/preview/all/auction-sell.tsx @@ -0,0 +1,13 @@ +import { SVGProps } from 'react'; + +export const AuctionSellPreview = (props: SVGProps) => { + return ( + + + + + + + + ); +}; diff --git a/src/components/trade/preview/all/channel.tsx b/src/components/trade/preview/all/channel.tsx new file mode 100644 index 0000000000..fbc558eea1 --- /dev/null +++ b/src/components/trade/preview/all/channel.tsx @@ -0,0 +1,13 @@ +import { SVGProps } from 'react'; + +export const ChannelPreview = (props: SVGProps) => { + return ( + + + + + + + + ); +}; diff --git a/src/components/trade/preview/all/triangle.tsx b/src/components/trade/preview/all/triangle.tsx new file mode 100644 index 0000000000..531d5a37dc --- /dev/null +++ b/src/components/trade/preview/all/triangle.tsx @@ -0,0 +1,16 @@ +import { SVGProps } from 'react'; + +export const TrianglePreview = (props: SVGProps) => { + return ( + + + + + + + + + + + ); +}; diff --git a/src/components/trade/preview/common.tsx b/src/components/trade/preview/common.tsx index b7d51b4040..44987f2408 100644 --- a/src/components/trade/preview/common.tsx +++ b/src/components/trade/preview/common.tsx @@ -103,6 +103,53 @@ export const PreviewCommonStrategyType = () => { + + + + + + + + + + + + + + + + + + + ; @@ -31,24 +29,19 @@ export default function ModalDuplicateStrategy({ const undercutStrategy = () => { if (isGradientStrategy(strategy)) { - // TODO: implement gradient undercut - const directions: StrategyDirection[] = []; - if (!isEmptyGradientOrder(strategy.buy)) directions.push('buy'); - if (!isEmptyGradientOrder(strategy.sell)) directions.push('sell'); navigate({ - to: '/trade/custom', + to: '/trade/triangle', search: { base: strategy.base.address, quote: strategy.quote.address, - directions, - buy_SD_: strategy.buy._sD_, - buy_ED_: strategy.buy._eD_, - buy_SP_: strategy.buy._sP_, - buy_EP_: strategy.buy._eP_, - sell_SD_: strategy.sell._sD_, - sell_ED_: strategy.sell._eD_, - sell_SP_: strategy.sell._sP_, - sell_EP_: strategy.sell._eP_, + buyStartDate: strategy.buy.startDate, + buyEndDate: strategy.buy.endDate, + buyStartPrice: strategy.buy.startPrice, + buyEndPrice: strategy.buy.endPrice, + sellStartDate: strategy.sell.startDate, + sellEndDate: strategy.sell.endDate, + sellStartPrice: strategy.sell.startPrice, + sellEndPrice: strategy.sell.endPrice, }, }); } else if (isOverlappingStrategy(strategy)) { diff --git a/src/libs/queries/extApi/activity.ts b/src/libs/queries/extApi/activity.ts index a9bb9a1ed9..fd7c3aa77b 100644 --- a/src/libs/queries/extApi/activity.ts +++ b/src/libs/queries/extApi/activity.ts @@ -4,15 +4,15 @@ import { Token } from 'libs/tokens'; export interface ActivityStaticOrder { min: string; max: string; - marginal: string; + marginalPrice: string; budget: string; } export interface ActivityGradientOrder { - _sD_: string; - _eD_: string; - _sP_: string; - _eP_: string; - marginal: string; + startDate: string; + endDate: string; + startPrice: string; + endPrice: string; + marginalPrice: string; budget: string; } @@ -28,6 +28,7 @@ interface BaseActivityStrategy { owner: string; buy: Order; sell: Order; + type: 'regular' | 'gradient'; } export type ActivityAction = | 'create' diff --git a/src/libs/queries/extApi/strategy.ts b/src/libs/queries/extApi/strategy.ts index 200f68a4ec..66a01ccba1 100644 --- a/src/libs/queries/extApi/strategy.ts +++ b/src/libs/queries/extApi/strategy.ts @@ -1,13 +1,23 @@ +import { GradientEncodedOrderBNStr } from 'components/strategies/common/types'; + export interface StrategiesSearchParams { page?: number; pageSize?: number; } -export interface StrategyOrderAPI { +export interface StaticOrderAPI { budget: string; min: string; max: string; - marginal: string; + marginalPrice: string; +} +export interface GradientOrderAPI { + budget: string; + startDate: string; + endDate: string; + startPrice: string; + endPrice: string; + marginalPrice: string; } interface EncodedOrderStr { @@ -17,19 +27,28 @@ interface EncodedOrderStr { B: string; } -export interface StrategyAPI { +type EncodedOrder = + Order extends StaticOrderAPI ? EncodedOrderStr : GradientEncodedOrderBNStr; + +export interface StrategyAPI { + type: Order extends GradientOrderAPI ? 'gradient' : 'regular'; id: string; owner: string; base: string; quote: string; - buy: StrategyOrderAPI; - sell: StrategyOrderAPI; + buy: Order; + sell: Order; + createdAt: number; encoded: { - order0: EncodedOrderStr; - order1: EncodedOrderStr; + order0: EncodedOrder; + order1: EncodedOrder; }; } +export type AnyStrategyAPI = + | StrategyAPI + | StrategyAPI; + export interface StrategyPagination { page: number; pageSize: number; @@ -39,6 +58,12 @@ export interface StrategyPagination { } export interface StrategyAPIResult { - strategies: StrategyAPI[]; + strategies: AnyStrategyAPI[]; pagination: StrategyPagination; } + +export const isGradientStrategyAPI = ( + s: AnyStrategyAPI, +): s is StrategyAPI => { + return s.type === 'gradient'; +}; diff --git a/src/libs/queries/sdk/gradient-mock.ts b/src/libs/queries/sdk/gradient-mock.ts deleted file mode 100644 index 6e4e73c8ec..0000000000 --- a/src/libs/queries/sdk/gradient-mock.ts +++ /dev/null @@ -1,20 +0,0 @@ -// @todo(gradient): remove this file when SDK is ready - -export interface SDKGradientStrategy { - id: string; - baseToken: string; - quoteToken: string; - buy_SP_: string; - buy_EP_: string; - buyPriceMarginal: string; - buy_SD_: string; - buy_ED_: string; - buyBudget: string; - sell_SP_: string; - sell_EP_: string; - sellPriceMarginal: string; - sell_SD_: string; - sell_ED_: string; - sellBudget: string; - encoded: any; -} diff --git a/src/libs/queries/sdk/maxSourceAmount.ts b/src/libs/queries/sdk/maxSourceAmount.ts index 476f2b39b2..d1de579d98 100644 --- a/src/libs/queries/sdk/maxSourceAmount.ts +++ b/src/libs/queries/sdk/maxSourceAmount.ts @@ -2,8 +2,9 @@ import { useQuery } from '@tanstack/react-query'; import { QueryKey, useGetAllStrategies } from 'libs/queries'; import { ONE_DAY_IN_MS } from 'utils/time'; import { carbonSDK } from 'libs/sdk'; -import config from 'config'; import { Token } from 'libs/tokens'; +import config from 'config'; +import { isStaticEncoded } from 'components/strategies/common/utils'; export const useGetMaxSource = (source: Token, target: Token) => { const { data: strategies } = useGetAllStrategies({ @@ -17,7 +18,7 @@ export const useGetMaxSource = (source: Token, target: Token) => { sourceToken: source.address, sourceDecimals: source.decimals, targetToken: target.address, - strategies: strategies!.map((s) => s.encoded).filter((e) => !!e), + strategies: strategies!.map((s) => s.encoded).filter(isStaticEncoded), }); }, enabled: !!strategies && !config.ui.useDexAggregator, diff --git a/src/libs/queries/sdk/strategy.ts b/src/libs/queries/sdk/strategy.ts index 6620aaddda..2226f5df9f 100644 --- a/src/libs/queries/sdk/strategy.ts +++ b/src/libs/queries/sdk/strategy.ts @@ -4,7 +4,6 @@ import { useWagmi } from 'libs/wagmi'; import { Token } from 'libs/tokens'; import { QueryKey } from 'libs/queries/queryKey'; import { SafeDecimal } from 'libs/safedecimal'; -import config from 'config'; import { useTokens } from 'hooks/useTokens'; import { EncodedStrategyBNStr, StrategyUpdate } from '@bancor/carbon-sdk'; import { MarginalPriceOptions } from '@bancor/carbon-sdk/strategy-management'; @@ -14,61 +13,86 @@ import { useGetAddressFromEns } from 'libs/queries/chain/ens'; import { AnyStrategy, EditOrders, + FormStaticOrder, StaticOrder, Strategy, } from 'components/strategies/common/types'; import { getStrategyStatus, isGradientStrategy, + isGradientStrategyId, isZero, } from 'components/strategies/common/utils'; -import { StrategyAPI, StrategyOrderAPI } from 'libs/queries/extApi/strategy'; +import { AnyStrategyAPI, StaticOrderAPI } from 'libs/queries/extApi/strategy'; import { carbonApi } from 'services/carbonApi'; import { useMemo } from 'react'; +import config from 'config'; const buildStrategyFromAPI = ( - s: StrategyAPI, + s: AnyStrategyAPI, getTokenById: (id: string) => Token | undefined, -): Strategy | undefined => { +): AnyStrategy | undefined => { const base = getTokenById(s.base); const quote = getTokenById(s.quote); if (!base || !quote) return; - const toOrder = (order: StrategyOrderAPI): StaticOrder => ({ - budget: order.budget, - min: order.min, - max: order.max, - marginalPrice: - 'marginalPrice' in order - ? (order.marginalPrice as string) - : order.marginal, - }); - const buy = toOrder(s.buy); - const sell = toOrder(s.sell); + // Gradient + if (s.type === 'gradient') { + const buy = s.buy; + const sell = s.sell; - return { - type: 'static', - id: s.id, - idDisplay: getLowestBits(s.id), - base, - quote, - buy, - sell, - owner: s.owner, - status: getStrategyStatus({ buy, sell }), - encoded: { + return { + type: 'gradient', id: s.id, - token0: s.base, - token1: s.quote, - order0: s.encoded.order0, - order1: s.encoded.order1, - }, - }; + idDisplay: getLowestBits(s.id), + createdAt: s.createdAt, + base, + quote, + buy, + sell, + owner: s.owner, + status: getStrategyStatus({ buy, sell }), + encoded: { + id: s.id, + token0: s.base, + token1: s.quote, + order0: s.encoded.order0, + order1: s.encoded.order1, + }, + }; + } else { + // TODO: remove after gradient is merged + const toOrder = (order: StaticOrderAPI): StaticOrderAPI => ({ + ...order, + marginalPrice: + 'marginal' in order ? (order.marginal as string) : order.marginalPrice, + }); + const { buy, sell } = s; + return { + type: 'regular', + id: s.id, + idDisplay: getLowestBits(s.id), + createdAt: s.createdAt, + base, + quote, + buy: toOrder(buy), + sell: toOrder(sell), + owner: s.owner, + status: getStrategyStatus({ buy, sell }), + encoded: { + id: s.id, + token0: s.base, + token1: s.quote, + order0: s.encoded.order0, + order1: s.encoded.order1, + }, + }; + } }; // READ const buildAPIStrategiesHelper = ( - strategies: StrategyAPI[], + strategies: AnyStrategyAPI[], getTokenById: (id: string) => Token | undefined, ) => { return strategies @@ -76,20 +100,17 @@ const buildAPIStrategiesHelper = ( .filter((strategy): strategy is Strategy => !!strategy); }; -const fetchAllStrategiesFromApi = async ( - getTokenById: (id: string) => Token | undefined, -) => { - const response = await carbonApi.getStrategies({ pageSize: 0 }); - return buildAPIStrategiesHelper(response.strategies, getTokenById); -}; - /** We need to add options to disable because we want to use different hooks for explorer */ export const useGetAllStrategies = (options: { enabled: boolean }) => { const { isPending, getTokenById } = useTokens(); return useQuery({ queryKey: QueryKey.strategyAll(), - queryFn: () => fetchAllStrategiesFromApi(getTokenById), + queryFn: async () => { + const response = await carbonApi.getStrategies({ pageSize: 0 }); + return buildAPIStrategiesHelper(response.strategies, getTokenById); + }, + refetchInterval: 30_000, enabled: options?.enabled && !isPending, retry: false, }); @@ -143,25 +164,43 @@ const reverseStrategy = (strategy: AnyStrategy): AnyStrategy => { if (isZero(value)) return '0'; return new SafeDecimal(1).div(value).toString(); }; - // @todo(gradient): implement reverse for gradient strategies - if (isGradientStrategy(strategy)) return strategy; - return { - ...strategy, - base: strategy.quote, - quote: strategy.base, - buy: { - min: invert(strategy.sell.max), - max: invert(strategy.sell.min), - marginalPrice: invert(strategy.sell.marginalPrice), - budget: strategy.sell.budget, - }, - sell: { - min: invert(strategy.buy.max), - max: invert(strategy.buy.min), - marginalPrice: invert(strategy.buy.marginalPrice), - budget: strategy.buy.budget, - }, - }; + if (isGradientStrategy(strategy)) { + return { + ...strategy, + base: strategy.quote, + quote: strategy.base, + buy: { + ...strategy.sell, + startPrice: invert(strategy.sell.startPrice), + endPrice: invert(strategy.sell.endPrice), + marginalPrice: invert(strategy.sell.marginalPrice), + }, + sell: { + ...strategy.buy, + startPrice: invert(strategy.buy.startPrice), + endPrice: invert(strategy.buy.endPrice), + marginalPrice: invert(strategy.buy.marginalPrice), + }, + }; + } else { + return { + ...strategy, + base: strategy.quote, + quote: strategy.base, + buy: { + min: invert(strategy.sell.max), + max: invert(strategy.sell.min), + marginalPrice: invert(strategy.sell.marginalPrice), + budget: strategy.sell.budget, + }, + sell: { + min: invert(strategy.buy.max), + max: invert(strategy.buy.min), + marginalPrice: invert(strategy.buy.marginalPrice), + budget: strategy.buy.budget, + }, + }; + } }; const normalizeStrategy = ( base: string, @@ -210,22 +249,6 @@ export const useTokenStrategies = (token?: string) => { // WRITE -const getFieldsToUpdate = (orders: EditOrders, strategy: AnyStrategy) => { - const { buy, sell } = orders; - const fields: Partial = {}; - if (isGradientStrategy(strategy)) { - // @todo(gradient) implement edit fields for gradient - } else { - if (buy.min !== strategy.buy.min) fields.buyPriceLow = buy.min; - if (buy.max !== strategy.buy.max) fields.buyPriceHigh = buy.max; - if (sell.min !== strategy.sell.min) fields.sellPriceLow = sell.min; - if (sell.max !== strategy.sell.max) fields.sellPriceHigh = sell.max; - } - if (buy.budget !== strategy.buy.budget) fields.buyBudget = buy.budget; - if (sell.budget !== strategy.sell.budget) fields.sellBudget = sell.budget; - return fields as StrategyUpdate; -}; - export interface CreateStrategyParams { base: string; quote: string; @@ -291,15 +314,27 @@ export const useUpdateStrategyQuery = (strategy: AnyStrategy) => { const { sendTransaction } = useWagmi(); return useMutation({ - mutationFn: async (orders: EditOrders) => { - const updates = getFieldsToUpdate(orders, strategy); + mutationFn: async (orders: EditOrders) => { if (!strategy.encoded) { throw new Error('No encoded found on the strategy'); } + if (strategy.type === 'gradient') { + throw new Error('Strategy is gradient, use dedicated function for it'); + } + const { buy, sell } = orders; + const fields: Partial = {}; + + if (buy.min !== strategy.buy.min) fields.buyPriceLow = buy.min; + if (buy.max !== strategy.buy.max) fields.buyPriceHigh = buy.max; + if (sell.min !== strategy.sell.min) fields.sellPriceLow = sell.min; + if (sell.max !== strategy.sell.max) fields.sellPriceHigh = sell.max; + if (buy.budget !== strategy.buy.budget) fields.buyBudget = buy.budget; + if (sell.budget !== strategy.sell.budget) fields.sellBudget = sell.budget; + const unsignedTx = await carbonSDK.updateStrategy( strategy.id, strategy.encoded, - updates, + fields as StrategyUpdate, orders.buy.marginalPrice, orders.sell.marginalPrice, ); @@ -316,7 +351,7 @@ export const useUpdateStrategyQuery = (strategy: AnyStrategy) => { rawAmount: getRawAmount( strategy.base, strategy.sell.budget, - updates.sellBudget, + fields.sellBudget, ), }, { @@ -324,7 +359,7 @@ export const useUpdateStrategyQuery = (strategy: AnyStrategy) => { rawAmount: getRawAmount( strategy.quote, strategy.buy.budget, - updates.buyBudget, + fields.buyBudget, ), }, ], @@ -340,20 +375,35 @@ export const usePauseStrategyQuery = () => { return useMutation({ mutationFn: async (strategy: AnyStrategy) => { - if (!strategy.encoded) + if (!strategy.encoded) { throw new Error('No encoded found on the strategy'); - const unsignedTx = await carbonSDK.updateStrategy( - strategy.id, - strategy.encoded, - { - buyPriceLow: '0', - buyPriceHigh: '0', - sellPriceLow: '0', - sellPriceHigh: '0', - }, - ); - - return sendTransaction(unsignedTx); + } + if (strategy.type === 'regular') { + const unsignedTx = await carbonSDK.updateStrategy( + strategy.id, + strategy.encoded, + { + buyPriceLow: '0', + buyPriceHigh: '0', + sellPriceLow: '0', + sellPriceHigh: '0', + }, + ); + return sendTransaction(unsignedTx); + } else { + console.log(strategy.encoded); + const unsignedTx = await carbonSDK.updateGradientStrategy( + strategy.id, + strategy.encoded, + { + buyInitialPrice: '0', + buyFinalPrice: '0', + sellInitialPrice: '0', + sellFinalPrice: '0', + }, + ); + return sendTransaction(unsignedTx); + } }, }); }; @@ -363,7 +413,9 @@ export const useDeleteStrategyQuery = () => { return useMutation({ mutationFn: async ({ id }: DeleteStrategyParams) => { - const unsignedTx = await carbonSDK.deleteStrategy(id); + const unsignedTx = isGradientStrategyId(BigInt(id)) + ? await carbonSDK.deleteGradientStrategy(id) + : await carbonSDK.deleteStrategy(id); return sendTransaction(unsignedTx); }, diff --git a/src/libs/queries/sdk/trade.ts b/src/libs/queries/sdk/trade.ts index 2e6ca984f9..fbd23ef951 100644 --- a/src/libs/queries/sdk/trade.ts +++ b/src/libs/queries/sdk/trade.ts @@ -14,6 +14,7 @@ import { useGetApprovalTokens } from 'hooks/useApproval'; import { useBatchTransaction } from 'libs/wagmi/batch-transaction'; import config from 'config'; import { useCarbonController } from 'hooks/useContract'; +import { isStaticEncoded } from 'components/strategies/common/utils'; interface GetTradeDataResult { tradeActions: TradeActionBNStr[]; @@ -256,8 +257,8 @@ export const useSDKTradeData = ({ ); return carbonSDK.getTradeData({ amount: input, - strategies: strategies!.map((s) => s.encoded).filter((e) => !!e), - tradeByTargetAmount: !isTradeBySource, + strategies: strategies!.map((s) => s.encoded).filter(isStaticEncoded), + isTradeByTarget: !isTradeBySource, sourceToken: sourceToken.address, sourceDecimals: sourceToken.decimals, targetToken: targetToken.address, diff --git a/src/libs/queries/sdk/tradeActions.ts b/src/libs/queries/sdk/tradeActions.ts index 72f2b28661..31b13fab89 100644 --- a/src/libs/queries/sdk/tradeActions.ts +++ b/src/libs/queries/sdk/tradeActions.ts @@ -28,7 +28,7 @@ export const useGetTradeActionsQuery = (props: Props) => { target.address, ); return carbonSDK.getTradeDataFromActions({ - tradeByTargetAmount: !isTradeBySource, + isTradeByTarget: !isTradeBySource, actionsWei, sourceDecimals: source.decimals, targetDecimals: target.decimals, diff --git a/src/libs/queries/sdk/tradeLiquidity.ts b/src/libs/queries/sdk/tradeLiquidity.ts index 33449dbaf9..fd9cfb04a3 100644 --- a/src/libs/queries/sdk/tradeLiquidity.ts +++ b/src/libs/queries/sdk/tradeLiquidity.ts @@ -2,8 +2,9 @@ import { useQuery } from '@tanstack/react-query'; import { QueryKey, useGetAllStrategies } from 'libs/queries'; import { ONE_DAY_IN_MS } from 'utils/time'; import { carbonSDK } from 'libs/sdk'; -import config from 'config'; import { Token } from 'libs/tokens'; +import { isStaticEncoded } from 'components/strategies/common/utils'; +import config from 'config'; export const useGetTradeLiquidity = (source: Token, target: Token) => { const { data: strategies } = useGetAllStrategies({ @@ -16,7 +17,7 @@ export const useGetTradeLiquidity = (source: Token, target: Token) => { sourceToken: source.address, targetToken: target.address, targetDecimals: target.decimals, - strategies: strategies!.map((s) => s.encoded).filter((e) => !!e), + strategies: strategies!.map((s) => s.encoded).filter(isStaticEncoded), }); }, enabled: !!strategies && !config.ui.useDexAggregator, diff --git a/src/libs/routing/routes/explorer.ts b/src/libs/routing/routes/explorer.ts index 16891d4e04..93c14c0c40 100644 --- a/src/libs/routing/routes/explorer.ts +++ b/src/libs/routing/routes/explorer.ts @@ -10,7 +10,8 @@ import { searchValidator, validPairFilter, validPairSort, - validStrategyFilter, + validStrategyFilterStatus, + validStrategyFilterType, validStrategySort, } from '../utils'; import { ExplorerPairs } from 'pages/explorer/pairs'; @@ -55,10 +56,17 @@ export const explorerPortfolioPage = createRoute({ getParentRoute: () => explorerLayout, path: 'strategies', component: ExplorerStrategies, + beforeLoad: ({ search }) => { + if (search.filter) search.status = search.filter; + delete search.filter; + }, validateSearch: searchValidator({ layout: v.optional(v.picklist(['grid', 'table'])), - filter: validStrategyFilter, + status: validStrategyFilterStatus, + type: validStrategyFilterType, sort: validStrategySort, + // @deprecated: use status instead + filter: validStrategyFilterStatus, }), }); diff --git a/src/libs/routing/routes/portfolio.ts b/src/libs/routing/routes/portfolio.ts index 0078344b10..acce99e29f 100644 --- a/src/libs/routing/routes/portfolio.ts +++ b/src/libs/routing/routes/portfolio.ts @@ -10,7 +10,8 @@ import { searchValidator, validPairFilter, validPairSort, - validStrategyFilter, + validStrategyFilterStatus, + validStrategyFilterType, validStrategySort, validString, } from '../utils'; @@ -52,10 +53,17 @@ export const portfolioStrategiesPage = createRoute({ getParentRoute: () => portfolioLayout, path: 'strategies', component: PortfolioStrategiesPage, + beforeLoad: ({ search }) => { + if (search.filter) search.status = search.filter; + delete search.filter; + }, validateSearch: searchValidator({ layout: v.optional(v.picklist(['grid', 'table'])), - filter: validStrategyFilter, + status: validStrategyFilterStatus, + type: validStrategyFilterType, sort: validStrategySort, + // @deprecated: use status instead + filter: validStrategyFilterStatus, }), }); diff --git a/src/libs/routing/routes/trade.ts b/src/libs/routing/routes/trade.ts index feeaece813..abaf6ce87f 100644 --- a/src/libs/routing/routes/trade.ts +++ b/src/libs/routing/routes/trade.ts @@ -1,4 +1,4 @@ -import { createRoute, redirect } from '@tanstack/react-router'; +import { AnyRoute, createRoute, redirect } from '@tanstack/react-router'; import { rootRoute } from 'libs/routing/routes/root'; import { searchValidator, @@ -17,7 +17,15 @@ import { TradeRecurring } from 'pages/trade/recurring'; import { TradeOverlapping } from 'pages/trade/overlapping'; import { MarginalPriceOptions } from '@bancor/carbon-sdk/strategy-management'; import { defaultSpread } from 'components/strategies/overlapping/utils'; +import { TradeAuction } from 'pages/trade/auction'; +import { TradeQuickAuction } from 'pages/trade/quick-auction'; +import { addMonths, startOfDay, subWeeks } from 'date-fns'; +import { toUnixUTC } from 'components/simulator/utils'; +import { TradeChannel } from 'pages/trade/channel'; +import { TradeQuickChannel } from 'pages/trade/quick-channel'; import * as v from 'valibot'; +import { TradeTriangle } from 'pages/trade/triangle'; +import { TradeQuickTriangle } from 'pages/trade/quick-triangle'; // TRADE TYPE export type StrategyType = @@ -56,6 +64,8 @@ export interface TradeRecurringSearch extends TradeSearch { sellSettings?: StrategySettings; } +type GetSearchParams = T['types']['searchSchema']; + // TRADE OVERLAPPING export type TradeOverlappingSearch = Partial< (typeof overlappingPage)['types']['searchSchema'] @@ -70,14 +80,7 @@ export interface TradeMarketSearch extends TradeSearch { } // TRADE AUCTION -export interface TradeAuctionSearch extends TradeSearch { - direction?: StrategyDirection; - start?: string; - end?: string; - min?: string; - max?: string; - budget?: string; -} +export type TradeAuctionSearch = GetSearchParams; // ROUTES export interface TradeSearch { @@ -178,39 +181,75 @@ const overlappingPage = createRoute({ }), }); +const setGradientChartDates = (search: { + chartStart?: string; + chartEnd?: string; +}) => { + if (!search.chartStart) { + const date = startOfDay(subWeeks(new Date(), 7)); + search.chartStart = toUnixUTC(date).toString(); + } + if (!search.chartEnd) { + const date = startOfDay(addMonths(new Date(), 2)); + search.chartEnd = toUnixUTC(date).toString(); + } +}; + const auctionPage = createRoute({ getParentRoute: () => tradePage, path: '/auction', - // @todo(gradient) - component: () => null, - // component: TradeAuction, + component: TradeAuction, + beforeLoad: ({ search }) => { + setGradientChartDates(search); + }, validateSearch: searchValidator({ direction: v.optional(validDirection), - _sP_: v.optional(validInputNumber), - _eP_: v.optional(validInputNumber), - _sD_: v.optional(validNumber), - _eD_: v.optional(validNumber), + startPrice: v.optional(validInputNumber), + endPrice: v.optional(validInputNumber), + startDate: v.optional(validNumber), + endDate: v.optional(validNumber), budget: v.optional(validInputNumber), }), }); -const customPage = createRoute({ +export type ChannelSearch = (typeof channelPage)['types']['searchSchema'] & + TradeSearch; +const channelPage = createRoute({ + getParentRoute: () => tradePage, + path: '/channel', + component: TradeChannel, + beforeLoad: ({ search }) => { + setGradientChartDates(search); + }, + validateSearch: searchValidator({ + deltaPrice: v.optional(validInputNumber), + deltaType: v.optional(v.picklist(['percent', 'token'])), + buyBudget: v.optional(validInputNumber), + sellStartPrice: v.optional(validInputNumber), + sellEndPrice: v.optional(validInputNumber), + sellStartDate: v.optional(validNumber), + sellEndDate: v.optional(validNumber), + sellBudget: v.optional(validInputNumber), + }), +}); + +const trianglePage = createRoute({ getParentRoute: () => tradePage, - path: '/custom', - // @todo(gradient) - component: () => null, - // component: TradeCustom, + path: '/triangle', + component: TradeTriangle, + beforeLoad: ({ search }) => { + setGradientChartDates(search); + }, validateSearch: searchValidator({ - directions: v.optional(v.array(validDirection)), - buy_SP_: v.optional(validInputNumber), - buy_EP_: v.optional(validInputNumber), - buy_SD_: v.optional(validNumber), - buy_ED_: v.optional(validNumber), + buyStartPrice: v.optional(validInputNumber), + buyEndPrice: v.optional(validInputNumber), + buyStartDate: v.optional(validNumber), + buyEndDate: v.optional(validNumber), buyBudget: v.optional(validInputNumber), - sell_SP_: v.optional(validInputNumber), - sell_EP_: v.optional(validInputNumber), - sell_SD_: v.optional(validNumber), - sell_ED_: v.optional(validNumber), + sellStartPrice: v.optional(validInputNumber), + sellEndPrice: v.optional(validInputNumber), + sellStartDate: v.optional(validNumber), + sellEndDate: v.optional(validNumber), sellBudget: v.optional(validInputNumber), }), }); @@ -218,32 +257,43 @@ const customPage = createRoute({ const quickAuctionPage = createRoute({ getParentRoute: () => tradePage, path: '/quick-auction', - // @todo(gradient) - component: () => null, - // component: TradeQuickAuction, + component: TradeQuickAuction, validateSearch: searchValidator({ direction: v.optional(validDirection), - _sP_: v.optional(validInputNumber), - _eP_: v.optional(validInputNumber), + startPrice: v.optional(validInputNumber), + endPrice: v.optional(validInputNumber), deltaTime: v.optional(validInputNumber), budget: v.optional(validInputNumber), }), }); -const quickCustomPage = createRoute({ +export type QuickChannelSearch = + (typeof quickChannelPage)['types']['searchSchema'] & TradeSearch; +const quickChannelPage = createRoute({ + getParentRoute: () => tradePage, + path: '/quick-channel', + component: TradeQuickChannel, + validateSearch: searchValidator({ + deltaPrice: v.optional(validInputNumber), + deltaType: v.optional(v.picklist(['percent', 'token'])), + buyBudget: v.optional(validInputNumber), + sellStartPrice: v.optional(validInputNumber), + sellEndPrice: v.optional(validInputNumber), + sellBudget: v.optional(validInputNumber), + sellDeltaTime: v.optional(validInputNumber), + }), +}); +const quickTrianglePage = createRoute({ getParentRoute: () => tradePage, - path: '/quick-custom', - // @todo(gradient) - component: () => null, - // component: TradeQuickCustom, + path: '/quick-triangle', + component: TradeQuickTriangle, validateSearch: searchValidator({ - directions: v.optional(v.array(validDirection)), - buy_SP_: v.optional(validInputNumber), - buy_EP_: v.optional(validInputNumber), + buyStartPrice: v.optional(validInputNumber), + buyEndPrice: v.optional(validInputNumber), buyDeltaTime: v.optional(validNumber), buyBudget: v.optional(validInputNumber), - sell_SP_: v.optional(validInputNumber), - sell_EP_: v.optional(validInputNumber), + sellStartPrice: v.optional(validInputNumber), + sellEndPrice: v.optional(validInputNumber), sellDeltaTime: v.optional(validNumber), sellBudget: v.optional(validInputNumber), }), @@ -255,7 +305,9 @@ export default tradePage.addChildren([ recurringPage, overlappingPage, auctionPage, - customPage, + channelPage, + trianglePage, quickAuctionPage, - quickCustomPage, + quickChannelPage, + quickTrianglePage, ]); diff --git a/src/libs/routing/utils.ts b/src/libs/routing/utils.ts index 5e3ce1ed54..0f8ca4f5af 100644 --- a/src/libs/routing/utils.ts +++ b/src/libs/routing/utils.ts @@ -71,9 +71,12 @@ export const validPairSort = v.optional( 'strategyAmount', ]), ); -export const validStrategyFilter = v.optional( +export const validStrategyFilterStatus = v.optional( v.picklist(['all', 'active', 'inactive']), ); +export const validStrategyFilterType = v.optional( + v.picklist(['all', 'gradient', 'regular']), +); export const validStrategySort = v.optional( v.picklist([ 'recent', diff --git a/src/libs/sdk/index.ts b/src/libs/sdk/index.ts index fd48928aa8..4cdb9ac6a7 100644 --- a/src/libs/sdk/index.ts +++ b/src/libs/sdk/index.ts @@ -22,6 +22,8 @@ const contractsConfig: ContractsConfig = { voucherAddress: config.addresses.carbon.voucher, carbonBatcherAddress: config.addresses.carbon.batcher, multiCallAddress: config.utils?.multicall3?.address, + gradientControllerAddress: config.addresses.carbon.gradientController, + carbonRouterAddress: config.addresses.carbon.router, }; const defaultCacheTTL = config.sdk.cacheTTL ?? ONE_HOUR_IN_MS; diff --git a/src/libs/testing-library/utils/mock.ts b/src/libs/testing-library/utils/mock.ts index 1869d78812..42a6ea36dc 100644 --- a/src/libs/testing-library/utils/mock.ts +++ b/src/libs/testing-library/utils/mock.ts @@ -146,9 +146,10 @@ export interface MockStrategyParams { } export const mockStrategy = (params: MockStrategyParams): Strategy => ({ - type: 'static', + type: 'regular', id: '1', idDisplay: '1', + createdAt: Date.now() / 1000, base: tokenList[params.base], quote: tokenList[params.quote], status: 'active', diff --git a/src/pages/cart/index.tsx b/src/pages/cart/index.tsx index d400c9396c..b0e65cf16b 100644 --- a/src/pages/cart/index.tsx +++ b/src/pages/cart/index.tsx @@ -18,8 +18,14 @@ import { useRestrictedCountry } from 'hooks/useRestrictedCountry'; import { useBatchTransaction } from 'libs/wagmi/batch-transaction'; import { TransactionRequest, parseUnits } from 'ethers'; import config from 'config'; +import { createGradientStrategyParams } from 'components/strategies/common/gradient/utils.sdk'; +import { + BatchCreateBuySellStrategy, + BatchCreateGradientStrategy, +} from '@bancor/carbon-sdk/strategy-management'; const batcher = config.addresses.carbon.batcher; +const router = config.addresses.carbon.router; const getApproveTokens = (strategies: AnyCartStrategy[]) => { if (!batcher) throw new Error('Batcher address not provided'); const tokens: Record = {}; @@ -96,50 +102,130 @@ export const CartPage = () => { const getRawAmount = (token: Token, amount: string) => { return parseUnits(amount, token.decimals).toString(); }; + const totalAssets = () => { + const amounts: Record = {}; + for (const strategy of strategies) { + const { base, quote, buy, sell } = strategy; + const baseToken = strategy.base.address; + const quoteToken = strategy.quote.address; + + amounts[baseToken] ||= BigInt(0); + const sellAmount = parseUnits(sell.budget, base.decimals); + amounts[baseToken] = amounts[baseToken] + sellAmount; + + amounts[quoteToken] ||= BigInt(0); + const buyAmount = parseUnits(buy.budget, quote.decimals); + amounts[quoteToken] = amounts[quoteToken] + buyAmount; + } + return Object.entries(amounts).map(([address, amount]) => ({ + address, + rawAmount: amount.toString(), + })); + }; const tokens = new Set(); const txs: TransactionRequest[] = []; if (canBatch) { for (const strategy of strategies) { - if (isGradientStrategy(strategy)) continue; - const { base, quote, buy, sell } = strategy; - const unsignedTx = await carbonSDK.createBuySellStrategy( - base.address, - quote.address, - buy.min, - buy.marginalPrice || buy.max, - buy.max, - buy.budget, - sell.min, - sell.marginalPrice || sell.min, - sell.max, - sell.budget, - ); - unsignedTx.customData = { - spender: config.addresses.carbon.carbonController, - assets: [ - { - address: base.address, - rawAmount: getRawAmount(base, sell.budget), - }, - { - address: quote.address, - rawAmount: getRawAmount(quote, buy.budget), - }, - ], - }; - txs.push(unsignedTx); + const { base, quote } = strategy; + const assets = [ + { + address: base.address, + rawAmount: getRawAmount(base, strategy.sell.budget), + }, + { + address: quote.address, + rawAmount: getRawAmount(quote, strategy.buy.budget), + }, + ]; + if (isGradientStrategy(strategy)) { + const params = createGradientStrategyParams(strategy); + const unsignedTx = await carbonSDK.createGradientStrategy( + ...params, + ); + unsignedTx.customData = { + spender: config.addresses.carbon.carbonController, + assets: assets, + }; + txs.push(unsignedTx); + } else { + const { base, quote, buy, sell } = strategy; + const unsignedTx = await carbonSDK.createBuySellStrategy( + base.address, + quote.address, + buy.min, + buy.marginalPrice || buy.max, + buy.max, + buy.budget, + sell.min, + sell.marginalPrice || sell.min, + sell.max, + sell.budget, + ); + unsignedTx.customData = { + spender: config.addresses.carbon.carbonController, + assets: assets, + }; + txs.push(unsignedTx); + } tokens.add(base.address); tokens.add(quote.address); } + } else if (router) { + const regular: BatchCreateBuySellStrategy[] = []; + const gradient: BatchCreateGradientStrategy[] = []; + for (const strategy of strategies) { + if (isGradientStrategy(strategy)) { + const params = createGradientStrategyParams(strategy); + gradient.push({ + baseToken: params[0], + quoteToken: params[1], + buyInitialPrice: params[2], + buyFinalPrice: params[3], + buyBudget: params[4], + buyTradingStartTime: params[5], + buyTradingEndTime: params[6], + buyGradientType: params[7], + sellInitialPrice: params[8], + sellFinalPrice: params[9], + sellBudget: params[10], + sellTradingStartTime: params[11], + sellTradingEndTime: params[12], + sellGradientType: params[13], + }); + } else { + const { base, quote, buy, sell } = strategy; + regular.push({ + baseToken: base.address, + quoteToken: quote.address, + buyPriceLow: buy.min, + buyPriceMarginal: buy.marginalPrice || buy.max, + buyPriceHigh: buy.max, + buyBudget: buy.budget, + sellPriceLow: sell.min, + sellPriceMarginal: sell.marginalPrice || sell.min, + sellPriceHigh: sell.max, + sellBudget: sell.budget, + }); + } + tokens.add(strategy.base.address); + tokens.add(strategy.quote.address); + } + const unsignedTx = await carbonSDK.routerCreateStrategies( + regular, + gradient, + { gasLimit: 10000000000000n }, + ); + unsignedTx.customData = { + spender: router, + assets: totalAssets(), + }; + txs.push(unsignedTx); } else { - // TODO: support gradient - const params: Parameters< - typeof carbonSDK.batchCreateBuySellStrategies - >[0] = []; + const regular: BatchCreateBuySellStrategy[] = []; for (const strategy of strategies) { if (isGradientStrategy(strategy)) continue; const { base, quote, buy, sell } = strategy; - params.push({ + regular.push({ baseToken: base.address, quoteToken: quote.address, buyPriceLow: buy.min, @@ -155,36 +241,14 @@ export const CartPage = () => { tokens.add(quote.address); } const unsignedTx = - await carbonSDK.batchCreateBuySellStrategies(params); - - const amounts: Record = {}; - for (const strategy of strategies) { - const base = strategy.base.address; - const quote = strategy.quote.address; - amounts[base] ||= BigInt(0); - const sellAmount = parseUnits( - strategy.sell.budget, - strategy.base.decimals, - ); - amounts[base] = amounts[base] + sellAmount; + await carbonSDK.batchCreateBuySellStrategies(regular); - amounts[quote] ||= BigInt(0); - const buyAmount = parseUnits( - strategy.buy.budget, - strategy.quote.decimals, - ); - amounts[quote] = amounts[quote] + buyAmount; - } unsignedTx.customData = { spender: batcher, - assets: Object.entries(amounts).map(([address, amount]) => ({ - address, - rawAmount: amount.toString(), - })), + assets: totalAssets(), }; txs.push(unsignedTx); } - const tx = await sendTransaction(txs); setConfirmation(false); setProcessing(true); @@ -194,15 +258,17 @@ export const CartPage = () => { setProcessing(false); clearCart(user!); nav({ to: '/portfolio/strategies' }); - cache.invalidateQueries({ - queryKey: QueryKey.strategyAll(), - }); - - for (const token of tokens) { + setTimeout(() => { cache.invalidateQueries({ - queryKey: QueryKey.balance(user!, token), + queryKey: QueryKey.strategyAll(), }); - } + + for (const token of tokens) { + cache.invalidateQueries({ + queryKey: QueryKey.balance(user!, token), + }); + } + }, 3000); } catch (err) { console.error(err); } finally { diff --git a/src/pages/landing/connected.tsx b/src/pages/landing/connected.tsx index f7490f169b..f29d20393c 100644 --- a/src/pages/landing/connected.tsx +++ b/src/pages/landing/connected.tsx @@ -5,6 +5,8 @@ import IconRecurring from 'assets/icons/recurring.svg?react'; import IconOverlapping from 'assets/icons/overlapping.svg?react'; import IconMarket from 'assets/icons/market.svg?react'; import IconRange from 'assets/icons/range.svg?react'; +import IconAuction from 'assets/icons/auction.svg?react'; +import IconChannel from 'assets/icons/channel.svg?react'; import { OverlappingPreview } from 'components/trade/preview/all/overlapping'; import { FullRangePreview } from 'components/trade/preview/all/full-range'; import { LimitSellPreview } from 'components/trade/preview/all/limit-sell'; @@ -16,8 +18,12 @@ import { ExplorerHeader } from 'components/explorer/ExplorerHeader'; import { PreviewCommonStrategyType } from 'components/trade/preview/common'; import { RangeBuyPreview } from 'components/trade/preview/all/range-buy'; import { SwapPreview } from 'components/trade/preview/all/swap'; -import config from 'config'; import { RewardBanner } from 'components/rewards/banner'; +import { AuctionBuyPreview } from 'components/trade/preview/all/auction-buy'; +import { AuctionSellPreview } from 'components/trade/preview/all/auction-sell'; +import { TrianglePreview } from 'components/trade/preview/all/triangle'; +import config from 'config'; +import { ChannelPreview } from 'components/trade/preview/all/channel'; const tabs = [ { @@ -95,6 +101,30 @@ const tabs = [ ), unique: true, }, + { + to: '/trade/auction', + search: { + direction: 'buy' as const, + }, + icon: , + title: 'Auction Buy', + description: + 'Buy tokens over time using a reverse Dutch auction with a continuously time changing price.', + preview: , + unique: true, + }, + { + to: '/trade/auction', + search: { + direction: 'sell' as const, + }, + icon: , + title: 'Auction Sell', + description: + 'Sell tokens over time using a Dutch auction. The price moves dynamically to find demand and optimize execution.', + preview: , + unique: true, + }, ], }, @@ -137,6 +167,26 @@ const tabs = [ ), unique: true, }, + { + to: '/trade/channel', + search: {}, + icon: , + title: 'Recurring Timed Channel', + description: + 'A paired Dutch / reverse Dutch auction forming a dynamic trading channel over time.', + preview: , + unique: true, + }, + { + to: '/trade/triangle', + search: {}, + icon: , + title: 'Recurring Timed Triangle', + description: + 'An advanced multi-leg auction setup combining dynamic pricing curves for custom strategies.', + preview: , + unique: true, + }, ], }, ]; @@ -174,7 +224,7 @@ export const ConnectedLandingPage = () => { hidden={active !== id} aria-labelledby={`tab-${id}`} > -
      +
        {items.map((item) => (
      • , }, { - name: 'Liquidity position', + name: 'Full Range Liquidity', + to: '/trade/overlapping' as const, + search: { preset: 'Infinity' } as const, + icon: , + }, + { + name: 'Concentrated Liquidity', to: '/trade/overlapping' as const, search: {}, icon: , @@ -52,6 +60,12 @@ const types = [ search: { buySettings: 'limit', sellSettings: 'limit' } as const, icon: , }, + { + name: 'Timed Auction', + to: '/trade/auction' as const, + search: { direction: 'buy' } as const, + icon: , + }, ], targets: ['Advanced', 'Experts'], }, @@ -71,6 +85,12 @@ const types = [ to: '/trade/recurring' as const, icon: , }, + { + name: 'Recurring Timed Auction', + to: '/trade/triangle' as const, + search: {} as const, + icon: , + }, ], targets: ['Professional', 'Quants'], }, diff --git a/src/pages/portfolio/edit/budget/recurring.tsx b/src/pages/portfolio/edit/budget/recurring.tsx index 6eac96ef2a..00e6f77a87 100644 --- a/src/pages/portfolio/edit/budget/recurring.tsx +++ b/src/pages/portfolio/edit/budget/recurring.tsx @@ -16,7 +16,10 @@ import { StrategyChartLegend } from 'components/strategies/common/StrategyChartL import { D3ChartRecurring } from 'components/strategies/common/d3Chart/recurring/D3ChartRecurring'; import { TradeChartContent } from 'components/strategies/common/d3Chart/TradeChartContent'; import { D3PricesAxis } from 'components/strategies/common/d3Chart/D3PriceAxis'; -import { EditOrders } from 'components/strategies/common/types'; +import { + EditOrders, + FormStaticOrder, +} from 'components/strategies/common/types'; export interface EditBudgetRecurringStrategySearch { chartStart?: string; @@ -50,7 +53,7 @@ export const EditBudgetRecurringPage = () => { search.sellBudget, ); - const orders: EditOrders = { + const orders: EditOrders = { buy: { min: buy.min, max: buy.max, diff --git a/src/pages/strategy/index.tsx b/src/pages/strategy/index.tsx index be36bbb5cf..030d7bdb82 100644 --- a/src/pages/strategy/index.tsx +++ b/src/pages/strategy/index.tsx @@ -36,6 +36,8 @@ import { D3ChartMarketPrice } from 'components/strategies/common/d3Chart/D3Chart import { useMarketPrice } from 'hooks/useMarketPrice'; import { PairChartHistory } from 'components/strategies/common/PairChartHistory'; import { useMemo } from 'react'; +import { D3ChartToday } from 'components/strategies/common/d3Chart/D3ChartToday'; +import { StrategyBlockDates } from 'components/strategies/overview/strategyBlock/StrategyBlockDates'; export const StrategyPage = () => { const { history } = useRouter(); @@ -128,7 +130,7 @@ export const StrategyPage = () => { />
        -
        +
        { strategy.status === 'active' ? '' : 'opacity-50', )} > + + + - + className="border-main-800 border-l-2" + > + +
        @@ -176,8 +181,7 @@ export const StrategyPage = () => { /> - {/* @todo(gradient) */} - {/* */} + diff --git a/src/pages/trade/auction.tsx b/src/pages/trade/auction.tsx index 8a2a572fbc..f44c2cf054 100644 --- a/src/pages/trade/auction.tsx +++ b/src/pages/trade/auction.tsx @@ -11,7 +11,10 @@ import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawL import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; import { useGradientOrder } from 'components/strategies/common/gradient/useGradientOrder'; import { CreateGradientOrder } from 'components/strategies/common/gradient/CreateGradientOrder'; -import { defaultGradientOrder } from 'components/strategies/common/gradient/utils'; +import { + defaultGradientMultipliers, + defaultGradientOrder, +} from 'components/strategies/common/gradient/utils'; import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; import { OrderDirection } from 'components/strategies/common/OrderDirection'; import { TradeChartContent } from 'components/strategies/common/d3Chart/TradeChartContent'; @@ -40,14 +43,20 @@ export const TradeAuction = () => { [navigate], ); - const baseOrder = useMemo(() => { - return defaultGradientOrder(search, marketPrice); - }, [search, marketPrice]); + const direction = search.direction ?? 'sell'; + const multi = useMemo(() => { + return defaultGradientMultipliers(base.address, quote.address, direction); + }, [base.address, quote.address, direction]); + + const initOrder = useMemo(() => { + return defaultGradientOrder(search, multi, marketPrice); + }, [search, multi, marketPrice]); + const { order, setOrder, drawing, onDrawingUpdate } = useGradientOrder( - baseOrder, + initOrder, saveOrder, ); - const direction = order.direction; + const orders = { buy: direction === 'buy' ? order : emptyGradientOrder(), sell: direction === 'sell' ? order : emptyGradientOrder(), @@ -55,14 +64,14 @@ export const TradeAuction = () => { const setDirection = useCallback( (direction: StrategyDirection) => { - const next = defaultGradientOrder( - { direction, budget: order.budget }, - marketPrice, - ); - delete next.marginalPrice; - setOrder(next); + saveOrder({ + direction, + startPrice: undefined, + endPrice: undefined, + budget: undefined, + }); }, - [marketPrice, order.budget, setOrder], + [saveOrder], ); return ( @@ -83,7 +92,7 @@ export const TradeAuction = () => { -
        +
        { >
        -
        +
        diff --git a/src/pages/trade/channel.tsx b/src/pages/trade/channel.tsx new file mode 100644 index 0000000000..ef1c874871 --- /dev/null +++ b/src/pages/trade/channel.tsx @@ -0,0 +1,110 @@ +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { StrategyChartHistory } from 'components/strategies/common/StrategyChartHistory'; +import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; +import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; +import { ChannelSearch, StrategyDirection } from 'libs/routing/routes/trade'; +import { useCallback, useMemo } from 'react'; +import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; +import { CreateGradientOrder } from 'components/strategies/common/gradient/CreateGradientOrder'; +import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; +import { TradeChartContent } from 'components/strategies/common/d3Chart/TradeChartContent'; +import { GradientOrderBlock } from 'components/strategies/common/types'; +import { cn } from 'utils/helpers'; +import { Warning } from 'components/common/WarningMessageWithIcon'; +import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; +import { CreateLayout } from 'components/strategies/create/CreateLayout'; +import { D3ChartToday } from 'components/strategies/common/d3Chart/D3ChartToday'; +import { D3EditChannel } from 'components/strategies/common/d3Chart/drawing/D3DrawChannel'; +import { ChannelOrder } from 'components/strategies/common/channel/CreateChannelOrder'; +import { useGradientChannelOrder } from 'components/strategies/common/channel/utils'; +import { isReverseGradientOrders } from 'components/strategies/common/gradient/utils'; +import { toOrderSearch } from 'components/strategies/common/useSetOrder'; +import style from 'components/strategies/common/order.module.css'; + +const url = '/trade/channel'; +export const TradeChannel = () => { + const { base, quote, marketPrice } = useStrategyFormCtx(); + const search = useSearch({ from: url }); + const navigate = useNavigate({ from: url }); + + const setSearch = useCallback( + (next: Partial) => { + navigate({ + params: (params) => params, + search: (previous) => ({ ...previous, ...next }), + replace: true, + resetScroll: false, + }); + }, + [navigate], + ); + + const setOrder = useCallback( + (next: Partial, direction: StrategyDirection) => { + const params = toOrderSearch(next, direction); + setSearch(params); + }, + [setSearch], + ); + + const deltaType = search.deltaType ?? 'percent'; + + const { drawing, onDrawingUpdate, buy, sell, deltaPrice } = + useGradientChannelOrder(search, setSearch, marketPrice); + + const priceError = useMemo(() => { + if (isReverseGradientOrders(buy, sell)) { + return 'Sell and Buy auction prices cannot cross. This configuration may result in loss of funds.'; + } + }, [buy, sell]); + + return ( + <> + } + > + + + + + + + + + +
        +
        + setOrder(next, 'sell')} + priceWarning={ + priceError && + } + /> +
        +
        + setOrder(next, 'buy')} + /> +
        +
        +
        +
        + + ); +}; diff --git a/src/pages/trade/custom.tsx b/src/pages/trade/custom.tsx deleted file mode 100644 index b862b24050..0000000000 --- a/src/pages/trade/custom.tsx +++ /dev/null @@ -1,246 +0,0 @@ -import { useNavigate, useSearch } from '@tanstack/react-router'; -import { StrategyChartHistory } from 'components/strategies/common/StrategyChartHistory'; -import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; -import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; -import { StrategyDirection } from 'libs/routing/routes/trade'; -import { useCallback, useMemo } from 'react'; -import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawLine'; -import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; -import { useGradientOrder } from 'components/strategies/common/gradient/useGradientOrder'; -import { CreateGradientOrder } from 'components/strategies/common/gradient/CreateGradientOrder'; -import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; -import { TradeChartContent } from 'components/strategies/common/d3Chart/TradeChartContent'; -import { GradientOrderBlock } from 'components/strategies/common/types'; -import { toOrderSearch } from 'components/strategies/common/useSetOrder'; -import DeleteIcon from 'assets/icons/delete.svg?react'; -import AddIcon from 'assets/icons/add.svg?react'; -import { - defaultGradientOrder, - isReverseGradientOrders, -} from 'components/strategies/common/gradient/utils'; -import { ChartPoint } from 'components/strategies/common/d3Chart/D3ChartContext'; -import { cn } from 'utils/helpers'; -import { Warning } from 'components/common/WarningMessageWithIcon'; -import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; -import { CreateLayout } from 'components/strategies/create/CreateLayout'; -import { D3ChartToday } from 'components/strategies/common/d3Chart/D3ChartToday'; -import style from 'components/strategies/common/order.module.css'; - -const url = '/trade/custom'; -export const TradeCustom = () => { - const { base, quote, marketPrice } = useStrategyFormCtx(); - const search = useSearch({ from: url }); - const navigate = useNavigate({ from: url }); - - const saveOrder = useCallback( - (next: Partial, direction: StrategyDirection) => { - const params = toOrderSearch(next, direction); - navigate({ - params: (params) => params, - search: (previous) => ({ ...previous, ...params }), - replace: true, - resetScroll: false, - }); - }, - [navigate], - ); - - const setDirections = useCallback( - (cb: (direction: StrategyDirection[]) => StrategyDirection[]) => { - navigate({ - params: (params) => params, - search: (previous) => ({ - ...previous, - directions: cb(previous.directions || []), - }), - replace: true, - resetScroll: false, - }); - }, - [navigate], - ); - - const baseBuy = useMemo(() => { - return defaultGradientOrder( - { - direction: 'buy', - _sP_: search.buy_SP_, - _eP_: search.buy_EP_, - _sD_: search.buy_SD_, - _eD_: search.buy_ED_, - budget: search.buyBudget, - }, - marketPrice, - ); - }, [ - marketPrice, - search.buyBudget, - search.buy_ED_, - search.buy_EP_, - search.buy_SD_, - search.buy_SP_, - ]); - - const baseSell = useMemo(() => { - return defaultGradientOrder( - { - direction: 'sell', - _sP_: search.sell_SP_, - _eP_: search.sell_EP_, - _sD_: search.sell_SD_, - _eD_: search.sell_ED_, - budget: search.sellBudget, - }, - marketPrice, - ); - }, [ - marketPrice, - search.sellBudget, - search.sell_ED_, - search.sell_EP_, - search.sell_SD_, - search.sell_SP_, - ]); - - const buy = useGradientOrder(baseBuy, (next) => saveOrder(next, 'buy')); - const sell = useGradientOrder(baseSell, (next) => saveOrder(next, 'sell')); - - const orders = useMemo(() => ({ buy, sell }), [buy, sell]); - - const addDirection = useCallback( - (direction: StrategyDirection) => { - setDirections((directions) => { - const set = new Set(directions); - set.add(direction); - return Array.from(set); - }); - }, - [setDirections], - ); - const removeDirection = useCallback( - (direction: StrategyDirection) => { - setDirections((directions) => { - const set = new Set(directions); - set.delete(direction); - saveOrder( - { - _sP_: undefined, - _eP_: undefined, - _sD_: undefined, - _eD_: undefined, - budget: undefined, - }, - direction, - ); - return Array.from(set); - }); - }, - [setDirections, saveOrder], - ); - - const onDrawingChange = useCallback( - (points: ChartPoint[], direction: StrategyDirection) => { - if (points.length) return orders[direction].onDrawingUpdate(points); - removeDirection(direction); - }, - [orders, removeDirection], - ); - - const priceError = useMemo(() => { - if (search.directions?.length !== 2) return; - if (isReverseGradientOrders(buy.order, sell.order)) { - return 'Orders are reversed. This strategy is currently set to Buy High and Sell Low. Please adjust your prices to avoid loss of funds.'; - } - }, [buy.order, search.directions?.length, sell.order]); - - return ( - <> - } - > - - {search.directions?.map((direction) => ( - onDrawingChange(points, direction)} - /> - ))} - - - {search.directions?.map((direction) => ( - - ))} - - - - -
        - {!search.directions?.length && ( -

        - Please select an order -

        - )} - {(['sell', 'buy'] as const).map((direction) => { - const { order, setOrder } = orders[direction]; - if (search.directions?.includes(direction)) { - return ( -
        - - - } - /> -
        - ); - } else { - return ( -
        - -
        - ); - } - })} -
        -
        -
        - - ); -}; diff --git a/src/pages/trade/quick-auction.tsx b/src/pages/trade/quick-auction.tsx index 8f1e37f3f6..857b38054c 100644 --- a/src/pages/trade/quick-auction.tsx +++ b/src/pages/trade/quick-auction.tsx @@ -23,6 +23,7 @@ import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawL import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; import { cn } from 'utils/helpers'; import style from 'components/strategies/common/order.module.css'; +import { defaultGradientMultipliers } from 'components/strategies/common/gradient/utils'; const url = '/trade/quick-auction'; export const TradeQuickAuction = () => { @@ -46,13 +47,17 @@ export const TradeQuickAuction = () => { [navigate], ); + const direction = search.direction ?? 'sell'; + const multi = useMemo(() => { + return defaultGradientMultipliers(base.address, quote.address, direction); + }, [base.address, quote.address, direction]); + const baseOrder = useMemo(() => { - return defaultQuickGradientOrder(search, marketPrice); - }, [search, marketPrice]); + return defaultQuickGradientOrder(search, multi, marketPrice); + }, [search, multi, marketPrice]); const { order, setOrder, drawing, onDrawingUpdate, gradientOrder } = useQuickGradientOrder(baseOrder, saveOrder); - const direction = order.direction; const orders = { buy: direction === 'buy' ? gradientOrder : emptyGradientOrder(), sell: direction === 'sell' ? gradientOrder : emptyGradientOrder(), @@ -66,14 +71,14 @@ export const TradeQuickAuction = () => { const setDirection = useCallback( (direction: StrategyDirection) => { - const next = defaultQuickGradientOrder( - { direction, budget: order.budget }, - marketPrice, - ); - delete next.marginalPrice; - setOrder(next); + saveOrder({ + direction, + startPrice: undefined, + endPrice: undefined, + budget: undefined, + }); }, - [marketPrice, order.budget, setOrder], + [saveOrder], ); return ( @@ -90,13 +95,13 @@ export const TradeQuickAuction = () => { formatQuickTime(x)} + formatX={formatQuickTime} /> -
        +
        { >
        -
        +
        diff --git a/src/pages/trade/quick-channel.tsx b/src/pages/trade/quick-channel.tsx new file mode 100644 index 0000000000..12acce732e --- /dev/null +++ b/src/pages/trade/quick-channel.tsx @@ -0,0 +1,120 @@ +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; +import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; +import { + QuickChannelSearch, + StrategyDirection, +} from 'libs/routing/routes/trade'; +import { useCallback, useMemo } from 'react'; +import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; +import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; +import { GradientOrderBlock } from 'components/strategies/common/types'; +import { toOrderSearch } from 'components/strategies/common/useSetOrder'; +import { isReverseGradientOrders } from 'components/strategies/common/gradient/utils'; +import { cn } from 'utils/helpers'; +import { Warning } from 'components/common/WarningMessageWithIcon'; +import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; +import { CreateLayout } from 'components/strategies/create/CreateLayout'; +import { + formatQuickTime, + quickToGradientOrder, +} from 'components/strategies/common/quick/utils'; +import { QuickGradientChart } from 'components/strategies/common/quick/QuickGradientChart'; +import { CreateQuickGradientOrder } from 'components/strategies/common/quick/CreateQuickGradientOrder'; +import { D3EditChannel } from 'components/strategies/common/d3Chart/drawing/D3DrawChannel'; +import { ChannelOrder } from 'components/strategies/common/channel/CreateChannelOrder'; +import { useQuickGradientChannelOrder } from 'components/strategies/common/channel/utils'; +import style from 'components/strategies/common/order.module.css'; + +const url = '/trade/quick-channel'; +export const TradeQuickChannel = () => { + const { base, quote, marketPrice } = useStrategyFormCtx(); + const search = useSearch({ from: url }); + const navigate = useNavigate({ from: url }); + + const setSearch = useCallback( + (next: Partial) => { + navigate({ + params: (params) => params, + search: (previous) => ({ ...previous, ...next }), + replace: true, + resetScroll: false, + }); + }, + [navigate], + ); + + const setOrder = useCallback( + (next: Partial, direction: StrategyDirection) => { + const params = toOrderSearch(next, direction); + setSearch(params); + }, + [setSearch], + ); + + const deltaType = search.deltaType ?? 'percent'; + + const { drawing, onDrawingUpdate, buy, sell, deltaPrice } = + useQuickGradientChannelOrder(search, setSearch, marketPrice); + + const buyGradient = useMemo(() => quickToGradientOrder(buy), [buy]); + const sellGradient = useMemo(() => quickToGradientOrder(sell), [sell]); + + const priceError = useMemo(() => { + if (isReverseGradientOrders(buyGradient, sellGradient)) { + return 'Sell and Buy auction prices cannot cross. This configuration may result in loss of funds.'; + } + }, [buyGradient, sellGradient]); + + return ( + <> + } + > + + + + + + + +
        +
        + setOrder(next, 'sell')} + priceWarning={ + priceError && + } + /> +
        +
        + setOrder(next, 'buy')} + /> +
        +
        +
        +
        + + ); +}; diff --git a/src/pages/trade/quick-custom.tsx b/src/pages/trade/quick-custom.tsx deleted file mode 100644 index 28d1162039..0000000000 --- a/src/pages/trade/quick-custom.tsx +++ /dev/null @@ -1,257 +0,0 @@ -import { useNavigate, useSearch } from '@tanstack/react-router'; -import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; -import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; -import { useMarketPrice } from 'hooks/useMarketPrice'; -import { StrategyDirection } from 'libs/routing/routes/trade'; -import { useCallback, useEffect, useMemo } from 'react'; -import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawLine'; -import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; -import { useQuickGradientOrder } from 'components/strategies/common/gradient/useGradientOrder'; -import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; -import { GradientOrderBlock } from 'components/strategies/common/types'; -import { toOrderSearch } from 'components/strategies/common/useSetOrder'; -import DeleteIcon from 'assets/icons/delete.svg?react'; -import AddIcon from 'assets/icons/add.svg?react'; -import { isReverseGradientOrders } from 'components/strategies/common/gradient/utils'; -import { ChartPoint } from 'components/strategies/common/d3Chart/D3ChartContext'; -import { cn } from 'utils/helpers'; -import { Warning } from 'components/common/WarningMessageWithIcon'; -import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; -import { CreateLayout } from 'components/strategies/create/CreateLayout'; -import { - defaultQuickGradientOrder, - formatQuickTime, -} from 'components/strategies/common/quick/utils'; -import { QuickGradientChart } from 'components/strategies/common/quick/QuickGradientChart'; -import { CreateQuickGradientOrder } from 'components/strategies/common/quick/CreateQuickGradientOrder'; -import style from 'components/strategies/common/order.module.css'; - -const url = '/trade/quick-custom'; -export const TradeQuickCustom = () => { - const { base, quote } = useStrategyFormCtx(); - const { marketPrice, isPending: pendingMarketPrice } = useMarketPrice({ - base, - quote, - }); - const search = useSearch({ from: url }); - const navigate = useNavigate({ from: url }); - - const saveOrder = useCallback( - (next: Partial, direction: StrategyDirection) => { - const params = toOrderSearch(next, direction); - navigate({ - params: (params) => params, - search: (previous) => ({ ...previous, ...params }), - replace: true, - resetScroll: false, - }); - }, - [navigate], - ); - - const setDirections = useCallback( - (cb: (direction: StrategyDirection[]) => StrategyDirection[]) => { - navigate({ - params: (params) => params, - search: (previous) => ({ - ...previous, - directions: cb(previous.directions || []), - }), - replace: true, - resetScroll: false, - }); - }, - [navigate], - ); - - const baseBuy = useMemo(() => { - return defaultQuickGradientOrder( - { - direction: 'buy', - _sP_: search.buy_SP_, - _eP_: search.buy_EP_, - deltaTime: search.buyDeltaTime, - budget: search.buyBudget, - }, - marketPrice, - ); - }, [ - marketPrice, - search.buyBudget, - search.buy_EP_, - search.buyDeltaTime, - search.buy_SP_, - ]); - - const baseSell = useMemo(() => { - return defaultQuickGradientOrder( - { - direction: 'sell', - _sP_: search.sell_SP_, - _eP_: search.sell_EP_, - deltaTime: search.sellDeltaTime, - budget: search.sellBudget, - }, - marketPrice, - ); - }, [ - marketPrice, - search.sellBudget, - search.sell_EP_, - search.sellDeltaTime, - search.sell_SP_, - ]); - - const buy = useQuickGradientOrder(baseBuy, (next) => { - return saveOrder(next, 'buy'); - }); - const sell = useQuickGradientOrder(baseSell, (next) => { - return saveOrder(next, 'sell'); - }); - - useEffect(() => { - if (pendingMarketPrice) return; - buy.setOrder(baseBuy); - sell.setOrder(baseSell); - // Only run this once the marketprice is ready - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pendingMarketPrice]); - - const orders = useMemo(() => ({ buy, sell }), [buy, sell]); - - const addDirection = useCallback( - (direction: StrategyDirection) => { - setDirections((directions) => { - const set = new Set(directions); - set.add(direction); - return Array.from(set); - }); - }, - [setDirections], - ); - const removeDirection = useCallback( - (direction: StrategyDirection) => { - setDirections((directions) => { - const set = new Set(directions); - set.delete(direction); - saveOrder( - { - _sP_: undefined, - _eP_: undefined, - _sD_: undefined, - _eD_: undefined, - budget: undefined, - }, - direction, - ); - return Array.from(set); - }); - }, - [setDirections, saveOrder], - ); - - const onDrawingChange = useCallback( - (points: ChartPoint[], direction: StrategyDirection) => { - if (points.length) return orders[direction].onDrawingUpdate(points); - removeDirection(direction); - }, - [orders, removeDirection], - ); - - const priceError = useMemo(() => { - if (search.directions?.length !== 2) return; - if (isReverseGradientOrders(buy.gradientOrder, sell.gradientOrder)) { - return 'Orders are reversed. This strategy is currently set to Buy High and Sell Low. Please adjust your prices to avoid loss of funds.'; - } - }, [buy.gradientOrder, search.directions?.length, sell.gradientOrder]); - - return ( - <> - } - > - - {search.directions?.map((direction) => ( - onDrawingChange(points, direction)} - /> - ))} - {search.directions?.map((direction) => ( - formatQuickTime(x)} - /> - ))} - - - - -
        - {!search.directions?.length && ( -

        - Please select an order -

        - )} - {(['sell', 'buy'] as const).map((direction) => { - const { order, setOrder } = orders[direction]; - if (search.directions?.includes(direction)) { - return ( -
        - - - } - /> -
        - ); - } else { - return ( -
        - -
        - ); - } - })} -
        -
        -
        - - ); -}; diff --git a/src/pages/trade/quick-triangle.tsx b/src/pages/trade/quick-triangle.tsx new file mode 100644 index 0000000000..021d81ccf6 --- /dev/null +++ b/src/pages/trade/quick-triangle.tsx @@ -0,0 +1,167 @@ +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; +import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; +import { useMarketPrice } from 'hooks/useMarketPrice'; +import { StrategyDirection } from 'libs/routing/routes/trade'; +import { useCallback, useEffect, useMemo } from 'react'; +import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; +import { useQuickGradientOrder } from 'components/strategies/common/gradient/useGradientOrder'; +import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; +import { GradientOrderBlock } from 'components/strategies/common/types'; +import { toOrderSearch } from 'components/strategies/common/useSetOrder'; +import { + defaultGradientMultipliers, + isReverseGradientOrders, +} from 'components/strategies/common/gradient/utils'; +import { cn } from 'utils/helpers'; +import { Warning } from 'components/common/WarningMessageWithIcon'; +import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; +import { CreateLayout } from 'components/strategies/create/CreateLayout'; +import { defaultQuickGradientOrder } from 'components/strategies/common/quick/utils'; +import { QuickGradientChart } from 'components/strategies/common/quick/QuickGradientChart'; +import { CreateQuickGradientOrder } from 'components/strategies/common/quick/CreateQuickGradientOrder'; +import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawLine'; +import style from 'components/strategies/common/order.module.css'; + +const url = '/trade/quick-triangle'; +export const TradeQuickTriangle = () => { + const { base, quote } = useStrategyFormCtx(); + const { marketPrice, isPending: pendingMarketPrice } = useMarketPrice({ + base, + quote, + }); + const search = useSearch({ from: url }); + const navigate = useNavigate({ from: url }); + + const saveOrder = useCallback( + (next: Partial, direction: StrategyDirection) => { + const params = toOrderSearch(next, direction); + navigate({ + params: (params) => params, + search: (previous) => ({ ...previous, ...params }), + replace: true, + resetScroll: false, + }); + }, + [navigate], + ); + + const baseSell = useMemo(() => { + return defaultQuickGradientOrder( + { + direction: 'sell', + startPrice: search.sellStartPrice, + endPrice: search.sellEndPrice, + deltaTime: search.sellDeltaTime, + budget: search.sellBudget, + }, + defaultGradientMultipliers(base.address, quote.address, 'sell'), + marketPrice, + ); + }, [ + search.sellStartPrice, + search.sellEndPrice, + search.sellDeltaTime, + search.sellBudget, + base.address, + quote.address, + marketPrice, + ]); + + const baseBuy = useMemo(() => { + return defaultQuickGradientOrder( + { + direction: 'buy', + startPrice: search.buyStartPrice, + endPrice: search.buyEndPrice, + deltaTime: search.buyDeltaTime, + budget: search.buyBudget, + }, + defaultGradientMultipliers(base.address, quote.address, 'buy'), + marketPrice, + ); + }, [ + search.buyStartPrice, + search.buyEndPrice, + search.buyDeltaTime, + search.buyBudget, + base.address, + quote.address, + marketPrice, + ]); + + const sell = useQuickGradientOrder(baseSell, (next) => + saveOrder(next, 'sell'), + ); + const buy = useQuickGradientOrder(baseBuy, (next) => saveOrder(next, 'buy')); + + useEffect(() => { + if (pendingMarketPrice) return; + buy.setOrder(baseBuy); + sell.setOrder(baseSell); + // Only run this once the marketprice is ready + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pendingMarketPrice]); + + const priceError = useMemo(() => { + if (isReverseGradientOrders(buy.gradientOrder, sell.gradientOrder)) { + return 'Sell and Buy auction prices cannot cross. This configuration may result in loss of funds.'; + } + }, [buy.gradientOrder, sell.gradientOrder]); + + return ( + <> + } + > + + + + + + + + + +
        +
        + + } + /> +
        +
        + + } + /> +
        +
        +
        +
        + + ); +}; diff --git a/src/pages/trade/triangle.tsx b/src/pages/trade/triangle.tsx new file mode 100644 index 0000000000..dbfe09fb23 --- /dev/null +++ b/src/pages/trade/triangle.tsx @@ -0,0 +1,157 @@ +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { StrategyChartHistory } from 'components/strategies/common/StrategyChartHistory'; +import { StrategyChartSection } from 'components/strategies/common/StrategyChartSection'; +import { useStrategyFormCtx } from 'components/strategies/common/StrategyFormContext'; +import { StrategyDirection } from 'libs/routing/routes/trade'; +import { useCallback, useMemo } from 'react'; +import { D3DrawingRanges } from 'components/strategies/common/d3Chart/drawing/D3DrawingRanges'; +import { useGradientOrder } from 'components/strategies/common/gradient/useGradientOrder'; +import { CreateGradientOrder } from 'components/strategies/common/gradient/CreateGradientOrder'; +import { CreateGradientStrategyForm } from 'components/strategies/common/gradient/CreateGradientStrategyForm'; +import { TradeChartContent } from 'components/strategies/common/d3Chart/TradeChartContent'; +import { GradientOrderBlock } from 'components/strategies/common/types'; +import { toOrderSearch } from 'components/strategies/common/useSetOrder'; +import { + defaultGradientMultipliers, + defaultGradientOrder, + isReverseGradientOrders, +} from 'components/strategies/common/gradient/utils'; +import { cn } from 'utils/helpers'; +import { Warning } from 'components/common/WarningMessageWithIcon'; +import { EditMarketPrice } from 'components/strategies/common/InitMarketPrice'; +import { CreateLayout } from 'components/strategies/create/CreateLayout'; +import { D3ChartToday } from 'components/strategies/common/d3Chart/D3ChartToday'; +import { D3EditLine } from 'components/strategies/common/d3Chart/drawing/D3DrawLine'; +import style from 'components/strategies/common/order.module.css'; + +const url = '/trade/triangle'; +export const TradeTriangle = () => { + const { base, quote, marketPrice } = useStrategyFormCtx(); + const search = useSearch({ from: url }); + const navigate = useNavigate({ from: url }); + + const saveOrder = useCallback( + (next: Partial, direction: StrategyDirection) => { + const params = toOrderSearch(next, direction); + navigate({ + params: (params) => params, + search: (previous) => ({ ...previous, ...params }), + replace: true, + resetScroll: false, + }); + }, + [navigate], + ); + + const baseBuy = useMemo(() => { + return defaultGradientOrder( + { + direction: 'buy', + startPrice: search.buyStartPrice, + endPrice: search.buyEndPrice, + startDate: search.buyStartDate, + endDate: search.buyEndDate, + budget: search.buyBudget, + }, + defaultGradientMultipliers(base.address, quote.address, 'buy'), + marketPrice, + ); + }, [ + base.address, + marketPrice, + quote.address, + search.buyBudget, + search.buyEndDate, + search.buyEndPrice, + search.buyStartDate, + search.buyStartPrice, + ]); + + const baseSell = useMemo(() => { + return defaultGradientOrder( + { + direction: 'sell', + startPrice: search.sellStartPrice, + endPrice: search.sellEndPrice, + startDate: search.sellStartDate, + endDate: search.sellEndDate, + budget: search.sellBudget, + }, + defaultGradientMultipliers(base.address, quote.address, 'sell'), + marketPrice, + ); + }, [ + base.address, + marketPrice, + quote.address, + search.sellBudget, + search.sellEndDate, + search.sellEndPrice, + search.sellStartDate, + search.sellStartPrice, + ]); + + const sell = useGradientOrder(baseSell, (next) => saveOrder(next, 'sell')); + const buy = useGradientOrder(baseBuy, (next) => saveOrder(next, 'buy')); + + const priceError = useMemo(() => { + if (isReverseGradientOrders(buy.order, sell.order)) { + return 'Sell and Buy auction prices cannot cross. This configuration may result in loss of funds.'; + } + }, [buy.order, sell.order]); + + return ( + <> + } + > + + + + + + + + + + + +
        +
        + + } + /> +
        +
        + + } + /> +
        +
        +
        +
        + + ); +}; diff --git a/src/services/events/index.ts b/src/services/events/index.ts index a27182fbf7..8202bf4b4e 100644 --- a/src/services/events/index.ts +++ b/src/services/events/index.ts @@ -22,7 +22,7 @@ interface StrategyProps { token_pair: string; strategy_base_token: string; strategy_quote_token: string; - strategy_category: 'static' | 'gradient'; + strategy_category: 'regular' | 'gradient'; strategy_type: StrategyType; }; } @@ -103,7 +103,7 @@ interface MigrateProps { token_pair: string; strategy_base_token: string; strategy_quote_token: string; - strategy_category: 'static' | 'gradient'; + strategy_category: 'regular' | 'gradient'; strategy_type: StrategyType; source_amm: string; }; diff --git a/src/theme.css b/src/theme.css index 09441c9929..b3e1eb4686 100644 --- a/src/theme.css +++ b/src/theme.css @@ -438,7 +438,8 @@ html[data-e2e="true"] { @apply border border-transparent bg-main-900/40 hover:bg-main-900/60 p-16 focus-within:border-main-400; box-shadow: inset 0 0 10px var(--color-shadow); &:has(:disabled) { - @apply bg-main-900/40; + box-shadow: none; + @apply opacity-60; } &:has(.text-warning) { @apply border-warning/80 focus-within:border-warning; diff --git a/src/workers/sdk.ts b/src/workers/sdk.ts index e7534cb364..e0f2e8097d 100644 --- a/src/workers/sdk.ts +++ b/src/workers/sdk.ts @@ -162,6 +162,19 @@ const sdkExposed = { overrides, ), getCacheDump: () => sdkCache.serialize(), + // Gradient + createGradientStrategy: ( + ...args: Parameters<(typeof carbonSDK)['createGradientBuySellStrategy']> + ) => carbonSDK.createGradientBuySellStrategy(...args), + deleteGradientStrategy: ( + ...args: Parameters<(typeof carbonSDK)['deleteGradientStrategy']> + ) => carbonSDK.deleteGradientStrategy(...args), + updateGradientStrategy: ( + ...args: Parameters<(typeof carbonSDK)['updateGradientStrategy']> + ) => carbonSDK.updateGradientStrategy(...args), + routerCreateStrategies: ( + ...args: Parameters<(typeof carbonSDK)['routerCreateStrategies']> + ) => carbonSDK.routerCreateStrategies(...args), }; export type CarbonSDKWebWorker = typeof sdkExposed;
    _S P_ + Start Price + - {_sP_} {quote.symbol} + {startPrice} {quote.symbol}
    _E P_End Price - {_eP_} {quote.symbol} + {endPrice} {quote.symbol}
    _S D_ + Start Date + - {_sD_Text} + {startDateText}
    _E D_End Date - {fromUnixUTC(_eD_).toLocaleString()} + {fromUnixUTC(endDate).toLocaleString()}