diff --git a/examples/pool/set-strategy-config.ts b/examples/pool/set-strategy-config.ts new file mode 100644 index 0000000..f8902a9 --- /dev/null +++ b/examples/pool/set-strategy-config.ts @@ -0,0 +1,54 @@ +import * as dotenv from 'dotenv' +import { + createPublicClient, + createWalletClient, + http, + parseUnits, + zeroHash, +} from 'viem' +import { arbitrumSepolia } from 'viem/chains' +import { privateKeyToAccount } from 'viem/accounts' + +import { setStrategyConfig } from '../../src' + +dotenv.config() + +const main = async () => { + const walletClient = createWalletClient({ + chain: arbitrumSepolia, + account: privateKeyToAccount( + (process.env.OWNER_PRIVATE_KEY || '0x') as `0x${string}`, + ), + transport: http(), + }) + const publicClient = createPublicClient({ + chain: arbitrumSepolia, + transport: http(), + }) + + const transaction = await setStrategyConfig({ + chainId: arbitrumSepolia.id, + userAddress: walletClient.account.address, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + config: { + referenceThreshold: '0.1', + rateA: '0.1', + rateB: '0.1', + minRateA: '0.003', + minRateB: '0.003', + priceThresholdA: '0.1', + priceThresholdB: '0.1', + }, + }) + + const hash = await walletClient.sendTransaction({ + ...transaction, + gasPrice: parseUnits('1', 9), + }) + await publicClient.waitForTransactionReceipt({ hash: hash }) + console.log(`set config hash: ${hash}`) +} + +main() diff --git a/src/apis/pool.ts b/src/apis/pool.ts index 30afd44..5d30211 100644 --- a/src/apis/pool.ts +++ b/src/apis/pool.ts @@ -4,8 +4,8 @@ import { CHAIN_IDS } from '../constants/chain' import { Pool } from '../model/pool' import { CONTRACT_ADDRESSES } from '../constants/addresses' import { toPoolKey } from '../utils/pool-key' -import { fetchOnChainOrders } from '../utils/order' import { REBALANCER_ABI } from '../abis/rebalancer/rebalancer-abi' +import { Market } from '../type' import { fetchMarket } from './market' @@ -15,17 +15,21 @@ export async function fetchPool( tokenAddresses: `0x${string}`[], salt: `0x${string}`, useSubgraph: boolean, + market?: Market, ): Promise { if (tokenAddresses.length !== 2) { throw new Error('Invalid token pair') } - const market = await fetchMarket( - publicClient, - chainId, - tokenAddresses, - useSubgraph, + if (!market) { + market = ( + await fetchMarket(publicClient, chainId, tokenAddresses, useSubgraph) + ).toJson() + } + const poolKey = toPoolKey( + BigInt(market.bidBook.id), + BigInt(market.askBook.id), + salt, ) - const poolKey = toPoolKey(market.bidBook.id, market.askBook.id, salt) const [ { bookIdA, bookIdB, reserveA, reserveB, orderListA, orderListB }, totalSupply, @@ -61,12 +65,6 @@ export async function fetchPool( totalLiquidityB.reserve + totalLiquidityB.cancelable + totalLiquidityB.claimable - const orders = await fetchOnChainOrders( - publicClient, - chainId, - [...orderListA, ...orderListB], - useSubgraph, - ) return new Pool({ chainId, market, @@ -76,10 +74,7 @@ export async function fetchPool( salt, poolKey, totalSupply: BigInt(totalSupply), - decimals: - market.base.decimals > market.quote.decimals - ? market.base.decimals - : market.quote.decimals, + decimals: 18, liquidityA: BigInt(liquidityA), liquidityB: BigInt(liquidityB), cancelableA: BigInt(totalLiquidityA.cancelable), @@ -88,7 +83,7 @@ export async function fetchPool( claimableB: BigInt(totalLiquidityB.claimable), reserveA: BigInt(reserveA), reserveB: BigInt(reserveB), - orderListA: orders.filter((order) => orderListA.includes(BigInt(order.id))), - orderListB: orders.filter((order) => orderListB.includes(BigInt(order.id))), + orderListA: orderListA.map((id: bigint) => BigInt(id)), + orderListB: orderListB.map((id: bigint) => BigInt(id)), }) } diff --git a/src/call.ts b/src/call.ts index 352f493..78369ad 100644 --- a/src/call.ts +++ b/src/call.ts @@ -44,6 +44,7 @@ import { emptyERC20PermitParams } from './constants/permit' import { abs } from './utils/math' import { toBytes32 } from './utils/pool-key' import { OPERATOR_ABI } from './abis/rebalancer/operator-abi' +import { STRATEGY_ABI } from './abis/rebalancer/strategy-abi' /** * Build a transaction to open a market. @@ -1470,7 +1471,7 @@ export const removeLiquidity = async ({ } } -export const rebalance = async ({ +export const refillOrder = async ({ chainId, userAddress, token0, @@ -1526,15 +1527,15 @@ export const rebalance = async ({ ) } -export const updateStrategyPrice = async ({ +export const adjustOrderPrice = async ({ chainId, userAddress, token0, token1, salt, oraclePrice, - priceA, - priceB, + bidPrice, + askPrice, alpha, options, }: { @@ -1544,20 +1545,26 @@ export const updateStrategyPrice = async ({ token1: `0x${string}` salt: `0x${string}` oraclePrice: string // price with currencyA as quote - priceA: string // price with currencyA as quote - priceB: string // price when currencyA as quote + bidPrice: string // price with bookA. bid price + askPrice: string // price with bookA. ask price alpha: string // alpha value, 0 < alpha <= 1 options?: { tickA?: bigint tickB?: bigint - roundingUpPriceA?: boolean - roundingUpPriceB?: boolean + roundingUpBidPrice?: boolean + roundingUpAskPrice?: boolean useSubgraph?: boolean } & DefaultWriteContractOptions }): Promise => { if (Number(alpha) <= 0 || Number(alpha) > 1) { throw new Error('Alpha value must be in the range (0, 1]') } + if (Number(bidPrice) <= 0 || Number(askPrice) <= 0) { + throw new Error('Price must be greater than 0') + } + if (Number(bidPrice) >= Number(askPrice)) { + throw new Error('Bid price must be less than ask price') + } const publicClient = createPublicClient({ chain: CHAIN_MAP[chainId], transport: options?.rpcUrl ? http(options.rpcUrl) : http(), @@ -1582,15 +1589,15 @@ export const updateStrategyPrice = async ({ }) `) } - const [roundingUpPriceA, roundingUpPriceB] = [ - options?.roundingUpPriceA ? options.roundingUpPriceA : false, - options?.roundingUpPriceB ? options.roundingUpPriceB : false, + const [roundingUpBidPrice, roundingUpAskPrice] = [ + options?.roundingUpBidPrice ? options.roundingUpBidPrice : false, + options?.roundingUpAskPrice ? options.roundingUpAskPrice : false, ] const { roundingDownTick: roundingDownTickA, roundingUpTick: roundingUpTickA, } = parsePrice( - Number(priceA), + Number(bidPrice), pool.currencyA.decimals, pool.currencyB.decimals, ) @@ -1598,7 +1605,7 @@ export const updateStrategyPrice = async ({ roundingDownTick: roundingDownTickB, roundingUpTick: roundingUpTickB, } = parsePrice( - Number(priceB), + Number(askPrice), pool.currencyA.decimals, pool.currencyB.decimals, ) @@ -1610,10 +1617,12 @@ export const updateStrategyPrice = async ({ ) const tickA = options?.tickA ? Number(options.tickA) - : Number(roundingUpPriceA ? roundingUpTickA : roundingDownTickA) + : Number(roundingUpBidPrice ? roundingUpTickA : roundingDownTickA) const tickB = options?.tickB ? Number(options.tickB) - : Number(invertTick(roundingUpPriceB ? roundingUpTickB : roundingDownTickB)) + : Number( + invertTick(roundingUpAskPrice ? roundingUpTickB : roundingDownTickB), + ) const alphaRaw = parseUnits(alpha, 6) @@ -1630,3 +1639,114 @@ export const updateStrategyPrice = async ({ options?.gasLimit, ) } + +export const setStrategyConfig = async ({ + chainId, + userAddress, + token0, + token1, + salt, + config, + options, +}: { + chainId: CHAIN_IDS + userAddress: `0x${string}` + token0: `0x${string}` + token1: `0x${string}` + salt: `0x${string}` + config: { + referenceThreshold: string // 0 <= referenceThreshold <= 1 + rateA: string // 0 <= rateA <= 1 + rateB: string // 0 <= rateB <= 1 + minRateA: string // 0 <= minRateA <= rateA + minRateB: string // 0 <= minRateB <= rateB + priceThresholdA: string // 0 <= priceThresholdA <= 1 + priceThresholdB: string // 0 <= priceThresholdB <= 1 + } + options?: { + useSubgraph?: boolean + } & DefaultWriteContractOptions +}): Promise => { + // validate config + if ( + Number(config.referenceThreshold) < 0 || + Number(config.referenceThreshold) > 1 + ) { + throw new Error('Reference threshold must be in the range [0, 1]') + } + if ( + Number(config.priceThresholdA) < 0 || + Number(config.priceThresholdA) > 1 || + Number(config.priceThresholdB) < 0 || + Number(config.priceThresholdB) > 1 + ) { + throw new Error('Price threshold must be in the range [0, 1]') + } + if ( + Number(config.rateA) < 0 || + Number(config.rateA) > 1 || + Number(config.rateB) < 0 || + Number(config.rateB) > 1 + ) { + throw new Error('Rate must be in the range [0, 1]') + } + if ( + Number(config.minRateA) < 0 || + Number(config.minRateA) > 1 || + Number(config.minRateB) < 0 || + Number(config.minRateB) > 1 + ) { + throw new Error('Min rate must be in the range [0, 1]') + } + if ( + Number(config.minRateA) > Number(config.rateA) || + Number(config.minRateB) > Number(config.rateB) + ) { + throw new Error('Min rate must be less or equal to rate') + } + const publicClient = createPublicClient({ + chain: CHAIN_MAP[chainId], + transport: options?.rpcUrl ? http(options.rpcUrl) : http(), + }) + const pool = await fetchPool( + publicClient, + chainId, + [token0, token1], + salt, + !!(options && options.useSubgraph), + ) + if (!pool.isOpened) { + throw new Error(` + Open the pool before set strategy config. + import { openPool } from '@clober/v2-sdk' + + const transaction = await openPool({ + chainId: ${chainId}, + tokenA: '${token0}', + tokenB: '${token1}', + }) + `) + } + + const configRaw = { + referenceThreshold: parseUnits(config.referenceThreshold, 6), + rateA: parseUnits(config.rateA, 6), + rateB: parseUnits(config.rateB, 6), + minRateA: parseUnits(config.minRateA, 6), + minRateB: parseUnits(config.minRateB, 6), + priceThresholdA: parseUnits(config.priceThresholdA, 6), + priceThresholdB: parseUnits(config.priceThresholdB, 6), + } + return buildTransaction( + publicClient, + { + chain: CHAIN_MAP[chainId], + account: userAddress, + address: CONTRACT_ADDRESSES[chainId]!.Strategy, + abi: STRATEGY_ABI, + functionName: 'setConfig', + args: [pool.key, configRaw], + }, + options?.gasLimit, + ) +} diff --git a/src/constants/addresses.ts b/src/constants/addresses.ts index a648c1b..982e2a8 100644 --- a/src/constants/addresses.ts +++ b/src/constants/addresses.ts @@ -22,6 +22,15 @@ export const CONTRACT_ADDRESSES: { Minter: getAddress('0xF2f51B00C2e9b77F23fD66649bbabf8a025c39eF'), Operator: getAddress('0x33559576B062D08230b467ea7DC7Ce75aFcbdE92'), }, + [CHAIN_IDS.CLOBER_TESTNET_2]: { + Controller: getAddress('0xFbbA685a39bE6640B8EB08c6E6DDf2664fD1D668'), + BookManager: getAddress('0xC528b9ED5d56d1D0d3C18A2342954CE1069138a4'), + BookViewer: getAddress('0x73c524e103C94Bf2743659d739586395B1A9e1BE'), + Rebalancer: getAddress('0xCF556d850277BC579c99C0729F4E72e62C57D811'), + Strategy: getAddress('0x8aDF62b0b6078EaE5a2D54e9e5DD2AA71F6748C4'), + Minter: getAddress('0xF2f51B00C2e9b77F23fD66649bbabf8a025c39eF'), + Operator: getAddress('0x33559576B062D08230b467ea7DC7Ce75aFcbdE92'), + }, [CHAIN_IDS.ARBITRUM_SEPOLIA]: { Controller: getAddress('0xFbbA685a39bE6640B8EB08c6E6DDf2664fD1D668'), BookManager: getAddress('0xC528b9ED5d56d1D0d3C18A2342954CE1069138a4'), diff --git a/src/constants/chain.ts b/src/constants/chain.ts index 6f32fdd..aa66802 100644 --- a/src/constants/chain.ts +++ b/src/constants/chain.ts @@ -1,10 +1,11 @@ import { arbitrumSepolia, base, type Chain, zkSync } from 'viem/chains' -import { cloberTestChain } from './test-chain' +import { cloberTestChain, cloberTestChain2 } from './test-chain' import { berachainBartioTestnet } from './bera-bartio-chain' export enum CHAIN_IDS { CLOBER_TESTNET = cloberTestChain.id, + CLOBER_TESTNET_2 = cloberTestChain2.id, ARBITRUM_SEPOLIA = arbitrumSepolia.id, BASE = base.id, BERACHAIN_TESTNET = berachainBartioTestnet.id, @@ -15,6 +16,7 @@ export const CHAIN_MAP: { [chain in CHAIN_IDS]: Chain } = { [CHAIN_IDS.CLOBER_TESTNET]: cloberTestChain, + [CHAIN_IDS.CLOBER_TESTNET_2]: cloberTestChain2, [CHAIN_IDS.ARBITRUM_SEPOLIA]: arbitrumSepolia, [CHAIN_IDS.BASE]: base, [CHAIN_IDS.BERACHAIN_TESTNET]: berachainBartioTestnet, @@ -23,5 +25,6 @@ export const CHAIN_MAP: { export const isTestnetChain = (chainId: CHAIN_IDS): boolean => chainId === CHAIN_IDS.CLOBER_TESTNET || + chainId === CHAIN_IDS.CLOBER_TESTNET_2 || chainId === CHAIN_IDS.ARBITRUM_SEPOLIA || chainId === CHAIN_IDS.BERACHAIN_TESTNET diff --git a/src/constants/currency.ts b/src/constants/currency.ts index ab73684..5fa51b7 100644 --- a/src/constants/currency.ts +++ b/src/constants/currency.ts @@ -22,6 +22,7 @@ export const NATIVE_CURRENCY: { [chain in CHAIN_IDS]: Currency } = { [CHAIN_IDS.CLOBER_TESTNET]: ETH, + [CHAIN_IDS.CLOBER_TESTNET_2]: ETH, [CHAIN_IDS.ARBITRUM_SEPOLIA]: ETH, [CHAIN_IDS.BASE]: ETH, [CHAIN_IDS.BERACHAIN_TESTNET]: BERA, @@ -32,6 +33,9 @@ export const WETH_ADDRESSES: { [chain in CHAIN_IDS]: `0x${string}`[] } = { [CHAIN_IDS.CLOBER_TESTNET]: [zeroAddress], + [CHAIN_IDS.CLOBER_TESTNET_2]: [ + '0xF2e615A933825De4B39b497f6e6991418Fb31b78', // Mock WETH + ], [CHAIN_IDS.ARBITRUM_SEPOLIA]: [ '0xF2e615A933825De4B39b497f6e6991418Fb31b78', // Mock WETH ], @@ -50,6 +54,9 @@ export const STABLE_COIN_ADDRESSES: { [CHAIN_IDS.CLOBER_TESTNET]: [ '0x00BFD44e79FB7f6dd5887A9426c8EF85A0CD23e0', // Mock USDT ], + [CHAIN_IDS.CLOBER_TESTNET_2]: [ + '0x00BFD44e79FB7f6dd5887A9426c8EF85A0CD23e0', // Mock USDT + ], [CHAIN_IDS.ARBITRUM_SEPOLIA]: [ '0x00BFD44e79FB7f6dd5887A9426c8EF85A0CD23e0', // Mock USDT ], diff --git a/src/constants/fee.ts b/src/constants/fee.ts index 396a736..a87c757 100644 --- a/src/constants/fee.ts +++ b/src/constants/fee.ts @@ -6,6 +6,7 @@ export const MAKER_DEFAULT_POLICY: { [chain in CHAIN_IDS]: FeePolicy } = { [CHAIN_IDS.CLOBER_TESTNET]: new FeePolicy(true, -300n), // -0.03%, + [CHAIN_IDS.CLOBER_TESTNET_2]: new FeePolicy(true, -300n), // -0.03%, [CHAIN_IDS.ARBITRUM_SEPOLIA]: new FeePolicy(true, 0n), // 0%, [CHAIN_IDS.BASE]: new FeePolicy(true, 0n), // 0%, [CHAIN_IDS.BERACHAIN_TESTNET]: new FeePolicy(true, 0n), // 0%, @@ -16,6 +17,7 @@ export const TAKER_DEFAULT_POLICY: { [chain in CHAIN_IDS]: FeePolicy } = { [CHAIN_IDS.CLOBER_TESTNET]: new FeePolicy(true, 1000n), // 0.1% + [CHAIN_IDS.CLOBER_TESTNET_2]: new FeePolicy(true, 1000n), // 0.1% [CHAIN_IDS.ARBITRUM_SEPOLIA]: new FeePolicy(true, 100n), // 0.01% [CHAIN_IDS.BASE]: new FeePolicy(true, 100n), // 0.01% [CHAIN_IDS.BERACHAIN_TESTNET]: new FeePolicy(true, 100n), // 0.01% diff --git a/src/constants/subgraph.ts b/src/constants/subgraph.ts index 4b9fb2c..7a7699b 100644 --- a/src/constants/subgraph.ts +++ b/src/constants/subgraph.ts @@ -7,6 +7,8 @@ const SUBGRAPH_URL: { } = { [CHAIN_IDS.CLOBER_TESTNET]: 'https://subgraph.satsuma-prod.com/f6a8c4889b7b/clober/v2-core-subgraph/api', + [CHAIN_IDS.CLOBER_TESTNET_2]: + 'https://subgraph.satsuma-prod.com/f6a8c4889b7b/clober/v2-core-subgraph/api', [CHAIN_IDS.ARBITRUM_SEPOLIA]: 'https://subgraph.satsuma-prod.com/f6a8c4889b7b/clober/v2-core-subgraph/api', [CHAIN_IDS.BASE]: diff --git a/src/constants/test-chain.ts b/src/constants/test-chain.ts index 5d27ed9..d6b5dba 100644 --- a/src/constants/test-chain.ts +++ b/src/constants/test-chain.ts @@ -23,3 +23,27 @@ export const cloberTestChain: Chain = { }, }, } + +export const cloberTestChain2: Chain = { + id: 77772, + name: 'Clober Test Chain 2', + nativeCurrency: { + name: 'Ether', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: { + default: { + http: ['http://127.0.0.1'], + }, + public: { + http: ['http://127.0.0.1'], + }, + }, + contracts: { + multicall3: { + address: '0xca11bde05977b3631167028862be2a173976ca11', + blockCreated: 81930, + }, + }, +} diff --git a/src/model/pool.ts b/src/model/pool.ts index 5b513a6..10cfbb8 100644 --- a/src/model/pool.ts +++ b/src/model/pool.ts @@ -1,12 +1,10 @@ import { formatUnits } from 'viem' -import { CHAIN_IDS, Currency } from '../type' +import { CHAIN_IDS, Currency, Market } from '../type' import { CONTRACT_ADDRESSES } from '../constants/addresses' import { toPoolKey } from '../utils/pool-key' -import { Market } from './market' import { Currency6909 } from './currency' -import { OnChainOpenOrder } from './open-order' export class Pool { key: `0x${string}` @@ -27,8 +25,8 @@ export class Pool { cancelableB: bigint claimableA: bigint claimableB: bigint - orderListA: OnChainOpenOrder[] - orderListB: OnChainOpenOrder[] + orderListA: bigint[] + orderListB: bigint[] constructor({ chainId, @@ -68,17 +66,23 @@ export class Pool { cancelableB: bigint claimableA: bigint claimableB: bigint - orderListA: OnChainOpenOrder[] - orderListB: OnChainOpenOrder[] + orderListA: bigint[] + orderListB: bigint[] }) { this.key = toPoolKey(bookIdA, bookIdB, salt) this.market = market this.isOpened = isOpened this.strategy = CONTRACT_ADDRESSES[chainId]!.Strategy - if (bookIdA === market.bidBook.id && bookIdB === market.askBook.id) { + if ( + bookIdA === BigInt(market.bidBook.id) && + bookIdB === BigInt(market.askBook.id) + ) { this.currencyA = market.bidBook.quote // or market.askBook.base this.currencyB = market.bidBook.base // or market.askBook.quote - } else if (bookIdA === market.askBook.id && bookIdB === market.bidBook.id) { + } else if ( + bookIdA === BigInt(market.askBook.id) && + bookIdB === BigInt(market.bidBook.id) + ) { this.currencyA = market.askBook.quote // or market.bidBook.base this.currencyB = market.askBook.base // or market.bidBook.quote } else if (bookIdA === 0n && bookIdB === 0n) { diff --git a/src/type.ts b/src/type.ts index 9a622e6..f277dc8 100644 --- a/src/type.ts +++ b/src/type.ts @@ -2,7 +2,6 @@ import type { Account } from 'viem' import { CHAIN_IDS } from './constants/chain' import type { Currency, Currency6909 } from './model/currency' -import { OnChainOpenOrder } from './model/open-order' export { CHAIN_IDS } from './constants/chain' export type { Currency } from './model/currency' @@ -44,8 +43,15 @@ export type Pool = { currencyB: Currency reserveA: string reserveB: string - orderListA: OnChainOpenOrder[] - orderListB: OnChainOpenOrder[] + totalSupply: bigint + liquidityA: bigint + liquidityB: bigint + cancelableA: bigint + cancelableB: bigint + claimableA: bigint + claimableB: bigint + orderListA: bigint[] + orderListB: bigint[] } export type Transaction = { diff --git a/src/view.ts b/src/view.ts index 55bc1a9..e845946 100644 --- a/src/view.ts +++ b/src/view.ts @@ -163,6 +163,7 @@ export const getPool = async ({ token1: `0x${string}` salt: `0x${string}` options?: { + market?: Market n?: number useSubgraph?: boolean } & DefaultReadContractOptions @@ -180,17 +181,25 @@ export const getPool = async ({ [token0, token1], salt, !!(options && options.useSubgraph), + options?.market, ) return { chainId, key: pool.key, - market: pool.market.toJson(), + market: pool.market, isOpened: pool.isOpened, strategy: pool.strategy, currencyA: pool.currencyA, currencyB: pool.currencyB, reserveA: pool.reserveA, reserveB: pool.reserveB, + totalSupply: pool.totalSupply, + liquidityA: pool.liquidityA, + liquidityB: pool.liquidityB, + cancelableA: pool.cancelableA, + cancelableB: pool.cancelableB, + claimableA: pool.claimableA, + claimableB: pool.claimableB, orderListA: pool.orderListA, orderListB: pool.orderListB, } diff --git a/test/adjust-order-price.test.ts b/test/adjust-order-price.test.ts new file mode 100644 index 0000000..3612191 --- /dev/null +++ b/test/adjust-order-price.test.ts @@ -0,0 +1,183 @@ +import { beforeEach, expect, test } from 'vitest' +import { + addLiquidity, + adjustOrderPrice, + getPool, + setStrategyConfig, +} from '@clober/v2-sdk' +import { zeroHash } from 'viem' +import BigNumber from 'bignumber.js' + +import { cloberTestChain2 } from '../src/constants/test-chain' + +import { account, FORK_URL } from './utils/constants' +import { createProxyClients2 } from './utils/utils' + +const clients = createProxyClients2( + Array.from({ length: 2 }, () => Math.floor(new Date().getTime())).map( + (id) => id, + ), +) + +beforeEach(async () => { + await Promise.all( + clients.map(({ testClient }) => { + return testClient.reset({ + jsonRpcUrl: FORK_URL, + blockNumber: 84239911n, + }) + }), + ) +}) + +test('Adjust order price', async () => { + const { publicClient, walletClient, testClient } = clients[0] as any + + const poolStep1 = await getPool({ + chainId: cloberTestChain2.id, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + + // Add liquidity to the pool + const { transaction: tx1 } = await addLiquidity({ + chainId: cloberTestChain2.id, + userAddress: account.address, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + amount0: '2000', + amount1: '1.0', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash1 = await walletClient.sendTransaction({ + ...tx1!, + account, + gasPrice: tx1!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash1 }) + + const poolStep2 = await getPool({ + chainId: cloberTestChain2.id, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + + await testClient.impersonateAccount({ + address: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + }) + const transactionForSetConfig = await setStrategyConfig({ + chainId: cloberTestChain2.id, + userAddress: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + config: { + referenceThreshold: '0.1', + rateA: '0.1', + rateB: '0.1', + minRateA: '0.003', + minRateB: '0.003', + priceThresholdA: '0.1', + priceThresholdB: '0.1', + }, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hashForSetConfig = await walletClient.sendTransaction({ + ...transactionForSetConfig!, + account: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + gasPrice: transactionForSetConfig!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hashForSetConfig }) + const adjustOrderPriceTx = await adjustOrderPrice({ + chainId: cloberTestChain2.id, + userAddress: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + oraclePrice: '2620', + bidPrice: '2610', + askPrice: '2630', + alpha: '0.5', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash2 = await walletClient.sendTransaction({ + ...adjustOrderPriceTx!, + account: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + gasPrice: adjustOrderPriceTx!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash2 }) + + const poolStep3 = await getPool({ + chainId: cloberTestChain2.id, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + + expect(BigNumber(poolStep1.reserveA).plus('2000').toString()).toBe( + poolStep2.reserveA, + ) + expect(BigNumber(poolStep1.reserveB).plus('1').toString()).toBe( + poolStep2.reserveB, + ) + expect(poolStep1.cancelableA).toBe(0n) + expect(poolStep1.cancelableB).toBe(0n) + expect(poolStep1.claimableA).toBe(0n) + expect(poolStep1.claimableB).toBe(0n) + + expect(poolStep2.cancelableA).toBe(0n) + expect(poolStep2.cancelableB).toBe(0n) + expect(poolStep2.claimableA).toBe(0n) + expect(poolStep2.claimableB).toBe(0n) + + expect(poolStep3.cancelableA).toBe(99999999n) + expect(poolStep3.cancelableB).toBe(38167546300000000n) + expect(poolStep3.claimableA).toBe(0n) + expect(poolStep3.claimableB).toBe(0n) +}) + +test('Adjust order price with invalid alpha', async () => { + const { publicClient } = clients[0] as any + + await expect( + adjustOrderPrice({ + chainId: cloberTestChain2.id, + userAddress: account.address, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + oraclePrice: '2620', + bidPrice: '2600', + askPrice: '2650', + alpha: '1.1', // Invalid alpha value + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }), + ).rejects.toThrow('Alpha value must be in the range (0, 1]') +}) diff --git a/test/refill-order.test.ts b/test/refill-order.test.ts new file mode 100644 index 0000000..ea703a2 --- /dev/null +++ b/test/refill-order.test.ts @@ -0,0 +1,221 @@ +import { beforeEach, expect, test } from 'vitest' +import { + addLiquidity, + adjustOrderPrice, + approveERC20, + getPool, + marketOrder, + refillOrder, + setStrategyConfig, +} from '@clober/v2-sdk' +import { zeroHash } from 'viem' + +import { cloberTestChain2 } from '../src/constants/test-chain' + +import { account, FORK_URL } from './utils/constants' +import { createProxyClients2 } from './utils/utils' + +const clients = createProxyClients2( + Array.from({ length: 2 }, () => Math.floor(new Date().getTime())).map( + (id) => id, + ), +) + +beforeEach(async () => { + await Promise.all( + clients.map(({ testClient }) => { + return testClient.reset({ + jsonRpcUrl: FORK_URL, + blockNumber: 84239911n, + }) + }), + ) +}) + +test('Refill order', async () => { + const { publicClient, walletClient, testClient } = clients[0] as any + + // Add liquidity to the pool + const { transaction: tx1 } = await addLiquidity({ + chainId: cloberTestChain2.id, + userAddress: account.address, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + amount0: '2000', + amount1: '1.0', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash1 = await walletClient.sendTransaction({ + ...tx1!, + account, + gasPrice: tx1!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash1 }) + + await testClient.impersonateAccount({ + address: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + }) + const transactionForSetConfig = await setStrategyConfig({ + chainId: cloberTestChain2.id, + userAddress: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + config: { + referenceThreshold: '0.1', + rateA: '0.1', + rateB: '0.1', + minRateA: '0.003', + minRateB: '0.003', + priceThresholdA: '0.1', + priceThresholdB: '0.1', + }, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hashForSetConfig = await walletClient.sendTransaction({ + ...transactionForSetConfig!, + account: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + gasPrice: transactionForSetConfig!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hashForSetConfig }) + const adjustOrderPriceTx = await adjustOrderPrice({ + chainId: cloberTestChain2.id, + userAddress: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + oraclePrice: '2620', + bidPrice: '2610', + askPrice: '2630', + alpha: '0.5', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash2 = await walletClient.sendTransaction({ + ...adjustOrderPriceTx!, + account: '0x5F79EE8f8fA862E98201120d83c4eC39D9468D49', + gasPrice: adjustOrderPriceTx!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash2 }) + + const approveHash1 = await approveERC20({ + chainId: cloberTestChain2.id, + walletClient, + token: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + amount: '3000', + options: { + rpcUrl: publicClient.transport.url!, + }, + }) + const approveReceipt1 = await publicClient.waitForTransactionReceipt({ + hash: approveHash1!, + }) + expect(approveReceipt1.status).toEqual('success') + const approveHash2 = await approveERC20({ + chainId: cloberTestChain2.id, + walletClient, + token: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + amount: '3', + options: { + rpcUrl: publicClient.transport.url!, + }, + }) + const approveReceipt2 = await publicClient.waitForTransactionReceipt({ + hash: approveHash2!, + }) + expect(approveReceipt2.status).toEqual('success') + + const { transaction: tx3 } = await marketOrder({ + chainId: cloberTestChain2.id, + userAddress: account.address, + inputToken: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + outputToken: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + amountIn: '100', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash3 = await walletClient.sendTransaction({ + ...tx3!, + account, + gasPrice: tx1!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash3 }) + const { transaction: tx4 } = await marketOrder({ + chainId: cloberTestChain2.id, + userAddress: account.address, + inputToken: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + outputToken: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + amountIn: '0.5', + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash4 = await walletClient.sendTransaction({ + ...tx4!, + account, + gasPrice: tx1!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash4 }) + + const poolStep4 = await getPool({ + chainId: cloberTestChain2.id, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + + const tx5 = await refillOrder({ + chainId: cloberTestChain2.id, + userAddress: account.address, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + const hash5 = await walletClient.sendTransaction({ + ...tx5!, + account, + gasPrice: tx1!.gasPrice! * 2n, + }) + await publicClient.waitForTransactionReceipt({ hash: hash5 }) + + const poolStep5 = await getPool({ + chainId: cloberTestChain2.id, + token0: '0x00bfd44e79fb7f6dd5887a9426c8ef85a0cd23e0', + token1: '0xF2e615A933825De4B39b497f6e6991418Fb31b78', + salt: zeroHash, + options: { + rpcUrl: publicClient.transport.url!, + useSubgraph: false, + }, + }) + + expect(poolStep4.cancelableA).toBe(0n) + expect(poolStep4.cancelableB).toBe(155953200000000n) + expect(poolStep4.claimableA).toBe(99997469n) + expect(poolStep4.claimableB).toBe(38329361063758775n) + + expect(poolStep5.cancelableA).toBe(99999872n) + expect(poolStep5.cancelableB).toBe(38167546300000000n) + expect(poolStep5.claimableA).toBe(0n) + expect(poolStep5.claimableB).toBe(0n) +}) diff --git a/test/setup/globalSetup.ts b/test/setup/globalSetup.ts index 75189e7..e7dfd46 100644 --- a/test/setup/globalSetup.ts +++ b/test/setup/globalSetup.ts @@ -1,7 +1,10 @@ import { startProxy } from '@viem/anvil' import * as dotenv from 'dotenv' -import { cloberTestChain } from '../../src/constants/test-chain' +import { + cloberTestChain, + cloberTestChain2, +} from '../../src/constants/test-chain' import { DEV_MNEMONIC_SEED, FORK_BLOCK_NUMBER } from '../utils/constants' dotenv.config() @@ -20,5 +23,21 @@ export default async function () { autoImpersonate: true, gasPrice: 0, }, + port: 8545, + }) + startProxy({ + options: { + chainId: cloberTestChain2.id, + forkUrl: + process.env.ARBITRUM_SEPOLIA_RPC_URL || + 'https://arbitrum-sepolia-archive.allthatnode.com', + forkBlockNumber: FORK_BLOCK_NUMBER, + mnemonic: DEV_MNEMONIC_SEED, + accounts: 10, + balance: 1000, // 1000 ETH + autoImpersonate: true, + gasPrice: 0, + }, + port: 8546, }) } diff --git a/test/utils/utils.ts b/test/utils/utils.ts index f5b3928..c01336c 100644 --- a/test/utils/utils.ts +++ b/test/utils/utils.ts @@ -5,7 +5,10 @@ import { http, } from 'viem' -import { cloberTestChain } from '../../src/constants/test-chain' +import { + cloberTestChain, + cloberTestChain2, +} from '../../src/constants/test-chain' import { account } from './constants' @@ -46,3 +49,31 @@ export function createProxyClients( return output as Tuple<(typeof output)[number], TIds['length']> } + +export function createProxyClients2( + ids: TIds, + port = 8546, +) { + const output = ids.map((i) => { + const publicClient = createPublicClient({ + chain: cloberTestChain2, + transport: http(`http://127.0.0.1:${port}/${i}`), + }) + + const testClient = createTestClient({ + chain: cloberTestChain2, + mode: 'anvil', + transport: http(`http://127.0.0.1:${port}/${i}`), + }) + + const walletClient = createWalletClient({ + chain: cloberTestChain2, + account, + transport: http(`http://127.0.0.1:${port}/${i}`), + }) + + return { publicClient, testClient, walletClient } as const + }) + + return output as Tuple<(typeof output)[number], TIds['length']> +}