-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add PSM actions to wagmi config and generate code * Add psm action abi * Add helper queries * Add hooks for psm actions * Take care of difference in decimals between assets token and gem * Pass tokens decimals instead of whole tokens * Use generate psm actions abi
- Loading branch information
Showing
12 changed files
with
2,146 additions
and
271 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
61 changes: 61 additions & 0 deletions
61
packages/app/src/domain/psm-actions/redeem-and-swap/gemMinAmountOutQuery.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import { psmActionsAbi } from '@/config/contracts-generated' | ||
import { toBigInt } from '@/utils/bigNumber' | ||
import { queryOptions } from '@tanstack/react-query' | ||
import { erc4626Abi } from 'viem' | ||
import { Config } from 'wagmi' | ||
import { readContract } from 'wagmi/actions' | ||
import { CheckedAddress } from '../../types/CheckedAddress' | ||
import { BaseUnitNumber } from '../../types/NumericValues' | ||
import { calculateGemMinAmountOut } from './utils/calculateGemMinAmountOut' | ||
|
||
export interface GemMinAmountOutKeyParams { | ||
psmActions: CheckedAddress | ||
gemDecimals: number | ||
assetsTokenDecimals: number | ||
sharesAmount: BaseUnitNumber | ||
chainId: number | ||
} | ||
|
||
export interface GemMinAmountOutOptionsParams extends GemMinAmountOutKeyParams { | ||
config: Config | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type | ||
export function gemMinAmountOutQueryOptions({ | ||
psmActions, | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
sharesAmount, | ||
chainId, | ||
config, | ||
}: GemMinAmountOutOptionsParams) { | ||
return queryOptions({ | ||
queryKey: gemMinAmountOutQueryKey({ psmActions, gemDecimals, assetsTokenDecimals, sharesAmount, chainId }), | ||
queryFn: async () => { | ||
const vault = await readContract(config, { | ||
address: psmActions, | ||
abi: psmActionsAbi, | ||
functionName: 'savingsToken', | ||
}) | ||
|
||
const assetsAmount = await readContract(config, { | ||
address: vault, | ||
abi: erc4626Abi, | ||
functionName: 'convertToAssets', | ||
args: [toBigInt(sharesAmount)], | ||
}) | ||
|
||
return calculateGemMinAmountOut({ gemDecimals, assetsTokenDecimals, assetsAmount }) | ||
}, | ||
}) | ||
} | ||
|
||
export function gemMinAmountOutQueryKey({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
psmActions, | ||
sharesAmount, | ||
chainId, | ||
}: GemMinAmountOutKeyParams): unknown[] { | ||
return ['gem-min-amount-out', gemDecimals, assetsTokenDecimals, psmActions, sharesAmount, chainId] | ||
} |
98 changes: 98 additions & 0 deletions
98
packages/app/src/domain/psm-actions/redeem-and-swap/useRedeemAndSwap.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import { psmActionsAbi, psmActionsAddress } from '@/config/contracts-generated' | ||
import { BaseUnitNumber } from '@/domain/types/NumericValues' | ||
import { getMockToken, testAddresses } from '@/test/integration/constants' | ||
import { handlers } from '@/test/integration/mockTransport' | ||
import { setupHookRenderer } from '@/test/integration/setupHookRenderer' | ||
import { toBigInt } from '@/utils/bigNumber' | ||
import { waitFor } from '@testing-library/react' | ||
import { erc4626Abi } from 'viem' | ||
import { mainnet } from 'viem/chains' | ||
import { UseRedeemAndSwap } from './useRedeemAndSwap' | ||
|
||
const gem = getMockToken({ address: testAddresses.token, decimals: 6 }) | ||
const assetsToken = getMockToken({ address: testAddresses.token2, decimals: 18 }) | ||
const account = testAddresses.alice | ||
const sharesAmount = BaseUnitNumber(1) | ||
const savingsToken = testAddresses.token3 | ||
const assetsAmount = BaseUnitNumber(1e18) | ||
|
||
const hookRenderer = setupHookRenderer({ | ||
hook: UseRedeemAndSwap, | ||
account, | ||
handlers: [ | ||
handlers.chainIdCall({ chainId: mainnet.id }), | ||
handlers.balanceCall({ balance: 0n, address: account }), | ||
handlers.contractCall({ | ||
to: psmActionsAddress[mainnet.id], | ||
abi: psmActionsAbi, | ||
functionName: 'savingsToken', | ||
result: savingsToken, | ||
}), | ||
handlers.contractCall({ | ||
to: savingsToken, | ||
abi: erc4626Abi, | ||
functionName: 'convertToAssets', | ||
args: [toBigInt(sharesAmount)], | ||
result: toBigInt(assetsAmount), | ||
}), | ||
], | ||
args: { gem, assetsToken, sharesAmount }, | ||
}) | ||
|
||
describe(UseRedeemAndSwap.name, () => { | ||
it('is not enabled for guest ', async () => { | ||
const { result } = hookRenderer({ account: undefined }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('is not enabled for 0 gem value', async () => { | ||
const { result } = hookRenderer({ args: { sharesAmount: BaseUnitNumber(0), gem, assetsToken } }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('is not enabled when explicitly disabled', async () => { | ||
const { result } = hookRenderer({ args: { enabled: false, sharesAmount, gem, assetsToken } }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('redeems using psm actions', async () => { | ||
const { result } = hookRenderer({ | ||
args: { | ||
gem, | ||
assetsToken, | ||
sharesAmount, | ||
}, | ||
extraHandlers: [ | ||
handlers.contractCall({ | ||
to: psmActionsAddress[mainnet.id], | ||
abi: psmActionsAbi, | ||
functionName: 'redeemAndSwap', | ||
args: [account, toBigInt(sharesAmount), toBigInt(assetsAmount.dividedBy(1e12))], | ||
from: account, | ||
result: 1n, | ||
}), | ||
handlers.mineTransaction(), | ||
], | ||
}) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('ready') | ||
}) | ||
expect((result.current as any).error).toBeUndefined() | ||
|
||
result.current.write() | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('success') | ||
}) | ||
}) | ||
}) |
72 changes: 72 additions & 0 deletions
72
packages/app/src/domain/psm-actions/redeem-and-swap/useRedeemAndSwap.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import { psmActionsConfig } from '@/config/contracts-generated' | ||
import { useContractAddress } from '@/domain/hooks/useContractAddress' | ||
import { toBigInt } from '@/utils/bigNumber' | ||
import { useQuery, useQueryClient } from '@tanstack/react-query' | ||
import { useAccount, useChainId, useConfig } from 'wagmi' | ||
import { ensureConfigTypes, useWrite } from '../../hooks/useWrite' | ||
import { BaseUnitNumber } from '../../types/NumericValues' | ||
import { Token } from '../../types/Token' | ||
import { balances } from '../../wallet/balances' | ||
import { gemMinAmountOutQueryOptions } from './gemMinAmountOutQuery' | ||
|
||
export interface UseRedeemAndSwapArgs { | ||
gem: Token | ||
assetsToken: Token | ||
sharesAmount: BaseUnitNumber | ||
onTransactionSettled?: () => void | ||
enabled?: boolean | ||
} | ||
|
||
// @note: Redeem a specified amount of `savingsToken` from the `savingsToken` | ||
// for `dai` and swap for `gem` in the PSM. Use this if you want to withdraw everything. | ||
// @note: Assumes PSM swap rate between `dai` and `gem` is 1:1. | ||
export function UseRedeemAndSwap({ | ||
gem, | ||
assetsToken, | ||
sharesAmount: _sharesAmount, | ||
onTransactionSettled, | ||
enabled: _enabled = true, | ||
}: UseRedeemAndSwapArgs): ReturnType<typeof useWrite> { | ||
const client = useQueryClient() | ||
const wagmiConfig = useConfig() | ||
const chainId = useChainId() | ||
|
||
const psmActions = useContractAddress(psmActionsConfig.address) | ||
|
||
const { address: receiver } = useAccount() | ||
const sharesAmount = toBigInt(_sharesAmount) | ||
const { data: gemMinAmountOut } = useQuery( | ||
gemMinAmountOutQueryOptions({ | ||
gemDecimals: gem.decimals, | ||
assetsTokenDecimals: assetsToken.decimals, | ||
psmActions, | ||
sharesAmount: _sharesAmount, | ||
chainId, | ||
config: wagmiConfig, | ||
}), | ||
) | ||
|
||
const config = ensureConfigTypes({ | ||
address: psmActions, | ||
abi: psmActionsConfig.abi, | ||
functionName: 'redeemAndSwap', | ||
args: [receiver!, sharesAmount, gemMinAmountOut!], | ||
}) | ||
const enabled = _enabled && _sharesAmount.gt(0) && !!receiver && !!gemMinAmountOut | ||
|
||
return useWrite( | ||
{ | ||
...config, | ||
enabled, | ||
}, | ||
{ | ||
onTransactionSettled: async () => { | ||
void client.invalidateQueries({ | ||
queryKey: balances({ wagmiConfig, chainId, account: receiver }).queryKey, | ||
}) | ||
|
||
onTransactionSettled?.() | ||
}, | ||
}, | ||
) | ||
} |
49 changes: 49 additions & 0 deletions
49
packages/app/src/domain/psm-actions/redeem-and-swap/utils/calculateGemMinAmountOut.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { calculateGemMinAmountOut } from './calculateGemMinAmountOut' | ||
|
||
describe(calculateGemMinAmountOut.name, () => { | ||
const gemDecimals = 6 | ||
const assetsTokenDecimals = 18 | ||
|
||
it('caluclates the gem min value', () => { | ||
expect( | ||
calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount: 10n ** 18n, | ||
}), | ||
).toBe(10n ** 6n) | ||
|
||
expect( | ||
calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount: 999_000000000000000n, | ||
}), | ||
).toBe(999_000n) | ||
|
||
// with rounding | ||
expect( | ||
calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount: 123456_000000000000n, | ||
}), | ||
).toBe(123456n) | ||
|
||
expect( | ||
calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount: 123456_555555555555n, | ||
}), | ||
).toBe(123456n) | ||
|
||
expect( | ||
calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount: 123456_999999999999n, | ||
}), | ||
).toBe(123456n) | ||
}) | ||
}) |
22 changes: 22 additions & 0 deletions
22
packages/app/src/domain/psm-actions/redeem-and-swap/utils/calculateGemMinAmountOut.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { NormalizedUnitNumber } from '@/domain/types/NumericValues' | ||
import { toBigInt } from '@/utils/bigNumber' | ||
import BigNumber from 'bignumber.js' | ||
import { calculateGemConversionFactor } from '../../utils/calculateGemConversionFactor' | ||
|
||
export interface CalculateGemMinAmountOutParams { | ||
gemDecimals: number | ||
assetsTokenDecimals: number | ||
assetsAmount: bigint | ||
} | ||
|
||
export function calculateGemMinAmountOut({ | ||
gemDecimals, | ||
assetsTokenDecimals, | ||
assetsAmount, | ||
}: CalculateGemMinAmountOutParams): bigint { | ||
const gemConversionFactor = calculateGemConversionFactor({ gemDecimals, assetsTokenDecimals }) | ||
const gemMinAmountOut = NormalizedUnitNumber(assetsAmount) | ||
.dividedBy(gemConversionFactor) | ||
.integerValue(BigNumber.ROUND_DOWN) | ||
return toBigInt(gemMinAmountOut) | ||
} |
75 changes: 75 additions & 0 deletions
75
packages/app/src/domain/psm-actions/useSwapAndDeposit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { psmActionsAbi, psmActionsAddress } from '@/config/contracts-generated' | ||
import { BaseUnitNumber } from '@/domain/types/NumericValues' | ||
import { getMockToken, testAddresses } from '@/test/integration/constants' | ||
import { handlers } from '@/test/integration/mockTransport' | ||
import { setupHookRenderer } from '@/test/integration/setupHookRenderer' | ||
import { toBigInt } from '@/utils/bigNumber' | ||
import { waitFor } from '@testing-library/react' | ||
import { mainnet } from 'viem/chains' | ||
import { useSwapAndDeposit } from './useSwapAndDeposit' | ||
|
||
const gem = getMockToken({ address: testAddresses.token, decimals: 6 }) | ||
const assetsToken = getMockToken({ address: testAddresses.token2, decimals: 18 }) | ||
const account = testAddresses.alice | ||
const gemAmount = BaseUnitNumber(1) | ||
|
||
const hookRenderer = setupHookRenderer({ | ||
hook: useSwapAndDeposit, | ||
account, | ||
handlers: [handlers.chainIdCall({ chainId: mainnet.id }), handlers.balanceCall({ balance: 0n, address: account })], | ||
args: { gem, assetsToken, gemAmount }, | ||
}) | ||
|
||
describe(useSwapAndDeposit.name, () => { | ||
it('is not enabled for guest ', async () => { | ||
const { result } = hookRenderer({ account: undefined }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('is not enabled for 0 gem value', async () => { | ||
const { result } = hookRenderer({ args: { gemAmount: BaseUnitNumber(0), gem, assetsToken } }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('is not enabled when explicitly disabled', async () => { | ||
const { result } = hookRenderer({ args: { enabled: false, gemAmount, gem, assetsToken } }) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('disabled') | ||
}) | ||
}) | ||
|
||
it('deposits using psm actions', async () => { | ||
const { result } = hookRenderer({ | ||
args: { gem, gemAmount, assetsToken }, | ||
extraHandlers: [ | ||
handlers.contractCall({ | ||
to: psmActionsAddress[mainnet.id], | ||
abi: psmActionsAbi, | ||
functionName: 'swapAndDeposit', | ||
args: [account, toBigInt(gemAmount), toBigInt(gemAmount.multipliedBy(1e12))], | ||
from: account, | ||
result: 1n, | ||
}), | ||
handlers.mineTransaction(), | ||
], | ||
}) | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('ready') | ||
}) | ||
expect((result.current as any).error).toBeUndefined() | ||
|
||
result.current.write() | ||
|
||
await waitFor(() => { | ||
expect(result.current.status.kind).toBe('success') | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.