From fb3663b1044a25a4a86ff7a5918e3626619d83b2 Mon Sep 17 00:00:00 2001 From: aliza Date: Tue, 11 Jun 2024 09:49:11 +0330 Subject: [PATCH 1/7] feat: add multi-swap --- appic_frontend/app/layout.jsx | 1 - appic_frontend/app/page.jsx | 4 +- appic_frontend/components/MultiSwap.jsx | 187 ++++++++++++++ appic_frontend/components/MultiSwapTable.jsx | 65 +++++ .../components/higerOrderComponents/modal.jsx | 2 - appic_frontend/components/sidebar.jsx | 8 +- appic_frontend/components/walletTokens.jsx | 1 - appic_frontend/redux/store.js | 1 - appic_frontend/styles/style.scss | 3 +- .../higherOrderComponents/modal.scss | 36 ++- .../styles/styleComponents/multi-swap.scss | 240 ++++++++++++++++++ appic_frontend/utils/darkClassGenerator.js | 13 +- 12 files changed, 541 insertions(+), 20 deletions(-) create mode 100644 appic_frontend/components/MultiSwap.jsx create mode 100644 appic_frontend/components/MultiSwapTable.jsx create mode 100644 appic_frontend/styles/styleComponents/multi-swap.scss diff --git a/appic_frontend/app/layout.jsx b/appic_frontend/app/layout.jsx index 1939ab3..dba2b25 100644 --- a/appic_frontend/app/layout.jsx +++ b/appic_frontend/app/layout.jsx @@ -26,4 +26,3 @@ export default function RootLayout({ children }) { ); } - diff --git a/appic_frontend/app/page.jsx b/appic_frontend/app/page.jsx index c9d23fe..f6aa632 100644 --- a/appic_frontend/app/page.jsx +++ b/appic_frontend/app/page.jsx @@ -7,6 +7,7 @@ import { useDispatch, useSelector } from 'react-redux'; import LoadingComponent from '@/components/higerOrderComponents/loadingComponent'; import Sidebar from '@/components/sidebar'; import DCA from '@/components/dcaRoot'; +import MultiSwap from '@/components/MultiSwap'; export default function Home() { const dispatch = useDispatch(); const [activeComponent, setActiveComponent] = useState(''); @@ -20,11 +21,10 @@ export default function Home() { <> {activeComponent == '' && } {activeComponent == 'DCA' && } + {activeComponent == 'MULTI-SWAP' && } )} - ; ); } - diff --git a/appic_frontend/components/MultiSwap.jsx b/appic_frontend/components/MultiSwap.jsx new file mode 100644 index 0000000..99d7b7d --- /dev/null +++ b/appic_frontend/components/MultiSwap.jsx @@ -0,0 +1,187 @@ +'use client'; + +import { useSelector } from 'react-redux'; +import LoadingComponent from './higerOrderComponents/loadingComponent'; +import { useState } from 'react'; +import MultiSwapTable from './MultiSwapTable'; +import Modal from './higerOrderComponents/modal'; +import darkModeClassnamegenerator, { darkClassGenerator } from '@/utils/darkClassGenerator'; +function MultiSwap() { + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalSearchValue, setModalSearchValue] = useState(''); + + const allTokens = useSelector((state) => state.supportedTokens.tokens); + const loader = useSelector((state) => state.wallet.items.loader); + const totalBalance = useSelector((state) => state.wallet.items.totalBalance); + const ownedTokens = useSelector((state) => state.wallet.items.assets); + const ownedTokensWithAddedProperties = ownedTokens.map((ownedToken) => { + const percentage = (ownedToken.usdBalance / totalBalance) * 100; + return { ...ownedToken, percentage, value: ownedToken.usdBalance, newValue: ownedToken.usdBalance, newPercentage: percentage }; + }); + + const [swapTokens, setSwapTokens] = useState(ownedTokensWithAddedProperties); + + const [checkedTokens, setCheckedTokens] = useState(() => swapTokens.map((swapToken) => swapToken.id)); + const filteredAllTokens = allTokens.filter((token) => token.name.toLowerCase().includes(modalSearchValue)); + + const handleSwapTokens = (addedTokens) => { + setSwapTokens([...swapTokens, ...addedTokens]); + }; + + const handleModalClose = () => { + const result = checkedTokens.filter((tokenId) => swapTokens.some((swapToken) => swapToken.id === tokenId)); + setCheckedTokens(result); + setIsModalOpen(false); + }; + + const handleTokenUpdate = (tokenId, newPercentage) => { + const newSwapTokens = swapTokens.map((swapToken) => { + if (swapToken.id !== tokenId) return swapToken; + const newValue = (totalBalance * newPercentage) / 100; + return { ...swapToken, newValue, newPercentage }; + }); + + setSwapTokens(newSwapTokens); + }; + + const handleConfirmSwap = () => { + console.log('Swap Confirmed Successfully!'); + }; + + const calcTotalAndLeft = () => { + const totall = swapTokens.reduce((accumulator, token) => { + return accumulator + Number(token.newPercentage); + }, 0); + + const left = totall > 100 ? 0 : 100 - totall; + + return [totall, left]; + }; + + const [totall, left] = calcTotalAndLeft(); + + const handleModalSearch = (e) => { + setModalSearchValue(e.target.value); + }; + + const handleCheckBox = (tokenId) => { + const isAlreadyChecked = checkedTokens.includes(tokenId); + if (isAlreadyChecked) { + setCheckedTokens([...checkedTokens].filter((checkedTokenId) => tokenId !== checkedTokenId)); + } else { + setCheckedTokens([...checkedTokens, tokenId]); + } + }; + + const handleAddTokens = () => { + const toAddTokens = allTokens + .filter((token) => { + return checkedTokens.includes(token.id); + }) + .map((toAddToken) => { + return { ...toAddToken, value: 0, percentage: 0, newValue: 0, newPercentage: 0 }; + }) + .filter((toAddToken) => ownedTokens.some((ownedToken) => toAddToken.id !== ownedToken.id)); + + setSwapTokens([...toAddTokens, ...ownedTokensWithAddedProperties]); + setIsModalOpen(false); + }; + + return ( +
+ {loader && } + + {totall > 100 ?

Totall percentage should NOT exceed 100%

: null} +
+ + +

+ Total: {'~' + totall + '%'} + {left + '% Left'} +

+
+ + +
+
+ +

Select a token

+ +
+
+ + + + +
+
+ +
+
+ {filteredAllTokens?.map((token) => { + return ( +
{ + // handleTokenSelection(token); + }} + key={token.id} + className="token token--multi-swap" + > +
+ +
+

{token.name}

+

{token.symbol}

+
+
+ {/*
+

{token.price}

+
*/} + handleCheckBox(token.id)} + checked={checkedTokens.includes(token.id)} + disabled={ownedTokens.some((ownedToken) => ownedToken.id === token.id)} + /> +
+ ); + })} +
+ +
+
+
+ ); +} + +export default MultiSwap; diff --git a/appic_frontend/components/MultiSwapTable.jsx b/appic_frontend/components/MultiSwapTable.jsx new file mode 100644 index 0000000..5a5eded --- /dev/null +++ b/appic_frontend/components/MultiSwapTable.jsx @@ -0,0 +1,65 @@ +'use client'; + +import { formatSignificantNumber } from '@/helper/number_formatter'; + +// Utility function to pick specified properties from an array of objects +const pickPropertiesFromArray = (array, properties) => { + return array.map((item) => { + return properties.reduce((acc, prop) => { + if (item.hasOwnProperty(prop)) { + acc[prop] = item[prop]; + } + return acc; + }, {}); + }); +}; + +const SELECTED_PROPERTIES = ['id', 'logo', 'name', 'price', 'value', 'percentage', 'newValue', 'newPercentage']; + +const MultiSwapTable = ({ swapTokens = [], onUpdate }) => { + console.log('sss', swapTokens); + const swapTokensWithSelectedProperties = pickPropertiesFromArray(swapTokens, SELECTED_PROPERTIES); + + // Extract the headers from the keys of the first object in the data array + const headers = swapTokensWithSelectedProperties.length > 0 ? Object.keys(swapTokensWithSelectedProperties[0]) : []; + + const getProperJSXForCell = (row, header) => { + switch (header) { + case 'logo': + return token logo; + case 'name': + return row[header]; + case 'newPercentage': + return ( +
+ onUpdate(row.id, e.target.value)} /> % +
+ ); + default: + return formatSignificantNumber(row[header]); + } + }; + + return ( +
+ + + {headers.map((header) => (header === 'id' ? null : ))} + + + {swapTokensWithSelectedProperties.map((row, index) => ( + + {headers.map((header) => { + if (header === 'id') return null; + return ; + })} + + ))} + +
{header.charAt(0).toUpperCase() + header.slice(1)}
{getProperJSXForCell(row, header)}
+
+ ); +}; + +export default MultiSwapTable; + diff --git a/appic_frontend/components/higerOrderComponents/modal.jsx b/appic_frontend/components/higerOrderComponents/modal.jsx index 42c86ec..1b2bb18 100644 --- a/appic_frontend/components/higerOrderComponents/modal.jsx +++ b/appic_frontend/components/higerOrderComponents/modal.jsx @@ -1,6 +1,5 @@ 'use client'; import darkModeClassnamegenerator from '@/utils/darkClassGenerator'; -import { useSelector } from 'react-redux'; function Modal({ children, active }) { return ( @@ -13,4 +12,3 @@ function Modal({ children, active }) { } export default Modal; - diff --git a/appic_frontend/components/sidebar.jsx b/appic_frontend/components/sidebar.jsx index 212317d..35868d7 100644 --- a/appic_frontend/components/sidebar.jsx +++ b/appic_frontend/components/sidebar.jsx @@ -50,7 +50,12 @@ function Sidebar({ setActiveComponent, activeComponent }) {

Auto Invest

-
+
{ + setActiveComponent('MULTI-SWAP'); + }} + className={`sidebar__options ${activeComponent == 'MULTI-SWAP' ? 'active' : ''} `} + > @@ -86,4 +91,3 @@ function Sidebar({ setActiveComponent, activeComponent }) { } export default Sidebar; - diff --git a/appic_frontend/components/walletTokens.jsx b/appic_frontend/components/walletTokens.jsx index 4467c3f..49a0d51 100644 --- a/appic_frontend/components/walletTokens.jsx +++ b/appic_frontend/components/walletTokens.jsx @@ -142,4 +142,3 @@ function WalletTokens({ setEditMode }) { } export default WalletTokens; - diff --git a/appic_frontend/redux/store.js b/appic_frontend/redux/store.js index 27d4553..fd273f3 100644 --- a/appic_frontend/redux/store.js +++ b/appic_frontend/redux/store.js @@ -30,4 +30,3 @@ export const store = configureStore({ }); export const getState = store.getState; - diff --git a/appic_frontend/styles/style.scss b/appic_frontend/styles/style.scss index 933472b..0280cbd 100644 --- a/appic_frontend/styles/style.scss +++ b/appic_frontend/styles/style.scss @@ -224,7 +224,8 @@ a { @import './styleComponents/DCAtransactionModal.scss'; @import './styleComponents/DCApositions.scss'; @import './styleComponents/createDCA.scss'; +@import './styleComponents/multi-swap.scss'; + //pages @import './pages/main.scss'; @import './pages/dca.scss'; - diff --git a/appic_frontend/styles/styleComponents/higherOrderComponents/modal.scss b/appic_frontend/styles/styleComponents/higherOrderComponents/modal.scss index dd3c72d..ce1ec84 100644 --- a/appic_frontend/styles/styleComponents/higherOrderComponents/modal.scss +++ b/appic_frontend/styles/styleComponents/higherOrderComponents/modal.scss @@ -8,6 +8,10 @@ overflow: hidden; pointer-events: none; + .tokens { + margin-bottom: 3rem; + } + .bg { width: 100%; height: 100%; @@ -100,7 +104,7 @@ opacity: 1; pointer-events: all; .container { - height: auto; + min-height: 40vh; max-height: 70%; padding: 2% 0; } @@ -112,9 +116,37 @@ .container { background-color: $dark-bg; + + .addTokenModal .topSection .closeBTN { + color: rgb(211, 57, 57); + } + + .title { + color: rgb(150, 150, 150); + } + .searchContainer { + background-color: $dark-tertiary; + + input { + color: inherit; + } + } + + .token--multi-swap { + input[type='checkbox']:disabled + .token-check-label { + background-color: rgba(white, $alpha: 0.1); + border: 2px solid rgb(86, 86, 86); + } + } + .token { + border-radius: 1rem; + background-color: $dark-tertiary; + .token_info .token_name { + color: white; + } + } } } } } } - diff --git a/appic_frontend/styles/styleComponents/multi-swap.scss b/appic_frontend/styles/styleComponents/multi-swap.scss new file mode 100644 index 0000000..6655511 --- /dev/null +++ b/appic_frontend/styles/styleComponents/multi-swap.scss @@ -0,0 +1,240 @@ +.multi-swap { + padding: 4rem; + background-color: white; + border-radius: 36px; + margin-top: 2rem; + border: 1px solid $light-border; + overflow: hidden; + color: $light-primary; + + &.dark { + border-color: $dark-border; + background-color: $dark-box-bg; + color: $dark-primary; + + .warning-message { + background-color: $dark-tertiary; + color: rgb(255, 40, 40); + } + + .cta-container { + .btn:disabled { + color: rgb(76, 74, 74); + background-color: rgba(255, 255, 255, 0.05); + } + + p > span + span { + text-align: center; + width: 100px; + padding: 1rem; + background-color: $dark-tertiary; + border-radius: 2rem; + color: rgb(221, 53, 53); + } + } + + .multi-swap-table { + th { + color: $dark-secondary; + } + + tr { + border-color: $dark-border; + } + + td { + .new-percentage { + background-color: $dark-tertiary; + } + } + } + } + + .warning-message { + text-align: center; + color: rgb(236, 66, 66); + font-size: 16px; + background-color: rgb(248, 240, 240); + padding: 1rem; + border-radius: 1rem; + margin-block: 2rem; + } + + .multi-swap-table-container { + overflow-x: auto; + } + + .multi-swap-table { + width: 100%; + min-width: 700px; + margin-bottom: 2rem; + font-size: 14px; + + th { + color: $light-secondary; + padding: 2rem; + } + + tr { + border-bottom: 1px solid $light-border; + } + + td { + height: 5rem; + text-align: center; + vertical-align: middle; + padding: 1rem; + + img { + max-height: 100%; + } + + .new-percentage { + font-weight: bold; + margin: 0 auto; + width: 70px; + padding: 1rem; + background-color: #f7eff9; + color: $light-secondary; + border-radius: 100px; + + * { + font-weight: bold; + } + } + + input[type='number'] { + width: 50%; + font-size: 14px; + color: $light-secondary; + outline: none; + background-color: transparent; + -moz-appearance: textfield; + } + + input[type='number']::-webkit-outer-spin-button, + input[type='number']::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; + } + } + } + + .cta-container { + margin-top: 2rem; + display: flex; + align-items: center; + gap: 2rem; + + @media screen and (max-width: 700px) { + flex-direction: column; + } + + .btn { + font-size: 14px; + padding: 1rem 1.7rem; + border-radius: 4px; + background-color: $light-buttons; + color: white; + cursor: pointer; + transition: all 0.2s ease-in-out; + &:not(:disabled):hover { + color: $light-secondary; + background-color: transparent; + box-shadow: 0px 0px 0px 2px $light-secondary; + } + + &:disabled { + color: rgb(168, 168, 168); + background-color: #d7d7d7; + cursor: not-allowed; + } + + @media screen and (max-width: 700px) { + width: 100%; + } + } + + .btn-add, + .btn-confirm { + display: flex; + gap: 1rem; + align-items: center; + justify-content: center; + // vertical-align: middle; + } + + p { + display: flex; + align-items: center; + gap: 1rem; + margin-left: 3rem; + font-weight: bold; + font-size: 16px; + color: $light-secondary; + + span { + margin-left: 1rem; + font-weight: normal; + color: rgb(22, 164, 22); + } + + span + span { + text-align: center; + width: 100px; + padding: 1rem; + background-color: rgb(252, 243, 243); + border-radius: 2rem; + color: rgb(255, 71, 71); + } + } + } + + .token--multi-swap { + position: relative; + margin-right: 1.25rem; + } + + .token_balance--multi-swap { + margin-left: auto; + margin-right: 1rem; + } + + .token-check-label { + position: absolute; + cursor: pointer; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + input[type='checkbox']:checked + .token-check-label { + border: 2px solid teal; + border-radius: 1rem; + } + + input[type='checkbox']:disabled + .token-check-label { + border: 2px solid rgb(205, 205, 205); + } + .confirm-add-swap-tokens { + margin-top: auto; + margin-bottom: 2rem; + font-size: 16px; + padding: 1rem 2.5rem; + border-radius: 4px; + background-color: $light-buttons; + color: white; + cursor: pointer; + display: flex; + gap: 1rem; + align-items: center; + transition: all 0.2s ease-in-out; + + &:hover { + color: $light-secondary; + background-color: transparent; + box-shadow: 0px 0px 0px 2px $light-secondary; + } + } +} diff --git a/appic_frontend/utils/darkClassGenerator.js b/appic_frontend/utils/darkClassGenerator.js index 57d67ef..bd49bf4 100644 --- a/appic_frontend/utils/darkClassGenerator.js +++ b/appic_frontend/utils/darkClassGenerator.js @@ -1,7 +1,7 @@ -"use client"; +'use client'; -import { getState } from "@/redux/store"; -import { useSelector } from "react-redux"; +import { getState } from '@/redux/store'; +import { useSelector } from 'react-redux'; // function for generating class name for dark mode @@ -29,13 +29,10 @@ export default function darkModeClassnamegenerator(className, activable) { } } - -export function darkClassGenerator(isDark,className) { - +export function darkClassGenerator(isDark, className) { if (isDark == true) { return `${className} dark`; } else { return `${className} `; } - -} \ No newline at end of file +} From 485a630972a970e469bf75d7d8a659799464a697 Mon Sep 17 00:00:00 2001 From: Chitranshu Date: Sat, 15 Jun 2024 01:33:56 +0530 Subject: [PATCH 2/7] transfer to multiswap --- appic_frontend/components/MultiSwap.jsx | 110 +++++++++++++++++- appic_frontend/config/canistersIDs.js | 1 + .../did/appic/appic_multiswap.did.js | 82 +++++++++++++ appic_frontend/did/index.js | 10 +- appic_frontend/package-lock.json | 74 +++++++++--- appic_frontend/package.json | 2 + 6 files changed, 260 insertions(+), 19 deletions(-) create mode 100644 appic_frontend/did/appic/appic_multiswap.did.js diff --git a/appic_frontend/components/MultiSwap.jsx b/appic_frontend/components/MultiSwap.jsx index 99d7b7d..440a39b 100644 --- a/appic_frontend/components/MultiSwap.jsx +++ b/appic_frontend/components/MultiSwap.jsx @@ -6,6 +6,13 @@ import { useState } from 'react'; import MultiSwapTable from './MultiSwapTable'; import Modal from './higerOrderComponents/modal'; import darkModeClassnamegenerator, { darkClassGenerator } from '@/utils/darkClassGenerator'; +import BigNumber from 'bignumber.js'; +import { BatchTransact } from '@/artemis-web3-adapter'; +import { AppicMultiswapidlFactory, icrcIdlFactory, dip20IdleFactory } from '@/did'; +import canistersIDs from '@/config/canistersIDs'; +import { artemisWalletAdapter } from '@/utils/walletConnector'; +import { Principal } from '@dfinity/principal'; +import { AccountIdentifier, SubAccount } from '@dfinity/ledger-icp'; function MultiSwap() { const [isModalOpen, setIsModalOpen] = useState(false); const [modalSearchValue, setModalSearchValue] = useState(''); @@ -44,8 +51,106 @@ function MultiSwap() { setSwapTokens(newSwapTokens); }; - const handleConfirmSwap = () => { + const handleConfirmSwap = async () => { console.log('Swap Confirmed Successfully!'); + + let sellTokens = []; + let buyTokens = []; + + swapTokens.forEach((token) => { + let newPercentage = parseFloat(token.newPercentage); + if (token.percentage - newPercentage > 0) { + // Calculate amtSell + let balance = new BigNumber(token.balance || 0); + let percentageDiff = new BigNumber(token.percentage).minus(newPercentage); + let amtSell = percentageDiff.times(balance).div(100).toFixed(0); // No decimals + + // Add amtSell property + token.amtSell = amtSell; + + sellTokens.push(token); + } else if (token.percentage - newPercentage < 0) { + buyTokens.push(token); + } + }); + let sellTokenIds = sellTokens.map((token) => token.id); + buyTokens = buyTokens.filter((token) => !sellTokenIds.includes(token.id)); + + console.log('Selling Tokens', sellTokens); + console.log('Buying Tokens', buyTokens); + + try { + let AppicActor = await artemisWalletAdapter.getCanisterActor(canistersIDs.APPIC_MULTISWAP, AppicMultiswapidlFactory, false); + const subAccount = await AppicActor.getSubAccount(); + + let transactions = {}; + + for (let i = 0; i < sellTokens.length; i++) { + if (sellTokens[i].tokenType === 'ICRC1') { + // transactions[sellTokens[i].id] = { + // canisterId: Principal.fromText(sellTokens[i].id), + // idl: icrcIdlFactory, + // methodName: 'icrc1_transfer', + // args: [ + // { + // to: { owner: Principal.fromText(canistersIDs.APPIC_MULTISWAP), subaccount: Array.from(subAccount) }, + // fee: [], + // memo: [], + // from_subaccount: [], + // created_at_time: [], + // amount: BigNumber(sellTokens[i].amtSell).toNumber(), + // }, + // ], + // }; + + console.log('---------', subAccount); + let icrc1 = await artemisWalletAdapter.getCanisterActor(sellTokens[i].id, icrcIdlFactory, false); + const tx = await icrc1.icrc1_transfer({ + to: { + owner: Principal.fromText(canistersIDs.APPIC_MULTISWAP), + subaccount: subAccount, + }, + fee: [], + memo: [], + from_subaccount: [], + created_at_time: [], + amount: BigNumber(sellTokens[i].amtSell).toNumber(), + }); + } else if (sellTokens[i].tokenType === 'ICRC2') { + transactions[sellTokens[i].id] = { + canisterId: Principal.fromText(sellTokens[i].id), + idl: icrcIdlFactory, + methodName: 'icrc2_approve', + args: [ + { + fee: [], + memo: [], + from_subaccount: [], + created_at_time: [], + expected_allowance: [], + expires_at: [], + amount: BigNumber(sellTokens[i].amtSell).toNumber(), + spender: { owner: Principal.fromText(canistersIDs.APPIC_MULTISWAP), subaccount: [] }, + }, + ], + }; + } else if (sellTokens[i].tokenType === 'YC' || sellTokens[i].tokenType === 'DIP20') { + transactions[sellTokens[i].id] = { + canisterId: Principal.fromText(sellTokens[i].id), + idl: dip20IdleFactory, + methodName: 'approve', + args: [Principal.fromText(canistersIDs.APPIC_MULTISWAP), BigNumber(sellTokens[i].amtSell).toNumber()], + }; + } + } + console.log(transactions); + // Execute transaction for calling approve function + let transactionsList = new BatchTransact(transactions, artemisWalletAdapter); + console.log(transactionsList); + await transactionsList.execute(); + } catch (error) { + console.log(error.message); + } }; const calcTotalAndLeft = () => { @@ -91,7 +196,7 @@ function MultiSwap() {
{loader && } - {totall > 100 ?

Totall percentage should NOT exceed 100%

: null} + {totall > 100 ?

Total percentage should NOT exceed 100%

: null}