diff --git a/.changeset/strange-chicken-battle.md b/.changeset/strange-chicken-battle.md deleted file mode 100644 index 8b91075e..00000000 --- a/.changeset/strange-chicken-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reservoir0x/relay-sdk': patch ---- - -Add blockProductionLagging to RelayChain diff --git a/.changeset/sweet-shoes-cross.md b/.changeset/sweet-shoes-cross.md deleted file mode 100644 index e6fc487a..00000000 --- a/.changeset/sweet-shoes-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@reservoir0x/relay-kit-ui': patch ---- - -Only show fill time on success page if sub 10s diff --git a/demo/pages/_app.tsx b/demo/pages/_app.tsx index 2d1f3751..3350ca99 100644 --- a/demo/pages/_app.tsx +++ b/demo/pages/_app.tsx @@ -33,13 +33,17 @@ const AppWrapper: FC = ({ children }) => { ReturnType | undefined >() const router = useRouter() - const relayApi = - router.query.api === 'testnets' ? TESTNET_RELAY_API : MAINNET_RELAY_API + const [relayApi, setRelayApi] = useState(MAINNET_RELAY_API) + + useEffect(() => { + const isTestnet = router.query.api === 'testnets' + setRelayApi(isTestnet ? TESTNET_RELAY_API : MAINNET_RELAY_API) + }, [router.query.api]) const { chains, viemChains } = useRelayChains(relayApi) useEffect(() => { - if (!wagmiConfig && chains && viemChains) { + if (chains && viemChains) { setWagmiConfig( getDefaultConfig({ appName: 'Relay SDK Demo', @@ -50,7 +54,7 @@ const AppWrapper: FC = ({ children }) => { }) ) } - }, [chains, relayApi]) + }, [chains, relayApi, viemChains]) if (!wagmiConfig || !chains) { return null @@ -65,6 +69,7 @@ const AppWrapper: FC = ({ children }) => { > { + const client = useRelayClient() + const [user, setUser] = useState( + '0x03508bB71268BBA25ECaCC8F620e01866650532c' + ) + const [originChainId, setOriginChainId] = useState(1) + const [destinationChainId, setDestinationChainId] = useState(10) + const [originCurrency, setOriginCurrency] = useState(zeroAddress) + const [destinationCurrency, setDestinationCurrency] = + useState(zeroAddress) + const [recipient, setRecipient] = useState() + const [tradeType, setTradeType] = useState<'EXACT_INPUT' | 'EXACT_OUTPUT'>( + 'EXACT_INPUT' + ) + const [source, setSource] = useState() + const [useExternalLiquidity, setUseExternalLiquidity] = + useState(false) + const [appFees, setAppFees] = useState<{ recipient: string; fee: string }[]>() + const [amount, setAmount] = useState('10000000000000000') + const [data, setData] = useState(undefined) + const { data: response } = usePrice(client ?? undefined, data, () => {}, { + enabled: data !== undefined && client !== undefined + }) + + return ( +
+
+
+ + + setUser(e.target.value.length > 0 ? e.target.value : undefined) + } + /> +
+
+ + setOriginChainId(+e.target.value)} + /> +
+
+ + setDestinationChainId(+e.target.value)} + /> +
+
+ + setOriginCurrency(e.target.value)} + /> +
+
+ + setDestinationCurrency(e.target.value)} + /> +
+
+ + + setRecipient( + e.target.value.length > 0 ? e.target.value : undefined + ) + } + /> +
+
+ + +
+
+ + { + if (e.target.value.length > 0) { + const fee = e.target.value.split(',').map((fee) => ({ + recipient: fee.split(':')[0], + fee: fee.split(':')[1] + })) + setAppFees(fee) + } else { + setAppFees(undefined) + } + }} + /> +
+
+ + setAmount(e.target.value)} + /> +
+ +
+
+
+          {JSON.stringify(response, null, 2)}
+        
+
+
+ ) +} + +export default UsePrice diff --git a/demo/pages/index.tsx b/demo/pages/index.tsx index b147cdae..498dce22 100644 --- a/demo/pages/index.tsx +++ b/demo/pages/index.tsx @@ -48,6 +48,7 @@ const Index: NextPage = () => { useRelayChains useRequests useTokenList + usePrice diff --git a/demo/pages/ui/chain.tsx b/demo/pages/ui/chain.tsx index e4d69898..66ab4466 100644 --- a/demo/pages/ui/chain.tsx +++ b/demo/pages/ui/chain.tsx @@ -2,6 +2,7 @@ import { NextPage } from 'next' import { ChainWidget } from '@reservoir0x/relay-kit-ui' import { useConnectModal } from '@rainbow-me/rainbowkit' import { Layout } from 'components/Layout' +import { zeroAddress } from 'viem' const ChainWidgetPage: NextPage = () => { const { openConnectModal } = useConnectModal() diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index 6a0c1fec..7cf65fbc 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -1,5 +1,43 @@ # @reservoir0x/relay-kit-hooks +## 1.0.18 + +### Patch Changes + +- Updated dependencies [ec9b20f] + - @reservoir0x/relay-sdk@1.0.12 + +## 1.0.17 + +### Patch Changes + +- Updated dependencies [92b9c71] + - @reservoir0x/relay-sdk@1.0.11 + +## 1.0.16 + +### Patch Changes + +- fac3415: Fix chain widget testnet configuration + +## 1.0.15 + +### Patch Changes + +- Add review quote step, refactor modal renderers +- 2d22eec: Add usePrice hook +- Updated dependencies +- Updated dependencies [2d22eec] + - @reservoir0x/relay-sdk@1.0.10 + +## 1.0.14 + +### Patch Changes + +- Updated dependencies [3648ec3] +- Updated dependencies [be74a6a] + - @reservoir0x/relay-sdk@1.0.9 + ## 1.0.13 ### Patch Changes diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 2f8e8620..3b2cf6b5 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@reservoir0x/relay-kit-hooks", - "version": "1.0.13", + "version": "1.0.18", "type": "module", "main": "./_cjs/src/index.js", "module": "./_esm/src/index.js", diff --git a/packages/hooks/src/hooks/usePrice.ts b/packages/hooks/src/hooks/usePrice.ts new file mode 100644 index 00000000..6d21abe6 --- /dev/null +++ b/packages/hooks/src/hooks/usePrice.ts @@ -0,0 +1,80 @@ +import { + MAINNET_RELAY_API, + RelayClient, + type Execute, + type paths +} from '@reservoir0x/relay-sdk' +import { axiosPostFetcher } from '../fetcher.js' +import { + useQuery, + type DefaultError, + type QueryKey +} from '@tanstack/react-query' +import type { AxiosRequestConfig } from 'axios' + +type PriceRequestBody = + paths['/price']['post']['requestBody']['content']['application/json'] + +export type PriceResponse = + paths['/price']['post']['responses']['200']['content']['application/json'] + +type QueryType = typeof useQuery< + PriceResponse, + DefaultError, + PriceResponse, + QueryKey +> +type QueryOptions = Parameters['0'] + +export const queryPrice = function ( + baseApiUrl: string = MAINNET_RELAY_API, + options?: PriceRequestBody +): Promise { + return new Promise((resolve, reject) => { + const url = new URL(`${baseApiUrl}/price`) + axiosPostFetcher(url.href, options) + .then((response) => { + const request: AxiosRequestConfig = { + url: url.href, + method: 'post', + data: options + } + resolve({ + ...response, + request + }) + }) + .catch((e) => { + reject(e) + }) + }) +} + +export default function usePrice( + client?: RelayClient, + options?: PriceRequestBody, + onResponse?: (data: Execute) => void, + queryOptions?: Partial +) { + const response = (useQuery as QueryType)({ + queryKey: ['usePrice', options], + queryFn: () => { + if (options && client?.source && !options.referrer) { + options.referrer = client.source + } + const promise = queryPrice(client?.baseApiUrl, options) + promise.then((response: any) => { + onResponse?.(response) + }) + return promise + }, + enabled: client !== undefined && options !== undefined, + retry: false, + ...queryOptions + }) + + return { + ...response, + data: response.error ? undefined : response.data + } +} diff --git a/packages/hooks/src/hooks/useQuote.ts b/packages/hooks/src/hooks/useQuote.ts index 0adbafed..24812304 100644 --- a/packages/hooks/src/hooks/useQuote.ts +++ b/packages/hooks/src/hooks/useQuote.ts @@ -54,7 +54,7 @@ export const queryQuote = function ( }) } -type onProgress = (data: ProgressData) => void +export type onProgress = (data: ProgressData) => void export default function ( client?: RelayClient, @@ -113,10 +113,18 @@ export default function ( ...response, data: response.error ? undefined : response.data, executeQuote - }) as Omit, 'data'> & { + } as Omit, 'data'> & { data?: ExecuteSwapResponse executeQuote: (onProgress: onProgress) => Promise | undefined - }, - [response.data, response.error, response.isLoading, executeQuote] + }), + [ + response.data, + response.error, + response.isLoading, + response.isFetching, + response.isRefetching, + response.dataUpdatedAt, + executeQuote + ] ) } diff --git a/packages/hooks/src/hooks/useRelayChains.ts b/packages/hooks/src/hooks/useRelayChains.ts index bc622b1e..09a1c208 100644 --- a/packages/hooks/src/hooks/useRelayChains.ts +++ b/packages/hooks/src/hooks/useRelayChains.ts @@ -42,7 +42,7 @@ export default function ( queryOptions?: Partial ) { const response = (useQuery as QueryType)({ - queryKey: ['useRelayChains', options], + queryKey: ['useRelayChains', baseApiUrl, options], queryFn: () => queryRelayChains(baseApiUrl, options), retry: false, ...queryOptions @@ -61,5 +61,5 @@ export default function ( viemChains?: ConfiguredViemChain['viemChain'][] chains?: ConfiguredViemChain[] } - }, [response.data, response.error, response.isLoading]) + }, [response.data, response.data?.chains, response.error, response.isLoading]) } diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 5969cd0f..3977f5bd 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,6 +1,7 @@ //hooks export { default as useRequests, queryRequests } from './hooks/useRequests.js' export { default as useQuote, queryQuote } from './hooks/useQuote.js' +export { default as usePrice, queryPrice } from './hooks/usePrice.js' export { default as useTokenList, queryTokenList @@ -16,3 +17,5 @@ export { //types export type { CurrencyList, Currency } from './hooks/useTokenList.js' +export type { PriceResponse } from './hooks/usePrice.js' +export type { ExecuteSwapResponse } from './hooks/useQuote.js' diff --git a/packages/relay-ethers-wallet-adapter/CHANGELOG.md b/packages/relay-ethers-wallet-adapter/CHANGELOG.md index 7483df4a..46954e5a 100644 --- a/packages/relay-ethers-wallet-adapter/CHANGELOG.md +++ b/packages/relay-ethers-wallet-adapter/CHANGELOG.md @@ -1,5 +1,36 @@ # @reservoir0x/relay-ethers-wallet-adapter +## 9.0.12 + +### Patch Changes + +- Updated dependencies [ec9b20f] + - @reservoir0x/relay-sdk@1.0.12 + +## 9.0.11 + +### Patch Changes + +- Updated dependencies [92b9c71] + - @reservoir0x/relay-sdk@1.0.11 + +## 9.0.10 + +### Patch Changes + +- Add review quote step, refactor modal renderers +- Updated dependencies +- Updated dependencies [2d22eec] + - @reservoir0x/relay-sdk@1.0.10 + +## 9.0.9 + +### Patch Changes + +- Updated dependencies [3648ec3] +- Updated dependencies [be74a6a] + - @reservoir0x/relay-sdk@1.0.9 + ## 9.0.8 ### Patch Changes diff --git a/packages/relay-ethers-wallet-adapter/package.json b/packages/relay-ethers-wallet-adapter/package.json index 5e4feafa..aaf1cc8d 100644 --- a/packages/relay-ethers-wallet-adapter/package.json +++ b/packages/relay-ethers-wallet-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@reservoir0x/relay-ethers-wallet-adapter", - "version": "9.0.8", + "version": "9.0.12", "description": "An adapter used to convert an ethersjs signer to an Adapted Wallet for use in the @reservoir0x/relay-sdk", "type": "module", "source": "./src/index.ts", diff --git a/packages/sdk/CHANGELOG.md b/packages/sdk/CHANGELOG.md index 739d7e12..735b563b 100644 --- a/packages/sdk/CHANGELOG.md +++ b/packages/sdk/CHANGELOG.md @@ -1,5 +1,31 @@ # @reservoir0x/relay-sdk +## 1.0.12 + +### Patch Changes + +- ec9b20f: Add supportsBridging to Relay Chain + +## 1.0.11 + +### Patch Changes + +- 92b9c71: Sync api types + +## 1.0.10 + +### Patch Changes + +- Add review quote step, refactor modal renderers +- 2d22eec: Add usePrice hook + +## 1.0.9 + +### Patch Changes + +- 3648ec3: Better contextualize actions based on operation +- be74a6a: Add blockProductionLagging to RelayChain + ## 1.0.8 ### Patch Changes diff --git a/packages/sdk/package.json b/packages/sdk/package.json index e8eb83e8..9e42dcac 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@reservoir0x/relay-sdk", - "version": "1.0.8", + "version": "1.0.12", "type": "module", "main": "./_cjs/src/index.js", "module": "./_esm/src/index.js", diff --git a/packages/sdk/src/routes/index.ts b/packages/sdk/src/routes/index.ts index e9f30a5f..8de662e1 100644 --- a/packages/sdk/src/routes/index.ts +++ b/packages/sdk/src/routes/index.ts @@ -10,6 +10,8 @@ export const routes = [ "/execute/magic-spend/deposit", "/execute/magic-spend/request", "/execute/permits", + "/quote", + "/price", "/lives", "/intents/status", "/intents/quote", diff --git a/packages/sdk/src/types/RelayChain.ts b/packages/sdk/src/types/RelayChain.ts index b7d6a73f..47ba8644 100644 --- a/packages/sdk/src/types/RelayChain.ts +++ b/packages/sdk/src/types/RelayChain.ts @@ -23,6 +23,7 @@ export type RelayChain = { symbol?: string name?: string decimals?: number + supportsBridging?: boolean } depositEnabled?: boolean blockProductionLagging?: boolean diff --git a/packages/sdk/src/types/api.ts b/packages/sdk/src/types/api.ts index 0b4e79dd..c3b44894 100644 --- a/packages/sdk/src/types/api.ts +++ b/packages/sdk/src/types/api.ts @@ -28,12 +28,25 @@ export interface paths { explorerName?: string; /** @description If the network supports depositing to this chain, e.g. allows this chain to be set as the destination chain */ depositEnabled?: boolean; + /** @description If relaying to and from this chain is disabled */ + disabled?: boolean; + /** @description The value limit at which the chain is partially disabled, if 0, the chain is not partially disabled. i.e, 1000000000000000000 to designate 1 ETH max withdrawal/deposit */ + partialDisableLimit?: number; + /** @description If the chain is experiencing issues where blocks are lagging behind or not being produced */ + blockProductionLagging?: boolean; currency?: { id?: string; symbol?: string; name?: string; decimals?: number; + supportsBridging?: boolean; }; + /** @description The fee in bps for withdrawing from this chain */ + withdrawalFee?: number; + /** @description The fee in bps for depositing to this chain */ + depositFee?: number; + /** @description If the chain has surge pricing enabled */ + surgeEnabled?: boolean; /** @description An array of erc20 currencies that the chain supports */ erc20Currencies?: { id?: string; @@ -41,8 +54,16 @@ export interface paths { name?: string; address?: string; decimals?: number; + /** @description If the currency supports bridging */ + supportsBridging?: boolean; /** @description If the erc20 currency supports permit via signature (EIP-2612) */ supportsPermit?: boolean; + /** @description The fee in bps for withdrawing from this chain */ + withdrawalFee?: number; + /** @description The fee in bps for depositing to this chain */ + depositFee?: number; + /** @description If the chain has surge pricing enabled */ + surgeEnabled?: boolean; }[]; }[]; }; @@ -51,241 +72,6 @@ export interface paths { }; }; }; - "/admin/chains": { - get: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - chains?: ({ - id?: number; - name?: string; - httpRpcUrl?: string; - wsRpcUrl?: string | null; - targetBalance?: string; - capacityPerRequest?: string; - feeBpsPrice?: string | null; - stack?: string | null; - httpRpcUrlPublic?: string | null; - wsRpcUrlPublic?: string | null; - explorerUrl?: string | null; - explorerName?: string | null; - displayName?: string | null; - depositAddress?: string | null; - baseChainId?: number | null; - enabled?: boolean; - rebalancePercentage?: string | null; - bufferPercentage?: string | null; - private?: boolean | null; - metadata?: { - [key: string]: unknown; - } | null; - })[]; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - }; - }; - }; - "/chains/add": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody: { - content: { - "application/json": { - id: number; - name: string; - httpRpcUrl: string; - wsRpcUrl: string; - targetBalance: string; - capacityPerRequest: string; - partialCapacityPerRequestAmount: string; - feeBpsPrice?: string; - stack?: string; - httpRpcUrlPublic: string; - wsRpcUrlPublic: string; - explorerUrl: string; - explorerName: string; - displayName: string; - depositAddress?: string; - baseChainId: number; - rebalancePercentage?: string; - bufferPercentage?: string; - private?: boolean; - active?: boolean; - metadata?: Record; - /** @enum {string} */ - nativeCurrency: "avax" | "degen" | "bnb" | "matic" | "eth" | "usdc" | "xai" | "dai" | "sipher"; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 400: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - /** @description Default Response */ - 500: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - }; - }; - }; - "/chains/update": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody: { - content: { - "application/json": { - id: number; - name?: string; - httpRpcUrl?: string; - wsRpcUrl?: string; - targetBalance?: string; - capacityPerRequest?: string; - partialCapacityPerRequestAmount?: string; - feeBpsPrice?: string; - stack?: string; - httpRpcUrlPublic?: string; - wsRpcUrlPublic?: string; - explorerUrl?: string; - explorerName?: string; - displayName?: string; - depositAddress?: string; - baseChainId?: number; - rebalancePercentage?: string; - bufferPercentage?: string; - private?: boolean; - active?: boolean; - metadata?: Record; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 400: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - /** @description Default Response */ - 500: { - content: { - "application/json": { - message?: string; - code?: string; - }; - }; - }; - }; - }; - }; - "/chains/status": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody?: { - content: { - "application/json": { - chainId?: number; - enabled?: boolean; - partialDisable?: boolean; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; "/config": { get: { parameters: { @@ -334,7 +120,7 @@ export interface paths { /** @description User address, when supplied returns user balance and max bridge amount */ user?: string; /** @description Restricts the user balance and capacity to a particular currency when supplied with a currency id. Defaults to the native currency of the destination chain. */ - currency?: "degen" | "eth" | "usdc" | "xai" | "sipher"; + currency?: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; }; }; responses: { @@ -349,6 +135,8 @@ export interface paths { /** @description Maximum amount that the user can bridge after fees in the native or supplied currency */ maxBridgeAmount?: string; }; + /** @description Total fee in the native or supplied currency for the bridge operation */ + fee?: string; solver?: { address?: string; /** @description Balance of the solver on the destination chain. Denoted in wei */ @@ -385,7 +173,7 @@ export interface paths { originChainId: number; destinationChainId: number; /** @enum {string} */ - currency: "degen" | "eth" | "usdc" | "xai" | "sipher"; + currency: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; /** @description Amount to bridge as the base amount (can be switched to exact input using the dedicated flag), denoted in wei */ amount: string; /** @description App fees to be charged for execution */ @@ -484,7 +272,7 @@ export interface paths { * @description Origin chain gas currency * @enum {string} */ - gasCurrency?: "avax" | "degen" | "bnb" | "matic" | "eth" | "usdc" | "xai" | "dai" | "sipher"; + gasCurrency?: "avax" | "degen" | "bnb" | "matic" | "eth" | "usdc" | "xai" | "dai" | "sipher" | "sol" | "pop"; /** @description Combination of the relayerGas and relayerService to give you the full relayer fee in wei */ relayer?: string; /** @description Destination chain gas fee in wei */ @@ -495,10 +283,10 @@ export interface paths { * @description The currency for all relayer fees (gas and service) * @enum {string} */ - relayerCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher"; + relayerCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; app?: string; /** @enum {string} */ - appCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher"; + appCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; }; breakdown?: { /** @description Amount that will be bridged in the estimated time */ @@ -560,7 +348,7 @@ export interface paths { originChainId: number; destinationChainId: number; /** @enum {string} */ - currency: "degen" | "eth" | "usdc" | "xai" | "sipher"; + currency: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; /** @description Amount to bridge as the base amount (can be switched to exact input using the dedicated flag), denoted in wei */ amount: string; /** @description App fees to be charged for execution */ @@ -984,7 +772,7 @@ export interface paths { * @description Origin chain gas currency * @enum {string} */ - gasCurrency?: "avax" | "degen" | "bnb" | "matic" | "eth" | "usdc" | "xai" | "dai" | "sipher"; + gasCurrency?: "avax" | "degen" | "bnb" | "matic" | "eth" | "usdc" | "xai" | "dai" | "sipher" | "sol" | "pop"; /** @description Combination of the relayerGas and relayerService to give you the full relayer fee in wei */ relayer?: string; /** @description Destination chain gas fee in wei */ @@ -995,10 +783,10 @@ export interface paths { * @description The currency for all relayer fees (gas and service) * @enum {string} */ - relayerCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher"; + relayerCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; app?: string; /** @enum {string} */ - appCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher"; + appCurrency?: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; }; /** * @example { @@ -1405,62 +1193,6 @@ export interface paths { }; }; }; - "/execute/permits": { - post: { - parameters: { - query: { - signature: string; - }; - }; - requestBody: { - content: { - "application/json": { - kind: string; - requestId: string; - /** @enum {string} */ - api?: "bridge" | "swap" | "user-swap"; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - steps?: { - id?: string; - action?: string; - description?: string; - kind?: string; - items?: { - status?: string; - data?: { - to?: string; - data?: string; - value?: string; - chainId?: number; - }; - check?: { - endpoint?: string; - method?: string; - }; - }[]; - }[]; - }; - }; - }; - /** @description Default Response */ - 400: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; "/execute/swap": { post: { requestBody: { @@ -1772,6 +1504,12 @@ export interface paths { }; /** @description A summary of the swap and what the user should expect to happen given an input */ details?: { + /** @description The operation that will be performed, possible options are send, swap, wrap, unwrap, bridge */ + operation?: string; + /** @description Estimated swap time in seconds */ + timeEstimate?: number; + /** @description The user's balance in the given currency on the origin chain */ + userBalance?: string; /** @description The address that deposited the funds */ sender?: string; /** @description The address that will be receiving the swap output */ @@ -1905,11 +1643,48 @@ export interface paths { }; }; }; - "/lives": { - get: { + "/execute/magic-spend/deposit": { + post: { + requestBody: { + content: { + "application/json": { + user: string; + originChainId: number; + /** @enum {string} */ + currencyId: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; + amount: string; + useForwarder?: boolean; + }; + }; + }; responses: { /** @description Default Response */ 200: { + content: { + "application/json": { + steps: { + id: string; + action: string; + description: string; + kind?: string; + items: { + status: string; + data: unknown; + }[]; + }[]; + }; + }; + }; + /** @description Default Response */ + 400: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 500: { content: { "application/json": { message?: string; @@ -1919,9 +1694,963 @@ export interface paths { }; }; }; - "/intents/status": { - get: { - parameters: { + "/execute/magic-spend/request": { + post: { + requestBody: { + content: { + "application/json": { + /** @enum {string} */ + version?: "v0.6"; + chainId: number; + userOp: { + sender: string; + nonce: string; + initCode: string; + callData: string; + callGasLimit: number; + verificationGasLimit: number; + preVerificationGas: number; + maxFeePerGas: number; + maxPriorityFeePerGas: number; + paymasterAndData: string; + signature: string; + }; + assets: ({ + /** @enum {string} */ + currencyId: "degen" | "eth" | "usdc" | "xai" | "sipher" | "pop"; + amount: string; + })[]; + }; + }; + }; + responses: { + /** @description Default Response */ + 200: { + content: { + "application/json": { + withdrawRequests: { + signature: string; + asset: string; + amount: string; + nonce: string; + expiry: number; + }[]; + }; + }; + }; + /** @description Default Response */ + 400: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 500: { + content: { + "application/json": { + message?: string; + }; + }; + }; + }; + }; + }; + "/execute/permits": { + post: { + parameters: { + query: { + signature: string; + }; + }; + requestBody: { + content: { + "application/json": { + kind: string; + requestId: string; + /** @enum {string} */ + api?: "bridge" | "swap" | "user-swap"; + }; + }; + }; + responses: { + /** @description Default Response */ + 200: { + content: { + "application/json": { + message?: string; + steps?: { + id?: string; + action?: string; + description?: string; + kind?: string; + items?: { + status?: string; + data?: { + to?: string; + data?: string; + value?: string; + chainId?: number; + }; + check?: { + endpoint?: string; + method?: string; + }; + }[]; + }[]; + }; + }; + }; + /** @description Default Response */ + 400: { + content: { + "application/json": { + message?: string; + }; + }; + }; + }; + }; + }; + "/quote": { + post: { + requestBody: { + content: { + "application/json": { + /** @description Address that is depositing funds on the origin chain and submitting transactions or signatures */ + user: string; + /** @description Address that is receiving the funds on the destination chain, if not specified then this will default to the user address */ + recipient?: string; + originChainId: number; + destinationChainId: number; + originCurrency: string; + destinationCurrency: string; + /** @description Amount to swap as the base amount (can be switched to exact input/output using the dedicated flag), denoted in the smallest unit of the specified currency (e.g., wei for ETH) */ + amount: string; + /** + * @description Whether to use the amount as the output or the input for the basis of the swap + * @enum {string} + */ + tradeType: "EXACT_INPUT" | "EXACT_OUTPUT"; + txs?: { + to?: string; + value?: string; + data?: string; + }[]; + referrer?: string; + /** @description Address to send the refund to in the case of failure, if not specified then the recipient address or user address is used */ + refundTo?: string; + /** @description Always refund on the origin chain in case of any issues */ + refundOnOrigin?: boolean; + /** + * @description Enable this to route payments via a receiver contract. This contract will emit an event when receiving payments before forwarding to the solver. This is needed when depositing from a smart contract as the payment will be an internal transaction and detecting such a transaction requires obtaining the transaction traces. + * @default true + */ + useReceiver?: boolean; + /** @description Enable this to use canonical+ bridging, trading speed for more liquidity */ + useExternalLiquidity?: boolean; + /** @description Enable this to use permit (eip3009) when bridging, only works on supported currency such as usdc */ + usePermit?: boolean; + /** @description Slippage tolerance for the swap, if not specified then the slippage tolerance is automatically calculated to avoid front-running. This value is in basis points (1/100th of a percent), e.g. 50 for 0.5% slippage */ + slippageTolerance?: string; + appFees?: { + /** @description Address that will receive the app fee, if not specified then the user address is used */ + recipient?: string; + /** @description App fees to be charged for execution in basis points, e.g. 100 = 1% */ + fee?: string; + }[]; + }; + }; + }; + responses: { + /** @description Default Response */ + 200: { + content: { + "application/json": { + /** + * @description An array of steps detailing what needs to be done to bridge, steps includes multiple items of the same kind (signature, transaction, etc) + * @example [ + * { + * "id": "deposit", + * "action": "Confirm transaction in your wallet", + * "description": "Depositing funds to the relayer to execute the swap for USDC", + * "kind": "transaction", + * "requestId": "0x92b99e6e1ee1deeb9531b5ad7f87091b3d71254b3176de9e8b5f6c6d0bd3a331", + * "items": [ + * { + * "status": "incomplete", + * "data": { + * "from": "0x0CccD55A5Ac261Ea29136831eeaA93bfE07f5Db6", + * "to": "0xf70da97812cb96acdf810712aa562db8dfa3dbef", + * "data": "0x00fad611", + * "value": "1000000000000000000", + * "maxFeePerGas": "12205661344", + * "maxPriorityFeePerGas": "2037863396", + * "chainId": 1 + * }, + * "check": { + * "endpoint": "/intents/status?requestId=0x92b99e6e1ee1deeb9531b5ad7f87091b3d71254b3176de9e8b5f6c6d0bd3a331", + * "method": "GET" + * } + * } + * ] + * } + * ] + */ + steps?: { + /** @description Unique identifier tied to the step */ + id?: string; + /** @description A call to action for the step */ + action?: string; + /** @description A short description of the step and what it entails */ + description?: string; + /** @description The kind of step, can either be a transaction or a signature. Transaction steps require submitting a transaction while signature steps require submitting a signature */ + kind?: string; + /** @description A unique identifier for this step, tying all related transactions together */ + requestId?: string; + /** @description While uncommon it is possible for steps to contain multiple items of the same kind (transaction/signature) grouped together that can be executed simultaneously. */ + items?: { + /** @description Can either be complete or incomplete, this can be locally controlled once the step item is completed (depending on the kind) and the check object (if returned) has been verified. Once all step items are complete, the bridge is complete */ + status?: string; + data?: unknown; + /** @description Details an endpoint and a method you should poll to get confirmation, the endpoint should return a boolean success flag which can be used to determine if the step item is complete */ + check?: { + /** @description The endpoint to confirm that the step item was successfully completed */ + endpoint?: string; + /** @description The REST method to access the endpoint */ + method?: string; + }; + }[]; + }[]; + fees?: { + /** + * @description Origin chain gas fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + gas?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Combination of the relayerGas and relayerService to give you the full relayer fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayer?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Destination chain gas fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayerGas?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Fees paid to the relay solver, note that this value can be negative (which represents network rewards for moving in a direction that optimizes liquidity distribution) + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayerService?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Fees paid to the app. Currency will be the same as the relayer fee currency. This needs to be claimed later by the app owner and is not immediately distributed to the app + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + app?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + }; + /** @description A summary of the swap and what the user should expect to happen given an input */ + details?: { + /** @description The operation that will be performed, possible options are send, swap, wrap, unwrap, bridge */ + operation?: string; + /** @description The address that deposited the funds */ + sender?: string; + /** @description The address that will be receiving the swap output */ + recipient?: string; + /** + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + currencyIn?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + currencyOut?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** @description The difference between the input and output values, including fees */ + totalImpact?: { + usd?: string; + percent?: string; + }; + /** @description The impact of the swap, not factoring in fees */ + swapImpact?: { + usd?: string; + percent?: string; + }; + /** @description The swap rate which is equal to 1 input unit in the output unit, e.g. 1 USDC -> x ETH. This value can fluctuate based on gas and fees. */ + rate?: string; + slippageTolerance?: { + /** @description The slippage tolerance on the origin chain swap */ + origin?: { + usd?: string; + value?: string; + percent?: string; + }; + /** @description The slippage tolerance on the destination chain swap */ + destination?: { + usd?: string; + value?: string; + percent?: string; + }; + }; + /** @description Estimated swap time in seconds */ + timeEstimate?: number; + /** @description The user's balance in the given currency on the origin chain */ + userBalance?: string; + }; + }; + }; + }; + /** @description Default Response */ + 400: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 401: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 500: { + content: { + "application/json": { + message?: string; + }; + }; + }; + }; + }; + }; + "/price": { + post: { + requestBody: { + content: { + "application/json": { + /** @description Address that is depositing funds on the origin chain and submitting transactions or signatures */ + user: string; + /** @description Address that is receiving the funds on the destination chain, if not specified then this will default to the user address */ + recipient?: string; + originChainId: number; + destinationChainId: number; + originCurrency: string; + destinationCurrency: string; + /** @description Amount to swap as the base amount (can be switched to exact input/output using the dedicated flag), denoted in the smallest unit of the specified currency (e.g., wei for ETH) */ + amount: string; + /** + * @description Whether to use the amount as the output or the input for the basis of the swap + * @enum {string} + */ + tradeType: "EXACT_INPUT" | "EXACT_OUTPUT"; + txs?: { + to?: string; + value?: string; + data?: string; + }[]; + referrer?: string; + /** @description Address to send the refund to in the case of failure, if not specified then the recipient address or user address is used */ + refundTo?: string; + /** @description Always refund on the origin chain in case of any issues */ + refundOnOrigin?: boolean; + /** + * @description Enable this to route payments via a receiver contract. This contract will emit an event when receiving payments before forwarding to the solver. This is needed when depositing from a smart contract as the payment will be an internal transaction and detecting such a transaction requires obtaining the transaction traces. + * @default true + */ + useReceiver?: boolean; + /** @description Enable this to use canonical+ bridging, trading speed for more liquidity */ + useExternalLiquidity?: boolean; + /** @description Enable this to use permit (eip3009) when bridging, only works on supported currency such as usdc */ + usePermit?: boolean; + /** @description Slippage tolerance for the swap, if not specified then the slippage tolerance is automatically calculated to avoid front-running. This value is in basis points (1/100th of a percent), e.g. 50 for 0.5% slippage */ + slippageTolerance?: string; + appFees?: { + /** @description Address that will receive the app fee, if not specified then the user address is used */ + recipient?: string; + /** @description App fees to be charged for execution in basis points, e.g. 100 = 1% */ + fee?: string; + }[]; + }; + }; + }; + responses: { + /** @description Default Response */ + 200: { + content: { + "application/json": { + fees?: { + /** + * @description Origin chain gas fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + gas?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Combination of the relayerGas and relayerService to give you the full relayer fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayer?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Destination chain gas fee + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayerGas?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Fees paid to the relay solver, note that this value can be negative (which represents network rewards for moving in a direction that optimizes liquidity distribution) + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + relayerService?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @description Fees paid to the app. Currency will be the same as the relayer fee currency. This needs to be claimed later by the app owner and is not immediately distributed to the app + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + app?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + }; + /** @description A summary of the swap and what the user should expect to happen given an input */ + details?: { + /** @description The operation that will be performed, possible options are send, swap, wrap, unwrap, bridge */ + operation?: string; + /** @description The address that deposited the funds */ + sender?: string; + /** @description The address that will be receiving the swap output */ + recipient?: string; + /** + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + currencyIn?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** + * @example { + * "currency": { + * "chainId": 8453, + * "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + * "symbol": "USDC", + * "name": "USD Coin", + * "decimals": 6, + * "metadata": { + * "logoURI": "https://ethereum-optimism.github.io/data/USDC/logo.png", + * "verified": false, + * "isNative": false + * } + * }, + * "amount": "30754920", + * "amountFormatted": "30.75492", + * "amountUsd": "30.901612" + * } + */ + currencyOut?: { + currency?: { + chainId?: number; + address?: string; + symbol?: string; + name?: string; + decimals?: number; + metadata?: { + logoURI?: string; + verified?: boolean; + isNative?: boolean; + }; + }; + amount?: string; + amountFormatted?: string; + amountUsd?: string; + }; + /** @description The difference between the input and output values, including fees */ + totalImpact?: { + usd?: string; + percent?: string; + }; + /** @description The impact of the swap, not factoring in fees */ + swapImpact?: { + usd?: string; + percent?: string; + }; + /** @description The swap rate which is equal to 1 input unit in the output unit, e.g. 1 USDC -> x ETH. This value can fluctuate based on gas and fees. */ + rate?: string; + slippageTolerance?: { + /** @description The slippage tolerance on the origin chain swap */ + origin?: { + usd?: string; + value?: string; + percent?: string; + }; + /** @description The slippage tolerance on the destination chain swap */ + destination?: { + usd?: string; + value?: string; + percent?: string; + }; + }; + /** @description Estimated swap time in seconds */ + timeEstimate?: number; + /** @description The user's balance in the given currency on the origin chain */ + userBalance?: string; + }; + }; + }; + }; + /** @description Default Response */ + 400: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 401: { + content: { + "application/json": { + message?: string; + }; + }; + }; + /** @description Default Response */ + 500: { + content: { + "application/json": { + message?: string; + }; + }; + }; + }; + }; + }; + "/lives": { + get: { + responses: { + /** @description Default Response */ + 200: { + content: { + "application/json": { + message?: string; + }; + }; + }; + }; + }; + }; + "/intents/status": { + get: { + parameters: { query?: { /** @description A unique id representing the execution in the Relay system. You can obtain this id from the requests api or the check object within the step items. */ requestId?: string; @@ -2004,6 +2733,8 @@ export interface paths { destinationChainId?: string; privateChainsToInclude?: string; id?: string; + startTimestamp?: number; + endTimestamp?: number; }; }; responses: { @@ -2092,12 +2823,7 @@ export interface paths { inTxs?: { /** @description Total fees in wei */ fee?: string; - data?: { - to?: string; - data?: string; - from?: string; - value?: string; - }; + data?: unknown; hash?: string; /** @description The type of transaction, always set to onchain */ type?: string; @@ -2105,6 +2831,7 @@ export interface paths { timestamp?: number; }[]; currency?: string; + feeCurrency?: string; appFees?: { recipient?: string; amount?: string; @@ -2192,12 +2919,7 @@ export interface paths { outTxs?: { /** @description Total fees in wei */ fee?: string; - data?: { - to?: string; - data?: string; - from?: string; - value?: string; - }; + data?: unknown; hash?: string; /** @description The type of transaction, always set to onchain */ type?: string; @@ -2265,68 +2987,6 @@ export interface paths { }; }; }; - "/admin/rebalance": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; - "/admin/tx-confirmation-delay": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody?: { - content: { - "application/json": { - delay?: number; - threshold?: number; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; "/conduit/install": { post: { parameters: { @@ -2409,6 +3069,7 @@ export interface paths { USDC?: number; XAI?: number; SIPHER?: number; + TG7?: number; }; }; }; @@ -2465,7 +3126,9 @@ export interface paths { get: { parameters: { query: { + /** @description Token address to get price for */ address: string; + /** @description Chain ID of the token */ chainId: number; }; }; @@ -2474,6 +3137,7 @@ export interface paths { 200: { content: { "application/json": { + /** @description Token price in USD */ price?: number; }; }; @@ -2482,6 +3146,7 @@ export interface paths { 400: { content: { "application/json": { + /** @description Error message */ error?: string; }; }; @@ -2489,122 +3154,6 @@ export interface paths { }; }; }; - "/currencies/add": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody: { - content: { - "application/json": { - chainId: number; - address: string; - symbol: string; - name: string; - decimals: number; - logoURI: string; - verified: boolean; - isNative: boolean; - groupID: string; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 400: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 500: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; - "/currencies/update": { - post: { - parameters: { - header: { - "x-admin-api-key": string; - }; - }; - requestBody: { - content: { - "application/json": { - chainId: number; - address: string; - symbol?: string; - name?: string; - decimals?: number; - logoURI?: string; - verified?: boolean; - isNative?: boolean; - groupID?: string; - }; - }; - }; - responses: { - /** @description Default Response */ - 200: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 400: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 401: { - content: { - "application/json": { - message?: string; - }; - }; - }; - /** @description Default Response */ - 500: { - content: { - "application/json": { - message?: string; - }; - }; - }; - }; - }; - }; } export type webhooks = Record; diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 3b150644..7892a2b3 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,5 +1,77 @@ # @reservoir0x/relay-kit-ui +## 1.1.16 + +### Patch Changes + +- Updated dependencies [ec9b20f] + - @reservoir0x/relay-sdk@1.0.12 + - @reservoir0x/relay-kit-hooks@1.0.18 + +## 1.1.15 + +### Patch Changes + +- Updated dependencies [92b9c71] + - @reservoir0x/relay-sdk@1.0.11 + - @reservoir0x/relay-kit-hooks@1.0.17 + +## 1.1.14 + +### Patch Changes + +- fac3415: Fix chain widget testnet configuration +- Updated dependencies [fac3415] + - @reservoir0x/relay-kit-hooks@1.0.16 + +## 1.1.13 + +### Patch Changes + +- 5acf10e: Fix swap widget cta bug + +## 1.1.12 + +### Patch Changes + +- 23b528c: Remove compact formatting from review quote + +## 1.1.11 + +### Patch Changes + +- 00da309: Decouple dune balance loading from token selector + +## 1.1.10 + +### Patch Changes + +- Add review quote step, refactor modal renderers +- af694ab: Fix dollar formatting sub 0 and price impact spacing +- e231382: Color coding price impact +- Updated dependencies +- Updated dependencies [2d22eec] + - @reservoir0x/relay-kit-hooks@1.0.15 + - @reservoir0x/relay-sdk@1.0.10 + +## 1.1.9 + +### Patch Changes + +- b3432f4: Fix dropdown item hover color + +## 1.1.8 + +### Patch Changes + +- 3648ec3: Better contextualize actions based on operation +- ea3e6c5: Only show fill time on success page if sub 10s +- d5b3f82: Export TokenSelector component +- Updated dependencies [3648ec3] +- Updated dependencies [be74a6a] + - @reservoir0x/relay-sdk@1.0.9 + - @reservoir0x/relay-kit-hooks@1.0.14 + ## 1.1.7 ### Patch Changes diff --git a/packages/ui/package.json b/packages/ui/package.json index a628a6c4..e44cdc5a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@reservoir0x/relay-kit-ui", - "version": "1.1.7", + "version": "1.1.16", "type": "module", "main": "./_cjs/src/index.js", "module": "./_esm/src/index.js", diff --git a/packages/ui/src/components/common/ErrorWell.tsx b/packages/ui/src/components/common/ErrorWell.tsx index aa9348d1..b734e1e9 100644 --- a/packages/ui/src/components/common/ErrorWell.tsx +++ b/packages/ui/src/components/common/ErrorWell.tsx @@ -1,12 +1,16 @@ import * as React from 'react' import { Text } from '../primitives/index.js' +import type { AxiosError } from 'axios' interface Props { - error?: Error | null + error?: Error | null | AxiosError } const ErrorWell: React.FC = ({ error }) => { const renderedErrorMessage = React.useMemo((): React.ReactNode => { + if (error && ((error as AxiosError).response?.data as any)?.message) { + return (error as any).response?.data?.message + } if ( error?.message?.includes('An internal error was received.') || !error?.message diff --git a/packages/ui/src/components/common/TokenSelector.tsx b/packages/ui/src/components/common/TokenSelector.tsx index c3b18190..5d848140 100644 --- a/packages/ui/src/components/common/TokenSelector.tsx +++ b/packages/ui/src/components/common/TokenSelector.tsx @@ -5,6 +5,7 @@ import { ChainIcon, Flex, Input, + Skeleton, Text } from '../primitives/index.js' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' @@ -13,7 +14,6 @@ import { faChevronLeft } from '@fortawesome/free-solid-svg-icons/faChevronLeft' import { faMagnifyingGlass } from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass' import { Modal } from '../common/Modal.js' import type { Token } from '../../types/index.js' -import { ChainTokenIcon } from '../primitives/ChainTokenIcon.js' import Fuse from 'fuse.js' import ChainFilter, { type ChainFilterValue } from '../common/ChainFilter.js' import useRelayClient from '../../hooks/useRelayClient.js' @@ -264,8 +264,7 @@ const TokenSelector: FC = ({ tokenBalances ]) - const isLoading = - isLoadingDuneBalances || isLoadingSuggestedTokens || isLoadingTokenList + const isLoading = isLoadingSuggestedTokens || isLoadingTokenList // eslint-disable-next-line react-hooks/exhaustive-deps const chainFuse = new Fuse( @@ -531,6 +530,7 @@ const TokenSelector: FC = ({ currencyList={list as EnhancedCurrencyList} setCurrencyList={setCurrencyList} selectToken={selectToken} + isLoadingDuneBalances={isLoadingDuneBalances} key={idx} /> ) : null @@ -679,12 +679,14 @@ type CurrencyRowProps = { currencyList: EnhancedCurrencyList setCurrencyList: (currencyList: EnhancedCurrencyList) => void selectToken: (currency: Currency, chainId?: number) => void + isLoadingDuneBalances: boolean } const CurrencyRow: FC = ({ currencyList, setCurrencyList, - selectToken + selectToken, + isLoadingDuneBalances }) => { const balance = currencyList.totalBalance const decimals = @@ -761,6 +763,9 @@ const CurrencyRow: FC = ({ ) : null} + {isLoadingDuneBalances && !balance ? ( + + ) : null} {balance ? ( {formatBN(balance, 5, decimals, compactBalance)} diff --git a/packages/ui/src/components/common/TransactionModal/SwapModal.tsx b/packages/ui/src/components/common/TransactionModal/SwapModal.tsx index d10449be..d65278f2 100644 --- a/packages/ui/src/components/common/TransactionModal/SwapModal.tsx +++ b/packages/ui/src/components/common/TransactionModal/SwapModal.tsx @@ -12,22 +12,30 @@ import { ErrorStep } from './steps/ErrorStep.js' import { ValidatingStep } from './steps/ValidatingStep.js' import { EventNames } from '../../../constants/events.js' import { SwapConfirmationStep } from './steps/SwapConfirmationStep.js' +import { ReviewQuoteStep } from './steps/ReviewQuoteStep.js' import { type Token } from '../../../types/index.js' import { SwapSuccessStep } from './steps/SwapSuccessStep.js' import { formatBN } from '../../../utils/numbers.js' +import type { TradeType } from '../../../components/widgets/SwapWidgetRenderer.js' import { extractQuoteId } from '../../../utils/quote.js' type SwapModalProps = { open: boolean fromToken?: Token toToken?: Token - fees?: CallFees address?: Address - steps?: Execute['steps'] | null - details?: Execute['details'] | null - error?: Error | null timeEstimate?: { time: number; formattedTime: string } isCanonical?: boolean + debouncedOutputAmountValue: string + debouncedInputAmountValue: string + amountInputValue: string + amountOutputValue: string + toDisplayName?: string + recipient?: Address + customToAddress?: Address + tradeType: TradeType + useExternalLiquidity: boolean + invalidateBalanceQueries: () => void onAnalyticEvent?: (eventName: string, data?: any) => void onOpenChange: (open: boolean) => void onSuccess?: (data: Execute) => void @@ -35,29 +43,47 @@ type SwapModalProps = { export const SwapModal: FC = (swapModalProps) => { const { - steps, - fees, - error, + open, address, - details, fromToken, toToken, + tradeType, + recipient, + debouncedInputAmountValue, + debouncedOutputAmountValue, + amountInputValue, + amountOutputValue, + useExternalLiquidity, timeEstimate, isCanonical, + invalidateBalanceQueries, onAnalyticEvent, onSuccess } = swapModalProps return ( { + recipient={recipient} + invalidateBalanceQueries={invalidateBalanceQueries} + onValidating={(quote) => { + const steps = quote?.steps onAnalyticEvent?.(EventNames.TRANSACTION_VALIDATING, { quote_id: steps ? extractQuoteId(steps) : undefined }) }} - onSuccess={() => { + onSuccess={(quote, steps) => { + const details = quote?.details + const fees = quote?.fees + const extraData: { gas_fee?: number relayer_fee?: number @@ -97,20 +123,16 @@ export const SwapModal: FC = (swapModalProps) => { .flat() }) onSuccess?.({ - steps: swapModalProps.steps as Execute['steps'], - fees: swapModalProps.fees, - details: swapModalProps.details as Execute['details'] + steps: steps, + fees: fees, + details: details }) }} > {(rendererProps) => { return ( = ({ onOpenChange, fromToken, toToken, - details, + quote, + isFetchingQuote, + isRefetchingQuote, + quoteError, + swap, + swapError, + setSwapError, address, - error, progressStep, setProgressStep, + setSteps, currentStep, setCurrentStep, currentStepItem, @@ -147,55 +175,77 @@ const InnerSwapModal: FC = ({ setStartTimestamp, onAnalyticEvent, timeEstimate, - isCanonical + isCanonical, + feeBreakdown, + quoteUpdatedAt }) => { useEffect(() => { if (!open) { if (currentStep) { onAnalyticEvent?.(EventNames.SWAP_MODAL_CLOSED) } - setProgressStep(TransactionProgressStep.WalletConfirmation) setCurrentStep(null) setCurrentStepItem(null) setAllTxHashes([]) setStartTimestamp(0) + setSwapError(null) } else { + setSteps(null) + setProgressStep(TransactionProgressStep.ReviewQuote) onAnalyticEvent?.(EventNames.SWAP_MODAL_OPEN) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]) + const details = quote?.details + const fromAmountFormatted = details?.currencyIn?.amount - ? formatBN(details?.currencyIn?.amount, 6, fromToken?.decimals) + ? formatBN(details?.currencyIn?.amount, 6, fromToken?.decimals, false) : '' const toAmountFormatted = details?.currencyOut?.amount - ? formatBN(details?.currencyOut.amount, 6, toToken?.decimals) + ? formatBN(details?.currencyOut.amount, 6, toToken?.decimals, false) : '' + const isReviewQuoteStep = progressStep === TransactionProgressStep.ReviewQuote + return ( - Swap Details + {isReviewQuoteStep ? 'Review Quote' : 'Swap Details'} + {progressStep === TransactionProgressStep.ReviewQuote ? ( + + ) : null} + {progressStep === TransactionProgressStep.WalletConfirmation ? ( = ({ onOpenChange={onOpenChange} timeEstimate={timeEstimate?.formattedTime} isCanonical={isCanonical} + details={details} /> ) : null} {progressStep === TransactionProgressStep.Error ? ( > + quote: ReturnType['data'] + isFetchingQuote: boolean + isRefetchingQuote: boolean + swap: () => void + quoteError: Error | null + swapError: Error | null + setSwapError: Dispatch> + steps: Execute['steps'] | null + setSteps: Dispatch> + waitingForSteps: boolean allTxHashes: TxHashes setAllTxHashes: Dispatch> transaction: ReturnType['data']['0'] @@ -49,39 +70,282 @@ export type ChildrenProps = { startTimestamp: number setStartTimestamp: Dispatch> requestId: string | null + quoteUpdatedAt: number + feeBreakdown: { + breakdown: BridgeFee[] + totalFees: { + usd?: string + priceImpactPercentage?: string + priceImpact?: string + swapImpact?: string + } + } | null } type Props = { - children: (props: ChildrenProps) => ReactNode + open: boolean address?: Address - steps?: Execute['steps'] | null - error?: Error | null - onSuccess?: () => void - onValidating?: () => void + fromToken?: Token + toToken?: Token + debouncedOutputAmountValue: string + debouncedInputAmountValue: string + amountInputValue: string + amountOutputValue: string + toDisplayName?: string + recipient?: Address + customToAddress?: Address + tradeType: TradeType + useExternalLiquidity: boolean + invalidateBalanceQueries: () => void + children: (props: ChildrenProps) => ReactNode + onSuccess?: ( + quote: ReturnType['data'], + steps: Execute['steps'] + ) => void + onAnalyticEvent?: (eventName: string, data?: any) => void + onSwapError?: (error: string, data?: Execute) => void + onValidating?: (quote: Execute) => void } export const TransactionModalRenderer: FC = ({ - children, + open, address, - steps, - error, + fromToken, + toToken, + debouncedInputAmountValue, + debouncedOutputAmountValue, + amountInputValue, + amountOutputValue, + toDisplayName, + recipient, + customToAddress, + tradeType, + useExternalLiquidity, + invalidateBalanceQueries, + children, onSuccess, + onAnalyticEvent, + onSwapError, onValidating }) => { + const [steps, setSteps] = useState(null) + const [progressStep, setProgressStep] = useState( - TransactionProgressStep.WalletConfirmation + TransactionProgressStep.ReviewQuote ) const [currentStep, setCurrentStep] = useState< - null | NonNullable['0'] + null | NonNullable['0'] >() const [currentStepItem, setCurrentStepItem] = useState< - null | NonNullable['0']['items']>['0'] + null | NonNullable['0']['items']>['0'] >() const [allTxHashes, setAllTxHashes] = useState([]) const [startTimestamp, setStartTimestamp] = useState(0) + const [waitingForSteps, setWaitingForSteps] = useState(false) + + const [swapError, setSwapError] = useState(null) + + const relayClient = useRelayClient() + const providerOptionsContext = useContext(ProviderOptionsContext) + const wagmiConfig = useConfig() + const walletClient = useWalletClient() + const { chainId: activeWalletChainId, connector } = useAccount() + + const { + data: quote, + isLoading: isFetchingQuote, + isRefetching: isRefetchingQuote, + executeQuote: executeSwap, + error: quoteError, + dataUpdatedAt: quoteUpdatedAt + } = useQuote( + relayClient ? relayClient : undefined, + walletClient.data, + fromToken && toToken + ? { + user: address ?? deadAddress, + originChainId: fromToken.chainId, + destinationChainId: toToken.chainId, + originCurrency: fromToken.address, + destinationCurrency: toToken.address, + recipient: recipient as string, + tradeType, + appFees: providerOptionsContext.appFees, + amount: + tradeType === 'EXACT_INPUT' + ? parseUnits( + debouncedInputAmountValue, + fromToken.decimals + ).toString() + : parseUnits( + debouncedOutputAmountValue, + toToken.decimals + ).toString(), + source: relayClient?.source ?? undefined, + useExternalLiquidity + } + : undefined, + () => {}, + ({ steps, details }) => { + onAnalyticEvent?.(EventNames.SWAP_EXECUTE_QUOTE_RECEIVED, { + wallet_connector: connector?.name, + quote_id: steps ? extractQuoteId(steps) : undefined, + amount_in: details?.currencyIn?.amountFormatted, + currency_in: details?.currencyIn?.currency?.symbol, + chain_id_in: details?.currencyIn?.currency?.chainId, + amount_out: details?.currencyOut?.amountFormatted, + currency_out: details?.currencyOut?.currency?.symbol, + chain_id_out: details?.currencyOut?.currency?.chainId + }) + }, + { + enabled: Boolean( + open && + progressStep === TransactionProgressStep.ReviewQuote && + relayClient && + ((tradeType === 'EXACT_INPUT' && + debouncedInputAmountValue && + debouncedInputAmountValue.length > 0 && + Number(debouncedInputAmountValue) !== 0) || + (tradeType === 'EXACT_OUTPUT' && + debouncedOutputAmountValue && + debouncedOutputAmountValue.length > 0 && + Number(debouncedOutputAmountValue) !== 0)) && + fromToken !== undefined && + toToken !== undefined + ), + refetchInterval: + open && + progressStep === TransactionProgressStep.ReviewQuote && + debouncedInputAmountValue === amountInputValue && + debouncedOutputAmountValue === amountOutputValue + ? 30000 + : undefined + } + ) + + const swap = useCallback(async () => { + try { + onAnalyticEvent?.(EventNames.SWAP_CTA_CLICKED) + setWaitingForSteps(true) + + if (!executeSwap) { + throw 'Missing a quote' + } + + if (fromToken && fromToken?.chainId !== activeWalletChainId) { + onAnalyticEvent?.(EventNames.SWAP_SWITCH_NETWORK) + await switchChain(wagmiConfig, { + chainId: fromToken.chainId + }) + } + + setProgressStep(TransactionProgressStep.WalletConfirmation) + + executeSwap(({ steps: currentSteps, details }) => { + setSteps(currentSteps) + }) + ?.catch((error: any) => { + if ( + error && + ((typeof error.message === 'string' && + error.message.includes('rejected')) || + (typeof error === 'string' && error.includes('rejected'))) + ) { + onAnalyticEvent?.(EventNames.USER_REJECTED_WALLET) + setProgressStep(TransactionProgressStep.ReviewQuote) + return + } + + const errorMessage = error?.response?.data?.message + ? new Error(error?.response?.data?.message) + : error + + onAnalyticEvent?.(EventNames.SWAP_ERROR, { + error_message: errorMessage, + wallet_connector: connector?.name, + quote_id: steps ? extractQuoteId(steps) : undefined, + amount_in: parseFloat(`${debouncedInputAmountValue}`), + currency_in: fromToken?.symbol, + chain_id_in: fromToken?.chainId, + amount_out: parseFloat(`${debouncedOutputAmountValue}`), + currency_out: toToken?.symbol, + chain_id_out: toToken?.chainId, + is_canonical: useExternalLiquidity, + txHashes: steps + ?.map((step) => { + let txHashes: { chainId: number; txHash: Address }[] = [] + step.items?.forEach((item) => { + if (item.txHashes) { + txHashes = txHashes.concat([ + ...(item.txHashes ?? []), + ...(item.internalTxHashes ?? []) + ]) + } + }) + return txHashes + }) + .flat() + }) + setSwapError(errorMessage) + onSwapError?.(errorMessage, quote as Execute) + }) + .finally(() => { + setWaitingForSteps(false) + invalidateBalanceQueries() + }) + } catch (e) { + setWaitingForSteps(false) + onAnalyticEvent?.(EventNames.SWAP_ERROR, { + error_message: e, + wallet_connector: connector?.name, + quote_id: steps ? extractQuoteId(steps) : undefined, + amount_in: parseFloat(`${debouncedInputAmountValue}`), + currency_in: fromToken?.symbol, + chain_id_in: fromToken?.chainId, + amount_out: parseFloat(`${debouncedOutputAmountValue}`), + currency_out: toToken?.symbol, + chain_id_out: toToken?.chainId, + is_canonical: useExternalLiquidity, + txHashes: steps + ?.map((step) => { + let txHashes: { chainId: number; txHash: Address }[] = [] + step.items?.forEach((item) => { + if (item.txHashes) { + txHashes = txHashes.concat([ + ...(item.txHashes ?? []), + ...(item.internalTxHashes ?? []) + ]) + } + }) + return txHashes + }) + .flat() + }) + onSwapError?.(e as any, quote as Execute) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + relayClient, + activeWalletChainId, + wagmiConfig, + address, + connector, + fromToken, + toToken, + customToAddress, + recipient, + debouncedInputAmountValue, + debouncedOutputAmountValue, + tradeType, + useExternalLiquidity, + executeSwap, + setSteps, + invalidateBalanceQueries + ]) useEffect(() => { - if (error) { + if (swapError || (quoteError && !isRefetchingQuote)) { setProgressStep(TransactionProgressStep.Error) return } @@ -124,7 +388,7 @@ export const TransactionModalRenderer: FC = ({ (txHashes.length > 0 || currentStepItem?.isValidatingSignature == true) && progressStep === TransactionProgressStep.WalletConfirmation ) { - onValidating?.() + onValidating?.(quote as Execute) setProgressStep(TransactionProgressStep.Validating) setStartTimestamp(new Date().getTime()) } @@ -145,11 +409,9 @@ export const TransactionModalRenderer: FC = ({ progressStep !== TransactionProgressStep.Success ) { setProgressStep(TransactionProgressStep.Success) - onSuccess?.() + onSuccess?.(quote, steps) } - }, [steps, error]) - - const client = useRelayClient() + }, [steps, quoteError, swapError]) // Fetch Success Tx const { data: transactions } = useRequests( @@ -159,7 +421,7 @@ export const TransactionModalRenderer: FC = ({ hash: allTxHashes[0]?.txHash } : undefined, - client?.baseApiUrl, + relayClient?.baseApiUrl, { enabled: progressStep === TransactionProgressStep.Success && allTxHashes[0] @@ -174,6 +436,15 @@ export const TransactionModalRenderer: FC = ({ const requestId = useMemo(() => extractDepositRequestId(steps), [steps]) + const feeBreakdown = useMemo(() => { + const chains = relayClient?.chains + const fromChain = chains?.find((chain) => chain.id === fromToken?.chainId) + const toChain = chains?.find((chain) => chain.id === toToken?.chainId) + return fromToken && toToken && fromChain && toChain && quote + ? parseFees(toChain, fromChain, quote) + : null + }, [quote, fromToken, toToken, relayClient]) + return ( <> {children({ @@ -183,6 +454,16 @@ export const TransactionModalRenderer: FC = ({ setCurrentStep, currentStepItem, setCurrentStepItem, + quote, + isFetchingQuote, + isRefetchingQuote, + swap, + steps, + setSteps, + waitingForSteps, + quoteError, + swapError, + setSwapError, allTxHashes, setAllTxHashes, transaction, @@ -192,7 +473,9 @@ export const TransactionModalRenderer: FC = ({ executionTimeSeconds, startTimestamp, setStartTimestamp, - requestId + quoteUpdatedAt, + requestId, + feeBreakdown })} ) diff --git a/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx b/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx new file mode 100644 index 00000000..0d6a9510 --- /dev/null +++ b/packages/ui/src/components/common/TransactionModal/steps/ReviewQuoteStep.tsx @@ -0,0 +1,327 @@ +import { useEffect, useState, type FC } from 'react' +import { + Button, + Flex, + Text, + ChainTokenIcon, + Skeleton +} from '../../../primitives/index.js' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { type Token } from '../../../../types/index.js' +import { faArrowRight } from '@fortawesome/free-solid-svg-icons/faArrowRight' +import type { useQuote } from '@reservoir0x/relay-kit-hooks' +import { formatDollar } from '../../../../utils/numbers.js' +import { LoadingSpinner } from '../../LoadingSpinner.js' +import { truncateAddress } from '../../../../utils/truncate.js' +import { calculatePriceTimeEstimate } from '../../../../utils/quote.js' +import { + faClock, + faGasPump, + faInfoCircle, + faTriangleExclamation +} from '@fortawesome/free-solid-svg-icons' +import type { ChildrenProps } from '../TransactionModalRenderer.js' +import { useAccount } from 'wagmi' +import { PriceImpactTooltip } from '../../../widgets/PriceImpactTooltip.js' +import React from 'react' + +type ReviewQuoteProps = { + fromToken?: Token + toToken?: Token + quote?: ReturnType['data'] + isFetchingQuote: boolean + isRefetchingQuote: boolean + waitingForSteps?: boolean + swap?: () => void + quoteUpdatedAt: number + feeBreakdown: ChildrenProps['feeBreakdown'] + fromAmountFormatted: string + toAmountFormatted: string +} + +const SECONDS_TO_UPDATE = 30 + +export const ReviewQuoteStep: FC = ({ + fromToken, + toToken, + quote, + isFetchingQuote, + isRefetchingQuote, + waitingForSteps, + swap, + quoteUpdatedAt, + feeBreakdown, + fromAmountFormatted, + toAmountFormatted +}) => { + const { address } = useAccount() + const details = quote?.details + const timeEstimate = calculatePriceTimeEstimate(quote?.details) + const connectedWalletIsNotRecipient = + quote && address !== quote?.details?.recipient + + const [timeLeft, setTimeLeft] = useState(SECONDS_TO_UPDATE) + + useEffect(() => { + const updateTimer = () => { + const now = Date.now() + const nextUpdateTime = + (quoteUpdatedAt ? quoteUpdatedAt : now) + SECONDS_TO_UPDATE * 1000 + const timeLeft = Math.max(0, Math.floor((nextUpdateTime - now) / 1000)) + setTimeLeft(timeLeft) + } + + // Initial update + updateTimer() + + // Set interval for subsequent updates + const interval = setInterval(updateTimer, 1000) + + return () => clearInterval(interval) + }, [quoteUpdatedAt]) + + const breakdown = [ + { + title: 'To address', + value: ( + + {truncateAddress(quote?.details?.recipient)} + + ) + }, + { + title: 'Estimated time', + value: ( + + + ~ {timeEstimate?.formattedTime} + + ) + }, + { + title: 'Network cost', + value: ( + + + + {formatDollar(Number(quote?.fees?.gas?.amountUsd ?? 0))} + + + ) + }, + { + title: 'Price Impact', + value: ( + + { +
+ + + {feeBreakdown?.totalFees?.priceImpactPercentage} + + + +
+ } +
+ ) + } + ] + + return ( + <> + + + + {isFetchingQuote ? ( + + ) : ( + + {fromAmountFormatted} {fromToken?.symbol} + + )} + {isFetchingQuote ? ( + + ) : details?.currencyIn?.amountUsd && + Number(details?.currencyIn?.amountUsd) > 0 ? ( + + {formatDollar(Number(details?.currencyIn?.amountUsd))} + + ) : null} + + + + + + {isFetchingQuote ? ( + + ) : ( + + {toAmountFormatted} {toToken?.symbol} + + )} + {isFetchingQuote ? ( + + ) : details?.currencyOut?.amountUsd && + Number(details?.currencyOut?.amountUsd) > 0 ? ( + + {formatDollar(Number(details?.currencyOut?.amountUsd))} + + ) : null} + + + + {breakdown.map((item) => ( + + + + {item.title} + + {isFetchingQuote ? ( + + ) : ( + item.value + )} + + {item.title === 'To address' && connectedWalletIsNotRecipient ? ( + + + + This isn't the connected wallet address. Please verify that + the recipient is correct.{' '} + + + ) : null} + + ))} + + {isFetchingQuote || isRefetchingQuote ? ( + + {isFetchingQuote ? 'Fetching' : 'Refreshing'} Quote + + ) : ( + + Quote expires in{' '} + + {timeLeft}s + + + )} + + + + ) +} diff --git a/packages/ui/src/components/common/TransactionModal/steps/SwapSuccessStep.tsx b/packages/ui/src/components/common/TransactionModal/steps/SwapSuccessStep.tsx index e5752c0a..696142d6 100644 --- a/packages/ui/src/components/common/TransactionModal/steps/SwapSuccessStep.tsx +++ b/packages/ui/src/components/common/TransactionModal/steps/SwapSuccessStep.tsx @@ -20,6 +20,7 @@ import { type Token } from '../../../../types/index.js' import type { useRequests } from '@reservoir0x/relay-kit-hooks' import { useRelayClient } from '../../../../hooks/index.js' import { faClockFour } from '@fortawesome/free-solid-svg-icons/faClockFour' +import type { Execute } from '@reservoir0x/relay-sdk' type SwapSuccessStepProps = { fromToken?: Token @@ -32,6 +33,7 @@ type SwapSuccessStepProps = { fillTime: string timeEstimate?: string isCanonical?: boolean + details?: Execute['details'] | null onOpenChange: (open: boolean) => void } @@ -46,17 +48,12 @@ export const SwapSuccessStep: FC = ({ seconds, timeEstimate, isCanonical, + details, onOpenChange }) => { const relayClient = useRelayClient() - const isWrap = - fromToken?.symbol === 'ETH' && - toToken?.symbol === 'WETH' && - fromToken.chainId === toToken.chainId - const isUnwrap = - fromToken?.symbol === 'WETH' && - toToken?.symbol === 'ETH' && - fromToken.chainId === toToken.chainId + const isWrap = details?.operation === 'wrap' + const isUnwrap = details?.operation === 'unwrap' const actionTitle = isWrap ? 'wrapped' : isUnwrap ? 'unwrapped' : 'swapped' const baseTransactionUrl = relayClient?.baseApiUrl.includes('testnets') diff --git a/packages/ui/src/components/primitives/Dialog.tsx b/packages/ui/src/components/primitives/Dialog.tsx index 2614b564..5d90289a 100644 --- a/packages/ui/src/components/primitives/Dialog.tsx +++ b/packages/ui/src/components/primitives/Dialog.tsx @@ -31,7 +31,10 @@ const Overlay = forwardRef< {children} diff --git a/packages/ui/src/components/primitives/Dropdown.tsx b/packages/ui/src/components/primitives/Dropdown.tsx index d7066aad..aaab58e7 100644 --- a/packages/ui/src/components/primitives/Dropdown.tsx +++ b/packages/ui/src/components/primitives/Dropdown.tsx @@ -52,10 +52,12 @@ const DropdownMenuItemCss = cva({ cursor: 'pointer', transition: 'backdrop-filter 250ms linear', _hover: { - backdropFilter: 'brightness(95%)' + backdropFilter: 'brightness(95%)', + backgroundColor: 'gray/10' }, '&:focus': { - backgroundColor: 'focus-color' + backdropFilter: 'brightness(95%)', + backgroundColor: 'gray/10' } } }) diff --git a/packages/ui/src/components/widgets/ChainWidget/index.tsx b/packages/ui/src/components/widgets/ChainWidget/index.tsx index 9201c276..f32a8539 100644 --- a/packages/ui/src/components/widgets/ChainWidget/index.tsx +++ b/packages/ui/src/components/widgets/ChainWidget/index.tsx @@ -1,4 +1,4 @@ -import { Flex, Button, Text, Box, ChainIcon } from '../../primitives/index.js' +import { Flex, Text, ChainIcon, Box } from '../../primitives/index.js' import { useEffect, useState, type FC } from 'react' import { useMounted, useRelayClient } from '../../../hooks/index.js' import type { Address } from 'viem' @@ -15,7 +15,6 @@ import type { Execute } from '@reservoir0x/relay-sdk' import { WidgetErrorWell } from '../WidgetErrorWell.js' import { BalanceDisplay } from '../../common/BalanceDisplay.js' import { EventNames } from '../../../constants/events.js' -import Tooltip from '../../primitives/Tooltip.js' import SwapWidgetRenderer from '../SwapWidgetRenderer.js' import WidgetContainer from '../WidgetContainer.js' import SwapButton from '../SwapButton.js' @@ -24,6 +23,7 @@ import FetchingQuoteLoader from '../FetchingQuoteLoader.js' import FeeBreakdown from '../FeeBreakdown.js' import WidgetTabs, { type WidgetTabId } from '../../widgets/WidgetTabs.js' import SwapRouteSelector from '../SwapRouteSelector.js' +import { PriceImpactTooltip } from '../PriceImpactTooltip.js' type ChainWidgetProps = { chainId: number @@ -55,11 +55,14 @@ const ChainWidget: FC = ({ onSwapError }) => { const isMounted = useMounted() + const [transactionModalOpen, setTransactionModalOpen] = useState(false) const [tabId, setTabId] = useState('deposit') const lockFromToken = tabId === 'withdraw' && (!tokens || tokens.length === 0) const lockToToken = tabId === 'deposit' && (!tokens || tokens.length === 0) const client = useRelayClient() const chain = client?.chains.find((chain) => chain.id === chainId) + const isTestnet = client?.baseApiUrl?.includes?.('testnets') + const defaultChainId = isTestnet ? 11155111 : 1 useEffect(() => { if (chainId !== defaultToken.chainId) { @@ -69,14 +72,15 @@ const ChainWidget: FC = ({ return ( = ({ context={tabId === 'deposit' ? 'Deposit' : 'Withdraw'} > {({ - quote, - steps, + price, feeBreakdown, fromToken, setFromToken, @@ -105,11 +108,9 @@ const ChainWidget: FC = ({ recipient, customToAddress, setCustomToAddress, - swap, tradeType, setTradeType, details, - waitingForSteps, isSameCurrencySameRecipientSwap, debouncedInputAmountValue, debouncedAmountInputControls, @@ -121,7 +122,7 @@ const ChainWidget: FC = ({ setAmountOutputValue, toBalance, isLoadingToBalance, - isFetchingQuote, + isFetchingPrice, isLoadingFromBalance, fromBalance, highRelayerServiceFee, @@ -134,8 +135,8 @@ const ChainWidget: FC = ({ supportsExternalLiquidity, timeEstimate, fetchingSolverConfig, + invalidateBalanceQueries, setUseExternalLiquidity, - setSteps, setDetails, setSwapError }) => { @@ -172,21 +173,26 @@ const ChainWidget: FC = ({ return ( { if (!open) { - setSteps(null) - setDetails(null) setSwapError(null) } }} useExternalLiquidity={useExternalLiquidity} + invalidateBalanceQueries={invalidateBalanceQueries} onSwapSuccess={onSwapSuccess} onAnalyticEvent={onAnalyticEvent} setCustomToAddress={setCustomToAddress} @@ -267,8 +273,8 @@ const ChainWidget: FC = ({ tradeType === 'EXACT_INPUT' ? amountInputValue : amountInputValue - ? formatFixedLength(amountInputValue, 8) - : amountInputValue + ? formatFixedLength(amountInputValue, 8) + : amountInputValue } setValue={(e) => { setAmountInputValue(e) @@ -284,12 +290,12 @@ const ChainWidget: FC = ({ css={{ textAlign: 'right', color: - isFetchingQuote && tradeType === 'EXACT_OUTPUT' + isFetchingPrice && tradeType === 'EXACT_OUTPUT' ? 'text-subtle' : 'input-color', _placeholder: { color: - isFetchingQuote && tradeType === 'EXACT_OUTPUT' + isFetchingPrice && tradeType === 'EXACT_OUTPUT' ? 'text-subtle' : 'input-color' } @@ -337,11 +343,11 @@ const ChainWidget: FC = ({ ) : null}
- {quote?.details?.currencyIn?.amountUsd && - Number(quote.details.currencyIn.amountUsd) > 0 ? ( + {price?.details?.currencyIn?.amountUsd && + Number(price.details.currencyIn.amountUsd) > 0 ? ( {formatDollar( - Number(quote.details.currencyIn.amountUsd) + Number(price.details.currencyIn.amountUsd) )} ) : null} @@ -419,8 +425,8 @@ const ChainWidget: FC = ({ tradeType === 'EXACT_OUTPUT' ? amountOutputValue : amountOutputValue - ? formatFixedLength(amountOutputValue, 8) - : amountOutputValue + ? formatFixedLength(amountOutputValue, 8) + : amountOutputValue } setValue={(e) => { setAmountOutputValue(e) @@ -436,12 +442,12 @@ const ChainWidget: FC = ({ }} css={{ color: - isFetchingQuote && tradeType === 'EXACT_INPUT' + isFetchingPrice && tradeType === 'EXACT_INPUT' ? 'gray11' : 'gray12', _placeholder: { color: - isFetchingQuote && tradeType === 'EXACT_INPUT' + isFetchingPrice && tradeType === 'EXACT_INPUT' ? 'gray11' : 'gray12' }, @@ -470,104 +476,51 @@ const ChainWidget: FC = ({ ) : ( )} - {quote?.details?.currencyOut?.amountUsd && - Number(quote.details.currencyOut.amountUsd) > 0 ? ( + {price?.details?.currencyOut?.amountUsd && + Number(price.details.currencyOut.amountUsd) > 0 ? ( {formatDollar( - Number(quote.details.currencyOut.amountUsd) + Number(price.details.currencyOut.amountUsd) )} - - - - Total Price Impact - - - {feeBreakdown?.totalFees.priceImpact} - - - ( - { - feeBreakdown?.totalFees - .priceImpactPercentage - } - ) - - - +
+ + - - - Swap Impact - - - {feeBreakdown?.totalFees.swapImpact} - - - {feeBreakdown?.breakdown.map((fee) => { - if (fee.id === 'origin-gas') { - return null - } - return ( - - - {fee.name} - - {fee.usd} - - ) - })} - - } - > -
- - + > ( { feeBreakdown?.totalFees .priceImpactPercentage } ) + + - +
- + ) : null} - + = ({ /> setTransactionModalOpen(true)} ctaCopy={ctaCopy} /> diff --git a/packages/ui/src/components/widgets/FeeBreakdown.tsx b/packages/ui/src/components/widgets/FeeBreakdown.tsx index e3344304..e29c160b 100644 --- a/packages/ui/src/components/widgets/FeeBreakdown.tsx +++ b/packages/ui/src/components/widgets/FeeBreakdown.tsx @@ -9,8 +9,8 @@ import { faClock } from '@fortawesome/free-solid-svg-icons/faClock' type Props = Pick< ChildrenProps, | 'feeBreakdown' - | 'isFetchingQuote' - | 'quote' + | 'isFetchingPrice' + | 'price' | 'toToken' | 'fromToken' | 'timeEstimate' @@ -18,20 +18,20 @@ type Props = Pick< const FeeBreakdown: FC = ({ feeBreakdown, - isFetchingQuote, - quote, + isFetchingPrice, + price, toToken, fromToken, timeEstimate }) => { - const swapRate = quote?.details?.rate + const swapRate = price?.details?.rate const originGasFee = feeBreakdown?.breakdown?.find( (fee) => fee.id === 'origin-gas' ) const compactSwapRate = Boolean(swapRate && swapRate.length > 8) const [rateMode, setRateMode] = useState<'input' | 'output'>('input') - if (!feeBreakdown || isFetchingQuote) { + if (!feeBreakdown || isFetchingPrice) { return null } diff --git a/packages/ui/src/components/widgets/FetchingQuoteLoader.tsx b/packages/ui/src/components/widgets/FetchingQuoteLoader.tsx index 9eafaeaf..2bb12923 100644 --- a/packages/ui/src/components/widgets/FetchingQuoteLoader.tsx +++ b/packages/ui/src/components/widgets/FetchingQuoteLoader.tsx @@ -12,7 +12,10 @@ const FetchingQuoteLoader: FC = ({ isLoading }) => { } return ( - + Fetching the best price diff --git a/packages/ui/src/components/widgets/PriceImpactTooltip.tsx b/packages/ui/src/components/widgets/PriceImpactTooltip.tsx new file mode 100644 index 00000000..f822ef24 --- /dev/null +++ b/packages/ui/src/components/widgets/PriceImpactTooltip.tsx @@ -0,0 +1,70 @@ +import type { FC, ReactNode } from 'react' +import { Flex, Text, Box } from '../primitives/index.js' +import Tooltip from '../primitives/Tooltip.js' +import type { ChildrenProps } from '../widgets/SwapWidgetRenderer.js' + +type PriceImpactTooltipProps = { + feeBreakdown: ChildrenProps['feeBreakdown'] + children: ReactNode + tooltipProps?: any +} + +export const PriceImpactTooltip: FC = ({ + feeBreakdown, + children, + tooltipProps +}) => { + return ( + + + + Total Price Impact{' '} + + + {feeBreakdown?.totalFees.priceImpact} + + + ({feeBreakdown?.totalFees.priceImpactPercentage}) + + + + + + Swap Impact + + {feeBreakdown?.totalFees.swapImpact} + + {feeBreakdown?.breakdown.map((fee) => { + if (fee.id === 'origin-gas') { + return null + } + return ( + + + {fee.name} + + {fee.usd} + + ) + })} + + } + {...tooltipProps} + > + {children} + + ) +} diff --git a/packages/ui/src/components/widgets/SwapButton.tsx b/packages/ui/src/components/widgets/SwapButton.tsx index 7d09ed2b..c99ad487 100644 --- a/packages/ui/src/components/widgets/SwapButton.tsx +++ b/packages/ui/src/components/widgets/SwapButton.tsx @@ -5,37 +5,35 @@ import type { ChildrenProps } from './SwapWidgetRenderer.js' import { EventNames } from '../../constants/events.js' type SwapButtonProps = { + transactionModalOpen: boolean onConnectWallet?: () => void onAnalyticEvent?: (eventName: string, data?: any) => void + onClick: () => void context: 'Swap' | 'Deposit' | 'Withdraw' } & Pick< ChildrenProps, - | 'quote' + | 'price' | 'address' | 'hasInsufficientBalance' | 'isInsufficientLiquidityError' - | 'steps' - | 'waitingForSteps' | 'debouncedInputAmountValue' | 'debouncedOutputAmountValue' | 'isSameCurrencySameRecipientSwap' - | 'swap' | 'ctaCopy' > const SwapButton: FC = ({ + transactionModalOpen, context, onConnectWallet, - quote, + price, address, hasInsufficientBalance, isInsufficientLiquidityError, - steps, - waitingForSteps, debouncedInputAmountValue, debouncedOutputAmountValue, isSameCurrencySameRecipientSwap, - swap, + onClick, ctaCopy, onAnalyticEvent }) => { @@ -46,16 +44,15 @@ const SwapButton: FC = ({ css={{ justifyContent: 'center' }} aria-label={context} disabled={ - !quote || + !price || hasInsufficientBalance || isInsufficientLiquidityError || - steps !== null || - waitingForSteps || + transactionModalOpen || Number(debouncedInputAmountValue) === 0 || Number(debouncedOutputAmountValue) === 0 || isSameCurrencySameRecipientSwap } - onClick={swap} + onClick={onClick} > {ctaCopy} diff --git a/packages/ui/src/components/widgets/SwapWidget/index.tsx b/packages/ui/src/components/widgets/SwapWidget/index.tsx index 10099c2a..8b83d207 100644 --- a/packages/ui/src/components/widgets/SwapWidget/index.tsx +++ b/packages/ui/src/components/widgets/SwapWidget/index.tsx @@ -1,5 +1,5 @@ import { Flex, Button, Text, Box, ChainIcon } from '../../primitives/index.js' -import type { FC } from 'react' +import { useState, type FC } from 'react' import { useMounted, useRelayClient } from '../../../hooks/index.js' import type { Address } from 'viem' import { formatUnits, zeroAddress } from 'viem' @@ -24,6 +24,7 @@ import TokenSelectorContainer from '../TokenSelectorContainer.js' import FetchingQuoteLoader from '../FetchingQuoteLoader.js' import FeeBreakdown from '../FeeBreakdown.js' import { mainnet } from 'viem/chains' +import { PriceImpactTooltip } from '../PriceImpactTooltip.js' type SwapWidgetProps = { defaultFromToken?: Token @@ -57,6 +58,7 @@ const SwapWidget: FC = ({ onSwapError }) => { const relayClient = useRelayClient() + const [transactionModalOpen, setTransactionModalOpen] = useState(false) const isMounted = useMounted() const hasLockedToken = lockFromToken || lockToToken const defaultChainId = relayClient?.chains[0].id ?? mainnet.id @@ -72,6 +74,7 @@ const SwapWidget: FC = ({ return ( = ({ onAnalyticEvent={onAnalyticEvent} > {({ - quote, - steps, + price, feeBreakdown, fromToken, setFromToken, @@ -95,11 +97,9 @@ const SwapWidget: FC = ({ recipient, customToAddress, setCustomToAddress, - swap, tradeType, setTradeType, details, - waitingForSteps, isSameCurrencySameRecipientSwap, debouncedInputAmountValue, debouncedAmountInputControls, @@ -111,7 +111,7 @@ const SwapWidget: FC = ({ setAmountOutputValue, toBalance, isLoadingToBalance, - isFetchingQuote, + isFetchingPrice, isLoadingFromBalance, fromBalance, highRelayerServiceFee, @@ -121,9 +121,9 @@ const SwapWidget: FC = ({ ctaCopy, isFromETH, timeEstimate, - setSteps, setDetails, - setSwapError + setSwapError, + invalidateBalanceQueries }) => { const handleSetFromToken = (token?: Token) => { setFromToken(token) @@ -144,23 +144,28 @@ const SwapWidget: FC = ({ return ( { if (!open) { - setSteps(null) - setDetails(null) setSwapError(null) } }} useExternalLiquidity={false} onSwapSuccess={onSwapSuccess} onAnalyticEvent={onAnalyticEvent} + invalidateBalanceQueries={invalidateBalanceQueries} setCustomToAddress={setCustomToAddress} timeEstimate={timeEstimate} > @@ -228,12 +233,12 @@ const SwapWidget: FC = ({ css={{ textAlign: 'right', color: - isFetchingQuote && tradeType === 'EXACT_OUTPUT' + isFetchingPrice && tradeType === 'EXACT_OUTPUT' ? 'text-subtle' : 'input-color', _placeholder: { color: - isFetchingQuote && tradeType === 'EXACT_OUTPUT' + isFetchingPrice && tradeType === 'EXACT_OUTPUT' ? 'text-subtle' : 'input-color' } @@ -281,11 +286,11 @@ const SwapWidget: FC = ({ ) : null} - {quote?.details?.currencyIn?.amountUsd && - Number(quote.details.currencyIn.amountUsd) > 0 ? ( + {price?.details?.currencyIn?.amountUsd && + Number(price.details.currencyIn.amountUsd) > 0 ? ( {formatDollar( - Number(quote.details.currencyIn.amountUsd) + Number(price.details.currencyIn.amountUsd) )} ) : null} @@ -423,12 +428,12 @@ const SwapWidget: FC = ({ }} css={{ color: - isFetchingQuote && tradeType === 'EXACT_INPUT' + isFetchingPrice && tradeType === 'EXACT_INPUT' ? 'gray11' : 'gray12', _placeholder: { color: - isFetchingQuote && tradeType === 'EXACT_INPUT' + isFetchingPrice && tradeType === 'EXACT_INPUT' ? 'gray11' : 'gray12' }, @@ -457,25 +462,28 @@ const SwapWidget: FC = ({ ) : ( )} - {quote?.details?.currencyOut?.amountUsd && - Number(quote.details.currencyOut.amountUsd) > 0 ? ( + {price?.details?.currencyOut?.amountUsd && + Number(price.details.currencyOut.amountUsd) > 0 ? ( {formatDollar( - Number(quote.details.currencyOut.amountUsd) + Number(price.details.currencyOut.amountUsd) )} - - - - Total Price Impact - - - {feeBreakdown?.totalFees.priceImpact} - - + + { +
+ + ( { feeBreakdown?.totalFees @@ -483,112 +491,57 @@ const SwapWidget: FC = ({ } ) + + + - - - - Swap Impact - - - {feeBreakdown?.totalFees.swapImpact} - - - {feeBreakdown?.breakdown.map((fee) => { - if (fee.id === 'origin-gas') { - return null - } - return ( - - - {fee.name} - - {fee.usd} - - ) - })} - +
} - > -
- - - ( - { - feeBreakdown?.totalFees - .priceImpactPercentage - } - ) - - - -
-
+
) : null}
- + setTransactionModalOpen(true)} ctaCopy={ctaCopy} /> diff --git a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx index 187fdcc2..48e73b59 100644 --- a/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx +++ b/packages/ui/src/components/widgets/SwapWidgetRenderer.tsx @@ -1,4 +1,4 @@ -import type { Dispatch, FC, ReactNode } from 'react' +import type { ComponentPropsWithoutRef, Dispatch, FC, ReactNode } from 'react' import { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCurrencyBalance, @@ -8,28 +8,28 @@ import { } from '../../hooks/index.js' import type { Address } from 'viem' import { formatUnits, parseUnits } from 'viem' -import { useAccount, useConfig, useWalletClient } from 'wagmi' +import { useAccount } from 'wagmi' import { useCapabilities } from 'wagmi/experimental' import type { BridgeFee, Token } from '../../types/index.js' import { useQueryClient } from '@tanstack/react-query' import { deadAddress } from '../../constants/address.js' import type { Execute } from '@reservoir0x/relay-sdk' import { + calculatePriceTimeEstimate, calculateRelayerFeeProportionUsd, - calculateTimeEstimate, - extractQuoteId, isHighRelayerServiceFeeUsd, parseFees } from '../../utils/quote.js' -import { switchChain } from 'wagmi/actions' -import { useQuote, useRelayConfig } from '@reservoir0x/relay-kit-hooks' +import { usePrice, useRelayConfig } from '@reservoir0x/relay-kit-hooks' import { EventNames } from '../../constants/events.js' import { ProviderOptionsContext } from '../../providers/RelayKitProvider.js' import type { DebouncedState } from 'usehooks-ts' +import type Text from '../../components/primitives/Text.js' -type TradeType = 'EXACT_INPUT' | 'EXACT_OUTPUT' +export type TradeType = 'EXACT_INPUT' | 'EXACT_OUTPUT' type SwapWidgetRendererProps = { + transactionModalOpen: boolean children: (props: ChildrenProps) => ReactNode defaultFromToken?: Token defaultToToken?: Token @@ -44,8 +44,8 @@ type SwapWidgetRendererProps = { } export type ChildrenProps = { - quote?: ReturnType['data'] - steps: null | Execute['steps'] + price?: ReturnType['data'] + transactionModalOpen: boolean details: null | Execute['details'] feeBreakdown: { breakdown: BridgeFee[] @@ -53,6 +53,7 @@ export type ChildrenProps = { usd?: string priceImpactPercentage?: string priceImpact?: string + priceImpactColor?: ComponentPropsWithoutRef['color'] swapImpact?: string } } | null @@ -67,10 +68,8 @@ export type ChildrenProps = { recipient?: Address customToAddress?: Address setCustomToAddress: Dispatch> - swap: () => void tradeType: TradeType setTradeType: Dispatch> - waitingForSteps: boolean isSameCurrencySameRecipientSwap: boolean amountInputValue: string debouncedInputAmountValue: string @@ -82,7 +81,7 @@ export type ChildrenProps = { debouncedAmountOutputControls: DebouncedState<(value: string) => void> toBalance?: bigint fromBalance?: bigint - isFetchingQuote: boolean + isFetchingPrice: boolean isLoadingToBalance: boolean isLoadingFromBalance: boolean highRelayerServiceFee: boolean @@ -95,13 +94,14 @@ export type ChildrenProps = { supportsExternalLiquidity: boolean timeEstimate?: { time: number; formattedTime: string } fetchingSolverConfig: boolean + invalidateBalanceQueries: () => void setUseExternalLiquidity: Dispatch> - setSteps: Dispatch> setDetails: Dispatch> setSwapError: Dispatch> } const SwapWidgetRenderer: FC = ({ + transactionModalOpen, defaultFromToken, defaultToToken, defaultToAddress, @@ -110,14 +110,11 @@ const SwapWidgetRenderer: FC = ({ context, fetchSolverConfig, children, - onAnalyticEvent, - onSwapError + onAnalyticEvent }) => { const providerOptionsContext = useContext(ProviderOptionsContext) - const wagmiConfig = useConfig() const relayClient = useRelayClient() - const walletClient = useWalletClient() - const { address, chainId: activeWalletChainId, connector } = useAccount() + const { address, connector } = useAccount() const [customToAddress, setCustomToAddress] = useState
( defaultToAddress ) @@ -129,9 +126,8 @@ const SwapWidgetRenderer: FC = ({ defaultTradeType ?? 'EXACT_INPUT' ) const queryClient = useQueryClient() - const [steps, setSteps] = useState(null) const [details, setDetails] = useState(null) - const [waitingForSteps, setWaitingForSteps] = useState(false) + const { value: amountInputValue, debouncedValue: debouncedInputAmountValue, @@ -227,13 +223,11 @@ const SwapWidgetRenderer: FC = ({ ) const { - data: quote, - isLoading: isFetchingQuote, - executeQuote: executeSwap, + data: price, + isLoading: isFetchingPrice, error - } = useQuote( + } = usePrice( relayClient ? relayClient : undefined, - walletClient.data, fromToken && toToken ? { user: address ?? deadAddress, @@ -254,15 +248,13 @@ const SwapWidgetRenderer: FC = ({ debouncedOutputAmountValue, toToken.decimals ).toString(), - source: relayClient?.source ?? undefined, + referrer: relayClient?.source ?? undefined, useExternalLiquidity } : undefined, - () => {}, - ({ steps, details }) => { + ({ details }) => { onAnalyticEvent?.(EventNames.SWAP_EXECUTE_QUOTE_RECEIVED, { wallet_connector: connector?.name, - quote_id: steps ? extractQuoteId(steps) : undefined, amount_in: details?.currencyIn?.amountFormatted, currency_in: details?.currencyIn?.currency?.symbol, chain_id_in: details?.currencyIn?.currency?.chainId, @@ -287,7 +279,7 @@ const SwapWidgetRenderer: FC = ({ fromToken !== undefined && toToken !== undefined, refetchInterval: - steps === null && + !transactionModalOpen && debouncedInputAmountValue === amountInputValue && debouncedOutputAmountValue === amountOutputValue ? 12000 @@ -297,40 +289,40 @@ const SwapWidgetRenderer: FC = ({ useEffect(() => { if (tradeType === 'EXACT_INPUT') { - const amountOut = quote?.details?.currencyOut?.amount ?? '' + const amountOut = price?.details?.currencyOut?.amount ?? '' setAmountOutputValue( amountOut !== '' ? formatUnits( BigInt(amountOut), - Number(quote?.details?.currencyOut?.currency?.decimals ?? 18) + Number(price?.details?.currencyOut?.currency?.decimals ?? 18) ) : '' ) } else if (tradeType === 'EXACT_OUTPUT') { - const amountIn = quote?.details?.currencyIn?.amount ?? '' + const amountIn = price?.details?.currencyIn?.amount ?? '' setAmountInputValue( amountIn !== '' ? formatUnits( BigInt(amountIn), - Number(quote?.details?.currencyIn?.currency?.decimals ?? 18) + Number(price?.details?.currencyIn?.currency?.decimals ?? 18) ) : '' ) } debouncedAmountInputControls.flush() debouncedAmountOutputControls.flush() - }, [quote, tradeType]) + }, [price, tradeType]) const feeBreakdown = useMemo(() => { const chains = relayClient?.chains const fromChain = chains?.find((chain) => chain.id === fromToken?.chainId) const toChain = chains?.find((chain) => chain.id === toToken?.chainId) - return fromToken && toToken && fromChain && toChain && quote - ? parseFees(toChain, fromChain, quote) + return fromToken && toToken && fromChain && toChain && price + ? parseFees(toChain, fromChain, price) : null - }, [quote, fromToken, toToken, relayClient]) + }, [price, fromToken, toToken, relayClient]) - const totalAmount = BigInt(quote?.details?.currencyIn?.amount ?? 0n) + const totalAmount = BigInt(price?.details?.currencyIn?.amount ?? 0n) const hasInsufficientBalance = Boolean( !fromBalanceErrorFetching && @@ -348,9 +340,9 @@ const SwapWidgetRenderer: FC = ({ const isInsufficientLiquidityError = fetchQuoteErrorMessage?.includes( 'No quotes available' ) - const highRelayerServiceFee = isHighRelayerServiceFeeUsd(quote) - const relayerFeeProportion = calculateRelayerFeeProportionUsd(quote) - const timeEstimate = calculateTimeEstimate(quote?.breakdown) + const highRelayerServiceFee = isHighRelayerServiceFeeUsd(price) + const relayerFeeProportion = calculateRelayerFeeProportionUsd(price) + const timeEstimate = calculatePriceTimeEstimate(price?.details) const isFromETH = fromToken?.symbol === 'ETH' @@ -368,13 +360,31 @@ const SwapWidgetRenderer: FC = ({ fromToken?.chainId === toToken?.chainId && address === recipient + const operation = price?.details?.operation || 'swap' + let ctaCopy: string = context || 'Swap' - if (context === 'Swap') { - if (isWrap) { + switch (operation) { + case 'wrap': { ctaCopy = 'Wrap' - } else if (isUnwrap) { + break + } + case 'unwrap': { ctaCopy = 'Unwrap' + break + } + case 'send': { + ctaCopy = 'Send' + break + } + case 'swap': + default: { + if (context === 'Swap') { + ctaCopy = 'Swap' + } else { + ctaCopy = context === 'Deposit' ? 'Deposit' : 'Withdraw' + } + break } } @@ -388,145 +398,37 @@ const SwapWidgetRenderer: FC = ({ ctaCopy = 'Insufficient Balance' } else if (isInsufficientLiquidityError) { ctaCopy = 'Insufficient Liquidity' - } else if (steps !== null) { - if (context === 'Swap') { - ctaCopy = 'Swapping' - if (isWrap) { + } else if (transactionModalOpen) { + switch (operation) { + case 'wrap': { ctaCopy = 'Wrapping' - } else if (isUnwrap) { + break + } + case 'unwrap': { ctaCopy = 'Unwrapping' + break } - } else { - ctaCopy = context === 'Deposit' ? 'Depositing' : 'Withdrawing' - } - } - - const swap = useCallback(async () => { - try { - onAnalyticEvent?.(EventNames.SWAP_CTA_CLICKED) - setWaitingForSteps(true) - - if (!executeSwap) { - throw 'Missing a quote' + case 'send': { + ctaCopy = 'Sending' + break } - - if (fromToken && fromToken?.chainId !== activeWalletChainId) { - onAnalyticEvent?.(EventNames.SWAP_SWITCH_NETWORK) - await switchChain(wagmiConfig, { - chainId: fromToken.chainId - }) + case 'swap': + default: { + if (context === 'Swap') { + ctaCopy = 'Swap' + } else { + ctaCopy = context === 'Deposit' ? 'Depositing' : 'Withdrawing' + } + break } - - executeSwap(({ steps, details }) => { - setSteps(steps) - setDetails(details) - }) - ?.catch((error: any) => { - if ( - error && - ((typeof error.message === 'string' && - error.message.includes('rejected')) || - (typeof error === 'string' && error.includes('rejected'))) - ) { - onAnalyticEvent?.(EventNames.USER_REJECTED_WALLET) - setSteps(null) - setDetails(null) - return - } - - const errorMessage = error?.response?.data?.message - ? new Error(error?.response?.data?.message) - : error - - onAnalyticEvent?.(EventNames.SWAP_ERROR, { - error_message: errorMessage, - wallet_connector: connector?.name, - quote_id: steps ? extractQuoteId(steps) : undefined, - amount_in: parseFloat(`${debouncedInputAmountValue}`), - currency_in: fromToken?.symbol, - chain_id_in: fromToken?.chainId, - amount_out: parseFloat(`${debouncedOutputAmountValue}`), - currency_out: toToken?.symbol, - chain_id_out: toToken?.chainId, - is_canonical: useExternalLiquidity, - txHashes: steps - ?.map((step) => { - let txHashes: { chainId: number; txHash: Address }[] = [] - step.items?.forEach((item) => { - if (item.txHashes) { - txHashes = txHashes.concat([ - ...(item.txHashes ?? []), - ...(item.internalTxHashes ?? []) - ]) - } - }) - return txHashes - }) - .flat() - }) - setSwapError(errorMessage) - onSwapError?.(errorMessage, quote as Execute) - }) - .finally(() => { - setWaitingForSteps(false) - invalidateBalanceQueries() - }) - } catch (e) { - setWaitingForSteps(false) - onAnalyticEvent?.(EventNames.SWAP_ERROR, { - error_message: e, - wallet_connector: connector?.name, - quote_id: steps ? extractQuoteId(steps) : undefined, - amount_in: parseFloat(`${debouncedInputAmountValue}`), - currency_in: fromToken?.symbol, - chain_id_in: fromToken?.chainId, - amount_out: parseFloat(`${debouncedOutputAmountValue}`), - currency_out: toToken?.symbol, - chain_id_out: toToken?.chainId, - is_canonical: useExternalLiquidity, - txHashes: steps - ?.map((step) => { - let txHashes: { chainId: number; txHash: Address }[] = [] - step.items?.forEach((item) => { - if (item.txHashes) { - txHashes = txHashes.concat([ - ...(item.txHashes ?? []), - ...(item.internalTxHashes ?? []) - ]) - } - }) - return txHashes - }) - .flat() - }) - onSwapError?.(e as any, quote as Execute) } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - relayClient, - activeWalletChainId, - wagmiConfig, - address, - connector, - fromToken, - toToken, - customToAddress, - recipient, - debouncedInputAmountValue, - debouncedOutputAmountValue, - tradeType, - useExternalLiquidity, - executeSwap, - setSteps, - setDetails, - invalidateBalanceQueries - ]) + } return ( <> {children({ - quote, - steps, + price, + transactionModalOpen, feeBreakdown, fromToken, setFromToken, @@ -539,11 +441,9 @@ const SwapWidgetRenderer: FC = ({ recipient, customToAddress, setCustomToAddress, - swap, tradeType, setTradeType, details, - waitingForSteps, isSameCurrencySameRecipientSwap, debouncedInputAmountValue, debouncedAmountInputControls, @@ -555,7 +455,7 @@ const SwapWidgetRenderer: FC = ({ setAmountOutputValue, toBalance, isLoadingToBalance, - isFetchingQuote, + isFetchingPrice, isLoadingFromBalance, fromBalance, highRelayerServiceFee, @@ -568,8 +468,8 @@ const SwapWidgetRenderer: FC = ({ supportsExternalLiquidity, timeEstimate, fetchingSolverConfig: config.isFetching, + invalidateBalanceQueries, setUseExternalLiquidity, - setSteps, setDetails, setSwapError })} diff --git a/packages/ui/src/components/widgets/WidgetContainer.tsx b/packages/ui/src/components/widgets/WidgetContainer.tsx index eb5145b3..9a137237 100644 --- a/packages/ui/src/components/widgets/WidgetContainer.tsx +++ b/packages/ui/src/components/widgets/WidgetContainer.tsx @@ -8,18 +8,26 @@ import type { Execute } from '@reservoir0x/relay-sdk' import { Flex } from '../primitives/index.js' export type WidgetContainerProps = { + transactionModalOpen: boolean + setTransactionModalOpen: React.Dispatch> children: (props: WidgetChildProps) => ReactNode onSwapModalOpenChange: (open: boolean) => void onAnalyticEvent?: (eventName: string, data?: any) => void onSwapSuccess?: (data: Execute) => void + invalidateBalanceQueries: () => void } & Pick< ChildrenProps, - | 'steps' | 'fromToken' | 'toToken' + | 'amountInputValue' + | 'amountOutputValue' + | 'debouncedInputAmountValue' + | 'debouncedOutputAmountValue' + | 'recipient' + | 'customToAddress' + | 'tradeType' | 'swapError' - | 'details' - | 'quote' + | 'price' | 'address' | 'setCustomToAddress' | 'useExternalLiquidity' @@ -32,19 +40,27 @@ export type WidgetChildProps = { } const WidgetContainer: FC = ({ + transactionModalOpen, + setTransactionModalOpen, children, - steps, fromToken, toToken, + debouncedInputAmountValue, + debouncedOutputAmountValue, + amountInputValue, + amountOutputValue, + tradeType, + customToAddress, swapError, - quote, - details, + price, address, useExternalLiquidity, timeEstimate, + recipient, onSwapModalOpenChange, onSwapSuccess, onAnalyticEvent, + invalidateBalanceQueries, setCustomToAddress }) => { const isMounted = useMounted() @@ -71,21 +87,26 @@ const WidgetContainer: FC = ({ })} {isMounted ? ( { onSwapModalOpenChange(open) + setTransactionModalOpen(open) }} fromToken={fromToken} toToken={toToken} - error={swapError} - steps={steps} - details={details} - fees={quote?.fees} + amountInputValue={amountInputValue} + amountOutputValue={amountOutputValue} + debouncedInputAmountValue={debouncedInputAmountValue} + debouncedOutputAmountValue={debouncedOutputAmountValue} + tradeType={tradeType} + useExternalLiquidity={useExternalLiquidity} address={address} + recipient={recipient} isCanonical={useExternalLiquidity} timeEstimate={timeEstimate} onAnalyticEvent={onAnalyticEvent} onSuccess={onSwapSuccess} + invalidateBalanceQueries={invalidateBalanceQueries} /> ) : null} 0) { - return '< $0.00' + return '< $0.01' } return formatted } diff --git a/packages/ui/src/utils/quote.ts b/packages/ui/src/utils/quote.ts index d6fcaa17..e42d7f28 100644 --- a/packages/ui/src/utils/quote.ts +++ b/packages/ui/src/utils/quote.ts @@ -2,7 +2,9 @@ import type { Execute, RelayChain } from '@reservoir0x/relay-sdk' import { formatBN, formatDollar } from './numbers.js' import type { BridgeFee } from '../types/index.js' import { formatSeconds } from './time.js' -import type { useQuote } from '@reservoir0x/relay-kit-hooks' +import type { useQuote, PriceResponse } from '@reservoir0x/relay-kit-hooks' +import type { ComponentPropsWithoutRef } from 'react' +import type Text from '../components/primitives/Text.js' type ExecuteSwapResponse = ReturnType['data'] @@ -16,6 +18,7 @@ export const parseFees = ( usd?: string priceImpactPercentage?: string priceImpact?: string + priceImpactColor?: ComponentPropsWithoutRef['color'] swapImpact?: string } } => { @@ -107,6 +110,18 @@ export const parseFees = ( currency: fees?.app?.currency }) } + + let priceImpactColor: ComponentPropsWithoutRef['color'] = + 'subtle' + + if (quote?.details?.totalImpact?.percent) { + const percent = Number(quote.details.totalImpact.percent) + if (percent <= -3) { + priceImpactColor = 'red' + } else if (percent >= 10) { + priceImpactColor = 'success' + } + } return { breakdown, totalFees: { @@ -119,6 +134,7 @@ export const parseFees = ( Math.abs(parseFloat(quote?.details?.totalImpact?.usd ?? 0)) ) : undefined, + priceImpactColor, swapImpact: quote?.details?.swapImpact?.usd ? formatDollar( Math.abs(parseFloat(quote?.details?.swapImpact?.usd ?? 0)) @@ -209,3 +225,15 @@ export const calculateTimeEstimate = (breakdown?: Execute['breakdown']) => { formattedTime } } + +export const calculatePriceTimeEstimate = ( + details?: PriceResponse['details'] +) => { + const time = details?.timeEstimate ?? 0 + const formattedTime = formatSeconds(time) + + return { + time, + formattedTime + } +}