From d629e7de5da88ce039d0c236acf3a28bbb0c7c18 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 11 Mar 2024 10:20:06 -0300 Subject: [PATCH 1/5] fix: nodes data default types --- src/components/Board.tsx | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 806e228..123fa09 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -17,13 +17,7 @@ import ReactFlow, { } from "reactflow"; import { Address } from "viem"; -import { ChainId } from "#/lib/publicClients"; -import { - INodeData, - IStopLossConditionData, - IStopLossRecipeData, - ISwapData, -} from "#/lib/types"; +import { INodeData, IStopLossConditionData, ISwapData } from "#/lib/types"; import { defaultEdgeProps } from "./edges"; import { AddHookEdge } from "./edges/AddHookEdge"; @@ -68,18 +62,21 @@ const initEdges = [ }, ]; -const createInitNodes = (data: IStopLossRecipeData) => - [ +const createInitNodes = ( + conditionData: IStopLossConditionData, + swapData: ISwapData +) => { + return [ { id: "condition", type: "stopLoss", - data: data as IStopLossConditionData, + data: conditionData, ...defaultNodeProps, }, { id: "swap", type: "swap", - data: data as ISwapData, + data: swapData, ...defaultNodeProps, }, { @@ -89,19 +86,17 @@ const createInitNodes = (data: IStopLossRecipeData) => ...defaultNodeProps, }, ] as Node[]; +}; export const Board = () => { const { safe: { safeAddress, chainId }, } = useSafeAppsSDK(); const layoutedNodes = getLayoutedNodes( - createInitNodes({ - ...getDefaultSwapData(chainId, safeAddress as Address), - ...defaultStopLossData, - preHooks: [], - postHooks: [], - chainId: chainId as ChainId, - }) + createInitNodes( + defaultStopLossData, + getDefaultSwapData(chainId, safeAddress as Address) + ) ); const [nodes, setNodes, onNodesChange] = From 76c4454a6010a9cc960394ada9de955e31f9d672 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 11 Mar 2024 10:33:52 -0300 Subject: [PATCH 2/5] fix: add validity time bucket tooltip text --- src/components/menus/SwapMenu.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/menus/SwapMenu.tsx b/src/components/menus/SwapMenu.tsx index d975380..6270b9c 100644 --- a/src/components/menus/SwapMenu.tsx +++ b/src/components/menus/SwapMenu.tsx @@ -31,7 +31,8 @@ const ALLOWED_SLIPPAGE_TOOLTIP_TEXT = const IS_PARTIALLY_FILLABLE_TOOLTIP_TEXT = "If checked, the order can be divided into smaller orders and executed separately."; -const VALIDITY_BUCKET_TIME_TOOLTIP_TEXT = "TODO"; +const VALIDITY_BUCKET_TIME_TOOLTIP_TEXT = + "How long the order will be valid, after placed on the orderbook."; export function SwapMenu({ data, @@ -154,7 +155,7 @@ export function SwapMenu({ onChange={() => setValue( "isPartiallyFillable", - !formData.isPartiallyFillable, + !formData.isPartiallyFillable ) } /> From 818d6d51ac94de0d16cf375ffda9f295e0853f71 Mon Sep 17 00:00:00 2001 From: Pedro Yves Fracari Date: Mon, 11 Mar 2024 15:25:59 -0300 Subject: [PATCH 3/5] integrate oracle router into the UI --- package.json | 2 +- pnpm-lock.yaml | 103 +++++++++++++++--- src/app/builder/page.tsx | 78 ++++++++++++- src/components/Board.tsx | 79 +++----------- src/components/menus/MultiSendMenu.tsx | 21 +++- .../menus/StopLossConditionMenu.tsx | 53 ++++++--- src/components/menus/StopLossRecipeMenu.tsx | 3 +- src/components/menus/SwapMenu.tsx | 48 +++++++- src/components/menus/index.tsx | 29 +---- src/components/nodes/StopLossNode.tsx | 52 +++++++-- src/lib/__tests__/oracleRouter.test.ts | 29 +++-- src/lib/abis/oracleMinimalAbi.ts | 2 +- src/lib/oracleRouter.ts | 76 +++++++------ src/lib/schema.ts | 47 ++++++-- src/lib/types.ts | 5 +- 15 files changed, 435 insertions(+), 192 deletions(-) diff --git a/package.json b/package.json index f28e807..6e054a7 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "test:watch": "pnpm exec jest --watchAll --verbose" }, "dependencies": { - "@bleu-fi/ui": "^0.1.33", + "@bleu-fi/ui": "^0.1.48", "@cowprotocol/app-data": "^1.2.2", "@graphql-codegen/typescript-operations": "^4.2.0", "@hookform/resolvers": "^3.3.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e57b29..d4258c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ overrides: dependencies: '@bleu-fi/ui': - specifier: ^0.1.33 - version: 0.1.33(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react-router-dom@6.22.1)(react@18.2.0) + specifier: ^0.1.48 + version: 0.1.48(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react-hook-form@7.50.1)(react-router-dom@6.22.1)(react@18.2.0) '@cowprotocol/app-data': specifier: ^1.2.2 version: 1.2.2(cross-fetch@4.0.0)(ethers@5.7.2)(ipfs-only-hash@4.0.0)(multiformats@9.9.0) @@ -975,12 +975,13 @@ packages: /@bcoe/v8-coverage@0.2.3: resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - /@bleu-fi/ui@0.1.33(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react-router-dom@6.22.1)(react@18.2.0): - resolution: {integrity: sha512-IYm1dWEDZI2U02KdU5cJDSHz2Grnn1wpFcjetHOtQ3I5xD30BlwjtfzlE0qTgH6JUufXp0P1h4EXnzXe0MP7VQ==, tarball: https://npm.pkg.github.com/download/@bleu-fi/ui/0.1.33/f582c712496414ca3b478a4178b0861c8da18ca5} + /@bleu-fi/ui@0.1.48(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react-hook-form@7.50.1)(react-router-dom@6.22.1)(react@18.2.0): + resolution: {integrity: sha512-Kt7UXc6ihT4rOS3LLW8lQ2YXgAKwEiGX+OQNAoCuXchuA3INuXNiH6e/8nVruc8csKiwH0qQlfaaDMLp+cuFdA==, tarball: https://npm.pkg.github.com/download/@bleu-fi/ui/0.1.48/dd1c252d8532f0dadb48fd881fc77a86aa571419} engines: {node: '>=18.0.0'} peerDependencies: react: '>=18' react-dom: '>=18' + react-hook-form: ^7.50.1 react-router-dom: ^6.21.0 dependencies: '@radix-ui/react-accordion': 1.1.2(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) @@ -1011,20 +1012,24 @@ packages: '@radix-ui/react-toggle': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-toggle-group': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.2.19)(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) - '@tanstack/react-table': 8.12.0(react-dom@18.2.0)(react@18.2.0) + '@tanstack/react-table': 8.13.2(react-dom@18.2.0)(react@18.2.0) class-variance-authority: 0.7.0 clsx: 2.1.0 cmdk: 0.2.1(@types/react@18.2.55)(react-dom@18.2.0)(react@18.2.0) copy-to-clipboard: 3.3.3 date-fns: 3.3.1 - jodit: 4.0.6 - jodit-react: 4.0.9(react-dom@18.2.0)(react@18.2.0) + i18next: 23.10.1 + jodit: 4.0.18 + jodit-react: 4.0.15(react-dom@18.2.0)(react@18.2.0) object-to-formdata: 4.5.1 react: 18.2.0 react-beautiful-dnd: 13.1.1(react-dom@18.2.0)(react@18.2.0) + react-colorful: 5.6.1(react-dom@18.2.0)(react@18.2.0) react-day-picker: 8.10.0(date-fns@3.3.1)(react@18.2.0) react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.1(react@18.2.0) + react-hook-form: 7.50.1(react@18.2.0) + react-i18next: 14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0) + react-router: 6.22.3(react@18.2.0) react-router-dom: 6.22.1(react-dom@18.2.0)(react@18.2.0) swr: 2.2.5(react@18.2.0) tailwind-merge: 2.2.1 @@ -4556,6 +4561,11 @@ packages: engines: {node: '>=14.0.0'} dev: false + /@remix-run/router@1.15.3: + resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} + engines: {node: '>=14.0.0'} + dev: false + /@repeaterjs/repeater@3.0.5: resolution: {integrity: sha512-l3YHBLAol6d/IKnB9LhpD0cEZWAoe3eFKUyTYWmFmCO2Q/WOckxLQAUyMZWwZV2M/m3+4vgRoaolFqaII82/TA==} dev: true @@ -4783,20 +4793,20 @@ packages: tslib: 2.6.2 dev: false - /@tanstack/react-table@8.12.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-LlEQ1Gpz4bfpiET+qmle4BhKDgKN3Y/sssc+O/wLqX8HRtjV+nhusYbllZlutZfMR8oeef83whKTj/VhaV8EeA==} + /@tanstack/react-table@8.13.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-b6mR3mYkjRtJ443QZh9sc7CvGTce81J35F/XMr0OoWbx0KIM7TTTdyNP2XKObvkLpYnLpCrYDwI3CZnLezWvpg==} engines: {node: '>=12'} peerDependencies: react: '>=16' react-dom: '>=16' dependencies: - '@tanstack/table-core': 8.12.0 + '@tanstack/table-core': 8.13.2 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /@tanstack/table-core@8.12.0: - resolution: {integrity: sha512-cq/ylWVrOwixmwNXQjgZaQw1Izf7+nPxjczum7paAnMtwPg1S2qRAJU+Jb8rEBUWm69voC/zcChmePlk2hc6ug==} + /@tanstack/table-core@8.13.2: + resolution: {integrity: sha512-/2saD1lWBUV6/uNAwrsg2tw58uvMJ07bO2F1IWMxjFRkJiXKQRuc3Oq2aufeobD3873+4oIM/DRySIw7+QsPPw==} engines: {node: '>=12'} dev: false @@ -8443,6 +8453,12 @@ packages: /html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + /http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -8499,6 +8515,12 @@ packages: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} + /i18next@23.10.1: + resolution: {integrity: sha512-NDiIzFbcs3O9PXpfhkjyf7WdqFn5Vq6mhzhtkXzj51aOcNuPNcTwuYNuXCpHsanZGHlHKL35G7huoFeVic1hng==} + dependencies: + '@babel/runtime': 7.23.9 + dev: false + /iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -9490,19 +9512,19 @@ packages: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true - /jodit-react@4.0.9(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-+Hlj8OMjyztMxtb5pukA+XquTJDsH+4UEm8QQvfmkLoxERB2kkoFyFXFL4E01AkWL0T+U0oL9+FqTxEi5N95pQ==} + /jodit-react@4.0.15(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-wcqz71yI8K2/ZIK9rMfUTRpRHZqe2Far+sd+waswADJkslxfnBhFZg/vz+9WIqT3lq42KbdZsqogMPU1JW9SrQ==} peerDependencies: react: ~0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 react-dom: ~0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: - jodit: 4.0.6 + jodit: 4.0.18 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) dev: false - /jodit@4.0.6: - resolution: {integrity: sha512-UyWSwQyGhim30mmWo2GH5HKjSsQN5KdhEebajKun08PPKaglDJi7cTXOa5hEUKAgl1PLUsMVKrBWFZdRzcuG6A==} + /jodit@4.0.18: + resolution: {integrity: sha512-S2Eorn+7ZhIFEaPMDwoBpLhTwli/NP9FwSWY7AbsBvAy3nnltwczxRkcfXGCYyrdlqua1Mml3MPTuQtOT8SGBg==} dependencies: autobind-decorator: 2.4.0 dev: false @@ -11508,6 +11530,16 @@ packages: - react-native dev: false + /react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-day-picker@8.10.0(date-fns@3.3.1)(react@18.2.0): resolution: {integrity: sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==} peerDependencies: @@ -11553,6 +11585,26 @@ packages: react: 18.2.0 dev: false + /react-i18next@14.1.0(i18next@23.10.1)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.9 + html-parse-stringify: 3.0.1 + i18next: 23.10.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-intersection-observer@9.8.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-wXHvMQUsTagh3X0Z6jDtGkIXc3VVCd2tjDRYR9kII3GKrZr0XF0xtpfdamo2n8BSF+zzfeeBVOTjxZWpBp9X0g==} peerDependencies: @@ -11690,6 +11742,16 @@ packages: react: 18.2.0 dev: false + /react-router@6.22.3(react@18.2.0): + resolution: {integrity: sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.15.3 + react: 18.2.0 + dev: false + /react-side-effect@2.1.2(react@18.2.0): resolution: {integrity: sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==} peerDependencies: @@ -13494,6 +13556,11 @@ packages: - typescript dev: false + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /w3c-xmlserializer@4.0.0: resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} engines: {node: '>=14'} diff --git a/src/app/builder/page.tsx b/src/app/builder/page.tsx index 6bf431c..d310f57 100644 --- a/src/app/builder/page.tsx +++ b/src/app/builder/page.tsx @@ -1,11 +1,85 @@ "use client"; -import { ReactFlowProvider } from "reactflow"; +import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; +import { useEffect, useState } from "react"; +import { Node, ReactFlowProvider } from "reactflow"; +import { Address } from "viem"; import { Board } from "#/components/Board"; +import { defaultEdgeProps } from "#/components/edges"; import Menu from "#/components/menus"; +import { defaultNodeProps } from "#/components/nodes"; +import { getDefaultStopLossData } from "#/components/nodes/StopLossNode"; +import { getDefaultSwapData } from "#/components/nodes/SwapNode"; +import { Spinner } from "#/components/Spinner"; +import { ChainId } from "#/lib/publicClients"; +import { INodeData } from "#/lib/types"; + +const createInitNodes = async (chainId: ChainId, safeAddress: Address) => { + const swapData = getDefaultSwapData(chainId, safeAddress); + const conditionData = await getDefaultStopLossData({ + chainId, + tokenBuy: swapData.tokenBuy, + tokenSell: swapData.tokenSell, + }); + + return [ + { + id: "condition", + type: "stopLoss", + data: conditionData, + ...defaultNodeProps, + }, + { + id: "swap", + type: "swap", + data: swapData, + ...defaultNodeProps, + }, + { + id: "end", + type: "endNode", + selectable: false, + ...defaultNodeProps, + }, + ] as Node[]; +}; + +const initEdges = [ + { + id: "condition-swap", + source: "condition", + target: "swap", + type: "addHook", + ...defaultEdgeProps, + }, + { + id: "swap-end", + source: "swap", + target: "end", + type: "addHook", + ...defaultEdgeProps, + }, +]; export default function PlaygroundPage() { + const { + safe: { safeAddress, chainId }, + } = useSafeAppsSDK(); + const [initNodes, setInitNodes] = useState[]>([]); + + useEffect(() => { + createInitNodes(chainId as ChainId, safeAddress as Address).then( + (nodes) => { + setInitNodes(nodes); + } + ); + }, [chainId, safeAddress]); + + if (initNodes.length === 0) { + return ; + } + return (
@@ -17,7 +91,7 @@ export default function PlaygroundPage() {
- +
diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 123fa09..bf6ba73 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -2,9 +2,9 @@ import "reactflow/dist/base.css"; -import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import React, { useCallback } from "react"; import ReactFlow, { + Edge, EdgeChange, getConnectedEdges, getIncomers, @@ -15,17 +15,15 @@ import ReactFlow, { useNodesState, useReactFlow, } from "reactflow"; -import { Address } from "viem"; -import { INodeData, IStopLossConditionData, ISwapData } from "#/lib/types"; +import { INodeData } from "#/lib/types"; import { defaultEdgeProps } from "./edges"; import { AddHookEdge } from "./edges/AddHookEdge"; -import { defaultNodeProps } from "./nodes"; import { EndNode } from "./nodes/EndNode"; import { MultiSendNode } from "./nodes/MultiSendNode"; -import { defaultStopLossData, StopLossNode } from "./nodes/StopLossNode"; -import { getDefaultSwapData, SwapNode } from "./nodes/SwapNode"; +import { StopLossNode } from "./nodes/StopLossNode"; +import { SwapNode } from "./nodes/SwapNode"; const nodeTypes = { swap: SwapNode, @@ -45,65 +43,20 @@ export const getLayoutedNodes = (nodes: Node[]) => { })); }; -const initEdges = [ - { - id: "condition-swap", - source: "condition", - target: "swap", - type: "addHook", - ...defaultEdgeProps, - }, - { - id: "swap-end", - source: "swap", - target: "end", - type: "addHook", - ...defaultEdgeProps, - }, -]; - -const createInitNodes = ( - conditionData: IStopLossConditionData, - swapData: ISwapData -) => { - return [ - { - id: "condition", - type: "stopLoss", - data: conditionData, - ...defaultNodeProps, - }, - { - id: "swap", - type: "swap", - data: swapData, - ...defaultNodeProps, - }, - { - id: "end", - type: "endNode", - selectable: false, - ...defaultNodeProps, - }, - ] as Node[]; -}; - -export const Board = () => { - const { - safe: { safeAddress, chainId }, - } = useSafeAppsSDK(); - const layoutedNodes = getLayoutedNodes( - createInitNodes( - defaultStopLossData, - getDefaultSwapData(chainId, safeAddress as Address) - ) - ); - - const [nodes, setNodes, onNodesChange] = - useNodesState(layoutedNodes); +export function Board({ + initNodes, + initEdges, +}: { + initNodes: Node[]; + initEdges: Edge[]; +}) { const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges); const { fitView } = useReactFlow(); + const [nodes, setNodes, onNodesChange] = useNodesState( + getLayoutedNodes(initNodes) + ); + const onNodesDelete = useCallback( (deleted: Node[]) => { if (deleted.some((node) => node.id === "swap" || node.id === "condition")) @@ -191,6 +144,6 @@ export const Board = () => { className="bg-blue2 size-full rounded-md shadow-md" /> ); -}; +} export function Flow() {} diff --git a/src/components/menus/MultiSendMenu.tsx b/src/components/menus/MultiSendMenu.tsx index da885a9..efe9534 100644 --- a/src/components/menus/MultiSendMenu.tsx +++ b/src/components/menus/MultiSendMenu.tsx @@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { PlusIcon, TrashIcon } from "@radix-ui/react-icons"; import { useState } from "react"; import { FieldValues, useForm } from "react-hook-form"; +import { useReactFlow } from "reactflow"; import { multiSendHookSchema } from "#/lib/schema"; import { IToken } from "#/lib/types"; @@ -13,11 +14,11 @@ import { TokenSelect } from "../TokenSelect"; import { Form } from "../ui/form"; export function MultiSendMenu({ - onSubmit, + id, defaultValues, }: { + id: string; defaultValues: FieldValues; - onSubmit: (data: FieldValues) => void; }) { const form = useForm({ resolver: zodResolver(multiSendHookSchema), @@ -27,6 +28,22 @@ export function MultiSendMenu({ const [lengthOfArguments, setLengthOfArguments] = useState(1); const formData = watch(); + const { setNodes, getNodes } = useReactFlow(); + + const onSubmit = (formData: FieldValues) => { + const newNodes = getNodes().map((node) => { + if (node.id === id) { + return { + ...node, + data: { ...node.data, ...formData }, + selected: false, + }; + } + return node; + }); + setNodes(newNodes); + }; + return (
diff --git a/src/components/menus/StopLossConditionMenu.tsx b/src/components/menus/StopLossConditionMenu.tsx index a8e1940..45f1c84 100644 --- a/src/components/menus/StopLossConditionMenu.tsx +++ b/src/components/menus/StopLossConditionMenu.tsx @@ -3,8 +3,10 @@ import { slateDarkA } from "@radix-ui/colors"; import { InfoCircledIcon } from "@radix-ui/react-icons"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { Controller, FieldValues, useForm } from "react-hook-form"; +import { useReactFlow } from "reactflow"; -import { stopLossConditionSchema } from "#/lib/schema"; +import { ChainId } from "#/lib/publicClients"; +import { generateStopLossConditionSchema } from "#/lib/schema"; import { IStopLossRecipeData, TIME_OPTIONS } from "#/lib/types"; import { buildBlockExplorerAddressURL } from "#/lib/utils"; @@ -26,22 +28,41 @@ const MAX_TIME_SINCE_LAST_ORACLE_UPDATE_TOOLTIP_TEXT = "If both oracle has not been updated within this time, the order will not be posted."; export function StopLossConditionMenu({ + id, data, defaultValues, - onSubmit, }: { + id: string; data: IStopLossRecipeData; defaultValues: FieldValues; - onSubmit: (data: FieldValues) => void; }) { + const { + safe: { chainId }, + } = useSafeAppsSDK(); + const stopLossConditionSchema = generateStopLossConditionSchema({ + chainId: chainId as ChainId, + }); const form = useForm({ resolver: zodResolver(stopLossConditionSchema), defaultValues, }); const { control } = form; - const { - safe: { chainId }, - } = useSafeAppsSDK(); + + const { setNodes, getNodes } = useReactFlow(); + + const onSubmit = (formData: FieldValues) => { + const newNodes = getNodes().map((node) => { + if (node.id === id) { + return { + ...node, + data: { ...node.data, ...formData }, + selected: false, + }; + } + return node; + }); + setNodes(newNodes); + }; return ( @@ -64,10 +85,12 @@ export function StopLossConditionMenu({ name="tokenSellOracle" label={`${data.tokenSell?.symbol} Oracle`} tooltipLink={ - buildBlockExplorerAddressURL({ - chainId, - address: data.tokenSellOracle, - })?.url + data.tokenSellOracle + ? buildBlockExplorerAddressURL({ + chainId, + address: data.tokenSellOracle, + })?.url + : undefined } tooltipText={ORACLE_TOOLTIP_TEXT} /> @@ -75,10 +98,12 @@ export function StopLossConditionMenu({ name="tokenBuyOracle" label={`${data.tokenBuy?.symbol} Oracle`} tooltipLink={ - buildBlockExplorerAddressURL({ - chainId, - address: data.tokenBuyOracle, - })?.url + data.tokenBuyOracle + ? buildBlockExplorerAddressURL({ + chainId, + address: data.tokenBuyOracle, + })?.url + : undefined } tooltipText={ORACLE_TOOLTIP_TEXT} /> diff --git a/src/components/menus/StopLossRecipeMenu.tsx b/src/components/menus/StopLossRecipeMenu.tsx index f37e15c..4aa4b71 100644 --- a/src/components/menus/StopLossRecipeMenu.tsx +++ b/src/components/menus/StopLossRecipeMenu.tsx @@ -2,6 +2,7 @@ import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { calculateAmounts } from "#/lib/calculateAmounts"; import { IStopLossRecipeData } from "#/lib/types"; +import { formatNumber } from "#/lib/utils"; import { Spinner } from "../Spinner"; import { TokenInfo } from "../TokenInfo"; @@ -32,7 +33,7 @@ export function StopLossRecipeMenu({ data }: { data?: IStopLossRecipeData }) {
Condition: - {data.strikePrice} {data.tokenSell.symbol} / + {formatNumber(data.strikePrice, 4)} {data.tokenSell.symbol} /{" "} {data.tokenBuy.symbol}
diff --git a/src/components/menus/SwapMenu.tsx b/src/components/menus/SwapMenu.tsx index 6270b9c..1a59a90 100644 --- a/src/components/menus/SwapMenu.tsx +++ b/src/components/menus/SwapMenu.tsx @@ -3,9 +3,11 @@ import { slateDarkA } from "@radix-ui/colors"; import { InfoCircledIcon } from "@radix-ui/react-icons"; import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { Controller, FieldValues, useForm } from "react-hook-form"; +import { useReactFlow } from "reactflow"; import { Address, formatUnits } from "viem"; import { useSafeBalances } from "#/hooks/useSafeBalances"; +import { CHAINS_ORACLE_ROUTER_FACTORY } from "#/lib/oracleRouter"; import { ChainId } from "#/lib/publicClients"; import { generateSwapSchema } from "#/lib/schema"; import { ISwapData, TIME_OPTIONS } from "#/lib/types"; @@ -36,12 +38,10 @@ const VALIDITY_BUCKET_TIME_TOOLTIP_TEXT = export function SwapMenu({ data, - onSubmit, defaultValues, }: { data: ISwapData; defaultValues: FieldValues; - onSubmit: (data: FieldValues) => void; }) { const { fetchBalance } = useSafeBalances(); const { @@ -73,6 +73,50 @@ export function SwapMenu({ amountDecimals ); + const { setNodes, getNodes } = useReactFlow(); + + const oracleRouterFactory = CHAINS_ORACLE_ROUTER_FACTORY[chainId as ChainId]; + + const onSubmit = async (formData: FieldValues) => { + const oracleRouter = new oracleRouterFactory({ + chainId: chainId as ChainId, + tokenSell: formData.tokenSell, + tokenBuy: formData.tokenBuy, + }); + + const oracles = await oracleRouter.findRoute().catch(() => { + return { tokenSellOracle: undefined, tokenBuyOracle: undefined }; + }); + + const oracleError = !(oracles.tokenSellOracle && oracles.tokenBuyOracle); + + const strikePrice = oracleError ? 0 : oracleRouter.calculatePrice(oracles); + + const newNodes = getNodes().map((node) => { + if (node.id === "swap") { + return { + ...node, + data: { ...node.data, ...formData }, + selected: false, + }; + } else if (node.id === "condition") { + return { + ...node, + data: { + ...node.data, + tokenSellOracle: oracles.tokenSellOracle, + tokenBuyOracle: oracles.tokenBuyOracle, + strikePrice, + oracleError, + }, + selected: false, + }; + } + return node; + }); + setNodes(newNodes); + }; + return (
diff --git a/src/components/menus/index.tsx b/src/components/menus/index.tsx index 6519c5f..b15e940 100644 --- a/src/components/menus/index.tsx +++ b/src/components/menus/index.tsx @@ -1,7 +1,6 @@ import { useSafeAppsSDK } from "@safe-global/safe-apps-react-sdk"; import { useEffect, useState } from "react"; -import { FieldValues } from "react-hook-form"; -import { Node, useNodes, useReactFlow } from "reactflow"; +import { Node, useNodes } from "reactflow"; import { Address } from "viem"; import { FALLBACK_STATES, useFallbackState } from "#/hooks/useFallbackState"; @@ -151,7 +150,9 @@ function DefaultMenu({ data }: { data: IStopLossRecipeData }) { )}