diff --git a/.changeset/tough-jokes-kneel.md b/.changeset/tough-jokes-kneel.md new file mode 100644 index 0000000..7238ed1 --- /dev/null +++ b/.changeset/tough-jokes-kneel.md @@ -0,0 +1,5 @@ +--- +"@mangrovedao/mgv": minor +--- + +Add kandel to the SDK diff --git a/README.md b/README.md deleted file mode 100644 index 27c68b2..0000000 --- a/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# mgv - -To install dependencies: - -```bash -bun install -``` - -To run: - -```bash -bun run src/index.ts -``` - -This project was created using `bun init` in bun v1.0.26. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/README.md b/README.md new file mode 120000 index 0000000..351df1d --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +src/README.md \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..db0ffea --- /dev/null +++ b/src/README.md @@ -0,0 +1,580 @@ +# mgv + +mgv is an sdk aimed at providing a simple way to interact with Mangrove. + +## General context + +There are some general contexts that needs to be set + +### Addresses + +The addresses needed to run view functions, market, and limit orders are gotten from the type + +```ts +// This is the type that holds mgv, mgvOrder, and mgvReader +import type { MangroveActionsDefaultParams } from "@mangrovedao/mgv"; + +const mangroveParams: MangroveActionsDefaultParams = { + mgv: "0x...", // mangrove core contract + mgvOrder: "0x...", // mangrove order contract + mgvReader: "0x...", // mangrove reader contract +} +``` + +Then we can also define tokens with helper functions like this + +```ts +import { buildToken } from "@mangrovedao/mgv"; + +export const WETH = buildToken({ + address: '0x...', + symbol: 'WETH', +}) +``` + +Then we can define a market this way: + +```ts +import type { MarketParams } from "@mangrovedao/mgv"; + +const WETH = buildToken({...}) +const USDC = buildToken({...}) + +const marketParams: MarketParams = { + base: WETH, + quote: USDC, + tickSpacing: 1n, // market tick spacing +} +``` + +Predefined tokens and addresses are available in the `@mangrovedao/mgv` package. + +```ts +import { blastMangrove, blastWETH, blastUSDB, blastMarkets } from "@mangrovedao/mgv/addresses"; +``` + +### logics + +The logics are hooks that can be attached to orders. An aave forke logic (sourcing from and to an aave fork) can be defined like this for example: + +```ts +import { buildLogic, aaveBalance, aaveOverLying } from "@mangrovedao/mgv/addresses"; + +const aaveForkLogic = buildLogic( + "aaveFork", + '0x...', // aave fork logic address + 1_300_000n, // gas required to run the logic + aaveOverLying, // the overlying logic + aaveBalance, // the balance logic +) +``` + +Some logics are predefined in the `@mangrovedao/mgv` package. + +```ts +import { blastLogics } from "@mangrovedao/mgv/addresses"; +``` + +### Market client + +The market client is a viem extension of the client object that interacts with the designated market. Given a defined market, we create and interacts with the market client like this: + +```ts +import { createClient } from "viem"; +import { publicMarketActions } from "@mangrovedao/mgv"; + +const marketClient = createClient({...}) + .extend(publicMarketActions(mangroveParams, market)) + +// getting the book, and configs for the given market +// This gives it at the latest block +const book = await marketClient.getBook({ depth: 100 }); +``` + +### General client + +the general client can be initiated without any context and can be used like this: + +```ts +import { createClient } from "viem"; +import { generalActions } from "@mangrovedao/mgv"; +import { blastMarkets, blastLogics } from "@mangrovedao/mgv/addresses"; + +const client = createClient() + .extend(generalActions) + +const {tokens, overlying, logicBalances} = await client.getBalances({ + markets: blastMarkets, + logics: blastLogics, + user: '0x...', +}) +``` + +this function is actually returning all tokens from the market alongside the balance of the user, all overlying tokens according to the logics alongside a flag that indicates if the logic is available for a given token, and finally logicBalances that gives all balances of tokens according to logics. + +Here are a few helper function that could be used to interact with the client: + +```ts +const {tokens, overlying, logicBalances} = await client.getBalances({ + markets: blastMarkets, + logics: blastLogics, + user: '0x...', +}) + +function availableLogics(token: Token): Logic[] { + return overlying.filter((overlying) => overlying.available) +} + +function getBalance(token: Token, logic?: Logic): bigint { + if (logic) { + return logicBalances.find(lb => { + return isAddressEqual(lb.token.address, token.address) && isAddressEqual(lb.logic.logic, logic.logic) + })?.balance ?? 0n + } + return tokens.find(t => isAddressEqual(t.address, token.address))?.balance ?? 0n +} +``` + +### Mangrove client + +The mangrove client is a view extension of the client object that interacts with the mangrove contracts. It can be initiated like this: + +```ts +import { createClient } from "viem"; +import { mangroveActions } from "@mangrovedao/mgv"; + +const mangroveClient = createClient({...}) + .extend(mangroveActions(mangroveParams)) + +// retrieves the user router +const router = await mangroveClient.getUserRouter({ + user: '0x...' +}) +``` + +## Making a market order + +To make a market order, there are 4 steps to follow: + +- Make a market order simulation against the current book to get the estimated price, slippage, fees, ... +- Get the needed steps for making a market order +- Execute the market order +- Parse the logs to get the real result of the market order + +### Making a market order simulation + +To make a market order simulation, we need to get the book and the order parameters. The book can be retrieved on the spot or can be retrieved from a global context pinging the blockchain to get the latest book. All amounts are to be converted to wei. It can be done with `parseUnits(amount, decimals)` exported from `viem`. + +```ts +import { book } from "./book.ts"; +import { marketOrderSimulation, BS } from "@mangrovedao/mgv"; + +// simulate buying 1 unit of the base token +const { + baseAmount, + quoteAmount, + gas, + feePaid, + maxTickEncountered, + minSlippage, + fillWants, + price, +} = marketOrderSimulation({ + book, + bs: BS.buy, + base: parseUnits('1', market.base.decimals), +}) +``` + +important values that it returns are baseAmount and quoteAmount that can then be used to modify the inputs. We can then use the remaining parameters to then place the market order after. + +### Getting the needed steps for making a market order + +On the market client, we can get the needed steps for making a market order like this: + +```ts +import { marketClient } from "./marketClient.ts"; + +const [approvalStep] = await marketClient.getMarketOrderSteps({ + bs: BS.buy, + user: '0x...', + sendAmount: quoteAmount, // this can be ignored +}) + +if (!approvalStep.done) { + // doing the approval according the approvalStep params +} +``` + +### Executing the market order + +To execute the market order, we can use the `simulate` function from the market client, this sends back a request that can be used to sign and broadcast the transaction (assuming all the steps from getMarketOrderSteps are done). + +```ts +const { + takerGot, + takerGave, + bounty, + feePaid, + request +} = await marketClient.simulateMarketOrderByVolumeAndMarket({ + baseAmount: parseEther('1'), + quoteAmount: parseEther('3000'), + bs: BS.buy, + slippage: 0.05, // 5% slippage + account: '0x...' +}) + +const tx = await walletClient.writeContract(request) +``` + +### Parsing the logs + +Once the transaction has been broadcasted, we can wait for the transaction receipt and pass the logs to a utils function to convert the logs of the market order to a more readable format. + +```ts +import { marketOrderResultFromLogs } from "@mangrovedao/mgv"; + +const tx = await walletClient.writeContract(request) + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx +}) + +const { + takerGot, + takerGave, + feePaid, + bounty, +} = marketOrderResultFromLogs( + actionParams, + market, + { + logs: receipt.logs, + bs: BS.buy, + taker: '0x...' + } +) +``` + +## Making a limit order + +To make a limit order, there are 4 steps to follow: +- Getting the user router +- Get the needed steps for making a limit order +- Execute the limit order +- Parse the logs to get the real result of the limit order + +### Getting the user router + +To get the user router, we can use the mangrove client like this: + +```ts +import { mangroveClient } from "./mangroveClient.ts"; + +// this value is deterministic +// it can be cached via tanstack query for example +const userRouter = await mangroveClient.getUserRouter({ + user: '0x...' +}) +``` + +### Getting the needed steps for making a limit order + +We need to get the steps for making a limit order. This will return a size 1 array with the first element being the approval step if needed. + +```ts +const steps = await marketClient.getLimitOrderSteps({ + bs: BS.buy, + user: '0x...', + userRouter, +}) +``` + +if there is a logic attached the the limit order we want to do, we have to pass the token address of the overlying token we will send. For example if we buy WETH with USDC, and use AAVE as logic, we have to pass the address of aUSDC. + +```ts +const steps = await marketClient.getLimitOrderSteps({ + bs: BS.buy, + user: '0x...', + userRouter, + logicToken: '0x...' +}) +``` + +### Executing the limit order + +To execute the limit order, we can use the `simulate` function from the market client, this sends back a request that can be used to sign and broadcast the transaction (assuming all the steps from getLimitOrderSteps are done). + +```ts +const { + request +} = await marketClient.simulateLimitOrder({ + baseAmount: parseEther('1'), + quoteAmount: parseEther('3000'), + bs: BS.buy, + book: book, + orderType: Order.GTC, + // If expiry date is ignored, then it will not expire + expiryDate: Date.now() / 1000 + 60 * 60, // 1 hour + // logics can be left to undefined (meaning no logic) + takerGivesLogic: blastOrbitLogic.logic, + takerWantsLogic: blastPacFinanceLogic.logic, +}) + +const tx = await walletClient.writeContract(request) +``` + +### Parsing the logs + +Once the transaction has been broadcasted, we can wait for the transaction receipt and pass the logs to a utils function to convert the logs of the limit order to a more readable format. + +```ts +import { limitOrderResultFromLogs } from "@mangrovedao/mgv"; + +const tx = await walletClient.writeContract(request) + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx +}) + +const { + takerGot, + takerGave, + feePaid, + bounty, + offer, +} = limitOrderResultFromLogs( + actionParams, + market, + { + logs: receipt.logs, + bs: BS.buy, + user: '0x...' + } +) + +if (offer) { + // a limit order was posted + const { + id, + tick, + gives, + wants, + gasprice, + gasreq + } = offer +} +``` + +## Updating a limit order + +To update a limit order, wa have to know the offer id of the limit order we want to udpate as well as the current gas requirement if we want to keep it the same. + +Then we simply have to use the `simulate` function from the market client, this sends back a request that can be used to sign and broadcast the transaction. + +```ts +const { request } = await marketClient.simulateUpdateOrder({ + id: 1n, + // new values for base and quote amount + baseAmount: parseEther('1'), + quoteAmount: parseEther('3000'), + // bs and book + bs: BS.buy, + book: book, + // has to be specified even if it is the same + restingOrderGasreq: 250_000n, +}) + +const tx = await walletClient.writeContract(request) + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx +}) +``` + +## Cancelling a limit order + +To cancel a limit order, we have to know the offer id of the limit order we want to cancel. + +Then we simply have to use the `simulate` function from the market client, this sends back a request that can be used to sign and broadcast the transaction. + +```ts +const { request } = await marketClient.simulateRemoveOrder({ + id: 1n, + bs: BS.buy, +}) + +const tx = await walletClient.writeContract(request) + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx +}) +``` + +This defaults to deprovisionning from mangrove (retracting all the funds unlocked on mangrove). But a flag `deprovision=false` can be passed to keep these funds on mangrove for future use. + +## Creating a kandel + +There are 2 types of client for kandel, the kandel client, and the kandel seeder client. One is used to deploy an instance of kandel, the other to interact with it. + +### The kandel clients + +To deploy a kandel, there is the kandel seeder client: + +```ts +import { kandelSeederActions } from "@mangrovedao/mgv"; +import { createClient } from "viem"; + +const client = createClient({ ... }) + +const kandelSeederClient = client.extend( + kandelSeederActions( + market, // the market object + "0x..." // the kandel seeder address + ) +) + +kandelSeederClient.simulateSow() +``` + +To interact with a kandel, there is the kandel client: + +```ts +import { kandelActions } from "@mangrovedao/mgv"; +import { createClient } from "viem"; + +const client = createClient({ ... }) + +const kandelClient = client.extend( + kandelActions( + market, // the market object + "0x..." // the kandel address + ) +) + +kandelClient.simulatePopulate({ ... }) +``` + +### Getting the populate params (validating params) + +In order to populate a kandel, we need to get the params for the populate function. These params gives us the distribution of the kandel. + +Here are the parameters to pass to the `validateKandelParams` function : + +| Parameter | Description | +| --- | --- | +| baseAmount | Amount of base to supply to the kandel | +| quoteAmount | Amount of quote to supply to the kandel | +| stepSize | The number of offers to jump in order to repost the dual offer | +| gasreq | The gas requirement to take a single offer | +| factor | A number to multiply the minimum volume by | +| asksLocalConfig | The local config for the asks | +| bidsLocalConfig | The local config for the bids | +| minPrice | The minimum price for the kandel | +| maxPrice | The maximum price for the kandel | +| midPrice | The wanted midPrice (should be the book midPrice) | +| pricePoints | The number of price points to use for the kandel | +| market | The market object | + +you need to pass all these parameters to the `validateKandelParams` function. + +```ts +import { validateKandelParams } from "@mangrovedao/mgv"; + +const { + params, + rawParams, + minBaseAmount, + minQuoteAmount, + minProvision, + isValid +} = validateKandelParams({ + baseAmount: parseEther('1'), + quoteAmount: parseEther('3000'), + stepSize: 1, + gasreq: 250_000n, + factor: 3, + asksLocalConfig, + bidsLocalConfig, + minPrice: 2900, + maxPrice: 3100, + midPrice, + pricePoints: 10n, + market, +}) +``` + +This function returns 5 params : +- `params` : the params to pass to the `populate` function +- `rawParams` : the raw params that have been adjusted. These have the same structure as the input params, but have been adjusted to fit the kandel constraints. +- `minBaseAmount` : the minimum base amount that can be used to populate the kandel +- `minQuoteAmount` : the minimum quote amount that can be used to populate the kandel +- `minProvision` : the minimum provision that can be used to populate the kandel +- `isValid` : a boolean that tells if the params are valid or not (amuont greater than min amount) + +### Getting the kandel steps + +In order to populate a kandel, we need to get the steps to do before calling the populate function. + +```ts +const steps = await kandelClient.getKandelSteps({ + userRouter, + user, + gasreq: 250_000n, +}) + +steps[0] // wether to deploy or not the user router +steps[1] // wether to bind or not the user router to kandel +steps[2] // wether to set the logic or not on the contract +steps[3] // the approval needed for the base logic +steps[4] // the approval needed for the quote logic +``` + +### Populating a kandel + +To populate a kandel, we need to call the `populate` function from the kandel client. + +```ts +const { + params, + isValid +} = validateKandelParams({ + ... +}) + +if (!isValid) { + // custom logic + return +} + + +// Logics of the steps in there +// ... + +const { request } = await kandelClient.simulatePopulate({ + ...params, + account: '0x...', +}) + +const tx = await walletClient.writeContract(request) + +const receipt = await publicClient.waitForTransactionReceipt({ + hash: tx +}) +``` + +### Retracting a kandel + +To retract a kandel, you can call the `simulateRetract` function. You have to know the number of price points in order to retract and pass it as the `to` parameter. `baseAmount` and/or `quoteAmount` can be specified if any amount is inside the kandel and to be removed from it. + +```ts +const { request } = await kandelClient.simulateRetract({ + to: pricePoints, + baseAmount: parseEther('1'), + quoteAmount: parseEther('3000'), + // both of these addresses are suppoesedly the same + recipient: '0x...', + account: '0x...', +}) +``` diff --git a/src/actions/balances.ts b/src/actions/balances.ts index 2a241ff..3a94dd8 100644 --- a/src/actions/balances.ts +++ b/src/actions/balances.ts @@ -1,10 +1,4 @@ -import { - type Address, - type Client, - erc20Abi, - isAddressEqual, - zeroAddress, -} from 'viem' +import { type Address, type Client, erc20Abi, isAddressEqual } from 'viem' import type { ContractFunctionParameters, MulticallParameters } from 'viem' import { multicall } from 'viem/actions' import type { Logic, OverlyingResponse } from '../addresses/logics/utils.js' @@ -28,18 +22,20 @@ export type GetBalancesArgs = GetBalancesParams & Omit +export type OverlyingResult = { + type: 'erc20' | 'erc721' + overlying?: Token + available: boolean + token: Token + logic: TLogic +} + export type GetBalanceResult = { tokens: { token: Token balance: bigint }[] - overlying: { - type: 'erc20' | 'erc721' - overlying: Address - available: boolean - token: Token - logic: TLogics[number] - }[] + overlying: OverlyingResult[] logicBalances: { token: Token logic: TLogics[number] @@ -75,8 +71,8 @@ export async function getBalances( const overlyingCalls = tokens.flatMap((token) => logics.map((logic) => logic.logicOverlying.getOverlyingContractParams({ - token: token.address, - logic: logic.logic, + token: token, + logic: logic, name: logic.name, }), ), @@ -114,10 +110,17 @@ export async function getBalances( const res = result[tokens.length * (i + 1) + j] const overlying: OverlyingResponse = res.status === 'success' - ? logic.logicOverlying.parseOverlyingContractResponse(res.result) + ? logic.logicOverlying.parseOverlyingContractResponse( + { + token: token, + logic: logic, + name: logic.name, + }, + res.result, + ) : { type: 'erc20', - overlying: zeroAddress, + overlying: undefined, available: false, } return { token, logic, ...overlying } diff --git a/src/actions/kandel/logic.ts b/src/actions/kandel/logic.ts new file mode 100644 index 0000000..91415a4 --- /dev/null +++ b/src/actions/kandel/logic.ts @@ -0,0 +1,41 @@ +import type { + Address, + Client, + SimulateContractParameters, + SimulateContractReturnType, +} from 'viem' +import { simulateContract } from 'viem/actions' +import { + type SetLogicsParams, + type logicsABI, + setLogicsParams, +} from '../../builder/kandel/logic.js' +import type { BuiltArgs } from '../../types/actions/index.js' +import { getAction } from '../../utils/getAction.js' + +type SimulationParams = SimulateContractParameters< + typeof logicsABI, + 'setLogics' +> + +export type SetLogicsArgs = SetLogicsParams & Omit +export type SetLogicsResult = SimulateContractReturnType< + typeof logicsABI, + 'setLogics' +> + +export async function simulateSetLogics( + client: Client, + kandel: Address, + args: SetLogicsArgs, +): Promise { + return getAction( + client, + simulateContract, + 'simulateContract', + )({ + ...(args as unknown as SimulationParams), + ...setLogicsParams(args), + address: kandel, + }) +} diff --git a/src/actions/kandel/populate.ts b/src/actions/kandel/populate.ts new file mode 100644 index 0000000..00b095e --- /dev/null +++ b/src/actions/kandel/populate.ts @@ -0,0 +1,72 @@ +import type { Address, Client } from 'viem' +import { + type SimulateContractParameters, + type SimulateContractReturnType, + simulateContract, +} from 'viem/actions' +import { + type PopulateChunkFromOffsetParams, + type PopulateFromOffsetParams, + type populateABI, + populateChunkFromOffsetParams, + populateFromOffsetParams, +} from '../../builder/kandel/populate.js' +import type { BuiltArgsWithValue } from '../../index.js' +import { getAction } from '../../utils/getAction.js' + +type SimulationPopulateParams = SimulateContractParameters< + typeof populateABI, + 'populateFromOffset' +> +export type PopulateArgs = PopulateFromOffsetParams & + Omit + +export type PopulateResult = SimulateContractReturnType< + typeof populateABI, + 'populateFromOffset' +> + +export function simulatePopulate( + client: Client, + kandel: Address, + args: PopulateArgs, +): Promise { + return getAction( + client, + simulateContract, + 'simulateContract', + )({ + ...(args as unknown as SimulationPopulateParams), + ...populateFromOffsetParams(args), + address: kandel, + }) +} + +type SimulationPopulateChunckParams = SimulateContractParameters< + typeof populateABI, + 'populateChunkFromOffset' +> + +export type PopulateChunkArgs = PopulateChunkFromOffsetParams & + Omit + +export type PopulateChunkResult = SimulateContractReturnType< + typeof populateABI, + 'populateChunkFromOffset' +> + +export function simulatePopulateChunk( + client: Client, + kandel: Address, + args: PopulateChunkArgs, +): Promise { + return getAction( + client, + simulateContract, + 'simulateContract', + )({ + ...(args as unknown as SimulationPopulateChunckParams), + ...populateChunkFromOffsetParams(args), + address: kandel, + }) +} diff --git a/src/actions/kandel/retract.ts b/src/actions/kandel/retract.ts new file mode 100644 index 0000000..631fb06 --- /dev/null +++ b/src/actions/kandel/retract.ts @@ -0,0 +1,42 @@ +import type { + Address, + Client, + SimulateContractParameters, + SimulateContractReturnType, +} from 'viem' +import { simulateContract } from 'viem/actions' +import { + type RetractParams, + type restractKandelABI, + retractParams, +} from '../../builder/kandel/retract.js' +import type { BuiltArgs } from '../../index.js' +import { getAction } from '../../utils/getAction.js' + +type SimulationParams = SimulateContractParameters< + typeof restractKandelABI, + 'retractAndWithdraw' +> + +export type RetractArgs = RetractParams & Omit + +export type SimulateRetractResult = SimulateContractReturnType< + typeof restractKandelABI, + 'retractAndWithdraw' +> + +export async function simulateRetract( + client: Client, + kandel: Address, + args: RetractArgs, +): Promise { + return getAction( + client, + simulateContract, + 'simulateContract', + )({ + ...(args as unknown as SimulationParams), + ...retractParams(args), + address: kandel, + }) +} diff --git a/src/actions/kandel/sow.ts b/src/actions/kandel/sow.ts new file mode 100644 index 0000000..0769b9a --- /dev/null +++ b/src/actions/kandel/sow.ts @@ -0,0 +1,36 @@ +import type { + Address, + Client, + SimulateContractParameters, + SimulateContractReturnType, +} from 'viem' +import { simulateContract } from 'viem/actions' +import { + type SowParams, + type sowABI, + sowParams, +} from '../../builder/kandel/sow.js' +import type { BuiltArgs, MarketParams } from '../../index.js' +import { getAction } from '../../utils/getAction.js' + +type SimulationParams = SimulateContractParameters + +export type SowArgs = SowParams & Omit +export type SimulateSowResult = SimulateContractReturnType + +export async function simulateSow( + client: Client, + market: MarketParams, + kandelSeeder: Address, + args?: SowArgs, +): Promise { + return getAction( + client, + simulateContract, + 'simulateContract', + )({ + ...(args as unknown as SimulationParams), + ...sowParams(market, args), + address: kandelSeeder, + }) +} diff --git a/src/actions/kandel/steps.ts b/src/actions/kandel/steps.ts new file mode 100644 index 0000000..c9ee467 --- /dev/null +++ b/src/actions/kandel/steps.ts @@ -0,0 +1,164 @@ +import { + type Address, + type Client, + type MulticallParameters, + isAddressEqual, + maxUint128, + maxUint256, + parseAbi, + zeroAddress, +} from 'viem' +import { multicall } from 'viem/actions' +import { getLogicsParams } from '../../builder/kandel/logic.js' +// import { getParamsParams } from '../../builder/kandel/populate.js' +import { tokenAllowanceParams } from '../../builder/tokens.js' +import type { KandelSteps, MarketParams } from '../../index.js' +import { getAction } from '../../utils/getAction.js' +import type { OverlyingResult } from '../balances.js' + +export const routerABI = parseAbi([ + 'function admin() public view returns (address current)', + 'function isBound(address mkr) public view returns (bool)', +]) + +// => Deploy user router instance if not exist +// => Create kandel instance +// => Bind the router with the kandel +// => Set the logics for the kandel +// => Approve the tokens for the kandel (2 steps) +// => populate the kandel + +export type GetKandelStepsParams = { + userRouter: Address + user: Address + baseOverlying?: OverlyingResult + quoteOverlying?: OverlyingResult + gasreq: bigint +} + +export type GetKandelStepsArgs = GetKandelStepsParams & + Omit + +export async function getKandelSteps( + client: Client, + market: MarketParams, + kandel: Address, + args: GetKandelStepsArgs, +): Promise { + const [admin, bound, logics, /*params,*/ baseAllowance, quoteAllowance] = + await getAction( + client, + multicall, + 'multicall', + )({ + ...args, + contracts: [ + { + address: args.userRouter, + abi: routerABI, + functionName: 'admin', + }, + { + address: args.userRouter, + abi: routerABI, + functionName: 'isBound', + args: [kandel], + }, + { + address: kandel, + ...getLogicsParams(), + }, + // { + // address: args.kandel || zeroAddress, + // ...getParamsParams(), + // }, + { + ...tokenAllowanceParams({ + owner: args.user, + spender: args.userRouter, + token: + args.baseOverlying?.available && args.baseOverlying.overlying + ? args.baseOverlying.overlying.address + : market.base.address, + }), + }, + { + ...tokenAllowanceParams({ + owner: args.user, + spender: args.userRouter, + token: + args.quoteOverlying?.available && args.quoteOverlying.overlying + ? args.quoteOverlying.overlying.address + : market.quote.address, + }), + }, + ], + allowFailure: true, + }) + + return [ + { + type: 'deployRouter', + params: { + owner: args.user, + }, + done: + admin.status === 'success' && isAddressEqual(admin.result, args.user), + }, + { + type: 'bind', + params: { + makerContract: kandel, + }, + done: bound.status === 'success' && bound.result, + }, + { + type: 'setKandelLogics', + params: { + kandel, + baseLogic: args.baseOverlying?.logic, + quoteLogic: args.quoteOverlying?.logic, + gasRequirement: args.gasreq, + }, + done: + logics.status === 'success' && + isAddressEqual( + logics.result[0], + args.baseOverlying?.logic.logic || zeroAddress, + ) && + isAddressEqual( + logics.result[1], + args.quoteOverlying?.logic.logic || zeroAddress, + ), // setting the gasreq in the populate instead of set logic + }, + { + type: 'erc20Approval', + params: { + token: + args.baseOverlying?.available && args.baseOverlying.overlying + ? args.baseOverlying.overlying + : market.base, + from: args.user, + spender: args.userRouter, + amount: maxUint256, + }, + done: + baseAllowance.status === 'success' && baseAllowance.result > maxUint128, + }, + { + type: 'erc20Approval', + params: { + token: + args.quoteOverlying?.available && args.quoteOverlying.overlying + ? args.quoteOverlying.overlying + : market.quote, + from: args.user, + spender: args.userRouter, + amount: maxUint256, + }, + done: + quoteAllowance.status === 'success' && + quoteAllowance.result > maxUint128, + }, + ] +} diff --git a/src/actions/kandel/view.ts b/src/actions/kandel/view.ts new file mode 100644 index 0000000..8607c18 --- /dev/null +++ b/src/actions/kandel/view.ts @@ -0,0 +1,145 @@ +import type { Address, Client, MulticallParameters } from 'viem' +import { multicall } from 'viem/actions' +import { + baseQuoteTickOffsetParams, + getOfferParams, + kandelParamsParams, + offeredVolumeParams, +} from '../../builder/kandel/view.js' +import { type MarketParams, priceFromTick } from '../../index.js' +import { BA } from '../../lib/enums.js' +import { rawPriceToHumanPrice } from '../../lib/human-readable.js' +import { unpackOffer } from '../../lib/offer.js' +import { getAction } from '../../utils/getAction.js' + +export type GetKandelStateParams = {} + +export type GetKandelStateArgs = GetKandelStateParams & + Omit + +type OfferParsed = { + tick: bigint + gives: bigint + price: number + ba: BA + index: bigint +} + +export type GetKandelStateResult = { + baseQuoteTickOffset: bigint + gasprice: number + gasreq: number + stepSize: number + pricePoints: number + quoteAmount: bigint + baseAmount: bigint + asks: OfferParsed[] + bids: OfferParsed[] +} + +export async function getKandelState( + client: Client, + market: MarketParams, + kandel: Address, + args: GetKandelStateArgs, +): Promise { + const [baseQuoteTickOffset, params, quoteAmount, baseAmount] = + await getAction( + client, + multicall, + 'multicall', + )({ + ...args, + contracts: [ + { + address: kandel, + ...baseQuoteTickOffsetParams, + }, + { + address: kandel, + ...kandelParamsParams, + }, + { + address: kandel, + ...offeredVolumeParams(BA.bids), + }, + { + address: kandel, + ...offeredVolumeParams(BA.asks), + }, + ], + allowFailure: true, + }) + + const pricePoints = + params.status === 'success' ? params.result.pricePoints : 0 + + const asks: OfferParsed[] = [] + const bids: OfferParsed[] = [] + if (pricePoints > 0) { + const offers = await getAction( + client, + multicall, + 'multicall', + )({ + ...args, + allowFailure: true, + contracts: Array.from({ length: pricePoints }).flatMap((_, i) => [ + { + address: kandel, + ...getOfferParams(BA.bids, BigInt(i)), + }, + { + address: kandel, + ...getOfferParams(BA.asks, BigInt(i)), + }, + ]), + }) + + asks.push( + ...offers + .filter((_, i) => i % 2 === 1) + .flatMap((offer, index) => + offer.status === 'success' + ? { ...unpackOffer(offer.result), index: BigInt(index) } + : [], + ) + .filter((o) => o.gives > 0n) + .map((offer) => ({ + ...offer, + ba: BA.asks, + price: rawPriceToHumanPrice(priceFromTick(-offer.tick), market), + })), + ) + bids.push( + ...offers + .filter((_, i) => i % 2 === 0) + .flatMap((offer, index) => + offer.status === 'success' + ? { ...unpackOffer(offer.result), index: BigInt(index) } + : [], + ) + .filter((o) => o.gives > 0n) + .map((offer) => ({ + ...offer, + ba: BA.bids, + price: rawPriceToHumanPrice(priceFromTick(offer.tick), market), + })), + ) + } + + return { + baseQuoteTickOffset: + baseQuoteTickOffset.status === 'success' + ? baseQuoteTickOffset.result + : 0n, + gasprice: params.status === 'success' ? params.result.gasprice : 0, + gasreq: params.status === 'success' ? params.result.gasreq : 0, + stepSize: params.status === 'success' ? params.result.stepSize : 0, + pricePoints, + quoteAmount: quoteAmount.status === 'success' ? quoteAmount.result : 0n, + baseAmount: baseAmount.status === 'success' ? baseAmount.result : 0n, + asks, + bids, + } +} diff --git a/src/actions/market-order.ts b/src/actions/market-order.ts index bae3cfe..a1c1c8e 100644 --- a/src/actions/market-order.ts +++ b/src/actions/market-order.ts @@ -49,8 +49,7 @@ export async function getMarketOrderSteps( args: GetMarketOrderStepsArgs, ): Promise { const { sendAmount: amount = maxUint256 } = args - const tokenToApprove = - args.bs === BS.buy ? market.quote.address : market.base.address + const tokenToApprove = args.bs === BS.buy ? market.quote : market.base const allowance = await getAction( client, @@ -61,7 +60,7 @@ export async function getMarketOrderSteps( ...tokenAllowanceParams({ owner: args.user, spender: actionParams.mgv, - token: tokenToApprove, + token: tokenToApprove.address, }), }) diff --git a/src/actions/offer/new.ts b/src/actions/offer/new.ts index c8ab632..7dc59fe 100644 --- a/src/actions/offer/new.ts +++ b/src/actions/offer/new.ts @@ -48,8 +48,7 @@ export async function getNewOfferSteps( args: GetNewOfferStepsArgs, ): Promise { const { sendAmount: amount = maxUint256 } = args - const tokenToApprove = - args.bs === BS.buy ? market.quote.address : market.base.address + const tokenToApprove = args.bs === BS.buy ? market.quote : market.base const allowance = await getAction( client, @@ -59,7 +58,7 @@ export async function getNewOfferSteps( ...tokenAllowanceParams({ owner: args.user, spender: actionParams.mgv, - token: tokenToApprove, + token: tokenToApprove.address, }), ...args, }) diff --git a/src/actions/order/new.ts b/src/actions/order/new.ts index 0d683f5..2123d76 100644 --- a/src/actions/order/new.ts +++ b/src/actions/order/new.ts @@ -29,13 +29,14 @@ import type { import type { LimitOrderSteps } from '../../types/actions/steps.js' import type { Prettify } from '../../types/lib.js' import { getAction } from '../../utils/getAction.js' +import type { OverlyingResult } from '../balances.js' export type GetLimitOrderStepsParams = { user: Address userRouter: Address bs: BS sendAmount?: bigint - logicToken?: Address + logic?: OverlyingResult } export type GetLimitOrderStepsArgs = Prettify< @@ -49,11 +50,12 @@ export async function getLimitOrderSteps( args: GetLimitOrderStepsArgs, ): Promise { const { sendAmount: amount = maxUint256 } = args - const tokenToApprove = args.logicToken - ? args.logicToken - : args.bs === BS.buy - ? market.quote.address - : market.base.address + const tokenToApprove = + args.logic?.available && args.logic.overlying + ? args.logic.overlying + : args.bs === BS.buy + ? market.quote + : market.base const allowance = await getAction( client, @@ -63,7 +65,7 @@ export async function getLimitOrderSteps( ...tokenAllowanceParams({ owner: args.user, spender: args.userRouter, - token: tokenToApprove, + token: tokenToApprove.address, }), ...args, }) diff --git a/src/addresses/index.ts b/src/addresses/index.ts index 2e0fbdc..4033b32 100644 --- a/src/addresses/index.ts +++ b/src/addresses/index.ts @@ -12,6 +12,7 @@ export { blastUSDB, blastMetaStreetWETHPUNKS20, blastMetaStreetWETHPUNKS40, + buildToken, } from './tokens/index.js' // --- mangrove --- diff --git a/src/addresses/logics/strategies/aave.ts b/src/addresses/logics/strategies/aave.ts index 0490a6a..e7e4eac 100644 --- a/src/addresses/logics/strategies/aave.ts +++ b/src/addresses/logics/strategies/aave.ts @@ -1,4 +1,5 @@ import { isAddressEqual, parseAbi, zeroAddress } from 'viem' +import { buildToken } from '../../tokens/utils.js' import type { RoutingLogicOverlying } from '../utils.js' import { baseBalance } from './base.js' @@ -13,16 +14,19 @@ export const aaveOverLying: RoutingLogicOverlying< > = { getOverlyingContractParams(params) { return { - address: params.token, + address: params.logic.logic, abi: aaveLogicABI, functionName: 'overlying', - args: [params.token], + args: [params.token.address], } }, - parseOverlyingContractResponse(response) { + parseOverlyingContractResponse(params, response) { return { type: 'erc20', - overlying: response, + overlying: buildToken({ + symbol: `${params.token.symbol} overlying`, + address: response, + }), available: !isAddressEqual(response, zeroAddress), } }, diff --git a/src/addresses/logics/utils.ts b/src/addresses/logics/utils.ts index d5b33da..704d4a1 100644 --- a/src/addresses/logics/utils.ts +++ b/src/addresses/logics/utils.ts @@ -6,16 +6,17 @@ import type { ContractFunctionParameters, ContractFunctionReturnType, } from 'viem' +import type { Token } from '../index.js' export type OverlyingParams = { - token: Address - logic: Address + token: Token + logic: Logic name: string } export type OverlyingResponse = { type: 'erc20' | 'erc721' - overlying: Address + overlying?: Token available: boolean } @@ -31,6 +32,7 @@ export type RoutingLogicOverlying< params: OverlyingParams, ) => ContractFunctionParameters parseOverlyingContractResponse: ( + params: OverlyingParams, response: ContractFunctionReturnType, ) => OverlyingResponse } diff --git a/src/builder/kandel/logic.ts b/src/builder/kandel/logic.ts new file mode 100644 index 0000000..7dcf58c --- /dev/null +++ b/src/builder/kandel/logic.ts @@ -0,0 +1,34 @@ +import { type Address, type ContractFunctionParameters, parseAbi } from 'viem' + +export const logicsABI = parseAbi([ + 'function setLogics(address baseLogic, address quoteLogic, uint gasreq) public', + 'function getLogics() public view returns (address baseLogic, address quoteLogic)', +]) + +export type SetLogicsParams = { + baseLogic: Address + quoteLogic: Address + gasreq: bigint +} + +export function setLogicsParams(params: SetLogicsParams) { + return { + abi: logicsABI, + functionName: 'setLogics', + args: [params.baseLogic, params.quoteLogic, params.gasreq], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} + +export function getLogicsParams() { + return { + abi: logicsABI, + functionName: 'getLogics', + args: [], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} diff --git a/src/builder/kandel/populate.ts b/src/builder/kandel/populate.ts new file mode 100644 index 0000000..ef0ea9c --- /dev/null +++ b/src/builder/kandel/populate.ts @@ -0,0 +1,100 @@ +import { type ContractFunctionParameters, parseAbi } from 'viem' + +export const populateABI = parseAbi([ + 'function populateFromOffset(uint from, uint to, int baseQuoteTickIndex0, uint _baseQuoteTickOffset, uint firstAskIndex, uint bidGives, uint askGives, Params calldata parameters, uint baseAmount, uint quoteAmount) public payable', + 'function populateChunkFromOffset(uint from, uint to, int baseQuoteTickIndex0, uint firstAskIndex, uint bidGives, uint askGives) public payable', +]) + +export type PopulateFromOffsetParams = { + from?: bigint + to?: bigint + baseQuoteTickIndex0: bigint + baseQuoteTickOffset: bigint + firstAskIndex: bigint + bidGives: bigint + askGives: bigint + gasprice?: bigint + gasreq: bigint + stepSize?: bigint + pricePoints: bigint + baseAmount?: bigint + quoteAmount?: bigint +} +export type PopulateChunkFromOffsetParams = { + from?: bigint + to: bigint + baseQuoteTickIndex0: bigint + firstAskIndex: bigint + bidGives: bigint + askGives: bigint +} +export function populateFromOffsetParams(params: PopulateFromOffsetParams) { + const { + baseQuoteTickIndex0, + baseQuoteTickOffset, + firstAskIndex, + bidGives, + askGives, + gasreq, + pricePoints, + stepSize, + from = 0n, + to = pricePoints, + baseAmount = 0n, + quoteAmount = 0n, + gasprice = 0n, + } = params + return { + abi: populateABI, + functionName: 'populateFromOffset', + args: [ + from, + to, + baseQuoteTickIndex0, + baseQuoteTickOffset, + firstAskIndex, + bidGives, + askGives, + { + gasprice: Number(gasprice), + gasreq: Number(gasreq), + stepSize: Number(stepSize), + pricePoints: Number(pricePoints), + }, + baseAmount, + quoteAmount, + ], + } satisfies Omit< + ContractFunctionParameters< + typeof populateABI, + 'payable', + 'populateFromOffset' + >, + 'address' + > +} + +export function populateChunkFromOffsetParams( + params: PopulateChunkFromOffsetParams, +) { + const { + baseQuoteTickIndex0, + firstAskIndex, + bidGives, + askGives, + from = 0n, + to, + } = params + return { + abi: populateABI, + functionName: 'populateChunkFromOffset', + args: [from, to, baseQuoteTickIndex0, firstAskIndex, bidGives, askGives], + } satisfies Omit< + ContractFunctionParameters< + typeof populateABI, + 'payable', + 'populateChunkFromOffset' + >, + 'address' + > +} diff --git a/src/builder/kandel/retract.ts b/src/builder/kandel/retract.ts new file mode 100644 index 0000000..457bebc --- /dev/null +++ b/src/builder/kandel/retract.ts @@ -0,0 +1,43 @@ +import { + type Address, + type ContractFunctionParameters, + maxUint256, + parseAbi, +} from 'viem' + +export const restractKandelABI = parseAbi([ + 'function retractAndWithdraw(uint from, uint to, uint baseAmount, uint quoteAmount, uint freeWei, address payable recipient) external', +]) + +export type RetractParams = { + from?: bigint + to: bigint + baseAmount?: bigint + quoteAmount?: bigint + freeWei?: bigint + recipient: Address +} + +export function retractParams(params: RetractParams) { + const { + from = 0n, + to, + baseAmount = 0n, + quoteAmount = 0n, + freeWei = maxUint256, + recipient, + } = params + + return { + abi: restractKandelABI, + functionName: 'retractAndWithdraw', + args: [from, to, baseAmount, quoteAmount, freeWei, recipient], + } satisfies Omit< + ContractFunctionParameters< + typeof restractKandelABI, + 'nonpayable', + 'retractAndWithdraw' + >, + 'address' + > +} diff --git a/src/builder/kandel/sow.ts b/src/builder/kandel/sow.ts new file mode 100644 index 0000000..d2313a3 --- /dev/null +++ b/src/builder/kandel/sow.ts @@ -0,0 +1,36 @@ +import { type ContractFunctionParameters, parseAbi } from 'viem' +import type { MarketParams } from '../../index.js' +import { olKeyABIRaw } from '../structs.js' + +export const sowABI = parseAbi([ + olKeyABIRaw, + 'function sow(OLKey memory olKeyBaseQuote, bool liquiditySharing) external returns (address kandel)', +]) + +/** + * @param base the base address + * @param quote the quote address + * @param tickSpacing the tick spacing + * @params liquiditySharing whether to share liquidity (deprecated) + */ +export type SowParams = { + liquiditySharing?: boolean +} + +export function sowParams(market: MarketParams, params?: SowParams) { + return { + abi: sowABI, + functionName: 'sow', + args: [ + { + outbound_tkn: market.base.address, + inbound_tkn: market.quote.address, + tickSpacing: market.tickSpacing, + }, + params?.liquiditySharing ?? false, + ], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} diff --git a/src/builder/kandel/view.ts b/src/builder/kandel/view.ts new file mode 100644 index 0000000..768de25 --- /dev/null +++ b/src/builder/kandel/view.ts @@ -0,0 +1,69 @@ +import { type ContractFunctionParameters, parseAbi } from 'viem' +import { BA } from '../../lib/enums.js' + +// ba: 0 is bid, 1 is ask +export const viewKandelABI = parseAbi([ + 'function baseQuoteTickOffset() public view returns (uint)', + 'struct Params { uint32 gasprice; uint24 gasreq; uint32 stepSize; uint32 pricePoints; }', + 'function params() public view returns (Params memory)', + 'function offeredVolume(uint8 ba) public view returns (uint volume)', + 'function getOffer(uint8 ba, uint index) public view returns (uint offer)', + 'function offerIdOfIndex(uint8 ba, uint index) public view returns (uint offerId)', +]) + +export const baseQuoteTickOffsetParams = { + abi: viewKandelABI, + functionName: 'baseQuoteTickOffset', +} satisfies Omit< + ContractFunctionParameters< + typeof viewKandelABI, + 'view', + 'baseQuoteTickOffset' + >, + 'address' +> + +export const kandelParamsParams = { + abi: viewKandelABI, + functionName: 'params', +} satisfies Omit< + ContractFunctionParameters, + 'address' +> + +function parseBA(ba: BA) { + return ba === BA.bids ? 0 : 1 +} + +export function offeredVolumeParams(ba: BA) { + return { + abi: viewKandelABI, + functionName: 'offeredVolume', + args: [parseBA(ba)], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} + +export function getOfferParams(ba: BA, index: bigint) { + return { + abi: viewKandelABI, + functionName: 'getOffer', + args: [parseBA(ba), index], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} + +export function offerIdOfIndexParams(ba: BA, index: bigint) { + return { + abi: viewKandelABI, + functionName: 'offerIdOfIndex', + args: [parseBA(ba), index], + } satisfies Omit< + ContractFunctionParameters, + 'address' + > +} diff --git a/src/bundle/index.ts b/src/bundle/index.ts index 04c8dd4..70f6e37 100644 --- a/src/bundle/index.ts +++ b/src/bundle/index.ts @@ -1,5 +1,15 @@ +export type { + PublicMarketActions, + GeneralActions, + MangroveActions, + KandelActions, + KandelSeederActions, +} from './public/index.js' + export { publicMarketActions, generalActions, mangroveActions, + kandelActions, + kandelSeederActions, } from './public/index.js' diff --git a/src/bundle/public/index.ts b/src/bundle/public/index.ts index c0ac31b..0e76699 100644 --- a/src/bundle/public/index.ts +++ b/src/bundle/public/index.ts @@ -1,3 +1,12 @@ -export { publicMarketActions } from './market-actions.js' -export { generalActions } from './general-actions.js' -export { mangroveActions } from './mangrove-actions.js' +export { + publicMarketActions, + type PublicMarketActions, +} from './market-actions.js' +export { generalActions, type GeneralActions } from './general-actions.js' +export { mangroveActions, type MangroveActions } from './mangrove-actions.js' +export { + kandelActions, + kandelSeederActions, + type KandelActions, + type KandelSeederActions, +} from './kandel-actions.js' diff --git a/src/bundle/public/kandel-actions.ts b/src/bundle/public/kandel-actions.ts new file mode 100644 index 0000000..12bc8df --- /dev/null +++ b/src/bundle/public/kandel-actions.ts @@ -0,0 +1,72 @@ +import type { Address, Client } from 'viem' +import { + type SetLogicsArgs, + type SetLogicsResult, + simulateSetLogics, +} from '../../actions/kandel/logic.js' +import { + type PopulateArgs, + type PopulateChunkArgs, + type PopulateChunkResult, + type PopulateResult, + simulatePopulate, + simulatePopulateChunk, +} from '../../actions/kandel/populate.js' +import { + type RetractArgs, + type SimulateRetractResult, + simulateRetract, +} from '../../actions/kandel/retract.js' +import { + type SimulateSowResult, + type SowArgs, + simulateSow, +} from '../../actions/kandel/sow.js' +import { + type GetKandelStepsArgs, + getKandelSteps, +} from '../../actions/kandel/steps.js' +import { + type GetKandelStateArgs, + type GetKandelStateResult, + getKandelState, +} from '../../actions/kandel/view.js' +import type { KandelSteps, MarketParams } from '../../index.js' + +export type KandelSeederActions = { + simulateSow: (args?: SowArgs) => Promise +} + +export function kandelSeederActions(market: MarketParams, seeder: Address) { + return (client: Client): KandelSeederActions => ({ + simulateSow: (args?: SowArgs) => simulateSow(client, market, seeder, args), + }) +} + +export type KandelActions = { + getKandelSteps: (args: GetKandelStepsArgs) => Promise + simulateSetLogics: (args: SetLogicsArgs) => Promise + simulatePopulate: (args: PopulateArgs) => Promise + simulatePopulateChunk: ( + args: PopulateChunkArgs, + ) => Promise + simulateRetract: (args: RetractArgs) => Promise + getKandelState: (args: GetKandelStateArgs) => Promise +} + +export function kandelActions(market: MarketParams, kandel: Address) { + return (client: Client): KandelActions => ({ + getKandelSteps: (args: GetKandelStepsArgs) => + getKandelSteps(client, market, kandel, args), + simulateSetLogics: (args: SetLogicsArgs) => + simulateSetLogics(client, kandel, args), + simulatePopulate: (args: PopulateArgs) => + simulatePopulate(client, kandel, args), + simulatePopulateChunk: (args: PopulateChunkArgs) => + simulatePopulateChunk(client, kandel, args), + simulateRetract: (args: RetractArgs) => + simulateRetract(client, kandel, args), + getKandelState: (args: GetKandelStateArgs) => + getKandelState(client, market, kandel, args), + }) +} diff --git a/src/index.ts b/src/index.ts index eef2e9a..f3dabae 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,15 @@ export type { AmountsToHumanPriceParams, AmountsParams, AmountsOutput, + CreateGeometricDistributionParams, + DistributionOffer, + Distribution, + KandelFromLogsResult, + RawKandelPositionParams, + PositionKandelParams, + RawKandelParams, + KandelParams, + ValidateParamsResult, } from './lib/index.js' export { @@ -32,6 +41,11 @@ export { marketOrderSimulation, marketOrderResultFromLogs, limitOrderResultFromLogs, + CreateDistributionError, + createGeometricDistribution, + seederEventsABI, + getKandelsFromLogs, + validateKandelParams, } from './lib/index.js' // --- Types --- @@ -64,10 +78,20 @@ export type { // --- bundles --- +export type { + PublicMarketActions, + GeneralActions, + MangroveActions, + KandelActions, + KandelSeederActions, +} from './bundle/index.js' + export { publicMarketActions, generalActions, mangroveActions, + kandelActions, + kandelSeederActions, } from './bundle/index.js' // --- addresses --- diff --git a/src/lib/index.ts b/src/lib/index.ts index d3d7ccc..f4ba039 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -31,6 +31,28 @@ export { amounts, } from './human-readable.js' +// kandel + +export type { + CreateGeometricDistributionParams, + DistributionOffer, + Distribution, + KandelFromLogsResult, + RawKandelPositionParams, + PositionKandelParams, + RawKandelParams, + KandelParams, + ValidateParamsResult, +} from './kandel/index.js' + +export { + CreateDistributionError, + createGeometricDistribution, + seederEventsABI, + getKandelsFromLogs, + validateKandelParams, +} from './kandel/index.js' + // limit-order export type { diff --git a/src/lib/kandel/distribution.ts b/src/lib/kandel/distribution.ts new file mode 100644 index 0000000..103882a --- /dev/null +++ b/src/lib/kandel/distribution.ts @@ -0,0 +1,181 @@ +import type { MarketParams } from '../../index.js' +import { BA } from '../enums.js' +import { rawPriceToHumanPrice } from '../human-readable.js' +import { outboundFromInbound, priceFromTick } from '../tick.js' + +export type CreateGeometricDistributionParams = { + baseQuoteTickIndex0: bigint + baseQuoteTickOffset: bigint + firstAskIndex: bigint + pricePoints: bigint + stepSize: bigint + market: MarketParams + from?: bigint + to?: bigint +} & ( + | { + bidGives: bigint + askGives?: undefined + } + | { + bidGives?: undefined + askGives: bigint + } + | { + bidGives: bigint + askGives: bigint + } +) + +export type DistributionOffer = { + index: bigint + tick: bigint + gives: bigint + price: number +} + +export type Distribution = { + asks: DistributionOffer[] + bids: DistributionOffer[] +} + +export class CreateDistributionError extends Error { + constructor(message: string) { + super(message) + this.name = 'CreateDistributionError' + } +} + +function transportDestination( + ba: BA, + index: bigint, + step: bigint, + pricePoints: bigint, +) { + let better = 0n + if (ba === BA.asks) { + better = index + step + if (better >= pricePoints) better = pricePoints - 1n + } else { + if (index >= step) { + better = index - step + } + } + return better +} + +function getBounds(params: CreateGeometricDistributionParams) { + let { + pricePoints, + from = 0n, + to = pricePoints, + stepSize, + firstAskIndex, + } = params + + // determine global bidBound + const bidHoleSize = stepSize / 2n + (stepSize % 2n) + let bidBound = firstAskIndex > bidHoleSize ? firstAskIndex - bidHoleSize : 0n + const lastBidWithDualAsk = pricePoints - stepSize + if (bidBound > lastBidWithDualAsk) bidBound = lastBidWithDualAsk + + firstAskIndex = firstAskIndex + stepSize / 2n + if (firstAskIndex < stepSize) firstAskIndex = stepSize + + if (to < bidBound) bidBound = to + if (from > firstAskIndex) firstAskIndex = from + + const count = + (from < bidBound ? bidBound - from : 0n) + + (to > firstAskIndex ? to - firstAskIndex : 0n) + + return { bidBound, firstAskIndex, from, to, count } +} + +export function createGeometricDistribution( + params: CreateGeometricDistributionParams, +): Distribution { + const { + baseQuoteTickIndex0, + baseQuoteTickOffset, + bidGives, + askGives, + stepSize, + pricePoints, + market, + } = params + + if (!bidGives && !askGives) { + throw new CreateDistributionError( + 'Either bidGives or askGives must be provided', + ) + } + + const { bidBound, firstAskIndex, from, to } = getBounds(params) + const asks: DistributionOffer[] = [] + const bids: DistributionOffer[] = [] + + let index = from + let tick = -(baseQuoteTickIndex0 + baseQuoteTickOffset * index) + + for (; index < bidBound; ++index) { + const price = market + ? rawPriceToHumanPrice(priceFromTick(-tick), market) + : priceFromTick(-tick) + bids.push({ + index, + tick, + gives: !bidGives ? outboundFromInbound(tick, askGives!) : bidGives, + price, + }) + + const dualIndex = transportDestination( + BA.asks, + index, + stepSize, + pricePoints, + ) + + asks.push({ + index: dualIndex, + tick: baseQuoteTickIndex0 + baseQuoteTickOffset * dualIndex, + gives: 0n, + price, + }) + + tick -= baseQuoteTickOffset + } + + index = firstAskIndex + tick = baseQuoteTickIndex0 + baseQuoteTickOffset * index + + for (; index < to; ++index) { + const price = market + ? rawPriceToHumanPrice(priceFromTick(tick), market) + : priceFromTick(tick) + asks.push({ + index, + tick, + gives: !askGives ? outboundFromInbound(tick, bidGives!) : askGives, + price, + }) + + const dualIndex = transportDestination( + BA.bids, + index, + stepSize, + pricePoints, + ) + + bids.push({ + index: dualIndex, + tick: -(baseQuoteTickIndex0 + baseQuoteTickOffset * dualIndex), + gives: 0n, + price, + }) + + tick += baseQuoteTickOffset + } + + return { asks, bids } +} diff --git a/src/lib/kandel/index.ts b/src/lib/kandel/index.ts new file mode 100644 index 0000000..f635cbc --- /dev/null +++ b/src/lib/kandel/index.ts @@ -0,0 +1,33 @@ +// distribution + +export type { + CreateGeometricDistributionParams, + DistributionOffer, + Distribution, +} from './distribution.js' + +export { + CreateDistributionError, + createGeometricDistribution, +} from './distribution.js' + +// logs + +export type { KandelFromLogsResult } from './logs.js' + +export { + seederEventsABI, + getKandelsFromLogs, +} from './logs.js' + +// params + +export type { + RawKandelPositionParams, + PositionKandelParams, + RawKandelParams, + KandelParams, + ValidateParamsResult, +} from './params.js' + +export { validateKandelParams } from './params.js' diff --git a/src/lib/kandel/logs.ts b/src/lib/kandel/logs.ts new file mode 100644 index 0000000..fbfb130 --- /dev/null +++ b/src/lib/kandel/logs.ts @@ -0,0 +1,39 @@ +import { + type Address, + type Hex, + type Log, + parseAbi, + parseEventLogs, +} from 'viem' + +export const seederEventsABI = parseAbi([ + 'event NewSmartKandel(address indexed owner, bytes32 indexed baseQuoteOlKeyHash, bytes32 indexed quoteBaseOlKeyHash, address kandel)', + 'event NewKandel(address indexed owner, bytes32 indexed baseQuoteOlKeyHash, bytes32 indexed quoteBaseOlKeyHash, address kandel)', +]) + +export type KandelFromLogsResult = { + address: Address + type: 'SmartKandel' | 'Kandel' + owner: Address + baseQuoteOlKeyHash: Hex +}[] + +export function getKandelsFromLogs(logs: Log[]) { + const events = parseEventLogs({ + logs, + abi: seederEventsABI, + eventName: ['NewKandel', 'NewSmartKandel'], + }) + const kandels = [] as KandelFromLogsResult + + for (const event of events) { + kandels.push({ + address: event.address, + type: event.eventName === 'NewSmartKandel' ? 'SmartKandel' : 'Kandel', + owner: event.args.owner, + baseQuoteOlKeyHash: event.args.baseQuoteOlKeyHash, + }) + } + + return kandels +} diff --git a/src/lib/kandel/params.ts b/src/lib/kandel/params.ts new file mode 100644 index 0000000..d15bec8 --- /dev/null +++ b/src/lib/kandel/params.ts @@ -0,0 +1,208 @@ +import { + type GlobalConfig, + type LocalConfig, + type MarketParams, + minVolume, +} from '../../index.js' +import { + humanPriceToRawPrice, + rawPriceToHumanPrice, +} from '../human-readable.js' +import { priceFromTick, tickFromPrice } from '../tick.js' +import { + type Distribution, + createGeometricDistribution, +} from './distribution.js' + +export type RawKandelPositionParams = { + minPrice: number + maxPrice: number + midPrice: number + pricePoints: bigint + market: MarketParams +} + +export type PositionKandelParams = { + baseQuoteTickIndex0: bigint + baseQuoteTickOffset: bigint + firstAskIndex: bigint + pricePoints: bigint +} + +function getKandelPositionRawParams( + params: RawKandelPositionParams, +): PositionKandelParams { + const { market, pricePoints } = params + const baseQuoteTickIndex0 = tickFromPrice( + humanPriceToRawPrice(params.minPrice, market), + market.tickSpacing, + ) + const midTick = tickFromPrice( + humanPriceToRawPrice(params.midPrice, market), + market.tickSpacing, + ) + const maxTick = tickFromPrice( + humanPriceToRawPrice(params.maxPrice, market), + market.tickSpacing, + ) + let baseQuoteTickOffset = + ((maxTick - baseQuoteTickIndex0) / + (pricePoints - 1n) / + market.tickSpacing) * + market.tickSpacing + if (baseQuoteTickOffset === 0n) baseQuoteTickOffset = market.tickSpacing + let firstAskIndex = 0n + + for (; firstAskIndex < pricePoints; ++firstAskIndex) { + if (baseQuoteTickIndex0 + firstAskIndex * baseQuoteTickOffset >= midTick) + break + } + + return { + baseQuoteTickIndex0, + baseQuoteTickOffset, + firstAskIndex, + pricePoints, + } +} + +export type RawKandelParams = RawKandelPositionParams & { + baseAmount: bigint + quoteAmount: bigint + stepSize: bigint + gasreq: bigint + factor: number + asksLocalConfig: LocalConfig + bidsLocalConfig: LocalConfig + marketConfig: GlobalConfig +} + +export type KandelParams = PositionKandelParams & { + stepSize: bigint + askGives: bigint + bidGives: bigint + gasreq: bigint +} + +export type ValidateParamsResult = { + params: KandelParams + rawParams: RawKandelParams + minBaseAmount: bigint + minQuoteAmount: bigint + minProvision: bigint + isValid: boolean +} + +function countBidsAndAsks(distribution: Distribution) { + let nBids = 0n + let nAsks = 0n + for (let i = 0; i < distribution.asks.length; i++) { + if (distribution.asks[i].gives !== 0n) nAsks++ + if (distribution.bids[i].gives !== 0n) nBids++ + } + return { + nBids, + nAsks, + } +} + +function changeGives( + distribution: Distribution, + bidGives: bigint, + askGives: bigint, +): Distribution { + for (let i = 0; i < distribution.asks.length; i++) { + if (distribution.asks[i].gives !== 0n) distribution.asks[i].gives = askGives + if (distribution.bids[i].gives !== 0n) distribution.bids[i].gives = bidGives + } + return distribution +} + +export function validateKandelParams( + params: RawKandelParams, +): ValidateParamsResult { + const { baseQuoteTickIndex0, baseQuoteTickOffset, firstAskIndex } = + getKandelPositionRawParams(params) + + const { + pricePoints, + stepSize, + market, + gasreq, + factor, + asksLocalConfig, + bidsLocalConfig, + marketConfig, + } = params + + let distribution = createGeometricDistribution({ + baseQuoteTickIndex0, + baseQuoteTickOffset, + firstAskIndex, + pricePoints, + stepSize, + market, + askGives: 1n, + bidGives: 1n, + }) + + const { nBids, nAsks } = countBidsAndAsks(distribution) + // asks gives base and bids gives quote + const askGives = params.baseAmount / nAsks + const bidGives = params.quoteAmount / nBids + + distribution = changeGives(distribution, bidGives, askGives) + + const baseAmount = askGives * nAsks + const quoteAmount = bidGives * nBids + + const minPrice = rawPriceToHumanPrice( + priceFromTick(baseQuoteTickIndex0), + market, + ) + const maxPrice = rawPriceToHumanPrice( + priceFromTick( + baseQuoteTickIndex0 + baseQuoteTickOffset * (pricePoints - 1n), + ), + market, + ) + + const bigintFactor = BigInt(factor * 10_000) + + const minAsk = (minVolume(asksLocalConfig, gasreq) * bigintFactor) / 10_000n + const minBid = (minVolume(bidsLocalConfig, gasreq) * bigintFactor) / 10_000n + + const minBaseAmount = minAsk * nAsks + const minQuoteAmount = minBid * nBids + const minProvision = + ((gasreq + asksLocalConfig.offer_gasbase) * nAsks + + (gasreq + bidsLocalConfig.offer_gasbase) * nBids) * + marketConfig.gasprice * + BigInt(1e6) + + const isValid = askGives >= minAsk && bidGives >= minBid + + return { + params: { + baseQuoteTickIndex0, + baseQuoteTickOffset, + firstAskIndex, + stepSize, + askGives, + bidGives, + gasreq, + pricePoints, + }, + rawParams: { + ...params, + baseAmount, + quoteAmount, + minPrice, + maxPrice, + }, + minBaseAmount, + minQuoteAmount, + minProvision, + isValid, + } +} diff --git a/src/lib/limit-order.ts b/src/lib/limit-order.ts index a0a5924..3bb1f6e 100644 --- a/src/lib/limit-order.ts +++ b/src/lib/limit-order.ts @@ -46,7 +46,7 @@ export function rawLimitOrderResultFromLogs( const loKeyHash = hash(flip(params.olKey)).toLowerCase() for (const event of events) { if ( - event.args.olKeyHash !== loKeyHash || + event.args.olKeyHash.toLowerCase() !== loKeyHash || !isAddressEqual(params.user, event.args.maker) ) continue diff --git a/src/lib/market-order-simulation.ts b/src/lib/market-order-simulation.ts index 239262e..00df19c 100644 --- a/src/lib/market-order-simulation.ts +++ b/src/lib/market-order-simulation.ts @@ -132,6 +132,7 @@ export type MarketOrderSimulationParams = { * @param feePaid the total fee paid in the base token if buying, or quote token if selling * @param maxTickEncountered the highest tick encountered * @param minSlippage the minimum slippage to specify + * @param price the price of the market order */ export type MarketOrderSimulationResult = { baseAmount: bigint @@ -141,6 +142,7 @@ export type MarketOrderSimulationResult = { maxTickEncountered: bigint minSlippage: number fillWants: boolean + price: number } /** @@ -192,6 +194,7 @@ export function marketOrderSimulation( feePaid: raw.feePaid, maxTickEncountered: raw.maxTickEncountered, minSlippage: slippage, + price, fillWants, } } diff --git a/src/lib/market-order.ts b/src/lib/market-order.ts index 27c044e..90925f9 100644 --- a/src/lib/market-order.ts +++ b/src/lib/market-order.ts @@ -66,7 +66,7 @@ export function rawMarketOrderResultFromLogs( // check if the event is related to the order if ( !isAddressEqual(taker, params.taker) || - olKeyHash.toLowerCase() !== olKeyHash + event.args.olKeyHash.toLowerCase() !== olKeyHash ) continue diff --git a/src/types/actions/steps.ts b/src/types/actions/steps.ts index 4274e0e..db4b28d 100644 --- a/src/types/actions/steps.ts +++ b/src/types/actions/steps.ts @@ -1,4 +1,5 @@ import type { Address } from 'viem' +import type { Logic, Token } from '../../index.js' import type { OLKey } from '../lib.js' export type Step< @@ -12,7 +13,7 @@ export type Step< export type ERC20ApprovalStep = Step< 'erc20Approval', - { token: Address; from: Address; spender: Address; amount: bigint } + { token: Token; from: Address; spender: Address; amount: bigint } > export type DeployRouterStep = Step<'deployRouter', { owner: Address }> @@ -25,8 +26,8 @@ export type SetKandelLogicsStep = Step< 'setKandelLogics', { kandel: Address - baseLogic: Address - quoteLogic: Address + baseLogic?: Logic + quoteLogic?: Logic gasRequirement: bigint } > @@ -42,7 +43,6 @@ export type AmplifiedOrderSteps = readonly [ ] export type KandelSteps = readonly [ DeployRouterStep, - SowKandelStep, BindStep, SetKandelLogicsStep, ERC20ApprovalStep,