diff --git a/src/fetchApproveToken.ts b/src/fetchApproveToken.ts new file mode 100644 index 0000000..4197037 --- /dev/null +++ b/src/fetchApproveToken.ts @@ -0,0 +1,23 @@ +import { ethers } from 'ethers'; + +export async function fetchApproveToken({ + provider, + walletAddress, + tokenAddress, + spenderAddress, + allowance, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + tokenAddress: string; + spenderAddress: string; + allowance: ethers.BigNumber; +}) { + const signer = provider.getSigner(walletAddress); + const Token = new ethers.Contract(tokenAddress, ['function approve(address spender, uint256 amount) returns (bool)'], signer); + const tx: ethers.ContractTransaction = await Token.approve(spenderAddress, allowance); + console.log({ tx }); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchMintUsd.ts b/src/fetchMintUsd.ts new file mode 100644 index 0000000..f1fa9c3 --- /dev/null +++ b/src/fetchMintUsd.ts @@ -0,0 +1,39 @@ +import { ethers } from 'ethers'; + +export async function fetchMintUsd({ + provider, + walletAddress, + CoreProxyContract, + accountId, + poolId, + tokenAddress, + mintUsdAmount, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + CoreProxyContract: { address: string; abi: string[] }; + accountId: ethers.BigNumber; + poolId: ethers.BigNumber; + tokenAddress: string; + mintUsdAmount: ethers.BigNumber; +}) { + const signer = provider.getSigner(walletAddress); + + const CoreProxy = new ethers.Contract(CoreProxyContract.address, CoreProxyContract.abi, signer); + const mintUsdTxnArgs = [ + // + accountId, + poolId, + tokenAddress, + mintUsdAmount, + ]; + console.log('mintUsdTxnArgs', mintUsdTxnArgs); + + console.time('mintUsd'); + const tx: ethers.ContractTransaction = await CoreProxy.mintUsd(...mintUsdTxnArgs); + console.timeEnd('mintUsd'); + console.log({ tx }); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchMintUsdWithPriceUpdate.ts b/src/fetchMintUsdWithPriceUpdate.ts new file mode 100644 index 0000000..a90d75c --- /dev/null +++ b/src/fetchMintUsdWithPriceUpdate.ts @@ -0,0 +1,64 @@ +import { ethers } from 'ethers'; + +export async function fetchMintUsdWithPriceUpdate({ + provider, + walletAddress, + CoreProxyContract, + MulticallContract, + accountId, + poolId, + tokenAddress, + mintUsdAmount, + 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; + mintUsdAmount: 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 mintUsdTxnArgs = [ + // + accountId, + poolId, + tokenAddress, + mintUsdAmount, + ]; + console.log({ mintUsdTxnArgs }); + + const mintUsdTxn = { + target: CoreProxyContract.address, + callData: CoreProxyInterface.encodeFunctionData('mintUsd', [...mintUsdTxnArgs]), + value: 0, + requireSuccess: true, + }; + console.log({ mintUsdTxn }); + + const signer = provider.getSigner(walletAddress); + const multicallTxn = { + from: walletAddress, + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, mintUsdTxn]]), + value: priceUpdateTxn.value, + }; + console.log({ multicallTxn }); + + console.time('mintUsd'); + const tx: ethers.ContractTransaction = await signer.sendTransaction(multicallTxn); + console.timeEnd('mintUsd'); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchPerpsCommitOrder.ts b/src/fetchPerpsCommitOrder.ts new file mode 100644 index 0000000..f28cd18 --- /dev/null +++ b/src/fetchPerpsCommitOrder.ts @@ -0,0 +1,31 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsCommitOrder({ + walletAddress, + provider, + PerpsMarketProxyContract, + orderCommitmentArgs, +}: { + walletAddress?: string; + provider: ethers.providers.Web3Provider; + PerpsMarketProxyContract: { address: string; abi: string[] }; + orderCommitmentArgs: { + marketId: string; + accountId: ethers.BigNumber; + sizeDelta: ethers.BigNumber; + settlementStrategyId: string; + acceptablePrice: ethers.BigNumber; + referrer: string; + trackingCode: string; + }; +}) { + const signer = provider.getSigner(walletAddress); + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, signer); + + console.time('fetchPerpsCommitOrder'); + const tx: ethers.ContractTransaction = await PerpsMarketProxy.commitOrder(orderCommitmentArgs); + console.timeEnd('fetchPerpsCommitOrder'); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchPerpsCommitOrderWithPriceUpdate.ts b/src/fetchPerpsCommitOrderWithPriceUpdate.ts new file mode 100644 index 0000000..f126167 --- /dev/null +++ b/src/fetchPerpsCommitOrderWithPriceUpdate.ts @@ -0,0 +1,58 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsCommitOrderWithPriceUpdate({ + walletAddress, + provider, + PerpsMarketProxyContract, + MulticallContract, + orderCommitmentArgs, + priceUpdateTxn, +}: { + walletAddress?: string; + provider: ethers.providers.Web3Provider; + PerpsMarketProxyContract: { address: string; abi: string[] }; + MulticallContract: { address: string; abi: string[] }; + orderCommitmentArgs: { + marketId: string; + accountId: ethers.BigNumber; + sizeDelta: ethers.BigNumber; + settlementStrategyId: string; + acceptablePrice: ethers.BigNumber; + referrer: string; + trackingCode: string; + }; + priceUpdateTxn: { + target: string; + callData: string; + value: number; + requireSuccess: boolean; + }; +}) { + const PerpsMarketPoxyInterface = new ethers.utils.Interface(PerpsMarketProxyContract.abi); + const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); + + const commitOrderTxn = { + target: PerpsMarketProxyContract.address, + callData: PerpsMarketPoxyInterface.encodeFunctionData('commitOrder', [orderCommitmentArgs]), + value: 0, + requireSuccess: true, + }; + console.log({ commitOrderTxn }); + const signer = provider.getSigner(walletAddress); + + const multicallTxn = { + from: walletAddress, + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, commitOrderTxn]]), + value: priceUpdateTxn.value, + }; + console.log({ multicallTxn }); + + console.time('fetchPerpsCommitOrderWithPriceUpdate'); + const tx: ethers.ContractTransaction = await signer.sendTransaction(multicallTxn); + console.timeEnd('fetchPerpsCommitOrderWithPriceUpdate'); + + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchPerpsGetAvailableMargin.ts b/src/fetchPerpsGetAvailableMargin.ts new file mode 100644 index 0000000..12722a2 --- /dev/null +++ b/src/fetchPerpsGetAvailableMargin.ts @@ -0,0 +1,18 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsGetAvailableMargin({ + provider, + perpsAccountId, + PerpsMarketProxyContract, +}: { + provider?: ethers.providers.BaseProvider; + perpsAccountId: ethers.BigNumber; + PerpsMarketProxyContract: { address: string; abi: string[] }; +}) { + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + console.time('fetchPerpsGetAvailableMargin'); + const availableMargin = await PerpsMarketProxy.getAvailableMargin(perpsAccountId); + console.timeEnd('fetchPerpsGetAvailableMargin'); + console.log({ availableMargin }); + return availableMargin; +} diff --git a/src/fetchPerpsGetMarketSummary.ts b/src/fetchPerpsGetMarketSummary.ts new file mode 100644 index 0000000..4dfa491 --- /dev/null +++ b/src/fetchPerpsGetMarketSummary.ts @@ -0,0 +1,23 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsGetMarketSummary({ + provider, + perpsMarketId, + PerpsMarketProxyContract, +}: { + provider: ethers.providers.BaseProvider; + perpsMarketId: ethers.BigNumber; + PerpsMarketProxyContract: { address: string; abi: string[] }; +}): Promise<{ + skew: ethers.BigNumber; + size: ethers.BigNumber; + maxOpenInterest: ethers.BigNumber; + currentFundingRate: ethers.BigNumber; + currentFundingVelocity: ethers.BigNumber; + indexPrice: ethers.BigNumber; +}> { + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + const marketSummary = await PerpsMarketProxy.getMarketSummary(perpsMarketId); + console.log({ marketSummary }); + return marketSummary; +} diff --git a/src/fetchPerpsGetMarketSummaryWithPriceUpdate.ts b/src/fetchPerpsGetMarketSummaryWithPriceUpdate.ts new file mode 100644 index 0000000..bc9ac98 --- /dev/null +++ b/src/fetchPerpsGetMarketSummaryWithPriceUpdate.ts @@ -0,0 +1,57 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsGetMarketSummaryWithPriceUpdate({ + provider, + perpsMarketId, + PerpsMarketProxyContract, + MulticallContract, + priceUpdateTxn, +}: { + provider: ethers.providers.BaseProvider; + perpsMarketId: ethers.BigNumber; + PerpsMarketProxyContract: { address: string; abi: string[] }; + MulticallContract: { address: string; abi: string[] }; + priceUpdateTxn: { + target: string; + callData: string; + value: number; + requireSuccess: boolean; + }; +}): Promise<{ + skew: ethers.BigNumber; + size: ethers.BigNumber; + maxOpenInterest: ethers.BigNumber; + currentFundingRate: ethers.BigNumber; + currentFundingVelocity: ethers.BigNumber; + indexPrice: ethers.BigNumber; +}> { + const PerpsMarketProxyInterface = new ethers.utils.Interface(PerpsMarketProxyContract.abi); + const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); + + const getMarketSummaryTxn = { + target: PerpsMarketProxyContract.address, + callData: PerpsMarketProxyInterface.encodeFunctionData('getMarketSummary', [perpsMarketId]), + value: 0, + requireSuccess: true, + }; + + const response = await provider.call({ + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, getMarketSummaryTxn]]), + value: priceUpdateTxn.value, + }); + + if (response) { + const decodedMulticall = MulticallInterface.decodeFunctionResult('aggregate3Value', response); + if (decodedMulticall?.returnData?.[1]?.returnData) { + const getMarketSummaryTxnData = decodedMulticall.returnData[1].returnData; + const marketSummary = PerpsMarketProxyInterface.decodeFunctionResult('getMarketSummary', getMarketSummaryTxnData); + console.log('>>>>> marketSummary', marketSummary); + return marketSummary[0]; + } + + console.error({ decodedMulticall }); + throw new Error('Unexpected multicall response'); + } + throw new Error('Empty multicall response'); +} diff --git a/src/fetchPerpsTotalCollateralValue.ts b/src/fetchPerpsTotalCollateralValue.ts new file mode 100644 index 0000000..1c2f374 --- /dev/null +++ b/src/fetchPerpsTotalCollateralValue.ts @@ -0,0 +1,18 @@ +import { ethers } from 'ethers'; + +export async function fetchPerpsTotalCollateralValue({ + provider, + PerpsMarketProxyContract, + perpsAccountId, +}: { + provider?: ethers.providers.BaseProvider; + PerpsMarketProxyContract: { address: string; abi: string[] }; + perpsAccountId: ethers.BigNumber; +}) { + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + console.time('fetchPerpsTotalCollateralValue'); + const totalCollateralValue = await PerpsMarketProxy.totalCollateralValue(perpsAccountId); + console.timeEnd('fetchPerpsTotalCollateralValue'); + console.log({ totalCollateralValue }); + return totalCollateralValue; +} diff --git a/src/fetchPriceUpdateTxn.ts b/src/fetchPriceUpdateTxn.ts index a1b8789..7516141 100644 --- a/src/fetchPriceUpdateTxn.ts +++ b/src/fetchPriceUpdateTxn.ts @@ -6,14 +6,17 @@ export async function fetchPriceUpdateTxn({ MulticallContract, PythERC7412WrapperContract, priceIds, + stalenessTolerance: stalenessToleranceFromSpotPriceData, }: { provider: ethers.providers.BaseProvider; MulticallContract: { address: string; abi: string[] }; PythERC7412WrapperContract: { address: string; abi: string[] }; priceIds: string[]; + stalenessTolerance?: ethers.BigNumber; }) { console.time('fetchPriceUpdateTxn'); - const stalenessTolerance = 1800; // half of 3600 required tolerance + const defaultStalenessTolerance = 1800; // half of 3600 required tolerance + const stalenessTolerance = stalenessToleranceFromSpotPriceData || defaultStalenessTolerance; const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); const PythERC7412WrapperInterface = new ethers.utils.Interface(PythERC7412WrapperContract.abi); diff --git a/src/fetchSpotSell.ts b/src/fetchSpotSell.ts new file mode 100644 index 0000000..925202e --- /dev/null +++ b/src/fetchSpotSell.ts @@ -0,0 +1,25 @@ +import { ethers } from 'ethers'; + +export async function fetchSpotSell({ + provider, + walletAddress, + SpotMarketProxyContract, + marketId, + amount, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + SpotMarketProxyContract: { address: string; abi: string[] }; + marketId: 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); + console.timeEnd('fetchSpotSell'); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchSpotSellWithPriceUpdate.ts b/src/fetchSpotSellWithPriceUpdate.ts new file mode 100644 index 0000000..0619295 --- /dev/null +++ b/src/fetchSpotSellWithPriceUpdate.ts @@ -0,0 +1,57 @@ +import { ethers } from 'ethers'; + +export async function fetchSpotSellWithPriceUpdate({ + provider, + walletAddress, + SpotMarketProxyContract, + MulticallContract, + marketId, + amount, + priceUpdateTxn, +}: { + provider: ethers.providers.Web3Provider; + walletAddress: string; + SpotMarketProxyContract: { address: string; abi: string[] }; + MulticallContract: { address: string; abi: string[] }; + marketId: string; + amount: ethers.BigNumber; + priceUpdateTxn: { + target: string; + callData: string; + value: number; + requireSuccess: boolean; + }; +}) { + const SpotMarketProxyInterface = new ethers.utils.Interface(SpotMarketProxyContract.abi); + const MulticallInterface = new ethers.utils.Interface(MulticallContract.abi); + + const sellArgs = [marketId, amount, amount, ethers.constants.AddressZero]; + + console.log({ sellArgs }); + + const sellTnx = { + target: SpotMarketProxyContract.address, + callData: SpotMarketProxyInterface.encodeFunctionData('sell', [...sellArgs]), + value: 0, + requireSuccess: true, + }; + console.log({ sellTnx }); + + const signer = provider.getSigner(walletAddress); + + const multicallTxn = { + from: walletAddress, + to: MulticallContract.address, + data: MulticallInterface.encodeFunctionData('aggregate3Value', [[priceUpdateTxn, sellTnx]]), + value: priceUpdateTxn.value, + }; + console.log({ multicallTxn }); + + console.time('fetchSpotSellWithPriceUpdate'); + const tx: ethers.ContractTransaction = await signer.sendTransaction(multicallTxn); + console.timeEnd('fetchSpotSellWithPriceUpdate'); + + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; +} diff --git a/src/fetchTokenAllowance.ts b/src/fetchTokenAllowance.ts new file mode 100644 index 0000000..b27d4b2 --- /dev/null +++ b/src/fetchTokenAllowance.ts @@ -0,0 +1,16 @@ +import { ethers } from 'ethers'; + +export async function fetchTokenAllowance({ + provider, + tokenAddress, + ownerAddress, + spenderAddress, +}: { + provider: ethers.providers.BaseProvider; + tokenAddress: string; + ownerAddress: string; + spenderAddress: string; +}) { + const Token = new ethers.Contract(tokenAddress, ['function allowance(address owner, address spender) view returns (uint256)'], provider); + return Token.allowance(ownerAddress, spenderAddress); +} diff --git a/src/fetchTokenBalance.ts b/src/fetchTokenBalance.ts new file mode 100644 index 0000000..8c30077 --- /dev/null +++ b/src/fetchTokenBalance.ts @@ -0,0 +1,14 @@ +import { ethers } from 'ethers'; + +export async function fetchTokenBalance({ + provider, + tokenAddress, + ownerAddress, +}: { + provider: ethers.providers.BaseProvider; + tokenAddress: string; + ownerAddress: string; +}) { + const Token = new ethers.Contract(tokenAddress, ['function balanceOf(address account) view returns (uint256)'], provider); + return Token.balanceOf(ownerAddress); +} diff --git a/src/getPythPrice.ts b/src/getPythPrice.ts new file mode 100644 index 0000000..ca40c99 --- /dev/null +++ b/src/getPythPrice.ts @@ -0,0 +1,17 @@ +import { EvmPriceServiceConnection } from '@pythnetwork/pyth-evm-js'; + +const PYTH_MAINNET_ENDPOINT = process.env.PYTH_MAINNET_ENDPOINT || 'https://hermes.pyth.network'; + +export async function getPythPrice({ feedId }: { feedId: string }) { + const priceService = new EvmPriceServiceConnection(PYTH_MAINNET_ENDPOINT); + const feeds = await priceService.getLatestPriceFeeds([feedId]); + + if (!feeds || feeds.length !== 1) { + throw Error(`Price feed not found, feed id: ${feedId}`); + } + + const [feed] = feeds; + const uncheckedPrice = feed.getPriceUnchecked(); + const price = uncheckedPrice.getPriceAsNumberUnchecked(); + return price; +} diff --git a/src/importWeth.ts b/src/importWeth.ts new file mode 100644 index 0000000..e5a53e4 --- /dev/null +++ b/src/importWeth.ts @@ -0,0 +1,47 @@ +const abi = [ + 'function name() view returns (string)', + 'function approve(address guy, uint256 wad) returns (bool)', + 'function totalSupply() view returns (uint256)', + 'function transferFrom(address src, address dst, uint256 wad) returns (bool)', + 'function withdraw(uint256 wad)', + 'function decimals() view returns (uint8)', + 'function balanceOf(address) view returns (uint256)', + 'function symbol() view returns (string)', + 'function transfer(address dst, uint256 wad) returns (bool)', + 'function deposit() payable', + 'function allowance(address, address) view returns (uint256)', + 'event Approval(address indexed src, address indexed guy, uint256 wad)', + 'event Transfer(address indexed src, address indexed dst, uint256 wad)', + 'event Deposit(address indexed dst, uint256 wad)', + 'event Withdrawal(address indexed src, uint256 wad)', +]; + +export async function importWeth(chainId: number, preset: string): Promise<{ address: string; abi: string[] }> { + const deployment = `${Number(chainId).toFixed(0)}-${preset}`; + switch (deployment) { + case '1-main': { + return { address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', abi: abi }; + } + case '11155111-main': { + return { address: '0xf531B8F309Be94191af87605CfBf600D71C2cFe0', abi: abi }; + } + case '10-main': { + return { address: '0x4200000000000000000000000000000000000006', abi: abi }; + } + case '8453-andromeda': { + return { address: '0x4200000000000000000000000000000000000006', abi: abi }; + } + case '84532-andromeda': { + return { address: '0x6E9fd273209C1DfA62a085ae017ff1bc778E4114', abi: abi }; + } + case '42161-main': { + return { address: '0x82af49447d8a07e3bd95bd0d56f35241523fbab1', abi: abi }; + } + case '421614-main': { + return { address: '0x980B62Da83eFf3D4576C647993b0c1D7faf17c73', abi: abi }; + } + default: { + throw new Error(`Unsupported deployment ${deployment} for WETH`); + } + } +} diff --git a/src/index.ts b/src/index.ts index 16fbb4c..6a0fb53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,3 +18,21 @@ export * from './useErrorParser'; export * from './useImports'; export * from './usePriceUpdateTxn'; export * from './useSynthetix'; +export * from './usePerpsGetMarkets'; +export * from './usePerpsGetMarketSummary'; +export * from './usePerpsMetadata'; +export * from './usePerpsAccounts'; +export * from './useMintUsd'; +export * from './useWethDeposit'; +export * from './usePerpsGetAvailableMargin'; +export * from './usePerpsSelectedAccountId'; +export * from './usePerpsCommitOrder'; +export * from './useCollateralTokens'; +export * from './useSelectedCollateralType'; +export * from './useAllPriceFeeds'; +export * from './fetchTokenBalance'; +export * from './fetchTokenAllowance'; +export * from './fetchApproveToken'; +export * from './useSpotGetSettlementStrategy'; +export * from './useSpotSell'; +export * from './useSpotGetPriceData'; diff --git a/src/useAllPriceFeeds.ts b/src/useAllPriceFeeds.ts new file mode 100644 index 0000000..40829c2 --- /dev/null +++ b/src/useAllPriceFeeds.ts @@ -0,0 +1,32 @@ +import { useQuery } from '@tanstack/react-query'; +import { useErrorParser } from './useErrorParser'; +import { useImportExtras } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useAllPriceFeeds() { + const { chainId } = useSynthetix(); + const { data: extras } = useImportExtras(); + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean(chainId && extras), + queryKey: [chainId, 'AllPriceFeeds'], + queryFn: async () => { + if (!(chainId && extras)) { + throw 'OMFG'; + } + return Object.entries(extras) + .filter( + ([key, value]) => + String(value).length === 66 && (key.startsWith('pyth_feed_id_') || (key.startsWith('pyth') && key.endsWith('FeedId'))) + ) + .map(([, value]) => value as string); + }, + staleTime: 60 * 60 * 1000, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/useCollateralTokens.ts b/src/useCollateralTokens.ts new file mode 100644 index 0000000..5a5d35b --- /dev/null +++ b/src/useCollateralTokens.ts @@ -0,0 +1,33 @@ +import { ethers } from 'ethers'; +import React from 'react'; +import { useImportCollateralTokens } from './useImports'; + +export function useCollateralTokens(): Array<{ + address: string; + symbol: string; + name: string; + decimals: number; + depositingEnabled: boolean; + oracleNodeId: string; + tokenAddress: string; + issuanceRatioD18: ethers.BigNumber; + liquidationRatioD18: ethers.BigNumber; + liquidationRewardD18: ethers.BigNumber; + minDelegationD18: ethers.BigNumber; +}> { + const { data: tokens } = useImportCollateralTokens(); + return React.useMemo(() => { + if (tokens) { + return tokens + .filter(({ depositingEnabled }) => depositingEnabled) + .map(({ issuanceRatioD18, liquidationRatioD18, liquidationRewardD18, minDelegationD18, ...rest }) => ({ + ...rest, + issuanceRatioD18: ethers.BigNumber.from(issuanceRatioD18), + liquidationRatioD18: ethers.BigNumber.from(liquidationRatioD18), + liquidationRewardD18: ethers.BigNumber.from(liquidationRewardD18), + minDelegationD18: ethers.BigNumber.from(minDelegationD18), + })); + } + return []; + }, [tokens]); +} diff --git a/src/useImports.ts b/src/useImports.ts index 8280913..e047aa8 100644 --- a/src/useImports.ts +++ b/src/useImports.ts @@ -13,6 +13,7 @@ import { importRewardsDistributors } from './importRewardsDistributors'; import { importSpotMarketProxy } from './importSpotMarketProxy'; import { importSynthTokens } from './importSynthTokens'; import { importSystemToken } from './importSystemToken'; +import { importWeth } from './importWeth'; import { useSynthetix } from './useSynthetix'; export function useImportContract( @@ -179,3 +180,23 @@ export function useImportCollateralTokens() { queryClient ); } + +export function useImportWethContract() { + const { chainId, preset, queryClient } = useSynthetix(); + + return useQuery( + { + queryKey: ['ImportWethContract', chainId, preset], + enabled: Boolean(chainId && preset), + queryFn: () => { + if (!(chainId && preset)) { + throw 'OMFG'; + } + return importWeth(chainId, preset); + }, + staleTime: 60 * 60 * 1000, + refetchInterval: false, + }, + queryClient + ); +} diff --git a/src/useMintUsd.ts b/src/useMintUsd.ts new file mode 100644 index 0000000..831c55f --- /dev/null +++ b/src/useMintUsd.ts @@ -0,0 +1,131 @@ +import { useMutation } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { fetchMintUsd } from './fetchMintUsd'; +import { fetchMintUsdWithPriceUpdate } from './fetchMintUsdWithPriceUpdate'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract, useImportSystemToken } from './useImports'; +import { usePriceUpdateTxn } from './usePriceUpdateTxn'; +import { useSynthetix } from './useSynthetix'; + +export function useMintUsd({ + provider, + walletAddress, + accountId, + collateralTokenAddress, + poolId, + onSuccess, +}: { + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + accountId?: ethers.BigNumber; + collateralTokenAddress?: string; + poolId?: ethers.BigNumber; + onSuccess: () => void; +}) { + const { chainId, queryClient } = useSynthetix(); + const { data: systemToken } = useImportSystemToken(); + + const { data: CoreProxyContract } = useImportContract('CoreProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + + const { data: priceUpdateTxn } = usePriceUpdateTxn({ provider }); + + const errorParser = useErrorParser(); + + return useMutation({ + retry: false, + mutationFn: async (mintUsdAmount: ethers.BigNumber) => { + if ( + !( + chainId && + provider && + walletAddress && + CoreProxyContract && + MulticallContract && + priceUpdateTxn && + accountId && + poolId && + collateralTokenAddress + ) + ) { + throw 'OMFG'; + } + + if (mintUsdAmount.eq(0)) { + throw new Error('Amount required'); + } + + console.log({ priceUpdateTxn }); + + if (priceUpdateTxn.value) { + console.log('-> fetchMintUsdWithPriceUpdate'); + await fetchMintUsdWithPriceUpdate({ + provider, + walletAddress, + CoreProxyContract, + MulticallContract, + accountId, + poolId, + tokenAddress: collateralTokenAddress, + mintUsdAmount, + priceUpdateTxn, + }); + return { priceUpdated: true }; + } + console.log('-> fetchMintUsd'); + await fetchMintUsd({ + walletAddress, + provider, + CoreProxyContract, + accountId, + poolId, + tokenAddress: collateralTokenAddress, + mintUsdAmount, + }); + return { priceUpdated: false }; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + onSuccess: ({ priceUpdated }) => { + if (!queryClient) return; + + if (priceUpdated) { + queryClient.invalidateQueries({ + queryKey: [chainId, 'PriceUpdateTxn'], + }); + } + + // Intentionally do not await + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'PositionDebt', + { CoreProxy: CoreProxyContract?.address, Multicall: MulticallContract?.address }, + { + accountId: accountId?.toHexString(), + tokenAddress: collateralTokenAddress, + }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [ + chainId, + 'AccountAvailableCollateral', + { CoreProxy: CoreProxyContract?.address }, + { + accountId: accountId?.toHexString(), + tokenAddress: systemToken?.address, + }, + ], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'AccountLastInteraction', { CoreProxy: CoreProxyContract?.address }, { accountId: accountId?.toHexString() }], + }); + + onSuccess(); + }, + }); +} diff --git a/src/usePerpsAccounts.ts b/src/usePerpsAccounts.ts new file mode 100644 index 0000000..ac37a03 --- /dev/null +++ b/src/usePerpsAccounts.ts @@ -0,0 +1,40 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsAccounts({ + provider, + walletAddress, +}: { + provider?: ethers.providers.BaseProvider; + walletAddress?: string; +}) { + const { chainId } = useSynthetix(); + const { data: PerpsAccountProxyContract } = useImportContract('PerpsAccountProxy'); + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean(chainId && provider && walletAddress && PerpsAccountProxyContract?.address), + queryKey: [chainId, 'Perps Accounts', { PerpsAccountProxy: PerpsAccountProxyContract?.address }, { ownerAddress: walletAddress }], + queryFn: async () => { + if (!(chainId && provider && walletAddress && PerpsAccountProxyContract?.address)) throw 'OMFG'; + const PerpsAccountProxy = new ethers.Contract(PerpsAccountProxyContract.address, PerpsAccountProxyContract.abi, provider); + const numberOfAccountTokens = await PerpsAccountProxy.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) => PerpsAccountProxy.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/usePerpsCommitOrder.ts b/src/usePerpsCommitOrder.ts new file mode 100644 index 0000000..ddfa842 --- /dev/null +++ b/src/usePerpsCommitOrder.ts @@ -0,0 +1,140 @@ +import { useMutation } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { fetchPerpsCommitOrder } from './fetchPerpsCommitOrder'; +import { fetchPerpsCommitOrderWithPriceUpdate } from './fetchPerpsCommitOrderWithPriceUpdate'; +import { fetchPerpsGetAvailableMargin } from './fetchPerpsGetAvailableMargin'; +import { fetchPerpsTotalCollateralValue } from './fetchPerpsTotalCollateralValue'; +import { getPythPrice } from './getPythPrice'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { usePriceUpdateTxn } from './usePriceUpdateTxn'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsCommitOrder({ + perpsAccountId, + marketId, + provider, + walletAddress, + feedId, + settlementStrategyId, + onSuccess, +}: { + perpsAccountId?: ethers.BigNumber; + marketId: string; + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + feedId?: string; + settlementStrategyId?: string; + onSuccess: () => void; +}) { + const { chainId, queryClient } = useSynthetix(); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + + const { data: priceUpdateTxn } = usePriceUpdateTxn({ provider }); + + const errorParser = useErrorParser(); + + return useMutation({ + retry: false, + mutationFn: async (sizeDelta: ethers.BigNumber) => { + if ( + !( + chainId && + perpsAccountId && + settlementStrategyId && + PerpsMarketProxyContract?.address && + MulticallContract?.address && + priceUpdateTxn && + walletAddress && + feedId && + provider + ) + ) { + throw 'OMFG'; + } + + if (sizeDelta.lte(0)) { + throw new Error('Amount required'); + } + + const availableMargin = await fetchPerpsGetAvailableMargin({ + provider, + perpsAccountId, + PerpsMarketProxyContract, + }); + + if (availableMargin.lt(sizeDelta)) { + throw new Error('Not enough available margin'); + } + + const totalCollateralValue = await fetchPerpsTotalCollateralValue({ + provider, + PerpsMarketProxyContract, + perpsAccountId, + }); + + if (totalCollateralValue.lt(sizeDelta)) { + throw new Error('Total collateral value is less than the size delta'); + } + + const pythPrice = await getPythPrice({ feedId }); + + const orderCommitmentArgs = { + marketId, + accountId: perpsAccountId, + sizeDelta, + settlementStrategyId, + acceptablePrice: ethers.utils.parseEther(Math.floor(pythPrice * (sizeDelta.gt(0) ? 1.05 : 0.95)).toString()), + referrer: ethers.constants.AddressZero, + trackingCode: ethers.utils.formatBytes32String('VD'), + }; + + console.log('priceUpdateTxn', priceUpdateTxn); + + if (priceUpdateTxn.value) { + console.log('-> fetchPerpsCommitOrderWithPriceUpdate'); + await fetchPerpsCommitOrderWithPriceUpdate({ + walletAddress, + provider, + PerpsMarketProxyContract, + MulticallContract, + orderCommitmentArgs, + priceUpdateTxn, + }); + return { priceUpdated: true }; + } + + console.log('-> fetchPerpsCommitOrder'); + await fetchPerpsCommitOrder({ + walletAddress, + provider, + PerpsMarketProxyContract, + orderCommitmentArgs, + }); + 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, 'PerpsGetOrder', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, perpsAccountId], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'Perps GetAvailableMargin', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, perpsAccountId], + }); + onSuccess(); + }, + }); +} diff --git a/src/usePerpsGetAvailableMargin.ts b/src/usePerpsGetAvailableMargin.ts new file mode 100644 index 0000000..8117076 --- /dev/null +++ b/src/usePerpsGetAvailableMargin.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { fetchPerpsGetAvailableMargin } from './fetchPerpsGetAvailableMargin'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { usePerpsSelectedAccountId } from './usePerpsSelectedAccountId'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetAvailableMargin({ + provider, + walletAddress, + perpsAccountId, +}: { provider?: ethers.providers.BaseProvider; walletAddress?: string; perpsAccountId: string }) { + const { chainId } = useSynthetix(); + const selectedAccountId = usePerpsSelectedAccountId({ provider, walletAddress, perpsAccountId }); + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean(chainId && provider && selectedAccountId && PerpsMarketProxyContract?.address), + queryKey: [chainId, 'Perps GetAvailableMargin', { PerpsMarketProxy: PerpsMarketProxyContract?.address }, selectedAccountId], + queryFn: async () => { + if (!(chainId && provider && selectedAccountId && PerpsMarketProxyContract?.address)) { + throw 'OMFG'; + } + + return await fetchPerpsGetAvailableMargin({ + provider, + perpsAccountId: selectedAccountId, + PerpsMarketProxyContract, + }); + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetMarketSummary.ts b/src/usePerpsGetMarketSummary.ts new file mode 100644 index 0000000..503fb7b --- /dev/null +++ b/src/usePerpsGetMarketSummary.ts @@ -0,0 +1,65 @@ +import { useQuery } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { fetchPerpsGetMarketSummary } from './fetchPerpsGetMarketSummary'; +import { fetchPerpsGetMarketSummaryWithPriceUpdate } from './fetchPerpsGetMarketSummaryWithPriceUpdate'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { usePriceUpdateTxn } from './usePriceUpdateTxn'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetMarketSummary({ + provider, + perpsMarketId, +}: { provider?: ethers.providers.BaseProvider; perpsMarketId: ethers.BigNumber }) { + const { chainId } = useSynthetix(); + const { data: priceUpdateTxn } = usePriceUpdateTxn({ provider }); + + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean( + chainId && provider && perpsMarketId && PerpsMarketProxyContract?.address && MulticallContract?.address && priceUpdateTxn + ), + queryKey: [ + chainId, + 'Perps GetMarketSummary', + { PerpsMarketProxy: PerpsMarketProxyContract?.address, Multicall: MulticallContract?.address }, + { perpsMarketId: perpsMarketId.toString() }, + ], + queryFn: async () => { + if (!(chainId && provider && perpsMarketId && PerpsMarketProxyContract?.address && MulticallContract?.address && priceUpdateTxn)) { + throw 'OMFG'; + } + + console.log({ + provider, + perpsMarketId, + PerpsMarketProxyContract, + MulticallContract, + priceUpdateTxn, + }); + + if (priceUpdateTxn.value) { + console.log('-> fetchPerpsGetMarketSummaryWithPriceUpdate'); + return await fetchPerpsGetMarketSummaryWithPriceUpdate({ + provider, + perpsMarketId, + PerpsMarketProxyContract, + MulticallContract, + priceUpdateTxn, + }); + } + + console.log('-> fetchPerpsGetMarketSummary'); + return await fetchPerpsGetMarketSummary({ provider, perpsMarketId, PerpsMarketProxyContract }); + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsGetMarkets.ts b/src/usePerpsGetMarkets.ts new file mode 100644 index 0000000..158e2b7 --- /dev/null +++ b/src/usePerpsGetMarkets.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsGetMarkets({ provider }: { provider?: ethers.providers.BaseProvider }) { + const { chainId } = useSynthetix(); + const errorParser = useErrorParser(); + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + + return useQuery({ + enabled: Boolean(chainId && provider && PerpsMarketProxyContract?.address), + queryKey: [chainId, 'Perps GetMarkets', { PerpsMarketProxy: PerpsMarketProxyContract?.address }], + queryFn: async () => { + if (!(chainId && provider && PerpsMarketProxyContract?.address)) { + throw new Error('OMFG'); + } + + console.time('usePerpsGetMarkets'); + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + console.timeEnd('usePerpsGetMarkets'); + const markets = await PerpsMarketProxy.getMarkets(); + + return markets; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsMetadata.ts b/src/usePerpsMetadata.ts new file mode 100644 index 0000000..a171376 --- /dev/null +++ b/src/usePerpsMetadata.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function usePerpsMetadata({ + provider, + perpsMarketId, +}: { provider?: ethers.providers.BaseProvider; perpsMarketId?: ethers.BigNumber }) { + const { chainId } = useSynthetix(); + const { data: PerpsMarketProxyContract } = useImportContract('PerpsMarketProxy'); + const errorParser = useErrorParser(); + + return useQuery<{ + name: string; + symbol: string; + }>({ + enabled: Boolean(chainId && provider && perpsMarketId && PerpsMarketProxyContract?.address), + queryKey: [ + chainId, + 'Perps Metadata', + { PerpsMarketProxy: PerpsMarketProxyContract?.address }, + { perpsMarketId: perpsMarketId?.toString() }, + ], + queryFn: async () => { + if (!(chainId && provider && perpsMarketId && PerpsMarketProxyContract?.address)) { + throw 'OMFG'; + } + + const PerpsMarketProxy = new ethers.Contract(PerpsMarketProxyContract.address, PerpsMarketProxyContract.abi, provider); + const { symbol, name } = await PerpsMarketProxy.metadata(perpsMarketId); + console.log({ symbol, name }); + return { symbol, name }; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/usePerpsSelectedAccountId.ts b/src/usePerpsSelectedAccountId.ts new file mode 100644 index 0000000..7b54bb2 --- /dev/null +++ b/src/usePerpsSelectedAccountId.ts @@ -0,0 +1,22 @@ +import { ethers } from 'ethers'; +import React from 'react'; +import { usePerpsAccounts } from './usePerpsAccounts'; + +export function usePerpsSelectedAccountId({ + provider, + walletAddress, + perpsAccountId, +}: { provider?: ethers.providers.BaseProvider; walletAddress?: string; perpsAccountId?: string }): ethers.BigNumber | undefined { + const { data: accounts } = usePerpsAccounts({ provider, walletAddress }); + + return React.useMemo(() => { + if (!perpsAccountId) { + return; + } + if (!accounts) { + return; + } + const bigNumberPerpsAccountId = ethers.BigNumber.from(perpsAccountId); + return accounts.find((id) => bigNumberPerpsAccountId.eq(id)); + }, [accounts, perpsAccountId]); +} diff --git a/src/usePriceUpdateTxn.ts b/src/usePriceUpdateTxn.ts index 0e4ab51..bc1843f 100644 --- a/src/usePriceUpdateTxn.ts +++ b/src/usePriceUpdateTxn.ts @@ -1,19 +1,20 @@ import { useQuery } from '@tanstack/react-query'; import type { ethers } from 'ethers'; import { fetchPriceUpdateTxn } from './fetchPriceUpdateTxn'; +import { useAllPriceFeeds } from './useAllPriceFeeds'; import { useErrorParser } from './useErrorParser'; import { useImportContract } from './useImports'; import { useSynthetix } from './useSynthetix'; export function usePriceUpdateTxn({ provider, - priceIds, }: { provider?: ethers.providers.BaseProvider; - priceIds?: string[]; }) { const { chainId, queryClient } = useSynthetix(); const errorParser = useErrorParser(); + const { data: priceIds } = useAllPriceFeeds(); + const { data: MulticallContract } = useImportContract('Multicall'); const { data: PythERC7412WrapperContract } = useImportContract('PythERC7412Wrapper'); diff --git a/src/useSelectedCollateralType.ts b/src/useSelectedCollateralType.ts new file mode 100644 index 0000000..d8f1f26 --- /dev/null +++ b/src/useSelectedCollateralType.ts @@ -0,0 +1,7 @@ +import React from 'react'; +import { useCollateralTokens } from './useCollateralTokens'; + +export function useSelectedCollateralType({ collateralType }: { collateralType: string }) { + const collateralTokens = useCollateralTokens(); + return React.useMemo(() => collateralTokens.find((token) => collateralType === token.address), [collateralTokens, collateralType]); +} diff --git a/src/useSpotGetPriceData.ts b/src/useSpotGetPriceData.ts new file mode 100644 index 0000000..6c90b6e --- /dev/null +++ b/src/useSpotGetPriceData.ts @@ -0,0 +1,35 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useSpotGetPriceData({ provider, synthMarketId }: { provider?: ethers.providers.BaseProvider; synthMarketId?: string }) { + const { chainId } = useSynthetix(); + const { data: SpotMarketProxyContract } = useImportContract('SpotMarketProxy'); + const errorParser = useErrorParser(); + + return useQuery<{ + buyFeedId: string; + sellFeedId: string; + strictPriceStalenessTolerance: ethers.BigNumber; + }>({ + enabled: Boolean(chainId && SpotMarketProxyContract?.address && provider && synthMarketId), + queryKey: [chainId, 'GetPriceData', { SpotMarketProxy: SpotMarketProxyContract?.address }, synthMarketId], + queryFn: async () => { + if (!(chainId && SpotMarketProxyContract?.address && provider && synthMarketId)) { + throw new Error('OMFG'); + } + + const SpotMarketProxy = new ethers.Contract(SpotMarketProxyContract.address, SpotMarketProxyContract.abi, provider); + const priceData = await SpotMarketProxy.getPriceData(synthMarketId); + console.log({ priceData }); + return priceData; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/useSpotGetSettlementStrategy.ts b/src/useSpotGetSettlementStrategy.ts new file mode 100644 index 0000000..8caebdc --- /dev/null +++ b/src/useSpotGetSettlementStrategy.ts @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useSpotGetSettlementStrategy({ + provider, + synthMarketId, + settlementStrategyId, +}: { provider?: ethers.providers.BaseProvider; synthMarketId?: string; settlementStrategyId?: string }) { + const { chainId } = useSynthetix(); + + const { data: SpotMarketProxyContract } = useImportContract('SpotMarketProxy'); + + const errorParser = useErrorParser(); + + return useQuery({ + enabled: Boolean(chainId && SpotMarketProxyContract?.address && provider && synthMarketId && settlementStrategyId), + queryKey: [ + chainId, + 'SpotGetSettlementStrategy', + { SpotMarketProxy: SpotMarketProxyContract?.address }, + { synthMarketId, settlementStrategyId }, + ], + queryFn: async () => { + if (!(chainId && SpotMarketProxyContract?.address && provider && synthMarketId && settlementStrategyId)) { + throw 'OMFG'; + } + + const SpotMarketProxy = new ethers.Contract(SpotMarketProxyContract.address, SpotMarketProxyContract.abi, provider); + const settlementStrategy = await SpotMarketProxy.getSettlementStrategy(synthMarketId, settlementStrategyId); + console.log({ settlementStrategy }); + return settlementStrategy; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + }); +} diff --git a/src/useSpotSell.ts b/src/useSpotSell.ts new file mode 100644 index 0000000..5544f79 --- /dev/null +++ b/src/useSpotSell.ts @@ -0,0 +1,153 @@ +import { useMutation } from '@tanstack/react-query'; +import type { ethers } from 'ethers'; +import { fetchApproveToken } from './fetchApproveToken'; +import { fetchPriceUpdateTxn } from './fetchPriceUpdateTxn'; +import { fetchSpotSell } from './fetchSpotSell'; +import { fetchSpotSellWithPriceUpdate } from './fetchSpotSellWithPriceUpdate'; +import { fetchTokenAllowance } from './fetchTokenAllowance'; +import { fetchTokenBalance } from './fetchTokenBalance'; +import { useAllPriceFeeds } from './useAllPriceFeeds'; +import { useErrorParser } from './useErrorParser'; +import { useImportContract, useImportSystemToken } from './useImports'; +import { useSpotGetPriceData } from './useSpotGetPriceData'; +import { useSpotGetSettlementStrategy } from './useSpotGetSettlementStrategy'; +import { useSynthetix } from './useSynthetix'; + +export function useSpotSell({ + provider, + walletAddress, + synthMarketId, + settlementStrategyId, + synthTokenAddress, + onSuccess, +}: { + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + synthMarketId?: string; + settlementStrategyId?: string; + synthTokenAddress?: string; + onSuccess: () => void; +}) { + const { chainId, queryClient } = useSynthetix(); + const errorParser = useErrorParser(); + + const { data: priceIds } = useAllPriceFeeds(); + const { data: SpotMarketProxyContract } = useImportContract('SpotMarketProxy'); + const { data: MulticallContract } = useImportContract('Multicall'); + const { data: PythERC7412WrapperContract } = useImportContract('PythERC7412Wrapper'); + const { data: systemToken } = useImportSystemToken(); + const { data: spotSettlementStrategy } = useSpotGetSettlementStrategy({ + provider, + synthMarketId, + settlementStrategyId, + }); + const { data: priceData } = useSpotGetPriceData({ provider, synthMarketId }); + + return useMutation({ + mutationFn: async (amount: ethers.BigNumber) => { + if ( + !( + chainId && + SpotMarketProxyContract?.address && + MulticallContract?.address && + PythERC7412WrapperContract?.address && + walletAddress && + synthMarketId && + synthTokenAddress && + systemToken && + provider && + priceIds && + spotSettlementStrategy && + priceData + ) + ) { + throw 'OMFG'; + } + + if (amount.lte(0)) { + throw new Error('Amount required'); + } + + const freshBalance = await fetchTokenBalance({ + provider, + ownerAddress: walletAddress, + tokenAddress: synthTokenAddress, + }); + + if (freshBalance.lt(amount)) { + throw new Error('Not enough balance'); + } + + const freshAllowance = await fetchTokenAllowance({ + provider, + ownerAddress: walletAddress, + tokenAddress: synthTokenAddress, + spenderAddress: SpotMarketProxyContract.address, + }); + + if (freshAllowance.lt(amount)) { + await fetchApproveToken({ + provider, + walletAddress, + tokenAddress: synthTokenAddress, + spenderAddress: SpotMarketProxyContract.address, + allowance: amount.sub(freshAllowance), + }); + } + + const freshPriceUpdateTxn = await fetchPriceUpdateTxn({ + provider, + MulticallContract, + PythERC7412WrapperContract, + priceIds: [spotSettlementStrategy.feedId], + stalenessTolerance: priceData.strictPriceStalenessTolerance, + }); + console.log('freshPriceUpdateTxn', freshPriceUpdateTxn); + + if (freshPriceUpdateTxn.value) { + console.log('-> fetchSpotSellWithPriceUpdate'); + await fetchSpotSellWithPriceUpdate({ + provider, + walletAddress, + SpotMarketProxyContract, + MulticallContract, + marketId: synthMarketId, + amount, + priceUpdateTxn: freshPriceUpdateTxn, + }); + return { priceUpdated: true }; + } + + console.log('-> fetchSpotSell'); + await fetchSpotSell({ + provider, + walletAddress, + SpotMarketProxyContract, + marketId: synthMarketId, + amount, + }); + 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, 'Balance', { tokenAddress: systemToken?.address, ownerAddress: walletAddress }], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'Balance', { tokenAddress: synthTokenAddress, ownerAddress: walletAddress }], + }); + onSuccess(); + }, + }); +} diff --git a/src/useWethDeposit.ts b/src/useWethDeposit.ts new file mode 100644 index 0000000..d139b2f --- /dev/null +++ b/src/useWethDeposit.ts @@ -0,0 +1,58 @@ +import { useMutation } from '@tanstack/react-query'; +import { ethers } from 'ethers'; +import { useErrorParser } from './useErrorParser'; +import { useImportWethContract } from './useImports'; +import { useSynthetix } from './useSynthetix'; + +export function useWethDeposit({ + provider, + walletAddress, + perpsAccountId, + tokenAddress, + onSuccess, +}: { + provider?: ethers.providers.Web3Provider; + walletAddress?: string; + perpsAccountId?: ethers.BigNumber; + tokenAddress?: string; + onSuccess: () => void; +}) { + const { chainId, queryClient } = useSynthetix(); + + const { data: WethContract } = useImportWethContract(); + + const errorParser = useErrorParser(); + + return useMutation({ + mutationFn: async (amount: ethers.BigNumber) => { + if (!(chainId && provider && walletAddress && perpsAccountId && WethContract)) { + throw 'OMFG'; + } + + const signer = provider.getSigner(walletAddress); + const Weth = new ethers.Contract(WethContract.address, WethContract.abi, signer); + const tx = await Weth.deposit({ + value: amount, + }); + const txResult = await tx.wait(); + console.log({ txResult }); + return txResult; + }, + throwOnError: (error) => { + // TODO: show toast + errorParser(error); + return false; + }, + onSuccess: () => { + if (!queryClient) return; + + queryClient.invalidateQueries({ + queryKey: [chainId, 'Balance', { tokenAddress, ownerAddress: walletAddress }], + }); + queryClient.invalidateQueries({ + queryKey: [chainId, 'EthBalance', { ownerAddress: walletAddress }], + }); + onSuccess(); + }, + }); +}