From 64efd481e02c873c5efb11cf8dd1ab69b11499a9 Mon Sep 17 00:00:00 2001 From: Volodymyr Derunov Date: Tue, 1 Oct 2024 17:53:22 +0300 Subject: [PATCH] Add hooks (#3) * Add hooks * Add more hooks * Rename marketId to synthMarketId and perpsMarketId * Refactor: Simplify parameters in useDelegateCollateral --- src/delegateCollateral.ts | 40 +++++ src/delegateCollateralWithPriceUpdate.ts | 70 ++++++++ src/fetchAccountAvailableCollateral.ts | 19 ++ src/fetchPerpsCommitOrder.ts | 2 +- src/fetchPerpsCommitOrderWithPriceUpdate.ts | 2 +- src/fetchPerpsSettleOrder.ts | 23 +++ src/fetchPerpsSettleOrderWithPriceUpdate.ts | 51 ++++++ src/fetchPositionCollateral.ts | 20 +++ src/fetchSpotSell.ts | 6 +- src/fetchSpotSellWithPriceUpdate.ts | 6 +- src/fetchStrictPriceUpdateTxn.ts | 33 ++++ src/getPythVaa.ts | 15 ++ src/index.ts | 12 ++ src/useAccounts.ts | 34 ++++ src/useDelegateCollateral.ts | 186 ++++++++++++++++++++ src/usePerpsCommitOrder.ts | 6 +- src/usePerpsCreateAccount.ts | 49 ++++++ src/usePerpsGetCollateralAmount.ts | 47 +++++ src/usePerpsGetOpenPosition.ts | 48 +++++ src/usePerpsGetOrder.ts | 46 +++++ src/usePerpsGetSettlementStrategy.ts | 50 ++++++ src/usePerpsSettleOrder.ts | 114 ++++++++++++ src/usePositionCollateral.ts | 55 ++++++ src/useSelectedAccountId.ts | 22 +++ src/useSelectedPoolId.ts | 14 ++ src/useSpotSell.ts | 4 +- 26 files changed, 961 insertions(+), 13 deletions(-) create mode 100644 src/delegateCollateral.ts create mode 100644 src/delegateCollateralWithPriceUpdate.ts create mode 100644 src/fetchAccountAvailableCollateral.ts create mode 100644 src/fetchPerpsSettleOrder.ts create mode 100644 src/fetchPerpsSettleOrderWithPriceUpdate.ts create mode 100644 src/fetchPositionCollateral.ts create mode 100644 src/fetchStrictPriceUpdateTxn.ts create mode 100644 src/getPythVaa.ts create mode 100644 src/useAccounts.ts create mode 100644 src/useDelegateCollateral.ts create mode 100644 src/usePerpsCreateAccount.ts create mode 100644 src/usePerpsGetCollateralAmount.ts create mode 100644 src/usePerpsGetOpenPosition.ts create mode 100644 src/usePerpsGetOrder.ts create mode 100644 src/usePerpsGetSettlementStrategy.ts create mode 100644 src/usePerpsSettleOrder.ts create mode 100644 src/usePositionCollateral.ts create mode 100644 src/useSelectedAccountId.ts create mode 100644 src/useSelectedPoolId.ts diff --git a/src/delegateCollateral.ts b/src/delegateCollateral.ts new file mode 100644 index 0000000..bfaac94 --- /dev/null +++ b/src/delegateCollateral.ts @@ -0,0 +1,40 @@ +import { ethers } from 'ethers'; + +export async function delegateCollateral({ + provider, + walletAddress, + CoreProxyContract, + accountId, + poolId, + tokenAddress, + delegateAmount, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + CoreProxyContract: { address: string; abi: string[] }; + accountId: ethers.BigNumber; + poolId: ethers.BigNumber; + tokenAddress: string; + delegateAmount: ethers.BigNumber; +}) { + const signer = provider.getSigner(walletAddress); + const CoreProxy = new ethers.Contract(CoreProxyContract.address, CoreProxyContract.abi, signer); + + const delegateCollateralTxnArgs = [ + // + accountId, + poolId, + tokenAddress, + delegateAmount, + ethers.utils.parseEther('1'), // Leverage + ]; + console.log('delegateCollateralTxnArgs', delegateCollateralTxnArgs); + + console.time('delegateCollateral'); + const tx: ethers.ContractTransaction = await CoreProxy.delegateCollateral(...delegateCollateralTxnArgs); + console.timeEnd('delegateCollateral'); + console.log({ tx }); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/delegateCollateralWithPriceUpdate.ts b/src/delegateCollateralWithPriceUpdate.ts new file mode 100644 index 0000000..3605fa9 --- /dev/null +++ b/src/delegateCollateralWithPriceUpdate.ts @@ -0,0 +1,70 @@ +import { ethers } from 'ethers'; + +export async function delegateCollateralWithPriceUpdate({ + provider, + walletAddress, + CoreProxyContract, + MulticallContract, + accountId, + poolId, + tokenAddress, + delegateAmount, + priceUpdateTxn, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + CoreProxyContract: { address: string; abi: string[] }; + MulticallContract: { address: string; abi: string[] }; + accountId: ethers.BigNumber; + poolId: ethers.BigNumber; + tokenAddress: string; + delegateAmount: ethers.BigNumber; + priceUpdateTxn: { + target: string; + callData: string; + value: number; + requireSuccess: boolean; + }; +}) { + const CoreProxyInterface = new ethers.utils.Interface(CoreProxyContract.abi); + const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); + + const delegateCollateralTxnArgs = [ + // + accountId, + poolId, + tokenAddress, + delegateAmount, + ethers.utils.parseEther('1'), // Leverage + ]; + console.log('delegateCollateralTxnArgs', delegateCollateralTxnArgs); + + const delegateCollateralTxn = { + target: CoreProxyContract.address, + callData: CoreProxyInterface.encodeFunctionData('delegateCollateral', [ + // + ...delegateCollateralTxnArgs, + ]), + value: 0, + requireSuccess: true, + }; + console.log({ delegateCollateralTxn }); + + const signer = provider.getSigner(walletAddress); + + const multicallTxn = { + from: walletAddress, + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, delegateCollateralTxn]]), + value: priceUpdateTxn.value, + }; + console.log({ multicallTxn }); + + console.time('delegateCollateralWithPriceUpdate'); + const tx: ethers.ContractTransaction = await signer.sendTransaction(multicallTxn); + console.timeEnd('delegateCollateralWithPriceUpdate'); + console.log({ tx }); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchAccountAvailableCollateral.ts b/src/fetchAccountAvailableCollateral.ts new file mode 100644 index 0000000..fdc0a19 --- /dev/null +++ b/src/fetchAccountAvailableCollateral.ts @@ -0,0 +1,19 @@ +import { ethers } from 'ethers'; + +export async function fetchAccountAvailableCollateral({ + provider, + CoreProxyContract, + accountId, + tokenAddress, +}: { + provider: ethers.providers.BaseProvider; + CoreProxyContract: { address: string; abi: string[] }; + accountId: ethers.BigNumber; + tokenAddress: string; +}) { + const CoreProxy = new ethers.Contract(CoreProxyContract.address, CoreProxyContract.abi, provider); + console.time('fetchAccountAvailableCollateral'); + const accountAvailableCollateral = await CoreProxy.getAccountAvailableCollateral(accountId, tokenAddress); + console.timeEnd('fetchAccountAvailableCollateral'); + return accountAvailableCollateral; +} diff --git a/src/fetchPerpsCommitOrder.ts b/src/fetchPerpsCommitOrder.ts index f28cd18..a1685ad 100644 --- a/src/fetchPerpsCommitOrder.ts +++ b/src/fetchPerpsCommitOrder.ts @@ -10,7 +10,7 @@ export async function fetchPerpsCommitOrder({ provider: ethers.providers.Web3Provider; PerpsMarketProxyContract: { address: string; abi: string[] }; orderCommitmentArgs: { - marketId: string; + perpsMarketId: string; accountId: ethers.BigNumber; sizeDelta: ethers.BigNumber; settlementStrategyId: string; diff --git a/src/fetchPerpsCommitOrderWithPriceUpdate.ts b/src/fetchPerpsCommitOrderWithPriceUpdate.ts index f126167..c55fc70 100644 --- a/src/fetchPerpsCommitOrderWithPriceUpdate.ts +++ b/src/fetchPerpsCommitOrderWithPriceUpdate.ts @@ -13,7 +13,7 @@ export async function fetchPerpsCommitOrderWithPriceUpdate({ PerpsMarketProxyContract: { address: string; abi: string[] }; MulticallContract: { address: string; abi: string[] }; orderCommitmentArgs: { - marketId: string; + perpsMarketId: string; accountId: ethers.BigNumber; sizeDelta: ethers.BigNumber; settlementStrategyId: string; diff --git a/src/fetchPerpsSettleOrder.ts b/src/fetchPerpsSettleOrder.ts new file mode 100644 index 0000000..abbb0d4 --- /dev/null +++ b/src/fetchPerpsSettleOrder.ts @@ -0,0 +1,23 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsSettleOrder({ + provider, + walletAddress, + PerpsMarketProxyContract, + perpsAccountId, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + PerpsMarketProxyContract: { address: string; abi: string[] }; + perpsAccountId: ethers.BigNumber; +}) { + const signer = provider.getSigner(walletAddress); + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, signer); + + console.time('fetchPerpsSettleOrder'); + const tx: ethers.ContractTransaction = await PerpsMarketProxy.settleOrder(perpsAccountId); + console.timeEnd('fetchPerpsSettleOrder'); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchPerpsSettleOrderWithPriceUpdate.ts b/src/fetchPerpsSettleOrderWithPriceUpdate.ts new file mode 100644 index 0000000..869931a --- /dev/null +++ b/src/fetchPerpsSettleOrderWithPriceUpdate.ts @@ -0,0 +1,51 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsSettleOrderWithPriceUpdate({ + provider, + walletAddress, + PerpsMarketProxyContract, + MulticallContract, + perpsAccountId, + priceUpdateTxn, +}: { + provider: ethers.providers.Web3Provider; + walletAddress?: string; + PerpsMarketProxyContract: { address: string; abi: string[] }; + MulticallContract: { address: string; abi: string[] }; + perpsAccountId: ethers.BigNumber; + priceUpdateTxn: { + target: string; + callData: string; + value: ethers.BigNumber; + requireSuccess: boolean; + }; +}) { + const PerpsMarketPoxyInterface = new ethers.utils.Interface(PerpsMarketProxyContract.abi); + const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); + + const settleOrderTxn = { + target: PerpsMarketProxyContract.address, + callData: PerpsMarketPoxyInterface.encodeFunctionData('settleOrder', [perpsAccountId]), + value: 0, + requireSuccess: true, + }; + console.log({ settleOrderTxn }); + + const signer = provider.getSigner(walletAddress); + + const multicallTxn = { + from: walletAddress, + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, settleOrderTxn]]), + value: priceUpdateTxn.value, + }; + console.log({ multicallTxn }); + + console.time('fetchPerpsSettleOrderWithPriceUpdate'); + const tx: ethers.ContractTransaction = await signer.sendTransaction(multicallTxn); + console.timeEnd('fetchPerpsSettleOrderWithPriceUpdate'); + + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchPositionCollateral.ts b/src/fetchPositionCollateral.ts new file mode 100644 index 0000000..1efbf14 --- /dev/null +++ b/src/fetchPositionCollateral.ts @@ -0,0 +1,20 @@ +import { ethers } from 'ethers'; + +export async function fetchPositionCollateral({ + provider, + CoreProxyContract, + accountId, + poolId, + tokenAddress, +}: { + provider: ethers.providers.BaseProvider; + CoreProxyContract: { address: string; abi: string[] }; + accountId: ethers.BigNumber; + poolId: ethers.BigNumber; + tokenAddress: string; +}) { + const CoreProxy = new ethers.Contract(CoreProxyContract.address, CoreProxyContract.abi, provider); + const positionCollateral = await CoreProxy.getPositionCollateral(accountId, poolId, tokenAddress); + console.log({ positionCollateral }); + return positionCollateral; +} diff --git a/src/fetchSpotSell.ts b/src/fetchSpotSell.ts index 925202e..81260d9 100644 --- a/src/fetchSpotSell.ts +++ b/src/fetchSpotSell.ts @@ -4,20 +4,20 @@ export async function fetchSpotSell({ provider, walletAddress, SpotMarketProxyContract, - marketId, + synthMarketId, amount, }: { provider: ethers.providers.Web3Provider; walletAddress: string; SpotMarketProxyContract: { address: string; abi: string[] }; - marketId: string; + synthMarketId: string; amount: ethers.BigNumber; }) { const signer = provider.getSigner(walletAddress); const SpotMarketProxy = new ethers.Contract(SpotMarketProxyContract.address, SpotMarketProxyContract.abi, signer); console.time('fetchSpotSell'); - const tx: ethers.ContractTransaction = await SpotMarketProxy.sell(marketId, amount, amount, ethers.constants.AddressZero); + const tx: ethers.ContractTransaction = await SpotMarketProxy.sell(synthMarketId, amount, amount, ethers.constants.AddressZero); console.timeEnd('fetchSpotSell'); const txResult = await tx.wait(); console.log({ txResult }); diff --git a/src/fetchSpotSellWithPriceUpdate.ts b/src/fetchSpotSellWithPriceUpdate.ts index 0619295..6e5c3a2 100644 --- a/src/fetchSpotSellWithPriceUpdate.ts +++ b/src/fetchSpotSellWithPriceUpdate.ts @@ -5,7 +5,7 @@ export async function fetchSpotSellWithPriceUpdate({ walletAddress, SpotMarketProxyContract, MulticallContract, - marketId, + synthMarketId, amount, priceUpdateTxn, }: { @@ -13,7 +13,7 @@ export async function fetchSpotSellWithPriceUpdate({ walletAddress: string; SpotMarketProxyContract: { address: string; abi: string[] }; MulticallContract: { address: string; abi: string[] }; - marketId: string; + synthMarketId: string; amount: ethers.BigNumber; priceUpdateTxn: { target: string; @@ -25,7 +25,7 @@ export async function fetchSpotSellWithPriceUpdate({ const SpotMarketProxyInterface = new ethers.utils.Interface(SpotMarketProxyContract.abi); const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); - const sellArgs = [marketId, amount, amount, ethers.constants.AddressZero]; + const sellArgs = [synthMarketId, amount, amount, ethers.constants.AddressZero]; console.log({ sellArgs }); diff --git a/src/fetchStrictPriceUpdateTxn.ts b/src/fetchStrictPriceUpdateTxn.ts new file mode 100644 index 0000000..0cc1437 --- /dev/null +++ b/src/fetchStrictPriceUpdateTxn.ts @@ -0,0 +1,33 @@ +import { ethers } from 'ethers'; +import { getPythVaa } from './getPythVaa'; + +export async function fetchStrictPriceUpdateTxn({ + commitmentTime, + commitmentPriceDelay, + PythERC7412WrapperContract, + feedId, +}: { + commitmentTime: ethers.BigNumber; + commitmentPriceDelay: ethers.BigNumber; + PythERC7412WrapperContract: { address: string; abi: string[] }; + feedId: string; +}) { + console.time('fetchStrictPriceUpdateTxn'); + const PythERC7412WrapperInterface = new ethers.utils.Interface(PythERC7412WrapperContract.abi); + const timestamp = commitmentTime.add(commitmentPriceDelay); + const offchainData = await getPythVaa({ pythPriceFeedId: feedId, timestamp: timestamp.toNumber() }); + const updateType = 2; + const offchainDataEncoded = ethers.utils.defaultAbiCoder.encode( + ['uint8', 'uint64', 'bytes32[]', 'bytes[]'], + [updateType, timestamp, [feedId], [offchainData]] + ); + console.timeEnd('fetchStrictPriceUpdateTxn'); + const priceUpdateTxn = { + target: PythERC7412WrapperContract.address, + callData: PythERC7412WrapperInterface.encodeFunctionData('fulfillOracleQuery', [offchainDataEncoded]), + value: ethers.BigNumber.from(1), + requireSuccess: true, + }; + console.log({ priceUpdateTxn }); + return priceUpdateTxn; +} diff --git a/src/getPythVaa.ts b/src/getPythVaa.ts new file mode 100644 index 0000000..ecda804 --- /dev/null +++ b/src/getPythVaa.ts @@ -0,0 +1,15 @@ +import { EvmPriceServiceConnection } from '@pythnetwork/pyth-evm-js'; + +const PYTH_MAINNET_ENDPOINT = process.env.PYTH_MAINNET_ENDPOINT || 'https://hermes.pyth.network'; + +const priceService = new EvmPriceServiceConnection(PYTH_MAINNET_ENDPOINT); + +function base64ToHex(str: string) { + const raw = Buffer.from(str, 'base64'); + return `0x${raw.toString('hex')}`; +} + +export async function getPythVaa({ pythPriceFeedId, timestamp }: { pythPriceFeedId: string; timestamp: number }) { + const [priceUpdateData] = await priceService.getVaa(pythPriceFeedId, timestamp); + return base64ToHex(priceUpdateData); +} diff --git a/src/index.ts b/src/index.ts index 6a0fb53..35d3e69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,3 +36,15 @@ export * from './fetchApproveToken'; export * from './useSpotGetSettlementStrategy'; export * from './useSpotSell'; export * from './useSpotGetPriceData'; +export * from './usePerpsGetCollateralAmount'; +export * from './usePerpsGetOrder'; +export * from './usePositionCollateral'; +export * from './useDelegateCollateral'; +export * from './useSelectedPoolId'; +export * from './fetchAccountAvailableCollateral'; +export * from './useAccounts'; +export * from './useSelectedAccountId'; +export * from './usePerpsCreateAccount'; +export * from './usePerpsGetOpenPosition'; +export * from './usePerpsGetSettlementStrategy'; +export * from './usePerpsSettleOrder'; diff --git a/src/useAccounts.ts b/src/useAccounts.ts new file mode 100644 index 0000000..b941393 --- /dev/null +++ b/src/useAccounts.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useAccounts({ provider, walletAddress }: { walletAddress?: string; provider?: ethers.providers.BaseProvider }) { + const { chainId } = useSynthetix(); + const errorParser = useErrorParser(); + const { data: AccountProxyContract } = useImportContract('AccountProxy'); + + return useQuery({ + enabled: Boolean(chainId && AccountProxyContract?.address && walletAddress && provider), + queryKey: [chainId, 'Accounts', { AccountProxy: AccountProxyContract?.address }, { ownerAddress: walletAddress }], + queryFn: async () => { + if (!(chainId && AccountProxyContract?.address && walletAddress && provider)) throw 'OMFG'; + const AccountProxy = new ethers.Contract(AccountProxyContract.address, AccountProxyContract.abi, provider); + const numberOfAccountTokens = await AccountProxy.balanceOf(walletAddress); + if (numberOfAccountTokens.eq(0)) { + // No accounts created yet + return []; + } + const accountIndexes = Array.from(Array(numberOfAccountTokens.toNumber()).keys()); + const accounts = await Promise.all(accountIndexes.map((i) => AccountProxy.tokenOfOwnerByIndex(walletAddress, i))); + return accounts; + }, + select: (accounts) => accounts.map((accountId) => ethers.BigNumber.from(accountId)), + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/useDelegateCollateral.ts b/src/useDelegateCollateral.ts new file mode 100644 index 0000000..658bbde --- /dev/null +++ b/src/useDelegateCollateral.ts @@ -0,0 +1,186 @@ +import { useMutation } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { delegateCollateral } from './delegateCollateral'; +import { delegateCollateralWithPriceUpdate } from './delegateCollateralWithPriceUpdate'; +import { fetchAccountAvailableCollateral } from './fetchAccountAvailableCollateral'; +import { fetchPositionCollateral } from './fetchPositionCollateral'; +import { fetchPriceUpdateTxn } from './fetchPriceUpdateTxn'; +import { useAllPriceFeeds } from './useAllPriceFeeds'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useDelegateCollateral({ + provider, + walletAddress, + collateralTypeTokenAddress, + poolId, + accountId, + onSuccess, +}: { + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + collateralTypeTokenAddress?: string; + poolId?: ethers.BigNumber; + accountId?: ethers.BigNumber; + onSuccess: () => void; +}) { + const { chainId, queryClient } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: CoreProxyContract } = useImportContract('CoreProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + const { data: PythERC7412WrapperContract } = useImportContract('PythERC7412Wrapper'); + + const { data: priceIds } = useAllPriceFeeds(); + + return useMutation({ + retry: false, + mutationFn: async (delegateAmountDelta: ethers.BigNumber) => { + if ( + !( + chainId && + CoreProxyContract && + MulticallContract && + PythERC7412WrapperContract && + priceIds && + provider && + walletAddress && + accountId && + poolId && + collateralTypeTokenAddress + ) + ) { + throw 'OMFG'; + } + + if (delegateAmountDelta.eq(0)) { + throw new Error('Amount required'); + } + + const freshPriceUpdateTxn = await fetchPriceUpdateTxn({ + provider, + MulticallContract, + PythERC7412WrapperContract, + priceIds, + }); + console.log('freshPriceUpdateTxn', freshPriceUpdateTxn); + + const freshAccountAvailableCollateral = await fetchAccountAvailableCollateral({ + provider, + CoreProxyContract, + accountId, + tokenAddress: collateralTypeTokenAddress, + }); + console.log('freshAccountAvailableCollateral', freshAccountAvailableCollateral); + + const hasEnoughDeposit = freshAccountAvailableCollateral.gte(delegateAmountDelta); + if (!hasEnoughDeposit) { + throw new Error('Not enough deposit'); + } + + const freshPositionCollateral = await fetchPositionCollateral({ + provider, + CoreProxyContract, + accountId, + poolId, + tokenAddress: collateralTypeTokenAddress, + }); + console.log('freshPositionCollateral', freshPositionCollateral); + + const delegateAmount = freshPositionCollateral.add(delegateAmountDelta); + console.log('delegateAmount', delegateAmount); + + if (freshPriceUpdateTxn.value) { + console.log('-> delegateCollateralWithPriceUpdate'); + await delegateCollateralWithPriceUpdate({ + provider, + walletAddress, + CoreProxyContract, + MulticallContract, + accountId, + poolId, + tokenAddress: collateralTypeTokenAddress, + delegateAmount, + priceUpdateTxn: freshPriceUpdateTxn, + }); + return { priceUpdated: true }; + } + console.log('-> delegateCollateral'); + await delegateCollateral({ + provider, + walletAddress, + CoreProxyContract, + accountId, + poolId, + tokenAddress: collateralTypeTokenAddress, + delegateAmount, + }); + + return { priceUpdated: false }; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + onSuccess: ({ priceUpdated }) => { + if (!queryClient) return; + + if (priceUpdated) { + queryClient.invalidateQueries({ + queryKey: [chainId, 'PriceUpdateTxn', { priceIds: priceIds?.map((p) => p.slice(0, 8)) }], + }); + } + + // Intentionally do not await + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'AccountCollateral', + { CoreProxy: CoreProxyContract?.address, Multicall: MulticallContract?.address }, + { + accountId: accountId?.toHexString(), + tokenAddress: collateralTypeTokenAddress, + }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'AccountAvailableCollateral', + { CoreProxy: CoreProxyContract?.address }, + { + accountId: accountId?.toHexString(), + tokenAddress: collateralTypeTokenAddress, + }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'PositionCollateral', + { CoreProxy: CoreProxyContract?.address }, + { + accountId: accountId?.toHexString(), + poolId: poolId?.toHexString(), + tokenAddress: collateralTypeTokenAddress, + }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'PositionDebt', + { CoreProxy: CoreProxyContract?.address, Multicall: MulticallContract?.address }, + { + accountId: accountId?.toHexString(), + tokenAddress: collateralTypeTokenAddress, + }, + ], + }); + + onSuccess(); + }, + }); +} diff --git a/src/usePerpsCommitOrder.ts b/src/usePerpsCommitOrder.ts index ddfa842..0bb79a1 100644 --- a/src/usePerpsCommitOrder.ts +++ b/src/usePerpsCommitOrder.ts @@ -12,7 +12,7 @@ import { useSynthetix } from './useSynthetix'; export function usePerpsCommitOrder({ perpsAccountId, - marketId, + perpsMarketId, provider, walletAddress, feedId, @@ -20,7 +20,7 @@ export function usePerpsCommitOrder({ onSuccess, }: { perpsAccountId?: ethers.BigNumber; - marketId: string; + perpsMarketId: string; provider?: ethers.providers.Web3Provider; walletAddress?: string; feedId?: string; @@ -82,7 +82,7 @@ export function usePerpsCommitOrder({ const pythPrice = await getPythPrice({ feedId }); const orderCommitmentArgs = { - marketId, + perpsMarketId, accountId: perpsAccountId, sizeDelta, settlementStrategyId, diff --git a/src/usePerpsCreateAccount.ts b/src/usePerpsCreateAccount.ts new file mode 100644 index 0000000..9ed299a --- /dev/null +++ b/src/usePerpsCreateAccount.ts @@ -0,0 +1,49 @@ +import { useMutation } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsCreateAccount({ + provider, + walletAddress, + handleAccountCreated, +}: { walletAddress?: string; provider?: ethers.providers.Web3Provider; handleAccountCreated: (accountId: string) => void }) { + const { chainId, queryClient } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const { data: PerpsAccountProxyContract } = useImportContract('PerpsAccountProxy'); + + return useMutation({ + mutationFn: async () => { + if (!(chainId && PerpsMarketProxyContract && PerpsAccountProxyContract && walletAddress && provider && queryClient)) throw 'OMFG'; + + const signer = provider.getSigner(walletAddress); + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, signer); + const tx: ethers.ContractTransaction = await PerpsMarketProxy['createAccount()'](); + console.log({ tx }); + const txResult = await tx.wait(); + console.log({ txResult }); + + const event = txResult.events?.find((e) => e.event === 'AccountCreated'); + if (event) { + const accountId = event?.args?.accountId?.toString(); + if (accountId) { + queryClient.setQueryData( + [chainId, 'PerpsAccounts', { PerpsAccountProxy: PerpsAccountProxyContract?.address }, { ownerAddress: walletAddress }], + (oldData: string[]) => oldData.concat([accountId]) + ); + handleAccountCreated(accountId); + } + } + + return txResult; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetCollateralAmount.ts b/src/usePerpsGetCollateralAmount.ts new file mode 100644 index 0000000..279f603 --- /dev/null +++ b/src/usePerpsGetCollateralAmount.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +const USDx_MARKET_ID = 0; + +export function usePerpsGetCollateralAmount({ + provider, + perpsAccountId, +}: { + provider?: ethers.providers.BaseProvider; + perpsAccountId?: ethers.BigNumber; +}) { + const { chainId } = useSynthetix(); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean(chainId && PerpsMarketProxyContract?.address && provider && perpsAccountId), + queryKey: [ + chainId, + 'PerpsGetCollateralAmount', + { PerpsMarketProxy: PerpsMarketProxyContract?.address }, + { collateral: USDx_MARKET_ID }, + perpsAccountId, + ], + queryFn: async () => { + if (!(chainId && PerpsMarketProxyContract?.address && provider && perpsAccountId)) { + throw 'OMFG'; + } + + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + const collateralAmount = await PerpsMarketProxy.getCollateralAmount(perpsAccountId, USDx_MARKET_ID); + console.log({ collateralAmount }); + return collateralAmount; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetOpenPosition.ts b/src/usePerpsGetOpenPosition.ts new file mode 100644 index 0000000..4a93232 --- /dev/null +++ b/src/usePerpsGetOpenPosition.ts @@ -0,0 +1,48 @@ +import { useQuery } from '@tanstack/react-query'; +import { type BigNumber, ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetOpenPosition({ + provider, + walletAddress, + perpsAccountId, + perpsMarketId, +}: { provider?: ethers.providers.Web3Provider; walletAddress?: string; perpsAccountId?: ethers.BigNumber; perpsMarketId?: string }) { + const { chainId } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + + return useQuery<{ + accruedFunding: BigNumber; + owedInterest: BigNumber; + positionSize: BigNumber; + totalPnl: BigNumber; + }>({ + enabled: Boolean(chainId && provider && PerpsMarketProxyContract?.address && walletAddress && perpsAccountId && perpsMarketId), + queryKey: [ + chainId, + 'PerpsGetOpenPosition', + { PerpsMarketProxy: PerpsMarketProxyContract?.address }, + { walletAddress, perpsAccountId, perpsMarketId }, + ], + queryFn: async () => { + if (!(chainId && provider && PerpsMarketProxyContract?.address && walletAddress && perpsAccountId && perpsMarketId)) { + throw 'OMFG'; + } + + const signer = provider.getSigner(walletAddress); + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, signer); + const openPosition = await PerpsMarketProxy.getOpenPosition(perpsAccountId, perpsMarketId); + console.log({ openPosition }); + return openPosition; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetOrder.ts b/src/usePerpsGetOrder.ts new file mode 100644 index 0000000..a84671d --- /dev/null +++ b/src/usePerpsGetOrder.ts @@ -0,0 +1,46 @@ +import { useQuery } from '@tanstack/react-query'; +import { type BigNumber, ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetOrder({ + provider, + perpsAccountId, +}: { provider?: ethers.providers.BaseProvider; perpsAccountId?: ethers.BigNumber }) { + const { chainId } = useSynthetix(); + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + + const errorParser = useErrorParser(); + + return useQuery<{ + commitmentTime: BigNumber; + request: { + marketId: BigNumber; + accountId: BigNumber; + sizeDelta: BigNumber; + settlementStrategyId: BigNumber; + acceptablePrice: BigNumber; + trackingCode: string; + referrer: string; + }; + }>({ + enabled: Boolean(chainId && PerpsMarketProxyContract?.address && provider && perpsAccountId), + queryKey: [chainId, 'PerpsGetOrder', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, perpsAccountId], + queryFn: async () => { + if (!(chainId && PerpsMarketProxyContract?.address && provider && perpsAccountId)) { + throw 'OMFG'; + } + + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + const order = await PerpsMarketProxy.getOrder(perpsAccountId); + console.log({ order }); + return order; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetSettlementStrategy.ts b/src/usePerpsGetSettlementStrategy.ts new file mode 100644 index 0000000..e8db89b --- /dev/null +++ b/src/usePerpsGetSettlementStrategy.ts @@ -0,0 +1,50 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetSettlementStrategy({ + provider, + settlementStrategyId, + perpsMarketId, +}: { settlementStrategyId?: string; provider?: ethers.providers.BaseProvider; perpsMarketId?: string }) { + const { chainId } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + + return useQuery<{ + commitmentPriceDelay: ethers.BigNumber; + disabled: boolean; + feedId: string; + priceVerificationContract: string; + settlementDelay: ethers.BigNumber; + settlementReward: ethers.BigNumber; + settlementWindowDuration: ethers.BigNumber; + strategyType: number; + }>({ + enabled: Boolean(chainId && provider && PerpsMarketProxyContract?.address && settlementStrategyId && perpsMarketId), + queryKey: [ + chainId, + 'PerpsGetSettlementStrategy', + { PerpsMarketProxy: PerpsMarketProxyContract?.address }, + { perpsMarketId, settlementStrategyId }, + ], + queryFn: async () => { + if (!(chainId && provider && PerpsMarketProxyContract?.address && settlementStrategyId && perpsMarketId)) { + throw 'OMFG'; + } + + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + const settlementStrategy = await PerpsMarketProxy.getSettlementStrategy(perpsMarketId, settlementStrategyId); + console.log({ settlementStrategy }); + return settlementStrategy; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsSettleOrder.ts b/src/usePerpsSettleOrder.ts new file mode 100644 index 0000000..b7bd6f2 --- /dev/null +++ b/src/usePerpsSettleOrder.ts @@ -0,0 +1,114 @@ +import { useMutation } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { fetchPerpsSettleOrder } from './fetchPerpsSettleOrder'; +import { fetchPerpsSettleOrderWithPriceUpdate } from './fetchPerpsSettleOrderWithPriceUpdate'; +import { fetchStrictPriceUpdateTxn } from './fetchStrictPriceUpdateTxn'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { usePerpsGetOrder } from './usePerpsGetOrder'; +import { usePerpsGetSettlementStrategy } from './usePerpsGetSettlementStrategy'; +import { usePerpsSelectedAccountId } from './usePerpsSelectedAccountId'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsSettleOrder({ + provider, + walletAddress, + perpsMarketId, + perpsAccountIdFromParams, + settlementStrategyId, +}: { + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + perpsMarketId?: string; + perpsAccountIdFromParams?: string; + settlementStrategyId?: string; +}) { + const { chainId, queryClient } = useSynthetix(); + const errorParser = useErrorParser(); + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + const { data: PythERC7412WrapperContract } = useImportContract('PythERC7412Wrapper'); + const perpsAccountId = usePerpsSelectedAccountId({ provider, walletAddress, perpsAccountId: perpsAccountIdFromParams }); + const { data: settlementStrategy } = usePerpsGetSettlementStrategy({ provider, perpsMarketId, settlementStrategyId }); + const { data: order } = usePerpsGetOrder({ provider, perpsAccountId }); + + return useMutation({ + retry: false, + mutationFn: async () => { + if ( + !( + chainId && + provider && + walletAddress && + PerpsMarketProxyContract?.address && + MulticallContract?.address && + PythERC7412WrapperContract?.address && + perpsAccountId && + perpsMarketId && + settlementStrategy && + order + ) + ) { + throw 'OMFG'; + } + + const freshStrictPriceUpdateTxn = await fetchStrictPriceUpdateTxn({ + commitmentTime: order.commitmentTime, + feedId: settlementStrategy.feedId, + commitmentPriceDelay: settlementStrategy.commitmentPriceDelay, + PythERC7412WrapperContract, + }); + + if (freshStrictPriceUpdateTxn.value) { + console.log('-> fetchPerpsSettleOrderWithPriceUpdate'); + await fetchPerpsSettleOrderWithPriceUpdate({ + provider, + walletAddress, + PerpsMarketProxyContract, + MulticallContract, + perpsAccountId, + priceUpdateTxn: freshStrictPriceUpdateTxn, + }); + return { priceUpdated: true }; + } + + console.log('-> fetchPerpsSettleOrder'); + await fetchPerpsSettleOrder({ + provider, + walletAddress, + PerpsMarketProxyContract, + perpsAccountId, + }); + return { priceUpdated: false }; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + onSuccess: ({ priceUpdated }) => { + if (!queryClient) return; + + if (priceUpdated) { + queryClient.invalidateQueries({ + queryKey: [chainId, 'PriceUpdateTxn'], + }); + } + + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'PerpsGetOpenPosition', + { PerpsMarketProxy: PerpsMarketProxyContract?.address }, + { walletAddress, perpsAccountId, perpsMarketId }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'PerpsGetOrder', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, perpsAccountId], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'Perps GetAvailableMargin', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, perpsAccountId], + }); + }, + }); +} diff --git a/src/usePositionCollateral.ts b/src/usePositionCollateral.ts new file mode 100644 index 0000000..5db3b9f --- /dev/null +++ b/src/usePositionCollateral.ts @@ -0,0 +1,55 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { fetchPositionCollateral } from './fetchPositionCollateral'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePositionCollateral({ + provider, + accountId, + poolId, + tokenAddress, +}: { + accountId?: ethers.BigNumber; + poolId?: ethers.BigNumber; + tokenAddress?: string; + provider?: ethers.providers.BaseProvider; +}) { + const { chainId } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: CoreProxyContract } = useImportContract('CoreProxy'); + + return useQuery({ + enabled: Boolean(chainId && CoreProxyContract?.address && provider && accountId && poolId && tokenAddress), + queryKey: [ + chainId, + 'PositionCollateral', + { CoreProxy: CoreProxyContract?.address }, + { + accountId: accountId?.toHexString(), + poolId: poolId?.toHexString(), + tokenAddress, + }, + ], + queryFn: async () => { + if (!(chainId && CoreProxyContract?.address && provider && accountId && poolId && tokenAddress)) { + throw 'OMFG'; + } + return fetchPositionCollateral({ + provider, + CoreProxyContract, + accountId, + poolId, + tokenAddress, + }); + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + select: (positionCollateral) => ethers.BigNumber.from(positionCollateral), + }); +} diff --git a/src/useSelectedAccountId.ts b/src/useSelectedAccountId.ts new file mode 100644 index 0000000..16a420d --- /dev/null +++ b/src/useSelectedAccountId.ts @@ -0,0 +1,22 @@ +import { ethers } from 'ethers'; +import React from 'react'; +import { useAccounts } from './useAccounts'; + +export function useSelectedAccountId({ + accountId, + provider, + walletAddress, +}: { accountId?: string; provider?: ethers.providers.BaseProvider; walletAddress?: string }) { + const { data: accounts } = useAccounts({ provider, walletAddress }); + + return React.useMemo(() => { + if (!accountId) { + return; + } + if (!accounts) { + return; + } + const accountIdBigNumber = ethers.BigNumber.from(accountId); + return accounts.find((id) => accountIdBigNumber.eq(id)); + }, [accounts, accountId]); +} diff --git a/src/useSelectedPoolId.ts b/src/useSelectedPoolId.ts new file mode 100644 index 0000000..586b714 --- /dev/null +++ b/src/useSelectedPoolId.ts @@ -0,0 +1,14 @@ +import { ethers } from 'ethers'; +import React from 'react'; + +const pools = [ethers.BigNumber.from('1')]; + +export function useSelectedPoolId({ poolId }: { poolId?: string }) { + return React.useMemo(() => { + if (!poolId) { + return ethers.BigNumber.from('1'); + } + const bigNumberPoolId = ethers.BigNumber.from(poolId); + return pools.find((id) => bigNumberPoolId.eq(id)); + }, [poolId]); +} diff --git a/src/useSpotSell.ts b/src/useSpotSell.ts index 5544f79..62569ce 100644 --- a/src/useSpotSell.ts +++ b/src/useSpotSell.ts @@ -111,7 +111,7 @@ export function useSpotSell({ walletAddress, SpotMarketProxyContract, MulticallContract, - marketId: synthMarketId, + synthMarketId, amount, priceUpdateTxn: freshPriceUpdateTxn, }); @@ -123,7 +123,7 @@ export function useSpotSell({ provider, walletAddress, SpotMarketProxyContract, - marketId: synthMarketId, + synthMarketId, amount, }); return { priceUpdated: false };