From 7fa63fe560cf21e628c5a97a9ba7fea7e8200f01 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Fri, 20 Sep 2024 17:43:17 +0200 Subject: [PATCH 01/27] feat: add permit2 signature step --- .../add-liquidity/[[...txHash]]/layout.tsx | 14 ++- .../pool/actions/LiquidityActionHelpers.ts | 6 + .../handlers/AddLiquidity.handler.ts | 16 ++- .../UnbalancedAddLiquidity.handler.ts | 54 +++++++-- .../add-liquidity/useAddLiquiditySteps.tsx | 30 ++++- .../relayer/signRelayerApproval.hooks.tsx | 9 +- lib/modules/relayer/signRelayerApproval.ts | 14 +-- .../tokens/approvals/approval-labels.spec.ts | 22 ++++ .../tokens/approvals/approval-labels.ts | 12 +- .../permit2/Permit2SignatureProvider.tsx | 40 +++++++ .../permit2/signPermit2TokenApprovals.tsx | 32 ++++++ .../permit2/useSignPermit2Approval.tsx | 107 ++++++++++++++++++ .../approvals/useTokenApprovalSteps.tsx | 2 +- .../transactions/transaction-steps/lib.tsx | 1 + .../transaction-steps/useSignPermit2Step.tsx | 68 +++++++++++ lib/modules/web3/useSdkViemClient.tsx | 11 ++ 16 files changed, 397 insertions(+), 41 deletions(-) create mode 100644 lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx create mode 100644 lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx create mode 100644 lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx create mode 100644 lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx create mode 100644 lib/modules/web3/useSdkViemClient.tsx diff --git a/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx b/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx index 8132372fd..f18629133 100644 --- a/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx +++ b/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx @@ -12,6 +12,7 @@ import { isHash } from 'viem' import { usePoolRedirect } from '@/lib/modules/pool/pool.hooks' import { DefaultPageContainer } from '@/lib/shared/components/containers/DefaultPageContainer' import { AddLiquidityProvider } from '@/lib/modules/pool/actions/add-liquidity/AddLiquidityProvider' +import { Permit2SignatureProvider } from '@/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider' type Props = PropsWithChildren<{ params: { txHash?: string[] } @@ -39,12 +40,15 @@ export default function AddLiquidityLayout({ params: { txHash }, children }: Pro return ( + {/* // TODO: do we really need a provider */} - - - {children} - - + + + + {children} + + + diff --git a/lib/modules/pool/actions/LiquidityActionHelpers.ts b/lib/modules/pool/actions/LiquidityActionHelpers.ts index cfdc4a7d8..9bf0e4481 100644 --- a/lib/modules/pool/actions/LiquidityActionHelpers.ts +++ b/lib/modules/pool/actions/LiquidityActionHelpers.ts @@ -137,6 +137,12 @@ export class LiquidityActionHelpers { return humanAmountsIn.some(amountIn => isSameAddress(amountIn.tokenAddress, nativeAssetAddress)) } + public isNativeAssetIn2(amountsIn: TokenAmount[]): boolean { + const nativeAssetAddress = this.networkConfig.tokens.nativeAsset.address + + return amountsIn.some(amountIn => isSameAddress(amountIn.token.address, nativeAssetAddress)) + } + public isNativeAsset(tokenAddress: Address): boolean { const nativeAssetAddress = this.networkConfig.tokens.nativeAsset.address diff --git a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts index d79b7d665..ad595fb91 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts @@ -1,6 +1,15 @@ +import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' +import { AddLiquidityQueryOutput, Permit2 } from '@balancer/sdk' +import { Address } from 'viem' import { BuildAddLiquidityInput, QueryAddLiquidityOutput } from '../add-liquidity.types' -import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' + +export interface Permit2AddLiquidityInput { + account: Address + slippagePercent: string + sdkQueryOutput: AddLiquidityQueryOutput +} /** * AddLiquidityHandler is an interface that defines the methods that must be implemented by a handler. @@ -27,4 +36,9 @@ export interface AddLiquidityHandler { It is responsibility of the UI to avoid calling buildAddLiquidityCallData before the last queryAddLiquidity was finished */ buildCallData(inputs: BuildAddLiquidityInput): Promise + + /* Sign permit2 for adding liquidity (for now only used by v3 pools) + TODO: generalize for other handlers using permit2 + */ + signPermit2?(input: Permit2AddLiquidityInput, walletClient: SdkClient): Promise } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index c33ee4716..e2c69e2bf 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -2,10 +2,15 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' import { getRpcUrl } from '@/lib/modules/web3/transports' +import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' import { AddLiquidity, + AddLiquidityBaseBuildCallInput, + AddLiquidityBaseQueryOutput, AddLiquidityKind, AddLiquidityUnbalancedInput, + Permit2, + Permit2Helper, PriceImpact, PriceImpactAmount, Slippage, @@ -13,11 +18,11 @@ import { import { Pool } from '../../../PoolProvider' import { LiquidityActionHelpers, - formatBuildCallParams, areEmptyAmounts, + formatBuildCallParams, } from '../../LiquidityActionHelpers' -import { AddLiquidityHandler } from './AddLiquidity.handler' import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' +import { AddLiquidityHandler, Permit2AddLiquidityInput } from './AddLiquidity.handler' /** * UnbalancedAddLiquidityHandler is a handler that implements the @@ -61,21 +66,17 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { } public async buildCallData({ - humanAmountsIn, - account, slippagePercent, queryOutput, + account, }: SdkBuildAddLiquidityInput): Promise { const addLiquidity = new AddLiquidity() - const baseBuildCallParams = { - ...queryOutput.sdkQueryOutput, - slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), - wethIsEth: this.helpers.isNativeAssetIn(humanAmountsIn), - } - const buildCallParams = formatBuildCallParams( - baseBuildCallParams, + this.constructBaseBuildCallInput({ + sdkQueryOutput: queryOutput.sdkQueryOutput, + slippagePercent: slippagePercent, + }), this.helpers.isV3Pool(), account ) @@ -91,6 +92,22 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { } } + public async signPermit2( + input: Permit2AddLiquidityInput, + sdkClient: SdkClient + ): Promise { + const signature = await Permit2Helper.signAddLiquidityApproval({ + ...this.constructBaseBuildCallInput({ + slippagePercent: input.slippagePercent, + sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, + }), + client: sdkClient, + owner: input.account, + }) + + return signature + } + /** * PRIVATE METHODS */ @@ -106,4 +123,19 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { kind: AddLiquidityKind.Unbalanced, } } + + public constructBaseBuildCallInput({ + slippagePercent, + sdkQueryOutput, + }: { + slippagePercent: string + sdkQueryOutput: AddLiquidityBaseQueryOutput + }): AddLiquidityBaseBuildCallInput { + const baseBuildCallParams = { + ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + wethIsEth: this.helpers.isNativeAssetIn2(sdkQueryOutput.amountsIn), + } + return baseBuildCallParams + } } diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index 59d33018e..ec2a28103 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -3,13 +3,17 @@ import { useShouldSignRelayerApproval } from '@/lib/modules/relayer/signRelayerA import { useApproveRelayerStep } from '@/lib/modules/relayer/useApproveRelayerStep' import { useRelayerMode } from '@/lib/modules/relayer/useRelayerMode' import { useTokenApprovalSteps } from '@/lib/modules/tokens/approvals/useTokenApprovalSteps' +import { getSpenderForAddLiquidity } from '@/lib/modules/tokens/token.helpers' +import { useSignPermit2Step } from '@/lib/modules/transactions/transaction-steps/useSignPermit2Step' +import { useSignRelayerStep } from '@/lib/modules/transactions/transaction-steps/useSignRelayerStep' +import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' +import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { AddLiquidityBaseQueryOutput } from '@balancer/sdk' import { useMemo } from 'react' import { usePool } from '../../PoolProvider' +import { requiresPermit2Approval } from '../../pool.helpers' import { LiquidityActionHelpers } from '../LiquidityActionHelpers' import { AddLiquidityStepParams, useAddLiquidityStep } from './useAddLiquidityStep' -import { requiresPermit2Approval } from '../../pool.helpers' -import { useSignRelayerStep } from '@/lib/modules/transactions/transaction-steps/useSignRelayerStep' -import { getSpenderForAddLiquidity } from '@/lib/modules/tokens/token.helpers' type AddLiquidityStepsParams = AddLiquidityStepParams & { helpers: LiquidityActionHelpers @@ -21,6 +25,7 @@ export function useAddLiquiditySteps({ simulationQuery, }: AddLiquidityStepsParams) { const { pool, chainId, chain } = usePool() + const { slippage } = useUserSettings() const relayerMode = useRelayerMode(pool) const shouldSignRelayerApproval = useShouldSignRelayerApproval(chainId, relayerMode) @@ -42,20 +47,33 @@ export function useAddLiquiditySteps({ isPermit2: requiresPermit2Approval(pool), }) + const signPermit2Step = useSignPermit2Step( + { + handler, + slippagePercent: slippage, + queryOutput: simulationQuery.data as AddLiquidityBaseQueryOutput, + }, + chainId + ) + const addLiquidityStep = useAddLiquidityStep({ handler, humanAmountsIn, simulationQuery, }) + const addSteps = requiresPermit2Approval(pool) + ? [signPermit2Step, addLiquidityStep] + : [addLiquidityStep] + const steps = useMemo(() => { if (relayerMode === 'approveRelayer') { - return [approveRelayerStep, ...tokenApprovalSteps, addLiquidityStep] + return [approveRelayerStep, ...tokenApprovalSteps, ...addSteps] } else if (shouldSignRelayerApproval) { - return [signRelayerStep, ...tokenApprovalSteps, addLiquidityStep] + return [signRelayerStep, ...tokenApprovalSteps, ...addSteps] } - return [...tokenApprovalSteps, addLiquidityStep] + return [...tokenApprovalSteps, ...addSteps] }, [ relayerMode, shouldSignRelayerApproval, diff --git a/lib/modules/relayer/signRelayerApproval.hooks.tsx b/lib/modules/relayer/signRelayerApproval.hooks.tsx index 70c33365a..ae8112294 100644 --- a/lib/modules/relayer/signRelayerApproval.hooks.tsx +++ b/lib/modules/relayer/signRelayerApproval.hooks.tsx @@ -8,6 +8,7 @@ import { RelayerMode } from './useRelayerMode' import { SignRelayerState, useRelayerSignature } from './RelayerSignatureProvider' import { SupportedChainId } from '@/lib/config/config.types' import { Toast } from '@/lib/shared/components/toasts/Toast' +import { useSdkViemClient } from '../web3/useSdkViemClient' export function useShouldSignRelayerApproval(chainId: SupportedChainId, relayerMode: RelayerMode) { const { hasApprovedRelayer } = useHasApprovedRelayer(chainId) @@ -23,22 +24,22 @@ export function useSignRelayerApproval(chainId: SupportedChainId) { const [error, setError] = useState() - const { data: walletClient } = useWalletClient() + const sdkClient = useSdkViemClient() useEffect(() => { - if (walletClient === undefined) { + if (sdkClient === undefined) { setSignRelayerState(SignRelayerState.Preparing) } else { setSignRelayerState(SignRelayerState.Ready) } - }, [setSignRelayerState, walletClient]) + }, [setSignRelayerState, sdkClient]) async function signRelayer() { setSignRelayerState(SignRelayerState.Confirming) setError(undefined) try { - const signature = await signRelayerApproval(userAddress, chainId, walletClient) + const signature = await signRelayerApproval(userAddress, chainId, sdkClient) if (signature) { setSignRelayerState(SignRelayerState.Completed) diff --git a/lib/modules/relayer/signRelayerApproval.ts b/lib/modules/relayer/signRelayerApproval.ts index 027671590..86888801f 100644 --- a/lib/modules/relayer/signRelayerApproval.ts +++ b/lib/modules/relayer/signRelayerApproval.ts @@ -2,26 +2,20 @@ import { getNetworkConfig } from '@/lib/config/app.config' import { SupportedChainId } from '@/lib/config/config.types' import { ensureError } from '@/lib/shared/utils/errors' import { Relayer } from '@balancer/sdk' -import { Address, publicActions, walletActions } from 'viem' -import { GetWalletClientReturnType } from 'wagmi/actions' - -// TODO: replace this type with the one exposed by the SDK (WIP) -type SignRelayerApprovalParams = Parameters -type SdkClient = SignRelayerApprovalParams[2] +import { Address } from 'viem' +import { SdkClient } from '../web3/useSdkViemClient' export async function signRelayerApproval( userAddress: Address, chainId: SupportedChainId, - client?: GetWalletClientReturnType + client?: SdkClient ): Promise
{ if (!client) return undefined - const publicClient = client.extend(publicActions).extend(walletActions) as SdkClient - const relayerV6Address = getNetworkConfig(chainId).contracts.balancer.relayerV6 try { - const signature = await Relayer.signRelayerApproval(relayerV6Address, userAddress, publicClient) + const signature = await Relayer.signRelayerApproval(relayerV6Address, userAddress, client) return signature } catch (e: unknown) { const error = ensureError(e) diff --git a/lib/modules/tokens/approvals/approval-labels.spec.ts b/lib/modules/tokens/approvals/approval-labels.spec.ts index 7678ec97a..f181cf26c 100644 --- a/lib/modules/tokens/approvals/approval-labels.spec.ts +++ b/lib/modules/tokens/approvals/approval-labels.spec.ts @@ -50,3 +50,25 @@ test('Token approval labels for unapprove', () => { } `) }) + +test('Token approval labels for permit2', () => { + const args: TokenApprovalLabelArgs = { + actionType: 'AddLiquidity', + symbol: 'WETH', + requiredRawAmount: 100000000n, + isPermit2: true, + } + const result = buildTokenApprovalLabels(args) + expect(result).toMatchInlineSnapshot(` + { + "confirmed": "WETH approved!", + "confirming": "Approving WETH...", + "description": "Approval of WETH for adding liquidity.", + "error": "Error approving WETH", + "init": "Approve Permit2: WETH", + "title": "WETH: Approve Permit2", + "tooltip": "You must approve WETH to add liquidity for this token on Balancer. + Approvals are required once per token, per wallet.", + } + `) +}) diff --git a/lib/modules/tokens/approvals/approval-labels.ts b/lib/modules/tokens/approvals/approval-labels.ts index 617e737c0..6ea086a93 100644 --- a/lib/modules/tokens/approvals/approval-labels.ts +++ b/lib/modules/tokens/approvals/approval-labels.ts @@ -1,3 +1,4 @@ +import { is } from 'date-fns/locale' import { BuildTransactionLabels } from '../../web3/contracts/transactionLabels' export type ApprovalAction = @@ -12,15 +13,17 @@ export type TokenApprovalLabelArgs = { actionType: ApprovalAction symbol: string requiredRawAmount: bigint + isPermit2?: boolean } export const buildTokenApprovalLabels: BuildTransactionLabels = ({ actionType, symbol, + isPermit2 = false, }: TokenApprovalLabelArgs) => { return { - init: initApprovalLabelFor(actionType, symbol), - title: `Approve ${symbol}`, + init: initApprovalLabelFor(actionType, symbol, isPermit2), + title: isPermit2 ? `${symbol}: Approve Permit2` : `Approve ${symbol}`, description: descriptionFor(actionType, symbol), confirming: actionType === 'Unapprove' ? `Unapproving ${symbol}...` : `Approving ${symbol}...`, confirmed: `${symbol} ${actionType === 'Unapprove' ? 'unapproved' : 'approved!'}`, @@ -29,7 +32,10 @@ export const buildTokenApprovalLabels: BuildTransactionLabels = ({ } } -function initApprovalLabelFor(actionType: ApprovalAction, symbol: string) { +function initApprovalLabelFor(actionType: ApprovalAction, symbol: string, isPermit2: boolean) { + if (isPermit2) { + return `Approve Permit2: ${symbol}` + } switch (actionType) { case 'Locking': return `Approve LP token to lock` diff --git a/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx new file mode 100644 index 000000000..31a0251b4 --- /dev/null +++ b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx @@ -0,0 +1,40 @@ +'use client' + +import { useMandatoryContext } from '@/lib/shared/utils/contexts' +import { Permit2 } from '@balancer/sdk' +import { PropsWithChildren, createContext, useState } from 'react' + +export enum SignPermit2State { + Ready = 'init', + Confirming = 'confirming', + Preparing = 'preparing', + Completed = 'completed', +} + +export type UsePermit2SignatureResponse = ReturnType +export const Permit2SignatureContext = createContext(null) + +export function _usePermit2Signature() { + const [permit2ApprovalSignature, setPermit2ApprovalSignature] = useState() + + const [signPermit2State, setSignPermit2State] = useState( + SignPermit2State.Preparing + ) + + return { + permit2ApprovalSignature, + setPermit2ApprovalSignature, + signPermit2State, + setSignPermit2State, + } +} + +export function Permit2SignatureProvider({ children }: PropsWithChildren) { + const hook = _usePermit2Signature() + return ( + {children} + ) +} + +export const usePermit2Signature = (): UsePermit2SignatureResponse => + useMandatoryContext(Permit2SignatureContext, 'Permit2Signature') diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx new file mode 100644 index 000000000..f78c48172 --- /dev/null +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx @@ -0,0 +1,32 @@ +import { + AddLiquidityHandler, + Permit2AddLiquidityInput, +} from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' +import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' +import { ensureError } from '@/lib/shared/utils/errors' +import { Permit2 } from '@balancer/sdk' + +type SignPermit2Params = { + handler: AddLiquidityHandler //TODO: maybe more handlers??? + sdkClient?: SdkClient + permit2Input: Permit2AddLiquidityInput +} +export async function signPermit2TokenApprovals({ + handler, + sdkClient, + permit2Input, +}: SignPermit2Params): Promise { + if (!sdkClient) return undefined + + try { + if (!handler.signPermit2) throw new Error('Handler does not implement signPermit2 method') + const signature = await handler.signPermit2(permit2Input, sdkClient) + return signature + } catch (e: unknown) { + const error = ensureError(e) + console.log(error) + // When the user explicitly rejects in the wallet we return undefined to ignore the error and do nothing + if (error.name === 'UserRejectedRequestError') return + throw error + } +} diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx new file mode 100644 index 000000000..dde550051 --- /dev/null +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx @@ -0,0 +1,107 @@ +import { AddLiquidityHandler } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' +import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { useSdkViemClient } from '@/lib/modules/web3/useSdkViemClient' +import { Toast } from '@/lib/shared/components/toasts/Toast' +import { AddLiquidityBaseQueryOutput } from '@balancer/sdk' +import { useToast } from '@chakra-ui/react' +import { useEffect, useState } from 'react' +import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' +import { signPermit2TokenApprovals } from './signPermit2TokenApprovals' + +export type AddLiquidityPermit2Params = { + handler: AddLiquidityHandler + queryOutput?: AddLiquidityBaseQueryOutput + slippagePercent: string +} +export function useSignPermit2Approval({ + queryOutput, + slippagePercent, + handler, +}: AddLiquidityPermit2Params) { + const toast = useToast() + const { userAddress } = useUserAccount() + + const { setSignPermit2State, setPermit2ApprovalSignature, signPermit2State } = + usePermit2Signature() + + const [error, setError] = useState() + + const sdkClient = useSdkViemClient() + + useEffect(() => { + if (sdkClient === undefined) { + setSignPermit2State(SignPermit2State.Preparing) + } else { + setSignPermit2State(SignPermit2State.Ready) + } + }, [setSignPermit2State, sdkClient]) + + async function signPermit2() { + if (!queryOutput) throw new Error('No input provided for permit2 signature') + setSignPermit2State(SignPermit2State.Confirming) + setError(undefined) + + try { + const signature = await signPermit2TokenApprovals({ + handler, + sdkClient, + permit2Input: { + account: userAddress, + slippagePercent, + sdkQueryOutput: queryOutput.sdkQueryOutput, + }, + }) + + if (signature) { + setSignPermit2State(SignPermit2State.Completed) + toast({ + title: 'Permit2 approval signed!', + description: '', + status: 'success', + duration: 5000, + isClosable: true, + render: ({ ...rest }) => , + }) + } else { + setSignPermit2State(SignPermit2State.Ready) + } + + setPermit2ApprovalSignature(signature) + } catch (error) { + console.error(error) + setError('Error in permit2 signature call') + setSignPermit2State(SignPermit2State.Ready) + } + } + + return { + signPermit2, + signPermit2State, + buttonLabel: getButtonLabel(signPermit2State), + isLoading: isLoading(signPermit2State) || !queryOutput, + isDisabled: isDisabled(signPermit2State), + error, + } +} + +function isDisabled(signPermit2State: SignPermit2State) { + return ( + signPermit2State === SignPermit2State.Confirming || + signPermit2State === SignPermit2State.Completed + ) +} + +function isLoading(signRelayerState: SignPermit2State) { + return ( + signRelayerState === SignPermit2State.Confirming || + signRelayerState === SignPermit2State.Preparing + ) +} + +function getButtonLabel(signPermit2State: SignPermit2State) { + if (signPermit2State === SignPermit2State.Ready) return 'Permit transfer: token name' + if (signPermit2State === SignPermit2State.Confirming) return 'Confirm permit2 signature in wallet' + if (signPermit2State === SignPermit2State.Preparing) return 'Preparing' + if (signPermit2State === SignPermit2State.Completed) return 'Permit2 Signed' + return '' +} diff --git a/lib/modules/tokens/approvals/useTokenApprovalSteps.tsx b/lib/modules/tokens/approvals/useTokenApprovalSteps.tsx index 0bf7eb358..8f10bfafd 100644 --- a/lib/modules/tokens/approvals/useTokenApprovalSteps.tsx +++ b/lib/modules/tokens/approvals/useTokenApprovalSteps.tsx @@ -68,7 +68,7 @@ export function useTokenApprovalSteps({ const { tokenAddress, requiredRawAmount, requestedRawAmount } = tokenAmountToApprove const token = getToken(tokenAddress, chain) const symbol = bptSymbol ?? (token && token?.symbol) ?? 'Unknown' - const labels = buildTokenApprovalLabels({ actionType, symbol }) + const labels = buildTokenApprovalLabels({ actionType, symbol, isPermit2 }) const id = tokenAddress const isComplete = () => { diff --git a/lib/modules/transactions/transaction-steps/lib.tsx b/lib/modules/transactions/transaction-steps/lib.tsx index 515a08419..eca264c12 100644 --- a/lib/modules/transactions/transaction-steps/lib.tsx +++ b/lib/modules/transactions/transaction-steps/lib.tsx @@ -41,6 +41,7 @@ export type StepType = | 'claim' | 'swap' | LockActionType + | 'signPermit2' export type TxActionId = | 'SignBatchRelayer' diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx new file mode 100644 index 000000000..bda801196 --- /dev/null +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -0,0 +1,68 @@ +'use client' + +import { ConnectWallet } from '@/lib/modules/web3/ConnectWallet' +import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' +import { Button, VStack } from '@chakra-ui/react' +import { useMemo } from 'react' +import { SignPermit2State } from '../../tokens/approvals/permit2/Permit2SignatureProvider' +import { + AddLiquidityPermit2Params, + useSignPermit2Approval, +} from '../../tokens/approvals/permit2/useSignPermit2Approval' +import { useChainSwitch } from '../../web3/useChainSwitch' +import { TransactionStep } from './lib' + +export const signRelayerStepTitle = 'Sign relayer' + +export function useSignPermit2Step( + params: AddLiquidityPermit2Params, + chainId: number +): TransactionStep { + const { isConnected } = useUserAccount() + const { signPermit2, signPermit2State, isLoading, isDisabled, buttonLabel, error } = + useSignPermit2Approval(params) + const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = + useChainSwitch(chainId) + + const SignPermitButton = () => ( + + {error && } + {!isConnected && } + {shouldChangeNetwork && isConnected && } + {!shouldChangeNetwork && isConnected && ( + + )} + + ) + + const isComplete = () => signPermit2State === SignPermit2State.Completed + + return useMemo( + () => ({ + id: 'sign-permit2', + stepType: 'signPermit2', + labels: { + // TODO: display token symbol/s + title: `Permit on balancer`, + init: `Permit transfer`, + tooltip: 'Sign permit2 transfer', + }, + isComplete, + renderAction: () => , + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [signPermit2State, isLoading, isConnected] + ) +} diff --git a/lib/modules/web3/useSdkViemClient.tsx b/lib/modules/web3/useSdkViemClient.tsx new file mode 100644 index 000000000..57ae3ad8a --- /dev/null +++ b/lib/modules/web3/useSdkViemClient.tsx @@ -0,0 +1,11 @@ +import { publicActions, walletActions } from 'viem' +import { useWalletClient } from 'wagmi' +import { Client, PublicActions, WalletActions } from 'viem' + +export type SdkClient = Client & WalletActions & PublicActions + +export function useSdkViemClient(): SdkClient | undefined { + const { data: walletClient } = useWalletClient() + if (!walletClient) return + return walletClient.extend(publicActions).extend(walletActions) as SdkClient +} From 7175c7c480cf28048198604eac8adf020310c37a Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 23 Sep 2024 11:35:21 +0200 Subject: [PATCH 02/27] fix: types --- .../pool/actions/add-liquidity/useAddLiquiditySteps.tsx | 5 ++--- lib/modules/relayer/signRelayerApproval.hooks.tsx | 1 - lib/modules/tokens/approvals/approval-labels.ts | 1 - .../tokens/approvals/permit2/useSignPermit2Approval.tsx | 4 ++-- lib/modules/web3/useSdkViemClient.tsx | 7 +++++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index ec2a28103..8b4414aea 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -7,12 +7,11 @@ import { getSpenderForAddLiquidity } from '@/lib/modules/tokens/token.helpers' import { useSignPermit2Step } from '@/lib/modules/transactions/transaction-steps/useSignPermit2Step' import { useSignRelayerStep } from '@/lib/modules/transactions/transaction-steps/useSignRelayerStep' import { useUserSettings } from '@/lib/modules/user/settings/UserSettingsProvider' -import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' -import { AddLiquidityBaseQueryOutput } from '@balancer/sdk' import { useMemo } from 'react' import { usePool } from '../../PoolProvider' import { requiresPermit2Approval } from '../../pool.helpers' import { LiquidityActionHelpers } from '../LiquidityActionHelpers' +import { SdkQueryAddLiquidityOutput } from './add-liquidity.types' import { AddLiquidityStepParams, useAddLiquidityStep } from './useAddLiquidityStep' type AddLiquidityStepsParams = AddLiquidityStepParams & { @@ -51,7 +50,7 @@ export function useAddLiquiditySteps({ { handler, slippagePercent: slippage, - queryOutput: simulationQuery.data as AddLiquidityBaseQueryOutput, + queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, }, chainId ) diff --git a/lib/modules/relayer/signRelayerApproval.hooks.tsx b/lib/modules/relayer/signRelayerApproval.hooks.tsx index ae8112294..b337ddd0a 100644 --- a/lib/modules/relayer/signRelayerApproval.hooks.tsx +++ b/lib/modules/relayer/signRelayerApproval.hooks.tsx @@ -1,7 +1,6 @@ import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' -import { useWalletClient } from 'wagmi' import { signRelayerApproval } from './signRelayerApproval' import { useHasApprovedRelayer } from './useHasApprovedRelayer' import { RelayerMode } from './useRelayerMode' diff --git a/lib/modules/tokens/approvals/approval-labels.ts b/lib/modules/tokens/approvals/approval-labels.ts index 6ea086a93..7fee35a0f 100644 --- a/lib/modules/tokens/approvals/approval-labels.ts +++ b/lib/modules/tokens/approvals/approval-labels.ts @@ -1,4 +1,3 @@ -import { is } from 'date-fns/locale' import { BuildTransactionLabels } from '../../web3/contracts/transactionLabels' export type ApprovalAction = diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx index dde550051..3745d86b2 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx @@ -1,8 +1,8 @@ +import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' import { AddLiquidityHandler } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { useSdkViemClient } from '@/lib/modules/web3/useSdkViemClient' import { Toast } from '@/lib/shared/components/toasts/Toast' -import { AddLiquidityBaseQueryOutput } from '@balancer/sdk' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' @@ -10,7 +10,7 @@ import { signPermit2TokenApprovals } from './signPermit2TokenApprovals' export type AddLiquidityPermit2Params = { handler: AddLiquidityHandler - queryOutput?: AddLiquidityBaseQueryOutput + queryOutput?: SdkQueryAddLiquidityOutput slippagePercent: string } export function useSignPermit2Approval({ diff --git a/lib/modules/web3/useSdkViemClient.tsx b/lib/modules/web3/useSdkViemClient.tsx index 57ae3ad8a..63bd0aaf6 100644 --- a/lib/modules/web3/useSdkViemClient.tsx +++ b/lib/modules/web3/useSdkViemClient.tsx @@ -1,8 +1,11 @@ import { publicActions, walletActions } from 'viem' import { useWalletClient } from 'wagmi' -import { Client, PublicActions, WalletActions } from 'viem' +import { Relayer } from '@balancer/sdk' -export type SdkClient = Client & WalletActions & PublicActions +// TODO: replace this type with the one exposed by the SDK (WIP) +// https://github.com/balancer/b-sdk/pull/417 +type SignRelayerApprovalParams = Parameters +export type SdkClient = SignRelayerApprovalParams[2] export function useSdkViemClient(): SdkClient | undefined { const { data: walletClient } = useWalletClient() From 7aa9c952e7925634d09bd7a925ad4fc4ab01f72f Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 23 Sep 2024 11:45:12 +0200 Subject: [PATCH 03/27] chore: rename includesNativeAsset --- lib/modules/pool/actions/LiquidityActionHelpers.ts | 2 +- .../add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/pool/actions/LiquidityActionHelpers.ts b/lib/modules/pool/actions/LiquidityActionHelpers.ts index 9bf0e4481..031676268 100644 --- a/lib/modules/pool/actions/LiquidityActionHelpers.ts +++ b/lib/modules/pool/actions/LiquidityActionHelpers.ts @@ -137,7 +137,7 @@ export class LiquidityActionHelpers { return humanAmountsIn.some(amountIn => isSameAddress(amountIn.tokenAddress, nativeAssetAddress)) } - public isNativeAssetIn2(amountsIn: TokenAmount[]): boolean { + public includesNativeAsset(amountsIn: TokenAmount[]): boolean { const nativeAssetAddress = this.networkConfig.tokens.nativeAsset.address return amountsIn.some(amountIn => isSameAddress(amountIn.token.address, nativeAssetAddress)) diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index e2c69e2bf..2f059d546 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -134,7 +134,7 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { const baseBuildCallParams = { ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), - wethIsEth: this.helpers.isNativeAssetIn2(sdkQueryOutput.amountsIn), + wethIsEth: this.helpers.includesNativeAsset(sdkQueryOutput.amountsIn), } return baseBuildCallParams } From a720db9c9a0a366c0ee0cf952c0294630af846a7 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 23 Sep 2024 18:23:02 +0200 Subject: [PATCH 04/27] feat: avoid buildcall when permit2 is nor signed --- .../[id]/add-liquidity/[[...txHash]]/layout.tsx | 1 - .../actions/add-liquidity/add-liquidity.types.ts | 8 +++++++- .../handlers/UnbalancedAddLiquidity.handler.ts | 11 ++++++++++- .../queries/useAddLiquidityBuildCallDataQuery.ts | 9 ++++++++- .../approvals/permit2/useSignPermit2Approval.tsx | 16 +++++++++++++--- test/utils/custom-renderers.tsx | 9 ++++++--- 6 files changed, 44 insertions(+), 10 deletions(-) diff --git a/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx b/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx index f18629133..b0b90e92d 100644 --- a/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx +++ b/app/(app)/pools/[chain]/[variant]/[id]/add-liquidity/[[...txHash]]/layout.tsx @@ -40,7 +40,6 @@ export default function AddLiquidityLayout({ params: { txHash }, children }: Pro return ( - {/* // TODO: do we really need a provider */} diff --git a/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts b/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts index cbb21749c..e148580e5 100644 --- a/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts +++ b/lib/modules/pool/actions/add-liquidity/add-liquidity.types.ts @@ -1,4 +1,9 @@ -import { AddLiquidityNestedQueryOutput, AddLiquidityQueryOutput, TokenAmount } from '@balancer/sdk' +import { + AddLiquidityNestedQueryOutput, + AddLiquidityQueryOutput, + Permit2, + TokenAmount, +} from '@balancer/sdk' import { Address } from 'viem' import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' @@ -17,6 +22,7 @@ export interface BuildAddLiquidityInput { slippagePercent: string queryOutput: QueryAddLiquidityOutput relayerApprovalSignature?: Address //only used by Nested Add Liquidity in signRelayer mode + permit2?: Permit2 //only used by v3 add liquidity } /* diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index 2f059d546..29fffa938 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -69,6 +69,7 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { slippagePercent, queryOutput, account, + permit2, }: SdkBuildAddLiquidityInput): Promise { const addLiquidity = new AddLiquidity() @@ -81,7 +82,14 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { account ) - const { callData, to, value } = addLiquidity.buildCall(buildCallParams) + if (this.helpers.isV3Pool() && !permit2) { + throw new Error('Permit2 signature is required for V3 pools') + } + + const { callData, to, value } = + this.helpers.isV3Pool() && permit2 + ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2) + : addLiquidity.buildCall(buildCallParams) return { account, @@ -103,6 +111,7 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { }), client: sdkClient, owner: input.account, + // nonces: input.sdkQueryOutput.amountsIn.map(a => (a.amount === 0n ? 0 : 2)), }) return signature diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts index 29aabfb31..73cd85eed 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts @@ -12,6 +12,8 @@ import { AddLiquiditySimulationQueryResult } from './useAddLiquiditySimulationQu import { useDebounce } from 'use-debounce' import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { useBlockNumber } from 'wagmi' +import { usePermit2Signature } from '@/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider' +import { isV3Pool } from '../../../pool.helpers' export type AddLiquidityBuildQueryResponse = ReturnType @@ -35,6 +37,7 @@ export function useAddLiquidityBuildCallDataQuery({ const { pool, chainId } = usePool() const { data: blockNumber } = useBlockNumber({ chainId }) const { relayerApprovalSignature } = useRelayerSignature() + const { permit2ApprovalSignature: permit2 } = usePermit2Signature() const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, defaultDebounceMs)[0] const params: AddLiquidityParams = { @@ -46,6 +49,8 @@ export function useAddLiquidityBuildCallDataQuery({ humanAmountsIn: debouncedHumanAmountsIn, } + const isValidPermit2 = isV3Pool(pool) && !!permit2 + const queryKey = addLiquidityKeys.buildCallData(params) const queryFn = async () => { @@ -56,15 +61,17 @@ export function useAddLiquidityBuildCallDataQuery({ slippagePercent: slippage, queryOutput, relayerApprovalSignature, // only present in Add Nested Liquidity with sign relayer mode + permit2, // only present in V3 pools }) console.log('Call data built:', response) + if (permit2) console.log('permit2 for call data:', permit2) return response } return useQuery({ queryKey, queryFn, - enabled: enabled && isConnected && !!simulationQuery.data, + enabled: enabled && isConnected && !!simulationQuery.data && isValidPermit2, gcTime: 0, meta: sentryMetaForAddLiquidityHandler('Error in add liquidity buildCallData query', { ...params, diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx index 3745d86b2..bbdd2c005 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/exhaustive-deps */ import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' import { AddLiquidityHandler } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' @@ -36,6 +37,15 @@ export function useSignPermit2Approval({ } }, [setSignPermit2State, sdkClient]) + //TODO: Generalize for Swaps and other potential signatures + const minimumBpt = queryOutput?.sdkQueryOutput.bptOut.amount + useEffect(() => { + if (minimumBpt) { + setPermit2ApprovalSignature(undefined) + setSignPermit2State(SignPermit2State.Ready) + } + }, [minimumBpt]) + async function signPermit2() { if (!queryOutput) throw new Error('No input provided for permit2 signature') setSignPermit2State(SignPermit2State.Confirming) @@ -91,10 +101,10 @@ function isDisabled(signPermit2State: SignPermit2State) { ) } -function isLoading(signRelayerState: SignPermit2State) { +function isLoading(signPermit2State: SignPermit2State) { return ( - signRelayerState === SignPermit2State.Confirming || - signRelayerState === SignPermit2State.Preparing + signPermit2State === SignPermit2State.Confirming || + signPermit2State === SignPermit2State.Preparing ) } diff --git a/test/utils/custom-renderers.tsx b/test/utils/custom-renderers.tsx index ff3fd8f73..ffbd11e81 100644 --- a/test/utils/custom-renderers.tsx +++ b/test/utils/custom-renderers.tsx @@ -27,6 +27,7 @@ import { aGqlPoolElementMock } from '../msw/builders/gqlPoolElement.builders' import { apolloTestClient } from './apollo-test-client' import { AppRouterContextProviderMock } from './app-router-context-provider-mock' import { testQueryClient } from './react-query' +import { Permit2SignatureProvider } from '@/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider' export type Wrapper = ({ children }: PropsWithChildren) => ReactNode @@ -106,9 +107,11 @@ export async function waitForLoadedUseQuery(hookResult: { current: { loading: bo export const DefaultAddLiquidityTestProvider = ({ children }: PropsWithChildren) => ( - - {children} - + + + {children} + + ) From 863ea60b34ea6d7f0634656acde682a490a7b79c Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 25 Sep 2024 11:09:19 +0200 Subject: [PATCH 05/27] chore: bump sdk to v0.26.1 --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 88eaa8dc3..bb324beda 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@apollo/client": "^3.11.8", - "@balancer/sdk": "^0.26.0", + "@balancer/sdk": "^0.26.1", "@chakra-ui/anatomy": "^2.2.2", "@chakra-ui/hooks": "^2.2.1", "@chakra-ui/icons": "^2.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f032dfd2..8b9558c0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.11.8 version: 3.11.8(@types/react@18.2.34)(graphql-ws@5.14.1(graphql@16.8.1))(graphql@16.8.1)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@balancer/sdk': - specifier: ^0.26.0 - version: 0.26.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4) + specifier: ^0.26.1 + version: 0.26.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4) '@chakra-ui/anatomy': specifier: ^2.2.2 version: 2.2.2 @@ -1311,8 +1311,8 @@ packages: resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} engines: {node: '>=6.9.0'} - '@balancer/sdk@0.26.0': - resolution: {integrity: sha512-FgYUiCCL1vL1r7qI6rQFZLc+3o0uvBAUYNo2sYtyFLfEQEJLI6WRlx0KfdNR361rcW9Ra4emDw8wEHY5GJ+I6w==} + '@balancer/sdk@0.26.1': + resolution: {integrity: sha512-RH4XKjtX2itE1SwDz5SZUOuO67XRcFVwCGysef6nh/iPFEf0qYdDAFAu5ms+V6ZXyVq3OmjDUrkarfDKrKkGhw==} engines: {node: '>=18.x'} '@bcoe/v8-coverage@0.2.3': @@ -10667,7 +10667,7 @@ snapshots: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 - '@balancer/sdk@0.26.0(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4)': + '@balancer/sdk@0.26.1(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: decimal.js-light: 2.5.1 lodash.clonedeep: 4.5.0 From d4f70d1824c917e2a5519b64cdba688b6556a47c Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 25 Sep 2024 18:35:38 +0200 Subject: [PATCH 06/27] fix: add nonces query --- app/(app)/debug/page.tsx | 3 + app/(app)/debug/permit2-allowance/page.tsx | 63 +++++++ lib/config/config.types.ts | 2 + lib/config/networks/sepolia.ts | 1 + .../handlers/AddLiquidity.handler.ts | 10 +- ...edAddLiquidity.handler.integration.spec.ts | 5 +- .../UnbalancedAddLiquidity.handler.ts | 21 ++- .../UnbalancedAddLiquidity.handlerLOCURA.ts | 157 ++++++++++++++++++ .../add-liquidity/useAddLiquiditySteps.tsx | 3 +- .../relayer/signRelayerApproval.hooks.tsx | 4 +- lib/modules/relayer/signRelayerApproval.ts | 5 +- ...ovals.tsx => signPermit2TokenTransfer.tsx} | 14 +- .../approvals/permit2/usePermit2Nonces.tsx | 52 ++++++ ...pproval.tsx => useSignPermit2Transfer.tsx} | 15 +- .../transaction-steps/useSignPermit2Step.tsx | 32 +++- lib/modules/web3/useSdkViemClient.tsx | 11 +- 16 files changed, 355 insertions(+), 43 deletions(-) create mode 100644 app/(app)/debug/permit2-allowance/page.tsx create mode 100644 lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts rename lib/modules/tokens/approvals/permit2/{signPermit2TokenApprovals.tsx => signPermit2TokenTransfer.tsx} (72%) create mode 100644 lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx rename lib/modules/tokens/approvals/permit2/{useSignPermit2Approval.tsx => useSignPermit2Transfer.tsx} (88%) diff --git a/app/(app)/debug/page.tsx b/app/(app)/debug/page.tsx index 2a9d617d1..75f8d7ba0 100644 --- a/app/(app)/debug/page.tsx +++ b/app/(app)/debug/page.tsx @@ -114,6 +114,9 @@ export default function Debug() { Remove allowance + + Permit2 allowance + ) diff --git a/app/(app)/debug/permit2-allowance/page.tsx b/app/(app)/debug/permit2-allowance/page.tsx new file mode 100644 index 000000000..7513f5d72 --- /dev/null +++ b/app/(app)/debug/permit2-allowance/page.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { Center, Input, Text, VStack } from '@chakra-ui/react' +import { useState } from 'react' +import { Address } from 'viem' +import { sepolia } from 'viem/chains' +import { useReadContract } from 'wagmi' +import { fNum } from '@/lib/shared/utils/numbers' +import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config' +import { permit2Abi } from '@balancer/sdk' + +export default function Page() { + const [tokenAddress, setTokenAddress] = useState
('' as Address) + + const { chain, userAddress } = useUserAccount() + + const chainId = chain?.id || sepolia.id + + const { data } = usePermit2Allowance({ chainId, tokenAddress, owner: userAddress }) + + return ( +
+ + + Enter address of token to check permit2 allowance in the current chain:{' '} + {chain ? chain.name : 'None'} + + setTokenAddress(e.target.value as Address)} /> + + {data && ( +
+
Amount: {fNum('integer', data[0])}
+
Expires: {data[1]}
+
Nonce: {data[2]}
+
+ )} +
+
+ ) +} + +type Params = { + chainId: number + tokenAddress: Address + owner: Address +} +function usePermit2Allowance({ chainId, tokenAddress, owner }: Params) { + const permit2Address = '0x000000000022D473030F116dDEE9F6B43aC78BA3' + const balancerRouter = getNetworkConfig(getGqlChain(chainId)).contracts.balancer.router! + const spender = balancerRouter + + return useReadContract({ + chainId, + address: permit2Address, + abi: permit2Abi, + functionName: 'allowance', + args: [owner, tokenAddress, spender], + query: { + enabled: !!tokenAddress && !!owner, + }, + }) +} diff --git a/lib/config/config.types.ts b/lib/config/config.types.ts index 71e273c2d..d8a4d5296 100644 --- a/lib/config/config.types.ts +++ b/lib/config/config.types.ts @@ -37,6 +37,8 @@ export interface ContractsConfig { vaultV2: Address // TODO: make it required when v3 is deployed in all networks vaultV3?: Address + // TODO: make it required when v3 is deployed in all networks + router?: Address relayerV6: Address minter: Address } diff --git a/lib/config/networks/sepolia.ts b/lib/config/networks/sepolia.ts index 97805ca46..c8ace9f12 100644 --- a/lib/config/networks/sepolia.ts +++ b/lib/config/networks/sepolia.ts @@ -32,6 +32,7 @@ const networkConfig: NetworkConfig = { balancer: { vaultV2: '0xBA12222222228d8Ba445958a75a0704d566BF2C8', vaultV3: '0x0EF1c156a7986F394d90eD1bEeA6483Cc435F542', + router: '0xB12FcB422aAe6720f882E22C340964a7723f2387', relayerV6: '0x7852fB9d0895e6e8b3EedA553c03F6e2F9124dF9', minter: '0x1783Cd84b3d01854A96B4eD5843753C2CcbD574A', }, diff --git a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts index ad595fb91..6365c4f92 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts @@ -1,9 +1,9 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' -import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' -import { AddLiquidityQueryOutput, Permit2 } from '@balancer/sdk' +import { AddLiquidityQueryOutput, Permit2, PublicWalletClient } from '@balancer/sdk' import { Address } from 'viem' import { BuildAddLiquidityInput, QueryAddLiquidityOutput } from '../add-liquidity.types' +import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' export interface Permit2AddLiquidityInput { account: Address @@ -40,5 +40,9 @@ export interface AddLiquidityHandler { /* Sign permit2 for adding liquidity (for now only used by v3 pools) TODO: generalize for other handlers using permit2 */ - signPermit2?(input: Permit2AddLiquidityInput, walletClient: SdkClient): Promise + signPermit2?( + input: Permit2AddLiquidityInput, + walletClient: PublicWalletClient, + nonces?: NoncesByTokenAddress + ): Promise } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts index 4cc1ad0b4..13ea19e42 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.integration.spec.ts @@ -7,6 +7,8 @@ import { UnbalancedAddLiquidityHandler } from './UnbalancedAddLiquidity.handler' import { selectAddLiquidityHandler } from './selectAddLiquidityHandler' import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { Pool } from '../../../PoolProvider' +import { getNetworkConfig } from '@/lib/config/app.config' +import { GqlChain } from '@/lib/shared/services/api/generated/graphql' function selectUnbalancedHandler() { return selectAddLiquidityHandler(aWjAuraWethPoolElementMock()) as UnbalancedAddLiquidityHandler @@ -110,7 +112,8 @@ describe.skip('When adding unbalanced liquidity for a V3 pool', async () => { queryOutput, }) - const sepoliaRouter = '0xB12FcB422aAe6720f882E22C340964a7723f2387' + const sepoliaRouter = getNetworkConfig(GqlChain.Sepolia).contracts.balancer.router + expect(result.to).toBe(sepoliaRouter) expect(result.data).toBeDefined() }) diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index 29fffa938..74376789b 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -2,7 +2,6 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' import { getRpcUrl } from '@/lib/modules/web3/transports' -import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' import { AddLiquidity, AddLiquidityBaseBuildCallInput, @@ -13,6 +12,7 @@ import { Permit2Helper, PriceImpact, PriceImpactAmount, + PublicWalletClient, Slippage, } from '@balancer/sdk' import { Pool } from '../../../PoolProvider' @@ -23,6 +23,7 @@ import { } from '../../LiquidityActionHelpers' import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' import { AddLiquidityHandler, Permit2AddLiquidityInput } from './AddLiquidity.handler' +import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' /** * UnbalancedAddLiquidityHandler is a handler that implements the @@ -102,18 +103,19 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { public async signPermit2( input: Permit2AddLiquidityInput, - sdkClient: SdkClient + sdkClient: PublicWalletClient, + nonces: NoncesByTokenAddress ): Promise { + const baseInput = this.constructBaseBuildCallInput({ + slippagePercent: input.slippagePercent, + sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, + }) const signature = await Permit2Helper.signAddLiquidityApproval({ - ...this.constructBaseBuildCallInput({ - slippagePercent: input.slippagePercent, - sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, - }), + ...baseInput, client: sdkClient, owner: input.account, - // nonces: input.sdkQueryOutput.amountsIn.map(a => (a.amount === 0n ? 0 : 2)), + nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), }) - return signature } @@ -145,6 +147,9 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), wethIsEth: this.helpers.includesNativeAsset(sdkQueryOutput.amountsIn), } + // baseBuildCallParams.amountsIn = baseBuildCallParams.amountsIn.filter( + // amountIn => amountIn.amount > 0n + // ) return baseBuildCallParams } } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts new file mode 100644 index 000000000..adb6a2371 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts @@ -0,0 +1,157 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { getRpcUrl } from '@/lib/modules/web3/transports' +import { + AddLiquidity, + AddLiquidityBaseBuildCallInput, + AddLiquidityBaseQueryOutput, + AddLiquidityKind, + AddLiquidityUnbalancedInput, + Permit2, + Permit2Helper, + PriceImpact, + PriceImpactAmount, + PublicWalletClient, + Slippage, +} from '@balancer/sdk' +import { Pool } from '../../../PoolProvider' +import { + LiquidityActionHelpers, + areEmptyAmounts, + formatBuildCallParams, +} from '../../LiquidityActionHelpers' +import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' +import { AddLiquidityHandler, Permit2AddLiquidityInput } from './AddLiquidity.handler' +import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' + +/** + * UnbalancedAddLiquidityHandler is a handler that implements the + * AddLiquidityHandler interface for unbalanced adds, e.g. where the user + * specifies the token amounts in. It uses the Balancer SDK to implement it's + * methods. It also handles the case where one of the input tokens is the native + * asset instead of the wrapped native asset. + */ +export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { + helpers: LiquidityActionHelpers + + constructor(pool: Pool) { + this.helpers = new LiquidityActionHelpers(pool) + } + + public async simulate( + humanAmountsIn: HumanTokenAmountWithAddress[] + ): Promise { + const addLiquidity = new AddLiquidity() + const addLiquidityInput = this.constructSdkInput(humanAmountsIn) + + const sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.poolState) + + return { bptOut: sdkQueryOutput.bptOut, sdkQueryOutput } + } + + public async getPriceImpact(humanAmountsIn: HumanTokenAmountWithAddress[]): Promise { + if (areEmptyAmounts(humanAmountsIn)) { + // Avoid price impact calculation when there are no amounts in + return 0 + } + + const addLiquidityInput = this.constructSdkInput(humanAmountsIn) + + const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalanced( + addLiquidityInput, + this.helpers.poolState + ) + + return priceImpactABA.decimal + } + + public async buildCallData({ + slippagePercent, + queryOutput, + account, + permit2, + }: SdkBuildAddLiquidityInput): Promise { + const addLiquidity = new AddLiquidity() + + const buildCallParams = formatBuildCallParams( + this.constructBaseBuildCallInput({ + sdkQueryOutput: queryOutput.sdkQueryOutput, + slippagePercent: slippagePercent, + }), + this.helpers.isV3Pool(), + account + ) + + if (this.helpers.isV3Pool() && !permit2) { + throw new Error('Permit2 signature is required for V3 pools') + } + + const { callData, to, value } = + this.helpers.isV3Pool() && permit2 + ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2) + : addLiquidity.buildCall(buildCallParams) + + return { + account, + chainId: this.helpers.chainId, + data: callData, + to, + value, + } + } + + public async signPermit2( + input: Permit2AddLiquidityInput, + sdkClient: PublicWalletClient, + nonces?: NoncesByTokenAddress + ): Promise { + // console.log('tengo los nonces super ricos') + const baseInput = this.constructBaseBuildCallInput({ + slippagePercent: input.slippagePercent, + sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, + }) + const signature = await Permit2Helper.signAddLiquidityApproval({ + ...baseInput, + client: sdkClient, + owner: input.account, + nonces: baseInput.amountsIn.map(a => (a.amount === 0n ? 3 : 1)), + }) + + return signature + } + + /** + * PRIVATE METHODS + */ + private constructSdkInput( + humanAmountsIn: HumanTokenAmountWithAddress[] + ): AddLiquidityUnbalancedInput { + const amountsIn = this.helpers.toSdkInputAmounts(humanAmountsIn) + + return { + chainId: this.helpers.chainId, + rpcUrl: getRpcUrl(this.helpers.chainId), + amountsIn, + kind: AddLiquidityKind.Unbalanced, + } + } + + public constructBaseBuildCallInput({ + slippagePercent, + sdkQueryOutput, + }: { + slippagePercent: string + sdkQueryOutput: AddLiquidityBaseQueryOutput + }): AddLiquidityBaseBuildCallInput { + const baseBuildCallParams = { + ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + wethIsEth: this.helpers.includesNativeAsset(sdkQueryOutput.amountsIn), + } + baseBuildCallParams.amountsIn = baseBuildCallParams.amountsIn.filter( + amountIn => amountIn.amount > 0n + ) + return baseBuildCallParams + } +} diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index 8b4414aea..ad6a0cc97 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -52,7 +52,8 @@ export function useAddLiquiditySteps({ slippagePercent: slippage, queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, }, - chainId + chainId, + requiresPermit2Approval(pool) ) const addLiquidityStep = useAddLiquidityStep({ diff --git a/lib/modules/relayer/signRelayerApproval.hooks.tsx b/lib/modules/relayer/signRelayerApproval.hooks.tsx index b337ddd0a..21ba1fe51 100644 --- a/lib/modules/relayer/signRelayerApproval.hooks.tsx +++ b/lib/modules/relayer/signRelayerApproval.hooks.tsx @@ -7,7 +7,7 @@ import { RelayerMode } from './useRelayerMode' import { SignRelayerState, useRelayerSignature } from './RelayerSignatureProvider' import { SupportedChainId } from '@/lib/config/config.types' import { Toast } from '@/lib/shared/components/toasts/Toast' -import { useSdkViemClient } from '../web3/useSdkViemClient' +import { useSdkWalletClient } from '../web3/useSdkViemClient' export function useShouldSignRelayerApproval(chainId: SupportedChainId, relayerMode: RelayerMode) { const { hasApprovedRelayer } = useHasApprovedRelayer(chainId) @@ -23,7 +23,7 @@ export function useSignRelayerApproval(chainId: SupportedChainId) { const [error, setError] = useState() - const sdkClient = useSdkViemClient() + const sdkClient = useSdkWalletClient() useEffect(() => { if (sdkClient === undefined) { diff --git a/lib/modules/relayer/signRelayerApproval.ts b/lib/modules/relayer/signRelayerApproval.ts index 86888801f..a808d1208 100644 --- a/lib/modules/relayer/signRelayerApproval.ts +++ b/lib/modules/relayer/signRelayerApproval.ts @@ -1,14 +1,13 @@ import { getNetworkConfig } from '@/lib/config/app.config' import { SupportedChainId } from '@/lib/config/config.types' import { ensureError } from '@/lib/shared/utils/errors' -import { Relayer } from '@balancer/sdk' +import { PublicWalletClient, Relayer } from '@balancer/sdk' import { Address } from 'viem' -import { SdkClient } from '../web3/useSdkViemClient' export async function signRelayerApproval( userAddress: Address, chainId: SupportedChainId, - client?: SdkClient + client?: PublicWalletClient ): Promise
{ if (!client) return undefined diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx similarity index 72% rename from lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx rename to lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx index f78c48172..672d08f64 100644 --- a/lib/modules/tokens/approvals/permit2/signPermit2TokenApprovals.tsx +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx @@ -2,25 +2,27 @@ import { AddLiquidityHandler, Permit2AddLiquidityInput, } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' -import { SdkClient } from '@/lib/modules/web3/useSdkViemClient' import { ensureError } from '@/lib/shared/utils/errors' -import { Permit2 } from '@balancer/sdk' +import { Permit2, PublicWalletClient } from '@balancer/sdk' +import { NoncesByTokenAddress } from './usePermit2Nonces' type SignPermit2Params = { - handler: AddLiquidityHandler //TODO: maybe more handlers??? - sdkClient?: SdkClient + handler: AddLiquidityHandler //TODO: generalize to other handlers? + sdkClient?: PublicWalletClient permit2Input: Permit2AddLiquidityInput + nonces: NoncesByTokenAddress } -export async function signPermit2TokenApprovals({ +export async function signPermit2TokenTransfer({ handler, sdkClient, permit2Input, + nonces, }: SignPermit2Params): Promise { if (!sdkClient) return undefined try { if (!handler.signPermit2) throw new Error('Handler does not implement signPermit2 method') - const signature = await handler.signPermit2(permit2Input, sdkClient) + const signature = await handler.signPermit2(permit2Input, sdkClient, nonces) return signature } catch (e: unknown) { const error = ensureError(e) diff --git a/lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx b/lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx new file mode 100644 index 000000000..9003de602 --- /dev/null +++ b/lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx @@ -0,0 +1,52 @@ +import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config' +import { permit2Abi } from '@balancer/sdk' +import { zipObject } from 'lodash' +import { Address } from 'viem' +import { useReadContracts } from 'wagmi' + +export type NoncesByTokenAddress = Record + +type Params = { + chainId: number + tokenAddresses?: Address[] + owner?: Address + enabled: boolean +} +export function usePermit2Nonces({ chainId, tokenAddresses, owner, enabled }: Params) { + const networkConfig = getNetworkConfig(getGqlChain(chainId)) + const permit2Address = networkConfig.contracts.permit2! + const balancerRouter = networkConfig.contracts.balancer.router! + const spender = balancerRouter + + const contracts = tokenAddresses?.map( + tokenAddress => + ({ + chainId, + address: permit2Address, + abi: permit2Abi, + functionName: 'allowance', + args: [owner, tokenAddress, spender], + } as const) + ) + + const { data, isLoading } = useReadContracts({ + contracts, + allowFailure: false, + query: { + enabled: enabled && tokenAddresses && tokenAddresses.length > 0 && !!owner, + }, + }) + + const nonces: NoncesByTokenAddress | undefined = + tokenAddresses && data + ? zipObject( + tokenAddresses, + data.map(result => result[2]) + ) + : undefined + + return { + isLoadingNonces: isLoading, + nonces, + } +} diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx similarity index 88% rename from lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx rename to lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index bbdd2c005..392b2a32f 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Approval.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -2,22 +2,25 @@ import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' import { AddLiquidityHandler } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' -import { useSdkViemClient } from '@/lib/modules/web3/useSdkViemClient' +import { useSdkWalletClient } from '@/lib/modules/web3/useSdkViemClient' import { Toast } from '@/lib/shared/components/toasts/Toast' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' -import { signPermit2TokenApprovals } from './signPermit2TokenApprovals' +import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' +import { NoncesByTokenAddress } from './usePermit2Nonces' export type AddLiquidityPermit2Params = { handler: AddLiquidityHandler queryOutput?: SdkQueryAddLiquidityOutput slippagePercent: string + nonces?: NoncesByTokenAddress } -export function useSignPermit2Approval({ +export function useSignPermit2Transfer({ queryOutput, slippagePercent, handler, + nonces, }: AddLiquidityPermit2Params) { const toast = useToast() const { userAddress } = useUserAccount() @@ -27,7 +30,7 @@ export function useSignPermit2Approval({ const [error, setError] = useState() - const sdkClient = useSdkViemClient() + const sdkClient = useSdkWalletClient() useEffect(() => { if (sdkClient === undefined) { @@ -48,11 +51,12 @@ export function useSignPermit2Approval({ async function signPermit2() { if (!queryOutput) throw new Error('No input provided for permit2 signature') + if (!nonces) throw new Error('No nonces provided for permit2 signature') setSignPermit2State(SignPermit2State.Confirming) setError(undefined) try { - const signature = await signPermit2TokenApprovals({ + const signature = await signPermit2TokenTransfer({ handler, sdkClient, permit2Input: { @@ -60,6 +64,7 @@ export function useSignPermit2Approval({ slippagePercent, sdkQueryOutput: queryOutput.sdkQueryOutput, }, + nonces, }) if (signature) { diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index bda801196..5d9fda51f 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -8,23 +8,43 @@ import { useMemo } from 'react' import { SignPermit2State } from '../../tokens/approvals/permit2/Permit2SignatureProvider' import { AddLiquidityPermit2Params, - useSignPermit2Approval, -} from '../../tokens/approvals/permit2/useSignPermit2Approval' + useSignPermit2Transfer, +} from '../../tokens/approvals/permit2/useSignPermit2Transfer' import { useChainSwitch } from '../../web3/useChainSwitch' import { TransactionStep } from './lib' +import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' export const signRelayerStepTitle = 'Sign relayer' export function useSignPermit2Step( params: AddLiquidityPermit2Params, - chainId: number + chainId: number, + enabled = false ): TransactionStep { - const { isConnected } = useUserAccount() - const { signPermit2, signPermit2State, isLoading, isDisabled, buttonLabel, error } = - useSignPermit2Approval(params) + const { isConnected, userAddress } = useUserAccount() + + //TODO: Move this hook into useSignPermit2Transfer? + //TODO: isLoading state depending on amountsIn (simulation loaded)? + const { isLoadingNonces, nonces } = usePermit2Nonces({ + chainId, + tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), + owner: userAddress, + enabled, + }) + + const { + signPermit2, + signPermit2State, + isLoading: isLoadingTransfer, + isDisabled, + buttonLabel, + error, + } = useSignPermit2Transfer({ ...params, nonces }) const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = useChainSwitch(chainId) + const isLoading = isLoadingTransfer || isLoadingNonces + const SignPermitButton = () => ( {error && } diff --git a/lib/modules/web3/useSdkViemClient.tsx b/lib/modules/web3/useSdkViemClient.tsx index 63bd0aaf6..0d87204ac 100644 --- a/lib/modules/web3/useSdkViemClient.tsx +++ b/lib/modules/web3/useSdkViemClient.tsx @@ -1,14 +1,9 @@ import { publicActions, walletActions } from 'viem' import { useWalletClient } from 'wagmi' -import { Relayer } from '@balancer/sdk' +import { PublicWalletClient } from '@balancer/sdk' -// TODO: replace this type with the one exposed by the SDK (WIP) -// https://github.com/balancer/b-sdk/pull/417 -type SignRelayerApprovalParams = Parameters -export type SdkClient = SignRelayerApprovalParams[2] - -export function useSdkViemClient(): SdkClient | undefined { +export function useSdkWalletClient(): PublicWalletClient | undefined { const { data: walletClient } = useWalletClient() if (!walletClient) return - return walletClient.extend(publicActions).extend(walletActions) as SdkClient + return walletClient.extend(publicActions).extend(walletActions) as PublicWalletClient } From 8666be664b667f6854125739cc550bdd1e6ec64d Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 25 Sep 2024 18:54:46 +0200 Subject: [PATCH 07/27] fix: remove old handler --- .../UnbalancedAddLiquidity.handlerLOCURA.ts | 157 ------------------ 1 file changed, 157 deletions(-) delete mode 100644 lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts deleted file mode 100644 index adb6a2371..000000000 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handlerLOCURA.ts +++ /dev/null @@ -1,157 +0,0 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' -import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' -import { getRpcUrl } from '@/lib/modules/web3/transports' -import { - AddLiquidity, - AddLiquidityBaseBuildCallInput, - AddLiquidityBaseQueryOutput, - AddLiquidityKind, - AddLiquidityUnbalancedInput, - Permit2, - Permit2Helper, - PriceImpact, - PriceImpactAmount, - PublicWalletClient, - Slippage, -} from '@balancer/sdk' -import { Pool } from '../../../PoolProvider' -import { - LiquidityActionHelpers, - areEmptyAmounts, - formatBuildCallParams, -} from '../../LiquidityActionHelpers' -import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' -import { AddLiquidityHandler, Permit2AddLiquidityInput } from './AddLiquidity.handler' -import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' - -/** - * UnbalancedAddLiquidityHandler is a handler that implements the - * AddLiquidityHandler interface for unbalanced adds, e.g. where the user - * specifies the token amounts in. It uses the Balancer SDK to implement it's - * methods. It also handles the case where one of the input tokens is the native - * asset instead of the wrapped native asset. - */ -export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { - helpers: LiquidityActionHelpers - - constructor(pool: Pool) { - this.helpers = new LiquidityActionHelpers(pool) - } - - public async simulate( - humanAmountsIn: HumanTokenAmountWithAddress[] - ): Promise { - const addLiquidity = new AddLiquidity() - const addLiquidityInput = this.constructSdkInput(humanAmountsIn) - - const sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.poolState) - - return { bptOut: sdkQueryOutput.bptOut, sdkQueryOutput } - } - - public async getPriceImpact(humanAmountsIn: HumanTokenAmountWithAddress[]): Promise { - if (areEmptyAmounts(humanAmountsIn)) { - // Avoid price impact calculation when there are no amounts in - return 0 - } - - const addLiquidityInput = this.constructSdkInput(humanAmountsIn) - - const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalanced( - addLiquidityInput, - this.helpers.poolState - ) - - return priceImpactABA.decimal - } - - public async buildCallData({ - slippagePercent, - queryOutput, - account, - permit2, - }: SdkBuildAddLiquidityInput): Promise { - const addLiquidity = new AddLiquidity() - - const buildCallParams = formatBuildCallParams( - this.constructBaseBuildCallInput({ - sdkQueryOutput: queryOutput.sdkQueryOutput, - slippagePercent: slippagePercent, - }), - this.helpers.isV3Pool(), - account - ) - - if (this.helpers.isV3Pool() && !permit2) { - throw new Error('Permit2 signature is required for V3 pools') - } - - const { callData, to, value } = - this.helpers.isV3Pool() && permit2 - ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2) - : addLiquidity.buildCall(buildCallParams) - - return { - account, - chainId: this.helpers.chainId, - data: callData, - to, - value, - } - } - - public async signPermit2( - input: Permit2AddLiquidityInput, - sdkClient: PublicWalletClient, - nonces?: NoncesByTokenAddress - ): Promise { - // console.log('tengo los nonces super ricos') - const baseInput = this.constructBaseBuildCallInput({ - slippagePercent: input.slippagePercent, - sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, - }) - const signature = await Permit2Helper.signAddLiquidityApproval({ - ...baseInput, - client: sdkClient, - owner: input.account, - nonces: baseInput.amountsIn.map(a => (a.amount === 0n ? 3 : 1)), - }) - - return signature - } - - /** - * PRIVATE METHODS - */ - private constructSdkInput( - humanAmountsIn: HumanTokenAmountWithAddress[] - ): AddLiquidityUnbalancedInput { - const amountsIn = this.helpers.toSdkInputAmounts(humanAmountsIn) - - return { - chainId: this.helpers.chainId, - rpcUrl: getRpcUrl(this.helpers.chainId), - amountsIn, - kind: AddLiquidityKind.Unbalanced, - } - } - - public constructBaseBuildCallInput({ - slippagePercent, - sdkQueryOutput, - }: { - slippagePercent: string - sdkQueryOutput: AddLiquidityBaseQueryOutput - }): AddLiquidityBaseBuildCallInput { - const baseBuildCallParams = { - ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), - slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), - wethIsEth: this.helpers.includesNativeAsset(sdkQueryOutput.amountsIn), - } - baseBuildCallParams.amountsIn = baseBuildCallParams.amountsIn.filter( - amountIn => amountIn.amount > 0n - ) - return baseBuildCallParams - } -} From 0125d2dcf212ca545d676cf1b2383e3bd8805ceb Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 25 Sep 2024 19:01:36 +0200 Subject: [PATCH 08/27] chore: rename permit2 transfer functions --- .../queries/useAddLiquidityBuildCallDataQuery.ts | 2 +- .../tokens/approvals/permit2/Permit2SignatureProvider.tsx | 6 +++--- .../tokens/approvals/permit2/useSignPermit2Transfer.tsx | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts index 73cd85eed..8bb745f67 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts @@ -37,7 +37,7 @@ export function useAddLiquidityBuildCallDataQuery({ const { pool, chainId } = usePool() const { data: blockNumber } = useBlockNumber({ chainId }) const { relayerApprovalSignature } = useRelayerSignature() - const { permit2ApprovalSignature: permit2 } = usePermit2Signature() + const { permit2TransferSignature: permit2 } = usePermit2Signature() const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, defaultDebounceMs)[0] const params: AddLiquidityParams = { diff --git a/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx index 31a0251b4..2ed9c6e47 100644 --- a/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx +++ b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx @@ -15,15 +15,15 @@ export type UsePermit2SignatureResponse = ReturnType(null) export function _usePermit2Signature() { - const [permit2ApprovalSignature, setPermit2ApprovalSignature] = useState() + const [permit2TransferSignature, setPermit2TransferSignature] = useState() const [signPermit2State, setSignPermit2State] = useState( SignPermit2State.Preparing ) return { - permit2ApprovalSignature, - setPermit2ApprovalSignature, + permit2TransferSignature, + setPermit2TransferSignature, signPermit2State, setSignPermit2State, } diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 392b2a32f..08fcc23d7 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -25,7 +25,7 @@ export function useSignPermit2Transfer({ const toast = useToast() const { userAddress } = useUserAccount() - const { setSignPermit2State, setPermit2ApprovalSignature, signPermit2State } = + const { setSignPermit2State, setPermit2TransferSignature, signPermit2State } = usePermit2Signature() const [error, setError] = useState() @@ -44,7 +44,7 @@ export function useSignPermit2Transfer({ const minimumBpt = queryOutput?.sdkQueryOutput.bptOut.amount useEffect(() => { if (minimumBpt) { - setPermit2ApprovalSignature(undefined) + setPermit2TransferSignature(undefined) setSignPermit2State(SignPermit2State.Ready) } }, [minimumBpt]) @@ -81,7 +81,7 @@ export function useSignPermit2Transfer({ setSignPermit2State(SignPermit2State.Ready) } - setPermit2ApprovalSignature(signature) + setPermit2TransferSignature(signature) } catch (error) { console.error(error) setError('Error in permit2 signature call') From 61b1563bd3e85214aa6ff3496653f2435c54c439 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Thu, 26 Sep 2024 10:08:48 +0200 Subject: [PATCH 09/27] chore: add token symbols to permit2 transfer label --- .../permit2/useSignPermit2Transfer.tsx | 22 ++++++++++++++++--- .../transaction-steps/useSignPermit2Step.tsx | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 08fcc23d7..7ae2e0d11 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -9,14 +9,17 @@ import { useEffect, useState } from 'react' import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' import { NoncesByTokenAddress } from './usePermit2Nonces' +import { useTokens } from '../../TokensProvider' export type AddLiquidityPermit2Params = { + chainId: number handler: AddLiquidityHandler queryOutput?: SdkQueryAddLiquidityOutput slippagePercent: string nonces?: NoncesByTokenAddress } export function useSignPermit2Transfer({ + chainId, queryOutput, slippagePercent, handler, @@ -24,6 +27,13 @@ export function useSignPermit2Transfer({ }: AddLiquidityPermit2Params) { const toast = useToast() const { userAddress } = useUserAccount() + const { getToken } = useTokens() + + //TODO: We will probably need to extract this logic to be reusable by other components (StepTracker) + const amountsIn = queryOutput?.sdkQueryOutput.amountsIn + const tokenSymbols = amountsIn + ?.filter(a => a.amount > 0n) + .map(a => getToken(a.token.address, chainId)?.symbol) const { setSignPermit2State, setPermit2TransferSignature, signPermit2State } = usePermit2Signature() @@ -92,7 +102,7 @@ export function useSignPermit2Transfer({ return { signPermit2, signPermit2State, - buttonLabel: getButtonLabel(signPermit2State), + buttonLabel: getButtonLabel(signPermit2State, tokenSymbols), isLoading: isLoading(signPermit2State) || !queryOutput, isDisabled: isDisabled(signPermit2State), error, @@ -113,10 +123,16 @@ function isLoading(signPermit2State: SignPermit2State) { ) } -function getButtonLabel(signPermit2State: SignPermit2State) { - if (signPermit2State === SignPermit2State.Ready) return 'Permit transfer: token name' +function getButtonLabel(signPermit2State: SignPermit2State, tokenSymbols?: (string | undefined)[]) { + if (signPermit2State === SignPermit2State.Ready) return getReadyLabel(tokenSymbols) if (signPermit2State === SignPermit2State.Confirming) return 'Confirm permit2 signature in wallet' if (signPermit2State === SignPermit2State.Preparing) return 'Preparing' if (signPermit2State === SignPermit2State.Completed) return 'Permit2 Signed' return '' } + +function getReadyLabel(tokenSymbols?: (string | undefined)[]) { + if (!tokenSymbols) return 'Permit transfer' + if (tokenSymbols.length === 1) return 'Permit transfer: ' + tokenSymbols[0] + return 'Permit transfers: ' + tokenSymbols.join(', ') +} diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index 5d9fda51f..c7651971f 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -39,7 +39,7 @@ export function useSignPermit2Step( isDisabled, buttonLabel, error, - } = useSignPermit2Transfer({ ...params, nonces }) + } = useSignPermit2Transfer({ ...params, chainId, nonces }) const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = useChainSwitch(chainId) From aaafbbd3e3ad4bb3cc3708f68d5aa03272d698da Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Thu, 26 Sep 2024 10:21:00 +0200 Subject: [PATCH 10/27] chore: missing parameter --- lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index ad6a0cc97..785268ffa 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -48,6 +48,7 @@ export function useAddLiquiditySteps({ const signPermit2Step = useSignPermit2Step( { + chainId, handler, slippagePercent: slippage, queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, From cbec43a42e1cdf333b05588bf1c68ae03ce305fb Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Fri, 27 Sep 2024 13:40:07 +0200 Subject: [PATCH 11/27] refactor: extract base abstract class --- .../pool/actions/LiquidityActionHelpers.ts | 11 +- .../handlers/AddLiquidity.handler.ts | 12 +- .../BaseUnbalancedAddLiquidity.handler.ts | 75 ++++++++++ .../UnbalancedAddLiquidity.handler.ts | 134 ++---------------- .../UnbalancedAddLiquidityV3.handler.ts | 46 ++++++ .../handlers/selectAddLiquidityHandler.ts | 3 + .../add-liquidity/handlers/v3Helpers.ts | 37 +++++ .../add-liquidity/useAddLiquiditySteps.tsx | 15 +- .../ProportionalRemoveLiquidity.handler.ts | 10 +- .../SingleTokenRemoveLiquidity.handler.ts | 14 +- .../permit2/signPermit2TokenTransfer.tsx | 48 +++++-- .../permit2/useSignPermit2Transfer.tsx | 15 +- .../transaction-steps/useSignPermit2Step.tsx | 14 +- 13 files changed, 245 insertions(+), 189 deletions(-) create mode 100644 lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts create mode 100644 lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts create mode 100644 lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts diff --git a/lib/modules/pool/actions/LiquidityActionHelpers.ts b/lib/modules/pool/actions/LiquidityActionHelpers.ts index 031676268..a1d0564ef 100644 --- a/lib/modules/pool/actions/LiquidityActionHelpers.ts +++ b/lib/modules/pool/actions/LiquidityActionHelpers.ts @@ -137,12 +137,6 @@ export class LiquidityActionHelpers { return humanAmountsIn.some(amountIn => isSameAddress(amountIn.tokenAddress, nativeAssetAddress)) } - public includesNativeAsset(amountsIn: TokenAmount[]): boolean { - const nativeAssetAddress = this.networkConfig.tokens.nativeAsset.address - - return amountsIn.some(amountIn => isSameAddress(amountIn.token.address, nativeAssetAddress)) - } - public isNativeAsset(tokenAddress: Address): boolean { const nativeAssetAddress = this.networkConfig.tokens.nativeAsset.address @@ -319,10 +313,7 @@ export function hasNoLiquidity(pool: Pool): boolean { } // When the pool has version < v3, it adds extra buildCall params (sender and recipient) that must be present only in V1/V2 -export function formatBuildCallParams(buildCallParams: T, isV3Pool: boolean, account: Address) { - // sender must be undefined for v3 pools - if (isV3Pool) return buildCallParams - +export function formatBuildCallParams(buildCallParams: T, account: Address) { // sender and recipient must be defined only for v1 and v2 pools return { ...buildCallParams, sender: account, recipient: account } } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts index 6365c4f92..c2834b640 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts @@ -1,9 +1,8 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' -import { AddLiquidityQueryOutput, Permit2, PublicWalletClient } from '@balancer/sdk' +import { AddLiquidityQueryOutput } from '@balancer/sdk' import { Address } from 'viem' import { BuildAddLiquidityInput, QueryAddLiquidityOutput } from '../add-liquidity.types' -import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' export interface Permit2AddLiquidityInput { account: Address @@ -36,13 +35,4 @@ export interface AddLiquidityHandler { It is responsibility of the UI to avoid calling buildAddLiquidityCallData before the last queryAddLiquidity was finished */ buildCallData(inputs: BuildAddLiquidityInput): Promise - - /* Sign permit2 for adding liquidity (for now only used by v3 pools) - TODO: generalize for other handlers using permit2 - */ - signPermit2?( - input: Permit2AddLiquidityInput, - walletClient: PublicWalletClient, - nonces?: NoncesByTokenAddress - ): Promise } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts new file mode 100644 index 000000000..64b25a7a7 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts @@ -0,0 +1,75 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { getRpcUrl } from '@/lib/modules/web3/transports' +import { + AddLiquidity, + AddLiquidityKind, + AddLiquidityUnbalancedInput, + PriceImpact, + PriceImpactAmount, +} from '@balancer/sdk' +import { Pool } from '../../../PoolProvider' +import { LiquidityActionHelpers, areEmptyAmounts } from '../../LiquidityActionHelpers' +import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' +import { AddLiquidityHandler } from './AddLiquidity.handler' + +/** + * UnbalancedAddLiquidityHandler is a handler that implements the + * AddLiquidityHandler interface for unbalanced adds, e.g. where the user + * specifies the token amounts in. It uses the Balancer SDK to implement it's + * methods. It also handles the case where one of the input tokens is the native + * asset instead of the wrapped native asset. + */ +export abstract class BaseUnbalancedAddLiquidityHandler implements AddLiquidityHandler { + protected helpers: LiquidityActionHelpers + + constructor(pool: Pool) { + this.helpers = new LiquidityActionHelpers(pool) + } + + public async simulate( + humanAmountsIn: HumanTokenAmountWithAddress[] + ): Promise { + const addLiquidity = new AddLiquidity() + const addLiquidityInput = this.constructSdkInput(humanAmountsIn) + + const sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.poolState) + + return { bptOut: sdkQueryOutput.bptOut, sdkQueryOutput } + } + + public async getPriceImpact(humanAmountsIn: HumanTokenAmountWithAddress[]): Promise { + if (areEmptyAmounts(humanAmountsIn)) { + // Avoid price impact calculation when there are no amounts in + return 0 + } + + const addLiquidityInput = this.constructSdkInput(humanAmountsIn) + + const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalanced( + addLiquidityInput, + this.helpers.poolState + ) + + return priceImpactABA.decimal + } + + public abstract buildCallData(input: SdkBuildAddLiquidityInput): Promise + + /** + * PRIVATE METHODS + */ + protected constructSdkInput( + humanAmountsIn: HumanTokenAmountWithAddress[] + ): AddLiquidityUnbalancedInput { + const amountsIn = this.helpers.toSdkInputAmounts(humanAmountsIn) + + return { + chainId: this.helpers.chainId, + rpcUrl: getRpcUrl(this.helpers.chainId), + amountsIn, + kind: AddLiquidityKind.Unbalanced, + } + } +} diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index 74376789b..4dc9c9770 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -1,96 +1,40 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' -import { getRpcUrl } from '@/lib/modules/web3/transports' -import { - AddLiquidity, - AddLiquidityBaseBuildCallInput, - AddLiquidityBaseQueryOutput, - AddLiquidityKind, - AddLiquidityUnbalancedInput, - Permit2, - Permit2Helper, - PriceImpact, - PriceImpactAmount, - PublicWalletClient, - Slippage, -} from '@balancer/sdk' -import { Pool } from '../../../PoolProvider' -import { - LiquidityActionHelpers, - areEmptyAmounts, - formatBuildCallParams, -} from '../../LiquidityActionHelpers' -import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-liquidity.types' -import { AddLiquidityHandler, Permit2AddLiquidityInput } from './AddLiquidity.handler' -import { NoncesByTokenAddress } from '@/lib/modules/tokens/approvals/permit2/usePermit2Nonces' +import { AddLiquidity } from '@balancer/sdk' +import { formatBuildCallParams } from '../../LiquidityActionHelpers' +import { SdkBuildAddLiquidityInput } from '../add-liquidity.types' +import { BaseUnbalancedAddLiquidityHandler } from './BaseUnbalancedAddLiquidity.handler' +import { constructBaseBuildCallInput } from './v3Helpers' /** * UnbalancedAddLiquidityHandler is a handler that implements the - * AddLiquidityHandler interface for unbalanced adds, e.g. where the user + * AddLiquidityHandler interface for unbalanced adds in v1(cowAMM) and v2 pools, e.g. where the user * specifies the token amounts in. It uses the Balancer SDK to implement it's * methods. It also handles the case where one of the input tokens is the native * asset instead of the wrapped native asset. */ -export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { - helpers: LiquidityActionHelpers - - constructor(pool: Pool) { - this.helpers = new LiquidityActionHelpers(pool) - } - - public async simulate( - humanAmountsIn: HumanTokenAmountWithAddress[] - ): Promise { - const addLiquidity = new AddLiquidity() - const addLiquidityInput = this.constructSdkInput(humanAmountsIn) - - const sdkQueryOutput = await addLiquidity.query(addLiquidityInput, this.helpers.poolState) - - return { bptOut: sdkQueryOutput.bptOut, sdkQueryOutput } - } - - public async getPriceImpact(humanAmountsIn: HumanTokenAmountWithAddress[]): Promise { - if (areEmptyAmounts(humanAmountsIn)) { - // Avoid price impact calculation when there are no amounts in - return 0 - } - - const addLiquidityInput = this.constructSdkInput(humanAmountsIn) - - const priceImpactABA: PriceImpactAmount = await PriceImpact.addLiquidityUnbalanced( - addLiquidityInput, - this.helpers.poolState - ) - - return priceImpactABA.decimal - } - +export class UnbalancedAddLiquidityHandler extends BaseUnbalancedAddLiquidityHandler { public async buildCallData({ + humanAmountsIn, slippagePercent, queryOutput, account, - permit2, }: SdkBuildAddLiquidityInput): Promise { const addLiquidity = new AddLiquidity() const buildCallParams = formatBuildCallParams( - this.constructBaseBuildCallInput({ + constructBaseBuildCallInput({ + humanAmountsIn, sdkQueryOutput: queryOutput.sdkQueryOutput, slippagePercent: slippagePercent, + pool: this.helpers.pool, }), - this.helpers.isV3Pool(), account ) - if (this.helpers.isV3Pool() && !permit2) { - throw new Error('Permit2 signature is required for V3 pools') - } + console.log({ buildCallParams }) - const { callData, to, value } = - this.helpers.isV3Pool() && permit2 - ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2) - : addLiquidity.buildCall(buildCallParams) + const { callData, to, value } = addLiquidity.buildCall(buildCallParams) return { account, @@ -100,56 +44,4 @@ export class UnbalancedAddLiquidityHandler implements AddLiquidityHandler { value, } } - - public async signPermit2( - input: Permit2AddLiquidityInput, - sdkClient: PublicWalletClient, - nonces: NoncesByTokenAddress - ): Promise { - const baseInput = this.constructBaseBuildCallInput({ - slippagePercent: input.slippagePercent, - sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, - }) - const signature = await Permit2Helper.signAddLiquidityApproval({ - ...baseInput, - client: sdkClient, - owner: input.account, - nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), - }) - return signature - } - - /** - * PRIVATE METHODS - */ - private constructSdkInput( - humanAmountsIn: HumanTokenAmountWithAddress[] - ): AddLiquidityUnbalancedInput { - const amountsIn = this.helpers.toSdkInputAmounts(humanAmountsIn) - - return { - chainId: this.helpers.chainId, - rpcUrl: getRpcUrl(this.helpers.chainId), - amountsIn, - kind: AddLiquidityKind.Unbalanced, - } - } - - public constructBaseBuildCallInput({ - slippagePercent, - sdkQueryOutput, - }: { - slippagePercent: string - sdkQueryOutput: AddLiquidityBaseQueryOutput - }): AddLiquidityBaseBuildCallInput { - const baseBuildCallParams = { - ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), - slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), - wethIsEth: this.helpers.includesNativeAsset(sdkQueryOutput.amountsIn), - } - // baseBuildCallParams.amountsIn = baseBuildCallParams.amountsIn.filter( - // amountIn => amountIn.amount > 0n - // ) - return baseBuildCallParams - } } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts new file mode 100644 index 000000000..25537f678 --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { AddLiquidity } from '@balancer/sdk' +import { SdkBuildAddLiquidityInput } from '../add-liquidity.types' +import { BaseUnbalancedAddLiquidityHandler } from './BaseUnbalancedAddLiquidity.handler' +import { constructBaseBuildCallInput } from './v3Helpers' + +/** + * UnbalancedAddLiquidityHandlerV3 is a handler that implements the + * AddLiquidityHandler interface for unbalanced V3 adds, e.g. where the user + * specifies the token amounts in. It uses the Balancer SDK to implement it's + * methods. It also handles the case where one of the input tokens is the native + * asset instead of the wrapped native asset. + */ +export class UnbalancedAddLiquidityHandlerV3 extends BaseUnbalancedAddLiquidityHandler { + public async buildCallData({ + humanAmountsIn, + slippagePercent, + queryOutput, + account, + permit2, + }: SdkBuildAddLiquidityInput): Promise { + const addLiquidity = new AddLiquidity() + + const buildCallParams = constructBaseBuildCallInput({ + humanAmountsIn, + sdkQueryOutput: queryOutput.sdkQueryOutput, + slippagePercent: slippagePercent, + pool: this.helpers.pool, + }) + + if (!permit2) { + throw new Error('Permit2 signature is required for V3 pools') + } + + const { callData, to, value } = addLiquidity.buildCallWithPermit2(buildCallParams, permit2) + + return { + account, + chainId: this.helpers.chainId, + data: callData, + to, + value, + } + } +} diff --git a/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts b/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts index 53e1bd166..cfd950866 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/selectAddLiquidityHandler.ts @@ -6,6 +6,8 @@ import { AddLiquidityHandler } from './AddLiquidity.handler' import { NestedAddLiquidityHandler } from './NestedAddLiquidity.handler' import { requiresProportionalInput, supportsNestedActions } from '../../LiquidityActionHelpers' import { ProportionalAddLiquidityHandler } from './ProportionalAddLiquidity.handler' +import { isV3Pool } from '../../../pool.helpers' +import { UnbalancedAddLiquidityHandlerV3 } from './UnbalancedAddLiquidityV3.handler' export function selectAddLiquidityHandler(pool: Pool): AddLiquidityHandler { // This is just an example to illustrate how edge-case handlers would receive different inputs but return a common contract @@ -22,5 +24,6 @@ export function selectAddLiquidityHandler(pool: Pool): AddLiquidityHandler { return new NestedAddLiquidityHandler(pool) } + if (isV3Pool(pool)) return new UnbalancedAddLiquidityHandlerV3(pool) return new UnbalancedAddLiquidityHandler(pool) } diff --git a/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts b/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts new file mode 100644 index 000000000..2c8e0bc6d --- /dev/null +++ b/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts @@ -0,0 +1,37 @@ +//TODO: move all this file logic to a better place + +import { + AddLiquidityBaseBuildCallInput, + AddLiquidityBaseQueryOutput, + Slippage, +} from '@balancer/sdk' +import { Pool } from '../../../PoolProvider' +import { LiquidityActionHelpers } from '../../LiquidityActionHelpers' +import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' + +// For now only valid for unbalanced adds +export function constructBaseBuildCallInput({ + humanAmountsIn, + slippagePercent, + sdkQueryOutput, + pool, +}: { + humanAmountsIn: HumanTokenAmountWithAddress[] + slippagePercent: string + sdkQueryOutput: AddLiquidityBaseQueryOutput + pool: Pool +}): AddLiquidityBaseBuildCallInput { + const helpers = new LiquidityActionHelpers(pool) + + console.log({ includesNativeAsset: helpers.isNativeAssetIn(humanAmountsIn) }) + + const baseBuildCallParams = { + ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + wethIsEth: helpers.isNativeAssetIn(humanAmountsIn), + } + // baseBuildCallParams.amountsIn = baseBuildCallParams.amountsIn.filter( + // amountIn => amountIn.amount > 0n + // ) + return baseBuildCallParams +} diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index 785268ffa..ef8ffb8f5 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -46,16 +46,13 @@ export function useAddLiquiditySteps({ isPermit2: requiresPermit2Approval(pool), }) - const signPermit2Step = useSignPermit2Step( - { - chainId, - handler, - slippagePercent: slippage, - queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, - }, + const signPermit2Step = useSignPermit2Step({ + pool, + humanAmountsIn, chainId, - requiresPermit2Approval(pool) - ) + slippagePercent: slippage, + queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, + }) const addLiquidityStep = useAddLiquidityStep({ handler, diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts index b3e36e89e..7b6294649 100644 --- a/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts +++ b/lib/modules/pool/actions/remove-liquidity/handlers/ProportionalRemoveLiquidity.handler.ts @@ -1,4 +1,5 @@ import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { getRpcUrl } from '@/lib/modules/web3/transports' import { HumanAmount, InputAmount, @@ -8,8 +9,8 @@ import { Slippage, } from '@balancer/sdk' import { Address, parseEther } from 'viem' -import { BPT_DECIMALS } from '../../../pool.constants' import { Pool } from '../../../PoolProvider' +import { BPT_DECIMALS } from '../../../pool.constants' import { LiquidityActionHelpers, formatBuildCallParams } from '../../LiquidityActionHelpers' import { QueryRemoveLiquidityInput, @@ -17,7 +18,6 @@ import { SdkQueryRemoveLiquidityOutput, } from '../remove-liquidity.types' import { RemoveLiquidityHandler } from './RemoveLiquidity.handler' -import { getRpcUrl } from '@/lib/modules/web3/transports' export class ProportionalRemoveLiquidityHandler implements RemoveLiquidityHandler { helpers: LiquidityActionHelpers @@ -56,11 +56,7 @@ export class ProportionalRemoveLiquidityHandler implements RemoveLiquidityHandle wethIsEth, } - const buildCallParams = formatBuildCallParams( - baseBuildCallParams, - this.helpers.isV3Pool(), - account - ) + const buildCallParams = formatBuildCallParams(baseBuildCallParams, account) const { callData, to, value } = removeLiquidity.buildCall(buildCallParams) diff --git a/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts index a0c8736f7..3204db5af 100644 --- a/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts +++ b/lib/modules/pool/actions/remove-liquidity/handlers/SingleTokenRemoveLiquidity.handler.ts @@ -1,4 +1,6 @@ import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' +import { getRpcUrl } from '@/lib/modules/web3/transports' +import { SentryError } from '@/lib/shared/utils/errors' import { HumanAmount, InputAmount, @@ -10,21 +12,19 @@ import { Slippage, } from '@balancer/sdk' import { Address, parseEther } from 'viem' -import { BPT_DECIMALS } from '../../../pool.constants' import { Pool } from '../../../PoolProvider' +import { BPT_DECIMALS } from '../../../pool.constants' import { LiquidityActionHelpers, formatBuildCallParams, isEmptyHumanAmount, } from '../../LiquidityActionHelpers' import { + QueryRemoveLiquidityInput, SdkBuildRemoveLiquidityInput, SdkQueryRemoveLiquidityOutput, - QueryRemoveLiquidityInput, } from '../remove-liquidity.types' import { RemoveLiquidityHandler } from './RemoveLiquidity.handler' -import { SentryError } from '@/lib/shared/utils/errors' -import { getRpcUrl } from '@/lib/modules/web3/transports' export class SingleTokenRemoveLiquidityHandler implements RemoveLiquidityHandler { helpers: LiquidityActionHelpers @@ -82,11 +82,7 @@ export class SingleTokenRemoveLiquidityHandler implements RemoveLiquidityHandler wethIsEth, } - const buildCallParams = formatBuildCallParams( - baseBuildCallParams, - this.helpers.isV3Pool(), - account - ) + const buildCallParams = formatBuildCallParams(baseBuildCallParams, account) const { callData, to, value } = removeLiquidity.buildCall(buildCallParams) diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx index 672d08f64..d32a11b05 100644 --- a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx @@ -1,19 +1,26 @@ -import { - AddLiquidityHandler, - Permit2AddLiquidityInput, -} from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' +import { Pool } from '@/lib/modules/pool/PoolProvider' +import { Permit2AddLiquidityInput } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' +import { constructBaseBuildCallInput } from '@/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers' import { ensureError } from '@/lib/shared/utils/errors' -import { Permit2, PublicWalletClient } from '@balancer/sdk' +import { + AddLiquidityBaseQueryOutput, + Permit2, + Permit2Helper, + PublicWalletClient, +} from '@balancer/sdk' import { NoncesByTokenAddress } from './usePermit2Nonces' +import { HumanTokenAmountWithAddress } from '../../token.types' type SignPermit2Params = { - handler: AddLiquidityHandler //TODO: generalize to other handlers? + humanAmountsIn: HumanTokenAmountWithAddress[] + pool: Pool sdkClient?: PublicWalletClient permit2Input: Permit2AddLiquidityInput nonces: NoncesByTokenAddress } export async function signPermit2TokenTransfer({ - handler, + humanAmountsIn, + pool, sdkClient, permit2Input, nonces, @@ -21,8 +28,7 @@ export async function signPermit2TokenTransfer({ if (!sdkClient) return undefined try { - if (!handler.signPermit2) throw new Error('Handler does not implement signPermit2 method') - const signature = await handler.signPermit2(permit2Input, sdkClient, nonces) + const signature = await signPermit2(pool, humanAmountsIn, permit2Input, sdkClient, nonces) return signature } catch (e: unknown) { const error = ensureError(e) @@ -32,3 +38,27 @@ export async function signPermit2TokenTransfer({ throw error } } + +// TODO: refactor to object parameters +async function signPermit2( + pool: Pool, + humanAmountsIn: HumanTokenAmountWithAddress[], + input: Permit2AddLiquidityInput, + sdkClient: PublicWalletClient, + nonces: NoncesByTokenAddress +): Promise { + const baseInput = constructBaseBuildCallInput({ + humanAmountsIn, + slippagePercent: input.slippagePercent, + // TODO: generalize for all v3 pool types + sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, + pool, + }) + const signature = await Permit2Helper.signAddLiquidityApproval({ + ...baseInput, + client: sdkClient, + owner: input.account, + nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), + }) + return signature +} diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 7ae2e0d11..76ff1780d 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -1,28 +1,30 @@ /* eslint-disable react-hooks/exhaustive-deps */ +import { Pool } from '@/lib/modules/pool/PoolProvider' import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' -import { AddLiquidityHandler } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { useSdkWalletClient } from '@/lib/modules/web3/useSdkViemClient' import { Toast } from '@/lib/shared/components/toasts/Toast' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' +import { useTokens } from '../../TokensProvider' import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' import { NoncesByTokenAddress } from './usePermit2Nonces' -import { useTokens } from '../../TokensProvider' +import { HumanTokenAmountWithAddress } from '../../token.types' export type AddLiquidityPermit2Params = { chainId: number - handler: AddLiquidityHandler + humanAmountsIn: HumanTokenAmountWithAddress[] + pool: Pool queryOutput?: SdkQueryAddLiquidityOutput slippagePercent: string nonces?: NoncesByTokenAddress } export function useSignPermit2Transfer({ chainId, + humanAmountsIn, queryOutput, slippagePercent, - handler, nonces, }: AddLiquidityPermit2Params) { const toast = useToast() @@ -59,7 +61,7 @@ export function useSignPermit2Transfer({ } }, [minimumBpt]) - async function signPermit2() { + async function signPermit2(pool: Pool) { if (!queryOutput) throw new Error('No input provided for permit2 signature') if (!nonces) throw new Error('No nonces provided for permit2 signature') setSignPermit2State(SignPermit2State.Confirming) @@ -67,7 +69,8 @@ export function useSignPermit2Transfer({ try { const signature = await signPermit2TokenTransfer({ - handler, + pool, + humanAmountsIn, sdkClient, permit2Input: { account: userAddress, diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index c7651971f..5cb036658 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -13,23 +13,23 @@ import { import { useChainSwitch } from '../../web3/useChainSwitch' import { TransactionStep } from './lib' import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' +import { requiresPermit2Approval } from '../../pool/pool.helpers' export const signRelayerStepTitle = 'Sign relayer' -export function useSignPermit2Step( - params: AddLiquidityPermit2Params, - chainId: number, - enabled = false -): TransactionStep { +export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep { const { isConnected, userAddress } = useUserAccount() + const { chainId } = params + //TODO: Move this hook into useSignPermit2Transfer? + //TODO: Move useSignPermit2Transfer up into this hook? //TODO: isLoading state depending on amountsIn (simulation loaded)? const { isLoadingNonces, nonces } = usePermit2Nonces({ chainId, tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), owner: userAddress, - enabled, + enabled: requiresPermit2Approval(params.pool), }) const { @@ -58,7 +58,7 @@ export function useSignPermit2Step( variant="primary" isDisabled={isDisabled} isLoading={isLoading} - onClick={signPermit2} + onClick={() => signPermit2(params.pool)} loadingText={buttonLabel} > {buttonLabel} From bb2285b99da360f3977d9231c322f9b0bc9f12fa Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 30 Sep 2024 18:50:29 +0200 Subject: [PATCH 12/27] refactor: extract signature helpers --- .../handlers/AddLiquidity.handler.ts | 8 -- .../add-liquidity/handlers/v3Helpers.ts | 22 +++++- .../add-liquidity/useAddLiquiditySteps.tsx | 1 - .../relayer/RelayerSignatureProvider.tsx | 12 +-- .../relayer/signRelayerApproval.hooks.tsx | 48 +++++------- .../permit2/Permit2SignatureProvider.tsx | 12 +-- .../permit2/signPermit2TokenTransfer.tsx | 51 +++++++------ .../permit2/useSignPermit2Transfer.tsx | 55 ++++++-------- .../TransactionStateProvider.tsx | 2 + .../transactions/transaction-steps/lib.tsx | 1 + .../transaction-steps/useSafeAppLogs.tsx | 2 + .../transaction-steps/useSignPermit2Step.tsx | 16 ++-- .../transaction-steps/useSignPermitStep.tsx | 73 +++++++++++++++++++ .../transaction-steps/useSignRelayerStep.tsx | 4 +- .../web3/signatures/signature.helpers.ts | 14 ++++ 15 files changed, 195 insertions(+), 126 deletions(-) create mode 100644 lib/modules/transactions/transaction-steps/useSignPermitStep.tsx create mode 100644 lib/modules/web3/signatures/signature.helpers.ts diff --git a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts index c2834b640..afd2a92c7 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler.ts @@ -1,15 +1,7 @@ import { HumanTokenAmountWithAddress } from '@/lib/modules/tokens/token.types' import { TransactionConfig } from '@/lib/modules/web3/contracts/contract.types' -import { AddLiquidityQueryOutput } from '@balancer/sdk' -import { Address } from 'viem' import { BuildAddLiquidityInput, QueryAddLiquidityOutput } from '../add-liquidity.types' -export interface Permit2AddLiquidityInput { - account: Address - slippagePercent: string - sdkQueryOutput: AddLiquidityQueryOutput -} - /** * AddLiquidityHandler is an interface that defines the methods that must be implemented by a handler. * They take standard inputs from the UI and return frontend standardised outputs. diff --git a/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts b/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts index 2c8e0bc6d..ebeab5a76 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers.ts @@ -3,6 +3,8 @@ import { AddLiquidityBaseBuildCallInput, AddLiquidityBaseQueryOutput, + RemoveLiquidityBaseBuildCallInput, + RemoveLiquidityQueryOutput, Slippage, } from '@balancer/sdk' import { Pool } from '../../../PoolProvider' @@ -23,8 +25,6 @@ export function constructBaseBuildCallInput({ }): AddLiquidityBaseBuildCallInput { const helpers = new LiquidityActionHelpers(pool) - console.log({ includesNativeAsset: helpers.isNativeAssetIn(humanAmountsIn) }) - const baseBuildCallParams = { ...(sdkQueryOutput as AddLiquidityBaseQueryOutput), slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), @@ -35,3 +35,21 @@ export function constructBaseBuildCallInput({ // ) return baseBuildCallParams } + +// For now only valid for unbalanced removes +export function constructRemoveBaseBuildCallInput({ + slippagePercent, + sdkQueryOutput, + wethIsEth, +}: { + slippagePercent: string + sdkQueryOutput: RemoveLiquidityQueryOutput + wethIsEth: boolean +}): RemoveLiquidityBaseBuildCallInput { + const baseBuildCallParams = { + ...sdkQueryOutput, + slippage: Slippage.fromPercentage(`${Number(slippagePercent)}`), + wethIsEth, + } + return baseBuildCallParams +} diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index ef8ffb8f5..b044ab009 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -49,7 +49,6 @@ export function useAddLiquiditySteps({ const signPermit2Step = useSignPermit2Step({ pool, humanAmountsIn, - chainId, slippagePercent: slippage, queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, }) diff --git a/lib/modules/relayer/RelayerSignatureProvider.tsx b/lib/modules/relayer/RelayerSignatureProvider.tsx index fb6c6bea5..d4148a7fa 100644 --- a/lib/modules/relayer/RelayerSignatureProvider.tsx +++ b/lib/modules/relayer/RelayerSignatureProvider.tsx @@ -4,13 +4,7 @@ import { useMandatoryContext } from '@/lib/shared/utils/contexts' import { PropsWithChildren, createContext, useState } from 'react' import { Address } from 'viem' - -export enum SignRelayerState { - Ready = 'init', - Confirming = 'confirming', - Preparing = 'preparing', - Completed = 'completed', -} +import { SignatureState } from '../web3/signatures/signature.helpers' export type UseRelayerSignatureResponse = ReturnType export const RelayerSignatureContext = createContext(null) @@ -18,9 +12,7 @@ export const RelayerSignatureContext = createContext() - const [signRelayerState, setSignRelayerState] = useState( - SignRelayerState.Preparing - ) + const [signRelayerState, setSignRelayerState] = useState(SignatureState.Preparing) return { relayerApprovalSignature, diff --git a/lib/modules/relayer/signRelayerApproval.hooks.tsx b/lib/modules/relayer/signRelayerApproval.hooks.tsx index 21ba1fe51..b105e217b 100644 --- a/lib/modules/relayer/signRelayerApproval.hooks.tsx +++ b/lib/modules/relayer/signRelayerApproval.hooks.tsx @@ -4,10 +4,15 @@ import { useEffect, useState } from 'react' import { signRelayerApproval } from './signRelayerApproval' import { useHasApprovedRelayer } from './useHasApprovedRelayer' import { RelayerMode } from './useRelayerMode' -import { SignRelayerState, useRelayerSignature } from './RelayerSignatureProvider' +import { useRelayerSignature } from './RelayerSignatureProvider' import { SupportedChainId } from '@/lib/config/config.types' import { Toast } from '@/lib/shared/components/toasts/Toast' import { useSdkWalletClient } from '../web3/useSdkViemClient' +import { + SignatureState, + isSignatureDisabled, + isSignatureLoading, +} from '../web3/signatures/signature.helpers' export function useShouldSignRelayerApproval(chainId: SupportedChainId, relayerMode: RelayerMode) { const { hasApprovedRelayer } = useHasApprovedRelayer(chainId) @@ -27,21 +32,21 @@ export function useSignRelayerApproval(chainId: SupportedChainId) { useEffect(() => { if (sdkClient === undefined) { - setSignRelayerState(SignRelayerState.Preparing) + setSignRelayerState(SignatureState.Preparing) } else { - setSignRelayerState(SignRelayerState.Ready) + setSignRelayerState(SignatureState.Ready) } }, [setSignRelayerState, sdkClient]) async function signRelayer() { - setSignRelayerState(SignRelayerState.Confirming) + setSignRelayerState(SignatureState.Confirming) setError(undefined) try { const signature = await signRelayerApproval(userAddress, chainId, sdkClient) if (signature) { - setSignRelayerState(SignRelayerState.Completed) + setSignRelayerState(SignatureState.Completed) toast({ title: 'Relayer approval signed!', description: '', @@ -51,15 +56,14 @@ export function useSignRelayerApproval(chainId: SupportedChainId) { render: ({ ...rest }) => , }) } else { - setSignRelayerState(SignRelayerState.Ready) + setSignRelayerState(SignatureState.Ready) } setRelayerApprovalSignature(signature) } catch (error) { - // TODO: refactor when we define a global error handling UX pattern console.error(error) setError('Error in relayer signature call') - setSignRelayerState(SignRelayerState.Ready) + setSignRelayerState(SignatureState.Ready) } } @@ -67,30 +71,16 @@ export function useSignRelayerApproval(chainId: SupportedChainId) { signRelayer, signRelayerState, buttonLabel: getButtonLabel(signRelayerState), - isLoading: isLoading(signRelayerState), - isDisabled: isDisabled(signRelayerState), + isLoading: isSignatureLoading(signRelayerState), + isDisabled: isSignatureDisabled(signRelayerState), error, } } -function isDisabled(signRelayerState: SignRelayerState) { - return ( - signRelayerState === SignRelayerState.Confirming || - signRelayerState === SignRelayerState.Completed - ) -} - -function isLoading(signRelayerState: SignRelayerState) { - return ( - signRelayerState === SignRelayerState.Confirming || - signRelayerState === SignRelayerState.Preparing - ) -} - -function getButtonLabel(signRelayerState: SignRelayerState) { - if (signRelayerState === SignRelayerState.Ready) return 'Sign relayer' - if (signRelayerState === SignRelayerState.Confirming) return 'Confirm relayer signature in wallet' - if (signRelayerState === SignRelayerState.Preparing) return 'Preparing' - if (signRelayerState === SignRelayerState.Completed) return 'Relayer Signed' +function getButtonLabel(signRelayerState: SignatureState) { + if (signRelayerState === SignatureState.Ready) return 'Sign relayer' + if (signRelayerState === SignatureState.Confirming) return 'Confirm relayer signature in wallet' + if (signRelayerState === SignatureState.Preparing) return 'Preparing' + if (signRelayerState === SignatureState.Completed) return 'Relayer Signed' return '' } diff --git a/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx index 2ed9c6e47..389c9e64e 100644 --- a/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx +++ b/lib/modules/tokens/approvals/permit2/Permit2SignatureProvider.tsx @@ -1,25 +1,17 @@ 'use client' +import { SignatureState } from '@/lib/modules/web3/signatures/signature.helpers' import { useMandatoryContext } from '@/lib/shared/utils/contexts' import { Permit2 } from '@balancer/sdk' import { PropsWithChildren, createContext, useState } from 'react' -export enum SignPermit2State { - Ready = 'init', - Confirming = 'confirming', - Preparing = 'preparing', - Completed = 'completed', -} - export type UsePermit2SignatureResponse = ReturnType export const Permit2SignatureContext = createContext(null) export function _usePermit2Signature() { const [permit2TransferSignature, setPermit2TransferSignature] = useState() - const [signPermit2State, setSignPermit2State] = useState( - SignPermit2State.Preparing - ) + const [signPermit2State, setSignPermit2State] = useState(SignatureState.Preparing) return { permit2TransferSignature, diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx index d32a11b05..a70687bf4 100644 --- a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx @@ -1,34 +1,37 @@ import { Pool } from '@/lib/modules/pool/PoolProvider' -import { Permit2AddLiquidityInput } from '@/lib/modules/pool/actions/add-liquidity/handlers/AddLiquidity.handler' import { constructBaseBuildCallInput } from '@/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers' import { ensureError } from '@/lib/shared/utils/errors' import { AddLiquidityBaseQueryOutput, + AddLiquidityQueryOutput, + Address, Permit2, Permit2Helper, PublicWalletClient, } from '@balancer/sdk' -import { NoncesByTokenAddress } from './usePermit2Nonces' import { HumanTokenAmountWithAddress } from '../../token.types' +import { NoncesByTokenAddress } from './usePermit2Nonces' + +export interface Permit2AddLiquidityInput { + account: Address + slippagePercent: string + sdkQueryOutput: AddLiquidityQueryOutput +} type SignPermit2Params = { - humanAmountsIn: HumanTokenAmountWithAddress[] - pool: Pool sdkClient?: PublicWalletClient + pool: Pool + humanAmountsIn: HumanTokenAmountWithAddress[] permit2Input: Permit2AddLiquidityInput nonces: NoncesByTokenAddress } -export async function signPermit2TokenTransfer({ - humanAmountsIn, - pool, - sdkClient, - permit2Input, - nonces, -}: SignPermit2Params): Promise { - if (!sdkClient) return undefined +export async function signPermit2TokenTransfer( + params: SignPermit2Params +): Promise { + if (!params.sdkClient) return undefined try { - const signature = await signPermit2(pool, humanAmountsIn, permit2Input, sdkClient, nonces) + const signature = await signPermit2(params) return signature } catch (e: unknown) { const error = ensureError(e) @@ -40,24 +43,24 @@ export async function signPermit2TokenTransfer({ } // TODO: refactor to object parameters -async function signPermit2( - pool: Pool, - humanAmountsIn: HumanTokenAmountWithAddress[], - input: Permit2AddLiquidityInput, - sdkClient: PublicWalletClient, - nonces: NoncesByTokenAddress -): Promise { +async function signPermit2({ + sdkClient, + pool, + humanAmountsIn, + permit2Input, + nonces, +}: SignPermit2Params): Promise { const baseInput = constructBaseBuildCallInput({ humanAmountsIn, - slippagePercent: input.slippagePercent, + slippagePercent: permit2Input.slippagePercent, // TODO: generalize for all v3 pool types - sdkQueryOutput: input.sdkQueryOutput as AddLiquidityBaseQueryOutput, + sdkQueryOutput: permit2Input.sdkQueryOutput as AddLiquidityBaseQueryOutput, pool, }) const signature = await Permit2Helper.signAddLiquidityApproval({ ...baseInput, - client: sdkClient, - owner: input.account, + client: sdkClient!, + owner: permit2Input.account, nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), }) return signature diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 76ff1780d..8c2feb5b4 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -7,13 +7,18 @@ import { Toast } from '@/lib/shared/components/toasts/Toast' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { useTokens } from '../../TokensProvider' -import { SignPermit2State, usePermit2Signature } from './Permit2SignatureProvider' +import { usePermit2Signature } from './Permit2SignatureProvider' import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' import { NoncesByTokenAddress } from './usePermit2Nonces' import { HumanTokenAmountWithAddress } from '../../token.types' +import { getChainId } from '@/lib/config/app.config' +import { + SignatureState, + isSignatureDisabled, + isSignatureLoading, +} from '@/lib/modules/web3/signatures/signature.helpers' export type AddLiquidityPermit2Params = { - chainId: number humanAmountsIn: HumanTokenAmountWithAddress[] pool: Pool queryOutput?: SdkQueryAddLiquidityOutput @@ -21,7 +26,7 @@ export type AddLiquidityPermit2Params = { nonces?: NoncesByTokenAddress } export function useSignPermit2Transfer({ - chainId, + pool, humanAmountsIn, queryOutput, slippagePercent, @@ -35,7 +40,7 @@ export function useSignPermit2Transfer({ const amountsIn = queryOutput?.sdkQueryOutput.amountsIn const tokenSymbols = amountsIn ?.filter(a => a.amount > 0n) - .map(a => getToken(a.token.address, chainId)?.symbol) + .map(a => getToken(a.token.address, getChainId(pool.chain))?.symbol) const { setSignPermit2State, setPermit2TransferSignature, signPermit2State } = usePermit2Signature() @@ -46,9 +51,9 @@ export function useSignPermit2Transfer({ useEffect(() => { if (sdkClient === undefined) { - setSignPermit2State(SignPermit2State.Preparing) + setSignPermit2State(SignatureState.Preparing) } else { - setSignPermit2State(SignPermit2State.Ready) + setSignPermit2State(SignatureState.Ready) } }, [setSignPermit2State, sdkClient]) @@ -57,14 +62,14 @@ export function useSignPermit2Transfer({ useEffect(() => { if (minimumBpt) { setPermit2TransferSignature(undefined) - setSignPermit2State(SignPermit2State.Ready) + setSignPermit2State(SignatureState.Ready) } }, [minimumBpt]) async function signPermit2(pool: Pool) { if (!queryOutput) throw new Error('No input provided for permit2 signature') if (!nonces) throw new Error('No nonces provided for permit2 signature') - setSignPermit2State(SignPermit2State.Confirming) + setSignPermit2State(SignatureState.Confirming) setError(undefined) try { @@ -81,7 +86,7 @@ export function useSignPermit2Transfer({ }) if (signature) { - setSignPermit2State(SignPermit2State.Completed) + setSignPermit2State(SignatureState.Completed) toast({ title: 'Permit2 approval signed!', description: '', @@ -91,14 +96,14 @@ export function useSignPermit2Transfer({ render: ({ ...rest }) => , }) } else { - setSignPermit2State(SignPermit2State.Ready) + setSignPermit2State(SignatureState.Ready) } setPermit2TransferSignature(signature) } catch (error) { console.error(error) setError('Error in permit2 signature call') - setSignPermit2State(SignPermit2State.Ready) + setSignPermit2State(SignatureState.Ready) } } @@ -106,31 +111,17 @@ export function useSignPermit2Transfer({ signPermit2, signPermit2State, buttonLabel: getButtonLabel(signPermit2State, tokenSymbols), - isLoading: isLoading(signPermit2State) || !queryOutput, - isDisabled: isDisabled(signPermit2State), + isLoading: isSignatureLoading(signPermit2State) || !queryOutput, + isDisabled: isSignatureDisabled(signPermit2State), error, } } -function isDisabled(signPermit2State: SignPermit2State) { - return ( - signPermit2State === SignPermit2State.Confirming || - signPermit2State === SignPermit2State.Completed - ) -} - -function isLoading(signPermit2State: SignPermit2State) { - return ( - signPermit2State === SignPermit2State.Confirming || - signPermit2State === SignPermit2State.Preparing - ) -} - -function getButtonLabel(signPermit2State: SignPermit2State, tokenSymbols?: (string | undefined)[]) { - if (signPermit2State === SignPermit2State.Ready) return getReadyLabel(tokenSymbols) - if (signPermit2State === SignPermit2State.Confirming) return 'Confirm permit2 signature in wallet' - if (signPermit2State === SignPermit2State.Preparing) return 'Preparing' - if (signPermit2State === SignPermit2State.Completed) return 'Permit2 Signed' +function getButtonLabel(signPermit2State: SignatureState, tokenSymbols?: (string | undefined)[]) { + if (signPermit2State === SignatureState.Ready) return getReadyLabel(tokenSymbols) + if (signPermit2State === SignatureState.Confirming) return 'Confirm permit2 signature in wallet' + if (signPermit2State === SignatureState.Preparing) return 'Preparing' + if (signPermit2State === SignatureState.Completed) return 'Permit2 Signed' return '' } diff --git a/lib/modules/transactions/transaction-steps/TransactionStateProvider.tsx b/lib/modules/transactions/transaction-steps/TransactionStateProvider.tsx index 31b9deb32..50e9071d7 100644 --- a/lib/modules/transactions/transaction-steps/TransactionStateProvider.tsx +++ b/lib/modules/transactions/transaction-steps/TransactionStateProvider.tsx @@ -19,6 +19,8 @@ export function _useTransactionState() { v = resetTransaction(v) } + // Avoid updating transaction if it's already successful (avoids unnecessary re-renders and side-effects) + if (getTransaction(k)?.result.status === 'success') return setTransactionMap(new Map(transactionMap.set(k, v))) } diff --git a/lib/modules/transactions/transaction-steps/lib.tsx b/lib/modules/transactions/transaction-steps/lib.tsx index eca264c12..c5a5841c9 100644 --- a/lib/modules/transactions/transaction-steps/lib.tsx +++ b/lib/modules/transactions/transaction-steps/lib.tsx @@ -41,6 +41,7 @@ export type StepType = | 'claim' | 'swap' | LockActionType + | 'signPermit' | 'signPermit2' export type TxActionId = diff --git a/lib/modules/transactions/transaction-steps/useSafeAppLogs.tsx b/lib/modules/transactions/transaction-steps/useSafeAppLogs.tsx index 944ee2a3f..7b0054b04 100644 --- a/lib/modules/transactions/transaction-steps/useSafeAppLogs.tsx +++ b/lib/modules/transactions/transaction-steps/useSafeAppLogs.tsx @@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query' import { useEffect, useState } from 'react' import { Hex, parseAbiItem } from 'viem' import { useUserAccount } from '../../web3/UserAccountProvider' +import { onlyExplicitRefetch } from '@/lib/shared/utils/queries' type Props = { enabled: boolean @@ -28,6 +29,7 @@ export function useSafeAppLogs({ enabled, hash, chainId, blockNumber }: Props) { select: data => data.find(log => log.args.txHash === hash), refetchInterval: safeTxHash ? 0 : 2000, // Refetch every 2 seconds until the safeTxHash is found enabled: enabled && !!hash && !!userAddress, + ...onlyExplicitRefetch, }) useEffect(() => { diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index 5cb036658..9eb00dadd 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -5,7 +5,6 @@ import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' import { Button, VStack } from '@chakra-ui/react' import { useMemo } from 'react' -import { SignPermit2State } from '../../tokens/approvals/permit2/Permit2SignatureProvider' import { AddLiquidityPermit2Params, useSignPermit2Transfer, @@ -14,19 +13,19 @@ import { useChainSwitch } from '../../web3/useChainSwitch' import { TransactionStep } from './lib' import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' import { requiresPermit2Approval } from '../../pool/pool.helpers' +import { getChainId } from '@/lib/config/app.config' +import { SignatureState } from '../../web3/signatures/signature.helpers' export const signRelayerStepTitle = 'Sign relayer' export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep { const { isConnected, userAddress } = useUserAccount() - const { chainId } = params - //TODO: Move this hook into useSignPermit2Transfer? //TODO: Move useSignPermit2Transfer up into this hook? //TODO: isLoading state depending on amountsIn (simulation loaded)? const { isLoadingNonces, nonces } = usePermit2Nonces({ - chainId, + chainId: getChainId(params.pool.chain), tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), owner: userAddress, enabled: requiresPermit2Approval(params.pool), @@ -39,9 +38,10 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti isDisabled, buttonLabel, error, - } = useSignPermit2Transfer({ ...params, chainId, nonces }) - const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = - useChainSwitch(chainId) + } = useSignPermit2Transfer({ ...params, nonces }) + const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = useChainSwitch( + getChainId(params.pool.chain) + ) const isLoading = isLoadingTransfer || isLoadingNonces @@ -67,7 +67,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti ) - const isComplete = () => signPermit2State === SignPermit2State.Completed + const isComplete = () => signPermit2State === SignatureState.Completed return useMemo( () => ({ diff --git a/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx b/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx new file mode 100644 index 000000000..e68aa1474 --- /dev/null +++ b/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx @@ -0,0 +1,73 @@ +'use client' + +import { ConnectWallet } from '@/lib/modules/web3/ConnectWallet' +import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' +import { Button, HStack, VStack, Text, Spacer } from '@chakra-ui/react' +import { useMemo } from 'react' +import { + RemoveLiquidityPermit1Params, + useSignPermitTransfer as useSignPermitTransfer, +} from '../../tokens/approvals/permit/useSignPermitTransfer' +import { useChainSwitch } from '../../web3/useChainSwitch' +import { TransactionStep } from './lib' +import { getChainId } from '@/lib/config/app.config' +import { SignIcon } from '@/lib/shared/components/icons/SignIcon' +import { primaryTextColor } from '@/lib/shared/services/chakra/themes/bal/colors' +import { SignatureState } from '../../web3/signatures/signature.helpers' + +export function useSignPermitStep(params: RemoveLiquidityPermit1Params): TransactionStep { + const { isConnected } = useUserAccount() + + const { signPermit, signPermitState, isLoading, isDisabled, buttonLabel, error } = + useSignPermitTransfer({ ...params }) + const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = useChainSwitch( + getChainId(params.pool.chain) + ) + + const SignPermitButton = () => ( + + {error && } + {!isConnected && } + {shouldChangeNetwork && isConnected && } + {!shouldChangeNetwork && isConnected && ( + + )} + + ) + + const isComplete = () => signPermitState === SignatureState.Completed + + return useMemo( + () => ({ + id: 'sign-permit', + stepType: 'signPermit', + labels: { + title: `Permit pool token on Balancer`, + init: `Permit transfer`, + tooltip: 'Sign permit transfer', + }, + isComplete, + renderAction: () => , + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [signPermitState, isLoading, isConnected] + ) +} diff --git a/lib/modules/transactions/transaction-steps/useSignRelayerStep.tsx b/lib/modules/transactions/transaction-steps/useSignRelayerStep.tsx index df5baf37d..e978099b1 100644 --- a/lib/modules/transactions/transaction-steps/useSignRelayerStep.tsx +++ b/lib/modules/transactions/transaction-steps/useSignRelayerStep.tsx @@ -5,11 +5,11 @@ import { ConnectWallet } from '@/lib/modules/web3/ConnectWallet' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { Alert, Button, VStack } from '@chakra-ui/react' import { TransactionStep } from './lib' -import { SignRelayerState } from '../../relayer/RelayerSignatureProvider' import { useMemo } from 'react' import { useChainSwitch } from '../../web3/useChainSwitch' import { getChainId } from '@/lib/config/app.config' import { GqlChain } from '@/lib/shared/services/api/generated/graphql' +import { SignatureState } from '../../web3/signatures/signature.helpers' export const signRelayerStepTitle = 'Sign relayer' @@ -47,7 +47,7 @@ export function useSignRelayerStep(chain: GqlChain): TransactionStep { ) - const isComplete = () => signRelayerState === SignRelayerState.Completed + const isComplete = () => signRelayerState === SignatureState.Completed return useMemo( () => ({ diff --git a/lib/modules/web3/signatures/signature.helpers.ts b/lib/modules/web3/signatures/signature.helpers.ts new file mode 100644 index 000000000..ac7e58966 --- /dev/null +++ b/lib/modules/web3/signatures/signature.helpers.ts @@ -0,0 +1,14 @@ +export enum SignatureState { + Ready = 'init', + Confirming = 'confirming', + Preparing = 'preparing', + Completed = 'completed', +} + +export function isSignatureDisabled(signatureState: SignatureState) { + return signatureState === SignatureState.Confirming || signatureState === SignatureState.Completed +} + +export function isSignatureLoading(signatureState: SignatureState) { + return signatureState === SignatureState.Confirming || signatureState === SignatureState.Preparing +} From 8db05205bf6082499c3ebd1ecd0b46f3ac9ac10f Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 30 Sep 2024 18:53:20 +0200 Subject: [PATCH 13/27] chore: delete file --- .../transaction-steps/useSignPermitStep.tsx | 73 ------------------- 1 file changed, 73 deletions(-) delete mode 100644 lib/modules/transactions/transaction-steps/useSignPermitStep.tsx diff --git a/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx b/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx deleted file mode 100644 index e68aa1474..000000000 --- a/lib/modules/transactions/transaction-steps/useSignPermitStep.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client' - -import { ConnectWallet } from '@/lib/modules/web3/ConnectWallet' -import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' -import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' -import { Button, HStack, VStack, Text, Spacer } from '@chakra-ui/react' -import { useMemo } from 'react' -import { - RemoveLiquidityPermit1Params, - useSignPermitTransfer as useSignPermitTransfer, -} from '../../tokens/approvals/permit/useSignPermitTransfer' -import { useChainSwitch } from '../../web3/useChainSwitch' -import { TransactionStep } from './lib' -import { getChainId } from '@/lib/config/app.config' -import { SignIcon } from '@/lib/shared/components/icons/SignIcon' -import { primaryTextColor } from '@/lib/shared/services/chakra/themes/bal/colors' -import { SignatureState } from '../../web3/signatures/signature.helpers' - -export function useSignPermitStep(params: RemoveLiquidityPermit1Params): TransactionStep { - const { isConnected } = useUserAccount() - - const { signPermit, signPermitState, isLoading, isDisabled, buttonLabel, error } = - useSignPermitTransfer({ ...params }) - const { shouldChangeNetwork, NetworkSwitchButton, networkSwitchButtonProps } = useChainSwitch( - getChainId(params.pool.chain) - ) - - const SignPermitButton = () => ( - - {error && } - {!isConnected && } - {shouldChangeNetwork && isConnected && } - {!shouldChangeNetwork && isConnected && ( - - )} - - ) - - const isComplete = () => signPermitState === SignatureState.Completed - - return useMemo( - () => ({ - id: 'sign-permit', - stepType: 'signPermit', - labels: { - title: `Permit pool token on Balancer`, - init: `Permit transfer`, - tooltip: 'Sign permit transfer', - }, - isComplete, - renderAction: () => , - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [signPermitState, isLoading, isConnected] - ) -} From 90ec0594f41ee046f3e77dd581782ee39b4c02c6 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Mon, 30 Sep 2024 20:00:22 +0200 Subject: [PATCH 14/27] chore: refactor isPermit parameters --- .../actions/add-liquidity/useAddLiquiditySteps.tsx | 14 +++++++++----- .../approvals/permit2/useSignPermit2Transfer.tsx | 1 + .../transaction-steps/useSignPermit2Step.tsx | 3 +-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index b044ab009..5dda3318b 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -37,13 +37,15 @@ export function useAddLiquiditySteps({ [humanAmountsIn, helpers] ) + const isPermit2 = requiresPermit2Approval(pool) + const { isLoading: isLoadingTokenApprovalSteps, steps: tokenApprovalSteps } = useTokenApprovalSteps({ spenderAddress: getSpenderForAddLiquidity(pool), chain: pool.chain, approvalAmounts: inputAmounts, actionType: 'AddLiquidity', - isPermit2: requiresPermit2Approval(pool), + isPermit2, }) const signPermit2Step = useSignPermit2Step({ @@ -51,17 +53,18 @@ export function useAddLiquiditySteps({ humanAmountsIn, slippagePercent: slippage, queryOutput: simulationQuery.data as SdkQueryAddLiquidityOutput, + isPermit2, }) + const isSignPermit2Loading = isPermit2 && !signPermit2Step + const addLiquidityStep = useAddLiquidityStep({ handler, humanAmountsIn, simulationQuery, }) - const addSteps = requiresPermit2Approval(pool) - ? [signPermit2Step, addLiquidityStep] - : [addLiquidityStep] + const addSteps = isPermit2 ? [signPermit2Step, addLiquidityStep] : [addLiquidityStep] const steps = useMemo(() => { if (relayerMode === 'approveRelayer') { @@ -78,11 +81,12 @@ export function useAddLiquiditySteps({ addLiquidityStep, approveRelayerStep, signRelayerStep, + signPermit2Step, humanAmountsIn, ]) return { - isLoadingSteps: isLoadingTokenApprovalSteps || isLoadingRelayerApproval, + isLoadingSteps: isLoadingTokenApprovalSteps || isLoadingRelayerApproval || isSignPermit2Loading, steps, } } diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 8c2feb5b4..36f06e4b1 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -24,6 +24,7 @@ export type AddLiquidityPermit2Params = { queryOutput?: SdkQueryAddLiquidityOutput slippagePercent: string nonces?: NoncesByTokenAddress + isPermit2: boolean } export function useSignPermit2Transfer({ pool, diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index 9eb00dadd..66e84c336 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -12,7 +12,6 @@ import { import { useChainSwitch } from '../../web3/useChainSwitch' import { TransactionStep } from './lib' import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' -import { requiresPermit2Approval } from '../../pool/pool.helpers' import { getChainId } from '@/lib/config/app.config' import { SignatureState } from '../../web3/signatures/signature.helpers' @@ -28,7 +27,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti chainId: getChainId(params.pool.chain), tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), owner: userAddress, - enabled: requiresPermit2Approval(params.pool), + enabled: params.isPermit2, }) const { From 46a2f82f0446fe83c03cd6a5c3fa1cbf0bb7ec68 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 10:04:32 +0200 Subject: [PATCH 15/27] chore: improve comment --- .../handlers/BaseUnbalancedAddLiquidity.handler.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts index 64b25a7a7..6e2532d46 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts @@ -15,11 +15,7 @@ import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-li import { AddLiquidityHandler } from './AddLiquidity.handler' /** - * UnbalancedAddLiquidityHandler is a handler that implements the - * AddLiquidityHandler interface for unbalanced adds, e.g. where the user - * specifies the token amounts in. It uses the Balancer SDK to implement it's - * methods. It also handles the case where one of the input tokens is the native - * asset instead of the wrapped native asset. + * Base abstract class that shares common logic shared by v3 and v2/v1 pool handlers */ export abstract class BaseUnbalancedAddLiquidityHandler implements AddLiquidityHandler { protected helpers: LiquidityActionHelpers From ae4198c0f0d5a407f81eff52d3bc85f214349cb7 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 10:16:48 +0200 Subject: [PATCH 16/27] chore: improve naming --- .../handlers/BaseUnbalancedAddLiquidity.handler.ts | 2 +- .../add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts | 2 -- .../tokens/approvals/permit2/signPermit2TokenTransfer.tsx | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts index 6e2532d46..c8ccd0cf4 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/BaseUnbalancedAddLiquidity.handler.ts @@ -15,7 +15,7 @@ import { SdkBuildAddLiquidityInput, SdkQueryAddLiquidityOutput } from '../add-li import { AddLiquidityHandler } from './AddLiquidity.handler' /** - * Base abstract class that shares common logic shared by v3 and v2/v1 pool handlers + * Base abstract class that shares common logic shared by v3 and v2/v1 pool unbalanced handlers */ export abstract class BaseUnbalancedAddLiquidityHandler implements AddLiquidityHandler { protected helpers: LiquidityActionHelpers diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts index 4dc9c9770..81f9635ca 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidity.handler.ts @@ -32,8 +32,6 @@ export class UnbalancedAddLiquidityHandler extends BaseUnbalancedAddLiquidityHan account ) - console.log({ buildCallParams }) - const { callData, to, value } = addLiquidity.buildCall(buildCallParams) return { diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx index a70687bf4..52b566422 100644 --- a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx @@ -42,7 +42,6 @@ export async function signPermit2TokenTransfer( } } -// TODO: refactor to object parameters async function signPermit2({ sdkClient, pool, @@ -50,6 +49,7 @@ async function signPermit2({ permit2Input, nonces, }: SignPermit2Params): Promise { + if (!sdkClient) throw new Error('Missing sdkClient') const baseInput = constructBaseBuildCallInput({ humanAmountsIn, slippagePercent: permit2Input.slippagePercent, @@ -59,7 +59,7 @@ async function signPermit2({ }) const signature = await Permit2Helper.signAddLiquidityApproval({ ...baseInput, - client: sdkClient!, + client: sdkClient, owner: permit2Input.account, nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), }) From 5e1e154300043e32a9092a86d4b371057b26056e Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:04:31 +0200 Subject: [PATCH 17/27] chore: improve permit2 step --- .../transaction-steps/useSignPermit2Step.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index 66e84c336..cd1f849c1 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -15,14 +15,9 @@ import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonce import { getChainId } from '@/lib/config/app.config' import { SignatureState } from '../../web3/signatures/signature.helpers' -export const signRelayerStepTitle = 'Sign relayer' - export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep { const { isConnected, userAddress } = useUserAccount() - //TODO: Move this hook into useSignPermit2Transfer? - //TODO: Move useSignPermit2Transfer up into this hook? - //TODO: isLoading state depending on amountsIn (simulation loaded)? const { isLoadingNonces, nonces } = usePermit2Nonces({ chainId: getChainId(params.pool.chain), tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), @@ -42,7 +37,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti getChainId(params.pool.chain) ) - const isLoading = isLoadingTransfer || isLoadingNonces + const isLoading = !!params.queryOutput || isLoadingTransfer || isLoadingNonces const SignPermitButton = () => ( @@ -73,7 +68,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti id: 'sign-permit2', stepType: 'signPermit2', labels: { - // TODO: display token symbol/s + // TODO: display nested permit tokens in Step Tracker title: `Permit on balancer`, init: `Permit transfer`, tooltip: 'Sign permit2 transfer', From b927f2f273518856518f3ef4fb807c00e960c246 Mon Sep 17 00:00:00 2001 From: uiuxxx Date: Tue, 1 Oct 2024 10:12:46 +0100 Subject: [PATCH 18/27] Change promo banners to 'CoW is Live' --- app/(app)/pools/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/(app)/pools/page.tsx b/app/(app)/pools/page.tsx index 04726d6e3..d9182cd9a 100644 --- a/app/(app)/pools/page.tsx +++ b/app/(app)/pools/page.tsx @@ -9,7 +9,7 @@ import { Suspense } from 'react' // import { getProjectConfig } from '@/lib/config/getProjectConfig' // import { GetFeaturedPoolsDocument } from '@/lib/shared/services/api/generated/graphql' // import { FeaturedPools } from '@/lib/modules/featured-pools/FeaturedPools' -import { CowGalxeQuestPromoBanner } from '@/lib/shared/components/promos/CowGalxeQuestPromoBanner' +import { CowPromoBanner } from '@/lib/shared/components/promos/CowPromoBanner' export default async function PoolsPage() { // Featured pools set up @@ -33,7 +33,7 @@ export default async function PoolsPage() { - + {/* From bb9ff7262cf44554761ceb85de4ce7fea13bbd1b Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:18:03 +0200 Subject: [PATCH 19/27] fix: receipt hook integration tests after foundry update --- .../receipts/receipt.hooks.integration.spec.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts index 71181dfa0..191d841be 100644 --- a/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts +++ b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts @@ -54,11 +54,11 @@ test('queries add liquidity transaction', async () => { expect(result.current.sentTokens).toEqual([ { - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', humanAmount: '12', }, { - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', humanAmount: '0.04', }, ]) @@ -99,11 +99,11 @@ test('queries remove liquidity transaction', async () => { expect(result.current.receivedTokens).toEqual([ { humanAmount: '16597.845312687911573359', - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', }, { humanAmount: '4.553531492712836774', - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, ]) @@ -112,8 +112,8 @@ test('queries remove liquidity transaction', async () => { describe('queries swap transaction', () => { const polAddress = '0x0000000000000000000000000000000000001010' - const wPolAddress = '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' - const daiAddress = '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063' + const wPolAddress = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' + const daiAddress = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063' test('when the native asset is not included (from DAI to WPOL)', async () => { const userAddress = '0xf76142b79Db34E57852d68F9c52C0E24f7349647' From ca06eb0069d37eda1f740b8064eb937fcaebceba Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:21:42 +0200 Subject: [PATCH 20/27] chore: avoid foundry cache --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 163aaaac3..4717f4114 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -53,5 +53,7 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 + with: + cache: false - name: Run integration tests run: pnpm test:integration From 89b708ee81376cb76d9c03044318ab739fa78930 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:26:18 +0200 Subject: [PATCH 21/27] chore: enable foundry cache again --- .github/workflows/checks.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4717f4114..163aaaac3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -53,7 +53,5 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 - with: - cache: false - name: Run integration tests run: pnpm test:integration From b20ee98d7844b522cf33392ebce9f2e822e128b8 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:29:57 +0200 Subject: [PATCH 22/27] chore: temporarily disable foundry cache --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 163aaaac3..4717f4114 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -53,5 +53,7 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 + with: + cache: false - name: Run integration tests run: pnpm test:integration From 3cb8df6566c307a6b14c0c68672068f7ddad4ac3 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 11:33:46 +0200 Subject: [PATCH 23/27] fix: receipt hook integration tests after foundry update --- .github/workflows/checks.yml | 2 -- .../receipts/receipt.hooks.integration.spec.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4717f4114..163aaaac3 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -53,7 +53,5 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 - with: - cache: false - name: Run integration tests run: pnpm test:integration diff --git a/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts index 71181dfa0..191d841be 100644 --- a/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts +++ b/lib/modules/transactions/transaction-steps/receipts/receipt.hooks.integration.spec.ts @@ -54,11 +54,11 @@ test('queries add liquidity transaction', async () => { expect(result.current.sentTokens).toEqual([ { - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', humanAmount: '12', }, { - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', humanAmount: '0.04', }, ]) @@ -99,11 +99,11 @@ test('queries remove liquidity transaction', async () => { expect(result.current.receivedTokens).toEqual([ { humanAmount: '16597.845312687911573359', - tokenAddress: '0x198d7387fa97a73f05b8578cdeff8f2a1f34cd1f', + tokenAddress: '0x198d7387Fa97A73F05b8578CdEFf8F2A1f34Cd1F', }, { humanAmount: '4.553531492712836774', - tokenAddress: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + tokenAddress: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', }, ]) @@ -112,8 +112,8 @@ test('queries remove liquidity transaction', async () => { describe('queries swap transaction', () => { const polAddress = '0x0000000000000000000000000000000000001010' - const wPolAddress = '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270' - const daiAddress = '0x8f3cf7ad23cd3cadbd9735aff958023239c6a063' + const wPolAddress = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270' + const daiAddress = '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063' test('when the native asset is not included (from DAI to WPOL)', async () => { const userAddress = '0xf76142b79Db34E57852d68F9c52C0E24f7349647' From bebdb884fae05b7cdf16044316efcf195965e8cd Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Tue, 1 Oct 2024 12:39:23 +0200 Subject: [PATCH 24/27] fix: sign permit2 loading condition --- .../transactions/transaction-steps/useSignPermit2Step.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index cd1f849c1..a559a74e7 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -37,7 +37,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti getChainId(params.pool.chain) ) - const isLoading = !!params.queryOutput || isLoadingTransfer || isLoadingNonces + const isLoading = isLoadingTransfer || isLoadingNonces const SignPermitButton = () => ( From 3b0b1c68f21076ef907ce3f5fe07eb548e2a4291 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 2 Oct 2024 09:45:56 +0200 Subject: [PATCH 25/27] chore: disable cache --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 163aaaac3..4717f4114 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -53,5 +53,7 @@ jobs: uses: ./.github/actions/setup - name: Set up foundry (includes anvil) uses: foundry-rs/foundry-toolchain@v1 + with: + cache: false - name: Run integration tests run: pnpm test:integration From 1fd092c78b0d23980f39888bb045064b5fd20433 Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Wed, 2 Oct 2024 15:58:05 +0200 Subject: [PATCH 26/27] chore: extract token helpers --- .../pool/actions/LiquidityActionHelpers.ts | 44 ++++++++++++++----- lib/modules/pool/pool.helpers.ts | 6 +-- lib/modules/tokens/TokensProvider.tsx | 2 + .../permit2/useSignPermit2Transfer.tsx | 31 +++++-------- 4 files changed, 47 insertions(+), 36 deletions(-) diff --git a/lib/modules/pool/actions/LiquidityActionHelpers.ts b/lib/modules/pool/actions/LiquidityActionHelpers.ts index a1d0564ef..65319fe09 100644 --- a/lib/modules/pool/actions/LiquidityActionHelpers.ts +++ b/lib/modules/pool/actions/LiquidityActionHelpers.ts @@ -11,21 +11,15 @@ import { MinimalToken, NestedPoolState, PoolState, + PoolStateWithBalances, + Token, TokenAmount, mapPoolToNestedPoolState, mapPoolType, - PoolStateWithBalances, - Token, } from '@balancer/sdk' -import { Hex, formatUnits, parseUnits, Address } from 'viem' -import { - isAffectedByCspIssue, - isComposableStableV1, - isCowAmmPool, - isGyro, - isV3Pool, -} from '../pool.helpers' -import { Pool } from '../PoolProvider' +import BigNumber from 'bignumber.js' +import { Address, Hex, formatUnits, parseUnits } from 'viem' +import { GetTokenFn } from '../../tokens/TokensProvider' import { isNativeAsset, isNativeOrWrappedNative, @@ -33,7 +27,15 @@ import { swapNativeWithWrapped, } from '../../tokens/token.helpers' import { HumanTokenAmountWithAddress } from '../../tokens/token.types' -import BigNumber from 'bignumber.js' +import { Pool } from '../PoolProvider' +import { + isAffectedByCspIssue, + isComposableStableV1, + isCowAmmPool, + isGyro, + isV3Pool, +} from '../pool.helpers' +import { SdkQueryAddLiquidityOutput } from './add-liquidity/add-liquidity.types' // Null object used to avoid conditional checks during hook loading state const NullPool: Pool = { @@ -317,3 +319,21 @@ export function formatBuildCallParams(buildCallParams: T, account: Address) { // sender and recipient must be defined only for v1 and v2 pools return { ...buildCallParams, sender: account, recipient: account } } + +export function getTokenSymbols( + getToken: GetTokenFn, + chain: GqlChain, + queryOutput?: SdkQueryAddLiquidityOutput +) { + if (!queryOutput?.sdkQueryOutput) return [] + const amountsIn = queryOutput.sdkQueryOutput.amountsIn + const tokenSymbols = amountsIn + ?.filter(a => a.amount > 0n) + .map(a => getToken(a.token.address, chain)?.symbol ?? 'Unknown') + return tokenSymbols +} + +export function getTokenAddresses(queryOutput?: SdkQueryAddLiquidityOutput): Address[] | undefined { + if (!queryOutput?.sdkQueryOutput) return undefined + return queryOutput.sdkQueryOutput.amountsIn.map(t => t.token.address) +} diff --git a/lib/modules/pool/pool.helpers.ts b/lib/modules/pool/pool.helpers.ts index de8a27a3b..2d4ad301a 100644 --- a/lib/modules/pool/pool.helpers.ts +++ b/lib/modules/pool/pool.helpers.ts @@ -26,6 +26,7 @@ import { balancerV2VaultAbi } from '../web3/contracts/abi/generated' import { balancerV3VaultAbi } from '../web3/contracts/abi/balancerV3VaultAbi' import { supportsNestedActions } from './actions/LiquidityActionHelpers' import { getLeafTokens } from '../tokens/token.helpers' +import { GetTokenFn } from '../tokens/TokensProvider' /** * METHODS @@ -329,10 +330,7 @@ export function getRateProviderWarnings(warnings: string[]) { return warnings.filter(warning => !isEmpty(warning)) } -export function getPoolTokens( - pool: Pool, - getToken: (address: string, chain: GqlChain) => GqlToken | undefined -): GqlToken[] { +export function getPoolTokens(pool: Pool, getToken: GetTokenFn): GqlToken[] { type PoolToken = Pool['poolTokens'][0] function toGqlTokens(tokens: PoolToken[]): GqlToken[] { return tokens diff --git a/lib/modules/tokens/TokensProvider.tsx b/lib/modules/tokens/TokensProvider.tsx index 5fc8549ed..248aeafba 100644 --- a/lib/modules/tokens/TokensProvider.tsx +++ b/lib/modules/tokens/TokensProvider.tsx @@ -25,6 +25,8 @@ import { mins } from '@/lib/shared/utils/time' export type UseTokensResult = ReturnType export const TokensContext = createContext(null) +export type GetTokenFn = (address: string, chain: GqlChain) => GqlToken | undefined + export function _useTokens( initTokenData: GetTokensQuery, initTokenPricesData: GetTokenPricesQuery, diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 36f06e4b1..12efd7af2 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -1,22 +1,22 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { Pool } from '@/lib/modules/pool/PoolProvider' +import { getTokenSymbols } from '@/lib/modules/pool/actions/LiquidityActionHelpers' import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { + SignatureState, + isSignatureDisabled, + isSignatureLoading, +} from '@/lib/modules/web3/signatures/signature.helpers' import { useSdkWalletClient } from '@/lib/modules/web3/useSdkViemClient' import { Toast } from '@/lib/shared/components/toasts/Toast' import { useToast } from '@chakra-ui/react' import { useEffect, useState } from 'react' import { useTokens } from '../../TokensProvider' +import { HumanTokenAmountWithAddress } from '../../token.types' import { usePermit2Signature } from './Permit2SignatureProvider' import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' import { NoncesByTokenAddress } from './usePermit2Nonces' -import { HumanTokenAmountWithAddress } from '../../token.types' -import { getChainId } from '@/lib/config/app.config' -import { - SignatureState, - isSignatureDisabled, - isSignatureLoading, -} from '@/lib/modules/web3/signatures/signature.helpers' export type AddLiquidityPermit2Params = { humanAmountsIn: HumanTokenAmountWithAddress[] @@ -37,13 +37,8 @@ export function useSignPermit2Transfer({ const { userAddress } = useUserAccount() const { getToken } = useTokens() - //TODO: We will probably need to extract this logic to be reusable by other components (StepTracker) - const amountsIn = queryOutput?.sdkQueryOutput.amountsIn - const tokenSymbols = amountsIn - ?.filter(a => a.amount > 0n) - .map(a => getToken(a.token.address, getChainId(pool.chain))?.symbol) - - const { setSignPermit2State, setPermit2TransferSignature, signPermit2State } = + const tokenSymbols = getTokenSymbols(getToken, pool.chain, queryOutput) + const { signPermit2State, setSignPermit2State, setPermit2TransferSignature } = usePermit2Signature() const [error, setError] = useState() @@ -51,12 +46,8 @@ export function useSignPermit2Transfer({ const sdkClient = useSdkWalletClient() useEffect(() => { - if (sdkClient === undefined) { - setSignPermit2State(SignatureState.Preparing) - } else { - setSignPermit2State(SignatureState.Ready) - } - }, [setSignPermit2State, sdkClient]) + if (sdkClient === undefined) setSignPermit2State(SignatureState.Preparing) + }, [sdkClient]) //TODO: Generalize for Swaps and other potential signatures const minimumBpt = queryOutput?.sdkQueryOutput.bptOut.amount From 1b115ee4e40fc7191540998ebc1441279ebb4bcd Mon Sep 17 00:00:00 2001 From: Alberto Gualis Date: Fri, 4 Oct 2024 16:30:26 +0200 Subject: [PATCH 27/27] feat: add 24h permit2 expiration --- app/(app)/debug/permit2-allowance/page.tsx | 11 ++--- .../UnbalancedAddLiquidityV3.handler.ts | 8 ++-- .../queries/add-liquidity-keys.ts | 6 ++- .../useAddLiquidityBuildCallDataQuery.ts | 7 ++-- .../add-liquidity/useAddLiquiditySteps.tsx | 3 +- .../approvals/permit2/permit2.helpers.ts | 18 ++++++++ .../permit2/signPermit2TokenTransfer.tsx | 20 ++++++++- ...mit2Nonces.tsx => usePermit2Allowance.tsx} | 27 +++++++++++- .../permit2/useSignPermit2Transfer.tsx | 2 +- .../TransactionStepButton.tsx | 6 ++- .../transaction-steps/useSignPermit2Step.tsx | 27 +++++++----- .../contracts/useManagedSendTransaction.ts | 42 ++++++++++--------- lib/shared/utils/time.ts | 8 ++++ 13 files changed, 135 insertions(+), 50 deletions(-) create mode 100644 lib/modules/tokens/approvals/permit2/permit2.helpers.ts rename lib/modules/tokens/approvals/permit2/{usePermit2Nonces.tsx => usePermit2Allowance.tsx} (59%) diff --git a/app/(app)/debug/permit2-allowance/page.tsx b/app/(app)/debug/permit2-allowance/page.tsx index 7513f5d72..37c7b4b52 100644 --- a/app/(app)/debug/permit2-allowance/page.tsx +++ b/app/(app)/debug/permit2-allowance/page.tsx @@ -1,14 +1,15 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ 'use client' +import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config' +import { BPT_DECIMALS } from '@/lib/modules/pool/pool.constants' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' +import { permit2Abi } from '@balancer/sdk' import { Center, Input, Text, VStack } from '@chakra-ui/react' import { useState } from 'react' -import { Address } from 'viem' +import { Address, formatUnits } from 'viem' import { sepolia } from 'viem/chains' import { useReadContract } from 'wagmi' -import { fNum } from '@/lib/shared/utils/numbers' -import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config' -import { permit2Abi } from '@balancer/sdk' export default function Page() { const [tokenAddress, setTokenAddress] = useState
('' as Address) @@ -30,7 +31,7 @@ export default function Page() { {data && (
-
Amount: {fNum('integer', data[0])}
+
Amount: {formatUnits(data[0], BPT_DECIMALS).toString()}
Expires: {data[1]}
Nonce: {data[2]}
diff --git a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts index 25537f678..9cfc7aae3 100644 --- a/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts +++ b/lib/modules/pool/actions/add-liquidity/handlers/UnbalancedAddLiquidityV3.handler.ts @@ -29,11 +29,9 @@ export class UnbalancedAddLiquidityHandlerV3 extends BaseUnbalancedAddLiquidityH pool: this.helpers.pool, }) - if (!permit2) { - throw new Error('Permit2 signature is required for V3 pools') - } - - const { callData, to, value } = addLiquidity.buildCallWithPermit2(buildCallParams, permit2) + const { callData, to, value } = permit2 + ? addLiquidity.buildCallWithPermit2(buildCallParams, permit2) + : addLiquidity.buildCall(buildCallParams) return { account, diff --git a/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts index 06f748a88..caa8d01e5 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/add-liquidity-keys.ts @@ -16,6 +16,7 @@ export type AddLiquidityParams = { poolType: GqlPoolType slippage: string humanAmountsIn: HumanTokenAmountWithAddress[] + hasPermit2?: boolean } function liquidityParams({ @@ -25,10 +26,13 @@ function liquidityParams({ poolType, slippage, humanAmountsIn, + hasPermit2, }: AddLiquidityParams) { return `${getHandlerClassName( handler - )}:${userAddress}:${poolId}:${slippage}:${stringifyHumanAmountsIn(poolType, humanAmountsIn)}` + )}:${userAddress}:${poolId}:${slippage}:${stringifyHumanAmountsIn(poolType, humanAmountsIn)}${ + hasPermit2 ? 'permit2' : 'no-permit2' + }` } export function stringifyHumanAmountsIn( diff --git a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts index fd3c6c02c..15f9c98f3 100644 --- a/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts +++ b/lib/modules/pool/actions/add-liquidity/queries/useAddLiquidityBuildCallDataQuery.ts @@ -40,6 +40,8 @@ export function useAddLiquidityBuildCallDataQuery({ const { permit2TransferSignature: permit2 } = usePermit2Signature() const debouncedHumanAmountsIn = useDebounce(humanAmountsIn, defaultDebounceMs)[0] + const hasPermit2 = isV3Pool(pool) && !!permit2 + const params: AddLiquidityParams = { handler, userAddress, @@ -47,10 +49,9 @@ export function useAddLiquidityBuildCallDataQuery({ poolId: pool.id, poolType: pool.type, humanAmountsIn: debouncedHumanAmountsIn, + hasPermit2, } - const isValidPermit2 = isV3Pool(pool) && !!permit2 - const queryKey = addLiquidityKeys.buildCallData(params) const queryFn = async () => { @@ -71,7 +72,7 @@ export function useAddLiquidityBuildCallDataQuery({ return useQuery({ queryKey, queryFn, - enabled: enabled && isConnected && !!simulationQuery.data && isValidPermit2, + enabled: enabled && isConnected && !!simulationQuery.data, gcTime: 0, meta: sentryMetaForAddLiquidityHandler('Error in add liquidity buildCallData query', { ...params, diff --git a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx index fdc784fed..8560bae0b 100644 --- a/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx +++ b/lib/modules/pool/actions/add-liquidity/useAddLiquiditySteps.tsx @@ -65,7 +65,8 @@ export function useAddLiquiditySteps({ slippage, }) - const addSteps = isPermit2 ? [signPermit2Step, addLiquidityStep] : [addLiquidityStep] + const addSteps = + isPermit2 && signPermit2Step ? [signPermit2Step, addLiquidityStep] : [addLiquidityStep] const steps = useMemo(() => { if (relayerMode === 'approveRelayer') { diff --git a/lib/modules/tokens/approvals/permit2/permit2.helpers.ts b/lib/modules/tokens/approvals/permit2/permit2.helpers.ts new file mode 100644 index 000000000..5f1948c41 --- /dev/null +++ b/lib/modules/tokens/approvals/permit2/permit2.helpers.ts @@ -0,0 +1,18 @@ +import { SdkQueryAddLiquidityOutput } from '@/lib/modules/pool/actions/add-liquidity/add-liquidity.types' +import { getNowTimestampInSecs } from '@/lib/shared/utils/time' +import { AllowedAmountsByTokenAddress, ExpirationByTokenAddress } from './usePermit2Allowance' + +export function hasValidPermit2( + queryOutput?: SdkQueryAddLiquidityOutput, + expirations?: ExpirationByTokenAddress, + allowedAmounts?: AllowedAmountsByTokenAddress +): boolean { + if (!expirations || !allowedAmounts) return false + const isValid = !!queryOutput?.sdkQueryOutput.amountsIn.every( + t => + t.amount === 0n || + (expirations[t.token.address] >= getNowTimestampInSecs() && + allowedAmounts[t.token.address] >= t.amount) + ) + return isValid +} diff --git a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx index 52b566422..2b569a783 100644 --- a/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx +++ b/lib/modules/tokens/approvals/permit2/signPermit2TokenTransfer.tsx @@ -1,16 +1,19 @@ import { Pool } from '@/lib/modules/pool/PoolProvider' import { constructBaseBuildCallInput } from '@/lib/modules/pool/actions/add-liquidity/handlers/v3Helpers' import { ensureError } from '@/lib/shared/utils/errors' +import { get24HoursFromNowInSecs } from '@/lib/shared/utils/time' import { AddLiquidityBaseQueryOutput, AddLiquidityQueryOutput, Address, + MaxAllowanceTransferAmount, Permit2, Permit2Helper, PublicWalletClient, + TokenAmount, } from '@balancer/sdk' import { HumanTokenAmountWithAddress } from '../../token.types' -import { NoncesByTokenAddress } from './usePermit2Nonces' +import { NoncesByTokenAddress } from './usePermit2Allowance' export interface Permit2AddLiquidityInput { account: Address @@ -57,11 +60,26 @@ async function signPermit2({ sdkQueryOutput: permit2Input.sdkQueryOutput as AddLiquidityBaseQueryOutput, pool, }) + const signature = await Permit2Helper.signAddLiquidityApproval({ ...baseInput, client: sdkClient, owner: permit2Input.account, nonces: baseInput.amountsIn.map(a => nonces[a.token.address]), + amountsIn: maximizePositiveAmounts(baseInput.amountsIn), + // Permit2 allowance expires in 24H + expirations: baseInput.amountsIn.map(() => get24HoursFromNowInSecs()), }) return signature } + +// Maximize amounts for permit2 approval for amounts > 0n +function maximizePositiveAmounts(amountsIn: TokenAmount[]): TokenAmount[] { + return amountsIn.map( + item => + ({ + ...item, + amount: item.amount > 0n ? MaxAllowanceTransferAmount : item.amount, + } as TokenAmount) + ) +} diff --git a/lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx b/lib/modules/tokens/approvals/permit2/usePermit2Allowance.tsx similarity index 59% rename from lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx rename to lib/modules/tokens/approvals/permit2/usePermit2Allowance.tsx index 9003de602..0d2608eef 100644 --- a/lib/modules/tokens/approvals/permit2/usePermit2Nonces.tsx +++ b/lib/modules/tokens/approvals/permit2/usePermit2Allowance.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { getGqlChain, getNetworkConfig } from '@/lib/config/app.config' import { permit2Abi } from '@balancer/sdk' import { zipObject } from 'lodash' @@ -5,6 +6,10 @@ import { Address } from 'viem' import { useReadContracts } from 'wagmi' export type NoncesByTokenAddress = Record +export type ExpirationByTokenAddress = Record +export type AllowedAmountsByTokenAddress = Record + +export type Permit2AllowanceResult = ReturnType type Params = { chainId: number @@ -12,7 +17,7 @@ type Params = { owner?: Address enabled: boolean } -export function usePermit2Nonces({ chainId, tokenAddresses, owner, enabled }: Params) { +export function usePermit2Allowance({ chainId, tokenAddresses, owner, enabled }: Params) { const networkConfig = getNetworkConfig(getGqlChain(chainId)) const permit2Address = networkConfig.contracts.permit2! const balancerRouter = networkConfig.contracts.balancer.router! @@ -45,8 +50,26 @@ export function usePermit2Nonces({ chainId, tokenAddresses, owner, enabled }: Pa ) : undefined + const expirations: ExpirationByTokenAddress | undefined = + tokenAddresses && data + ? zipObject( + tokenAddresses, + data.map(result => result[1]) + ) + : undefined + + const allowedAmounts: AllowedAmountsByTokenAddress | undefined = + tokenAddresses && data + ? zipObject( + tokenAddresses, + data.map(result => result[0]) + ) + : undefined + return { - isLoadingNonces: isLoading, + isLoadingPermit2Allowances: isLoading, nonces, + expirations, + allowedAmounts, } } diff --git a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx index 12efd7af2..c8e819ea4 100644 --- a/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx +++ b/lib/modules/tokens/approvals/permit2/useSignPermit2Transfer.tsx @@ -16,7 +16,7 @@ import { useTokens } from '../../TokensProvider' import { HumanTokenAmountWithAddress } from '../../token.types' import { usePermit2Signature } from './Permit2SignatureProvider' import { signPermit2TokenTransfer } from './signPermit2TokenTransfer' -import { NoncesByTokenAddress } from './usePermit2Nonces' +import { NoncesByTokenAddress } from './usePermit2Allowance' export type AddLiquidityPermit2Params = { humanAmountsIn: HumanTokenAmountWithAddress[] diff --git a/lib/modules/transactions/transaction-steps/TransactionStepButton.tsx b/lib/modules/transactions/transaction-steps/TransactionStepButton.tsx index b197b309b..4830f3eb6 100644 --- a/lib/modules/transactions/transaction-steps/TransactionStepButton.tsx +++ b/lib/modules/transactions/transaction-steps/TransactionStepButton.tsx @@ -33,7 +33,11 @@ export function TransactionStepButton({ step }: Props) { const hasSimulationError = simulation.isError const isIdle = isConnected && simulation.isStale && !simulation.data const isButtonDisabled = - transactionState === TransactionState.Loading || hasSimulationError || isIdle || isComplete + transactionState === TransactionState.Loading || + hasSimulationError || + isIdle || + isComplete || + !executeAsync // no executeAsync is undefined while the txConfig is being built async function handleOnClick() { setExecutionError(undefined) diff --git a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx index a559a74e7..88bded7e6 100644 --- a/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx +++ b/lib/modules/transactions/transaction-steps/useSignPermit2Step.tsx @@ -1,30 +1,38 @@ 'use client' +import { getChainId } from '@/lib/config/app.config' import { ConnectWallet } from '@/lib/modules/web3/ConnectWallet' import { useUserAccount } from '@/lib/modules/web3/UserAccountProvider' import { BalAlert } from '@/lib/shared/components/alerts/BalAlert' import { Button, VStack } from '@chakra-ui/react' import { useMemo } from 'react' +import { getTokenAddresses } from '../../pool/actions/LiquidityActionHelpers' +import { usePermit2Allowance } from '../../tokens/approvals/permit2/usePermit2Allowance' import { AddLiquidityPermit2Params, useSignPermit2Transfer, } from '../../tokens/approvals/permit2/useSignPermit2Transfer' +import { SignatureState } from '../../web3/signatures/signature.helpers' import { useChainSwitch } from '../../web3/useChainSwitch' import { TransactionStep } from './lib' -import { usePermit2Nonces } from '../../tokens/approvals/permit2/usePermit2Nonces' -import { getChainId } from '@/lib/config/app.config' -import { SignatureState } from '../../web3/signatures/signature.helpers' +import { hasValidPermit2 } from '../../tokens/approvals/permit2/permit2.helpers' -export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep { +/* + Returns a transaction step to sign a permit2 transfer for the given pool and token amounts + If the permit2 allowance is expired for one of the positive token amounts in: returns undefined + */ +export function useSignPermit2Step(params: AddLiquidityPermit2Params): TransactionStep | undefined { const { isConnected, userAddress } = useUserAccount() - const { isLoadingNonces, nonces } = usePermit2Nonces({ + const { isLoadingPermit2Allowances, nonces, expirations, allowedAmounts } = usePermit2Allowance({ chainId: getChainId(params.pool.chain), - tokenAddresses: params.queryOutput?.sdkQueryOutput.amountsIn.map(t => t.token.address), + tokenAddresses: getTokenAddresses(params.queryOutput), owner: userAddress, enabled: params.isPermit2, }) + const isValidPermit2 = hasValidPermit2(params.queryOutput, expirations, allowedAmounts) + const { signPermit2, signPermit2State, @@ -37,7 +45,7 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti getChainId(params.pool.chain) ) - const isLoading = isLoadingTransfer || isLoadingNonces + const isLoading = isLoadingTransfer || isLoadingPermit2Allowances const SignPermitButton = () => ( @@ -61,14 +69,13 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti ) - const isComplete = () => signPermit2State === SignatureState.Completed + const isComplete = () => signPermit2State === SignatureState.Completed || isValidPermit2 return useMemo( () => ({ id: 'sign-permit2', stepType: 'signPermit2', labels: { - // TODO: display nested permit tokens in Step Tracker title: `Permit on balancer`, init: `Permit transfer`, tooltip: 'Sign permit2 transfer', @@ -77,6 +84,6 @@ export function useSignPermit2Step(params: AddLiquidityPermit2Params): Transacti renderAction: () => , }), // eslint-disable-next-line react-hooks/exhaustive-deps - [signPermit2State, isLoading, isConnected] + [signPermit2State, isLoading, isConnected, isValidPermit2] ) } diff --git a/lib/modules/web3/contracts/useManagedSendTransaction.ts b/lib/modules/web3/contracts/useManagedSendTransaction.ts index 680c1bb0f..2abb4d65b 100644 --- a/lib/modules/web3/contracts/useManagedSendTransaction.ts +++ b/lib/modules/web3/contracts/useManagedSendTransaction.ts @@ -127,26 +127,28 @@ export function useManagedSendTransaction({ hash: bundle.result.data?.transactionHash, }) - const managedSendAsync = async () => { - if (!estimateGasQuery.data) return - if (!txConfig?.to) return - try { - return writeMutation.sendTransactionAsync({ - chainId, - to: txConfig.to, - data: txConfig.data, - value: txConfig.value, - gas: estimateGasQuery.data, - }) - } catch (e: unknown) { - captureWagmiExecutionError(e, 'Error in send transaction execution', { - chainId, - txConfig, - gas: estimateGasQuery.data, - }) - throw e - } - } + const managedSendAsync = txConfig + ? async () => { + if (!estimateGasQuery.data) return + if (!txConfig?.to) return + try { + return writeMutation.sendTransactionAsync({ + chainId, + to: txConfig.to, + data: txConfig.data, + value: txConfig.value, + gas: estimateGasQuery.data, + }) + } catch (e: unknown) { + captureWagmiExecutionError(e, 'Error in send transaction execution', { + chainId, + txConfig, + gas: estimateGasQuery.data, + }) + throw e + } + } + : undefined return { ...bundle, diff --git a/lib/shared/utils/time.ts b/lib/shared/utils/time.ts index 99c2779c8..1622be5a1 100644 --- a/lib/shared/utils/time.ts +++ b/lib/shared/utils/time.ts @@ -101,6 +101,10 @@ export function getTimestampSecondsFromNow(secs: number): number { return Math.ceil(Date.now() / 1000) + secs } +export function getNowTimestampInSecs(): number { + return Math.floor(Date.now() / 1000) +} + export function getTimestampInMinsFromNow(mins: number) { const nowInSecs = Date.now() / 1000 const secondsToAdd = mins * 60 @@ -121,3 +125,7 @@ export function getTimestamp() { minsAgo: (minutes: number) => sub(millisecondsToSeconds(new Date().getTime()), { minutes }), } } + +export function get24HoursFromNowInSecs() { + return getTimestampInMinsFromNow(24 * 60) +}