diff --git a/package.json b/package.json index fa7ab0d..377797b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@crocswap-libs/sdk", - "version": "0.2.89", + "version": "0.3.0", "description": "🛠🐊🛠 An SDK for building applications on top of CrocSwap", "author": "Ben Wolski ", "repository": "https://github.com/CrocSwap/sdk.git", diff --git a/src/abis/external/L1GasPriceOracle.ts b/src/abis/external/L1GasPriceOracle.ts new file mode 100644 index 0000000..9cab83a --- /dev/null +++ b/src/abis/external/L1GasPriceOracle.ts @@ -0,0 +1 @@ +export const L1_GAS_PRICE_ORACLE_ABI = [{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"l1BaseFee","type":"uint256"}],"name":"L1BaseFeeUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"overhead","type":"uint256"}],"name":"OverheadUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"_oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"_newOwner","type":"address"}],"name":"OwnershipTransferred","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint256","name":"scalar","type":"uint256"}],"name":"ScalarUpdated","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"_oldWhitelist","type":"address"},{"indexed":false,"internalType":"address","name":"_newWhitelist","type":"address"}],"name":"UpdateWhitelist","type":"event"},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1Fee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"_data","type":"bytes"}],"name":"getL1GasUsed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"l1BaseFee","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"overhead","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"scalar","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_l1BaseFee","type":"uint256"}],"name":"setL1BaseFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_overhead","type":"uint256"}],"name":"setOverhead","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_scalar","type":"uint256"}],"name":"setScalar","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"_newWhitelist","type":"address"}],"name":"updateWhitelist","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"whitelist","outputs":[{"internalType":"contract IWhitelist","name":"","type":"address"}],"stateMutability":"view","type":"function"}] as const; \ No newline at end of file diff --git a/src/swap.ts b/src/swap.ts index f6937bc..cf4c2d8 100644 --- a/src/swap.ts +++ b/src/swap.ts @@ -3,7 +3,7 @@ import { BigNumber, ContractFunction } from "ethers"; import { TransactionResponse } from '@ethersproject/providers'; import { CrocContext } from './context'; import { CrocPoolView } from './pool'; -import { decodeCrocPrice } from './utils'; +import { decodeCrocPrice, getUnsignedRawTransaction } from './utils'; import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens'; import { AddressZero } from '@ethersproject/constants'; import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags"; @@ -139,6 +139,26 @@ export class CrocSwapPlan { return base.userCmd(HOT_PROXY_IDX, cmd, await this.buildTxArgs(surplusFlags, args.gasEst)) } + /** + * Utility function to generate a "signed" raw transaction for a swap, used for L1 gas estimation on L2's like Scroll. + * Extra 0xFF...F is appended to the unsigned raw transaction to simulate the signature and other missing fields. + * + * Note: This function is only intended for L1 gas estimation, and does not generate valid signed transactions. + */ + async getFauxRawTx (args: CrocSwapExecOpts = { }): Promise<`0x${string}`> { + const TIP = 0 + const surplusFlags = this.maskSurplusArgs(args) + + const unsignedTx = await (await this.context).dex.populateTransaction.swap + (this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex, + this.sellBase, this.qtyInBase, await this.qty, TIP, + await this.calcLimitPrice(), await this.calcSlipQty(), surplusFlags, + await this.buildTxArgs(surplusFlags)) + + // append 160 'f's to the end of the raw transaction to simulate the signature and other missing fields + return getUnsignedRawTransaction(unsignedTx) + "f".repeat(160) as `0x${string}` + } + async calcImpact(): Promise { const TIP = 0 const limitPrice = this.sellBase ? MAX_SQRT_PRICE : MIN_SQRT_PRICE diff --git a/src/utils/gas.ts b/src/utils/gas.ts new file mode 100644 index 0000000..de1292f --- /dev/null +++ b/src/utils/gas.ts @@ -0,0 +1,66 @@ +import { BigNumber, Contract, PopulatedTransaction, Transaction, utils } from "ethers"; +import { CrocEnv } from "../croc"; +import { L1_GAS_PRICE_ORACLE_ABI } from "../abis/external/L1GasPriceOracle"; + +/** + * Compute the raw transaction data for a given transaction. + * + * ref: https://docs.ethers.org/v5/cookbook/transactions/#cookbook--compute-raw-transaction + */ +export function getRawTransaction(tx: Transaction) { + function addKey(accum: any, key: keyof Transaction) { + if (tx[key]) { accum[key] = tx[key]; } + return accum; + } + + // Extract the relevant parts of the transaction and signature + const txFields = ["accessList","chainId","data","gasPrice","gasLimit","maxFeePerGas","maxPriorityFeePerGas","nonce","to","type","value"] as const; + const sigFields = ["v","r","s"] as const; + + // Serialize the signed transaction + const raw = utils.serializeTransaction(txFields.reduce(addKey, { }), sigFields.reduce(addKey, { })); + + // Double check things went well + if (utils.keccak256(raw) !== tx.hash) { throw new Error("serializing failed!"); } + + return raw as `0x${string}`; +} + +/** + * Compute the raw transaction data for a given transaction without the signature. + * + * ref: https://docs.ethers.org/v5/cookbook/transactions/#cookbook--compute-raw-transaction + */ +export function getUnsignedRawTransaction(tx: PopulatedTransaction) { + function addKey(accum: any, key: keyof PopulatedTransaction) { + if (tx[key]) { accum[key] = tx[key]; } + return accum; + } + + // Extract the relevant parts of the transaction and signature + const txFields = ["accessList","chainId","data","gasPrice","gasLimit","maxFeePerGas","maxPriorityFeePerGas","nonce","to","type","value"] as const; + + // Serialize the signed transaction + const raw = utils.serializeTransaction(txFields.reduce(addKey, { })); + + return raw as `0x${string}`; +} + +/** + * Estimates the additional L1 gas on Scroll for any data which is a RLP-encoded transaction with signature. + */ +export async function estimateScrollL1Gas(crocEnv: CrocEnv, rawTransaction: `0x${string}`): Promise { + const crocContext = await crocEnv.context; + const chainId = crocContext.chain.chainId; + const isScroll = chainId === "0x82750" || chainId === "0x8274f"; + if (!isScroll) { + return BigNumber.from(0); + } + + const L1_GAS_PRICE_ORACLE_ADDRESS = "0x5300000000000000000000000000000000000002"; + const l1GasPriceOracle = new Contract(L1_GAS_PRICE_ORACLE_ADDRESS, L1_GAS_PRICE_ORACLE_ABI, crocContext.provider); + + // function getL1Fee(bytes memory _data) external view override returns (uint256); + const l1Gas = await l1GasPriceOracle.getL1Fee(rawTransaction) as BigNumber; + return l1Gas; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 1a5cdcb..9fda2c3 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,3 +2,4 @@ export * from "./math"; export * from "./price"; export * from "./token"; export * from "./liquidity"; +export * from "./gas";