From 82b281b8d7cd9de313d19e2ff3324341125ad5b0 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 10 Jul 2024 19:21:16 +0200 Subject: [PATCH] 42 integrate axelar bridge to polygon prototype (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use of pen ephemeral account, trigger transfer when tokens received * Phase 6b (#44) * Execute Nabla after SEP-24 * Some fixes * Fix `yarn test` not working by importing a preset that enhances transpiling vite code * Fix errors: useQuery result must not be undefined * Update min offramp amount for nTokens * Fix typo --------- Co-authored-by: Marcel Ebert * use pen ephemeral address in performSwap * Refactor wagmi config * Add contract abis * Implement swap with squid router * wip * Refactor squidrouter service * Small refactoring * Use swap in Inputkeys * Fix transaction status issues * cleanup old prototype logic * wip. testing integration of squidrouter * Make USDC the default selection * Delete duplicate /contracts/Erc20.ts * format, remove abi duplicate * testing ephemeral funding * Add logic to fund ephemeral account on Pendulum * Change USDC ERC20 address * Add funding account phrase * Fix signing transaction to fund ephemeral acc * trigger funding of eph. account after swap, fix deposit event listener * wip. testing * Fix lint errors * Use USDC instead of USDC.e * Increase funding account * Add quickfix with apiplus * Use public horizon in signing service * Fix errors * Remove unused wallet components * Change slippageBasisPoints from 20 to 30 * crop input box for mobile * cleanup pendulum ephemeral * Fix styling for mobile * Remove media query to avoid conflicts * remove token dust * fix for clean pen ephemeral * use new integrator id and plus squid route * use getROuteApiPlus function * Remove message about minimum EURC amount * Change button for anchor to a element * attempt to transfer all tokens regardless of balance * log ephemeral account on offramp * log ephemeral account after sep is completed --------- Co-authored-by: Gianfranco Co-authored-by: Torsten Stüber <15174476+TorstenStueber@users.noreply.github.com> Co-authored-by: gianfra-t <96739519+gianfra-t@users.noreply.github.com> Co-authored-by: bogdanS98 --- .eslintrc | 5 +- App.css | 19 +- config/babel.jest.cjs | 1 + jest.config.cjs | 3 - package.json | 2 + src/GlobalStateProvider.tsx | 2 +- src/assets/coins/USDC.png | Bin 0 -> 120909 bytes src/components/GenericEvent.tsx | 9 +- src/components/InputKeys/AmountSelector.tsx | 8 +- src/components/InputKeys/From.tsx | 78 ++-- src/components/InputKeys/SelectionModal.tsx | 16 +- src/components/InputKeys/To.tsx | 45 +-- src/components/InputKeys/index.tsx | 230 ++++-------- src/components/Nabla/BalanceState.tsx | 93 ++--- src/components/Nabla/schema.tsx | 11 - src/components/Nabla/useSwapForm.tsx | 92 ++--- src/components/Sep24Component.tsx | 69 +--- src/config/index.ts | 19 +- src/constants/constants.ts | 1 + src/constants/tokenConfig.ts | 40 +- src/contracts/ERC20.ts | 224 +++++++++++ src/contracts/SquidReceiver.ts | 149 ++++++++ src/helpers/contracts.ts | 18 +- src/helpers/transaction.ts | 14 - src/hooks/nabla/useContractRead.ts | 12 +- src/hooks/nabla/useTokenAmountOut.ts | 143 +++---- src/main.tsx | 46 +-- src/pages/landing/index.tsx | 144 +++++-- src/services/anchor/index.ts | 13 +- src/services/nabla.ts | 105 +++--- src/services/polkadot/ephemeral.tsx | 132 +++++++ src/services/polkadot/eventParsers.tsx | 42 +++ src/services/polkadot/index.tsx | 6 +- src/services/squidrouter/config.ts | 21 ++ src/services/squidrouter/index.tsx | 174 +++++++++ src/services/squidrouter/payload.ts | 38 ++ src/services/squidrouter/route.ts | 244 ++++++++++++ src/wagmiConfig.ts | 37 ++ yarn.lock | 395 +++++++++++++++++++- 39 files changed, 1971 insertions(+), 729 deletions(-) create mode 100644 src/assets/coins/USDC.png create mode 100644 src/contracts/ERC20.ts create mode 100644 src/contracts/SquidReceiver.ts delete mode 100644 src/helpers/transaction.ts create mode 100644 src/services/polkadot/ephemeral.tsx create mode 100644 src/services/squidrouter/config.ts create mode 100644 src/services/squidrouter/index.tsx create mode 100644 src/services/squidrouter/payload.ts create mode 100644 src/services/squidrouter/route.ts create mode 100644 src/wagmiConfig.ts diff --git a/.eslintrc b/.eslintrc index f585df85..4d8777a3 100644 --- a/.eslintrc +++ b/.eslintrc @@ -24,11 +24,14 @@ "version": "detect" } }, - "plugins": ["react", "@typescript-eslint", "jest"], + "plugins": ["react", "react-hooks", "@typescript-eslint", "jest"], "rules": { "react/react-in-jsx-scope": "off", "react-hooks/exhaustive-deps": "error", + "react-hooks/rules-of-hooks": "error", "react/prop-types": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "prefer-const": "warn", "@typescript-eslint/no-unused-vars": [ "warn", { diff --git a/App.css b/App.css index 1d27074d..b6c41bb3 100644 --- a/App.css +++ b/App.css @@ -93,7 +93,6 @@ body { #app { width: 100%; - min-width: 1800px; box-sizing: border-box; } @@ -127,27 +126,15 @@ body { } .inputBox.active { - width: 35%; margin-top: 5px; } -.eventsContainer { - display: flex; - flex-direction: column; - align-items: center; - overflow-y: scroll; - max-height: 70vh; -} - input { margin: 10px; padding: 10px; } .eventBox { - margin: 20px auto; - width: 30%; - padding: 20px; border: 1px solid #ccc; box-shadow: 0 0 10px #ccc; transition: all 0.5s ease-in-out; @@ -172,14 +159,11 @@ input { } .eventBox.active { - transform: scale(1.3); + transform: scale(1.2); background-color: #f0f0f0; } .eventBox.error { - margin: 20px auto; - width: 30%; - padding: 20px; border: 1px solid #ccc; box-shadow: 0 0 10px #ccc; transition: all 0.5s ease-in-out; @@ -194,7 +178,6 @@ input { flex-direction: column; align-items: center; justify-content: center; - margin-top: 50px; } @tailwind base; diff --git a/config/babel.jest.cjs b/config/babel.jest.cjs index 1ff9b3ca..51ad48c7 100644 --- a/config/babel.jest.cjs +++ b/config/babel.jest.cjs @@ -10,6 +10,7 @@ module.exports = babelJest.createTransformer({ }, ], '@babel/preset-env', + 'babel-preset-vite', ], plugins: [ [ diff --git a/jest.config.cjs b/jest.config.cjs index 197d4666..36a392be 100644 --- a/jest.config.cjs +++ b/jest.config.cjs @@ -1,8 +1,5 @@ module.exports = { roots: ['/src'], - transform: { - '\\.(ts|tsx)?$': 'babel-jest', - }, testMatch: ['**/?(*.)+(spec|test).{ts,tsx}'], testEnvironment: 'jsdom', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], diff --git a/package.json b/package.json index 6a210642..4e2db19f 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "tailwindcss": "^3.4.3", "viem": "2.x", "wagmi": "^2.10.3", + "web3": "^4.10.0", "yup": "^1.4.0" }, "devDependencies": { @@ -89,6 +90,7 @@ "@types/testing-library__jest-dom": "^5.14.5", "@typescript-eslint/eslint-plugin": "^5.53.0", "@typescript-eslint/parser": "^5.53.0", + "babel-preset-vite": "^1.1.3", "esbuild-plugin-polyfill-node": "^0.3.0", "eslint": "^8.34.0", "eslint-plugin-jest": "^27.2.1", diff --git a/src/GlobalStateProvider.tsx b/src/GlobalStateProvider.tsx index 300e76de..7a0def8a 100644 --- a/src/GlobalStateProvider.tsx +++ b/src/GlobalStateProvider.tsx @@ -46,7 +46,7 @@ const GlobalStateProvider = ({ children }: { children: ComponentChildren }) => { getThemeName, dAppName, }), - [dAppName, getThemeName, tenantName, walletAccount], + [dAppName, getThemeName, tenantName], ); return {children}; diff --git a/src/assets/coins/USDC.png b/src/assets/coins/USDC.png new file mode 100644 index 0000000000000000000000000000000000000000..fb2bc4802d50cfbf3101256f64bebf2e35f3cab3 GIT binary patch literal 120909 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 event, className }, ref) => { - const classes = `eventBox ${className} ${event.status}`; - return ( -

-

{event.value}

+
+ {event.value}
); }); diff --git a/src/components/InputKeys/AmountSelector.tsx b/src/components/InputKeys/AmountSelector.tsx index 9a8934a9..1c172da4 100644 --- a/src/components/InputKeys/AmountSelector.tsx +++ b/src/components/InputKeys/AmountSelector.tsx @@ -57,18 +57,12 @@ export function AmountSelector 20) return `The number you entered is too large`; - } }; const errorMessage = determineErrorMessage(); diff --git a/src/components/InputKeys/From.tsx b/src/components/InputKeys/From.tsx index d392e7d4..590cb6a9 100644 --- a/src/components/InputKeys/From.tsx +++ b/src/components/InputKeys/From.tsx @@ -16,7 +16,7 @@ interface FromProps; - fromTokenBalances: { [key: string]: BalanceInfo }; + tokenBalance: BalanceInfo; offrampStarted: boolean; } @@ -27,12 +27,12 @@ export function From) { const { setValue } = useFormContext(); - const fromTokenBalance = tokenId ? fromTokenBalances[tokenId] : undefined; - + // we can get rid of this and just load USDC balance, not pass tokenBalance object. + const fromTokenBalance = tokenBalance; return (
- {fromToken && ( - Pendulum - )} + {fromToken && Pendulum} {!fromToken && Pendulum} {fromToken?.assetCode || 'Select'}
-
-
- {fromTokenBalance !== undefined && ( - <> - - Your Balance: - - - - - )} -
+
+ {fromTokenBalance !== undefined && ( +
+ + Your Balance: + + + +
+ )}
); diff --git a/src/components/InputKeys/SelectionModal.tsx b/src/components/InputKeys/SelectionModal.tsx index 49eecc21..3683cad4 100644 --- a/src/components/InputKeys/SelectionModal.tsx +++ b/src/components/InputKeys/SelectionModal.tsx @@ -2,7 +2,7 @@ import { Skeleton } from '../Skeleton'; import { Avatar, AvatarProps, Modal, Button, Input, ButtonProps } from 'react-daisyui'; import { ChangeEvent, useMemo, useState } from 'preact/compat'; import { CheckIcon } from '@heroicons/react/20/solid'; -import { TOKEN_CONFIG, TokenDetails } from '../../constants/tokenConfig'; +import { TOKEN_CONFIG, TokenDetails, TokenType } from '../../constants/tokenConfig'; interface PoolSelectorModalProps extends PoolListProps { isLoading?: boolean; @@ -49,13 +49,19 @@ function PoolList({ onSelect, selected, mode }: PoolListProps) { const poolList = useMemo(() => { const poolList: TokenDetails[] = []; - Object.keys(TOKEN_CONFIG).forEach((token) => { + (Object.keys(TOKEN_CONFIG) as TokenType[]).forEach((token) => { // special case rules // do not allow non-offramp tokens in the to field, if (mode.type === 'to' && mode.swap && !TOKEN_CONFIG[token].isOfframp) return; - // only allow USDT asset code - if (mode.type === 'from' && mode.swap && TOKEN_CONFIG[token].assetCode !== 'USDT') return; + // only allow USDC asset code from otherChain property + if ( + mode.type === 'from' && + mode.swap && + TOKEN_CONFIG[token].assetCode !== 'USDC' && + TOKEN_CONFIG[token].isPolygonChain !== true + ) + return; // Do not allow non offrampable tokens in the from field if no swap if (mode.type === 'from' && !mode.swap && !TOKEN_CONFIG[token].isOfframp) return; @@ -97,7 +103,7 @@ function PoolList({ onSelect, selected, mode }: PoolListProps) { diff --git a/src/components/InputKeys/To.tsx b/src/components/InputKeys/To.tsx index 90844c0e..12ac0988 100644 --- a/src/components/InputKeys/To.tsx +++ b/src/components/InputKeys/To.tsx @@ -2,9 +2,10 @@ import { ArrowPathRoundedSquareIcon, ChevronDownIcon } from '@heroicons/react/20 import { useEffect } from 'preact/compat'; import { Button } from 'react-daisyui'; import { useFormContext } from 'react-hook-form'; +import Big from 'big.js'; import pendulumIcon from '../../assets/pendulum-icon.svg'; -import { NumberLoader } from '../Nabla/TokenBalance'; +import { NumberLoader, TokenBalance } from '../Nabla/TokenBalance'; import { Skeleton } from '../Skeleton'; import { SwapFormValues } from '../Nabla/schema'; import { UseTokenOutAmountResult } from '../../hooks/nabla/useTokenAmountOut'; @@ -18,9 +19,7 @@ export interface ToProps { fromToken: TokenDetails | undefined; toToken: TokenDetails | undefined; toAmountQuote: UseTokenOutAmountResult; - fromAmount: number | undefined; - slippage: number; - fromTokenBalances: { [key: string]: BalanceInfo }; + fromAmount: Big | undefined; } export function To({ @@ -30,11 +29,8 @@ export function To({ onOpenSelector, toAmountQuote, fromAmount, - slippage, - fromTokenBalances, }: ToProps): JSX.Element | null { - const toTokenBalance = - fromTokenBalances && tokenId && fromTokenBalances[tokenId] ? fromTokenBalances[tokenId].approximateNumber : 0; + const toTokenBalance = undefined; // replace with use state const [isOpen, { toggle }] = useBoolean(true); @@ -54,9 +50,9 @@ export function To({
{toAmountQuote.isLoading ? ( - ) : toAmountQuote.data !== undefined ? ( + ) : toAmountQuote.data !== undefined && toAmountQuote.data !== null ? ( `${toAmountQuote.data.amountOut.approximateStrings.atLeast4Decimals}` - ) : fromAmount !== undefined && fromAmount > 0 ? ( + ) : fromAmount !== undefined && fromAmount.gt(0) ? (
{/*
{toToken ? : '$ -'}
*/} -
Your balance: {toTokenBalance}
+
+ Your balance:{' '} + {toTokenBalance ? : '0'} +
{`1 ${fromToken.assetCode} = ${toAmountQuote.data.effectiveExchangeRate} ${toToken.assetCode}`} ) : ( `-` @@ -132,22 +125,10 @@ export function To({
N/A
)}
-
-
Minimum received after slippage ({slippage}%)
- {toAmountQuote.data !== undefined && toToken !== undefined ? ( -
- - {toAmountQuote.data !== undefined ? toAmountQuote.data.minAmountOut : ''} {toToken?.assetCode || ''} - -
- ) : ( -
N/A
- )} -
Swap fee:
- {toAmountQuote.data !== undefined ? toAmountQuote.data.swapFee.approximateStrings.atLeast2Decimals : ''}{' '} + {toAmountQuote.data != undefined ? toAmountQuote.data.swapFee.approximateStrings.atLeast2Decimals : ''}{' '} {toToken?.assetCode || ''}
diff --git a/src/components/InputKeys/index.tsx b/src/components/InputKeys/index.tsx index 788238f1..280fc14c 100644 --- a/src/components/InputKeys/index.tsx +++ b/src/components/InputKeys/index.tsx @@ -2,10 +2,11 @@ import React, { useState, useEffect, useCallback } from 'react'; import { ConnectButton } from '@rainbow-me/rainbowkit'; import { getApiManagerInstance } from '../../services/polkadot/polkadotApi'; import { useAccountBalance } from '../Nabla/BalanceState'; -import { TOKEN_CONFIG, TokenDetails } from '../../constants/tokenConfig'; +import { TOKEN_CONFIG, TokenType } from '../../constants/tokenConfig'; import { useTokenOutAmount } from '../../hooks/nabla/useTokenAmountOut'; import { ApiPromise } from '../../services/polkadot/polkadotApi'; import { Button, Card } from 'react-daisyui'; +import { Tabs } from 'react-daisyui'; import { From } from './From'; import { PoolSelectorModal } from './SelectionModal'; import { FormProvider } from 'react-hook-form'; @@ -13,34 +14,26 @@ import { To } from './To'; import { useSwapForm } from '../Nabla/useSwapForm'; import { toBigNumber } from '../../helpers/parseNumbers'; import { Skeleton } from '../Skeleton'; -import { Tabs } from 'react-daisyui'; +import { config } from '../../config'; +import Big from 'big.js'; +import { ExecutionInput } from '../../pages/landing'; +import { useAccount, useSignMessage } from 'wagmi'; const { RadioTab } = Tabs; interface InputBoxProps { - onSubmit: ( - userSubstrateAddress: string, - swapsFirst: boolean, - selectedAsset: string, - swap: SwapOptions, - maxBalanceFrom: number, - ) => void; + onSubmit: (input: ExecutionInput) => void; dAppName: string; } export interface SwapSettings { - slippage?: number; - deadline: number; from: string; to: string; } export interface SwapOptions { assetIn: string; - assetOut: string; - amountIn: number; - minAmountOut: number; - initialDesired: number; + minAmountOut: Big; } function Loader() { @@ -52,13 +45,11 @@ function Loader() { } const InputBox: React.FC = ({ onSubmit, dAppName }) => { - // TODO - use different wallet account - const walletAccount: { address: string; source: string } = { address: '', source: '' }; const [isSubmitted, setIsSubmitted] = useState(false); - + const { address } = useAccount(); + const { signMessage } = useSignMessage(); const [api, setApi] = useState(null); - const { balances, isBalanceLoading, balanceError } = useAccountBalance(walletAccount?.address); - const [activeTab, setActiveTab] = useState<'swap' | 'direct'>('direct'); + const { balance, isBalanceLoading } = useAccountBalance(address); useEffect(() => { const initializeApiManager = async () => { @@ -68,20 +59,9 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { }; initializeApiManager().catch(console.error); - setActiveTab('swap'); }, []); - useEffect(() => { - if (activeTab === 'swap') { - // force usdt selection - onFromChange(TOKEN_CONFIG.usdt); - } else { - // removes possible usdt selected - form.setValue('from', ''); - } - }, [activeTab]); - - const wantsSwap = activeTab === 'swap'; + const wantsSwap = true; const { tokensModal: [modalType, setModalType], @@ -89,9 +69,9 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { onToChange, form, fromAmount, + fromAmountString, fromToken, toToken, - slippage, from, to, } = useSwapForm(); @@ -99,74 +79,79 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { const tokenOutData = useTokenOutAmount({ wantsSwap, api: api, - walletAccount, - fromAmount, + fromAmountString, fromToken: from, toToken: to, maximumFromAmount: undefined, - slippage, + xcmFees: config.xcm.fees, + slippageBasisPoints: config.swap.slippageBasisPoints, form, }); + const { + formState: { errors }, + } = form; + + const formErrorMessage = errors.fromAmount?.message ?? errors.root?.message; + const handleSubmit = async () => { - if (fromAmount === 0) { + if (fromAmount === undefined || fromAmount.eq(0)) { alert('Please enter an amount to offramp.'); return; } + if (tokenOutData.data === null) return; + let assetToOfframp; + let swapOptions: SwapOptions | undefined; if (wantsSwap) { // ensure the swap was calculated and no errors were found - if (inputHasErrors || tokenOutData.isLoading) { + if (inputHasErrors || tokenOutData.isLoading || tokenOutData.data === undefined) { return; } - assetToOfframp = to; + + swapOptions = { + assetIn: from, + minAmountOut: tokenOutData.data.amountOut.preciseBigDecimal, + }; + assetToOfframp = to as TokenType; } else { - assetToOfframp = from; + assetToOfframp = from as TokenType; } - if (!walletAccount?.address) { + if (!address) { alert('Please connect to a wallet first.'); return; } // check balance of the asset used to offramp directly or to pay for the swap - if (balances[from].approximateNumber < fromAmount) { - alert( - `Insufficient balance to offramp. Current balance is ${ - balances[from].approximateNumber - } ${from.toUpperCase()}.`, - ); + if (balance.preciseBigDecimal.lt(fromAmount)) { + alert(`Insufficient balance to offramp. Current balance is ${balance.approximateNumber} ${from.toUpperCase()}.`); return; } // If swap will happen, check the minimum comparing to the minimum expected swap const minWithdrawalAmountBigNumber = toBigNumber( - TOKEN_CONFIG[assetToOfframp].minWithdrawalAmount!, - TOKEN_CONFIG[assetToOfframp].decimals, + TOKEN_CONFIG[assetToOfframp as TokenType].minWithdrawalAmount!, + TOKEN_CONFIG[assetToOfframp as TokenType].decimals, ); - let minAmountOutBigNumber = toBigNumber('0', TOKEN_CONFIG[assetToOfframp].decimals); - if (tokenOutData.data) { - minAmountOutBigNumber = toBigNumber(tokenOutData.data.minAmountOut ?? '0', 0); - } - - if (wantsSwap && assetToOfframp && minWithdrawalAmountBigNumber.gt(minAmountOutBigNumber)) { + if ( + wantsSwap && + from && + tokenOutData.data !== undefined && + minWithdrawalAmountBigNumber.gt(tokenOutData.data.amountOut.preciseBigDecimal) + ) { alert(`Insufficient balance to offramp. Minimum withdrawal amount for ${assetToOfframp} is not met.`); return; } - let initialDesired = 0; - if (tokenOutData.data) { - initialDesired = tokenOutData.data.amountOut.approximateNumber; - } - setIsSubmitted(true); console.log( 'submitting offramp', '\n', 'user address: ', - walletAccount.address, + address, '\n', 'wants swap: ', wantsSwap, @@ -183,34 +168,15 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { 'asset out: ', to, '\n', - 'min amount out: ', - tokenOutData.data?.minAmountOut, - '\n', 'initial desired: ', tokenOutData.data?.amountOut.approximateNumber, ); - const maxBalanceFrom = balances[from].approximateNumber; - - onSubmit( - walletAccount!.address, - wantsSwap, - assetToOfframp, - { - amountIn: fromAmount, - assetIn: from, - assetOut: to, - minAmountOut: minAmountOutBigNumber.toNumber(), - initialDesired, - }, - maxBalanceFrom, - ); + onSubmit({ assetToOfframp, amountIn: fromAmount, swapOptions }); }; // we don't propagate errors if wants swap is not defined - const inputHasErrors = wantsSwap - ? form.formState.errors.fromAmount?.message !== undefined || form.formState.errors.root?.message !== undefined - : false; + const inputHasErrors = wantsSwap && formErrorMessage !== undefined; return (
@@ -229,11 +195,10 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { />
-
+
{!isSubmitted && (
    -
  • Ensure to have enough token funds in Pendulum wallet (min. 10 Tokens)
  • Do not close this window until the process is completed.
  • This is a non-custodial prototype, please use at your own risk.
@@ -245,76 +210,39 @@ const InputBox: React.FC = ({ onSubmit, dAppName }) => { Offramp Asset
- - - setActiveTab('swap')}> -
- {api === null || isBalanceLoading ? ( - - ) : wantsSwap ? ( - - setModalType('from')} - inputHasError={inputHasErrors} - form={form} - fromFormFieldName="fromAmount" - fromTokenBalances={balances} - /> -
{tokenOutData.error &&

{tokenOutData.error}

}
-
- setModalType('to')} - fromAmount={fromAmount} - slippage={slippage} - /> -
- ) : null} -
-
- - setActiveTab('direct')}> -
- {api === null || isBalanceLoading ? ( - - ) : wantsSwap ? null : ( - - setModalType('from')} - inputHasError={inputHasErrors} - form={form} - fromFormFieldName="fromAmount" - fromTokenBalances={balances} - /> -
- {tokenOutData.error && !tokenOutData.error.includes('missing') && ( -

{tokenOutData.error}

- )} -
-
- )} -
-
-
+
+ {api === null || isBalanceLoading ? ( + + ) : wantsSwap ? ( + + setModalType('from')} + inputHasError={inputHasErrors} + form={form} + fromFormFieldName="fromAmount" + tokenBalance={balance} + /> +
{formErrorMessage !== undefined &&

{formErrorMessage}

}
+
+ setModalType('to')} + fromAmount={fromAmount} + /> +
+ ) : null} +
- {!(from === '') && !isSubmitted && walletAccount?.address ? ( - ) : null} diff --git a/src/components/Nabla/BalanceState.tsx b/src/components/Nabla/BalanceState.tsx index 27efad23..ed6acaff 100644 --- a/src/components/Nabla/BalanceState.tsx +++ b/src/components/Nabla/BalanceState.tsx @@ -1,82 +1,63 @@ import { useEffect, useState } from 'react'; -import { stringDecimalToBN, toBigNumber } from '../../helpers/parseNumbers'; +import { toBigNumber } from '../../helpers/parseNumbers'; import { getApiManagerInstance } from '../../services/polkadot/polkadotApi'; import { TOKEN_CONFIG } from '../../constants/tokenConfig'; -import { stringifyBigWithSignificantDecimals } from '../../helpers/contracts'; +import { parseContractBalanceResponse } from '../../helpers/contracts'; import { ContractBalance } from '../../helpers/contracts'; +import { useReadContract } from 'wagmi'; +import BigNumber from 'big.js'; +import erc20ABI from '../../contracts/ERC20'; + export interface BalanceInfo extends ContractBalance { canWithdraw: boolean; } export interface UseAccountBalanceResponse { - balances: { [key: string]: BalanceInfo }; + balance: BalanceInfo; isBalanceLoading: boolean; balanceError?: Error; } +const zeroBalance = { + ...parseContractBalanceResponse(6, BigInt(0)), + canWithdraw: false, +}; export const useAccountBalance = (address?: string): UseAccountBalanceResponse => { - const [balances, setBalances] = useState<{ [key: string]: BalanceInfo }>({}); - const [isBalanceLoading, setIsLoading] = useState(false); + const [balanceParsed, setBalance] = useState(zeroBalance); + const [isBalanceLoading, setIsLoading] = useState(true); const [balanceError, setError] = useState(); + const { data: balance } = useReadContract({ + abi: erc20ABI, + address: TOKEN_CONFIG.usdc.erc20AddressNativeChain as `0x${string}`, + functionName: 'balanceOf', + args: [address], + }); + useEffect(() => { const fetchBalances = async () => { if (!address) { - setBalances({}); - return; - } - - const apiManager = await getApiManagerInstance(); - const apiComponents = await apiManager.getApiComponents(); - if (!apiComponents) { - setBalances({}); + setBalance({ + ...zeroBalance, + canWithdraw: false, + }); return; } - - setIsLoading(true); - const newBalances: { [key: string]: BalanceInfo } = {}; - try { - for (const [key, config] of Object.entries(TOKEN_CONFIG)) { - const response = (await apiComponents.api.query.tokens.accounts(address, config.currencyId)).toHuman() as any; - - const rawBalance = response?.free || '0'; - const preciseBigDecimal = toBigNumber(rawBalance, TOKEN_CONFIG[key].decimals); - const balanceBigNumber = toBigNumber(rawBalance, 0); - - const atLeast2Decimals = stringifyBigWithSignificantDecimals(preciseBigDecimal, 2); - const atLeast4Decimals = stringifyBigWithSignificantDecimals(preciseBigDecimal, 4); - - const contractBalance = { - rawBalance: balanceBigNumber, - decimals: config.decimals, - preciseBigDecimal, - preciseString: balanceBigNumber.toString(), - approximateStrings: { - atLeast2Decimals: atLeast2Decimals, - atLeast4Decimals: atLeast4Decimals, - }, - approximateNumber: preciseBigDecimal.toNumber(), - }; + const rawBalance = balance as bigint; + const contractBalance = parseContractBalanceResponse(6, rawBalance); - // if it is offramped, it should always ahve minWithrawalAmount defined - if (config.isOfframp && config.minWithdrawalAmount) { - const minWithdrawalAmount = toBigNumber(config.minWithdrawalAmount, 0); - const canWithdraw = balanceBigNumber.gte(minWithdrawalAmount); + // We don't need this, now. Unless we wan't to support also offramp from native polygon chain. + // otherwise, the minimum is irrelevant. + const minWithdrawalAmount = toBigNumber(100, 0); + const canWithdraw = contractBalance.rawBalance.gte(minWithdrawalAmount); - newBalances[key] = { - ...contractBalance, - canWithdraw, - }; - continue; - } + const balancePolygonAsset = { + ...contractBalance, + canWithdraw, + }; - newBalances[key] = { - ...contractBalance, - canWithdraw: false, - }; - } - setBalances(newBalances); + setBalance(balancePolygonAsset); } catch (err) { setError(err as Error); } finally { @@ -85,10 +66,10 @@ export const useAccountBalance = (address?: string): UseAccountBalanceResponse = }; fetchBalances(); - }, [address]); + }, [address, balance]); return { - balances, + balance: balanceParsed, isBalanceLoading, balanceError, }; diff --git a/src/components/Nabla/schema.tsx b/src/components/Nabla/schema.tsx index b56513a9..671363b2 100644 --- a/src/components/Nabla/schema.tsx +++ b/src/components/Nabla/schema.tsx @@ -5,15 +5,6 @@ export type SwapFormValues = { fromAmount: string; to: string; toAmount: string; - slippage: number | undefined; - deadline: number; -}; - -/* eslint-disable @typescript-eslint/no-explicit-any */ -const transformNumber = (value: any, originalValue: any) => { - if (!originalValue) return 0; - if (typeof originalValue === 'string' && originalValue !== '') value = Number(originalValue) ?? 0; - return value; }; const schema = Yup.object().shape({ @@ -21,8 +12,6 @@ const schema = Yup.object().shape({ fromAmount: Yup.string().required(), to: Yup.string().min(5).required(), toAmount: Yup.string().required(), - slippage: Yup.number().nullable().transform(transformNumber), - deadline: Yup.number().nullable().transform(transformNumber), }); export default schema; diff --git a/src/components/Nabla/useSwapForm.tsx b/src/components/Nabla/useSwapForm.tsx index 020f4681..21747451 100644 --- a/src/components/Nabla/useSwapForm.tsx +++ b/src/components/Nabla/useSwapForm.tsx @@ -1,18 +1,17 @@ import { useState, useCallback, useMemo } from 'react'; - -import { TOKEN_CONFIG, TokenDetails } from '../../constants/tokenConfig'; - +import Big from 'big.js'; import { Resolver, useForm, useWatch } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { SwapFormValues } from './schema'; -import schema from './schema'; + +import { TOKEN_CONFIG, TokenDetails, TokenType } from '../../constants/tokenConfig'; +import schema, { SwapFormValues } from './schema'; import { storageService } from '../../services/localStorage'; -import { getValidDeadline, getValidSlippage } from '../../helpers/transaction'; import { storageKeys } from '../../constants/localStorage'; -import { config } from '../../config'; import { debounce } from '../../helpers/function'; import { SwapSettings } from '../InputKeys'; + const storageSet = debounce(storageService.set, 1000); +const setStorageForSwapSettings = storageSet.bind(null, storageKeys.SWAP_SETTINGS); export const useSwapForm = () => { const tokensModal = useState(); @@ -21,10 +20,8 @@ export const useSwapForm = () => { const initialState = useMemo(() => { const storageValues = storageService.getParsed(storageKeys.SWAP_SETTINGS); return { - from: storageValues?.from ?? '', + from: storageValues?.from ?? 'usdc', to: storageValues?.to ?? '', - slippage: getValidSlippage(storageValues?.slippage), - deadline: getValidDeadline(storageValues?.deadline ?? 0), }; }, []); @@ -37,30 +34,16 @@ export const useSwapForm = () => { const from = useWatch({ control, name: 'from' }); const to = useWatch({ control, name: 'to' }); - const fromToken = from ? TOKEN_CONFIG[from] : undefined; - const toToken = to ? TOKEN_CONFIG[to] : undefined; - - const updateStorage = useCallback( - (newValues: Partial) => { - const prev = form.getValues(); - const updated = { - slippage: prev.slippage || config.swap.defaults.slippage, - deadline: prev.deadline || config.swap.defaults.deadline, - ...newValues, - }; - storageSet(storageKeys.SWAP_SETTINGS, updated); - return updated; - }, - [form], - ); + const fromToken = from ? TOKEN_CONFIG[from as TokenType] : undefined; + const toToken = to ? TOKEN_CONFIG[to as TokenType] : undefined; const onFromChange = useCallback( (a: TokenDetails) => { - const f = a.assetCode; - const prev = form.getValues(); - const tokenKey = Object.entries(TOKEN_CONFIG).filter(([key, tokenDetails]) => { - return tokenDetails.assetCode === f; - })[0][0]; + const prev = getValues(); + const tokenKey = Object.keys(TOKEN_CONFIG).find( + (key) => TOKEN_CONFIG[key as TokenType]!.assetCode === a.assetCode, + ); + if (!tokenKey) return; const updated = { from: tokenKey, @@ -68,31 +51,34 @@ export const useSwapForm = () => { }; if (updated.to && prev?.to === tokenKey) setValue('to', updated.to); - updateStorage(updated); - form.setValue('from', updated.from); + setStorageForSwapSettings(updated); + setValue('from', tokenKey); setTokenModal(undefined); }, - [form, form.getValues, setTokenModal, form.setValue, updateStorage], + [getValues, setValue, setTokenModal], ); const onToChange = useCallback( (a: TokenDetails) => { - const f = a.assetCode; - const prev = form.getValues(); - const tokenKey = Object.entries(TOKEN_CONFIG).filter(([key, tokenDetails]) => { - return tokenDetails.assetCode === f; - })[0][0]; + const prev = getValues(); + const tokenKey = Object.keys(TOKEN_CONFIG).find( + (key) => TOKEN_CONFIG[key as TokenType]!.assetCode === a.assetCode, + ); + if (!tokenKey) return; + const updated = { to: tokenKey, from: prev?.from === tokenKey ? prev?.to : prev?.from, }; - updateStorage(updated); - if (updated.from && prev?.from !== updated.from) form.setValue('from', updated.from); - form.setValue('to', updated.to); + + if (updated.from && prev?.from !== updated.from) setValue('from', updated.from); + setStorageForSwapSettings(updated); + setValue('to', tokenKey); + setTokenModal(undefined); }, - [form, setTokenModal, updateStorage], + [getValues, setTokenModal, setValue], ); const fromAmountString = useWatch({ @@ -101,17 +87,12 @@ export const useSwapForm = () => { defaultValue: '0', }); - const slippage = getValidSlippage( - Number( - useWatch({ - control, - name: 'slippage', - defaultValue: config.swap.defaults.slippage, - }), - ), - ); - - const fromAmount = Number(fromAmountString); + let fromAmount: Big | undefined; + try { + fromAmount = new Big(fromAmountString); + } catch { + // no action required + } return { form, @@ -120,10 +101,9 @@ export const useSwapForm = () => { tokensModal, onFromChange, onToChange, - updateStorage, fromAmount, + fromAmountString, fromToken, toToken, - slippage, }; }; diff --git a/src/components/Sep24Component.tsx b/src/components/Sep24Component.tsx index 70296c1b..d31803c8 100644 --- a/src/components/Sep24Component.tsx +++ b/src/components/Sep24Component.tsx @@ -1,69 +1,40 @@ -import React, { useState, useEffect } from 'react'; -import { IAnchorSessionParams, ISep24Intermediate, Sep24Result } from '../services/anchor'; +import React, { useEffect, useState } from 'react'; +import { IAnchorSessionParams, Sep24Result } from '../services/anchor'; import { sep24First, sep24Second } from '../services/anchor'; import { EventStatus } from './GenericEvent'; import { Button } from 'react-daisyui'; -import { sep10 } from '../services/anchor'; + interface Sep24Props { sessionParams: IAnchorSessionParams | null; onSep24Complete: (sep24Reslt: Sep24Result) => void; - setAnchorSessionParams: (params: IAnchorSessionParams) => void; addEvent: (message: string, status: EventStatus) => void; } -interface Sep24ProcessStatus { - processStarted: boolean; - waitingSep24Second: boolean; -} - const Sep24: React.FC = ({ sessionParams, onSep24Complete, addEvent }) => { - const [iframe, iframeOpened] = useState(false); - const [externalWindowClicked, setExternalWindowClicked] = useState(false); - const [sep24IntermediateValues, setSep24IntermediateValues] = useState(null); - const [processStatus, setProcessStatus] = useState({ - processStarted: false, - waitingSep24Second: false, - }); + const [sep24Url, setSep24Url] = useState(undefined); - const onExternalWindowClicked = () => { + useEffect(() => { if (sessionParams) { - sep24First(sessionParams, addEvent).then((response) => { - setProcessStatus({ - processStarted: true, - waitingSep24Second: false, - }); - window.open(`${response.url}`, '_blank'); - setSep24IntermediateValues(response); - iframeOpened(true); - }); - } + (async () => { + const firstResponse = await sep24First(sessionParams, addEvent); + setSep24Url(firstResponse.url); + const secondResponse = await sep24Second(firstResponse, sessionParams); + onSep24Complete(secondResponse); + })(); - setExternalWindowClicked(true); - }; - - const handleIframeCompletion = () => { - // at this point setSep24IntermediateValues should not be null, as well as - // sessionParams - iframeOpened(false); - sep24Second(sep24IntermediateValues!, sessionParams!, addEvent).then((response) => { - onSep24Complete(response); - }); - addEvent('Waiting for confirmation from Anchor', EventStatus.Waiting); - setProcessStatus({ processStarted: true, waitingSep24Second: true }); - }; + addEvent('Waiting for confirmation from Anchor', EventStatus.Waiting); + } + }, [sessionParams]); return (
diff --git a/src/config/index.ts b/src/config/index.ts index 32a3bac6..ef6515b4 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -49,23 +49,10 @@ export const config = { explorer: 'https://polkadot.js.org/apps/?rpc=wss%3A%2F%2Frpc-foucoco.pendulumchain.tech#/explorer/query', }, } satisfies TenantConfig, - transaction: { - settings: { - slippage: { - min: 0.1, - max: 99.9, - }, - deadline: { - min: 1, - max: 1440, - }, - }, - }, + xcm: { fees: '0.016' }, swap: { - defaults: { - slippage: 0.5, - deadline: 30, - }, + slippageBasisPoints: 30, + deadline: 30, }, walletConnect: { url: 'wss://relay.walletconnect.com', diff --git a/src/constants/constants.ts b/src/constants/constants.ts index d86d17d1..4be6e9f6 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -7,6 +7,7 @@ export const PENDULUM_WSS = 'wss://rpc-pendulum.prd.pendulumchain.tech'; //export const PENDULUM_WSS = 'ws://localhost:8000'; export const NABLA_ROUTER = '6buMJsFCbXpHRyacKTjBn3Jss241b2aA7CZf9tKzKHMJWpcJ'; +export const TRANSFER_WAITING_TIME_SECONDS = 6000; export const SIGNING_SERVICE_URL = config.maybeSignerServiceUrl || (config.isProd ? 'https://prototype-signer-service.pendulumchain.tech' : 'http://localhost:3000'); diff --git a/src/constants/tokenConfig.ts b/src/constants/tokenConfig.ts index 51dbda60..1c928c25 100644 --- a/src/constants/tokenConfig.ts +++ b/src/constants/tokenConfig.ts @@ -1,3 +1,8 @@ +import BrlIcon from '../assets/coins/BRL.png'; +import UsdtIcon from '../assets/coins/USDT.png'; +import EurcIcon from '../assets/coins/EURC.png'; +import UsdcIcon from '../assets/coins/USDC.png'; + export interface TokenDetails { currencyId: any; isOfframp: boolean; @@ -11,11 +16,13 @@ export interface TokenDetails { vaultAccountId?: string; minWithdrawalAmount?: string; assetCodeHex?: string; // Optional property + icon: string; + isPolygonChain?: boolean; + erc20AddressNativeChain?: string; } -export interface TokenConfig { - [key: string]: TokenDetails; -} +export type TokenType = 'brl' | 'eurc' | 'usdc'; +export type TokenConfig = Record; // Every asset specified in here must either be offrampable or be swapable to an offrampable asset export const TOKEN_CONFIG: TokenConfig = { @@ -32,9 +39,10 @@ export const TOKEN_CONFIG: TokenConfig = { canSwapTo: ['usdt', 'eurc'], assetIssuer: 'GDVKY2GU2DRXWTBEYJJWSFXIGBZV6AZNBVVSUHEPZI54LIS6BA7DVVSP', vaultAccountId: '6g7fKQQZ9VfbBTQSaKBcATV4psApFra5EDwKLARFZCCVnSWS', - minWithdrawalAmount: '1000000000000', + minWithdrawalAmount: '200000000000000', assetCodeHex: '0x42524c00', erc20Address: '6dZCR7KVmrcxBoUTcM3vUgpQagQAW2wg2izMrT3N4reftwW5', + icon: BrlIcon, }, eurc: { tomlFileUrl: 'https://mykobo.co/.well-known/stellar.toml', @@ -51,13 +59,25 @@ export const TOKEN_CONFIG: TokenConfig = { vaultAccountId: '6bsD97dS8ZyomMmp1DLCnCtx25oABtf19dypQKdZe6FBQXSm', erc20Address: '6fA9DRKJ12oTXfSAU7ZZGZ9gEQ92YnyRXeJzW1wXekPzeXZC', minWithdrawalAmount: '10000000000000', + icon: EurcIcon, }, - usdt: { - assetCode: 'USDT', - currencyId: { XCM: 1 }, + // We treat many of the properties of polygon token as the equivalent axl{X} one on Pendulum. + // we will receive + usdc: { + assetCode: 'USDC', + erc20AddressNativeChain: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e on Polygon + // erc20AddressNativeChain: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // USDC on Polygon + // erc20Address is that of axlUSDC on pendulum + // this is done to provide the user with the expected exchange rate + erc20Address: '6cXCaQeLQtYhyaQgMGaLcBakgfdgNiSoENW2LA2z8nLBcpSh', + // Decimals should be consistent in BOTH CHAINS decimals: 6, - canSwapTo: ['brl', 'eurc'], + // currency id of axlUSDC + currencyId: { XCM: 12 }, isOfframp: false, - erc20Address: '6cRE6nw1eW8Lq452D39Jw3FeradDmUkoEvCgiRkTYxqmP6cs', + // whatever axlUSDC can be offramped to... + canSwapTo: ['eurc'], + icon: UsdcIcon, + isPolygonChain: true, }, -}; +} as const; diff --git a/src/contracts/ERC20.ts b/src/contracts/ERC20.ts new file mode 100644 index 00000000..16f760f3 --- /dev/null +++ b/src/contracts/ERC20.ts @@ -0,0 +1,224 @@ +const erc20ABI = [ + { + constant: true, + inputs: [], + name: 'name', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_spender', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'approve', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'totalSupply', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_from', + type: 'address', + }, + { + name: '_to', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'transferFrom', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'decimals', + outputs: [ + { + name: '', + type: 'uint8', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + ], + name: 'balanceOf', + outputs: [ + { + name: 'balance', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: true, + inputs: [], + name: 'symbol', + outputs: [ + { + name: '', + type: 'string', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + constant: false, + inputs: [ + { + name: '_to', + type: 'address', + }, + { + name: '_value', + type: 'uint256', + }, + ], + name: 'transfer', + outputs: [ + { + name: '', + type: 'bool', + }, + ], + payable: false, + stateMutability: 'nonpayable', + type: 'function', + }, + { + constant: true, + inputs: [ + { + name: '_owner', + type: 'address', + }, + { + name: '_spender', + type: 'address', + }, + ], + name: 'allowance', + outputs: [ + { + name: '', + type: 'uint256', + }, + ], + payable: false, + stateMutability: 'view', + type: 'function', + }, + { + payable: true, + stateMutability: 'payable', + type: 'fallback', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'owner', + type: 'address', + }, + { + indexed: true, + name: 'spender', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Approval', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, +]; + +export default erc20ABI; diff --git a/src/contracts/SquidReceiver.ts b/src/contracts/SquidReceiver.ts new file mode 100644 index 00000000..d3360c49 --- /dev/null +++ b/src/contracts/SquidReceiver.ts @@ -0,0 +1,149 @@ +export const squidReceiverABI = [ + { + anonymous: false, + inputs: [ + { + components: [ + { + internalType: 'uint8', + name: 'parents', + type: 'uint8', + }, + { + internalType: 'bytes[]', + name: 'interior', + type: 'bytes[]', + }, + ], + indexed: false, + internalType: 'struct Xtokens.Multilocation', + name: '', + type: 'tuple', + }, + { + indexed: false, + internalType: 'uint256', + name: '', + type: 'uint256', + }, + { + components: [ + { + internalType: 'uint8', + name: 'parents', + type: 'uint8', + }, + { + internalType: 'bytes[]', + name: 'interior', + type: 'bytes[]', + }, + ], + indexed: false, + internalType: 'struct Xtokens.Multilocation', + name: '', + type: 'tuple', + }, + { + indexed: false, + internalType: 'uint64', + name: '', + type: 'uint64', + }, + ], + name: 'MultiassetCall', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'bytes', + name: '', + type: 'bytes', + }, + ], + name: 'MultiassetError', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint256', + name: 'balance', + type: 'uint256', + }, + ], + name: 'ReceiveBalance', + type: 'event', + }, + { + inputs: [ + { + internalType: 'bytes', + name: 'payload', + type: 'bytes', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'executeXCM', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes', + name: 'payload', + type: 'bytes', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'executeXCMMock', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'lastTransferDetails', + outputs: [ + { + components: [ + { + internalType: 'uint8', + name: 'parents', + type: 'uint8', + }, + { + internalType: 'bytes[]', + name: 'interior', + type: 'bytes[]', + }, + ], + internalType: 'struct Xtokens.Multilocation', + name: 'destination', + type: 'tuple', + }, + { + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, +]; diff --git a/src/helpers/contracts.ts b/src/helpers/contracts.ts index a3a7ce6d..11339c26 100644 --- a/src/helpers/contracts.ts +++ b/src/helpers/contracts.ts @@ -51,19 +51,20 @@ export interface ContractBalance { approximateNumber: number; } -export function parseContractBalanceResponse(decimals: number, balanceResponse: INumber): ContractBalance; +export function parseContractBalanceResponse(decimals: number, balanceResponse: INumber | bigint): ContractBalance; export function parseContractBalanceResponse( decimals: number | undefined, - balanceResponse: INumber | undefined, + balanceResponse: INumber | bigint | undefined, ): ContractBalance | undefined; export function parseContractBalanceResponse( decimals: number | undefined, - balanceResponse: INumber | undefined, + balanceResponse: INumber | bigint | undefined, ): ContractBalance | undefined { - const rawBalanceBigInt = balanceResponse?.toBigInt(); - if (rawBalanceBigInt === undefined || decimals === undefined) return undefined; + if (balanceResponse === undefined || decimals === undefined) return undefined; + + const rawBalanceBigInt = typeof balanceResponse === 'bigint' ? balanceResponse : balanceResponse.toBigInt(); const rawBalanceString = rawBalanceBigInt.toString(); const preciseBigDecimal = multiplyByPowerOfTen(new BigNumber(rawBalanceString), -decimals); @@ -102,10 +103,15 @@ export function stringifyBigWithSignificantDecimals(big: BigNumber, decimals: nu return rounded.toFixed(significantDecimals, 0); } -function multiplyByPowerOfTen(bigDecimal: BigNumber, power: number) { +export function multiplyByPowerOfTen(bigDecimal: BigNumber, power: number) { const newBigDecimal = new BigNumber(bigDecimal); if (newBigDecimal.c[0] === 0) return newBigDecimal; newBigDecimal.e += power; return newBigDecimal; } + +// difference of two bigints, clamp to 0 +export function clampedDifference(a: bigint, b: bigint) { + return a > b ? a - b : 0; +} diff --git a/src/helpers/transaction.ts b/src/helpers/transaction.ts deleted file mode 100644 index fdc86c2c..00000000 --- a/src/helpers/transaction.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { config } from '../config'; - -const { slippage, deadline } = config.transaction.settings; - -export function getValidSlippage(val: number): number; -export function getValidSlippage(val: number | undefined): number | undefined; - -export function getValidSlippage(val: number | undefined): number | undefined { - return val !== undefined ? Math.min(Math.max(val, slippage.min), slippage.max) : val; -} - -export function getValidDeadline(val: number): number { - return Math.min(Math.max(val, deadline.min), deadline.max); -} diff --git a/src/hooks/nabla/useContractRead.ts b/src/hooks/nabla/useContractRead.ts index 2c0c7245..10589d85 100644 --- a/src/hooks/nabla/useContractRead.ts +++ b/src/hooks/nabla/useContractRead.ts @@ -22,15 +22,15 @@ export type UseContractReadProps = { noWalletAddressRequired?: boolean; parseSuccessOutput: (successResult: any) => ReturnType; parseError: string | ((errorResult: MessageCallErrorResult) => string); - queryOptions: QueryOptions; + queryOptions: QueryOptions; }; -export type UseContractReadResult = UseQueryResult; +export type UseContractReadResult = UseQueryResult; export function useContractRead( key: QueryKey, - api: ApiPromise, - walletAddress: string, + api: ApiPromise | null, + walletAddress: string | undefined, { abi, address, @@ -53,7 +53,7 @@ export function useContractRead( const queryKey = enabled ? key : emptyCacheKey; const queryFn = async () => { - if (!enabled) return; + if (!enabled) return null; const limits = defaultReadLimits; if (isDevelopment) { @@ -86,7 +86,7 @@ export function useContractRead( return parseSuccessOutput(response.value); }; - const query = useQuery({ ...queryOptions, queryKey, queryFn, enabled, retry: false }); + const query = useQuery({ ...queryOptions, queryKey, queryFn, enabled, retry: false }); return query; } diff --git a/src/hooks/nabla/useTokenAmountOut.ts b/src/hooks/nabla/useTokenAmountOut.ts index f8a9062b..0f807a54 100644 --- a/src/hooks/nabla/useTokenAmountOut.ts +++ b/src/hooks/nabla/useTokenAmountOut.ts @@ -1,155 +1,126 @@ import BigNumber from 'big.js'; import { activeOptions, cacheKeys } from '../../constants/cache'; import { routerAbi } from '../../contracts/Router'; -import { ContractBalance, parseContractBalanceResponse } from '../../helpers/contracts'; -import { decimalToCustom, toBigNumber } from '../../helpers/parseNumbers'; +import { + ContractBalance, + clampedDifference, + multiplyByPowerOfTen, + parseContractBalanceResponse, + stringifyBigWithSignificantDecimals, +} from '../../helpers/contracts'; import { NABLA_ROUTER } from '../../constants/constants'; import { useContractRead } from './useContractRead'; import { UseQueryResult } from '@tanstack/react-query'; -import { TokenDetails } from '../../constants/tokenConfig'; import { useDebouncedValue } from '../useDebouncedValue'; -import { TOKEN_CONFIG } from '../../constants/tokenConfig'; -import { WalletAccount } from '@talismn/connect-wallets'; +import { TOKEN_CONFIG, TokenType } from '../../constants/tokenConfig'; import { ApiPromise } from '../../services/polkadot/polkadotApi'; import { FieldValues, UseFormReturn } from 'react-hook-form'; import { useEffect } from 'preact/hooks'; +import Big from 'big.js'; export type UseTokenOutAmountProps = { wantsSwap: boolean; api: ApiPromise | null; - walletAccount: WalletAccount | undefined; - fromAmount: number | null; + fromAmountString: string; fromToken: string; toToken: string; maximumFromAmount: BigNumber | undefined; - slippage: number; + xcmFees: string; + slippageBasisPoints: number; form: UseFormReturn; }; export interface UseTokenOutAmountResult { isLoading: boolean; enabled: boolean; - data: TokenOutData | undefined; - error: string | null; - refetch?: UseQueryResult['refetch']; + data: TokenOutData | undefined | null; + refetch?: UseQueryResult['refetch']; } + export interface TokenOutData { amountOut: ContractBalance; swapFee: ContractBalance; effectiveExchangeRate: string; - minAmountOut: string; } export function useTokenOutAmount({ wantsSwap, api, - walletAccount, - fromAmount, + fromAmountString, fromToken, toToken, maximumFromAmount, - slippage, + xcmFees, + slippageBasisPoints, form, }: UseTokenOutAmountProps) { const { setError, clearErrors } = form; - // Handle different errors either from form or parameters needed for the swap - const inputHasErrors = form.formState.errors.fromAmount?.message !== undefined; - if (inputHasErrors) { - console.log('errors', form.formState.errors.fromAmount?.message); - return { - isLoading: false, - enabled: false, - data: undefined, - error: form.formState.errors.fromAmount?.message ?? 'The specified swap cannot be performed at the moment', - refetch: undefined, - }; + const debouncedFromAmountString = useDebouncedValue(fromAmountString, 800); + let debouncedAmountBigDecimal: Big | undefined; + try { + debouncedAmountBigDecimal = new Big(debouncedFromAmountString); + } catch { + // no action required } - if (!walletAccount) { - return { isLoading: false, enabled: false, data: undefined, error: 'Wallet not connected', refetch: undefined }; - } + const fromTokenDetails = TOKEN_CONFIG[fromToken as TokenType]; + const toTokenDetails = TOKEN_CONFIG[toToken as TokenType]; - if (fromToken === '' || toToken === '' || fromAmount === null || api === null || !wantsSwap) { - return { - isLoading: false, - enabled: false, - data: undefined, - error: 'Required parameters are missing', - refetch: undefined, - }; - } + const fromTokenDecimals = fromTokenDetails?.decimals; - const fromTokenDetails: TokenDetails = TOKEN_CONFIG[fromToken]; - const toTokenDetails: TokenDetails = TOKEN_CONFIG[toToken]; - - const debouncedFromAmount = useDebouncedValue(fromAmount, 800); - const debouncedAmountBigDecimal = decimalToCustom(debouncedFromAmount.toString(), fromTokenDetails.decimals); - - // Even though we check for errors, due to possible delay in value update we need to check that the value is not - // less than 1, or larger than e+20, since BigNumber.toString() will return scientific notation. - // this is no error, but temporary empty return until the value gets properly updated. - if ( - debouncedAmountBigDecimal === undefined || - debouncedAmountBigDecimal.lt(new BigNumber(1)) || - debouncedAmountBigDecimal.e > 20 - ) { - return { isLoading: false, enabled: false, data: undefined, error: '', refetch: undefined }; - } + const amountInOriginal = + fromTokenDecimals !== undefined && debouncedAmountBigDecimal !== undefined + ? multiplyByPowerOfTen(debouncedAmountBigDecimal, fromTokenDecimals).toFixed(0, 0) + : undefined; + + const rawXcmFees = multiplyByPowerOfTen(BigNumber(xcmFees), fromTokenDetails.decimals).toFixed(0, 0); + const amountIn = + amountInOriginal !== undefined + ? clampedDifference(BigInt(amountInOriginal), BigInt(rawXcmFees)).toString() + : undefined; const enabled = - fromToken !== undefined && - toToken !== undefined && + api !== undefined && + wantsSwap && + fromTokenDetails !== undefined && + toTokenDetails !== undefined && debouncedAmountBigDecimal !== undefined && debouncedAmountBigDecimal.gt(new BigNumber(0)) && (maximumFromAmount === undefined || debouncedAmountBigDecimal.lte(maximumFromAmount)); - const amountIn = debouncedAmountBigDecimal?.toString(); - - const { isLoading, fetchStatus, data, error, refetch } = useContractRead( - [cacheKeys.tokenOutAmount, fromTokenDetails.erc20Address, toTokenDetails.erc20Address, amountIn], + const { isLoading, fetchStatus, data, error, refetch } = useContractRead( + [cacheKeys.tokenOutAmount, fromTokenDetails?.erc20Address, toTokenDetails?.erc20Address, amountIn], api, - walletAccount.address, + undefined, // Does not matter since noWalletAddressRequired is true { abi: routerAbi, address: NABLA_ROUTER, method: 'getAmountOut', - args: [amountIn, [fromTokenDetails.erc20Address, toTokenDetails.erc20Address]], + args: [amountIn, [fromTokenDetails?.erc20Address, toTokenDetails?.erc20Address]], noWalletAddressRequired: true, queryOptions: { ...activeOptions['30s'], enabled, }, parseSuccessOutput: (data) => { - if (toToken === undefined || fromToken === undefined || debouncedAmountBigDecimal === undefined) { - return undefined; + if (toTokenDetails === undefined || fromTokenDetails === undefined || debouncedAmountBigDecimal === undefined) { + return null; } - const amountOut = parseContractBalanceResponse(toTokenDetails.decimals, data[0]); - const swapFee = parseContractBalanceResponse(toTokenDetails.decimals, data[1]); - // - const decimalDifference = fromTokenDetails.decimals - toTokenDetails.decimals; - let effectiveExchangeRate; - if (decimalDifference > 0) { - const decimalDiffCorrection = new BigNumber(10).pow(decimalDifference); - effectiveExchangeRate = amountOut.rawBalance - .div(debouncedAmountBigDecimal) - .mul(decimalDiffCorrection) - .toString(); - } else { - const decimalDiffCorrection = new BigNumber(10).pow(-decimalDifference); - effectiveExchangeRate = amountOut.rawBalance - .div(debouncedAmountBigDecimal.mul(decimalDiffCorrection)) - .toString(); - } + const bigIntResponse = data[0]?.toBigInt(); + const reducedResponse = (bigIntResponse * BigInt(10000 - slippageBasisPoints)) / 10000n; - const minAmountOut = amountOut.approximateNumber * (1 - slippage / 100); + const amountOut = parseContractBalanceResponse(toTokenDetails.decimals, reducedResponse); + const swapFee = parseContractBalanceResponse(toTokenDetails.decimals, data[1]); return { amountOut, - effectiveExchangeRate, + effectiveExchangeRate: stringifyBigWithSignificantDecimals( + amountOut.preciseBigDecimal.div(debouncedAmountBigDecimal), + 4, + ), swapFee, - minAmountOut: minAmountOut.toString(), }; }, parseError: (error) => { @@ -169,7 +140,7 @@ export function useTokenOutAmount({ }, ); - const pending = (isLoading && fetchStatus !== 'idle') || debouncedFromAmount !== fromAmount; + const pending = (isLoading && fetchStatus !== 'idle') || debouncedFromAmountString !== fromAmountString; useEffect(() => { if (pending) return; if (error === null) { @@ -179,5 +150,5 @@ export function useTokenOutAmount({ } }, [error, pending, clearErrors, setError]); - return { isLoading: pending, enabled, data, error, refetch }; + return { isLoading: pending, enabled, data, refetch }; } diff --git a/src/main.tsx b/src/main.tsx index 6d97e345..efc5752a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,57 +7,21 @@ import '@rainbow-me/rainbowkit/styles.css'; import { render } from 'preact'; import { BrowserRouter } from 'react-router-dom'; import { ThemeProvider } from '@mui/material'; -import { connectorsForWallets, getDefaultConfig, RainbowKitProvider } from '@rainbow-me/rainbowkit'; -import { createConfig, http, WagmiProvider } from 'wagmi'; -import { polygon, base } from 'wagmi/chains'; -import { QueryClientProvider, QueryClient } from '@tanstack/react-query'; +import { RainbowKitProvider } from '@rainbow-me/rainbowkit'; +import { WagmiProvider } from 'wagmi'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { App } from './app'; import defaultTheme from './theme'; import { GlobalState, GlobalStateContext, GlobalStateProvider } from './GlobalStateProvider'; -import { walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; -import { injectedWallet } from '@rainbow-me/rainbowkit/wallets'; -import { safeWallet } from '@rainbow-me/rainbowkit/wallets'; -import { createClient } from 'viem'; +import { wagmiConfig } from './wagmiConfig'; const queryClient = new QueryClient(); -const connectors = connectorsForWallets( - [ - { - groupName: 'Recommended', - wallets: [injectedWallet, safeWallet, walletConnectWallet], - }, - ], - { - appName: 'Vortex', - projectId: '495a5f574d57e27fd65caa26d9ea4f10', - }, -); - -const defaultConfig = getDefaultConfig({ - appName: 'Vortex', - projectId: '495a5f574d57e27fd65caa26d9ea4f10', - chains: [polygon], - ssr: false, // If your dApp uses server side rendering (SSR) -}); - -const config = createConfig({ - client({ chain }) { - return createClient({ - chain, - transport: http(), - }); - }, - chains: defaultConfig.chains, - connectors, - ssr: false, -}); - render( - + diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index 7147daa5..20581be2 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,8 +1,9 @@ +import Big from 'big.js'; +import { useState, useEffect, useRef } from 'react'; + import '../../../App.css'; -import React, { useState, useEffect, useRef } from 'react'; import InputBox from '../../components/InputKeys'; import { SwapOptions } from '../../components/InputKeys'; - import EventBox from '../../components/GenericEvent'; import { GenericEvent, EventStatus } from '../../components/GenericEvent'; import { fetchTomlValues, IAnchorSessionParams, Sep24Result, getEphemeralKeys, sep10 } from '../../services/anchor'; @@ -19,6 +20,18 @@ import { useGlobalState } from '../../GlobalStateProvider'; import { fetchSigningServicePK } from '../../services/signingService'; import { TOKEN_CONFIG, TokenDetails } from '../../constants/tokenConfig'; import { performSwap } from '../../services/nabla'; +import { TRANSFER_WAITING_TIME_SECONDS } from '../../constants/constants'; +import { + waitForTokenReceptionEvent, + getEphemeralAccount, + checkBalance, + fundEphemeralAccount, + cleanEphemeralAccount, +} from '../../services/polkadot/ephemeral'; +import { stringifyBigWithSignificantDecimals } from '../../helpers/contracts'; +import { useSquidRouterSwap, TransactionStatus } from '../../services/squidrouter'; +import { decimalToCustom } from '../../helpers/parseNumbers'; +import { TokenType } from '../../constants/tokenConfig'; enum OperationStatus { Idle, @@ -30,9 +43,16 @@ enum OperationStatus { Error, } +export interface ExecutionInput { + assetToOfframp: TokenType; + amountIn: Big; + swapOptions: SwapOptions | undefined; // undefined means direct offramp +} + function Landing() { // system status const [status, setStatus] = useState(OperationStatus.Idle); + const [executionInput, setExecutionInput] = useState(undefined); // events state and refs const [events, setEvents] = useState([]); @@ -40,7 +60,6 @@ function Landing() { const [activeEventIndex, setActiveEventIndex] = useState(-1); // seession and operations states - const [userAddress, setUserAddress] = useState(null); const [fundingPK, setFundingPK] = useState(null); const [anchorSessionParams, setAnchorSessionParams] = useState(null); const [stellarOperations, setStellarOperations] = useState(null); @@ -51,35 +70,22 @@ function Landing() { const [canInitiate, setCanInitiate] = useState(false); const [backendError, setBackendError] = useState(false); - // Wallet states - // const { walletAccount, dAppName } = useGlobalState(); - // TODO - use different wallet account - const walletAccount = null; - - const handleOnSubmit = async ( - userSubstrateAddress: string, - swapsFirst: boolean, - selectedAsset: string, - swapOptions: SwapOptions, - maxBalanceFrom: number, - ) => { - setUserAddress(userSubstrateAddress); - - const tokenConfig: TokenDetails = TOKEN_CONFIG[selectedAsset]; + //Squidrouter hook + const [amountInNative, setAmountIn] = useState('0'); + const { transactionStatus, executeSquidRouterSwap, error } = useSquidRouterSwap(amountInNative); + const handleOnSubmit = async ({ assetToOfframp, amountIn, swapOptions }: ExecutionInput) => { + // we always want swap now, but for now we hardcode the starting token + setAmountIn(decimalToCustom(amountIn, TOKEN_CONFIG.usdc.decimals).toFixed()); + setExecutionInput({ assetToOfframp, amountIn, swapOptions }); + + + const tokenConfig: TokenDetails = TOKEN_CONFIG[assetToOfframp]; const values = await fetchTomlValues(tokenConfig.tomlFileUrl!); - // perform swap if necessary - // if no swapping, the balance to offramp is the initial desired amount - // otherwise, it will be the obtained value from the swap. - let balanceToOfframp = swapOptions.amountIn; - if (swapsFirst) { - balanceToOfframp = await performSwap( - { swap: swapOptions, userAddress: userSubstrateAddress, walletAccount: walletAccount! }, - addEvent, - ); - } - // truncate the value to 2 decimal places - const balanceToOfframpTruncated = Math.trunc(balanceToOfframp * 100) / 100; + const amountToOfframp = swapOptions !== undefined ? swapOptions.minAmountOut : amountIn; + console.log(amountToOfframp); + + const truncatedAmountToOfframp = stringifyBigWithSignificantDecimals(amountToOfframp.round(2, 0), 2); const token = await sep10(values, addEvent); @@ -87,7 +93,7 @@ function Landing() { token, tomlValues: values, tokenConfig, - offrampAmount: balanceToOfframpTruncated.toString(), + offrampAmount: truncatedAmountToOfframp, }); // showing (rendering) the Sep24 component will trigger the Sep24 process setShowSep24(true); @@ -104,6 +110,54 @@ function Landing() { ); setSep24Result(result); + if (executionInput === undefined) return; + const { assetToOfframp, amountIn, swapOptions } = executionInput; + // Start the squid router process + executeSquidRouterSwap(); + + // log ephemeral pk + const ephemeralAccount = getEphemeralAccount().address; + addEvent(`Pendulum ephemeral account: ${ephemeralAccount}`, EventStatus.Waiting); + + // Wait for ephemeral to receive native balance + // And wait for ephemeral to receive the funds of the token to be offramped + + const tokenToReceive = swapOptions ? TOKEN_CONFIG.usdc.currencyId : TOKEN_CONFIG[assetToOfframp].currencyId; + + console.log('Waiting to receive token: ', tokenToReceive); + const tokenTransferEvent = await waitForTokenReceptionEvent(tokenToReceive, TRANSFER_WAITING_TIME_SECONDS * 1000); + console.log('token received', tokenTransferEvent); + + // call checkBalance until it returns true + let ready; + do { + ready = await checkBalance(); + } while (!ready); + + if (swapOptions) { + const enteredAmountDecimal = new Big(result.amount); + //TESTING commented since we are mocking the response of the KYC + // if (enteredAmountDecimal.gt(swapOptions.minAmountOut)) { + // addEvent( + // `The amount you entered is too high. Maximum possible amount to offramp: ${swapOptions.minAmountOut.toString()}), you entered: ${ + // result.amount + // }.`, + // EventStatus.Error, + // ); + // return; + // } + + await performSwap( + { + amountInRaw: tokenTransferEvent.amountRaw, + assetOut: assetToOfframp, + assetIn: swapOptions.assetIn, + minAmountOut: swapOptions.minAmountOut, + }, + addEvent, + ); + } + // set up the ephemeral account and operations we will later neeed try { addEvent('Settings stellar accounts', EventStatus.Waiting); @@ -129,10 +183,11 @@ function Landing() { const executeRedeem = useCallback( async (sepResult: Sep24Result) => { try { + const ephemeralAccount = getEphemeralAccount(); await executeSpacewalkRedeem( getEphemeralKeys().publicKey(), sepResult.amount, - walletAccount!, + ephemeralAccount, anchorSessionParams!.tokenConfig, addEvent, ); @@ -146,7 +201,7 @@ function Landing() { //this will trigger finalizeOfframp setStatus(OperationStatus.FinalizingOfframp); }, - [walletAccount, anchorSessionParams], + [anchorSessionParams], ); const finalizeOfframp = useCallback(async () => { @@ -164,6 +219,7 @@ function Landing() { // and successful // This will not affect the user await cleanupStellarEphemeral(stellarOperations!.mergeAccountTransaction, addEvent); + await cleanEphemeralAccount(executionInput?.assetToOfframp!); }, [stellarOperations]); const addEvent = (message: string, status: EventStatus) => { @@ -177,6 +233,19 @@ function Landing() { } }; + // Fund the ephemeral account after the squid swap is completed + + // Should we fund this after approval or after the swap is completed? + // Right now, the SwapCompleted variant is never set. + useEffect(() => { + console.log('Transaction status: ', transactionStatus); + if (transactionStatus == TransactionStatus.SpendingApproved) { + console.log('Funding account after squid swap is completed'); + addEvent('Approval to Squidrouter completed', EventStatus.Success); + fundEphemeralAccount(); + } + }, [transactionStatus, error]); + useEffect(() => { scrollToLatestEvent(); }, [events]); @@ -218,15 +287,10 @@ function Landing() { {canInitiate && } {showSep24 && (
- +
)} -
+
{events.map((event, index) => ( void, ): Promise { const { id } = sep24Values; const { token, tomlValues } = sessionParams; const { sep24Url } = tomlValues; - // Mock, testing - // await new Promise((resolve) => setTimeout(resolve, 1000)); - // return { - // amount: "10.3", - // memo: "todo", - // memoType: "text", - // offrampingAccount: "GADBL6LKYBPNGXBKNONXTFVIRMQIXHH2ZW67SVA2R7XM6VBXMD2O6DIS", - // }; - // end mock testing - let status; - let transaction: any; do { await new Promise((resolve) => setTimeout(resolve, 1000)); const idParam = new URLSearchParams({ id }); diff --git a/src/services/nabla.ts b/src/services/nabla.ts index 4dbb79c6..5776fb7f 100644 --- a/src/services/nabla.ts +++ b/src/services/nabla.ts @@ -1,47 +1,57 @@ -import { SwapOptions } from '../components/InputKeys'; +import { Abi } from '@polkadot/api-contract'; +import Big from 'big.js'; +import { readMessage, ReadMessageResult, executeMessage, ExecuteMessageResult } from '@pendulum-chain/api-solang'; + import { EventStatus } from '../components/GenericEvent'; import { getApiManagerInstance } from './polkadot/polkadotApi'; -import { Abi } from '@polkadot/api-contract'; import { erc20WrapperAbi } from '../contracts/ERC20Wrapper'; import { routerAbi } from '../contracts/Router'; import { NABLA_ROUTER } from '../constants/constants'; -import { defaultReadLimits } from '../helpers/contracts'; -import { readMessage, ReadMessageResult, executeMessage, ExecuteMessageResult } from '@pendulum-chain/api-solang'; +import { defaultReadLimits, multiplyByPowerOfTen } from '../helpers/contracts'; import { parseContractBalanceResponse } from '../helpers/contracts'; -import { TOKEN_CONFIG } from '../constants/tokenConfig'; +import { TOKEN_CONFIG, TokenType } from '../constants/tokenConfig'; import { WalletAccount } from '@talismn/connect-wallets'; import { defaultWriteLimits, createWriteOptions } from '../helpers/contracts'; -import { stringDecimalToBN } from '../helpers/parseNumbers'; import { toBigNumber } from '../helpers/parseNumbers'; +import { Keyring } from '@polkadot/api'; +import { getEphemeralAccount } from './polkadot/ephemeral'; export interface PerformSwapProps { - swap: SwapOptions; - userAddress: string; - walletAccount: WalletAccount; + amountInRaw: Big; + assetOut: string; + assetIn: string; + minAmountOut: Big; } export async function performSwap( - { swap, userAddress, walletAccount }: PerformSwapProps, + { amountInRaw, assetOut, assetIn, minAmountOut }: PerformSwapProps, renderEvent: (event: string, status: EventStatus) => void, -): Promise { +): Promise { // event attempting swap + const assetInDetails = TOKEN_CONFIG[assetIn as TokenType]; + const assetOutDetails = TOKEN_CONFIG[assetOut as TokenType]; + + const amountIn = toBigNumber(amountInRaw, assetInDetails.decimals); + renderEvent('Attempting swap', EventStatus.Waiting); + console.log('swap', 'Attempting swap', amountIn, assetOut, assetIn, minAmountOut); // get chain api, abi const pendulumApiComponents = (await getApiManagerInstance()).apiData!; const erc20ContractAbi = new Abi(erc20WrapperAbi, pendulumApiComponents.api.registry.getChainProperties()); const routerAbiObject = new Abi(routerAbi, pendulumApiComponents.api.registry.getChainProperties()); // get asset details - const assetInDetails = TOKEN_CONFIG[swap.assetIn]; - const assetOutDetails = TOKEN_CONFIG[swap.assetOut]; - // call the current allowance of the user + // get ephermal keypair and account + const keypairEphemeral = getEphemeralAccount(); + + // call the current allowance of the ephemeral const response: ReadMessageResult = await readMessage({ abi: erc20ContractAbi, api: pendulumApiComponents.api, contractDeploymentAddress: assetInDetails.erc20Address!, - callerAddress: walletAccount.address, + callerAddress: keypairEphemeral.address, messageName: 'allowance', - messageArguments: [walletAccount.address, NABLA_ROUTER], + messageArguments: [keypairEphemeral.address, NABLA_ROUTER], limits: defaultReadLimits, }); @@ -52,25 +62,29 @@ export async function performSwap( } const currentAllowance = parseContractBalanceResponse(assetInDetails.decimals, response.value); - const amountToSwapBig = stringDecimalToBN(swap.amountIn.toString(), assetInDetails.decimals); - const amountMinBig = stringDecimalToBN(swap.minAmountOut?.toString() ?? '0', assetInDetails.decimals); + + // Probably no need to multiply by power of ten here since amountIn comes from the event + //const rawAmountToSwapBig = multiplyByPowerOfTen(amountIn, assetInDetails.decimals); + const rawAmountToSwapBig = amountInRaw; + const rawAmountMinBig = multiplyByPowerOfTen(minAmountOut, assetOutDetails.decimals); + //maybe do allowance - if (currentAllowance !== undefined && currentAllowance.rawBalance.lt(amountToSwapBig)) { + if (currentAllowance !== undefined && currentAllowance.rawBalance.lt(rawAmountToSwapBig)) { try { renderEvent( - `Please sign approval swap: ${toBigNumber( - amountToSwapBig, + `Approving tokens: ${toBigNumber( + rawAmountToSwapBig, assetInDetails.decimals, )} ${assetInDetails.assetCode.toUpperCase()}`, EventStatus.Waiting, ); await approve({ api: pendulumApiComponents.api, - amount: amountToSwapBig.toString(), + amount: rawAmountToSwapBig.toFixed(), // toString can render exponential notation token: assetInDetails.erc20Address!, spender: NABLA_ROUTER, contractAbi: erc20ContractAbi, - walletAccount, + keypairEphemeral, }); } catch (e) { renderEvent(`Could not approve token: ${e}`, EventStatus.Error); @@ -79,7 +93,7 @@ export async function performSwap( } // balance before the swap const responseBalanceBefore = ( - await pendulumApiComponents.api.query.tokens.accounts(userAddress, assetOutDetails.currencyId) + await pendulumApiComponents.api.query.tokens.accounts(keypairEphemeral.address, assetOutDetails.currencyId) ).toHuman() as any; const rawBalanceBefore = responseBalanceBefore?.free || '0'; @@ -87,20 +101,19 @@ export async function performSwap( // Try swap try { + //TODO amountIN has all zeroes now, need to fix the message. renderEvent( - `Please sign transaction to swap ${swap.amountIn} ${assetInDetails.assetCode.toUpperCase()} to ${ - swap.initialDesired - } ${assetOutDetails.assetCode.toUpperCase()} `, + `Swapping ${amountIn} ${assetInDetails.assetCode.toUpperCase()} to ${minAmountOut} ${assetOutDetails.assetCode.toUpperCase()} `, EventStatus.Waiting, ); await doActualSwap({ api: pendulumApiComponents.api, - amount: amountToSwapBig.toString(), - amountMin: amountMinBig.toString(), + amount: rawAmountToSwapBig.toFixed(), // toString can render exponential notation + amountMin: rawAmountMinBig.toFixed(), // toString can render exponential notation tokenIn: assetInDetails.erc20Address!, tokenOut: assetOutDetails.erc20Address!, contractAbi: routerAbiObject, - walletAccount, + keypairEphemeral, }); } catch (e) { let errorMessage = ''; @@ -112,13 +125,13 @@ export async function performSwap( } else { errorMessage = 'Something went wrong'; } - renderEvent(`Could not swap token: ${errorMessage}`, EventStatus.Error); + renderEvent(`Could not swap the required amount of token: ${errorMessage}`, EventStatus.Error); return Promise.reject('Could not swap token'); } //verify token balance before releasing this process. const responseBalanceAfter = ( - await pendulumApiComponents.api.query.tokens.accounts(userAddress, assetOutDetails.currencyId) + await pendulumApiComponents.api.query.tokens.accounts(keypairEphemeral.address, assetOutDetails.currencyId) ).toHuman() as any; const rawBalanceAfter = responseBalanceAfter?.free || '0'; @@ -126,23 +139,23 @@ export async function performSwap( const actualOfframpValue = balanceAfterBigDecimal.sub(balanceBeforeBigDecimal); - renderEvent(`Swap successful. Amount to offramp : ${actualOfframpValue}`, EventStatus.Success); + renderEvent(`Swap successful. Amount received: ${actualOfframpValue}`, EventStatus.Success); - return actualOfframpValue.toNumber(); + return actualOfframpValue; } -async function approve({ api, token, spender, amount, contractAbi, walletAccount }: any) { +async function approve({ api, token, spender, amount, contractAbi, keypairEphemeral }: any) { console.log('write', `call approve ${token} for ${spender} with amount ${amount} `); + const response = await executeMessage({ abi: contractAbi, api, - callerAddress: walletAccount.address, + callerAddress: keypairEphemeral.address, contractDeploymentAddress: token, getSigner: () => Promise.resolve({ - type: 'signer', - address: walletAccount.address, - signer: walletAccount.signer, + type: 'keypair', + keypair: keypairEphemeral, }), messageName: 'approve', messageArguments: [spender, amount], @@ -150,28 +163,28 @@ async function approve({ api, token, spender, amount, contractAbi, walletAccount gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance }); - console.log('write', 'call approve response', walletAccount.address, [spender, amount], response); + console.log('write', 'call approve response', keypairEphemeral.address, [spender, amount], response); if (response?.result?.type !== 'success') throw response; return response; } -async function doActualSwap({ api, tokenIn, tokenOut, amount, amountMin, contractAbi, walletAccount }: any) { +async function doActualSwap({ api, tokenIn, tokenOut, amount, amountMin, contractAbi, keypairEphemeral }: any) { console.log('write', `call swap ${tokenIn} for ${tokenOut} with amount ${amount}, minimum expexted ${amountMin} `); + const response = await executeMessage({ abi: contractAbi, api, - callerAddress: walletAccount.address, + callerAddress: keypairEphemeral.address, contractDeploymentAddress: NABLA_ROUTER, getSigner: () => Promise.resolve({ - type: 'signer', - address: walletAccount.address, - signer: walletAccount.signer, + type: 'keypair', + keypair: keypairEphemeral, }), messageName: 'swapExactTokensForTokens', // Params found at https://github.com/0xamberhq/contracts/blob/e3ab9132dbe2d54a467bdae3fff20c13400f4d84/contracts/src/core/Router.sol#L98 - messageArguments: [amount, amountMin, [tokenIn, tokenOut], walletAccount.address, calcDeadline(5)], + messageArguments: [amount, amountMin, [tokenIn, tokenOut], keypairEphemeral.address, calcDeadline(5)], limits: { ...defaultWriteLimits, ...createWriteOptions(api) }, gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance }); diff --git a/src/services/polkadot/ephemeral.tsx b/src/services/polkadot/ephemeral.tsx new file mode 100644 index 00000000..d8340550 --- /dev/null +++ b/src/services/polkadot/ephemeral.tsx @@ -0,0 +1,132 @@ +import { Keyring } from '@polkadot/api'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { mnemonicGenerate } from '@polkadot/util-crypto'; +import { getApiManagerInstance } from './polkadotApi'; +import { parseTokenDepositEvent, TokenTransferEvent } from './eventParsers'; +import { compareObjects } from './eventParsers'; +import { getAddressForFormat } from '../../helpers/addressFormatter'; +import { decimalToNative } from '../../helpers/parseNumbers'; +import { TokenType } from '../../constants/tokenConfig'; +import { TOKEN_CONFIG } from '../../constants/tokenConfig'; + +let fundingAccountKeypair: KeyringPair | null = null; +const FUNDING_AMOUNT = decimalToNative(0.1).toNumber(); // 0.1 PEN +// TODO: replace +const SEED_PHRASE = 'hood protect select grace number hurt lottery property stomach grit bamboo field'; + +// print the public key using the correct ss58 format +async function printEphemeralAccount(seedPhrase: string) { + const { apiData } = await getApiManagerInstance(); + const keyring = new Keyring({ type: 'sr25519', ss58Format: apiData?.ss58Format }); + fundingAccountKeypair = keyring.addFromUri(seedPhrase); + console.log('Ephemeral account seedphrase: ', seedPhrase); + console.log('Ephemeral account created:', fundingAccountKeypair.address); +} + +export const getEphemeralAccount = () => { + if (!fundingAccountKeypair) { + const seedPhrase = mnemonicGenerate(); + const keyring = new Keyring({ type: 'sr25519' }); + fundingAccountKeypair = keyring.addFromUri(seedPhrase); + printEphemeralAccount(seedPhrase); + } + return fundingAccountKeypair; +}; + +export const fundEphemeralAccount = async () => { + try { + const pendulumApiComponents = await getApiManagerInstance(); + const apiData = pendulumApiComponents.apiData!; + + const ephemeralAddress = getEphemeralAccount().address; + + const keyring = new Keyring({ type: 'sr25519' }); + const fundingAccountKeypair = keyring.addFromUri(SEED_PHRASE); + + await apiData.api.tx.balances.transfer(ephemeralAddress, FUNDING_AMOUNT).signAndSend(fundingAccountKeypair); + } catch (error) { + console.error('Error funding account', error); + } +}; + +export const cleanEphemeralAccount = async (token: TokenType) => { + try { + const pendulumApiComponents = await getApiManagerInstance(); + const apiData = pendulumApiComponents.apiData!; + + const ephemeralKeyring = getEphemeralAccount(); + const ephemeralAddress = getAddressForFormat(ephemeralKeyring.address, apiData.ss58Format); + + const keyring = new Keyring({ type: 'sr25519' }); + const fundingAccountKeypair = keyring.addFromUri(SEED_PHRASE); + const fundingAccountAddress = getAddressForFormat(fundingAccountKeypair.address, apiData.ss58Format); + + // probably will never be exactly '0', but to be safe + // TODO: if the value is too small, do we really want to transfer token dust and spend fees? + await apiData.api.tx.tokens.transferAll(fundingAccountAddress, TOKEN_CONFIG[token].currencyId, false).signAndSend(ephemeralKeyring); + + await apiData.api.tx.balances.transferAll(fundingAccountAddress, false).signAndSend(ephemeralKeyring); + } catch (error) { + console.error('Error cleaning pendulum ephemeral account', error); + } +}; + +// function to check balance of account, native token +export async function checkBalance(): Promise { + const pendulumApiComponents = await getApiManagerInstance(); + if (!fundingAccountKeypair) { + return false; + } + const { data: balance } = await pendulumApiComponents.apiData!.api.query.system.account( + fundingAccountKeypair?.address, + ); + + // check if balance is higher than minimum required, then we consider the account ready + return balance.free.toNumber() >= FUNDING_AMOUNT; +} + +export async function waitForTokenReceptionEvent( + expectedCurrencyId: any, + maxWaitingTimeMs: number, +): Promise { + const pendulumApiComponents = await getApiManagerInstance(); + const ephemeralAddress = getEphemeralAccount().address; + const apiData = pendulumApiComponents.apiData!; + + const filter = (event: any) => { + if (event.event.section === 'tokens' && event.event.method === 'Deposited') { + console.log('Deposit Event:', event); + const eventParsed = parseTokenDepositEvent(event); + if (eventParsed.to != getAddressForFormat(ephemeralAddress, apiData.ss58Format)) { + return null; + } + if (compareObjects(eventParsed.currencyId, expectedCurrencyId)) { + return eventParsed; + } + } + return null; + }; + + return new Promise((resolve, reject) => { + let unsubscribeFromEventsPromise: Promise<() => void> | null = null; + const timeout = setTimeout(() => { + if (unsubscribeFromEventsPromise) { + unsubscribeFromEventsPromise.then((unsubscribe) => unsubscribe()); + } + reject(new Error(`Max waiting time exceeded for token reception`)); + }, maxWaitingTimeMs); + + unsubscribeFromEventsPromise = apiData.api.query.system.events((events) => { + events.forEach((event) => { + const eventParsed = filter(event); + if (eventParsed) { + if (unsubscribeFromEventsPromise) { + unsubscribeFromEventsPromise.then((unsubscribe) => unsubscribe()); + } + clearTimeout(timeout); + resolve(eventParsed); + } + }); + }); + }); +} diff --git a/src/services/polkadot/eventParsers.tsx b/src/services/polkadot/eventParsers.tsx index b48b43f2..ad39f2b7 100644 --- a/src/services/polkadot/eventParsers.tsx +++ b/src/services/polkadot/eventParsers.tsx @@ -1,7 +1,10 @@ import { stellarHexToPublic, hexToString } from './convert'; +import Big from 'big.js'; export type SpacewalkRedeemRequestEvent = ReturnType; +export type TokenTransferEvent = ReturnType; + export function parseEventRedeemRequest(event: any) { const rawEventData = JSON.parse(event.event.data.toString()); const mappedData = { @@ -75,3 +78,42 @@ function extractStellarAssetInfo(data: any) { throw new Error('Invalid Stellar type'); } } + +export function parseTokenDepositEvent(event: any) { + const rawEventData = JSON.parse(event.event.data.toString()); + const mappedData = { + currencyId: rawEventData[0], + to: rawEventData[1].toString() as string, + amountRaw: new Big(rawEventData[2].toString()) as Big, + }; + return mappedData; +} + +// Both functions used to compare betweem CurrencyId's +// where {XCM: x} == {xcm: x} +function normalizeObjectKeys(obj: any) { + return Object.keys(obj).reduce((acc: any, key) => { + acc[key.toLowerCase()] = obj[key]; + return acc; + }, {}); +} + +export function compareObjects(obj1: any, obj2: any) { + const normalizedObj1 = normalizeObjectKeys(obj1); + const normalizedObj2 = normalizeObjectKeys(obj2); + + const keys1 = Object.keys(normalizedObj1); + const keys2 = Object.keys(normalizedObj2); + + if (keys1.length !== keys2.length) { + return false; + } + + for (const key of keys1) { + if (normalizedObj1[key] !== normalizedObj2[key]) { + return false; + } + } + + return true; +} diff --git a/src/services/polkadot/index.tsx b/src/services/polkadot/index.tsx index 3ef987e7..2a3c2c0d 100644 --- a/src/services/polkadot/index.tsx +++ b/src/services/polkadot/index.tsx @@ -36,14 +36,10 @@ export async function executeSpacewalkRedeem( const stellarTargetKeypair = Keypair.fromPublicKey(stellarTargetAccountId); const stellarTargetAccountIdRaw = stellarTargetKeypair.rawPublicKey(); - console.log(`Requesting redeem of ${amountRaw} tokens for vault ${prettyPrintVaultId(targetVaultId)}`); - let redeemRequestEvent; try { renderEvent( - `Requesting redeem of ${amountRaw} tokens for vault ${prettyPrintVaultId( - targetVaultId, - )}. Please sign the transaction`, + `Requesting redeem of ${amountRaw} tokens for vault ${prettyPrintVaultId(targetVaultId)}`, EventStatus.Waiting, ); redeemRequestEvent = await vaultService.requestRedeem(accountOrPair, amountRaw, stellarTargetAccountIdRaw); diff --git a/src/services/squidrouter/config.ts b/src/services/squidrouter/config.ts new file mode 100644 index 00000000..af385d6e --- /dev/null +++ b/src/services/squidrouter/config.ts @@ -0,0 +1,21 @@ +import { TOKEN_CONFIG } from '../../constants/tokenConfig'; + +interface Config { + fromChainId: string; + toChainId: string; + fromToken: `0x${string}`; + axlUSDC_MOONBEAM: string; + integratorId: string; + receivingContractAddress: string; +} + +export function getSquidRouterConfig(): Config { + return { + fromChainId: '137', + toChainId: '1284', + fromToken: TOKEN_CONFIG.usdc.erc20AddressNativeChain as `0x${string}`, + axlUSDC_MOONBEAM: '0xca01a1d0993565291051daff390892518acfad3a', + integratorId: 'pendulum-7cffebc5-f84f-4669-96b4-4f8c82640811', + receivingContractAddress: '0x066d12e8f155c87a87d9db96eac0594e872c16b2', + }; +} diff --git a/src/services/squidrouter/index.tsx b/src/services/squidrouter/index.tsx new file mode 100644 index 00000000..8fbebe9b --- /dev/null +++ b/src/services/squidrouter/index.tsx @@ -0,0 +1,174 @@ +import { useAccount, useSendTransaction, useWaitForTransactionReceipt, useWriteContract } from 'wagmi'; +import { useCallback, useEffect, useState } from 'preact/compat'; +import { getRouteTransactionRequest } from './route'; +import erc20ABI from '../../contracts/ERC20'; +import { getSquidRouterConfig } from './config'; + +function useApproveSpending( + transactionRequestTarget: string | undefined, + fromToken: `0x${string}`, + fromAmount: string, +) { + const { data: hash, error, isPending, writeContract } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ + hash, + }); + + const approveSpending = useCallback(async () => { + console.log('Asking for approval of', transactionRequestTarget, fromToken, fromAmount); + + writeContract({ + abi: erc20ABI, + address: fromToken, + functionName: 'approve', + args: [transactionRequestTarget, fromAmount], + }); + }, [fromToken, fromAmount, transactionRequestTarget, writeContract]); + + return { + approveSpending, + error, + isPending, + isConfirming, + isConfirmed, + }; +} + +function useSendSwapTransaction(transactionRequest: any) { + const { data: hash, isPending, error, status, sendTransaction } = useSendTransaction(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: hash }); + + const sendSwapTransaction = useCallback(async () => { + if (!transactionRequest) { + console.error('No transaction request found'); + return; + } + + console.log('Sending swap transaction'); + + // Execute the swap transaction + sendTransaction({ + to: transactionRequest.target, + data: transactionRequest.data, + value: transactionRequest.value, + gas: BigInt(transactionRequest.gasLimit) * BigInt(2), + }); + }, [transactionRequest, sendTransaction]); + + return { + hash, + error, + sendSwapTransaction, + isConfirming, + isConfirmed, + }; +} + +export enum TransactionStatus { + Idle = 'Idle', + RouteRequested = 'RouteRequested', + ApproveSpending = 'ApproveSpending', + SpendingApproved = 'SpendingApproved', + InitiateSwap = 'InitiateSwap', + SwapCompleted = 'SwapCompleted', +} + +export function useSquidRouterSwap(amount: string) { + const { fromToken } = getSquidRouterConfig(); + + const [requestId, setRequestId] = useState(''); + const [transactionRequest, setTransactionRequest] = useState(); + const [transactionStatus, setTransactionStatus] = useState(TransactionStatus.Idle); + + const accountData = useAccount(); + + const { + approveSpending, + isConfirming: isApprovalConfirming, + isConfirmed: isSpendingApproved, + error: approveError, + } = useApproveSpending(transactionRequest?.target, fromToken, amount); + + const { + hash, + isConfirming: isSwapConfirming, + isConfirmed: isSwapCompleted, + sendSwapTransaction, + error: swapError, + } = useSendSwapTransaction(transactionRequest); + // Update the transaction status + useEffect(() => { + if (isApprovalConfirming) { + setTransactionStatus(TransactionStatus.ApproveSpending); + } else if (isSpendingApproved) { + setTransactionStatus(TransactionStatus.SpendingApproved); + } else if (isSwapConfirming) { + setTransactionStatus(TransactionStatus.InitiateSwap); + } else if (isSwapCompleted) { + setTransactionStatus(TransactionStatus.SwapCompleted); + } + }, [ + approveError, + swapError, + isApprovalConfirming, + isSpendingApproved, + isSwapConfirming, + isSwapCompleted, + transactionStatus, + ]); + + useEffect(() => { + if (!transactionRequest || transactionStatus !== TransactionStatus.RouteRequested) return; + + console.log('Calling function to approve spending'); + // Approve the transactionRequest.target to spend fromAmount of fromToken + approveSpending().catch((error) => console.error('Error approving spending:', error)); + }, [approveSpending, transactionRequest, transactionStatus]); + + useEffect(() => { + if (!isSpendingApproved || transactionStatus !== TransactionStatus.SpendingApproved) return; + + console.log('Transaction approved, executing swap'); + // Execute the swap transaction + sendSwapTransaction().catch((error) => console.error('Error sending swap transaction:', error)); + }, [isSpendingApproved, sendSwapTransaction, transactionStatus]); + + useEffect(() => { + if (!hash || !isSwapCompleted) return; + + console.log('Transaction confirmed!'); + // Show the transaction receipt with Axelarscan link + const axelarScanLink = 'https://axelarscan.io/gmp/' + hash; + console.log(`Finished! Check Axelarscan for details: ${axelarScanLink}`); + + // Update transaction status until it completes + // We don't do anything with the follow-up for now, but we might in the future + // updateTransactionStatus(hash, requestId).catch((error) => + // console.error('Error updating transaction status:', error), + // ); + }, [hash, isSwapCompleted]); + + const executeSquidRouterSwap = useCallback(async () => { + if (!accountData.address || !amount) { + console.error('No account address found or amount found'); + return; + } + + // Reset the transaction status + setTransactionStatus(TransactionStatus.RouteRequested); + + // Start by getting the transaction request for the Route + getRouteTransactionRequest(accountData.address, amount) + .then(({ requestId, transactionRequest }) => { + setRequestId(requestId); + setTransactionRequest(transactionRequest); + }) + .catch((error) => console.error('Error sending transaction request:', error)); + }, [accountData.address, amount]); + + return { + transactionStatus, + executeSquidRouterSwap, + error: approveError || swapError, + }; +} diff --git a/src/services/squidrouter/payload.ts b/src/services/squidrouter/payload.ts new file mode 100644 index 00000000..bfc5d1f5 --- /dev/null +++ b/src/services/squidrouter/payload.ts @@ -0,0 +1,38 @@ +import { eth } from 'web3'; + +function encodePayload(address: string): string { + // Encode the payload + // Asset should match the one received on Moonbeam side. Right now this is not used + // on the contract so it can be anything. + + const asset = [0, ['0x0424', '0x8d0BBbA567Ae73a06A8678e53Dc7ADD0AF6b7039']]; + const destination = [1, ['0x000000082E', '0x01' + address.slice(2) + '00']]; + + // Encode the data + const payload = eth.abi.encodeParameters( + [ + { + type: 'tuple', + components: [ + { type: 'uint8', name: 'parents' }, + { type: 'bytes[]', name: 'interior' }, + ], + name: 'asset', + }, + { + type: 'tuple', + components: [ + { type: 'uint8', name: 'parents' }, + { type: 'bytes[]', name: 'interior' }, + ], + name: 'destination', + }, + { type: 'uint256', name: 'weight' }, + ], + [asset, destination, 1000000000000000], + ); + + return payload; +} + +export default encodePayload; diff --git a/src/services/squidrouter/route.ts b/src/services/squidrouter/route.ts new file mode 100644 index 00000000..de74f7f4 --- /dev/null +++ b/src/services/squidrouter/route.ts @@ -0,0 +1,244 @@ +import axios from 'axios'; +import { encodeFunctionData } from 'viem'; +import { squidReceiverABI } from '../../contracts/SquidReceiver'; +import erc20ABI from '../../contracts/ERC20'; +import { getSquidRouterConfig } from './config'; +import encodePayload from './payload'; +import { getEphemeralAccount } from '../polkadot/ephemeral'; +import { u8aToHex } from '@polkadot/util'; +import { decodeAddress } from '@polkadot/util-crypto'; + +interface RouteParams { + fromAddress: string; + fromChain: string; + fromToken: string; + fromAmount: string; + toChain: string; + toToken: string; + toAddress: string; + slippageConfig: { + autoMode: number; + }; + enableExpress: boolean; + postHook: { + chainType: string; + calls: any[]; + provider: string; + description: string; + logoURI: string; + }; +} + +function createRouteParams(userAddress: string, amount: string): RouteParams { + const { fromToken, fromChainId, toChainId, receivingContractAddress, axlUSDC_MOONBEAM } = getSquidRouterConfig(); + + // TODO this must be approval, should we use max amount?? Or is this unsafe. + const approvalErc20 = encodeFunctionData({ + abi: erc20ABI, + functionName: 'approve', + args: [receivingContractAddress, 1000000000], + }); + + const ephemeralAccount = getEphemeralAccount(); + const ephemeralAccountHex = u8aToHex(decodeAddress(ephemeralAccount.address)); + + const payload = encodePayload(ephemeralAccountHex); + + const executeXCMEncodedData = encodeFunctionData({ + abi: squidReceiverABI, + functionName: 'executeXCM', + args: [payload, '0'], + }); + + return { + fromAddress: userAddress, + fromChain: fromChainId, + fromToken: fromToken, + fromAmount: amount, + toChain: toChainId, + toToken: axlUSDC_MOONBEAM, + toAddress: userAddress, + slippageConfig: { + autoMode: 1, + }, + enableExpress: true, + postHook: { + chainType: 'evm', + calls: [ + // approval call. + { + callType: 0, + target: axlUSDC_MOONBEAM, + value: '0', // this will be replaced by the full native balance of the multicall after the swap + callData: approvalErc20, + payload: { + tokenAddress: axlUSDC_MOONBEAM, // unused in callType 2, dummy value + inputPos: '1', // unused + }, + estimatedGas: '500000', + chainType: 'evm', + }, + // trigger the xcm call + { + callType: 1, // SquidCallType.FULL_TOKEN_BALANCE + target: receivingContractAddress, + value: '0', + callData: executeXCMEncodedData, + payload: { + tokenAddress: axlUSDC_MOONBEAM, + inputPos: '1', + }, + estimatedGas: '700000', + chainType: 'evm', + }, + ], + provider: 'Pendulum', //This should be the name of your product or application that is triggering the hook + description: 'Pendulum post hook', + logoURI: 'https://pbs.twimg.com/profile_images/1548647667135291394/W2WOtKUq_400x400.jpg', //Add your product or application's logo here + }, + }; +} + +async function getRoute(params: RouteParams) { + const { integratorId } = getSquidRouterConfig(); + + try { + const result = await axios.post( + 'https://apiplus.squidrouter.com/v2/route', + + params, + { + headers: { + 'x-integrator-id': integratorId, + 'Content-Type': 'application/json', + }, + }, + ); + const requestId = result.headers['x-request-id']; // Retrieve request ID from response headers + return { data: result.data, requestId: requestId }; + } catch (error) { + if (error) { + console.error('API error:', (error as any).response.data); + } + console.error('Error with parameters:', params); + throw error; + } +} + +async function getRouteApiPlus(params: RouteParams) { + // This is the integrator ID for the Squid API by https://v2.app.squidrouter.com/ + const { integratorId } = getSquidRouterConfig(); + const url = 'https://apiplus.squidrouter.com/v2/route'; + + try { + const result = await axios.post(url, params, { + headers: { + 'x-integrator-id': integratorId, + 'Content-Type': 'application/json', + }, + }); + + const requestId = result.headers['x-request-id']; // Retrieve request ID from response headers + return { data: result.data, requestId: requestId }; + } catch (error) { + if (error) { + console.error('API error:', (error as any).response.data); + } + console.error('Error with parameters:', params); + throw error; + } +} + +export async function getRouteTransactionRequest(userAddress: string, amount: string) { + const routeParams = createRouteParams(userAddress, amount); + + // Get the swap route using Squid API + const routeResult = await getRouteApiPlus(routeParams); + const route = routeResult.data.route; + const requestId = routeResult.requestId; + + console.log('Calculated route:', route); + console.log('requestId:', requestId); + + const transactionRequest = route.transactionRequest; + + return { + requestId, + transactionRequest, + }; +} + +// Function to get the optimal route for the swap using Squid API +interface StatusParams { + transactionId: string; + requestId: string; + fromChainId: string; + toChainId: string; +} + +// Function to get the status of the transaction using Squid API +async function getStatus(params: StatusParams) { + const { integratorId } = getSquidRouterConfig(); + + try { + const result = await axios.get('https://v2.api.squidrouter.com/v2/status', { + params: { + transactionId: params.transactionId, + requestId: params.requestId, + fromChainId: params.fromChainId, + toChainId: params.toChainId, + }, + headers: { + 'x-integrator-id': integratorId, + }, + }); + return result.data; + } catch (error) { + if (error) { + console.error('API error:', (error as any).response.data); + } + console.error('Error with parameters:', params); + throw error; + } +} + +// Function to periodically check the transaction status until it completes +export async function updateTransactionStatus(txHash: string, requestId: string) { + const { fromChainId, toChainId } = getSquidRouterConfig(); + + const getStatusParams = { + transactionId: txHash, + requestId: requestId, + fromChainId: fromChainId, + toChainId: toChainId, + }; + + let status; + const completedStatuses = ['success', 'partial_success', 'needs_gas', 'not_found']; + const maxRetries = 15; // Maximum number of retries for status check + let retryCount = 0; + + do { + try { + status = await getStatus(getStatusParams); + console.log(`Route status: ${status.squidTransactionStatus}`); + } catch (error) { + if ((error as any).response && (error as any).response.status === 404) { + retryCount++; + if (retryCount >= maxRetries) { + console.error('Max retries reached. Transaction not found.'); + break; + } + console.log('Transaction not found. Retrying...'); + await new Promise((resolve) => setTimeout(resolve, 20000)); + continue; + } else { + throw error; + } + } + + if (!completedStatuses.includes(status.squidTransactionStatus)) { + await new Promise((resolve) => setTimeout(resolve, 5000)); + } + } while (!completedStatuses.includes(status.squidTransactionStatus)); +} diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts new file mode 100644 index 00000000..43506d55 --- /dev/null +++ b/src/wagmiConfig.ts @@ -0,0 +1,37 @@ +import { connectorsForWallets, getDefaultConfig } from '@rainbow-me/rainbowkit'; +import { injectedWallet, safeWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; +import { polygon } from 'wagmi/chains'; +import { createConfig, http } from 'wagmi'; +import { createClient } from 'viem'; + +const connectors = connectorsForWallets( + [ + { + groupName: 'Recommended', + wallets: [injectedWallet, safeWallet, walletConnectWallet], + }, + ], + { + appName: 'Vortex', + projectId: '495a5f574d57e27fd65caa26d9ea4f10', + }, +); + +const defaultConfig = getDefaultConfig({ + appName: 'Vortex', + projectId: '495a5f574d57e27fd65caa26d9ea4f10', + chains: [polygon], + ssr: false, // If your dApp uses server side rendering (SSR) +}); + +export const wagmiConfig = createConfig({ + client({ chain }) { + return createClient({ + chain, + transport: http(), + }); + }, + chains: defaultConfig.chains, + connectors, + ssr: false, +}); diff --git a/yarn.lock b/yarn.lock index b3a8c209..a672e69c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,13 @@ __metadata: languageName: node linkType: hard +"@adraffy/ens-normalize@npm:^1.8.8": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: 4cb938c4abb88a346d50cb0ea44243ab3574330c81d4f5aaaf9dfee584b96189d0faa404de0fcbef5a1b73909ea4ebc3e63d84bd23f9949e5c8d4085207a5091 + languageName: node + linkType: hard + "@alloc/quick-lru@npm:^5.2.0": version: 5.2.0 resolution: "@alloc/quick-lru@npm:5.2.0" @@ -1393,21 +1400,21 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": - version: 7.23.5 - resolution: "@babel/runtime@npm:7.23.5" +"@babel/runtime@npm:^7.13.9, @babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.8.3": + version: 7.24.7 + resolution: "@babel/runtime@npm:7.24.7" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 0f1669f639af30a0a2948ffcefa2c61935f337b0777bd94f8d7bc66bba8e7d4499e725caeb0449540d9c6d67399b733c4e719babb43ce9a0f33095aa01b42b37 + checksum: 7b77f566165dee62db3db0296e71d08cafda3f34e1b0dcefcd68427272e17c1704f4e4369bff76651b07b6e49d3ea5a0ce344818af9116e9292e4381e0918c76 languageName: node linkType: hard -"@babel/runtime@npm:^7.19.4, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.8.3": - version: 7.24.7 - resolution: "@babel/runtime@npm:7.24.7" +"@babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.23.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.7": + version: 7.23.5 + resolution: "@babel/runtime@npm:7.23.5" dependencies: regenerator-runtime: "npm:^0.14.0" - checksum: 7b77f566165dee62db3db0296e71d08cafda3f34e1b0dcefcd68427272e17c1704f4e4369bff76651b07b6e49d3ea5a0ce344818af9116e9292e4381e0918c76 + checksum: 0f1669f639af30a0a2948ffcefa2c61935f337b0777bd94f8d7bc66bba8e7d4499e725caeb0449540d9c6d67399b733c4e719babb43ce9a0f33095aa01b42b37 languageName: node linkType: hard @@ -5277,6 +5284,19 @@ __metadata: languageName: node linkType: hard +"@types/babel__core@npm:^7.1.12": + version: 7.20.5 + resolution: "@types/babel__core@npm:7.20.5" + dependencies: + "@babel/parser": "npm:^7.20.7" + "@babel/types": "npm:^7.20.7" + "@types/babel__generator": "npm:*" + "@types/babel__template": "npm:*" + "@types/babel__traverse": "npm:*" + checksum: c32838d280b5ab59d62557f9e331d3831f8e547ee10b4f85cb78753d97d521270cebfc73ce501e9fb27fe71884d1ba75e18658692c2f4117543f0fc4e3e118b3 + languageName: node + linkType: hard + "@types/babel__core@npm:^7.1.14": version: 7.20.0 resolution: "@types/babel__core@npm:7.20.0" @@ -5583,6 +5603,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:8.5.3": + version: 8.5.3 + resolution: "@types/ws@npm:8.5.3" + dependencies: + "@types/node": "npm:*" + checksum: 08aac698ce6480b532d8311f790a8744ae489ccdd98f374cfe4b8245855439825c64b031abcbba4f30fb280da6cc2b02a4e261e16341d058ffaeecaa24ba2bd3 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -6275,6 +6304,19 @@ __metadata: languageName: node linkType: hard +"abitype@npm:0.7.1": + version: 0.7.1 + resolution: "abitype@npm:0.7.1" + peerDependencies: + typescript: ">=4.9.4" + zod: ^3 >=3.19.1 + peerDependenciesMeta: + zod: + optional: true + checksum: deee4a18c9c7218ab2e5e57e07e4cb3e2f3e785657be364d098ab0587cd552c4fbb41e1bdddbc6fa52387f51ebd181461fe70a13127cc77091655775fdfb18fe + languageName: node + linkType: hard + "abitype@npm:0.9.8": version: 0.9.8 resolution: "abitype@npm:0.9.8" @@ -6869,6 +6911,37 @@ __metadata: languageName: node linkType: hard +"babel-plugin-transform-vite-meta-env@npm:1.0.3": + version: 1.0.3 + resolution: "babel-plugin-transform-vite-meta-env@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.9" + "@types/babel__core": "npm:^7.1.12" + checksum: 60afd6358e3a16a8aae7ee911cd7a2dbdd0427a61f2d3e7eb9eaa85e4c895a1e22c620fdfd166306103f6733c2f6c5de1bc567c371d572b0f785856cc02bda32 + languageName: node + linkType: hard + +"babel-plugin-transform-vite-meta-glob@npm:1.1.2": + version: 1.1.2 + resolution: "babel-plugin-transform-vite-meta-glob@npm:1.1.2" + dependencies: + "@babel/runtime": "npm:^7.13.9" + "@types/babel__core": "npm:^7.1.12" + glob: "npm:^10.3.10" + checksum: 8a73f38a0827f07f2e5cd8f41066eb7fac08ec2ce900adc671cae817a55faf349f4f17946971d3fd80eb5f31657cd58c44e6da8f5a47668e39da0d90facd64bd + languageName: node + linkType: hard + +"babel-plugin-transform-vite-meta-hot@npm:1.0.0": + version: 1.0.0 + resolution: "babel-plugin-transform-vite-meta-hot@npm:1.0.0" + dependencies: + "@babel/runtime": "npm:^7.13.9" + "@types/babel__core": "npm:^7.1.12" + checksum: bcaeecbe89f6f279c8bf90afa2cec0f3e86576179b268cefba9acac4070799a243e66d6e31ec6283068df964a5beb746390ed025b52bb6fc651e69a1610ccaa5 + languageName: node + linkType: hard + "babel-preset-current-node-syntax@npm:^1.0.0": version: 1.0.1 resolution: "babel-preset-current-node-syntax@npm:1.0.1" @@ -6903,6 +6976,19 @@ __metadata: languageName: node linkType: hard +"babel-preset-vite@npm:^1.1.3": + version: 1.1.3 + resolution: "babel-preset-vite@npm:1.1.3" + dependencies: + "@babel/runtime": "npm:^7.13.9" + "@types/babel__core": "npm:^7.1.12" + babel-plugin-transform-vite-meta-env: "npm:1.0.3" + babel-plugin-transform-vite-meta-glob: "npm:1.1.2" + babel-plugin-transform-vite-meta-hot: "npm:1.0.0" + checksum: 243b43c25846a597a03cf36b1b0e6b39d209f5a13207a238bdc9a03b61fcdd450abe2a26595cde17513c321ba63071868def875a54664e45058b5c532f8306a4 + languageName: node + linkType: hard + "babel-runtime@npm:6.26.0": version: 6.26.0 resolution: "babel-runtime@npm:6.26.0" @@ -7763,7 +7849,7 @@ __metadata: languageName: node linkType: hard -"crc-32@npm:^1.2.0": +"crc-32@npm:^1.2.0, crc-32@npm:^1.2.2": version: 1.2.2 resolution: "crc-32@npm:1.2.2" bin: @@ -11264,6 +11350,15 @@ __metadata: languageName: node linkType: hard +"isomorphic-ws@npm:^5.0.0": + version: 5.0.0 + resolution: "isomorphic-ws@npm:5.0.0" + peerDependencies: + ws: "*" + checksum: e20eb2aee09ba96247465fda40c6d22c1153394c0144fa34fe6609f341af4c8c564f60ea3ba762335a7a9c306809349f9b863c8beedf2beea09b299834ad5398 + languageName: node + linkType: hard + "isows@npm:1.0.3": version: 1.0.3 resolution: "isows@npm:1.0.3" @@ -13883,6 +13978,7 @@ __metadata: "@walletconnect/modal": "npm:^2.6.2" "@walletconnect/universal-provider": "npm:^2.12.2" autoprefixer: "npm:^10.4.19" + babel-preset-vite: "npm:^1.1.3" big.js: "npm:^6.2.1" bn.js: "npm:^5.2.1" buffer: "npm:^6.0.3" @@ -13912,6 +14008,7 @@ __metadata: viem: "npm:2.x" vite: "npm:^3.2.5" wagmi: "npm:^2.10.3" + web3: "npm:^4.10.0" yup: "npm:^1.4.0" languageName: unknown linkType: soft @@ -15761,6 +15858,13 @@ __metadata: languageName: node linkType: hard +"setimmediate@npm:^1.0.5": + version: 1.0.5 + resolution: "setimmediate@npm:1.0.5" + checksum: 76e3f5d7f4b581b6100ff819761f04a984fa3f3990e72a6554b57188ded53efce2d3d6c0932c10f810b7c59414f85e2ab3c11521877d1dea1ce0b56dc906f485 + languageName: node + linkType: hard + "setprototypeof@npm:1.1.0": version: 1.1.0 resolution: "setprototypeof@npm:1.1.0" @@ -17365,7 +17469,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.4": +"util@npm:^0.12.4, util@npm:^0.12.5": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: @@ -17605,6 +17709,270 @@ __metadata: languageName: node linkType: hard +"web3-core@npm:^4.3.0, web3-core@npm:^4.4.0, web3-core@npm:^4.5.0": + version: 4.5.0 + resolution: "web3-core@npm:4.5.0" + dependencies: + web3-errors: "npm:^1.2.0" + web3-eth-accounts: "npm:^4.1.2" + web3-eth-iban: "npm:^4.0.7" + web3-providers-http: "npm:^4.1.0" + web3-providers-ipc: "npm:^4.0.7" + web3-providers-ws: "npm:^4.0.7" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + dependenciesMeta: + web3-providers-ipc: + optional: true + checksum: d5cf3ea45727ee416c6b125c401f8555d88e255d7efbef5b57aac073556489c14c41f6be5c2fe555ca21259e9bc4bb895bd3fcc0d802bfaf680c03fad3cb22a8 + languageName: node + linkType: hard + +"web3-errors@npm:^1.1.3, web3-errors@npm:^1.1.4, web3-errors@npm:^1.2.0": + version: 1.2.0 + resolution: "web3-errors@npm:1.2.0" + dependencies: + web3-types: "npm:^1.6.0" + checksum: 99d0ecc4368c2969cc799ed4ef1b35f233dc3e7e0ccde713bcc850be4e75725ce175127342512504057ca48186266d6a053f129634f20ae23c405855a766d13d + languageName: node + linkType: hard + +"web3-eth-abi@npm:^4.2.2": + version: 4.2.2 + resolution: "web3-eth-abi@npm:4.2.2" + dependencies: + abitype: "npm:0.7.1" + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.6.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + checksum: 61ebfc77769ca1bd80cf363f6d15847b863000f0b873bfec055e1af7905812805fe474716675b7f8189899426d501eb139558e29c1f77ea850b49bac37320eca + languageName: node + linkType: hard + +"web3-eth-accounts@npm:^4.1.2": + version: 4.1.2 + resolution: "web3-eth-accounts@npm:4.1.2" + dependencies: + "@ethereumjs/rlp": "npm:^4.0.1" + crc-32: "npm:^1.2.2" + ethereum-cryptography: "npm:^2.0.0" + web3-errors: "npm:^1.1.4" + web3-types: "npm:^1.6.0" + web3-utils: "npm:^4.2.3" + web3-validator: "npm:^2.0.5" + checksum: da3d56da54b625c958d26cab7dbe27000b54dce4a07d6f89f10ca744497a3374fe1dcbd197fedaaf6dd3786eced5ee1d73a60e0aeb4c80b9cf08a55cdb2df6fc + languageName: node + linkType: hard + +"web3-eth-contract@npm:^4.5.0": + version: 4.5.0 + resolution: "web3-eth-contract@npm:4.5.0" + dependencies: + web3-core: "npm:^4.4.0" + web3-errors: "npm:^1.2.0" + web3-eth: "npm:^4.7.0" + web3-eth-abi: "npm:^4.2.2" + web3-types: "npm:^1.6.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + checksum: 9ae7ee5561449843e91cfcc983369bf652be41c00e3ab4d994d13af87394528177b09b7aadc194acf004489a4c86d4789f887c73f8a85f27781a9cca8b72edc1 + languageName: node + linkType: hard + +"web3-eth-ens@npm:^4.4.0": + version: 4.4.0 + resolution: "web3-eth-ens@npm:4.4.0" + dependencies: + "@adraffy/ens-normalize": "npm:^1.8.8" + web3-core: "npm:^4.5.0" + web3-errors: "npm:^1.2.0" + web3-eth: "npm:^4.8.0" + web3-eth-contract: "npm:^4.5.0" + web3-net: "npm:^4.1.0" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + checksum: 25a1535e095d8ffcbc0641041af69e42aa60ba2989477108a5678c42a06135df9134ccc6024c89c216cb3408848e3905ee178d5b12e3bb740e895ee6ee0bd2cf + languageName: node + linkType: hard + +"web3-eth-iban@npm:^4.0.7": + version: 4.0.7 + resolution: "web3-eth-iban@npm:4.0.7" + dependencies: + web3-errors: "npm:^1.1.3" + web3-types: "npm:^1.3.0" + web3-utils: "npm:^4.0.7" + web3-validator: "npm:^2.0.3" + checksum: 9d7521b4d4aef3a0d697905c7859d8e4d7ce82234320beecba9b24d254592a7ccf0354f329289b4e11a816fcbe3eceb842c4c87678f5e8ec622c8351bc1b9170 + languageName: node + linkType: hard + +"web3-eth-personal@npm:^4.0.8": + version: 4.0.8 + resolution: "web3-eth-personal@npm:4.0.8" + dependencies: + web3-core: "npm:^4.3.0" + web3-eth: "npm:^4.3.1" + web3-rpc-methods: "npm:^1.1.3" + web3-types: "npm:^1.3.0" + web3-utils: "npm:^4.0.7" + web3-validator: "npm:^2.0.3" + checksum: 6e9ab6298a3469e37bcf8930136673b3eff3ac95a763b5b924cda0861326508a12902c9b53d443057ea9c64b12b0b1ed9b66f3a0031b74746605010880d0fc7c + languageName: node + linkType: hard + +"web3-eth@npm:^4.3.1, web3-eth@npm:^4.7.0, web3-eth@npm:^4.8.0": + version: 4.8.0 + resolution: "web3-eth@npm:4.8.0" + dependencies: + setimmediate: "npm:^1.0.5" + web3-core: "npm:^4.5.0" + web3-errors: "npm:^1.2.0" + web3-eth-abi: "npm:^4.2.2" + web3-eth-accounts: "npm:^4.1.2" + web3-net: "npm:^4.1.0" + web3-providers-ws: "npm:^4.0.7" + web3-rpc-methods: "npm:^1.3.0" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + checksum: c0822835bbecbf1f9a115d1c1e060b999479350a116e8f1b3e86ece3add9eba8217c6a6f59d7b1612032db05fef1e56e6901a3de15e1140d0123b1993267c484 + languageName: node + linkType: hard + +"web3-net@npm:^4.1.0": + version: 4.1.0 + resolution: "web3-net@npm:4.1.0" + dependencies: + web3-core: "npm:^4.4.0" + web3-rpc-methods: "npm:^1.3.0" + web3-types: "npm:^1.6.0" + web3-utils: "npm:^4.3.0" + checksum: 2899ed28d9afda9f9faee6424752cb967dabf79128bce25321318e069a41571b9bd9477b480f290fd65f07cd6c0c641def0d72f31a730705112bd14c301f4e5e + languageName: node + linkType: hard + +"web3-providers-http@npm:^4.1.0": + version: 4.1.0 + resolution: "web3-providers-http@npm:4.1.0" + dependencies: + cross-fetch: "npm:^4.0.0" + web3-errors: "npm:^1.1.3" + web3-types: "npm:^1.3.0" + web3-utils: "npm:^4.0.7" + checksum: d98d3cedd8caadb7f6f8ab6faa74d6f42be5808e729a93d815771fb7287f9fffa9ecdc047dceaac783a329c63947f006bca758f2241dc57070aefb62cdb0f2dc + languageName: node + linkType: hard + +"web3-providers-ipc@npm:^4.0.7": + version: 4.0.7 + resolution: "web3-providers-ipc@npm:4.0.7" + dependencies: + web3-errors: "npm:^1.1.3" + web3-types: "npm:^1.3.0" + web3-utils: "npm:^4.0.7" + checksum: b953818479f5d9c7b748e10977430fd7e377696f9160ae19b1917c0317e89671c4be824c06723b6fda190258927160fcec0e8e7c1aa87a5f0344008ef7649cda + languageName: node + linkType: hard + +"web3-providers-ws@npm:^4.0.7": + version: 4.0.7 + resolution: "web3-providers-ws@npm:4.0.7" + dependencies: + "@types/ws": "npm:8.5.3" + isomorphic-ws: "npm:^5.0.0" + web3-errors: "npm:^1.1.3" + web3-types: "npm:^1.3.0" + web3-utils: "npm:^4.0.7" + ws: "npm:^8.8.1" + checksum: ceb2da6a1534bd2f6d60533777b0b1e35de9947d07a856be64499aedbe3ba48f744ab6196dcaf60f252e2a1a7939680dcc15db656f10afe39a17282a89f9d575 + languageName: node + linkType: hard + +"web3-rpc-methods@npm:^1.1.3, web3-rpc-methods@npm:^1.3.0": + version: 1.3.0 + resolution: "web3-rpc-methods@npm:1.3.0" + dependencies: + web3-core: "npm:^4.4.0" + web3-types: "npm:^1.6.0" + web3-validator: "npm:^2.0.6" + checksum: 8c134b1f2ae1cf94d5c452c53fe699d5951c22c62ea82084559db06722a5f0db2047be4209172ff90432c42f70cf8081fea0ea85a024e4cbcd0e037efd9acfa8 + languageName: node + linkType: hard + +"web3-rpc-providers@npm:^1.0.0-rc.0": + version: 1.0.0-rc.0 + resolution: "web3-rpc-providers@npm:1.0.0-rc.0" + dependencies: + web3-providers-http: "npm:^4.1.0" + web3-providers-ws: "npm:^4.0.7" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.0" + checksum: 6c5fd162f08f9df7ba7d0e3f413b6fd380fa60c4d72a624168e6a7c5b77b57877ae4fb7624f60a8d5f55c1ddf7758e3a7d0d3de088703528a9d5da14060ed871 + languageName: node + linkType: hard + +"web3-types@npm:^1.3.0, web3-types@npm:^1.6.0, web3-types@npm:^1.7.0": + version: 1.7.0 + resolution: "web3-types@npm:1.7.0" + checksum: fcd5d7a9a94579fcd01fa86dfa70e6afb269f66a7ce60e6786849e64ff6e4a107f1c25cb2784343a48952ac36d4bf3093a73b75de6ebcc971308e6b44abb211f + languageName: node + linkType: hard + +"web3-utils@npm:^4.0.7, web3-utils@npm:^4.2.3, web3-utils@npm:^4.3.0": + version: 4.3.0 + resolution: "web3-utils@npm:4.3.0" + dependencies: + ethereum-cryptography: "npm:^2.0.0" + eventemitter3: "npm:^5.0.1" + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.6.0" + web3-validator: "npm:^2.0.6" + checksum: 486178b47721b8a3f0f4c56c0f0250e9891656ba79c22c16915152ed811e29901567442fa87bbd5b4ad83280e2d4fc23221bcae7b24efb624d74210c5f2c05bb + languageName: node + linkType: hard + +"web3-validator@npm:^2.0.3, web3-validator@npm:^2.0.5, web3-validator@npm:^2.0.6": + version: 2.0.6 + resolution: "web3-validator@npm:2.0.6" + dependencies: + ethereum-cryptography: "npm:^2.0.0" + util: "npm:^0.12.5" + web3-errors: "npm:^1.2.0" + web3-types: "npm:^1.6.0" + zod: "npm:^3.21.4" + checksum: 4df08e5317d55cdb674cbd11d7534a6cb41abfa4912cf3ff976c2b34a98e84500732fa0cade68a848e57b61259b4c9b377773f57de6bb69a5029c2ddef1cd0ab + languageName: node + linkType: hard + +"web3@npm:^4.10.0": + version: 4.10.0 + resolution: "web3@npm:4.10.0" + dependencies: + web3-core: "npm:^4.5.0" + web3-errors: "npm:^1.2.0" + web3-eth: "npm:^4.8.0" + web3-eth-abi: "npm:^4.2.2" + web3-eth-accounts: "npm:^4.1.2" + web3-eth-contract: "npm:^4.5.0" + web3-eth-ens: "npm:^4.4.0" + web3-eth-iban: "npm:^4.0.7" + web3-eth-personal: "npm:^4.0.8" + web3-net: "npm:^4.1.0" + web3-providers-http: "npm:^4.1.0" + web3-providers-ws: "npm:^4.0.7" + web3-rpc-methods: "npm:^1.3.0" + web3-rpc-providers: "npm:^1.0.0-rc.0" + web3-types: "npm:^1.7.0" + web3-utils: "npm:^4.3.0" + web3-validator: "npm:^2.0.6" + checksum: 0a842b9d2a6b68e16908452291709dceb20fbe4ec88e59ca5cca79b9c1d9e27e497f92cefa3aa3f2e6fc0ea6f015843a3f66fc552bf665acbd4dd8960f98e73b + languageName: node + linkType: hard + "webextension-polyfill@npm:>=0.10.0 <1.0": version: 0.12.0 resolution: "webextension-polyfill@npm:0.12.0" @@ -18062,6 +18430,13 @@ __metadata: languageName: node linkType: hard +"zod@npm:^3.21.4": + version: 3.23.8 + resolution: "zod@npm:3.23.8" + checksum: 846fd73e1af0def79c19d510ea9e4a795544a67d5b34b7e1c4d0425bf6bfd1c719446d94cdfa1721c1987d891321d61f779e8236fde517dc0e524aa851a6eff1 + languageName: node + linkType: hard + "zustand@npm:4.4.1": version: 4.4.1 resolution: "zustand@npm:4.4.1"