From 8b2e3fa12b5b479fef91affbdfeed9023d0ee041 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Thu, 19 Sep 2024 14:55:15 -0600 Subject: [PATCH 01/14] feat: add zerox api to SDK --- projects/sdk/src/index.ts | 6 + projects/sdk/src/lib/BeanstalkSDK.ts | 4 + .../src/lib/farm/actions/PipelineConvert.ts | 64 ++++ projects/sdk/src/lib/farm/actions/index.ts | 2 + projects/sdk/src/lib/matcha/index.ts | 3 + projects/sdk/src/lib/matcha/types.ts | 284 ++++++++++++++++++ projects/sdk/src/lib/matcha/zeroX.ts | 55 ++++ .../sdk/src/lib/silo/GenConvertOperation.ts | 40 --- 8 files changed, 418 insertions(+), 40 deletions(-) create mode 100644 projects/sdk/src/lib/farm/actions/PipelineConvert.ts create mode 100644 projects/sdk/src/lib/matcha/index.ts create mode 100644 projects/sdk/src/lib/matcha/types.ts create mode 100644 projects/sdk/src/lib/matcha/zeroX.ts delete mode 100644 projects/sdk/src/lib/silo/GenConvertOperation.ts diff --git a/projects/sdk/src/index.ts b/projects/sdk/src/index.ts index 9aafa90a5..633145b92 100644 --- a/projects/sdk/src/index.ts +++ b/projects/sdk/src/index.ts @@ -27,5 +27,11 @@ export type { AdvancedPipeCallStruct as AdvancedPipeStruct } from "src/lib/depot"; +export type { + ZeroExQuoteParams, + ZeroExQuoteResponse, + ZeroExAPIRequestParams +} from "src/lib/matcha/types"; + // Utilities export * as TestUtils from "./utils/TestUtils"; diff --git a/projects/sdk/src/lib/BeanstalkSDK.ts b/projects/sdk/src/lib/BeanstalkSDK.ts index f9b45fedb..63f44e4bf 100644 --- a/projects/sdk/src/lib/BeanstalkSDK.ts +++ b/projects/sdk/src/lib/BeanstalkSDK.ts @@ -17,6 +17,7 @@ import defaultSettings from "src/defaultSettings.json"; import { WellsSDK } from "@beanstalk/sdk-wells"; import { ChainId, ChainResolver } from "@beanstalk/sdk-core"; import { Field } from "./field"; +import { ZeroX } from "./matcha"; export type Provider = ethers.providers.JsonRpcProvider; export type Signer = ethers.Signer; @@ -28,6 +29,7 @@ export type BeanstalkConfig = Partial<{ subgraphUrl: string; source: DataSource; DEBUG: boolean; + zeroXApiKey?: string; }>; type Reconfigurable = Pick; @@ -55,6 +57,7 @@ export class BeanstalkSDK { public readonly pools: Pools; public readonly graphql: GraphQLClient; public readonly queries: Queries; + public readonly zeroX: ZeroX; public readonly farm: Farm; public readonly silo: Silo; @@ -83,6 +86,7 @@ export class BeanstalkSDK { this.pools = new Pools(this); this.graphql = new GraphQLClient(this.subgraphUrl); this.queries = getQueries(this.graphql); + this.zeroX = new ZeroX(config?.zeroXApiKey); // // Internal this.events = new EventManager(this); diff --git a/projects/sdk/src/lib/farm/actions/PipelineConvert.ts b/projects/sdk/src/lib/farm/actions/PipelineConvert.ts new file mode 100644 index 000000000..9d891214a --- /dev/null +++ b/projects/sdk/src/lib/farm/actions/PipelineConvert.ts @@ -0,0 +1,64 @@ +import { ethers } from "ethers"; +import { BasicPreparedResult, RunContext, StepClass } from "src/classes/Workflow"; +import { BeanstalkSDK } from "src/lib/BeanstalkSDK"; +import { ERC20Token } from "src/classes/Token"; +import { TokenValue } from "@beanstalk/sdk-core"; +import { AdvancedPipeCallStruct } from "src/lib/depot"; + +export class PipelineConvert extends StepClass { + static sdk: BeanstalkSDK; + public name: string = "pipeline-convert"; + + constructor( + private _tokenIn: ERC20Token, + public readonly _stems: ethers.BigNumberish[], + public readonly _amounts: ethers.BigNumberish[], + private _tokenOut: ERC20Token, + private _amountIn: TokenValue, + private _minAmountOut: TokenValue, + public readonly advancedPipeStructs: AdvancedPipeCallStruct[] + ) { + super(); + } + + async run(_amountInStep: ethers.BigNumber, context: RunContext) { + return { + name: this.name, + amountOut: _amountInStep, + prepare: () => { + PipelineConvert.sdk.debug(`[${this.name}.encode()]`, { + tokenIn: this._tokenIn, + amounts: this._amounts, + stems: this._stems, + tokenOut: this._tokenOut, + amountIn: this._amountIn, + minAmountOut: this._minAmountOut, + advancedPipeStructs: this.advancedPipeStructs + }); + return { + target: PipelineConvert.sdk.contracts.beanstalk.address, + callData: PipelineConvert.sdk.contracts.beanstalk.interface.encodeFunctionData( + "pipelineConvert", + [ + this._tokenIn.address, + this._stems, + this._amounts, + this._tokenOut.address, + this.advancedPipeStructs + ] + ) + }; + }, + decode: (data: string) => + PipelineConvert.sdk.contracts.beanstalk.interface.decodeFunctionData( + "pipelineConvert", + data + ), + decodeResult: (result: string) => + PipelineConvert.sdk.contracts.beanstalk.interface.decodeFunctionResult( + "pipelineConvert", + result + ) + }; + } +} diff --git a/projects/sdk/src/lib/farm/actions/index.ts b/projects/sdk/src/lib/farm/actions/index.ts index a754af007..b01431133 100644 --- a/projects/sdk/src/lib/farm/actions/index.ts +++ b/projects/sdk/src/lib/farm/actions/index.ts @@ -20,6 +20,7 @@ import { UniswapV3Swap } from "./UniswapV3Swap"; import { DevDebug } from "./_DevDebug"; import { LidoWrapSteth } from "./LidoWrapSteth"; import { LidoUnwrapWstETH } from "./LidoUnwrapWstETH"; +import { PipelineConvert } from "./PipelineConvert"; export { // Approvals @@ -44,6 +45,7 @@ export { ClaimWithdrawal, TransferDeposits, TransferDeposit, + PipelineConvert, // Lido LidoWrapSteth, diff --git a/projects/sdk/src/lib/matcha/index.ts b/projects/sdk/src/lib/matcha/index.ts new file mode 100644 index 000000000..dc0bb4471 --- /dev/null +++ b/projects/sdk/src/lib/matcha/index.ts @@ -0,0 +1,3 @@ +export * from "./types"; + +export { ZeroX } from "./zeroX"; diff --git a/projects/sdk/src/lib/matcha/types.ts b/projects/sdk/src/lib/matcha/types.ts new file mode 100644 index 000000000..9f2bef486 --- /dev/null +++ b/projects/sdk/src/lib/matcha/types.ts @@ -0,0 +1,284 @@ +export interface ZeroExQuoteParams extends ZeroExAPIRequestParams { + mode: "exactInput" | "exactOutput"; + enabled: boolean; +} + +export interface ZeroExAPIRequestParams { + /** + * The ERC20 token address of the token you want to sell. It is recommended to always use the token address + * instead of token symbols (e.g. ETH ) which may not be recognized by the API. + */ + sellToken: string; + /** + * The ERC20 token address of the token you want to receive. It is recommended to always use the token address + * instead of token symbols (e.g. ETH ) which may not be recognized by the API. + */ + buyToken: string; + /** + * (Optional) The amount of sellToken (in sellToken base units) you want to send. Either sellAmount or buyAmount + * must be present in a request. Specifying sellAmount is the recommended way to interact with + * 0x API as it covers all on-chain sources. + */ + sellAmount?: string; + /** + * (Optional) The amount of buyToken(in buyToken base units) you want to receive. Either sellAmount + * or buyAmount must be present in a request. Note that some on-chain sources do not allow + * specifying buyAmount, when using buyAmount these sources are excluded. + */ + buyAmount?: string; + /** + * (Optional, default is 0.01 for 1%) The maximum acceptable slippage of the buyToken amount if sellAmount + * is provided; The maximum acceptable slippage of the sellAmount amount if buyAmount is provided + * (e.g. 0.03 for 3% slippage allowed). The lowest possible value that can be set for this parameter + * is 0; in other words, no amount of slippage would be allowed. If no value for this optional parameter is + * provided in the API request, the default slippage percentage is 1%. + */ + slippagePercentage?: string; + /** + * (Optional, defaults to ethgasstation "fast") The target gas price (in wei) for the swap transaction. + * If the price is too low to achieve the quote, an error will be returned. + */ + gasPrice?: string; + /** + * (Optional) The address which will fill the quote. While optional, we highly recommend providing this + * parameter if possible so that the API can more accurately estimate the gas required for the swap transaction. + * This helps when validating the entire transaction for success, and catches revert issues. If the validation + * fails, a Revert Error will be returned in the response. The quote should be fillable if this address is provided. + * + * Also, make sure this address has enough token balance. Additionally, including the takerAddress is required + * if you want to integrate RFQ liquidity. + */ + takerAddress?: string; + /** + * (Optional) Liquidity sources (Uniswap, SushiSwap, 0x, Curve, etc) that will not be included in the provided quote. + * See the docs for a full list of sources. + * + * This parameter cannot be combined with includedSources. + */ + excludedSources?: string; + /** + * (Optional) Typically used to filter for RFQ liquidity without any other DEX orders which this is useful + * for testing your RFQ integration. To do so, set it to 0x. + * + * This parameter cannot be combined with excludedSources. + */ + includedSources?: string; + /** + * (Optional) Normally, whenever a takerAddress is provided, the API will validate the quote for the user. + * + * For more details, see "How does takerAddress help with catching issues?" in the docs. + * + * When this parameter is set to true, that validation will be skipped. + * + * Also see Quote Validation in the docs. . + */ + skipValidation?: boolean; + /** + * (Optional) The ETH address that should receive affiliate fees specified with buyTokenPercentageFee. + * Can be used combination with buyTokenPercentageFee to set a commission/trading fee when using the API. + * + * Learn more about how to setup a trading fee/commission fee/transaction fee in the FAQs. + */ + feeRecipient?: string; + /** + * (Optional) The percentage (denoted as a decimal between 0 - 1.0 where 1.0 represents 100%) of + * the buyAmount that should be attributed to feeRecipient as affiliate fees. Note that this requires + * that the feeRecipient parameter is also specified in the request. Learn more about how to setup + * a trading fee/commission fee/transaction fee in the FAQs. + */ + buyTokenPercentageFee?: string; + /** + * (Optional, defaults to 100%) The percentage (between 0 - 1.0) of allowed price impact. + * + * When priceImpactProtectionPercentage is set, estimatedPriceImpact is returned which estimates the change + * in the price of the specified asset that would be caused by the executed swap due to price impact. + * + * If the estimated price impact is above the percentage indicated, an error will be returned. For example, + * if PriceImpactProtectionPercentage=.15 (15%), any quote with a price impact higher than 15% will return an error. + * + * This is an opt-in feature, the default value of 1.0 will disable the feature. When it is set to 1.0 (100%) + * it means that every transaction is allowed to pass. + * + * Note: When we fail to calculate Price Impact we will return null and Price Impact Protection will be disabled + * See affects on estimatedPriceImpact in the Response fields. Read more about price + * impact protection and how to set it up in the docs. + */ + priceImpactProtectionPercentage?: string; + /** + * (Optional) The recipient address of any trade surplus fees. If specified, this address will collect trade surplus + * when applicable. Otherwise, trade surplus will not be collected. + * + * Note: Trade surplus is only sent to this address for sells. It is a no-op for buys. + * Read more about "Can I collect trade surplus?" in the FAQs. + */ + feeRecipientTradeSurplus?: string; + /** + * (Optional) A boolean field. If set to true, the 0x Swap API quote request should sell the entirety of the + * caller's takerToken balance. A sellAmount is still required, even if it is a best guess, because it is + * how a reasonable minimum received amount is determined after slippage. + * + * Note: This parameter is only required for special cases, such as when setting up a multi-step transaction + * or composable operation, where the entire balance is not known ahead of time. Read more about + * "Is there a way to sell assets via Swap API if the exact sellToken amount is not known + * before the transaction is executed?" in the FAQs. + */ + shouldSellEntireBalance?: boolean; +} + +interface ZeroExOrder { + type: number; + source: string; + makerToken: string; + takerToken: string; + makerAmount: string; + takerAmount: string; + fillData: any; + fill: any; +} + +interface ZeroExFee { + feeType: string | "volume"; + feeToken: string; + feeAmount: string; + billingType: string | "on-chain"; +} + +interface ZeroExSource { + name: string; + proportion: string; +} + +/** + * Response type from 0x quote-v1 swap API. + * + * @link https://0x.org/docs/1.0/0x-swap-api/api-references/get-swap-v1-quote + */ +export interface ZeroExQuoteResponse { + /** + * + */ + chainId: number; + /** + * If {buyAmount} was specifed in the request, it provides the price of buyToken in sellToken & vice versa. + * Does not include slippage + */ + price: string; + /** + * Similar to price, but with fees removed from the price calculation. Price as if not fee is charged. + */ + grossPrice: string; + /** + * When priceImpactProtectionPercentage is set, this value returns the estimated change in the price of + * the specified asset that would be caused by the executed swap. + */ + estimatedPriceImpact: string | null; + /** + * The amount of ether (in wei) that should be sent with the transaction. + */ + value: string; + /** + * The gas price (in wei) that should be used to send the transaction. + * The transaction needs to be sent with this gasPrice or lower for the transaction to be successful. + */ + gasPrice: string; + /** + * The estimated gas limit that should be used to send the transaction to guarantee settlement. + * While a computed estimate is returned in all responses, an accurate estimate will only be returned if + * a takerAddress is included in the request. + */ + gas: string; + /** + * The estimate for the amount of gas that will actually be used in the transaction. Always less than gas. + */ + estimatedGas: string; + /** + * The maximum amount of ether (in wei) that will be paid towards the protocol fee, and what is used to compute the value field of the transaction. + * Note, as of ZEIP-91, protocol fees have been removed for all order types. + */ + protocolFee: string; + /** + * The minimum amount of ether (in wei) that will be paid towards the protocol fee during the transaction. + */ + minimumProtocolFee: string; + /** + * The ERC20 token address of the token you want to receive in quote. + */ + buyTokenAddress: string; + /** + * The amount of buyToken (in buyToken units) that would be bought in this swap. + * Certain on-chain sources do not allow specifying buyAmount, when using buyAmount these sources are excluded. + */ + buyAmount: string; + /** + * Similar to buyAmount but with fees removed. This is the buyAmount as if no fee is charged. + */ + grossBuyAmount: string; + /** + * The ERC20 token address of the token you want to sell with quote. + */ + sellTokenAddress: string; + /** + * The amount of sellToken (in sellToken units) that would be sold in this swap. + * Specifying sellAmount is the recommended way to interact with 0xAPI as it covers all on-chain sources. + */ + sellAmount: string; + /** + * Similar to sellAmount but with fees removed. + * This is the sellAmount as if no fee is charged. + * Note: Currently, this will be the same as sellAmount as fees can only be configured to occur on the buyToken. + */ + grossSellAmount: string; + /** + * The percentage distribution of buyAmount or sellAmount split between each liquidity source. + */ + sources: ZeroExSource[]; + /** + * The target contract address for which the user needs to have an allowance in order to be able to complete the swap. + * Typically this is the 0x Exchange Proxy contract address for the specified chain. + * For swaps with "ETH" as sellToken, wrapping "ETH" to "WETH" or unwrapping "WETH" to "ETH" no allowance is needed, + * a null address of 0x0000000000000000000000000000000000000000 is then returned instead. + */ + allowanceTarget: string; + /** + * The rate between ETH and sellToken + */ + sellTokenToEthRate: string; + /** + * The rate between ETH and buyToken + */ + buyTokenToEthRate: string; + /** + * The address of the contract to send call data to. + */ + to: string; + /** + * + */ + from: string; + /** + * The call data + */ + data: string; + /** + * The price which must be met or else the entire transaction will revert. This price is influenced by the slippagePercentage parameter. + * On-chain sources may encounter price movements from quote to settlement. + */ + guaranteedPrice: string; + /** + * The details used to fill orders, used by market makers. If orders is not empty, there will be a type on each order. + * For wrap/unwrap, orders is empty. otherwise, should be populated. + */ + orders: ZeroExOrder[]; + /** + * 0x Swap API fees that would be charged. + */ + fees: Record; + /** + * + */ + decodedUniqueId: string; + /** + * + */ + auxiliaryChainData: any; +} diff --git a/projects/sdk/src/lib/matcha/zeroX.ts b/projects/sdk/src/lib/matcha/zeroX.ts new file mode 100644 index 000000000..3a94496eb --- /dev/null +++ b/projects/sdk/src/lib/matcha/zeroX.ts @@ -0,0 +1,55 @@ +import { ZeroExAPIRequestParams, ZeroExQuoteParams, ZeroExQuoteResponse } from "./types"; + +export class ZeroX { + readonly swapV1Endpoint = "http://arbitrum.api.0x.org/swap/v1/quote"; + + constructor(private _apiKey: string = "") {} + + /** + * fetch the quote from the 0x API + * + * params + * - slippagePercentage: In human readable form. 0.01 = 1%. Defaults to 0.001 (0.1%) + * - skipValidation: defaults to true + * - shouldSellEntireBalance: defaults to false + */ + async fetchQuote(args: ZeroExQuoteParams) { + if (!this._apiKey) { + throw new Error("Cannot fetch from 0x without an API key"); + } + + const fetchParams = new URLSearchParams( + this.generateQuoteParams(args) as unknown as Record + ); + + const options = { + method: "GET", + headers: new Headers({ + "0x-api-key": this._apiKey + }) + }; + + const url = `${this.swapV1Endpoint}?${fetchParams.toString()}`; + + return fetch(url, options).then((r) => r.json()) as Promise; + } + + private generateQuoteParams(args: ZeroExQuoteParams): ZeroExAPIRequestParams { + const { enabled, mode, ...params } = args; + + if (!params.buyToken && !params.sellToken) { + throw new Error("buyToken and sellToken and required"); + } + + if (!params.sellAmount && !params.buyAmount) { + throw new Error("sellAmount or buyAmount is required"); + } + + return { + ...params, + slippagePercentage: params.slippagePercentage ?? "0.01", + skipValidation: params.skipValidation ?? true, + shouldSellEntireBalance: params.shouldSellEntireBalance ?? false + }; + } +} diff --git a/projects/sdk/src/lib/silo/GenConvertOperation.ts b/projects/sdk/src/lib/silo/GenConvertOperation.ts deleted file mode 100644 index 0db132ce2..000000000 --- a/projects/sdk/src/lib/silo/GenConvertOperation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { BasinWell } from "src/classes/Pool"; -import { BeanstalkSDK } from "../BeanstalkSDK"; -import { ERC20Token } from "src/classes/Token"; -import { AdvancedPipePreparedResult } from "../depot/pipe"; - -export class PipelineConvertOperation { - static sdk: BeanstalkSDK; - - /** - * The whitelisted token to convert from. - */ - readonly inputToken: ERC20Token; - - /** - * The whitelisted token to convert to. - */ - target: ERC20Token; - - advancedPipeCalls: Required[] = []; - - constructor(sdk: BeanstalkSDK, inputToken: ERC20Token) { - PipelineConvertOperation.sdk = sdk; - - this.validateIsWhitelisted(inputToken); - this.inputToken = inputToken; - } - - setTarget(token: ERC20Token) { - this.validateIsWhitelisted(token); - this.target = token; - } - - initialize(token: ERC20Token) {} - - private validateIsWhitelisted(token: ERC20Token) { - if (!PipelineConvertOperation.sdk.tokens.isWhitelisted(token)) { - throw new Error(`GenConvertOperation: Token ${token.symbol} is not whitelisted in the Silo.`); - } - } -} From 7fa0e32589202283cc6b0e62b80d794440b3dfd4 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Thu, 19 Sep 2024 14:55:30 -0600 Subject: [PATCH 02/14] feat: add generated files --- projects/ui/src/graph/graphql.schema.json | 34 +- protocol/abi/Beanstalk.json | 1111 ++++++++--------- protocol/abi/MockBeanstalk.json | 1318 ++++++++++----------- 3 files changed, 1143 insertions(+), 1320 deletions(-) diff --git a/projects/ui/src/graph/graphql.schema.json b/projects/ui/src/graph/graphql.schema.json index d1177191e..e0262f1d3 100644 --- a/projects/ui/src/graph/graphql.schema.json +++ b/projects/ui/src/graph/graphql.schema.json @@ -166576,7 +166576,9 @@ "name": "derivedFrom", "description": "creates a virtual field on the entity that may be queried but cannot be set manually through the mappings API.", "isRepeatable": false, - "locations": ["FIELD_DEFINITION"], + "locations": [ + "FIELD_DEFINITION" + ], "args": [ { "name": "field", @@ -166600,14 +166602,20 @@ "name": "entity", "description": "Marks the GraphQL type as indexable entity. Each type that should be an entity is required to be annotated with this directive.", "isRepeatable": false, - "locations": ["OBJECT"], + "locations": [ + "OBJECT" + ], "args": [] }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", "isRepeatable": false, - "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], "args": [ { "name": "if", @@ -166631,14 +166639,20 @@ "name": "oneOf", "description": "Indicates exactly one field must be supplied and this field must not be `null`.", "isRepeatable": false, - "locations": ["INPUT_OBJECT"], + "locations": [ + "INPUT_OBJECT" + ], "args": [] }, { "name": "skip", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", "isRepeatable": false, - "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], "args": [ { "name": "if", @@ -166662,7 +166676,9 @@ "name": "specifiedBy", "description": "Exposes a URL that specifies the behavior of this scalar.", "isRepeatable": false, - "locations": ["SCALAR"], + "locations": [ + "SCALAR" + ], "args": [ { "name": "url", @@ -166686,7 +166702,9 @@ "name": "subgraphId", "description": "Defined a Subgraph ID for an object type", "isRepeatable": false, - "locations": ["OBJECT"], + "locations": [ + "OBJECT" + ], "args": [ { "name": "id", @@ -166708,4 +166726,4 @@ } ] } -} +} \ No newline at end of file diff --git a/protocol/abi/Beanstalk.json b/protocol/abi/Beanstalk.json index c5945020b..ffd90c0dd 100644 --- a/protocol/abi/Beanstalk.json +++ b/protocol/abi/Beanstalk.json @@ -4450,43 +4450,6 @@ "stateMutability": "payable", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "indexed": false, - "internalType": "int96", - "name": "stem", - "type": "int96" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "bdv", - "type": "uint256" - } - ], - "name": "AddDeposit", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -4704,43 +4667,6 @@ "name": "TransferBatch", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "depositId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TransferSingle", - "type": "event" - }, { "inputs": [ { @@ -4934,6 +4860,35 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "well", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "reserves", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lookback", + "type": "uint256" + } + ], + "name": "calculateDeltaBFromReserves", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -8056,9 +8011,9 @@ "type": "bytes1" }, { - "internalType": "int24", + "internalType": "int32", "name": "deltaStalkEarnedPerSeason", - "type": "int24" + "type": "int32" }, { "internalType": "uint128", @@ -8185,6 +8140,80 @@ "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "int96", + "name": "stem", + "type": "int96" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bdv", + "type": "uint256" + } + ], + "name": "AddDeposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, { "inputs": [ { @@ -9102,30 +9131,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "percentOfDepositedBdv", - "type": "uint256" - } - ], - "name": "calcGaugePointsWithParams", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -9184,47 +9189,74 @@ "type": "function" }, { - "inputs": [], - "name": "getAverageGrownStalkPerBdv", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], + "name": "getCaseData", + "outputs": [ + { + "internalType": "bytes32", + "name": "casesData", + "type": "bytes32" + } + ], "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "getAverageGrownStalkPerBdvPerSeason", + "name": "getCases", "outputs": [ { - "internalType": "uint128", - "name": "", - "type": "uint128" + "internalType": "bytes32[144]", + "name": "cases", + "type": "bytes32[144]" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "getBeanGaugePointsPerBdv", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], + "name": "getChangeFromCaseId", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "int8", + "name": "", + "type": "int8" + }, + { + "internalType": "uint80", + "name": "", + "type": "uint80" + }, + { + "internalType": "int80", + "name": "", + "type": "int80" + } + ], "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "getBeanToMaxLpGpPerBdvRatio", + "name": "getDeltaPodDemandLowerBound", "outputs": [ { "internalType": "uint256", @@ -9237,112 +9269,7 @@ }, { "inputs": [], - "name": "getBeanToMaxLpGpPerBdvRatioScaled", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getCaseData", - "outputs": [ - { - "internalType": "bytes32", - "name": "casesData", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getCases", - "outputs": [ - { - "internalType": "bytes32[144]", - "name": "cases", - "type": "bytes32[144]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getChangeFromCaseId", - "outputs": [ - { - "internalType": "uint32", - "name": "", - "type": "uint32" - }, - { - "internalType": "int8", - "name": "", - "type": "int8" - }, - { - "internalType": "uint80", - "name": "", - "type": "uint80" - }, - { - "internalType": "int80", - "name": "", - "type": "int80" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getDeltaPodDemand", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getDeltaPodDemandLowerBound", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getDeltaPodDemandUpperBound", + "name": "getDeltaPodDemandUpperBound", "outputs": [ { "internalType": "uint256", @@ -9456,121 +9383,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePoints", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePointsPerBdvForToken", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "well", - "type": "address" - } - ], - "name": "getGaugePointsPerBdvForWell", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePointsWithParams", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getGrownStalkIssuedPerGp", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getGrownStalkIssuedPerSeason", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLargestGpPerBdv", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getLargestLiqWell", @@ -9584,19 +9396,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "getLiquidityToSupplyRatio", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getLpToSupplyRatioLowerBound", @@ -9662,25 +9461,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - } - ], - "name": "getPodRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getPodRateLowerBound", @@ -9863,37 +9643,7 @@ }, { "inputs": [], - "name": "getSeedGauge", - "outputs": [ - { - "components": [ - { - "internalType": "uint128", - "name": "averageGrownStalkPerBdvPerSeason", - "type": "uint128" - }, - { - "internalType": "uint128", - "name": "beanToMaxLpGpPerBdvRatio", - "type": "uint128" - }, - { - "internalType": "bytes32[4]", - "name": "_buffer", - "type": "bytes32[4]" - } - ], - "internalType": "struct SeedGauge", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getTargetSeasonsToCatchUp", + "name": "getTargetSeasonsToCatchUp", "outputs": [ { "internalType": "uint256", @@ -9904,19 +9654,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "getTotalBdv", - "outputs": [ - { - "internalType": "uint256", - "name": "totalBdv", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getTotalUsdLiquidity", @@ -10472,64 +10209,342 @@ "type": "int8" }, { - "indexed": false, + "indexed": false, + "internalType": "uint256", + "name": "fieldId", + "type": "uint256" + } + ], + "name": "TemperatureChange", + "type": "event" + }, + { + "inputs": [], + "name": "getShipmentRoutes", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "planContract", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "planSelector", + "type": "bytes4" + }, + { + "internalType": "enum ShipmentRecipient", + "name": "recipient", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ShipmentRoute[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum LibTransfer.To", + "name": "mode", + "type": "uint8" + } + ], + "name": "gm", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "seasonTime", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "planContract", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "planSelector", + "type": "bytes4" + }, + { + "internalType": "enum ShipmentRecipient", + "name": "recipient", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ShipmentRoute[]", + "name": "shipmentRoutes", + "type": "tuple[]" + } + ], + "name": "setShipmentRoutes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "sunrise", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "percentOfDepositedBdv", + "type": "uint256" + } + ], + "name": "calcGaugePointsWithParams", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAverageGrownStalkPerBdv", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAverageGrownStalkPerBdvPerSeason", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanGaugePointsPerBdv", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanToMaxLpGpPerBdvRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanToMaxLpGpPerBdvRatioScaled", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDeltaPodDemand", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePointsPerBdvForToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "well", + "type": "address" + } + ], + "name": "getGaugePointsPerBdvForWell", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePointsWithParams", + "outputs": [ + { "internalType": "uint256", - "name": "fieldId", + "name": "", "type": "uint256" } ], - "name": "TemperatureChange", - "type": "event" + "stateMutability": "view", + "type": "function" }, { "inputs": [], - "name": "getShipmentRoutes", + "name": "getGrownStalkIssuedPerGp", "outputs": [ { - "components": [ - { - "internalType": "address", - "name": "planContract", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "planSelector", - "type": "bytes4" - }, - { - "internalType": "enum ShipmentRecipient", - "name": "recipient", - "type": "uint8" - }, - { - "internalType": "bytes", - "name": "data", - "type": "bytes" - } - ], - "internalType": "struct ShipmentRoute[]", + "internalType": "uint256", "name": "", - "type": "tuple[]" + "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - }, + "inputs": [], + "name": "getGrownStalkIssuedPerSeason", + "outputs": [ { - "internalType": "enum LibTransfer.To", - "name": "mode", - "type": "uint8" + "internalType": "uint256", + "name": "", + "type": "uint256" } ], - "name": "gm", + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLargestGpPerBdv", "outputs": [ { "internalType": "uint256", @@ -10537,17 +10552,17 @@ "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "seasonTime", + "name": "getLiquidityToSupplyRatio", "outputs": [ { - "internalType": "uint32", + "internalType": "uint256", "name": "", - "type": "uint32" + "type": "uint256" } ], "stateMutability": "view", @@ -10555,50 +10570,64 @@ }, { "inputs": [ + { + "internalType": "uint256", + "name": "fieldId", + "type": "uint256" + } + ], + "name": "getPodRate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getSeedGauge", + "outputs": [ { "components": [ { - "internalType": "address", - "name": "planContract", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "planSelector", - "type": "bytes4" + "internalType": "uint128", + "name": "averageGrownStalkPerBdvPerSeason", + "type": "uint128" }, { - "internalType": "enum ShipmentRecipient", - "name": "recipient", - "type": "uint8" + "internalType": "uint128", + "name": "beanToMaxLpGpPerBdvRatio", + "type": "uint128" }, { - "internalType": "bytes", - "name": "data", - "type": "bytes" + "internalType": "bytes32[4]", + "name": "_buffer", + "type": "bytes32[4]" } ], - "internalType": "struct ShipmentRoute[]", - "name": "shipmentRoutes", - "type": "tuple[]" + "internalType": "struct SeedGauge", + "name": "", + "type": "tuple" } ], - "name": "setShipmentRoutes", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "sunrise", + "name": "getTotalBdv", "outputs": [ { "internalType": "uint256", - "name": "", + "name": "totalBdv", "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "view", "type": "function" }, { @@ -11376,68 +11405,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "components": [ - { - "components": [ - { - "internalType": "address", - "name": "orderer", - "type": "address" - }, - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - }, - { - "internalType": "uint24", - "name": "pricePerPod", - "type": "uint24" - }, - { - "internalType": "uint256", - "name": "maxPlaceInLine", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minFillAmount", - "type": "uint256" - } - ], - "internalType": "struct Order.PodOrder", - "name": "podOrder", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "beanAmount", - "type": "uint256" - } - ], - "internalType": "struct L1RecieverFacet.L1PodOrder[]", - "name": "orders", - "type": "tuple[]" - }, - { - "internalType": "bytes32[]", - "name": "proof", - "type": "bytes32[]" - } - ], - "name": "issuePodOrders", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -11573,74 +11540,6 @@ "stateMutability": "pure", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "components": [ - { - "components": [ - { - "internalType": "address", - "name": "orderer", - "type": "address" - }, - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - }, - { - "internalType": "uint24", - "name": "pricePerPod", - "type": "uint24" - }, - { - "internalType": "uint256", - "name": "maxPlaceInLine", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minFillAmount", - "type": "uint256" - } - ], - "internalType": "struct Order.PodOrder", - "name": "podOrder", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "beanAmount", - "type": "uint256" - } - ], - "internalType": "struct L1RecieverFacet.L1PodOrder[]", - "name": "orders", - "type": "tuple[]" - }, - { - "internalType": "bytes32[]", - "name": "proof", - "type": "bytes32[]" - } - ], - "name": "verifyOrderProof", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [ { diff --git a/protocol/abi/MockBeanstalk.json b/protocol/abi/MockBeanstalk.json index 94170ab5d..46a62687f 100644 --- a/protocol/abi/MockBeanstalk.json +++ b/protocol/abi/MockBeanstalk.json @@ -4450,43 +4450,6 @@ "stateMutability": "payable", "type": "function" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "account", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "indexed": false, - "internalType": "int96", - "name": "stem", - "type": "int96" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "bdv", - "type": "uint256" - } - ], - "name": "AddDeposit", - "type": "event" - }, { "anonymous": false, "inputs": [ @@ -4704,43 +4667,6 @@ "name": "TransferBatch", "type": "event" }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "operator", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "depositId", - "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "TransferSingle", - "type": "event" - }, { "inputs": [ { @@ -4934,6 +4860,35 @@ "stateMutability": "payable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "well", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "reserves", + "type": "uint256[]" + }, + { + "internalType": "uint256", + "name": "lookback", + "type": "uint256" + } + ], + "name": "calculateDeltaBFromReserves", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -8056,9 +8011,9 @@ "type": "bytes1" }, { - "internalType": "int24", + "internalType": "int32", "name": "deltaStalkEarnedPerSeason", - "type": "int24" + "type": "int32" }, { "internalType": "uint128", @@ -8185,6 +8140,80 @@ "stateMutability": "view", "type": "function" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "int96", + "name": "stem", + "type": "int96" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bdv", + "type": "uint256" + } + ], + "name": "AddDeposit", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, { "inputs": [ { @@ -8891,30 +8920,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - }, - { - "internalType": "uint256", - "name": "percentOfDepositedBdv", - "type": "uint256" - } - ], - "name": "calcGaugePointsWithParams", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -8973,47 +8978,74 @@ "type": "function" }, { - "inputs": [], - "name": "getAverageGrownStalkPerBdv", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], + "name": "getCaseData", + "outputs": [ + { + "internalType": "bytes32", + "name": "casesData", + "type": "bytes32" + } + ], "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "getAverageGrownStalkPerBdvPerSeason", + "name": "getCases", "outputs": [ { - "internalType": "uint128", - "name": "", - "type": "uint128" + "internalType": "bytes32[144]", + "name": "cases", + "type": "bytes32[144]" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "getBeanGaugePointsPerBdv", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], + "name": "getChangeFromCaseId", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + }, + { + "internalType": "int8", + "name": "", + "type": "int8" + }, + { + "internalType": "uint80", + "name": "", + "type": "uint80" + }, + { + "internalType": "int80", + "name": "", + "type": "int80" + } + ], "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "getBeanToMaxLpGpPerBdvRatio", + "name": "getDeltaPodDemandLowerBound", "outputs": [ { "internalType": "uint256", @@ -9026,7 +9058,7 @@ }, { "inputs": [], - "name": "getBeanToMaxLpGpPerBdvRatioScaled", + "name": "getDeltaPodDemandUpperBound", "outputs": [ { "internalType": "uint256", @@ -9037,67 +9069,91 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getCaseData", - "outputs": [ - { - "internalType": "bytes32", - "name": "casesData", - "type": "bytes32" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], - "name": "getCases", - "outputs": [ - { - "internalType": "bytes32[144]", - "name": "cases", - "type": "bytes32[144]" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getChangeFromCaseId", + "name": "getEvaluationParameters", "outputs": [ { - "internalType": "uint32", - "name": "", - "type": "uint32" - }, - { - "internalType": "int8", - "name": "", - "type": "int8" - }, - { - "internalType": "uint80", - "name": "", - "type": "uint80" - }, - { - "internalType": "int80", + "components": [ + { + "internalType": "uint256", + "name": "maxBeanMaxLpGpPerBdvRatio", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minBeanMaxLpGpPerBdvRatio", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "targetSeasonsToCatchUp", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "podRateLowerBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "podRateOptimal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "podRateUpperBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deltaPodDemandLowerBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deltaPodDemandUpperBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lpToSupplyRatioUpperBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lpToSupplyRatioOptimal", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lpToSupplyRatioLowerBound", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "excessivePriceThreshold", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "soilCoefficientHigh", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "soilCoefficientLow", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "baseReward", + "type": "uint256" + } + ], + "internalType": "struct EvaluationParameters", "name": "", - "type": "int80" + "type": "tuple" } ], "stateMutability": "view", @@ -9105,7 +9161,7 @@ }, { "inputs": [], - "name": "getDeltaPodDemand", + "name": "getExcessivePriceThreshold", "outputs": [ { "internalType": "uint256", @@ -9118,12 +9174,12 @@ }, { "inputs": [], - "name": "getDeltaPodDemandLowerBound", + "name": "getLargestLiqWell", "outputs": [ { - "internalType": "uint256", + "internalType": "address", "name": "", - "type": "uint256" + "type": "address" } ], "stateMutability": "view", @@ -9131,7 +9187,7 @@ }, { "inputs": [], - "name": "getDeltaPodDemandUpperBound", + "name": "getLpToSupplyRatioLowerBound", "outputs": [ { "internalType": "uint256", @@ -9144,26 +9200,7 @@ }, { "inputs": [], - "name": "getExcessivePriceThreshold", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePoints", + "name": "getLpToSupplyRatioOptimal", "outputs": [ { "internalType": "uint256", @@ -9175,14 +9212,8 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePointsPerBdvForToken", + "inputs": [], + "name": "getLpToSupplyRatioUpperBound", "outputs": [ { "internalType": "uint256", @@ -9194,14 +9225,8 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "well", - "type": "address" - } - ], - "name": "getGaugePointsPerBdvForWell", + "inputs": [], + "name": "getMaxBeanMaxLpGpPerBdvRatio", "outputs": [ { "internalType": "uint256", @@ -9213,14 +9238,8 @@ "type": "function" }, { - "inputs": [ - { - "internalType": "address", - "name": "token", - "type": "address" - } - ], - "name": "getGaugePointsWithParams", + "inputs": [], + "name": "getMinBeanMaxLpGpPerBdvRatio", "outputs": [ { "internalType": "uint256", @@ -9233,7 +9252,7 @@ }, { "inputs": [], - "name": "getGrownStalkIssuedPerGp", + "name": "getPodRateLowerBound", "outputs": [ { "internalType": "uint256", @@ -9246,7 +9265,7 @@ }, { "inputs": [], - "name": "getGrownStalkIssuedPerSeason", + "name": "getPodRateOptimal", "outputs": [ { "internalType": "uint256", @@ -9259,7 +9278,7 @@ }, { "inputs": [], - "name": "getLargestGpPerBdv", + "name": "getPodRateUpperBound", "outputs": [ { "internalType": "uint256", @@ -9271,65 +9290,38 @@ "type": "function" }, { - "inputs": [], - "name": "getLargestLiqWell", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLiquidityToSupplyRatio", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLpToSupplyRatioLowerBound", + "name": "getRelBeanToMaxLpRatioChangeFromCaseId", "outputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" + "internalType": "int80", + "name": "l", + "type": "int80" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "getLpToSupplyRatioOptimal", - "outputs": [ + "inputs": [ { "internalType": "uint256", - "name": "", + "name": "caseId", "type": "uint256" } ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getLpToSupplyRatioUpperBound", + "name": "getRelTemperatureChangeFromCaseId", "outputs": [ { - "internalType": "uint256", - "name": "", - "type": "uint256" + "internalType": "uint32", + "name": "mt", + "type": "uint32" } ], "stateMutability": "view", @@ -9337,129 +9329,7 @@ }, { "inputs": [], - "name": "getMaxBeanMaxLpGpPerBdvRatio", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getMinBeanMaxLpGpPerBdvRatio", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - } - ], - "name": "getPodRate", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPodRateLowerBound", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPodRateOptimal", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getPodRateUpperBound", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getRelBeanToMaxLpRatioChangeFromCaseId", - "outputs": [ - { - "internalType": "int80", - "name": "l", - "type": "int80" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "caseId", - "type": "uint256" - } - ], - "name": "getRelTemperatureChangeFromCaseId", - "outputs": [ - { - "internalType": "uint32", - "name": "mt", - "type": "uint32" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getSeasonStruct", + "name": "getSeasonStruct", "outputs": [ { "components": [ @@ -9560,126 +9430,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "getSeedGauge", - "outputs": [ - { - "components": [ - { - "internalType": "uint128", - "name": "averageGrownStalkPerBdvPerSeason", - "type": "uint128" - }, - { - "internalType": "uint128", - "name": "beanToMaxLpGpPerBdvRatio", - "type": "uint128" - }, - { - "internalType": "bytes32[4]", - "name": "_buffer", - "type": "bytes32[4]" - } - ], - "internalType": "struct SeedGauge", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "getSeedGaugeSetting", - "outputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "maxBeanMaxLpGpPerBdvRatio", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minBeanMaxLpGpPerBdvRatio", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "targetSeasonsToCatchUp", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "podRateLowerBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "podRateOptimal", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "podRateUpperBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "deltaPodDemandLowerBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "deltaPodDemandUpperBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "lpToSupplyRatioUpperBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "lpToSupplyRatioOptimal", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "lpToSupplyRatioLowerBound", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "excessivePriceThreshold", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "soilCoefficientHigh", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "soilCoefficientLow", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "baseReward", - "type": "uint256" - } - ], - "internalType": "struct EvaluationParameters", - "name": "", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getTargetSeasonsToCatchUp", @@ -9693,19 +9443,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "getTotalBdv", - "outputs": [ - { - "internalType": "uint256", - "name": "totalBdv", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getTotalUsdLiquidity", @@ -10318,7 +10055,298 @@ "type": "uint8" } ], - "name": "gm", + "name": "gm", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "seasonTime", + "outputs": [ + { + "internalType": "uint32", + "name": "", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "address", + "name": "planContract", + "type": "address" + }, + { + "internalType": "bytes4", + "name": "planSelector", + "type": "bytes4" + }, + { + "internalType": "enum ShipmentRecipient", + "name": "recipient", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct ShipmentRoute[]", + "name": "shipmentRoutes", + "type": "tuple[]" + } + ], + "name": "setShipmentRoutes", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "sunrise", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "percentOfDepositedBdv", + "type": "uint256" + } + ], + "name": "calcGaugePointsWithParams", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAverageGrownStalkPerBdv", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getAverageGrownStalkPerBdvPerSeason", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanGaugePointsPerBdv", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanToMaxLpGpPerBdvRatio", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getBeanToMaxLpGpPerBdvRatioScaled", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getDeltaPodDemand", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePoints", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePointsPerBdvForToken", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "well", + "type": "address" + } + ], + "name": "getGaugePointsPerBdvForWell", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + } + ], + "name": "getGaugePointsWithParams", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGrownStalkIssuedPerGp", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getGrownStalkIssuedPerSeason", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLargestGpPerBdv", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getLiquidityToSupplyRatio", "outputs": [ { "internalType": "uint256", @@ -10326,68 +10354,69 @@ "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "view", "type": "function" }, { - "inputs": [], - "name": "seasonTime", + "inputs": [ + { + "internalType": "uint256", + "name": "fieldId", + "type": "uint256" + } + ], + "name": "getPodRate", "outputs": [ { - "internalType": "uint32", + "internalType": "uint256", "name": "", - "type": "uint32" + "type": "uint256" } ], "stateMutability": "view", "type": "function" }, { - "inputs": [ + "inputs": [], + "name": "getSeedGauge", + "outputs": [ { "components": [ { - "internalType": "address", - "name": "planContract", - "type": "address" - }, - { - "internalType": "bytes4", - "name": "planSelector", - "type": "bytes4" + "internalType": "uint128", + "name": "averageGrownStalkPerBdvPerSeason", + "type": "uint128" }, { - "internalType": "enum ShipmentRecipient", - "name": "recipient", - "type": "uint8" + "internalType": "uint128", + "name": "beanToMaxLpGpPerBdvRatio", + "type": "uint128" }, { - "internalType": "bytes", - "name": "data", - "type": "bytes" + "internalType": "bytes32[4]", + "name": "_buffer", + "type": "bytes32[4]" } ], - "internalType": "struct ShipmentRoute[]", - "name": "shipmentRoutes", - "type": "tuple[]" + "internalType": "struct SeedGauge", + "name": "", + "type": "tuple" } ], - "name": "setShipmentRoutes", - "outputs": [], - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { "inputs": [], - "name": "sunrise", + "name": "getTotalBdv", "outputs": [ { "internalType": "uint256", - "name": "", + "name": "totalBdv", "type": "uint256" } ], - "stateMutability": "payable", + "stateMutability": "view", "type": "function" }, { @@ -11165,68 +11194,6 @@ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "components": [ - { - "components": [ - { - "internalType": "address", - "name": "orderer", - "type": "address" - }, - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - }, - { - "internalType": "uint24", - "name": "pricePerPod", - "type": "uint24" - }, - { - "internalType": "uint256", - "name": "maxPlaceInLine", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minFillAmount", - "type": "uint256" - } - ], - "internalType": "struct Order.PodOrder", - "name": "podOrder", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "beanAmount", - "type": "uint256" - } - ], - "internalType": "struct L1RecieverFacet.L1PodOrder[]", - "name": "orders", - "type": "tuple[]" - }, - { - "internalType": "bytes32[]", - "name": "proof", - "type": "bytes32[]" - } - ], - "name": "issuePodOrders", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { @@ -11362,74 +11329,6 @@ "stateMutability": "pure", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "components": [ - { - "components": [ - { - "internalType": "address", - "name": "orderer", - "type": "address" - }, - { - "internalType": "uint256", - "name": "fieldId", - "type": "uint256" - }, - { - "internalType": "uint24", - "name": "pricePerPod", - "type": "uint24" - }, - { - "internalType": "uint256", - "name": "maxPlaceInLine", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "minFillAmount", - "type": "uint256" - } - ], - "internalType": "struct Order.PodOrder", - "name": "podOrder", - "type": "tuple" - }, - { - "internalType": "uint256", - "name": "beanAmount", - "type": "uint256" - } - ], - "internalType": "struct L1RecieverFacet.L1PodOrder[]", - "name": "orders", - "type": "tuple[]" - }, - { - "internalType": "bytes32[]", - "name": "proof", - "type": "bytes32[]" - } - ], - "name": "verifyOrderProof", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "pure", - "type": "function" - }, { "inputs": [ { @@ -12157,35 +12056,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "well", - "type": "address" - }, - { - "internalType": "uint256[]", - "name": "reserves", - "type": "uint256[]" - }, - { - "internalType": "uint256", - "name": "lookback", - "type": "uint256" - } - ], - "name": "calculateDeltaBFromReservesE", - "outputs": [ - { - "internalType": "int256", - "name": "", - "type": "int256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "anonymous": false, "inputs": [ @@ -12630,6 +12500,42 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint32", + "name": "season", + "type": "uint32" + } + ], + "name": "mockSetMilestoneSeason", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "int96", + "name": "stem", + "type": "int96" + } + ], + "name": "mockSetMilestoneStem", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "mockStepGauge", From 065307e7357265ae1f321247213ae28b917ee89d Mon Sep 17 00:00:00 2001 From: Spacebean Date: Thu, 19 Sep 2024 15:10:23 -0600 Subject: [PATCH 03/14] feat: add .env args + well LP token logos --- projects/ui/.env.development | 1 + projects/ui/.env.production | 1 + projects/ui/src/components/App/SdkProvider.tsx | 13 +++++++++---- projects/ui/src/constants/tokens.ts | 12 ++++++++---- projects/ui/src/env.d.ts | 5 +++++ .../ui/src/img/tokens/bean-usdc-well-lp-logo.svg | 11 +++++++++++ .../ui/src/img/tokens/bean-usdt-well-lp-logo.svg | 11 +++++++++++ .../ui/src/img/tokens/bean-wbtc-well-lp-logo.svg | 16 ++++++++++++++++ .../src/img/tokens/bean-weeth-well-lp-logo.svg | 11 +++++++++++ .../lib/PipelineConvert/usePipelineConvert.ts | 1 + 10 files changed, 74 insertions(+), 8 deletions(-) create mode 100644 projects/ui/src/img/tokens/bean-usdc-well-lp-logo.svg create mode 100644 projects/ui/src/img/tokens/bean-usdt-well-lp-logo.svg create mode 100644 projects/ui/src/img/tokens/bean-wbtc-well-lp-logo.svg create mode 100644 projects/ui/src/img/tokens/bean-weeth-well-lp-logo.svg create mode 100644 projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts diff --git a/projects/ui/.env.development b/projects/ui/.env.development index 4cdd4e38f..19b2429db 100644 --- a/projects/ui/.env.development +++ b/projects/ui/.env.development @@ -8,3 +8,4 @@ VITE_ALCHEMY_API_KEY="ds4ljBC_Pq-PaIQ3aHo04t27y2n8qpry" VITE_THEGRAPH_API_KEY="4c0b9364a121c1f2aa96fe61cb73d705" VITE_WALLETCONNECT_PROJECT_ID=2159ea7542f2b547554f8c85eca0cec1 VITE_SNAPSHOT_API_KEY="83b2ba4f5e943503dad56d4afea4a5205ace935d702cb8c0a1151c995b474f59" +VITE_ZERO_X_API_KEY="" \ No newline at end of file diff --git a/projects/ui/.env.production b/projects/ui/.env.production index 346595d3e..77403a824 100644 --- a/projects/ui/.env.production +++ b/projects/ui/.env.production @@ -6,3 +6,4 @@ VITE_ALCHEMY_API_KEY="iByabvqm_66b_Bkl9M-wJJGdCTuy19R3" VITE_THEGRAPH_API_KEY="4c0b9364a121c1f2aa96fe61cb73d705" VITE_SNAPSHOT_API_KEY="83b2ba4f5e943503dad56d4afea4a5205ace935d702cb8c0a1151c995b474f59" +VITE_ZERO_X_API_KEY="" \ No newline at end of file diff --git a/projects/ui/src/components/App/SdkProvider.tsx b/projects/ui/src/components/App/SdkProvider.tsx index 7b409111b..a63c46d2d 100644 --- a/projects/ui/src/components/App/SdkProvider.tsx +++ b/projects/ui/src/components/App/SdkProvider.tsx @@ -17,6 +17,10 @@ import rinsableSproutLogo from '~/img/beanstalk/rinsable-sprout-icon.svg'; import beanEthLpLogo from '~/img/tokens/bean-eth-lp-logo.svg'; import beanEthWellLpLogo from '~/img/tokens/bean-eth-well-lp-logo.svg'; import beathWstethWellLPLogo from '~/img/tokens/bean-wsteth-logo.svg'; +import beanUsdcWellLpLogo from '~/img/tokens/bean-usdc-well-lp-logo.svg'; +import beanWbtcWellLpLogo from '~/img/tokens/bean-wbtc-well-lp-logo.svg'; +import beanUsdtWellLpLogo from '~/img/tokens/bean-usdt-well-lp-logo.svg'; +import beanWeethWellLpLogo from '~/img/tokens/bean-weeth-well-lp-logo.svg'; // ERC-20 Token Images import crv3Logo from '~/img/tokens/crv3-logo.png'; @@ -64,10 +68,10 @@ const setTokenMetadatas = (sdk: BeanstalkSDK) => { logo: beathWstethWellLPLogo, }); sdk.tokens.UNRIPE_BEAN_WSTETH.setMetadata({ logo: unripeBeanWstethLogoUrl }); - sdk.tokens.BEAN_WEETH_WELL_LP.setMetadata({ logo: beathWstethWellLPLogo }); // TODO: fix me - sdk.tokens.BEAN_WBTC_WELL_LP.setMetadata({ logo: beathWstethWellLPLogo }); // TODO: fix me - sdk.tokens.BEAN_USDC_WELL_LP.setMetadata({ logo: beathWstethWellLPLogo }); // TODO: fix me - sdk.tokens.BEAN_USDT_WELL_LP.setMetadata({ logo: beathWstethWellLPLogo }); // TODO: fix me + sdk.tokens.BEAN_WEETH_WELL_LP.setMetadata({ logo: beanWeethWellLpLogo }); + sdk.tokens.BEAN_WBTC_WELL_LP.setMetadata({ logo: beanWbtcWellLpLogo }); + sdk.tokens.BEAN_USDC_WELL_LP.setMetadata({ logo: beanUsdcWellLpLogo }); + sdk.tokens.BEAN_USDT_WELL_LP.setMetadata({ logo: beanUsdtWellLpLogo }); // ERC-20 tokens sdk.tokens.BEAN.setMetadata({ logo: beanCircleLogo }); @@ -113,6 +117,7 @@ const useBeanstalkSdkContext = () => { signer: signer ?? undefined, source: datasource, DEBUG: IS_DEVELOPMENT_ENV, + zeroXApiKey: import.meta.env.VITE_ZERO_X_API_KEY, ...(subgraphUrl ? { subgraphUrl } : {}), }); diff --git a/projects/ui/src/constants/tokens.ts b/projects/ui/src/constants/tokens.ts index 2a635650c..7b21aec4e 100644 --- a/projects/ui/src/constants/tokens.ts +++ b/projects/ui/src/constants/tokens.ts @@ -17,6 +17,10 @@ import rinsableSproutLogo from '~/img/beanstalk/rinsable-sprout-icon.svg'; import beanEthLpLogoUrl from '~/img/tokens/bean-eth-lp-logo.svg'; import beanEthWellLpLogoUrl from '~/img/tokens/bean-eth-well-lp-logo.svg'; import beanLusdLogoUrl from '~/img/tokens/bean-lusd-logo.svg'; +import beanUsdcWellLpLogo from '~/img/tokens/bean-usdc-well-lp-logo.svg'; +import beanWbtcWellLpLogo from '~/img/tokens/bean-wbtc-well-lp-logo.svg'; +import beanUsdtWellLpLogo from '~/img/tokens/bean-usdt-well-lp-logo.svg'; +import beanWeethWellLpLogo from '~/img/tokens/bean-weeth-well-lp-logo.svg'; // ERC-20 Token Images import wstethLogo from '~/img/tokens/wsteth-logo.svg'; @@ -301,7 +305,7 @@ export const BEAN_WEETH_WELL_LP = makeChainToken( name: 'BEAN:weETH LP', symbol: 'BEANweETH', isLP: true, - logo: beanWstethLogo, // TODO: replace with bean:weeth logo + logo: beanWeethWellLpLogo, isUnripe: false, displayDecimals: 2, }, @@ -316,7 +320,7 @@ export const BEAN_WBTC_WELL_LP = makeChainToken( symbol: 'BEANWBTC', isLP: true, isUnripe: false, - logo: beanWstethLogo, // TODO: replace with bean:weeth logo + logo: beanWbtcWellLpLogo, displayDecimals: 2, }, { ...defaultRewards } @@ -330,7 +334,7 @@ export const BEAN_USDC_WELL_LP = makeChainToken( symbol: 'BEANUSDC', isLP: true, isUnripe: false, - logo: beanWstethLogo, // TODO: replace with bean:weeth logo + logo: beanUsdcWellLpLogo, displayDecimals: 2, }, { ...defaultRewards } @@ -344,7 +348,7 @@ export const BEAN_USDT_WELL_LP = makeChainToken( symbol: 'BEANUSDT', isLP: true, isUnripe: false, - logo: beanWstethLogo, // TODO: replace with bean:weeth logo + logo: beanUsdtWellLpLogo, displayDecimals: 2, }, { ...defaultRewards } diff --git a/projects/ui/src/env.d.ts b/projects/ui/src/env.d.ts index 9e5d4b609..da3005c7e 100644 --- a/projects/ui/src/env.d.ts +++ b/projects/ui/src/env.d.ts @@ -21,6 +21,11 @@ interface ImportMetaEnv { * If set, don't add CSP meta tag */ readonly DISABLE_CSP?: any; + + /** + * API key for used for ZeroX Swap API + */ + readonly VITE_ZERO_X_API_KEY: string; } interface ImportMeta { diff --git a/projects/ui/src/img/tokens/bean-usdc-well-lp-logo.svg b/projects/ui/src/img/tokens/bean-usdc-well-lp-logo.svg new file mode 100644 index 000000000..92b880af5 --- /dev/null +++ b/projects/ui/src/img/tokens/bean-usdc-well-lp-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/projects/ui/src/img/tokens/bean-usdt-well-lp-logo.svg b/projects/ui/src/img/tokens/bean-usdt-well-lp-logo.svg new file mode 100644 index 000000000..9d462dc85 --- /dev/null +++ b/projects/ui/src/img/tokens/bean-usdt-well-lp-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/projects/ui/src/img/tokens/bean-wbtc-well-lp-logo.svg b/projects/ui/src/img/tokens/bean-wbtc-well-lp-logo.svg new file mode 100644 index 000000000..af60ce358 --- /dev/null +++ b/projects/ui/src/img/tokens/bean-wbtc-well-lp-logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/projects/ui/src/img/tokens/bean-weeth-well-lp-logo.svg b/projects/ui/src/img/tokens/bean-weeth-well-lp-logo.svg new file mode 100644 index 000000000..d92b3a690 --- /dev/null +++ b/projects/ui/src/img/tokens/bean-weeth-well-lp-logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts new file mode 100644 index 000000000..badea2283 --- /dev/null +++ b/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts @@ -0,0 +1 @@ +export function usePipelineConvert() {} From 51494258cdebb1d7fda8d4fa649e7c77fda4b583 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Thu, 19 Sep 2024 20:51:53 -0600 Subject: [PATCH 04/14] fix: 0x quote params --- projects/sdk/src/lib/matcha/zeroX.ts | 48 +++++++++++++++++++++------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/projects/sdk/src/lib/matcha/zeroX.ts b/projects/sdk/src/lib/matcha/zeroX.ts index 3a94496eb..2ca78b4fa 100644 --- a/projects/sdk/src/lib/matcha/zeroX.ts +++ b/projects/sdk/src/lib/matcha/zeroX.ts @@ -1,19 +1,28 @@ -import { ZeroExAPIRequestParams, ZeroExQuoteParams, ZeroExQuoteResponse } from "./types"; +import { ZeroExAPIRequestParams, ZeroExQuoteResponse } from "./types"; export class ZeroX { readonly swapV1Endpoint = "http://arbitrum.api.0x.org/swap/v1/quote"; constructor(private _apiKey: string = "") {} + /** + * Exposing here to allow other modules to use their own API key if needed + */ + setApiKey(_apiKey: string) { + this._apiKey = _apiKey; + } + /** * fetch the quote from the 0x API - * - * params + * @notes defaults: * - slippagePercentage: In human readable form. 0.01 = 1%. Defaults to 0.001 (0.1%) * - skipValidation: defaults to true * - shouldSellEntireBalance: defaults to false */ - async fetchQuote(args: ZeroExQuoteParams) { + async fetchSwapQuote( + args: T , + requestInit?: Omit + ): Promise { if (!this._apiKey) { throw new Error("Cannot fetch from 0x without an API key"); } @@ -23,10 +32,11 @@ export class ZeroX { ); const options = { + ...requestInit, method: "GET", headers: new Headers({ "0x-api-key": this._apiKey - }) + }), }; const url = `${this.swapV1Endpoint}?${fetchParams.toString()}`; @@ -34,9 +44,13 @@ export class ZeroX { return fetch(url, options).then((r) => r.json()) as Promise; } - private generateQuoteParams(args: ZeroExQuoteParams): ZeroExAPIRequestParams { - const { enabled, mode, ...params } = args; - + /** + * Generate the params for the 0x API + * @throws if required params are missing + * + * @returns the params for the 0x API + */ + private generateQuoteParams(params: T): ZeroExAPIRequestParams { if (!params.buyToken && !params.sellToken) { throw new Error("buyToken and sellToken and required"); } @@ -45,11 +59,23 @@ export class ZeroX { throw new Error("sellAmount or buyAmount is required"); } + // Return all the params to filter out the ones that are not part of the request return { - ...params, + sellToken: params.sellToken, + buyToken: params.buyToken, + sellAmount: params.sellAmount, + buyAmount: params.buyAmount, slippagePercentage: params.slippagePercentage ?? "0.01", - skipValidation: params.skipValidation ?? true, - shouldSellEntireBalance: params.shouldSellEntireBalance ?? false + gasPrice: params.gasPrice, + takerAddress: params.takerAddress, + excludedSources: params.excludedSources, + includedSources: params.includedSources, + skipValidation: params.skipValidation ?? true, // defaults to true b/c most of our swaps go through advFarm / pipeline calls + feeRecipient: params.feeRecipient, + buyTokenPercentageFee: params.buyTokenPercentageFee, + priceImpactProtectionPercentage: params.priceImpactProtectionPercentage, + feeRecipientTradeSurplus: params.feeRecipientTradeSurplus, + shouldSellEntireBalance: params.shouldSellEntireBalance ?? false, }; } } From 34672c336d9b37ee52c6efdc6c98503bd68a2ff7 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 01:16:39 -0600 Subject: [PATCH 05/14] feat: add base convert implementation --- .../Actions/Convert/DefaultConvertForm.tsx | 504 ++++++++++ .../Actions/Convert/PipelineConvertForm.tsx | 391 ++++++++ .../components/Silo/Actions/Convert/index.tsx | 940 ++++++++++++++++++ .../components/Silo/Actions/Convert/types.ts | 47 + projects/ui/src/hooks/app/useDebounce.ts | 107 ++ 5 files changed, 1989 insertions(+) create mode 100644 projects/ui/src/components/Silo/Actions/Convert/DefaultConvertForm.tsx create mode 100644 projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx create mode 100644 projects/ui/src/components/Silo/Actions/Convert/index.tsx create mode 100644 projects/ui/src/components/Silo/Actions/Convert/types.ts create mode 100644 projects/ui/src/hooks/app/useDebounce.ts diff --git a/projects/ui/src/components/Silo/Actions/Convert/DefaultConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/DefaultConvertForm.tsx new file mode 100644 index 000000000..17eda4aff --- /dev/null +++ b/projects/ui/src/components/Silo/Actions/Convert/DefaultConvertForm.tsx @@ -0,0 +1,504 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Box, Stack, Typography, Tooltip, TextField } from '@mui/material'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import { Form } from 'formik'; +import BigNumber from 'bignumber.js'; +import { Token, ERC20Token, ConvertDetails } from '@beanstalk/sdk'; +import { useSelector } from 'react-redux'; +import { SmartSubmitButton } from '~/components/Common/Form'; +import TxnPreview from '~/components/Common/Form/TxnPreview'; +import TxnSeparator from '~/components/Common/Form/TxnSeparator'; +import PillRow from '~/components/Common/Form/PillRow'; +import { TokenSelectMode } from '~/components/Common/Form/TokenSelectDialog'; +import { displayBN, displayFullBN, MaxBN, MinBN } from '~/util/Tokens'; +import { ZERO_BN } from '~/constants'; +import useToggle from '~/hooks/display/useToggle'; +import { tokenValueToBN, bnToTokenValue, transform } from '~/util'; +import useBDV from '~/hooks/beanstalk/useBDV'; +import TokenIcon from '~/components/Common/TokenIcon'; +import { ActionType } from '~/util/Actions'; +import { FC } from '~/types'; +import TokenSelectDialogNew from '~/components/Common/Form/TokenSelectDialogNew'; +import TokenQuoteProviderWithParams from '~/components/Common/Form/TokenQuoteProviderWithParams'; +import { QuoteHandlerWithParams } from '~/hooks/ledger/useQuoteWithParams'; +import WarningAlert from '~/components/Common/Alert/WarningAlert'; +import TokenOutput from '~/components/Common/Form/TokenOutput'; +import TxnAccordion from '~/components/Common/TxnAccordion'; +import AdditionalTxnsAccordion from '~/components/Common/Form/FormTxn/AdditionalTxnsAccordion'; +import useFarmerFormTxnsActions from '~/hooks/farmer/form-txn/useFarmerFormTxnActions'; +import AddPlantTxnToggle from '~/components/Common/Form/FormTxn/AddPlantTxnToggle'; +import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; +import StatHorizontal from '~/components/Common/StatHorizontal'; +import { BeanstalkPalette, FontSize } from '~/components/App/muiTheme'; +import { AppState } from '~/state'; +import { ConvertQuoteHandlerParams, BaseConvertFormProps } from './types'; + +interface Props extends BaseConvertFormProps { + conversion: ConvertDetails; + handleQuote: QuoteHandlerWithParams; +} + +export const DefaultConvertForm: FC = ({ + tokenList, + siloBalances, + handleQuote, + plantAndDoX, + sdk, + // Formik + values, + isSubmitting, + setFieldValue, + conversion, +}) => { + /// Local state + const [isTokenSelectVisible, showTokenSelect, hideTokenSelect] = useToggle(); + const getBDV = useBDV(); + const [isChopping, setIsChopping] = useState(false); + const [confirmText, setConfirmText] = useState(''); + const [choppingConfirmed, setChoppingConfirmed] = useState(false); + const unripeTokens = useSelector( + (_state) => _state._bean.unripe + ); + + const plantCrate = plantAndDoX?.crate?.bn; + + /// Extract values from form state + const tokenIn = values.tokens[0].token; // converting from token + const amountIn = values.tokens[0].amount; // amount of from token + const tokenOut = values.tokenOut; // converting to token + const amountOut = values.tokens[0].amountOut; // amount of to token + const maxAmountIn = values.maxAmountIn; + const canConvert = maxAmountIn?.gt(0) || false; + + // FIXME: these use old structs instead of SDK + const siloBalance = siloBalances[tokenIn.address]; + const depositedAmount = siloBalance?.deposited.convertibleAmount || ZERO_BN; + + const isQuoting = values.tokens[0].quoting || false; + const slippage = values.settings.slippage; + + const isUsingPlanted = Boolean( + values.farmActions.primary?.includes(FormTxn.PLANT) && + sdk.tokens.BEAN.equals(tokenIn) + ); + + const totalAmountIn = + isUsingPlanted && plantCrate + ? (amountIn || ZERO_BN).plus(plantCrate.amount) + : amountIn; + + /// Derived form state + let isReady = false; + let buttonLoading = false; + let buttonContent = 'Convert'; + let bdvOut: BigNumber; // the BDV received after re-depositing `amountOut` of `tokenOut`. + let bdvIn: BigNumber; // BDV of amountIn. + let depositsBDV: BigNumber; // BDV of the deposited crates. + let deltaBDV: BigNumber | undefined; // the change in BDV during the convert. should always be >= 0. + let deltaStalk; // the change in Stalk during the convert. should always be >= 0. + let deltaSeedsPerBDV; // change in seeds per BDV for this pathway. ex: bean (2 seeds) -> bean:3crv (4 seeds) = +2 seeds. + let deltaSeeds; // the change in seeds during the convert. + + const txnActions = useFarmerFormTxnsActions({ mode: 'plantToggle' }); + + /// Change button state and prepare outputs + if (depositedAmount.eq(0) && (!plantCrate || plantCrate.amount.eq(0))) { + buttonContent = 'Nothing to Convert'; + } else if (values.maxAmountIn === null) { + if (values.tokenOut) { + buttonContent = 'Refreshing convert data...'; + buttonLoading = false; + } else { + buttonContent = 'No output selected'; + buttonLoading = false; + } + } else if (!canConvert) { + // buttonContent = 'Pathway unavailable'; + } else { + buttonContent = isChopping ? 'Chop and Convert' : 'Convert'; + if ( + tokenOut && + (amountOut?.gt(0) || isUsingPlanted) && + totalAmountIn?.gt(0) + ) { + isReady = true; + bdvOut = getBDV(tokenOut).times(amountOut || ZERO_BN); + bdvIn = getBDV(tokenIn).times(totalAmountIn || ZERO_BN); + depositsBDV = transform(conversion.bdv.abs(), 'bnjs'); + deltaBDV = MaxBN(bdvOut.minus(depositsBDV), ZERO_BN); + deltaStalk = MaxBN( + tokenValueToBN(tokenOut.getStalk(bnToTokenValue(tokenOut, deltaBDV))), + ZERO_BN + ); + deltaSeedsPerBDV = tokenOut + .getSeeds() + .sub(tokenValueToBN(tokenIn.getSeeds()).toNumber()); + deltaSeeds = tokenValueToBN( + tokenOut + .getSeeds(bnToTokenValue(tokenOut, bdvOut)) // seeds for depositing this token with new BDV + .sub(bnToTokenValue(tokenOut, conversion.seeds.abs())) + ); // seeds lost when converting + } + } + + useEffect(() => { + if (isChopping) { + if (confirmText.toUpperCase() === 'CHOP MY ASSETS') { + setChoppingConfirmed(true); + } else { + setChoppingConfirmed(false); + } + } else { + setChoppingConfirmed(true); + } + }, [isChopping, confirmText, setChoppingConfirmed]); + + function getBDVTooltip(instantBDV: BigNumber, depositBDV: BigNumber) { + return ( + + + ~{displayFullBN(instantBDV, 2, 2)} + + + ~{displayFullBN(depositBDV, 2, 2)} + + + ); + } + + function showOutputBDV() { + if (isChopping) return bdvOut || ZERO_BN; + return MaxBN(depositsBDV || ZERO_BN, bdvOut || ZERO_BN); + } + + /// When a new output token is selected, reset maxAmountIn. + const handleSelectTokenOut = useCallback( + async (_tokens: Set) => { + const arr = Array.from(_tokens); + if (arr.length !== 1) throw new Error(); + const _tokenOut = arr[0]; + /// only reset if the user clicked a different token + if (tokenOut !== _tokenOut) { + setFieldValue('tokenOut', _tokenOut); + setFieldValue('maxAmountIn', null); + setConfirmText(''); + } + }, + [setFieldValue, tokenOut] + ); + + useEffect(() => { + setConfirmText(''); + }, [amountIn]); + + /// When `tokenIn` or `tokenOut` changes, refresh the + /// max amount that the user can input of `tokenIn`. + /// FIXME: flash when clicking convert tab + useEffect(() => { + (async () => { + if (tokenOut) { + const maxAmount = await ConvertFarmStep.getMaxConvert( + sdk, + tokenIn, + tokenOut + ); + const _maxAmountIn = tokenValueToBN(maxAmount); + setFieldValue('maxAmountIn', _maxAmountIn); + + const _maxAmountInStr = tokenIn.amount(_maxAmountIn.toString()); + console.debug('[Convert][maxAmountIn]: ', _maxAmountInStr); + + // Figure out if we're chopping + const chopping = + (tokenIn.address === sdk.tokens.UNRIPE_BEAN.address && + tokenOut?.address === sdk.tokens.BEAN.address) || + (tokenIn.address === sdk.tokens.UNRIPE_BEAN_WSTETH.address && + tokenOut?.address === sdk.tokens.BEAN_WSTETH_WELL_LP.address); + + setIsChopping(chopping); + } + })(); + }, [sdk, setFieldValue, tokenIn, tokenOut]); + + const quoteHandlerParams = useMemo( + () => ({ + slippage: slippage, + isConvertingPlanted: isUsingPlanted, + }), + [slippage, isUsingPlanted] + ); + const maxAmountUsed = + totalAmountIn && maxAmountIn ? totalAmountIn.div(maxAmountIn) : null; + + const disabledFormActions = useMemo( + () => (tokenIn.isUnripe ? [FormTxn.ENROOT] : undefined), + [tokenIn.isUnripe] + ); + + const getConvertWarning = () => { + let pool = tokenIn.isLP ? tokenIn.symbol : tokenOut!.symbol; + if (tokenOut && !tokenOut.equals(sdk.tokens.BEAN_CRV3_LP)) { + pool += ' Well'; + } else { + pool += ' pool'; + } + if (['urBEANETH', 'urBEAN'].includes(tokenIn.symbol)) pool = 'BEANETH Well'; + + const lowerOrGreater = + tokenIn.isLP || tokenIn.symbol === 'urBEANETH' ? 'less' : 'greater'; + + const message = `${tokenIn.symbol} can only be Converted to ${tokenOut?.symbol} when deltaB in the ${pool} is ${lowerOrGreater} than 0.`; + + return message; + }; + + const chopPercent = unripeTokens[tokenIn?.address || 0]?.chopPenalty || 0; + + return ( +
+ + + {/* User Input: token amount */} + + _amountOut && + deltaBDV && ( + + + + ~{displayFullBN(depositsBDV, 2)} BDV + + + + + ) + } + tokenSelectLabel={tokenIn.symbol} + disabled={ + !values.maxAmountIn || // still loading `maxAmountIn` + values.maxAmountIn.eq(0) // = 0 means we can't make this conversion + } + params={quoteHandlerParams} + /> + {!canConvert && tokenOut && maxAmountIn ? null : ( + + )} + {/* User Input: destination token */} + {depositedAmount.gt(0) ? ( + + {tokenOut ? : null} + {tokenOut?.symbol || 'Select token'} + + ) : null} + + {/* Warning Alert */} + {!canConvert && tokenOut && maxAmountIn && depositedAmount.gt(0) ? ( + + + {getConvertWarning()} +
+
+
+ ) : null} + {/* Outputs */} + {totalAmountIn && + tokenOut && + maxAmountIn && + (amountOut?.gt(0) || isUsingPlanted) ? ( + <> + + + {isChopping && ( + + You will forfeit {displayBN(chopPercent)}% your claim to + future Ripe assets through this transaction +
+
+ )} + + + Converting will increase the BDV of your Deposit by{' '} + {displayFullBN(deltaBDV || ZERO_BN, 6)} + {deltaBDV?.gt(0) ? ', resulting in a gain of Stalk' : ''}. + + ) : ( + <> + The BDV of your Deposit won't change with this + Convert. + + ) + } + /> + + Converting from {tokenIn.symbol} to {tokenOut.symbol}{' '} + results in{' '} + {!deltaSeedsPerBDV || deltaSeedsPerBDV.eq(0) + ? 'no change in SEEDS per BDV' + : `a ${ + deltaSeedsPerBDV.gt(0) ? 'gain' : 'loss' + } of ${deltaSeedsPerBDV.abs().toHuman()} Seeds per BDV`} + . + + } + /> +
+ + {/* Warnings */} + {maxAmountUsed && maxAmountUsed.gt(0.9) ? ( + + + You are converting{' '} + {displayFullBN(maxAmountUsed.times(100), 4, 0)}% of the way to + the peg. When Converting all the way to the peg, the Convert + may fail due to a small amount of slippage in the direction of + the peg. + + + ) : null} + + {/* Add-on transactions */} + {!isUsingPlanted && ( + + )} + + {/* Transation preview */} + + + + + + + ) : null} + + {isReady && isChopping && ( + + + This conversion will effectively perform a CHOP opperation. Please + confirm you understand this by typing{' '} + "CHOP MY ASSETS"below. + + setConfirmText(e.target.value)} + sx={{ + background: '#f5d1d1', + borderRadius: '10px', + border: '1px solid red', + input: { color: '#880202', textTransform: 'uppercase' }, + }} + /> + + )} + + {/* Submit */} + + {buttonContent} + +
+ + ); +}; diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx new file mode 100644 index 000000000..86ed142c3 --- /dev/null +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -0,0 +1,391 @@ +import React, { useEffect } from 'react'; + +import { Form } from 'formik'; +import BigNumber from 'bignumber.js'; +import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { + Box, + CircularProgress, + Stack, + Tooltip, + Typography, +} from '@mui/material'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; + +import { BasinWell, Token, TokenSiloBalance, TokenValue } from '@beanstalk/sdk'; + +import { ZERO_BN } from '~/constants'; +import useDebounce from '~/hooks/app/useDebounce'; +import useBDV from '~/hooks/beanstalk/useBDV'; +import useToggle from '~/hooks/display/useToggle'; +import TokenSelectDialog, { + TokenSelectMode, +} from '~/components/Common/Form/TokenSelectDialogNew'; +import { + TokenAdornment, + TokenInputField, + TxnPreview, +} from '~/components/Common/Form'; +import Row from '~/components/Common/Row'; +import PillRow from '~/components/Common/Form/PillRow'; +import TokenIcon from '~/components/Common/TokenIcon'; +import WarningAlert from '~/components/Common/Alert/WarningAlert'; +import TxnAccordion from '~/components/Common/TxnAccordion'; +import StatHorizontal from '~/components/Common/StatHorizontal'; + +import { ActionType, displayFullBN } from '~/util'; +import { useAccount } from 'wagmi'; +import { BaseConvertFormProps } from './types'; + +interface Props extends BaseConvertFormProps { + farmerBalances: TokenSiloBalance | undefined; +} + +const defaultWellLpOut = [TokenValue.ZERO, TokenValue.ZERO]; + +const baseQueryOptions = { + refetchOnWindowFocus: true, + staleTime: 20_000, // 20 seconds stale time + refetchIntervalInBackground: false, +}; + +// prettier-ignore +const queryKeys = { + wellLPOut: (sourceWell: BasinWell,targetWell: BasinWell,amountIn: BigNumber) => [ + ['pipe-convert'], ['source-lp-out'], sourceWell?.address || 'no-source-well', targetWell?.address || 'no-target-well', amountIn.toString(), + ], + swapOut: (sellToken: Token, buyToken: Token, amountIn: TokenValue, slippage: number) => [ + ['pipe-convert'], ['swap-out'], sellToken?.address || 'no-sell-token', buyToken?.address || 'no-buy-token', amountIn.toHuman(), slippage, + ], + addLiquidity: (tokensIn: Token[], beanIn: TokenValue | undefined, nonBeanIn: TokenValue | undefined + ) => [ + ['pipe-convert'], + 'add-liquidity', + ...tokensIn.map((t) => t.address), + beanIn?.blockchainString || '0', + nonBeanIn?.blockchainString || '0', + ], +}; + +interface PipelineConvertFormProps extends Props { + sourceWell: BasinWell; + targetWell: BasinWell; +} + +const PipelineConvertFormInner = ({ + sourceWell, + targetWell, + tokenList, + siloBalances, + farmerBalances: balance, // sdk type + // handleQuote, + sdk, + // Formik + values, + isSubmitting, + setFieldValue, +}: PipelineConvertFormProps) => { + const [tokenSelectOpen, showTokenSelect, hideTokenSelect] = useToggle(); + const account = useAccount(); + const getBDV = useBDV(); + + const sourceToken = sourceWell.lpToken; // LP token of source well + const targetToken = targetWell.lpToken; // LP token of target well + const BEAN = sdk.tokens.BEAN; + + // Form values + const amountIn = values.tokens[0].amount; // amount of from token + + const maxConvertable = ( + balance?.convertibleAmount || TokenValue.ZERO + ).toHuman(); + + const maxConvertableBN = new BigNumber(maxConvertable); + + // const amountOut = values.tokens[0].amountOut; // amount of to token + // const maxAmountIn = values.maxAmountIn; + // const canConvert = maxAmountIn?.gt(0) || false; + // const plantCrate = plantAndDoX?.crate?.bn; + + const debouncedAmountIn = useDebounce(amountIn ?? ZERO_BN); + + const sourceIndexes = getWellTokenIndexes(sourceWell, BEAN); + const targetIndexes = getWellTokenIndexes(targetWell, BEAN); + + const sellToken = sourceWell.tokens[sourceIndexes.nonBeanIndex]; // token we will sell when after removing liquidity in equal parts + const buyToken = targetWell.tokens[targetIndexes.nonBeanIndex]; // token we will buy to add liquidity + + const slippage = values.settings.slippage; + + const fetchEnabled = account.address && maxConvertableBN.gt(0); + + const { data: removeOut, ...removeOutQuery } = useQuery({ + queryKey: queryKeys.wellLPOut(sourceWell, targetWell, debouncedAmountIn), + queryFn: async () => { + const outAmount = await sourceWell.getRemoveLiquidityOutEqual( + sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()) + ); + + console.log(`[pipelineConvert/removeOutQuery (1)]: amountOut: `, { + BEAN: outAmount[sourceIndexes.beanIndex].toNumber(), + [`${sellToken.symbol}`]: + outAmount[sourceIndexes.nonBeanIndex].toNumber(), + }); + return outAmount; + }, + enabled: fetchEnabled && debouncedAmountIn?.gt(0) && amountIn?.gt(0), + initialData: defaultWellLpOut, + ...baseQueryOptions, + }); + + const beanAmountOut = removeOut[sourceIndexes.beanIndex]; + const swapAmountIn = removeOut[sourceIndexes.nonBeanIndex]; + + // prettier-ignore + const { data: swapQuote, ...swapOutQuery } = useQuery({ + queryKey: queryKeys.swapOut(sellToken, buyToken, swapAmountIn, slippage), + queryFn: async () => { + const quote = await sdk.zeroX.fetchSwapQuote({ + // 0x requests are formatted such that 0.01 = 1%. Everywhere else in the UI we use 0.01 = 0.01% ?? BS3TODO: VALIDATE ME + slippagePercentage: (slippage / 10).toString(), + sellToken: sellToken.address, + buyToken: buyToken.address, + sellAmount: swapAmountIn.blockchainString, + }); + console.log( + `[pipelineConvert/swapOutQuery (2)]: buyAmount: ${quote?.buyAmount}` + ); + return quote; + }, + retryDelay: 500, // We get 10 requests per second from 0x, so wait 500ms before trying again. + enabled: fetchEnabled && swapAmountIn?.gt(0) && getNextChainedQueryEnabled(removeOutQuery), + ...baseQueryOptions, + }); + + const buyAmount = buyToken.fromBlockchain(swapQuote?.buyAmount || '0'); + const addLiqTokens = targetWell.tokens; + + const { data: targetAmountOut, ...addLiquidityQuery } = useQuery({ + queryKey: queryKeys.addLiquidity(addLiqTokens, beanAmountOut, buyAmount), + queryFn: async () => { + const outAmount = await targetWell.getAddLiquidityOut([ + beanAmountOut, + buyAmount, + ]); + + setFieldValue('tokens.0.amountOut', new BigNumber(outAmount.toHuman())); + console.log( + `[pipelineConvert/addLiquidityQuery (3)]: amountOut: ${outAmount.toNumber()}` + ); + return outAmount; + }, + enabled: + fetchEnabled && + buyAmount.gt(0) && + getNextChainedQueryEnabled(swapOutQuery), + ...baseQueryOptions, + }); + + /// When a new output token is selected, reset maxAmountIn. + const handleSelectTokenOut = async (_selectedTokens: Set) => { + const selected = [..._selectedTokens]?.[0]; + + if (!selected || _selectedTokens.size !== 1) { + throw new Error(); + } + + /// only reset if the user clicked a different token + if (targetToken !== selected) { + setFieldValue('tokenOut', selected); + setFieldValue('maxAmountIn', null); + } + }; + + // prettier-ignore + const isLoading = removeOutQuery.isLoading || swapOutQuery.isLoading || addLiquidityQuery.isLoading; + useEffect(() => { + setFieldValue('tokens.0.quoting', isLoading); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + return ( + <> +
+ + + + ), + }} + quote={ + + + + ~ x instant BDV + {/* ~{displayFullBN(instantBDV, 2, 2)} */} + + + ~ x deposited BDV + {/* ~{displayFullBN(depositBDV, 2, 2)} */} + + + } + placement="top" + > + + + {/* ~{displayFullBN(depositsBDV, 2)} BDV */}x BDV + + + + + {/* {displayQuote(state.amountOut, tokenOut)} */} + {isLoading && ( + + )} + + } + /> + {maxConvertableBN.gt(0) ? ( + + {targetToken ? : null} + {targetToken?.symbol || 'Select token'} + + ) : null} + {/* You may Lose Grown Stalk warning here */} + + + You may lose Grown Stalk through this transaction. + + + {amountIn?.gt(0) && targetAmountOut?.gt(0) && ( + + + + + + )} + + + + ); +}; + +export const PipelineConvertForm = ({ values, sdk, ...restProps }: Props) => { + const sourceToken = values.tokens[0].token; + const targetToken = values.tokenOut; + + // No need to memoize wells since they their object references don't change + const sourceWell = sourceToken && sdk.pools.getWellByLPToken(sourceToken); + const targetWell = targetToken && sdk.pools.getWellByLPToken(targetToken); + + if (!sourceWell || !targetWell) return null; + + return ( + + ); +}; + +// ------------------------------------------ +// Utils +// ------------------------------------------ + +function getWellTokenIndexes(well: BasinWell | undefined, bean: Token) { + const beanIndex = well?.tokens?.[0].equals(bean) ? 0 : 1; + const nonBeanIndex = beanIndex === 0 ? 1 : 0; + + return { + beanIndex, + nonBeanIndex, + } as const; +} + +/** + * We want to limit the next chained query to only run when the previous query is successful & has no errors. + * Additionally, we don't want the next query start if the previous query is either loading or fetching. + */ +function getNextChainedQueryEnabled(query: Omit) { + return ( + query.isSuccess && !query.isLoading && !query.isFetching && !query.isError + ); +} +// const swapAmountIn = removeOutQuery.data?.[sourceWellNonBeanIndex]; + +// const swapOutQuery = useQuery({ +// queryKey: queryKeys.swapOut(swapTokenIn, swapTokenOut, swapAmountIn), +// queryFn: ({ signal }) => { +// if (!swapTokenIn || !swapTokenOut || !swapAmountIn) return TokenValue.ZERO; +// const controller = new AbortController(); +// signal.addEventListener('abort', () => controller.abort()); + +// const params = sdk.zeroX.fetchQuote({ +// slippagePercentage: values.settings.slippage.toString(), +// buyToken: swapTokenIn.address, +// sellToken: swapTokenOut.address, +// sellAmount: swapAmountIn.blockchainString, +// mode: "" +// }) +// }, +// enabled: !!swapTokenIn && !!swapTokenOut && swapAmountIn?.gt(0), +// initialData: TokenValue.ZERO, +// }); diff --git a/projects/ui/src/components/Silo/Actions/Convert/index.tsx b/projects/ui/src/components/Silo/Actions/Convert/index.tsx new file mode 100644 index 000000000..5837e4f35 --- /dev/null +++ b/projects/ui/src/components/Silo/Actions/Convert/index.tsx @@ -0,0 +1,940 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { Formik, FormikHelpers } from 'formik'; +import BigNumber from 'bignumber.js'; +import { + Token, + ERC20Token, + BeanstalkSDK, + TokenValue, + ConvertDetails, + FarmToMode, + FarmFromMode, + TokenSiloBalance, +} from '@beanstalk/sdk'; +import { SettingInput, TxnSettings } from '~/components/Common/Form'; +import { tokenValueToBN } from '~/util'; +import { FarmerSilo } from '~/state/farmer/silo'; +import useSeason from '~/hooks/beanstalk/useSeason'; +import TransactionToast from '~/components/Common/TxnToast'; +import { useFetchPools } from '~/state/bean/pools/updater'; +import useFarmerSilo from '~/hooks/farmer/useFarmerSilo'; +import useFormMiddleware from '~/hooks/ledger/useFormMiddleware'; +import useSdk from '~/hooks/sdk'; +import { QuoteHandlerWithParams } from '~/hooks/ledger/useQuoteWithParams'; +import useAccount from '~/hooks/ledger/useAccount'; +import FormTxnProvider from '~/components/Common/Form/FormTxnProvider'; +import useFormTxnContext from '~/hooks/sdk/useFormTxnContext'; +import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; +import { useWhitelistedTokens } from '~/hooks/beanstalk/useTokens'; +import { useFetchFarmerSilo } from '~/state/farmer/silo/updater'; +import { DefaultConvertForm } from './DefaultConvertForm'; +import { PipelineConvertForm } from './PipelineConvertForm'; +import { + BaseConvertFormProps, + ConvertFormSubmitHandler, + ConvertFormValues, + ConvertProps, + ConvertQuoteHandlerParams, +} from './types'; + +interface Props extends BaseConvertFormProps { + farmerBalances: TokenSiloBalance | undefined; +} + +// ---------- Regular Convert via Beanstalk.convert() ---------- +const DefaultConvertFormWrapper = (props: Props) => { + const { sdk, plantAndDoX, farmerBalances, currentSeason } = props; + + const [conversion, setConversion] = useState({ + actions: [], + amount: TokenValue.ZERO, + bdv: TokenValue.ZERO, + crates: [], + seeds: TokenValue.ZERO, + stalk: TokenValue.ZERO, + }); + + /// Handlers + // This handler does not run when _tokenIn = _tokenOut (direct deposit) + const handleQuote = useCallback< + QuoteHandlerWithParams + >( + async (tokenIn, _amountIn, tokenOut, { slippage, isConvertingPlanted }) => { + try { + if (!farmerBalances?.convertibleDeposits) { + throw new Error('No balances found'); + } + const { plantAction } = plantAndDoX; + + const includePlant = !!(isConvertingPlanted && plantAction); + + const result = await ConvertFarmStep._handleConversion( + sdk, + farmerBalances.convertibleDeposits, + tokenIn, + tokenOut, + tokenIn.amount(_amountIn.toString() || '0'), + currentSeason.toNumber(), + slippage, + includePlant ? plantAction : undefined + ); + + setConversion(result.conversion); + + return tokenValueToBN(result.minAmountOut); + } catch (e) { + console.debug('[Convert/handleQuote]: FAILED: ', e); + return new BigNumber('0'); + } + }, + [farmerBalances?.convertibleDeposits, sdk, currentSeason, plantAndDoX] + ); + + return ( + + ); +}; + +const PipelineConvertFormWrapper = (props: Props) => { + const [conversion, setConversion] = useState({ + // pull this to the parent? + actions: [], + amount: TokenValue.ZERO, + bdv: TokenValue.ZERO, + crates: [], + seeds: TokenValue.ZERO, + stalk: TokenValue.ZERO, + }); + + return ; +}; + +// ---------- Convert Form Router ---------- +/** + * Depending on whether the conversion requires a pipeline convert, + * return the appropriate convert form. + */ +const ConvertFormRouter = (props: Props) => { + const tokenOut = props.values.tokenOut as ERC20Token; + + if (!tokenOut) return null; + + if (isPipelineConvert(props.fromToken, tokenOut)) { + return ; + } + + return ; +}; + +// ---------- Convert Forms Wrapper ---------- +// Acts as a Prop provider for the Convert forms +const ConvertFormsWrapper = ({ fromToken }: ConvertProps) => { + const sdk = useSdk(); + const farmerSilo = useFarmerSilo(); + const season = useSeason(); + + /// Form + const middleware = useFormMiddleware(); + const formTxnContext = useFormTxnContext(); + + /// Token List + const [tokenList, initialTokenOut] = useConvertTokenList(fromToken); + + const initialValues: ConvertFormValues = useMemo( + () => ({ + // Settings + settings: { + slippage: 0.05, + }, + // Token Inputs + tokens: [ + { + token: fromToken, + amount: undefined, + quoting: false, + amountOut: undefined, + }, + ], + // Convert data + maxAmountIn: undefined, + // Token Outputs + tokenOut: initialTokenOut, + farmActions: { + preset: fromToken.isLP || fromToken.isUnripe ? 'noPrimary' : 'plant', + primary: undefined, + secondary: undefined, + implied: [FormTxn.MOW], + }, + }), + [fromToken, initialTokenOut] + ); + + const farmerBalances = farmerSilo.balancesSdk.get(fromToken); + + const defaultSubmitHandler = useDefaultConvertSubmitHandler({ + sdk, + farmerSilo, + middleware, + initialValues, + formTxnContext, + season, + fromToken, + }); + + const submitHandler: ConvertFormSubmitHandler = useCallback( + async (values, formActions) => { + const tokenOut = values.tokenOut; + if (!tokenOut) { + throw new Error('no token out set'); + } + + if (isPipelineConvert(fromToken, tokenOut as ERC20Token)) { + return defaultSubmitHandler(values, formActions); + } + }, + [defaultSubmitHandler, fromToken] + ); + + return ( + + {(formikProps) => ( + <> + + + + + + )} + + ); +}; + +const Convert = (props: ConvertProps) => ( + + + +); + +export default Convert; + +// ----------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------- + +function useDefaultConvertSubmitHandler({ + sdk, + farmerSilo, + middleware, + formTxnContext, + initialValues, + season, + fromToken, +}: { + sdk: BeanstalkSDK; + farmerSilo: FarmerSilo; + middleware: ReturnType; + formTxnContext: ReturnType; + initialValues: ConvertFormValues; + season: BigNumber; + fromToken: ERC20Token; +}) { + const { txnBundler, plantAndDoX, refetch } = formTxnContext; + const account = useAccount(); + const [refetchFarmerSilo] = useFetchFarmerSilo(); + const [refetchPools] = useFetchPools(); + + return useCallback( + async ( + values: ConvertFormValues, + formActions: FormikHelpers + ) => { + const farmerBalances = farmerSilo.balancesSdk.get(fromToken); + let txToast; + try { + middleware.before(); + + /// FormData + const slippage = values?.settings?.slippage; + const tokenIn = values.tokens[0].token; + const tokenOut = values.tokenOut; + const _amountIn = values.tokens[0].amount; + + /// Validation + if (!account) throw new Error('Wallet connection required'); + if (!slippage) throw new Error('No slippage value set.'); + if (!tokenOut) throw new Error('Conversion pathway not set'); + if (!farmerBalances) throw new Error('No balances found'); + + txToast = new TransactionToast({ + loading: 'Converting...', + success: 'Convert successful.', + }); + + let txn; + + const { plantAction } = plantAndDoX; + + const amountIn = tokenIn.amount(_amountIn?.toString() || '0'); // amount of from token + const isPlanting = + plantAndDoX && values.farmActions.primary?.includes(FormTxn.PLANT); + + const convertTxn = new ConvertFarmStep( + sdk, + tokenIn, + tokenOut, + season.toNumber(), + farmerBalances.convertibleDeposits + ); + + const { getEncoded, minAmountOut } = await convertTxn.handleConversion( + amountIn, + slippage, + isPlanting ? plantAction : undefined + ); + + convertTxn.build(getEncoded, minAmountOut); + const actionsPerformed = txnBundler.setFarmSteps(values.farmActions); + + if (!isPlanting) { + const { execute } = await txnBundler.bundle( + convertTxn, + amountIn, + slippage, + 1.2 + ); + + txn = await execute(); + } else { + // Create Advanced Farm operation for alt-route Converts + const farm = sdk.farm.createAdvancedFarm('Alternative Convert'); + + // Get Earned Beans data + const stemTips = await sdk.silo.getStemTip(tokenIn); + const earnedBeans = await sdk.silo.getEarnedBeans(account); + const earnedStem = stemTips.toString(); + const earnedAmount = earnedBeans.toBlockchain(); + + // Plant + farm.add(new sdk.farm.actions.Plant()); + + // Withdraw Planted deposit crate + farm.add( + new sdk.farm.actions.WithdrawDeposit( + tokenIn.address, + earnedStem, + earnedAmount, + FarmToMode.INTERNAL + ) + ); + + // Transfer to Well + farm.add( + new sdk.farm.actions.TransferToken( + tokenIn.address, + sdk.pools.BEAN_ETH_WELL.address, + FarmFromMode.INTERNAL, + FarmToMode.EXTERNAL + ) + ); + + // Create Pipeline operation + const pipe = sdk.farm.createAdvancedPipe('pipelineDeposit'); + + // (Pipeline) - Call sync on Well + pipe.add( + new sdk.farm.actions.WellSync( + sdk.pools.BEAN_ETH_WELL, + tokenIn, + sdk.contracts.pipeline.address + ), + { tag: 'amountToDeposit' } + ); + + // (Pipeline) - Approve transfer of sync output + const approveClipboard = { + tag: 'amountToDeposit', + copySlot: 0, + pasteSlot: 1, + }; + pipe.add( + new sdk.farm.actions.ApproveERC20( + sdk.pools.BEAN_ETH_WELL.lpToken, + sdk.contracts.beanstalk.address, + approveClipboard + ) + ); + + // (Pipeline) - Transfer sync output to Beanstalk + const transferClipboard = { + tag: 'amountToDeposit', + copySlot: 0, + pasteSlot: 2, + }; + pipe.add( + new sdk.farm.actions.TransferToken( + sdk.tokens.BEAN_ETH_WELL_LP.address, + account, + FarmFromMode.EXTERNAL, + FarmToMode.INTERNAL, + transferClipboard + ) + ); + + // Add Pipeline operation to the Advanced Pipe operation + farm.add(pipe); + + // Deposit Advanced Pipe output to Silo + farm.add( + new sdk.farm.actions.Deposit( + sdk.tokens.BEAN_ETH_WELL_LP, + FarmFromMode.INTERNAL + ) + ); + + // Convert the other Deposits as usual + if (amountIn.gt(0)) { + const convertData = sdk.silo.siloConvert.calculateConvert( + tokenIn, + tokenOut, + amountIn, + farmerBalances.convertibleDeposits, + season.toNumber() + ); + const amountOut = await sdk.contracts.beanstalk.getAmountOut( + tokenIn.address, + tokenOut.address, + convertData.amount.toBlockchain() + ); + const _minAmountOut = TokenValue.fromBlockchain( + amountOut.toString(), + tokenOut.decimals + ).mul(1 - slippage); + farm.add( + new sdk.farm.actions.Convert( + sdk.tokens.BEAN, + sdk.tokens.BEAN_ETH_WELL_LP, + amountIn, + _minAmountOut, + convertData.crates + ) + ); + } + + // Mow Grown Stalk + const tokensWithStalk: Map = new Map(); + farmerSilo.stalk.grownByToken.forEach((value, token) => { + if (value.gt(0)) { + tokensWithStalk.set(token, value); + } + }); + if (tokensWithStalk.size > 0) { + farm.add(new sdk.farm.actions.Mow(account, tokensWithStalk)); + } + + const gasEstimate = await farm.estimateGas(earnedBeans, { + slippage: slippage, + }); + const adjustedGas = Math.round( + gasEstimate.toNumber() * 1.2 + ).toString(); + txn = await farm.execute( + earnedBeans, + { slippage: slippage }, + { gasLimit: adjustedGas } + ); + } + + txToast.confirming(txn); + + const receipt = await txn.wait(); + + await refetch(actionsPerformed, { farmerSilo: true }, [ + refetchPools, // update prices to account for pool conversion + refetchFarmerSilo, + ]); + + txToast.success(receipt); + + /// Reset the max Amount In + const _maxAmountIn = await ConvertFarmStep.getMaxConvert( + sdk, + tokenIn, + tokenOut + ); + + formActions.resetForm({ + values: { + ...initialValues, + maxAmountIn: tokenValueToBN(_maxAmountIn), + }, + }); + } catch (err) { + console.error(err); + if (txToast) { + txToast.error(err); + } else { + const errorToast = new TransactionToast({}); + errorToast.error(err); + } + formActions.setSubmitting(false); + } + }, + [ + fromToken, + sdk, + season, + account, + txnBundler, + middleware, + plantAndDoX, + initialValues, + farmerSilo, + refetch, + refetchPools, + refetchFarmerSilo, + ] + ); +} + +function isPipelineConvert( + fromToken: ERC20Token, + toToken: ERC20Token | undefined +) { + if (!toToken) return false; + if (fromToken.isLP && toToken.isLP) { + // Make sure it isn't a lambda convert + return !fromToken.equals(toToken); + } + return false; +} + +function useConvertTokenList( + fromToken: ERC20Token +): [tokenList: ERC20Token[], initialTokenOut: ERC20Token] { + const { whitelist, tokenMap: whitelistLookup } = useWhitelistedTokens(); + const sdk = useSdk(); + return useMemo(() => { + const pathSet = new Set( + sdk.silo.siloConvert.getConversionPaths(fromToken).map((t) => t.address) + ); + + if (!fromToken.isUnripe) { + whitelist.forEach((toToken) => { + !toToken.isUnripe && pathSet.add(toToken.address); + }); + } + + const list = Array.from(pathSet).map((address) => whitelistLookup[address]); + return [ + list, // all available tokens to convert to + list?.[0], // tokenOut is the first available token that isn't the fromToken + ]; + }, [fromToken, sdk, whitelist, whitelistLookup]); +} + +// const ConvertPropProvider: FC<{ +// fromToken: ERC20Token; +// }> = ({ fromToken }) => { +// const sdk = useSdk(); + +// /// Token List +// const [tokenList, initialTokenOut] = useMemo(() => { +// const paths = sdk.silo.siloConvert.getConversionPaths(fromToken); +// const _tokenList = paths.filter((_token) => !_token.equals(fromToken)); +// return [ +// _tokenList, // all available tokens to convert to +// _tokenList?.[0], // tokenOut is the first available token that isn't the fromToken +// ]; +// }, [sdk, fromToken]); + +// /// Beanstalk +// const season = useSeason(); +// const [refetchPools] = useFetchPools(); + +// /// Farmer +// const farmerSilo = useFarmerSilo(); +// const farmerSiloBalances = farmerSilo.balances; +// const account = useAccount(); + +// /// Temporary solution. Remove this when we move the site to use the new sdk types. +// const [farmerBalances, refetchFarmerBalances] = useAsyncMemo(async () => { +// if (!account) return undefined; +// console.debug( +// `[Convert] Fetching silo balances for SILO:${fromToken.symbol}` +// ); +// return sdk.silo.getBalance(fromToken, account, { +// source: DataSource.LEDGER, +// }); +// }, [account, sdk]); + +// /// Form +// const middleware = useFormMiddleware(); +// const { txnBundler, plantAndDoX, refetch } = useFormTxnContext(); +// const [conversion, setConversion] = useState({ +// actions: [], +// amount: TokenValue.ZERO, +// bdv: TokenValue.ZERO, +// crates: [], +// seeds: TokenValue.ZERO, +// stalk: TokenValue.ZERO, +// }); + +// const initialValues: ConvertFormValues = useMemo( +// () => ({ +// // Settings +// settings: { +// slippage: 0.05, +// }, +// // Token Inputs +// tokens: [ +// { +// token: fromToken, +// amount: undefined, +// quoting: false, +// amountOut: undefined, +// }, +// ], +// // Convert data +// maxAmountIn: undefined, +// // Token Outputs +// tokenOut: initialTokenOut, +// farmActions: { +// preset: fromToken.isLP || fromToken.isUnripe ? 'noPrimary' : 'plant', +// primary: undefined, +// secondary: undefined, +// implied: [FormTxn.MOW], +// }, +// }), +// [fromToken, initialTokenOut] +// ); + +// /// Handlers +// // This handler does not run when _tokenIn = _tokenOut (direct deposit) +// const handleQuote = useCallback< +// QuoteHandlerWithParams +// >( +// async (tokenIn, _amountIn, tokenOut, { slippage, isConvertingPlanted }) => { +// try { +// if (!farmerBalances?.convertibleDeposits) { +// throw new Error('No balances found'); +// } +// const { plantAction } = plantAndDoX; + +// const includePlant = !!(isConvertingPlanted && plantAction); + +// const result = await ConvertFarmStep._handleConversion( +// sdk, +// farmerBalances.convertibleDeposits, +// tokenIn, +// tokenOut, +// tokenIn.amount(_amountIn.toString() || '0'), +// season.toNumber(), +// slippage, +// includePlant ? plantAction : undefined +// ); + +// setConversion(result.conversion); + +// return tokenValueToBN(result.minAmountOut); +// } catch (e) { +// console.debug('[Convert/handleQuote]: FAILED: ', e); +// return new BigNumber('0'); +// } +// }, +// [farmerBalances?.convertibleDeposits, sdk, season, plantAndDoX] +// ); + +// const onSubmit = useCallback( +// async ( +// values: ConvertFormValues, +// formActions: FormikHelpers +// ) => { +// let txToast; +// try { +// middleware.before(); + +// /// FormData +// const slippage = values?.settings?.slippage; +// const tokenIn = values.tokens[0].token; +// const tokenOut = values.tokenOut; +// const _amountIn = values.tokens[0].amount; + +// /// Validation +// if (!account) throw new Error('Wallet connection required'); +// if (!slippage) throw new Error('No slippage value set.'); +// if (!tokenOut) throw new Error('Conversion pathway not set'); +// if (!farmerBalances) throw new Error('No balances found'); + +// txToast = new TransactionToast({ +// loading: 'Converting...', +// success: 'Convert successful.', +// }); + +// let txn; + +// const { plantAction } = plantAndDoX; + +// const amountIn = tokenIn.amount(_amountIn?.toString() || '0'); // amount of from token +// const isPlanting = +// plantAndDoX && values.farmActions.primary?.includes(FormTxn.PLANT); + +// const convertTxn = new ConvertFarmStep( +// sdk, +// tokenIn, +// tokenOut, +// season.toNumber(), +// farmerBalances.convertibleDeposits +// ); + +// const { getEncoded, minAmountOut } = await convertTxn.handleConversion( +// amountIn, +// slippage, +// isPlanting ? plantAction : undefined +// ); + +// convertTxn.build(getEncoded, minAmountOut); +// const actionsPerformed = txnBundler.setFarmSteps(values.farmActions); + +// if (!isPlanting) { +// const { execute } = await txnBundler.bundle( +// convertTxn, +// amountIn, +// slippage, +// 1.2 +// ); + +// txn = await execute(); +// } else { +// // Create Advanced Farm operation for alt-route Converts +// const farm = sdk.farm.createAdvancedFarm('Alternative Convert'); + +// // Get Earned Beans data +// const stemTips = await sdk.silo.getStemTip(tokenIn); +// const earnedBeans = await sdk.silo.getEarnedBeans(account); +// const earnedStem = stemTips.toString(); +// const earnedAmount = earnedBeans.toBlockchain(); + +// // Plant +// farm.add(new sdk.farm.actions.Plant()); + +// // Withdraw Planted deposit crate +// farm.add( +// new sdk.farm.actions.WithdrawDeposit( +// tokenIn.address, +// earnedStem, +// earnedAmount, +// FarmToMode.INTERNAL +// ) +// ); + +// // Transfer to Well +// farm.add( +// new sdk.farm.actions.TransferToken( +// tokenIn.address, +// sdk.pools.BEAN_ETH_WELL.address, +// FarmFromMode.INTERNAL, +// FarmToMode.EXTERNAL +// ) +// ); + +// // Create Pipeline operation +// const pipe = sdk.farm.createAdvancedPipe('pipelineDeposit'); + +// // (Pipeline) - Call sync on Well +// pipe.add( +// new sdk.farm.actions.WellSync( +// sdk.pools.BEAN_ETH_WELL, +// tokenIn, +// sdk.contracts.pipeline.address +// ), +// { tag: 'amountToDeposit' } +// ); + +// // (Pipeline) - Approve transfer of sync output +// const approveClipboard = { +// tag: 'amountToDeposit', +// copySlot: 0, +// pasteSlot: 1, +// }; +// pipe.add( +// new sdk.farm.actions.ApproveERC20( +// sdk.pools.BEAN_ETH_WELL.lpToken, +// sdk.contracts.beanstalk.address, +// approveClipboard +// ) +// ); + +// // (Pipeline) - Transfer sync output to Beanstalk +// const transferClipboard = { +// tag: 'amountToDeposit', +// copySlot: 0, +// pasteSlot: 2, +// }; +// pipe.add( +// new sdk.farm.actions.TransferToken( +// sdk.tokens.BEAN_ETH_WELL_LP.address, +// account, +// FarmFromMode.EXTERNAL, +// FarmToMode.INTERNAL, +// transferClipboard +// ) +// ); + +// // Add Pipeline operation to the Advanced Pipe operation +// farm.add(pipe); + +// // Deposit Advanced Pipe output to Silo +// farm.add( +// new sdk.farm.actions.Deposit( +// sdk.tokens.BEAN_ETH_WELL_LP, +// FarmFromMode.INTERNAL +// ) +// ); + +// // Convert the other Deposits as usual +// if (amountIn.gt(0)) { +// const convertData = sdk.silo.siloConvert.calculateConvert( +// tokenIn, +// tokenOut, +// amountIn, +// farmerBalances.convertibleDeposits, +// season.toNumber() +// ); +// const amountOut = await sdk.contracts.beanstalk.getAmountOut( +// tokenIn.address, +// tokenOut.address, +// convertData.amount.toBlockchain() +// ); +// const _minAmountOut = TokenValue.fromBlockchain( +// amountOut.toString(), +// tokenOut.decimals +// ).mul(1 - slippage); +// farm.add( +// new sdk.farm.actions.Convert( +// sdk.tokens.BEAN, +// sdk.tokens.BEAN_ETH_WELL_LP, +// amountIn, +// _minAmountOut, +// convertData.crates +// ) +// ); +// } + +// // Mow Grown Stalk +// const tokensWithStalk: Map = new Map(); +// farmerSilo.stalk.grownByToken.forEach((value, token) => { +// if (value.gt(0)) { +// tokensWithStalk.set(token, value); +// } +// }); +// if (tokensWithStalk.size > 0) { +// farm.add(new sdk.farm.actions.Mow(account, tokensWithStalk)); +// } + +// const gasEstimate = await farm.estimateGas(earnedBeans, { +// slippage: slippage, +// }); +// const adjustedGas = Math.round( +// gasEstimate.toNumber() * 1.2 +// ).toString(); +// txn = await farm.execute( +// earnedBeans, +// { slippage: slippage }, +// { gasLimit: adjustedGas } +// ); +// } + +// txToast.confirming(txn); + +// const receipt = await txn.wait(); + +// await refetch(actionsPerformed, { farmerSilo: true }, [ +// refetchPools, // update prices to account for pool conversion +// refetchFarmerBalances, +// ]); + +// txToast.success(receipt); + +// /// Reset the max Amount In +// const _maxAmountIn = await ConvertFarmStep.getMaxConvert( +// sdk, +// tokenIn, +// tokenOut +// ); + +// formActions.resetForm({ +// values: { +// ...initialValues, +// maxAmountIn: tokenValueToBN(_maxAmountIn), +// }, +// }); +// } catch (err) { +// console.error(err); +// if (txToast) { +// txToast.error(err); +// } else { +// const errorToast = new TransactionToast({}); +// errorToast.error(err); +// } +// formActions.setSubmitting(false); +// } +// }, +// [ +// sdk, +// season, +// account, +// txnBundler, +// middleware, +// plantAndDoX, +// initialValues, +// farmerBalances, +// farmerSilo, +// refetch, +// refetchPools, +// refetchFarmerBalances, +// ] +// ); + +// return ( +// +// {(formikProps) => ( +// <> +// +// +// +// +// +// )} +// +// ); +// }; diff --git a/projects/ui/src/components/Silo/Actions/Convert/types.ts b/projects/ui/src/components/Silo/Actions/Convert/types.ts new file mode 100644 index 000000000..c58779dd5 --- /dev/null +++ b/projects/ui/src/components/Silo/Actions/Convert/types.ts @@ -0,0 +1,47 @@ +import { BeanstalkSDK, ERC20Token, NativeToken, Token } from '@beanstalk/sdk'; +import BigNumber from 'bignumber.js'; +import { FormikHelpers, FormikProps } from 'formik'; +import { FormStateNew, FormTxnsFormState } from '~/components/Common/Form'; +import usePlantAndDoX from '~/hooks/farmer/form-txn/usePlantAndDoX'; +import { FarmerSilo } from '~/state/farmer/silo'; + +// ---------- FORMIK ---------- +export type ConvertFormValues = FormStateNew & { + settings: { + slippage: number; + }; + maxAmountIn: BigNumber | undefined; + tokenOut: Token | undefined; +} & FormTxnsFormState; + +export type ConvertFormSubmitHandler = ( + values: ConvertFormValues, + formActions: FormikHelpers +) => Promise; + +export type ConvertQuoteHandlerParams = { + slippage: number; + isConvertingPlanted: boolean; +}; + +// ---------- COMPONENT ---------- + +type BaseConvertFormikProps = FormikProps; + +// Base Props +export interface ConvertProps { + fromToken: ERC20Token; +} + +export interface BaseConvertFormProps + extends ConvertProps, + BaseConvertFormikProps { + /** List of tokens that can be converted to. */ + tokenList: (ERC20Token | NativeToken)[]; + /** Farmer's silo balances */ + siloBalances: FarmerSilo['balances']; + currentSeason: BigNumber; + /** other */ + sdk: BeanstalkSDK; + plantAndDoX: ReturnType; +} diff --git a/projects/ui/src/hooks/app/useDebounce.ts b/projects/ui/src/hooks/app/useDebounce.ts new file mode 100644 index 000000000..f6d18ef0c --- /dev/null +++ b/projects/ui/src/hooks/app/useDebounce.ts @@ -0,0 +1,107 @@ +import { useState, useEffect, useRef, useMemo } from 'react'; +import { debounce } from 'lodash'; +import { exists } from '~/util'; + +type AddressIsh = { address: string }; +type ToStringIsh = { toString: () => string }; +type ToHumanIsh = { toHuman: () => string }; + +interface DebounceOptions { + leading?: boolean; + trailing?: boolean; + maxWait?: number; + equalityFn?: (left: T, right: T) => boolean; +} + +function defaultEqualityFn(left: T, right: T): boolean { + return left === right; +} +function addressIshEqualityFn(left: unknown, right: unknown): boolean { + return ( + (left as AddressIsh).address.toLowerCase() === + (right as AddressIsh).address.toLowerCase() + ); +} +function toStringIshEqualityFn(left: unknown, right: unknown): boolean { + return (left as ToStringIsh).toString() === (right as ToStringIsh).toString(); +} +function toHumanIshEqualityFn(left: unknown, right: unknown): boolean { + return (left as ToHumanIsh).toHuman() === (right as ToHumanIsh).toHuman(); +} + +const getDefaultEqualityFn = ( + value: T +): ((left: T, right: T) => boolean) => { + if (exists(value)) { + if (typeof value === 'object') { + if ('address' in value) { + return addressIshEqualityFn; + } + if ('toString' in value && typeof value.toString === 'function') { + return toStringIshEqualityFn; + } + if ('toHuman' in value && typeof value.toHuman === 'function') { + return toHumanIshEqualityFn; + } + } + } + return defaultEqualityFn; +}; + +/** + * Debounces a value, updating the debounced value when the input value changes. + * @param value - The value to debounce. + * @param delay - The delay in milliseconds. + * @param options - Options for the debounce function. + * - `leading`: Whether to invoke the debounced function on the leading edge of the timeout. + * - `trailing`: Whether to invoke the debounced function on the trailing edge of the timeout. + * - `maxWait`: The maximum time to wait before invoking the debounced function. + * - `equalityFn`: A function to determine if two values are equal. Important to memoize this. + * @returns The debounced value. + */ +function useDebounce( + value: T, + delay: number = 250, + options: DebounceOptions = {} +): T { + const { + leading = false, + trailing = true, + maxWait, + equalityFn = getDefaultEqualityFn(value), + } = options; + + const [debouncedValue, setDebouncedValue] = useState(value); + const previousValueRef = useRef(value); + + const debouncedFn = useMemo(() => { + const fn = debounce( + (newValue: T) => { + setDebouncedValue(newValue); + }, + delay, + { leading, trailing, maxWait } + ); + + return fn; + }, [delay, leading, trailing, maxWait]); + + useEffect(() => { + if (!equalityFn(previousValueRef.current, value)) { + previousValueRef.current = value; + debouncedFn(value); + + if (leading) { + setDebouncedValue(value); + } + } + + return () => { + debouncedFn.cancel(); + }; + }, [value, debouncedFn, equalityFn, leading]); + + return debouncedValue; +} + +export default useDebounce; From fa004c7e59a0df87e77db19d8fbacc1457e243cf Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 01:17:30 -0600 Subject: [PATCH 06/14] feat: add base convert + update gql schema --- .../src/components/Silo/Actions/Convert.tsx | 960 ------------------ projects/ui/src/graph/graphql.schema.json | 46 +- .../ui/src/graph/schema-snapshot1.graphql | 1 + .../lib/PipelineConvert/usePipelineConvert.ts | 61 +- 4 files changed, 81 insertions(+), 987 deletions(-) delete mode 100644 projects/ui/src/components/Silo/Actions/Convert.tsx diff --git a/projects/ui/src/components/Silo/Actions/Convert.tsx b/projects/ui/src/components/Silo/Actions/Convert.tsx deleted file mode 100644 index 4b82de42f..000000000 --- a/projects/ui/src/components/Silo/Actions/Convert.tsx +++ /dev/null @@ -1,960 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Box, Stack, Typography, Tooltip, TextField } from '@mui/material'; -import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; -import { Form, Formik, FormikHelpers, FormikProps } from 'formik'; -import BigNumber from 'bignumber.js'; -import { - Token, - ERC20Token, - NativeToken, - DataSource, - BeanstalkSDK, - TokenValue, - ConvertDetails, - FarmToMode, - FarmFromMode, -} from '@beanstalk/sdk'; -import { useSelector } from 'react-redux'; -import { - FormStateNew, - FormTxnsFormState, - SettingInput, - SmartSubmitButton, - TxnSettings, -} from '~/components/Common/Form'; -import TxnPreview from '~/components/Common/Form/TxnPreview'; -import TxnSeparator from '~/components/Common/Form/TxnSeparator'; -import PillRow from '~/components/Common/Form/PillRow'; -import { TokenSelectMode } from '~/components/Common/Form/TokenSelectDialog'; -import { displayBN, displayFullBN, MaxBN, MinBN } from '~/util/Tokens'; -import { ZERO_BN } from '~/constants'; -import useToggle from '~/hooks/display/useToggle'; -import { tokenValueToBN, bnToTokenValue, transform } from '~/util'; -import { FarmerSilo } from '~/state/farmer/silo'; -import useSeason from '~/hooks/beanstalk/useSeason'; -import TransactionToast from '~/components/Common/TxnToast'; -import useBDV from '~/hooks/beanstalk/useBDV'; -import TokenIcon from '~/components/Common/TokenIcon'; -import { useFetchPools } from '~/state/bean/pools/updater'; -import { ActionType } from '~/util/Actions'; -import useFarmerSilo from '~/hooks/farmer/useFarmerSilo'; -import { FC } from '~/types'; -import useFormMiddleware from '~/hooks/ledger/useFormMiddleware'; -import TokenSelectDialogNew from '~/components/Common/Form/TokenSelectDialogNew'; -import TokenQuoteProviderWithParams from '~/components/Common/Form/TokenQuoteProviderWithParams'; -import useSdk from '~/hooks/sdk'; -import { QuoteHandlerWithParams } from '~/hooks/ledger/useQuoteWithParams'; -import useAccount from '~/hooks/ledger/useAccount'; -import WarningAlert from '~/components/Common/Alert/WarningAlert'; -import TokenOutput from '~/components/Common/Form/TokenOutput'; -import TxnAccordion from '~/components/Common/TxnAccordion'; -import AdditionalTxnsAccordion from '~/components/Common/Form/FormTxn/AdditionalTxnsAccordion'; -import useFarmerFormTxnsActions from '~/hooks/farmer/form-txn/useFarmerFormTxnActions'; -import useAsyncMemo from '~/hooks/display/useAsyncMemo'; -import AddPlantTxnToggle from '~/components/Common/Form/FormTxn/AddPlantTxnToggle'; -import FormTxnProvider from '~/components/Common/Form/FormTxnProvider'; -import useFormTxnContext from '~/hooks/sdk/useFormTxnContext'; -import { FormTxn, ConvertFarmStep } from '~/lib/Txn'; -import usePlantAndDoX from '~/hooks/farmer/form-txn/usePlantAndDoX'; -import StatHorizontal from '~/components/Common/StatHorizontal'; -import { BeanstalkPalette, FontSize } from '~/components/App/muiTheme'; -import { AppState } from '~/state'; - -// ----------------------------------------------------------------------- - -type ConvertFormValues = FormStateNew & { - settings: { - slippage: number; - }; - maxAmountIn: BigNumber | undefined; - tokenOut: Token | undefined; -} & FormTxnsFormState; - -type ConvertQuoteHandlerParams = { - slippage: number; - isConvertingPlanted: boolean; -}; - -// ----------------------------------------------------------------------- - -const ConvertForm: FC< - FormikProps & { - /** List of tokens that can be converted to. */ - tokenList: (ERC20Token | NativeToken)[]; - /** Farmer's silo balances */ - siloBalances: FarmerSilo['balances']; - handleQuote: QuoteHandlerWithParams; - currentSeason: BigNumber; - /** other */ - sdk: BeanstalkSDK; - conversion: ConvertDetails; - plantAndDoX: ReturnType; - } -> = ({ - tokenList, - siloBalances, - handleQuote, - plantAndDoX, - sdk, - // Formik - values, - isSubmitting, - setFieldValue, - conversion, -}) => { - /// Local state - const [isTokenSelectVisible, showTokenSelect, hideTokenSelect] = useToggle(); - const getBDV = useBDV(); - const [isChopping, setIsChopping] = useState(false); - const [confirmText, setConfirmText] = useState(''); - const [choppingConfirmed, setChoppingConfirmed] = useState(false); - const unripeTokens = useSelector( - (_state) => _state._bean.unripe - ); - - const plantCrate = plantAndDoX?.crate?.bn; - - /// Extract values from form state - const tokenIn = values.tokens[0].token; // converting from token - const amountIn = values.tokens[0].amount; // amount of from token - const tokenOut = values.tokenOut; // converting to token - const amountOut = values.tokens[0].amountOut; // amount of to token - const maxAmountIn = values.maxAmountIn; - const canConvert = maxAmountIn?.gt(0) || false; - - // FIXME: these use old structs instead of SDK - const siloBalance = siloBalances[tokenIn.address]; - const depositedAmount = siloBalance?.deposited.convertibleAmount || ZERO_BN; - - const isQuoting = values.tokens[0].quoting || false; - const slippage = values.settings.slippage; - - const isUsingPlanted = Boolean( - values.farmActions.primary?.includes(FormTxn.PLANT) && - sdk.tokens.BEAN.equals(tokenIn) - ); - - const totalAmountIn = - isUsingPlanted && plantCrate - ? (amountIn || ZERO_BN).plus(plantCrate.amount) - : amountIn; - - /// Derived form state - let isReady = false; - let buttonLoading = false; - let buttonContent = 'Convert'; - let bdvOut: BigNumber; // the BDV received after re-depositing `amountOut` of `tokenOut`. - let bdvIn: BigNumber; // BDV of amountIn. - let depositsBDV: BigNumber; // BDV of the deposited crates. - let deltaBDV: BigNumber | undefined; // the change in BDV during the convert. should always be >= 0. - let deltaStalk; // the change in Stalk during the convert. should always be >= 0. - let deltaSeedsPerBDV; // change in seeds per BDV for this pathway. ex: bean (2 seeds) -> bean:3crv (4 seeds) = +2 seeds. - let deltaSeeds; // the change in seeds during the convert. - - const txnActions = useFarmerFormTxnsActions({ mode: 'plantToggle' }); - - /// Change button state and prepare outputs - if (depositedAmount.eq(0) && (!plantCrate || plantCrate.amount.eq(0))) { - buttonContent = 'Nothing to Convert'; - } else if (values.maxAmountIn === null) { - if (values.tokenOut) { - buttonContent = 'Refreshing convert data...'; - buttonLoading = false; - } else { - buttonContent = 'No output selected'; - buttonLoading = false; - } - } else if (!canConvert) { - // buttonContent = 'Pathway unavailable'; - } else { - buttonContent = isChopping ? 'Chop and Convert' : 'Convert'; - if ( - tokenOut && - (amountOut?.gt(0) || isUsingPlanted) && - totalAmountIn?.gt(0) - ) { - isReady = true; - bdvOut = getBDV(tokenOut).times(amountOut || ZERO_BN); - bdvIn = getBDV(tokenIn).times(totalAmountIn || ZERO_BN); - depositsBDV = transform(conversion.bdv.abs(), 'bnjs'); - deltaBDV = MaxBN(bdvOut.minus(depositsBDV), ZERO_BN); - deltaStalk = MaxBN( - tokenValueToBN(tokenOut.getStalk(bnToTokenValue(tokenOut, deltaBDV))), - ZERO_BN - ); - deltaSeedsPerBDV = tokenOut - .getSeeds() - .sub(tokenValueToBN(tokenIn.getSeeds()).toNumber()); - deltaSeeds = tokenValueToBN( - tokenOut - .getSeeds(bnToTokenValue(tokenOut, bdvOut)) // seeds for depositing this token with new BDV - .sub(bnToTokenValue(tokenOut, conversion.seeds.abs())) - ); // seeds lost when converting - } - } - - useEffect(() => { - if (isChopping) { - if (confirmText.toUpperCase() === 'CHOP MY ASSETS') { - setChoppingConfirmed(true); - } else { - setChoppingConfirmed(false); - } - } else { - setChoppingConfirmed(true); - } - }, [isChopping, confirmText, setChoppingConfirmed]); - - function getBDVTooltip(instantBDV: BigNumber, depositBDV: BigNumber) { - return ( - - - ~{displayFullBN(instantBDV, 2, 2)} - - - ~{displayFullBN(depositBDV, 2, 2)} - - - ); - } - - function showOutputBDV() { - if (isChopping) return bdvOut || ZERO_BN; - return MaxBN(depositsBDV || ZERO_BN, bdvOut || ZERO_BN); - } - - /// When a new output token is selected, reset maxAmountIn. - const handleSelectTokenOut = useCallback( - async (_tokens: Set) => { - const arr = Array.from(_tokens); - if (arr.length !== 1) throw new Error(); - const _tokenOut = arr[0]; - /// only reset if the user clicked a different token - if (tokenOut !== _tokenOut) { - setFieldValue('tokenOut', _tokenOut); - setFieldValue('maxAmountIn', null); - setConfirmText(''); - } - }, - [setFieldValue, tokenOut] - ); - - useEffect(() => { - setConfirmText(''); - }, [amountIn]); - - /// When `tokenIn` or `tokenOut` changes, refresh the - /// max amount that the user can input of `tokenIn`. - /// FIXME: flash when clicking convert tab - useEffect(() => { - (async () => { - if (tokenOut) { - const maxAmount = await ConvertFarmStep.getMaxConvert( - sdk, - tokenIn, - tokenOut - ); - const _maxAmountIn = tokenValueToBN(maxAmount); - setFieldValue('maxAmountIn', _maxAmountIn); - - const _maxAmountInStr = tokenIn.amount(_maxAmountIn.toString()); - console.debug('[Convert][maxAmountIn]: ', _maxAmountInStr); - - // Figure out if we're chopping - const chopping = - (tokenIn.address === sdk.tokens.UNRIPE_BEAN.address && - tokenOut?.address === sdk.tokens.BEAN.address) || - (tokenIn.address === sdk.tokens.UNRIPE_BEAN_WSTETH.address && - tokenOut?.address === sdk.tokens.BEAN_WSTETH_WELL_LP.address); - - setIsChopping(chopping); - } - })(); - }, [sdk, setFieldValue, tokenIn, tokenOut]); - - const quoteHandlerParams = useMemo( - () => ({ - slippage: slippage, - isConvertingPlanted: isUsingPlanted, - }), - [slippage, isUsingPlanted] - ); - const maxAmountUsed = - totalAmountIn && maxAmountIn ? totalAmountIn.div(maxAmountIn) : null; - - const disabledFormActions = useMemo( - () => (tokenIn.isUnripe ? [FormTxn.ENROOT] : undefined), - [tokenIn.isUnripe] - ); - - const getConvertWarning = () => { - let pool = tokenIn.isLP ? tokenIn.symbol : tokenOut!.symbol; - if (tokenOut && !tokenOut.equals(sdk.tokens.BEAN_CRV3_LP)) { - pool += ' Well'; - } else { - pool += ' pool'; - } - if (['urBEANETH', 'urBEAN'].includes(tokenIn.symbol)) pool = 'BEANETH Well'; - - const lowerOrGreater = - tokenIn.isLP || tokenIn.symbol === 'urBEANETH' ? 'less' : 'greater'; - - const message = `${tokenIn.symbol} can only be Converted to ${tokenOut?.symbol} when deltaB in the ${pool} is ${lowerOrGreater} than 0.`; - - return message; - }; - - const chopPercent = unripeTokens[tokenIn?.address || 0]?.chopPenalty || 0; - - return ( -
- - - {/* User Input: token amount */} - - _amountOut && - deltaBDV && ( - - - - ~{displayFullBN(depositsBDV, 2)} BDV - - - - - ) - } - tokenSelectLabel={tokenIn.symbol} - disabled={ - !values.maxAmountIn || // still loading `maxAmountIn` - values.maxAmountIn.eq(0) // = 0 means we can't make this conversion - } - params={quoteHandlerParams} - /> - {!canConvert && tokenOut && maxAmountIn ? null : ( - - )} - {/* User Input: destination token */} - {depositedAmount.gt(0) ? ( - - {tokenOut ? : null} - {tokenOut?.symbol || 'Select token'} - - ) : null} - - {/* Warning Alert */} - {!canConvert && tokenOut && maxAmountIn && depositedAmount.gt(0) ? ( - - - {getConvertWarning()} -
-
-
- ) : null} - {/* Outputs */} - {totalAmountIn && - tokenOut && - maxAmountIn && - (amountOut?.gt(0) || isUsingPlanted) ? ( - <> - - - {isChopping && ( - - You will forfeit {displayBN(chopPercent)}% your claim to - future Ripe assets through this transaction -
-
- )} - - - Converting will increase the BDV of your Deposit by{' '} - {displayFullBN(deltaBDV || ZERO_BN, 6)} - {deltaBDV?.gt(0) ? ', resulting in a gain of Stalk' : ''}. - - ) : ( - <> - The BDV of your Deposit won't change with this - Convert. - - ) - } - /> - - Converting from {tokenIn.symbol} to {tokenOut.symbol}{' '} - results in{' '} - {!deltaSeedsPerBDV || deltaSeedsPerBDV.eq(0) - ? 'no change in SEEDS per BDV' - : `a ${ - deltaSeedsPerBDV.gt(0) ? 'gain' : 'loss' - } of ${deltaSeedsPerBDV.abs().toHuman()} Seeds per BDV`} - . - - } - /> -
- - {/* Warnings */} - {maxAmountUsed && maxAmountUsed.gt(0.9) ? ( - - - You are converting{' '} - {displayFullBN(maxAmountUsed.times(100), 4, 0)}% of the way to - the peg. When Converting all the way to the peg, the Convert - may fail due to a small amount of slippage in the direction of - the peg. - - - ) : null} - - {/* Add-on transactions */} - {!isUsingPlanted && ( - - )} - - {/* Transation preview */} - - - - - - - ) : null} - - {isReady && isChopping && ( - - - This conversion will effectively perform a CHOP opperation. Please - confirm you understand this by typing{' '} - "CHOP MY ASSETS"below. - - setConfirmText(e.target.value)} - sx={{ - background: '#f5d1d1', - borderRadius: '10px', - border: '1px solid red', - input: { color: '#880202', textTransform: 'uppercase' }, - }} - /> - - )} - - {/* Submit */} - - {buttonContent} - -
- - ); -}; - -// ----------------------------------------------------------------------- - -const ConvertPropProvider: FC<{ - fromToken: ERC20Token | NativeToken; -}> = ({ fromToken }) => { - const sdk = useSdk(); - - /// Token List - const [tokenList, initialTokenOut] = useMemo(() => { - // We don't support native token converts - if (fromToken instanceof NativeToken) return [[], undefined]; - const paths = sdk.silo.siloConvert.getConversionPaths(fromToken); - const _tokenList = paths.filter((_token) => !_token.equals(fromToken)); - return [ - _tokenList, // all available tokens to convert to - _tokenList?.[0], // tokenOut is the first available token that isn't the fromToken - ]; - }, [sdk, fromToken]); - - /// Beanstalk - const season = useSeason(); - const [refetchPools] = useFetchPools(); - - /// Farmer - const farmerSilo = useFarmerSilo(); - const farmerSiloBalances = farmerSilo.balances; - const account = useAccount(); - - /// Temporary solution. Remove this when we move the site to use the new sdk types. - const [farmerBalances, refetchFarmerBalances] = useAsyncMemo(async () => { - if (!account) return undefined; - console.debug( - `[Convert] Fetching silo balances for SILO:${fromToken.symbol}` - ); - return sdk.silo.getBalance(fromToken, account, { - source: DataSource.LEDGER, - }); - }, [account, sdk]); - - /// Form - const middleware = useFormMiddleware(); - const { txnBundler, plantAndDoX, refetch } = useFormTxnContext(); - const [conversion, setConversion] = useState({ - actions: [], - amount: TokenValue.ZERO, - bdv: TokenValue.ZERO, - crates: [], - seeds: TokenValue.ZERO, - stalk: TokenValue.ZERO, - }); - - const initialValues: ConvertFormValues = useMemo( - () => ({ - // Settings - settings: { - slippage: 0.05, - }, - // Token Inputs - tokens: [ - { - token: fromToken, - amount: undefined, - quoting: false, - amountOut: undefined, - }, - ], - // Convert data - maxAmountIn: undefined, - // Token Outputs - tokenOut: initialTokenOut, - farmActions: { - preset: fromToken.isLP || fromToken.isUnripe ? 'noPrimary' : 'plant', - primary: undefined, - secondary: undefined, - implied: [FormTxn.MOW], - }, - }), - [fromToken, initialTokenOut] - ); - - /// Handlers - // This handler does not run when _tokenIn = _tokenOut (direct deposit) - const handleQuote = useCallback< - QuoteHandlerWithParams - >( - async (tokenIn, _amountIn, tokenOut, { slippage, isConvertingPlanted }) => { - try { - if (!farmerBalances?.convertibleDeposits) { - throw new Error('No balances found'); - } - const { plantAction } = plantAndDoX; - - const includePlant = !!(isConvertingPlanted && plantAction); - - const result = await ConvertFarmStep._handleConversion( - sdk, - farmerBalances.convertibleDeposits, - tokenIn, - tokenOut, - tokenIn.amount(_amountIn.toString() || '0'), - season.toNumber(), - slippage, - includePlant ? plantAction : undefined - ); - - setConversion(result.conversion); - - return tokenValueToBN(result.minAmountOut); - } catch (e) { - console.debug('[Convert/handleQuote]: FAILED: ', e); - return new BigNumber('0'); - } - }, - [farmerBalances?.convertibleDeposits, sdk, season, plantAndDoX] - ); - - const onSubmit = useCallback( - async ( - values: ConvertFormValues, - formActions: FormikHelpers - ) => { - let txToast; - try { - middleware.before(); - - /// FormData - const slippage = values?.settings?.slippage; - const tokenIn = values.tokens[0].token; - const tokenOut = values.tokenOut; - const _amountIn = values.tokens[0].amount; - - /// Validation - if (!account) throw new Error('Wallet connection required'); - if (!slippage) throw new Error('No slippage value set.'); - if (!tokenOut) throw new Error('Conversion pathway not set'); - if (!farmerBalances) throw new Error('No balances found'); - - txToast = new TransactionToast({ - loading: 'Converting...', - success: 'Convert successful.', - }); - - let txn; - - const { plantAction } = plantAndDoX; - - const amountIn = tokenIn.amount(_amountIn?.toString() || '0'); // amount of from token - const isPlanting = - plantAndDoX && values.farmActions.primary?.includes(FormTxn.PLANT); - - const convertTxn = new ConvertFarmStep( - sdk, - tokenIn, - tokenOut, - season.toNumber(), - farmerBalances.convertibleDeposits - ); - - const { getEncoded, minAmountOut } = await convertTxn.handleConversion( - amountIn, - slippage, - isPlanting ? plantAction : undefined - ); - - convertTxn.build(getEncoded, minAmountOut); - const actionsPerformed = txnBundler.setFarmSteps(values.farmActions); - - if (!isPlanting) { - const { execute } = await txnBundler.bundle( - convertTxn, - amountIn, - slippage, - 1.2 - ); - - txn = await execute(); - } else { - // Create Advanced Farm operation for alt-route Converts - const farm = sdk.farm.createAdvancedFarm('Alternative Convert'); - - // Get Earned Beans data - const stemTips = await sdk.silo.getStemTip(tokenIn); - const earnedBeans = await sdk.silo.getEarnedBeans(account); - const earnedStem = stemTips.toString(); - const earnedAmount = earnedBeans.toBlockchain(); - - // Plant - farm.add(new sdk.farm.actions.Plant()); - - // Withdraw Planted deposit crate - farm.add( - new sdk.farm.actions.WithdrawDeposit( - tokenIn.address, - earnedStem, - earnedAmount, - FarmToMode.INTERNAL - ) - ); - - // Transfer to Well - farm.add( - new sdk.farm.actions.TransferToken( - tokenIn.address, - sdk.pools.BEAN_ETH_WELL.address, - FarmFromMode.INTERNAL, - FarmToMode.EXTERNAL - ) - ); - - // Create Pipeline operation - const pipe = sdk.farm.createAdvancedPipe('pipelineDeposit'); - - // (Pipeline) - Call sync on Well - pipe.add( - new sdk.farm.actions.WellSync( - sdk.pools.BEAN_ETH_WELL, - tokenIn, - sdk.contracts.pipeline.address - ), - { tag: 'amountToDeposit' } - ); - - // (Pipeline) - Approve transfer of sync output - const approveClipboard = { - tag: 'amountToDeposit', - copySlot: 0, - pasteSlot: 1, - }; - pipe.add( - new sdk.farm.actions.ApproveERC20( - sdk.pools.BEAN_ETH_WELL.lpToken, - sdk.contracts.beanstalk.address, - approveClipboard - ) - ); - - // (Pipeline) - Transfer sync output to Beanstalk - const transferClipboard = { - tag: 'amountToDeposit', - copySlot: 0, - pasteSlot: 2, - }; - pipe.add( - new sdk.farm.actions.TransferToken( - sdk.tokens.BEAN_ETH_WELL_LP.address, - account, - FarmFromMode.EXTERNAL, - FarmToMode.INTERNAL, - transferClipboard - ) - ); - - // Add Pipeline operation to the Advanced Pipe operation - farm.add(pipe); - - // Deposit Advanced Pipe output to Silo - farm.add( - new sdk.farm.actions.Deposit( - sdk.tokens.BEAN_ETH_WELL_LP, - FarmFromMode.INTERNAL - ) - ); - - // Convert the other Deposits as usual - if (amountIn.gt(0)) { - const convertData = sdk.silo.siloConvert.calculateConvert( - tokenIn, - tokenOut, - amountIn, - farmerBalances.convertibleDeposits, - season.toNumber() - ); - const amountOut = await sdk.contracts.beanstalk.getAmountOut( - tokenIn.address, - tokenOut.address, - convertData.amount.toBlockchain() - ); - const _minAmountOut = TokenValue.fromBlockchain( - amountOut.toString(), - tokenOut.decimals - ).mul(1 - slippage); - farm.add( - new sdk.farm.actions.Convert( - sdk.tokens.BEAN, - sdk.tokens.BEAN_ETH_WELL_LP, - amountIn, - _minAmountOut, - convertData.crates - ) - ); - } - - // Mow Grown Stalk - const tokensWithStalk: Map = new Map(); - farmerSilo.stalk.grownByToken.forEach((value, token) => { - if (value.gt(0)) { - tokensWithStalk.set(token, value); - } - }); - if (tokensWithStalk.size > 0) { - farm.add(new sdk.farm.actions.Mow(account, tokensWithStalk)); - } - - const gasEstimate = await farm.estimateGas(earnedBeans, { - slippage: slippage, - }); - const adjustedGas = Math.round( - gasEstimate.toNumber() * 1.2 - ).toString(); - txn = await farm.execute( - earnedBeans, - { slippage: slippage }, - { gasLimit: adjustedGas } - ); - } - - txToast.confirming(txn); - - const receipt = await txn.wait(); - - await refetch(actionsPerformed, { farmerSilo: true }, [ - refetchPools, // update prices to account for pool conversion - refetchFarmerBalances, - ]); - - txToast.success(receipt); - - /// Reset the max Amount In - const _maxAmountIn = await ConvertFarmStep.getMaxConvert( - sdk, - tokenIn, - tokenOut - ); - - formActions.resetForm({ - values: { - ...initialValues, - maxAmountIn: tokenValueToBN(_maxAmountIn), - }, - }); - } catch (err) { - console.error(err); - if (txToast) { - txToast.error(err); - } else { - const errorToast = new TransactionToast({}); - errorToast.error(err); - } - formActions.setSubmitting(false); - } - }, - [ - sdk, - season, - account, - txnBundler, - middleware, - plantAndDoX, - initialValues, - farmerBalances, - farmerSilo, - refetch, - refetchPools, - refetchFarmerBalances, - ] - ); - - return ( - - {(formikProps) => ( - <> - - - - - - )} - - ); -}; - -const Convert: FC<{ - fromToken: ERC20Token | NativeToken; -}> = (props) => ( - - - -); - -export default Convert; diff --git a/projects/ui/src/graph/graphql.schema.json b/projects/ui/src/graph/graphql.schema.json index e0262f1d3..54ea1099c 100644 --- a/projects/ui/src/graph/graphql.schema.json +++ b/projects/ui/src/graph/graphql.schema.json @@ -128883,6 +128883,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "proposalsCount30d", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "rank", "description": null, @@ -166576,9 +166588,7 @@ "name": "derivedFrom", "description": "creates a virtual field on the entity that may be queried but cannot be set manually through the mappings API.", "isRepeatable": false, - "locations": [ - "FIELD_DEFINITION" - ], + "locations": ["FIELD_DEFINITION"], "args": [ { "name": "field", @@ -166602,20 +166612,14 @@ "name": "entity", "description": "Marks the GraphQL type as indexable entity. Each type that should be an entity is required to be annotated with this directive.", "isRepeatable": false, - "locations": [ - "OBJECT" - ], + "locations": ["OBJECT"], "args": [] }, { "name": "include", "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", "isRepeatable": false, - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -166639,20 +166643,14 @@ "name": "oneOf", "description": "Indicates exactly one field must be supplied and this field must not be `null`.", "isRepeatable": false, - "locations": [ - "INPUT_OBJECT" - ], + "locations": ["INPUT_OBJECT"], "args": [] }, { "name": "skip", "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", "isRepeatable": false, - "locations": [ - "FIELD", - "FRAGMENT_SPREAD", - "INLINE_FRAGMENT" - ], + "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [ { "name": "if", @@ -166676,9 +166674,7 @@ "name": "specifiedBy", "description": "Exposes a URL that specifies the behavior of this scalar.", "isRepeatable": false, - "locations": [ - "SCALAR" - ], + "locations": ["SCALAR"], "args": [ { "name": "url", @@ -166702,9 +166698,7 @@ "name": "subgraphId", "description": "Defined a Subgraph ID for an object type", "isRepeatable": false, - "locations": [ - "OBJECT" - ], + "locations": ["OBJECT"], "args": [ { "name": "id", @@ -166726,4 +166720,4 @@ } ] } -} \ No newline at end of file +} diff --git a/projects/ui/src/graph/schema-snapshot1.graphql b/projects/ui/src/graph/schema-snapshot1.graphql index 232815531..167bab37f 100644 --- a/projects/ui/src/graph/schema-snapshot1.graphql +++ b/projects/ui/src/graph/schema-snapshot1.graphql @@ -320,6 +320,7 @@ type Space { proposalsCount: Int proposalsCount1d: Int proposalsCount7d: Int + proposalsCount30d: Int rank: Float skin: String strategies: [Strategy] diff --git a/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts index badea2283..eadb05808 100644 --- a/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts +++ b/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts @@ -1 +1,60 @@ -export function usePipelineConvert() {} +import { useCallback, useEffect, useState } from 'react'; +import { ERC20Token, AdvancedPipeStruct } from '@beanstalk/sdk'; +import useSdk from '~/hooks/sdk'; + +function getAdvancedPipeCalls( + inputToken: ERC20Token, + outputToken: ERC20Token +): AdvancedPipeStruct[] { + return []; +} + +export interface IUsePipelineConvertReturn { + // Whether the pipeline convert can run it's estimates + runMode: boolean; + // The target token to convert to + setTarget: (token: ERC20Token) => void; +} + +export function usePipelineConvert( + inputToken: ERC20Token, + _outputToken: ERC20Token +): IUsePipelineConvertReturn { + const sdk = useSdk(); + + const [target, handleSetTarget] = useState(_outputToken); + const [runMode, setRunMode] = useState(false); + + const zeroX = sdk.zeroX; + + useEffect(() => { + setRunMode(target.isLP && inputToken.isLP); + }, [inputToken.isLP, target.isLP]); + + // Error validation + useEffect(() => { + const tk = sdk.tokens.findByAddress(inputToken.address); + if (!tk || !sdk.tokens.siloWhitelist.has(tk)) { + throw new Error( + `Token ${inputToken.address} is not whitelisted in the Silo.` + ); + } + }, [inputToken.address, sdk.tokens]); + + const setTarget = useCallback( + (token: ERC20Token) => { + if (sdk.tokens.siloWhitelist.has(token)) { + handleSetTarget(token); + } else { + throw new Error( + `Token ${token.symbol} is not whitelisted in the Silo.` + ); + } + }, + [sdk.tokens.siloWhitelist] + ); + + return { runMode, setTarget }; +} + +export function useConvertPaths() {} From 40cdbee9431331b8d804c55636179dbf2da69ad1 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 02:31:26 -0600 Subject: [PATCH 07/14] feat: create tenderly api --- projects/ui/src/env.d.ts | 20 ++++ projects/ui/src/util/tenderly/api.ts | 128 ++++++++++++++++++++++ projects/ui/src/util/tenderly/types.ts | 145 +++++++++++++++++++++++++ 3 files changed, 293 insertions(+) create mode 100644 projects/ui/src/util/tenderly/api.ts create mode 100644 projects/ui/src/util/tenderly/types.ts diff --git a/projects/ui/src/env.d.ts b/projects/ui/src/env.d.ts index da3005c7e..3180c07ec 100644 --- a/projects/ui/src/env.d.ts +++ b/projects/ui/src/env.d.ts @@ -26,6 +26,26 @@ interface ImportMetaEnv { * API key for used for ZeroX Swap API */ readonly VITE_ZERO_X_API_KEY: string; + + /** + * + */ + readonly VITE_TENDERLY_ACCOUNT_SLUG: string; + + /** + * + */ + readonly VITE_TENDERLY_PROJECT_SLUG: string; + + /** + * + */ + readonly VITE_TENDERLY_ACCESS_KEY: string; + + /** + * Tenderly Virtual Network ID for testnet + */ + readonly VITE_TENDERLY_VNET_ID?: string; } interface ImportMeta { diff --git a/projects/ui/src/util/tenderly/api.ts b/projects/ui/src/util/tenderly/api.ts new file mode 100644 index 000000000..ed08cec4a --- /dev/null +++ b/projects/ui/src/util/tenderly/api.ts @@ -0,0 +1,128 @@ +import { ChainResolver } from '@beanstalk/sdk-core'; +import { + TenderlySimulatePayload, + TenderlySimulateTxnParams, + TenderlyVnetSimulationPayload, +} from './types'; + +const TENDERLY_API_KEY = import.meta.env.VITE_TENDERLY_ACCESS_KEY; + +const TENDERLY_ACCOUNT_SLUG = import.meta.env.VITE_TENDERLY_ACCOUNT_SLUG; + +const TENDERLY_PROJECT_SLUG = import.meta.env.VITE_TENDERLY_PROJECT_SLUG; + +const TENDERLY_VNET_ID = import.meta.env.VITE_TENDERLY_VNET_ID; + +const isDevMode = import.meta.env.DEV; + +const baseEndpoint = 'https://api.tenderly.co/api/v1/account/'; + +const tenderlyEndpoint = `${baseEndpoint}/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}`; + +const testnetEndpoint = `${tenderlyEndpoint}/vnetId/${TENDERLY_VNET_ID}`; + +type RequestInit = { + signal?: AbortSignal; +}; + +const baseHeaders = new Headers({ + 'Content-Type': 'application/json', + 'X-Access-Key': TENDERLY_API_KEY, +}); + +const tenderlyVnetSimulateTxn = async ( + payload: TenderlySimulateTxnParams, + requestInit?: RequestInit +) => { + const endpoint = `${testnetEndpoint}/transactions/simulate`; + + const params: TenderlyVnetSimulationPayload = { + callArgs: { + from: payload.from, + to: payload.to, + gas: payload.gas?.toString(), + data: payload.callData, + value: payload.value, + }, + }; + + if (payload.blockNumber) { + params.blockNumber = payload.blockNumber.toString(); + } + + const options = { + ...requestInit, + method: 'POST', + headers: baseHeaders, + body: JSON.stringify(params), + }; + + return fetch(endpoint, options); +}; + +const tenderlyProdSimulateTxn = async ( + payload: TenderlySimulateTxnParams, + requestInit?: RequestInit +) => { + const endpoint = `${tenderlyEndpoint}/simulate`; + + const params: TenderlySimulatePayload = { + network_id: payload.chainId.toString(), + from: payload.from, + to: payload.to, + input: payload.callData, + gas: payload.gas, + simulation_type: payload.simulationType || 'full', + value: payload.value, + block_number: payload.blockNumber, + }; + + const options = { + ...requestInit, + method: 'POST', + headers: baseHeaders, + body: JSON.stringify(params), + }; + + return fetch(endpoint, options); +}; + +export const tenderlySimulateTxn = async ( + payload: TenderlySimulateTxnParams, + requestInit?: RequestInit +) => { + const requestFn = + isDevMode && ChainResolver.isTestnet(payload.chainId) + ? tenderlyVnetSimulateTxn + : tenderlyProdSimulateTxn; + + try { + return requestFn(payload, requestInit).then((response) => { + if (!response.ok) { + throw new Error(`Error simulating transaction: ${response.status}`); + } + + const remaining = response.headers.get('X-Tdly-Limit'); + const resetTimeStamp = response.headers.get('X-Tdly-Reset-Timestamp'); + + console.debug( + `[TENDERLY]: ${remaining} requests remaining. Reset at ${resetTimeStamp}` + ); + + const data = response.json(); + + console.debug(`[TENDERLY] SUCCESS. response:`, response); + + return { + requestInfo: { + remaining, + resetTimeStamp, + }, + ...data, + }; + }); + } catch (error) { + console.error('Error simulating transaction', error); + throw error; + } +}; diff --git a/projects/ui/src/util/tenderly/types.ts b/projects/ui/src/util/tenderly/types.ts new file mode 100644 index 000000000..b9b134774 --- /dev/null +++ b/projects/ui/src/util/tenderly/types.ts @@ -0,0 +1,145 @@ +import { SupportedChainId } from '~/constants'; + +export interface TenderlySimulateTxnParams { + /** + * Network ID + */ + chainId: SupportedChainId; + /** + * Address initiating the transaction + */ + from: string; + /** + * Recipient address of the transaction + */ + to: string; + /** + * Call data string + */ + callData: string; + /** + * Type of simulation to run. Applicable only in production. + */ + simulationType?: 'full' | 'quick' | 'abi'; + /** + * Amount of Ether (in wei) sent along with the transaction. + */ + value?: string; + /** + * Amount of gas provided for the simulation. + */ + gas?: number; + /** + * Number of the block to be used for the simulation. + */ + blockNumber?: number; +} + +/** + * Tenderly simulate transaction payload + * + * @notes NOT for virtual testnet + * + * @see https://docs.tenderly.co/reference/api#/operations/simulateTransaction + */ +export interface TenderlySimulatePayload { + /** + * ID of the network on which the simulation is being run. + */ + network_id: string; + /** + * Address initiating the transaction + */ + from: string; + /** + * Recipient address of the transaction + */ + to: string; + /** + * Calldata string + */ + input: string; + /** + * Amount of gas provided for the simulation. + */ + gas?: number; + /** + * Number of the block to be used for the simulation. + */ + block_number?: number; + /** + * Index of the transaction within the block. + */ + transaction_index?: number; + /** + * String representation of a number that represents price of the gas in wei. + */ + gas_price?: string; + /** + * Flag that enables precise gas estimation. + */ + estimate_gas?: boolean; + /** + * Amount of Ether (in wei) sent along with the transaction. + */ + value?: string; + /** + * Flag indicating whether to save the simulation in dashboard UI. + */ + save?: boolean; + /** + * Flag indicating whether to save failed simulation in dashboard UI. + */ + save_failed?: boolean; + /** + * + */ + simulation_type: 'full' | 'quick' | 'abi'; +} + +/** + * Tenderly simulate transaction payload ONLY for virtual testnet + * + * @see https://docs.tenderly.co/reference/api#/operations/simulateTx + */ +export interface TenderlyVnetSimulationPayload { + callArgs: { + /** + * Address initiating the transaction + */ + from: string; + /** + * Recipient address of the transaction + */ + to: string; + /** + * Amount of gas provided for the simulation. + */ + gas?: string; + /** + * String representation of a number that represents price of the gas in wei. + */ + gasPrice?: string; + /** + * Amount of Ether (in wei) sent along with the transaction. + */ + data: string; + /** + * Amount of Ether (in wei) sent along with the transaction. + */ + value?: string; + }; + /** + * Number of the block to be used for the simulation. + */ + blockNumber?: string; + blockOverrides?: { + number: string; + timestamp: string; + }; + stateOverrides?: { + [address: string]: { + balance: string; + }; + }; +} From 54466a3f48d0d110fc46946eb593a39b17cec5c2 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 02:31:50 -0600 Subject: [PATCH 08/14] feat: update .env files --- projects/ui/.env.development | 6 +++++- projects/ui/.env.production | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/projects/ui/.env.development b/projects/ui/.env.development index 19b2429db..72b81f1f6 100644 --- a/projects/ui/.env.development +++ b/projects/ui/.env.development @@ -8,4 +8,8 @@ VITE_ALCHEMY_API_KEY="ds4ljBC_Pq-PaIQ3aHo04t27y2n8qpry" VITE_THEGRAPH_API_KEY="4c0b9364a121c1f2aa96fe61cb73d705" VITE_WALLETCONNECT_PROJECT_ID=2159ea7542f2b547554f8c85eca0cec1 VITE_SNAPSHOT_API_KEY="83b2ba4f5e943503dad56d4afea4a5205ace935d702cb8c0a1151c995b474f59" -VITE_ZERO_X_API_KEY="" \ No newline at end of file +VITE_ZERO_X_API_KEY="" +VITE_TENDERLY_ACCOUNT_SLUG="" +VITE_TENDERLY_PROJECT_SLUG="" +VITE_TENDERLY_ACCESS_KEY="" +VITE_TENDERLY_VNET_ID="" \ No newline at end of file diff --git a/projects/ui/.env.production b/projects/ui/.env.production index 77403a824..c3ee8bf22 100644 --- a/projects/ui/.env.production +++ b/projects/ui/.env.production @@ -6,4 +6,7 @@ VITE_ALCHEMY_API_KEY="iByabvqm_66b_Bkl9M-wJJGdCTuy19R3" VITE_THEGRAPH_API_KEY="4c0b9364a121c1f2aa96fe61cb73d705" VITE_SNAPSHOT_API_KEY="83b2ba4f5e943503dad56d4afea4a5205ace935d702cb8c0a1151c995b474f59" -VITE_ZERO_X_API_KEY="" \ No newline at end of file +VITE_ZERO_X_API_KEY="" +VITE_TENDERLY_ACCOUNT_SLUG="" +VITE_TENDERLY_PROJECT_SLUG="" +VITE_TENDERLY_ACCESS_KEY="" \ No newline at end of file From bfcd29cbc82be358236e2bc901aea56d90b298e6 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 04:14:57 -0600 Subject: [PATCH 09/14] fix: update redirect error + update vite.config + update piplineconvertform --- projects/sdk/src/lib/matcha/zeroX.ts | 32 ++-- .../Actions/Convert/PipelineConvertForm.tsx | 164 +++++++++--------- projects/ui/vite.config.mts | 10 +- 3 files changed, 111 insertions(+), 95 deletions(-) diff --git a/projects/sdk/src/lib/matcha/zeroX.ts b/projects/sdk/src/lib/matcha/zeroX.ts index 2ca78b4fa..1be78046d 100644 --- a/projects/sdk/src/lib/matcha/zeroX.ts +++ b/projects/sdk/src/lib/matcha/zeroX.ts @@ -1,7 +1,7 @@ import { ZeroExAPIRequestParams, ZeroExQuoteResponse } from "./types"; export class ZeroX { - readonly swapV1Endpoint = "http://arbitrum.api.0x.org/swap/v1/quote"; + readonly swapV1Endpoint = "https://arbitrum.api.0x.org/swap/v1/quote"; constructor(private _apiKey: string = "") {} @@ -14,14 +14,14 @@ export class ZeroX { /** * fetch the quote from the 0x API - * @notes defaults: + * @notes defaults: * - slippagePercentage: In human readable form. 0.01 = 1%. Defaults to 0.001 (0.1%) * - skipValidation: defaults to true * - shouldSellEntireBalance: defaults to false */ async fetchSwapQuote( - args: T , - requestInit?: Omit + args: T, + requestInit?: Omit ): Promise { if (!this._apiKey) { throw new Error("Cannot fetch from 0x without an API key"); @@ -35,8 +35,10 @@ export class ZeroX { ...requestInit, method: "GET", headers: new Headers({ + "Content-Type": "application/json", + Accept: "application/json", "0x-api-key": this._apiKey - }), + }) }; const url = `${this.swapV1Endpoint}?${fetchParams.toString()}`; @@ -47,10 +49,12 @@ export class ZeroX { /** * Generate the params for the 0x API * @throws if required params are missing - * + * * @returns the params for the 0x API */ - private generateQuoteParams(params: T): ZeroExAPIRequestParams { + private generateQuoteParams( + params: T + ): ZeroExAPIRequestParams { if (!params.buyToken && !params.sellToken) { throw new Error("buyToken and sellToken and required"); } @@ -59,8 +63,7 @@ export class ZeroX { throw new Error("sellAmount or buyAmount is required"); } - // Return all the params to filter out the ones that are not part of the request - return { + const quoteParams = { sellToken: params.sellToken, buyToken: params.buyToken, sellAmount: params.sellAmount, @@ -75,7 +78,16 @@ export class ZeroX { buyTokenPercentageFee: params.buyTokenPercentageFee, priceImpactProtectionPercentage: params.priceImpactProtectionPercentage, feeRecipientTradeSurplus: params.feeRecipientTradeSurplus, - shouldSellEntireBalance: params.shouldSellEntireBalance ?? false, + shouldSellEntireBalance: params.shouldSellEntireBalance ?? false }; + + Object.keys(quoteParams).forEach((_key) => { + const key = _key as keyof typeof quoteParams; + if (quoteParams[key] === undefined || quoteParams[key] === null) { + delete quoteParams[key]; + } + }); + + return quoteParams; } } diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 86ed142c3..6327669de 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useCallback } from 'react'; import { Form } from 'formik'; import BigNumber from 'bignumber.js'; @@ -34,7 +34,7 @@ import TxnAccordion from '~/components/Common/TxnAccordion'; import StatHorizontal from '~/components/Common/StatHorizontal'; import { ActionType, displayFullBN } from '~/util'; -import { useAccount } from 'wagmi'; +import useSdk from '~/hooks/sdk'; import { BaseConvertFormProps } from './types'; interface Props extends BaseConvertFormProps { @@ -86,7 +86,6 @@ const PipelineConvertFormInner = ({ setFieldValue, }: PipelineConvertFormProps) => { const [tokenSelectOpen, showTokenSelect, hideTokenSelect] = useToggle(); - const account = useAccount(); const getBDV = useBDV(); const sourceToken = sourceWell.lpToken; // LP token of source well @@ -109,80 +108,69 @@ const PipelineConvertFormInner = ({ const debouncedAmountIn = useDebounce(amountIn ?? ZERO_BN); - const sourceIndexes = getWellTokenIndexes(sourceWell, BEAN); - const targetIndexes = getWellTokenIndexes(targetWell, BEAN); + const sourceIdx = getWellTokenIndexes(sourceWell, BEAN); + const targetIdx = getWellTokenIndexes(targetWell, BEAN); - const sellToken = sourceWell.tokens[sourceIndexes.nonBeanIndex]; // token we will sell when after removing liquidity in equal parts - const buyToken = targetWell.tokens[targetIndexes.nonBeanIndex]; // token we will buy to add liquidity + const sellToken = sourceWell.tokens[sourceIdx.nonBeanIndex]; // token we will sell when after removing liquidity in equal parts + const buyToken = targetWell.tokens[targetIdx.nonBeanIndex]; // token we will buy to add liquidity const slippage = values.settings.slippage; - const fetchEnabled = account.address && maxConvertableBN.gt(0); - - const { data: removeOut, ...removeOutQuery } = useQuery({ - queryKey: queryKeys.wellLPOut(sourceWell, targetWell, debouncedAmountIn), - queryFn: async () => { - const outAmount = await sourceWell.getRemoveLiquidityOutEqual( - sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()) - ); - - console.log(`[pipelineConvert/removeOutQuery (1)]: amountOut: `, { - BEAN: outAmount[sourceIndexes.beanIndex].toNumber(), - [`${sellToken.symbol}`]: - outAmount[sourceIndexes.nonBeanIndex].toNumber(), - }); - return outAmount; - }, - enabled: fetchEnabled && debouncedAmountIn?.gt(0) && amountIn?.gt(0), - initialData: defaultWellLpOut, - ...baseQueryOptions, - }); - - const beanAmountOut = removeOut[sourceIndexes.beanIndex]; - const swapAmountIn = removeOut[sourceIndexes.nonBeanIndex]; - // prettier-ignore - const { data: swapQuote, ...swapOutQuery } = useQuery({ - queryKey: queryKeys.swapOut(sellToken, buyToken, swapAmountIn, slippage), - queryFn: async () => { - const quote = await sdk.zeroX.fetchSwapQuote({ - // 0x requests are formatted such that 0.01 = 1%. Everywhere else in the UI we use 0.01 = 0.01% ?? BS3TODO: VALIDATE ME - slippagePercentage: (slippage / 10).toString(), - sellToken: sellToken.address, - buyToken: buyToken.address, - sellAmount: swapAmountIn.blockchainString, - }); - console.log( - `[pipelineConvert/swapOutQuery (2)]: buyAmount: ${quote?.buyAmount}` - ); - return quote; - }, - retryDelay: 500, // We get 10 requests per second from 0x, so wait 500ms before trying again. - enabled: fetchEnabled && swapAmountIn?.gt(0) && getNextChainedQueryEnabled(removeOutQuery), - ...baseQueryOptions, - }); - - const buyAmount = buyToken.fromBlockchain(swapQuote?.buyAmount || '0'); - const addLiqTokens = targetWell.tokens; - - const { data: targetAmountOut, ...addLiquidityQuery } = useQuery({ - queryKey: queryKeys.addLiquidity(addLiqTokens, beanAmountOut, buyAmount), + const { data } = useQuery({ + queryKey: ['pipelineConvert', sourceWell.address, targetWell.address, debouncedAmountIn.toString()], queryFn: async () => { - const outAmount = await targetWell.getAddLiquidityOut([ - beanAmountOut, - buyAmount, - ]); - - setFieldValue('tokens.0.amountOut', new BigNumber(outAmount.toHuman())); - console.log( - `[pipelineConvert/addLiquidityQuery (3)]: amountOut: ${outAmount.toNumber()}` - ); - return outAmount; + setFieldValue('tokens.0.quoting', true); + try { + const sourceLPAmountOut = await sourceWell.getRemoveLiquidityOutEqual( + sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()) + ); + + console.debug(`[pipelineConvert/removeLiquidity (1)] result:`, { + BEAN: sourceLPAmountOut[sourceIdx.beanIndex].toNumber(), + [`${sellToken.symbol}`]: sourceLPAmountOut[sourceIdx.nonBeanIndex].toNumber(), + }); + + const beanAmountOut = sourceLPAmountOut[sourceIdx.beanIndex]; + const swapAmountIn = sourceLPAmountOut[sourceIdx.nonBeanIndex]; + + const quote = await sdk.zeroX.fetchSwapQuote({ + sellToken: sellToken.address, + buyToken: buyToken.address, + sellAmount: swapAmountIn.blockchainString, + takerAddress: sdk.contracts.pipeline.address, + shouldSellEntireBalance: true, + // 0x requests are formatted such that 0.01 = 1%. Everywhere else in the UI we use 0.01 = 0.01% ?? BS3TODO: VALIDATE ME + slippagePercentage: (slippage / 10).toString(), + }); + + console.debug(`[pipelineConvert/0xQuote (2)] result:`, { quote }); + + const swapAmountOut = buyToken.fromBlockchain(quote?.buyAmount || '0'); + const targetLPAmountOut = await targetWell.getAddLiquidityOut([ + beanAmountOut, + swapAmountOut, + ]); + console.debug(`[pipelineConvert/addLiquidity (3)] result:`, { + amount: targetLPAmountOut.toNumber(), + }); + + setFieldValue('tokens.0.amountOut', new BigNumber(targetLPAmountOut.toHuman())); + return { + beanAmountOut, + swapAmountIn, + swapAmountOut, + quote, + targetLPAmountOut, + }; + } catch (e) { + console.debug('[pipelineConvert/query] FAILED: ', e); + throw e; + } finally { + setFieldValue('tokens.0.quoting', false); + } }, - enabled: - fetchEnabled && - buyAmount.gt(0) && - getNextChainedQueryEnabled(swapOutQuery), + enabled: maxConvertableBN.gt(0) && debouncedAmountIn?.gt(0), ...baseQueryOptions, }); @@ -201,12 +189,7 @@ const PipelineConvertFormInner = ({ } }; - // prettier-ignore - const isLoading = removeOutQuery.isLoading || swapOutQuery.isLoading || addLiquidityQuery.isLoading; - useEffect(() => { - setFieldValue('tokens.0.quoting', isLoading); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLoading]); + const isQuoting = values.tokens?.[0]?.quoting; return ( <> @@ -222,6 +205,7 @@ const PipelineConvertFormInner = ({ ), }} + balance={maxConvertableBN} quote={ {/* {displayQuote(state.amountOut, tokenOut)} */} - {isLoading && ( + {isQuoting && ( - {amountIn?.gt(0) && targetAmountOut?.gt(0) && ( + {amountIn?.gt(0) && data?.targetLPAmountOut?.gt(0) && ( { - const sourceToken = values.tokens[0].token; + const sourceToken = values.tokens?.[0].token; const targetToken = values.tokenOut; // No need to memoize wells since they their object references don't change @@ -369,6 +354,21 @@ function getNextChainedQueryEnabled(query: Omit) { query.isSuccess && !query.isLoading && !query.isFetching && !query.isError ); } + +function useBuildWorkflow( + sourceWell: BasinWell, + targetWell: BasinWell, + amountIn: TokenValue, + swapTokenIn: Token, + swapTokenOut: Token, + swapAmountOut: TokenValue, + minTargetLPOut: TokenValue, + slippage: number +) { + const sdk = useSdk(); + + const buildWorkflow = useCallback(async () => {}, []); +} // const swapAmountIn = removeOutQuery.data?.[sourceWellNonBeanIndex]; // const swapOutQuery = useQuery({ diff --git a/projects/ui/vite.config.mts b/projects/ui/vite.config.mts index 4bf61b8ab..38b48a32c 100644 --- a/projects/ui/vite.config.mts +++ b/projects/ui/vite.config.mts @@ -1,4 +1,4 @@ -import { defineConfig, splitVendorChunkPlugin, UserConfig } from 'vite'; +import { defineConfig, splitVendorChunkPlugin } from 'vite'; import path from 'path'; import { createHtmlPlugin } from 'vite-plugin-html'; import react from '@vitejs/plugin-react'; @@ -41,6 +41,7 @@ const CSP = buildCSP({ '*.google-analytics.com', '*.doubleclick.net', 'https://gateway-arbitrum.network.thegraph.com', // Decentralized subgraph + '*.0x.org', // 0x API ], 'style-src': [ "'self'", @@ -63,7 +64,10 @@ const CSP = buildCSP({ 'https://cf-ipfs.com/', // Gov proposal images, 'https://*.ipfs.cf-ipfs.com/', ], - 'frame-src': ['https://verify.walletconnect.com/', 'https://verify.walletconnect.org'], // for walletconnect + 'frame-src': [ + 'https://verify.walletconnect.com/', + 'https://verify.walletconnect.org', + ], // for walletconnect }); // @ts-ignore @@ -129,4 +133,4 @@ export default defineConfig(({ command }) => ({ ], }, }, -})); +})); From ef563ddaf50885f0563198e95bc5dec3afc8e892 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 04:18:33 -0600 Subject: [PATCH 10/14] feat: add tenderly to csp + vite config --- projects/ui/src/util/tenderly/index.ts | 2 ++ projects/ui/vite.config.mts | 1 + 2 files changed, 3 insertions(+) create mode 100644 projects/ui/src/util/tenderly/index.ts diff --git a/projects/ui/src/util/tenderly/index.ts b/projects/ui/src/util/tenderly/index.ts new file mode 100644 index 000000000..f3b202c9c --- /dev/null +++ b/projects/ui/src/util/tenderly/index.ts @@ -0,0 +1,2 @@ +export * from './types'; +export * from './api'; diff --git a/projects/ui/vite.config.mts b/projects/ui/vite.config.mts index 38b48a32c..57afb664c 100644 --- a/projects/ui/vite.config.mts +++ b/projects/ui/vite.config.mts @@ -42,6 +42,7 @@ const CSP = buildCSP({ '*.doubleclick.net', 'https://gateway-arbitrum.network.thegraph.com', // Decentralized subgraph '*.0x.org', // 0x API + '*.tenderly.co', // Tenderly API ], 'style-src': [ "'self'", From ef62d742a53cad866afa235d20a07f30cc8d4e58 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 05:50:32 -0600 Subject: [PATCH 11/14] feat: finish pipeline-convert equal<>equal logic --- .../Actions/Convert/PipelineConvertForm.tsx | 95 ++----- .../lib/PipelineConvert/PipelineConvert.ts | 236 ++++++++++++++++++ .../lib/PipelineConvert/usePipelineConvert.ts | 60 ----- 3 files changed, 259 insertions(+), 132 deletions(-) create mode 100644 projects/ui/src/lib/PipelineConvert/PipelineConvert.ts delete mode 100644 projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 6327669de..7957063a6 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -1,8 +1,8 @@ -import React, { useCallback } from 'react'; +import React from 'react'; import { Form } from 'formik'; import BigNumber from 'bignumber.js'; -import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { Box, CircularProgress, @@ -34,44 +34,23 @@ import TxnAccordion from '~/components/Common/TxnAccordion'; import StatHorizontal from '~/components/Common/StatHorizontal'; import { ActionType, displayFullBN } from '~/util'; -import useSdk from '~/hooks/sdk'; import { BaseConvertFormProps } from './types'; interface Props extends BaseConvertFormProps { farmerBalances: TokenSiloBalance | undefined; } -const defaultWellLpOut = [TokenValue.ZERO, TokenValue.ZERO]; - -const baseQueryOptions = { - refetchOnWindowFocus: true, - staleTime: 20_000, // 20 seconds stale time - refetchIntervalInBackground: false, -}; - -// prettier-ignore -const queryKeys = { - wellLPOut: (sourceWell: BasinWell,targetWell: BasinWell,amountIn: BigNumber) => [ - ['pipe-convert'], ['source-lp-out'], sourceWell?.address || 'no-source-well', targetWell?.address || 'no-target-well', amountIn.toString(), - ], - swapOut: (sellToken: Token, buyToken: Token, amountIn: TokenValue, slippage: number) => [ - ['pipe-convert'], ['swap-out'], sellToken?.address || 'no-sell-token', buyToken?.address || 'no-buy-token', amountIn.toHuman(), slippage, - ], - addLiquidity: (tokensIn: Token[], beanIn: TokenValue | undefined, nonBeanIn: TokenValue | undefined - ) => [ - ['pipe-convert'], - 'add-liquidity', - ...tokensIn.map((t) => t.address), - beanIn?.blockchainString || '0', - nonBeanIn?.blockchainString || '0', - ], -}; - interface PipelineConvertFormProps extends Props { sourceWell: BasinWell; targetWell: BasinWell; } +const baseQueryOptions = { + staleTime: 20_000, // 20 seconds stale time + refetchOnWindowFocus: true, + refetchIntervalInBackground: false, +} as const; + const PipelineConvertFormInner = ({ sourceWell, targetWell, @@ -92,30 +71,25 @@ const PipelineConvertFormInner = ({ const targetToken = targetWell.lpToken; // LP token of target well const BEAN = sdk.tokens.BEAN; - // Form values - const amountIn = values.tokens[0].amount; // amount of from token - - const maxConvertable = ( - balance?.convertibleAmount || TokenValue.ZERO - ).toHuman(); - - const maxConvertableBN = new BigNumber(maxConvertable); + const debouncedAmountIn = useDebounce(values.tokens[0].amount ?? ZERO_BN); // - // const amountOut = values.tokens[0].amountOut; // amount of to token - // const maxAmountIn = values.maxAmountIn; - // const canConvert = maxAmountIn?.gt(0) || false; - // const plantCrate = plantAndDoX?.crate?.bn; - - const debouncedAmountIn = useDebounce(amountIn ?? ZERO_BN); + const maxConvertableBN = new BigNumber( + (balance?.convertibleAmount || TokenValue.ZERO).toHuman() + ); - const sourceIdx = getWellTokenIndexes(sourceWell, BEAN); - const targetIdx = getWellTokenIndexes(targetWell, BEAN); + const sourceIdx = getWellTokenIndexes(sourceWell, BEAN); // token indexes of source well + const targetIdx = getWellTokenIndexes(targetWell, BEAN); // token indexes of target well const sellToken = sourceWell.tokens[sourceIdx.nonBeanIndex]; // token we will sell when after removing liquidity in equal parts const buyToken = targetWell.tokens[targetIdx.nonBeanIndex]; // token we will buy to add liquidity const slippage = values.settings.slippage; + // const amountOut = values.tokens[0].amountOut; // amount of to token + // const maxAmountIn = values.maxAmountIn; + // const canConvert = maxAmountIn?.gt(0) || false; + // const plantCrate = plantAndDoX?.crate?.bn; + // prettier-ignore const { data } = useQuery({ queryKey: ['pipelineConvert', sourceWell.address, targetWell.address, debouncedAmountIn.toString()], @@ -189,6 +163,7 @@ const PipelineConvertFormInner = ({ } }; + // same as query.isFetching & query.isLoading const isQuoting = values.tokens?.[0]?.quoting; return ( @@ -209,7 +184,7 @@ const PipelineConvertFormInner = ({ balanceLabel="Deposited Balance" // MUI fullWidth - max={new BigNumber(maxConvertable)} + max={maxConvertableBN} InputProps={{ endAdornment: ( - {amountIn?.gt(0) && data?.targetLPAmountOut?.gt(0) && ( + {debouncedAmountIn?.gt(0) && data?.targetLPAmountOut?.gt(0) && ( ) { - return ( - query.isSuccess && !query.isLoading && !query.isFetching && !query.isError - ); -} - -function useBuildWorkflow( - sourceWell: BasinWell, - targetWell: BasinWell, - amountIn: TokenValue, - swapTokenIn: Token, - swapTokenOut: Token, - swapAmountOut: TokenValue, - minTargetLPOut: TokenValue, - slippage: number -) { - const sdk = useSdk(); - - const buildWorkflow = useCallback(async () => {}, []); -} // const swapAmountIn = removeOutQuery.data?.[sourceWellNonBeanIndex]; // const swapOutQuery = useQuery({ diff --git a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts new file mode 100644 index 000000000..7dbbbc57c --- /dev/null +++ b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts @@ -0,0 +1,236 @@ +import { ethers } from 'ethers'; +import { + BeanstalkSDK, + BasinWell, + TokenValue, + AdvancedPipeStruct, + ERC20Token, + Clipboard, + ZeroExQuoteResponse, +} from '@beanstalk/sdk'; + +/** + * Parameters needed for PipelineConvert for the equal<->equal + * equal<>equal refering to + * - remove liquidity in equal proportions from source Well + * - add liquidity in equal proportions to target Well + */ +export interface BuildPipeCallArgsEqual { + sdk: BeanstalkSDK; + source: { + well: BasinWell; + lpAmountIn: TokenValue; + beanAmountOut: TokenValue; + nonBeanAmountOut: TokenValue; + }; + swap: { + buyToken: ERC20Token; + sellToken: ERC20Token; + // amount from 0xQuote.buyAmount + buyAmount: TokenValue; + // 0x quote.allowanceTarget + quote: ZeroExQuoteResponse; + }; + target: { + well: BasinWell; + amountOut: TokenValue; + }; + slippage: number; +} + +export class PipelineConvert { + private static erc20Approve( + token: ERC20Token, + spender: string, + amount: ethers.BigNumberish = ethers.constants.MaxUint256, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: token.address, + callData: token + .getContract() + .interface.encodeFunctionData('approve', [spender, amount]), + clipboard, + }; + } + + private static getRemoveLiquidityEqual( + sourceWell: BasinWell, + amountIn: TokenValue, + minAmountsOut: TokenValue[], + recipient: string, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: sourceWell.address, + callData: sourceWell + .getContract() + .interface.encodeFunctionData('removeLiquidity', [ + amountIn.toBigNumber(), + minAmountsOut.map((a) => a.toBigNumber()), + recipient, + ethers.constants.MaxUint256, + ]), + clipboard, + }; + } + + private static wellSync( + well: BasinWell, + recipient: string, + amount: ethers.BigNumberish, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: well.address, + callData: well + .getContract() + .interface.encodeFunctionData('sync', [recipient, amount]), + clipboard, + }; + } + + private static transferToken( + token: ERC20Token, + recipient: string, + amount: ethers.BigNumberish, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: token.address, + callData: token + .getContract() + .interface.encodeFunctionData('transfer', [recipient, amount]), + clipboard, + }; + } + + private static junctionGte( + junction: BeanstalkSDK['contracts']['junction'], + left: ethers.BigNumberish, + right: ethers.BigNumberish, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: junction.address, + callData: junction.interface.encodeFunctionData('gte', [left, right]), + clipboard: clipboard, + }; + } + + private static junctionCheck( + junction: BeanstalkSDK['contracts']['junction'], + value: boolean, + clipboard: string = ethers.constants.HashZero + ): AdvancedPipeStruct { + return { + target: junction.address, + callData: junction.interface.encodeFunctionData('check', [value]), + clipboard, + }; + } + + static buildEqual2Equal({ + sdk, + source, + swap, + target, + slippage, + }: BuildPipeCallArgsEqual): AdvancedPipeStruct[] { + if ( + swap.quote.from.toLowerCase() !== + sdk.contracts.pipeline.address.toLowerCase() + ) { + throw new Error('Swap quote from address must be pipeline'); + } + + const pipe: AdvancedPipeStruct[] = []; + + const sourceWellAmountsOut = [ + source.beanAmountOut, + source.nonBeanAmountOut, + ]; + if (!source.well.tokens[0].equals(sdk.tokens.BEAN)) { + sourceWellAmountsOut.reverse(); + } + + // 0. Approve source well to spend LP tokens + pipe.push( + PipelineConvert.erc20Approve(source.well.lpToken, source.well.address) + ); + + // 1. Remove liquidity from source well & set recipient to pipeline + pipe.push( + PipelineConvert.getRemoveLiquidityEqual( + source.well, + source.lpAmountIn, + sourceWellAmountsOut.map((a) => a.subSlippage(slippage)), + sdk.contracts.pipeline.address + ) + ); + + // 2. Approve 0x + pipe.push( + PipelineConvert.erc20Approve(swap.sellToken, swap.quote.allowanceTarget) + ); + + // 3. Swap nonBeanToken1 for nonBeanToken2. recipient MUST be Pipeline or this will fail. + pipe.push({ + target: swap.quote.to, + callData: swap.quote.data, + clipboard: Clipboard.encode([]), + }); + + // 4. transfer BuyToken to target well + pipe.push( + PipelineConvert.transferToken( + swap.buyToken, + target.well.address, + ethers.constants.Zero, + Clipboard.encodeSlot(3, 0, 1) + ) + ); + + // 5. Transfer well.tokens[0] to target well + pipe.push( + PipelineConvert.transferToken( + sdk.tokens.BEAN, + target.well.address, + ethers.constants.Zero, + Clipboard.encodeSlot(1, 3, 1) + ) + ); + + const minLPOut = target.amountOut.subSlippage(slippage).toBigNumber(); + + // 6. Call Sync on target well + pipe.push( + PipelineConvert.wellSync( + target.well, + sdk.contracts.pipeline.address, + minLPOut + ) + ); + + // 7. Check if amount receieved from sync >= minLPOut + pipe.push( + PipelineConvert.junctionGte( + sdk.contracts.junction, + ethers.constants.Zero, + minLPOut, + Clipboard.encodeSlot(6, 0, 0) + ) + ); + + // 8. Check 7 is true + pipe.push( + PipelineConvert.junctionCheck( + sdk.contracts.junction, + true, + Clipboard.encodeSlot(7, 0, 0) + ) + ); + + return pipe; + } +} diff --git a/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts deleted file mode 100644 index eadb05808..000000000 --- a/projects/ui/src/lib/PipelineConvert/usePipelineConvert.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { ERC20Token, AdvancedPipeStruct } from '@beanstalk/sdk'; -import useSdk from '~/hooks/sdk'; - -function getAdvancedPipeCalls( - inputToken: ERC20Token, - outputToken: ERC20Token -): AdvancedPipeStruct[] { - return []; -} - -export interface IUsePipelineConvertReturn { - // Whether the pipeline convert can run it's estimates - runMode: boolean; - // The target token to convert to - setTarget: (token: ERC20Token) => void; -} - -export function usePipelineConvert( - inputToken: ERC20Token, - _outputToken: ERC20Token -): IUsePipelineConvertReturn { - const sdk = useSdk(); - - const [target, handleSetTarget] = useState(_outputToken); - const [runMode, setRunMode] = useState(false); - - const zeroX = sdk.zeroX; - - useEffect(() => { - setRunMode(target.isLP && inputToken.isLP); - }, [inputToken.isLP, target.isLP]); - - // Error validation - useEffect(() => { - const tk = sdk.tokens.findByAddress(inputToken.address); - if (!tk || !sdk.tokens.siloWhitelist.has(tk)) { - throw new Error( - `Token ${inputToken.address} is not whitelisted in the Silo.` - ); - } - }, [inputToken.address, sdk.tokens]); - - const setTarget = useCallback( - (token: ERC20Token) => { - if (sdk.tokens.siloWhitelist.has(token)) { - handleSetTarget(token); - } else { - throw new Error( - `Token ${token.symbol} is not whitelisted in the Silo.` - ); - } - }, - [sdk.tokens.siloWhitelist] - ); - - return { runMode, setTarget }; -} - -export function useConvertPaths() {} From e82d8068c0f63069475c258ff0532e9481741e57 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 05:53:38 -0600 Subject: [PATCH 12/14] feat: remove pipeline recipient check --- projects/ui/src/lib/PipelineConvert/PipelineConvert.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts index 7dbbbc57c..2557e7501 100644 --- a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts +++ b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts @@ -137,13 +137,6 @@ export class PipelineConvert { target, slippage, }: BuildPipeCallArgsEqual): AdvancedPipeStruct[] { - if ( - swap.quote.from.toLowerCase() !== - sdk.contracts.pipeline.address.toLowerCase() - ) { - throw new Error('Swap quote from address must be pipeline'); - } - const pipe: AdvancedPipeStruct[] = []; const sourceWellAmountsOut = [ From d63f18ff31bd472c46943d570f8cb42c72963f8c Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 06:23:07 -0600 Subject: [PATCH 13/14] feat: update pipeconvert display values --- .../Actions/Convert/PipelineConvertForm.tsx | 72 +++++++++++++++++++ .../lib/PipelineConvert/PipelineConvert.ts | 18 ++--- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 7957063a6..4db46cebe 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -34,6 +34,7 @@ import TxnAccordion from '~/components/Common/TxnAccordion'; import StatHorizontal from '~/components/Common/StatHorizontal'; import { ActionType, displayFullBN } from '~/util'; +import { PipelineConvertUtil } from '~/lib/PipelineConvert/PipelineConvert'; import { BaseConvertFormProps } from './types'; interface Props extends BaseConvertFormProps { @@ -66,6 +67,7 @@ const PipelineConvertFormInner = ({ }: PipelineConvertFormProps) => { const [tokenSelectOpen, showTokenSelect, hideTokenSelect] = useToggle(); const getBDV = useBDV(); + const sourceToken = sourceWell.lpToken; // LP token of source well const targetToken = targetWell.lpToken; // LP token of target well @@ -77,6 +79,14 @@ const PipelineConvertFormInner = ({ (balance?.convertibleAmount || TokenValue.ZERO).toHuman() ); + const pickedDeposits = sdk.silo.siloConvert.calculateConvert( + sourceToken, + targetToken, + sourceToken.fromHuman(debouncedAmountIn.toString()), + balance?.convertibleDeposits || [], + 0 + ); + const sourceIdx = getWellTokenIndexes(sourceWell, BEAN); // token indexes of source well const targetIdx = getWellTokenIndexes(targetWell, BEAN); // token indexes of target well @@ -148,6 +158,56 @@ const PipelineConvertFormInner = ({ ...baseQueryOptions, }); + const { data: staticCallData } = useQuery({ + queryKey: ['pipelineConvert/callStatic', sourceWell.address, targetWell.address, data?.targetLPAmountOut?.toString()], + queryFn: async () => { + if (!data) return; + try { + const advPipeCalls = PipelineConvertUtil.buildEqual2Equal({ + sdk, + source: { + well: sourceWell, + lpAmountIn: sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()), + beanAmountOut: data.beanAmountOut, + nonBeanAmountOut: data.swapAmountOut, + }, + swap: { + buyToken, + sellToken, + buyAmount: data.swapAmountOut, + quote: data.quote, + }, + target: { + well: targetWell, + amountOut: data.targetLPAmountOut, + }, + slippage, + }); + + const datas = await sdk.contracts.beanstalk.callStatic.pipelineConvert( + sourceToken.address, + pickedDeposits.crates.map((c) => c.stem), + pickedDeposits.crates.map((c) => c.amount.toBigNumber()), + targetToken.address, + advPipeCalls + ).then((result) => ({ + toStem: result.toStem, + fromAmount: result.fromAmount, + toAmount: result.toAmount, + fromBdv: result.fromBdv, + toBdv: result.toBdv, + })); + + console.debug(`[pipelineConvert/callStatic] result:`, datas); + return datas; + } catch (e) { + console.debug('[pipelineConvert/callStatic] FAILED: ', e); + throw e; + } + }, + enabled: !!data && debouncedAmountIn?.gt(0), + }) + /// When a new output token is selected, reset maxAmountIn. const handleSelectTokenOut = async (_selectedTokens: Set) => { const selected = [..._selectedTokens]?.[0]; @@ -248,6 +308,18 @@ const PipelineConvertFormInner = ({ {targetToken?.symbol || 'Select token'} ) : null} + + {staticCallData && ( + + values from pipe convert: + + )} + {staticCallData ? (Object.entries(staticCallData).map(([k, v]) => ( + + {k}: {v.toString()} + + ))) : "Failed to load results from static call"} + {/* You may Lose Grown Stalk warning here */} diff --git a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts index 2557e7501..c31d8fba1 100644 --- a/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts +++ b/projects/ui/src/lib/PipelineConvert/PipelineConvert.ts @@ -38,7 +38,7 @@ export interface BuildPipeCallArgsEqual { slippage: number; } -export class PipelineConvert { +export class PipelineConvertUtil { private static erc20Approve( token: ERC20Token, spender: string, @@ -149,12 +149,12 @@ export class PipelineConvert { // 0. Approve source well to spend LP tokens pipe.push( - PipelineConvert.erc20Approve(source.well.lpToken, source.well.address) + PipelineConvertUtil.erc20Approve(source.well.lpToken, source.well.address) ); // 1. Remove liquidity from source well & set recipient to pipeline pipe.push( - PipelineConvert.getRemoveLiquidityEqual( + PipelineConvertUtil.getRemoveLiquidityEqual( source.well, source.lpAmountIn, sourceWellAmountsOut.map((a) => a.subSlippage(slippage)), @@ -164,7 +164,7 @@ export class PipelineConvert { // 2. Approve 0x pipe.push( - PipelineConvert.erc20Approve(swap.sellToken, swap.quote.allowanceTarget) + PipelineConvertUtil.erc20Approve(swap.sellToken, swap.quote.allowanceTarget) ); // 3. Swap nonBeanToken1 for nonBeanToken2. recipient MUST be Pipeline or this will fail. @@ -176,7 +176,7 @@ export class PipelineConvert { // 4. transfer BuyToken to target well pipe.push( - PipelineConvert.transferToken( + PipelineConvertUtil.transferToken( swap.buyToken, target.well.address, ethers.constants.Zero, @@ -186,7 +186,7 @@ export class PipelineConvert { // 5. Transfer well.tokens[0] to target well pipe.push( - PipelineConvert.transferToken( + PipelineConvertUtil.transferToken( sdk.tokens.BEAN, target.well.address, ethers.constants.Zero, @@ -198,7 +198,7 @@ export class PipelineConvert { // 6. Call Sync on target well pipe.push( - PipelineConvert.wellSync( + PipelineConvertUtil.wellSync( target.well, sdk.contracts.pipeline.address, minLPOut @@ -207,7 +207,7 @@ export class PipelineConvert { // 7. Check if amount receieved from sync >= minLPOut pipe.push( - PipelineConvert.junctionGte( + PipelineConvertUtil.junctionGte( sdk.contracts.junction, ethers.constants.Zero, minLPOut, @@ -217,7 +217,7 @@ export class PipelineConvert { // 8. Check 7 is true pipe.push( - PipelineConvert.junctionCheck( + PipelineConvertUtil.junctionCheck( sdk.contracts.junction, true, Clipboard.encodeSlot(7, 0, 0) From c753aeb017e7401612dd55418abd249496d9b1d5 Mon Sep 17 00:00:00 2001 From: Spacebean Date: Fri, 20 Sep 2024 06:54:27 -0600 Subject: [PATCH 14/14] feat: update pipe convert form --- .../Silo/Actions/Convert/PipelineConvertForm.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx index 4db46cebe..99be73d6f 100644 --- a/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx +++ b/projects/ui/src/components/Silo/Actions/Convert/PipelineConvertForm.tsx @@ -106,8 +106,9 @@ const PipelineConvertFormInner = ({ queryFn: async () => { setFieldValue('tokens.0.quoting', true); try { + const lpIn = sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()); const sourceLPAmountOut = await sourceWell.getRemoveLiquidityOutEqual( - sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()) + lpIn ); console.debug(`[pipelineConvert/removeLiquidity (1)] result:`, { @@ -125,7 +126,7 @@ const PipelineConvertFormInner = ({ takerAddress: sdk.contracts.pipeline.address, shouldSellEntireBalance: true, // 0x requests are formatted such that 0.01 = 1%. Everywhere else in the UI we use 0.01 = 0.01% ?? BS3TODO: VALIDATE ME - slippagePercentage: (slippage / 10).toString(), + slippagePercentage: (slippage * 100).toString(), }); console.debug(`[pipelineConvert/0xQuote (2)] result:`, { quote }); @@ -141,6 +142,7 @@ const PipelineConvertFormInner = ({ setFieldValue('tokens.0.amountOut', new BigNumber(targetLPAmountOut.toHuman())); return { + amountIn: lpIn, beanAmountOut, swapAmountIn, swapAmountOut, @@ -167,7 +169,7 @@ const PipelineConvertFormInner = ({ sdk, source: { well: sourceWell, - lpAmountIn: sourceWell.lpToken.fromHuman(debouncedAmountIn.toString()), + lpAmountIn: data.amountIn, beanAmountOut: data.beanAmountOut, nonBeanAmountOut: data.swapAmountOut, }, @@ -205,7 +207,9 @@ const PipelineConvertFormInner = ({ throw e; } }, + retry: 2, enabled: !!data && debouncedAmountIn?.gt(0), + ...baseQueryOptions }) /// When a new output token is selected, reset maxAmountIn.