Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@crocswap-libs/sdk",
"version": "2.0.14",
"version": "2.1.0",
"description": "🛠🐊🛠 An SDK for building applications on top of CrocSwap",
"author": "Ben Wolski <ben@crocodilelabs.io>",
"repository": "https://github.com/CrocSwap/sdk.git",
Expand Down
24 changes: 22 additions & 2 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CHAIN_SPECS, ChainSpec } from "./constants";

export interface CrocContext {
provider: Provider;
actor: Provider | Signer;
actor: Signer;
dex: Contract;
router?: Contract;
routerBypass?: Contract;
Expand Down Expand Up @@ -107,7 +107,7 @@ function inflateContracts(
const context = lookupChain(chainId);
return {
provider: provider,
actor: actor,
actor: actor as Signer,
dex: new Contract(context.addrs.dex, CROC_ABI, actor),
router: context.addrs.router ? new Contract(context.addrs.router || ZeroAddress, CROC_ABI, actor) : undefined,
routerBypass: context.addrs.routerBypass ? new Contract(context.addrs.routerBypass || ZeroAddress, CROC_ABI, actor) : undefined,
Expand Down Expand Up @@ -142,3 +142,23 @@ export async function ensureChain(cntx: CrocContext) {
throw new Error(`Wrong chain selected in the wallet: expected ${contextNetwork.displayName} (${contextNetwork.chainId}) but got ${walletNetwork.name} (0x${Number(walletNetwork.chainId).toString(16)})`)
}
}

// Attempt to call `eth_estimateGas` using the wallet's provider, and fall back
// to the frontend's provider if it fails or times out.
export async function estimateGas(cntx: CrocContext, populatedTx: ethers.ContractTransaction): Promise<bigint> {
if (cntx.actor) {
if (!populatedTx.from)
populatedTx.from = (await cntx.actor.getAddress()).toLowerCase();
try {
const result = await Promise.race([
new Promise((_, reject) => setTimeout(() => reject(new Error("Gas estimation timed out")), 2000)),
cntx.actor.estimateGas(populatedTx)
]) as bigint;
return result;
} catch (e) {
console.warn("Failed to estimate gas with wallet provider, falling back to frontend provider", e);
}
}

return await cntx.provider.estimateGas(populatedTx);
}
10 changes: 6 additions & 4 deletions src/knockout.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TransactionResponse, ZeroAddress } from 'ethers';
import { ChainSpec } from "./constants";
import { CrocContext, ensureChain } from './context';
import { CrocContext, ensureChain, estimateGas } from './context';
import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags";
import { KnockoutEncoder } from "./encoding/knockout";
import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens';
import { baseTokenForQuoteConc, bigIntToFloat, floatToBigInt, GAS_PADDING, quoteTokenForBaseConc, roundForConcLiq } from "./utils";
import { sendTransaction } from './vendorEthers';


export class CrocKnockoutHandle {
Expand Down Expand Up @@ -87,9 +88,10 @@ export class CrocKnockoutHandle {
let cntx = await this.context
if (txArgs === undefined) { txArgs = {} }
await ensureChain(cntx)
const gasEst = await cntx.dex.userCmd.estimateGas(KNOCKOUT_PATH, calldata, txArgs)
Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return cntx.dex.userCmd(KNOCKOUT_PATH, calldata, txArgs);
const populatedTx = await cntx.dex.userCmd.populateTransaction(KNOCKOUT_PATH, calldata, txArgs)
const gasEst = await estimateGas(cntx, populatedTx);
Object.assign(populatedTx, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return sendTransaction(cntx, populatedTx);
}

private maskSurplusFlags (opts?: CrocKnockoutOpts): number {
Expand Down
17 changes: 10 additions & 7 deletions src/pool.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TransactionResponse, ZeroAddress } from 'ethers';
import { CrocContext, ensureChain } from "./context";
import { CrocContext, ensureChain, estimateGas } from "./context";
import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags";
import { PoolInitEncoder } from "./encoding/init";
import { WarmPathEncoder } from './encoding/liquidity';
import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens';
import { bigIntToFloat, concBaseSlippagePrice, concDepositSkew, concQuoteSlippagePrice, decodeCrocPrice, fromDisplayPrice, GAS_PADDING, neighborTicks, pinTickLower, pinTickOutside, pinTickUpper, roundForConcLiq, tickToPrice, toDisplayPrice, toDisplayQty } from './utils';
import { sendTransaction } from './vendorEthers';

type PriceRange = [number, number]
type TickRange = [number, number]
Expand Down Expand Up @@ -128,9 +129,10 @@ export class CrocPoolView {

let cntx = await this.context
await ensureChain(cntx)
const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.cold, calldata, txArgs)
Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return cntx.dex.userCmd(cntx.chain.proxyPaths.cold, calldata, txArgs)
const populatedTx = await cntx.dex.userCmd.populateTransaction(cntx.chain.proxyPaths.cold, calldata, txArgs)
const gasEst = await estimateGas(cntx, populatedTx)
Object.assign(populatedTx, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return sendTransaction(cntx, populatedTx);
}

async mintAmbientBase (qty: TokenQty, limits: PriceRange, opts?: CrocLpOpts):
Expand Down Expand Up @@ -190,9 +192,10 @@ export class CrocPoolView {
let cntx = await this.context
if (txArgs === undefined) { txArgs = {} }
await ensureChain(cntx)
const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.liq, calldata, txArgs)
Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return cntx.dex.userCmd(cntx.chain.proxyPaths.liq, calldata, txArgs);
const populatedTx = await cntx.dex.userCmd.populateTransaction(cntx.chain.proxyPaths.liq, calldata, txArgs)
const gasEst = await estimateGas(cntx, populatedTx);
Object.assign(populatedTx, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return sendTransaction(cntx, populatedTx);
}

private async mintAmbient (qty: TokenQty, isQtyBase: boolean,
Expand Down
9 changes: 6 additions & 3 deletions src/recipes/reposition.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { TransactionResponse } from "ethers";
import { ensureChain } from "../context";
import { ensureChain, estimateGas } from "../context";
import { OrderDirective, PoolDirective } from "../encoding/longform";
import { CrocPoolView } from "../pool";
import { CrocSwapPlan } from "../swap";
import { CrocTokenView } from "../tokens";
import { encodeCrocPrice, GAS_PADDING, tickToPrice } from "../utils";
import { baseTokenForConcLiq, concDepositBalance, quoteTokenForConcLiq } from "../utils/liquidity";
import { sendTransaction } from "../vendorEthers";


interface RepositionTarget {
Expand Down Expand Up @@ -37,8 +38,10 @@ export class CrocReposition {
const cntx = await this.pool.context
const path = cntx.chain.proxyPaths.long
await ensureChain(cntx)
const gasEst = await cntx.dex.userCmd.estimateGas(path, directive.encodeBytes())
return cntx.dex.userCmd(path, directive.encodeBytes(), { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
const populatedTx = await cntx.dex.userCmd.populateTransaction(path, directive.encodeBytes())
const gasEst = await estimateGas(cntx, populatedTx)
Object.assign(populatedTx, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return sendTransaction(cntx, populatedTx)
}

async simStatic() {
Expand Down
34 changes: 28 additions & 6 deletions src/swap.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ethers, TransactionResponse, ZeroAddress } from "ethers";
import { MAX_SQRT_PRICE, MIN_SQRT_PRICE } from "./constants";
import { CrocContext, ensureChain } from './context';
import { CrocContext, ensureChain, estimateGas } from './context';
import { CrocSurplusFlags, decodeSurplusFlag, encodeSurplusArg } from "./encoding/flags";
import { CrocPoolView } from './pool';
import { CrocSlotReader } from "./slots";
import { CrocEthView, CrocTokenView, sortBaseQuoteViews, TokenQty } from './tokens';
import { decodeCrocPrice, GAS_PADDING, getUnsignedRawTransaction } from './utils';
import { sendTransaction, staticCall } from "./vendorEthers";

/* Describes the predicted impact of a given swap.
* @property sellQty The total quantity of tokens predicted to be sold by the swapper to the dex.
Expand Down Expand Up @@ -67,15 +68,15 @@ export class CrocSwapPlan {
}

private async sendTx (args: CrocSwapExecOpts): Promise<TransactionResponse> {
return this.hotPathCall(await this.txBase(), 'send', args)
return this.hotPathCall(await this.txBase(), 'send', args) as Promise<TransactionResponse>;
}

private async callStatic (args: CrocSwapExecOpts): Promise<TransactionResponse> {
return this.hotPathCall(await this.txBase(), 'staticCall', args)
return this.hotPathCall(await this.txBase(), 'staticCall', args) as Promise<TransactionResponse>;
}

async estimateGas (args: CrocSwapExecOpts = { }): Promise<bigint> {
return this.hotPathCall(await this.txBase(), 'estimateGas', args)
return this.hotPathCall(await this.txBase(), 'estimateGas', args) as Promise<bigint>;
}

private async txBase() {
Expand Down Expand Up @@ -112,10 +113,21 @@ export class CrocSwapPlan {
const TIP = 0
const surplusFlags = this.maskSurplusArgs(args)

return contract.swap[callType](this.baseToken.tokenAddr, this.quoteToken.tokenAddr, (await this.context).chain.poolIndex,
const populatedTx = await contract.swap.populateTransaction(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, args.gasEst), )

switch (callType) {
case 'estimateGas':
return estimateGas(await this.context, populatedTx);
case 'send':
return sendTransaction(await this.context, populatedTx);
case 'staticCall':
return await staticCall(await this.context, populatedTx, contract, contract.swap.fragment);
default:
throw new Error(`Invalid call type: ${callType}`);
}
}

private async userCmdCall(contract: ethers.Contract, callType: 'send' | 'staticCall' | 'estimateGas', args: CrocSwapExecOpts) {
Expand All @@ -130,7 +142,17 @@ export class CrocSwapPlan {
this.sellBase, this.qtyInBase, await this.qty, TIP,
await this.calcLimitPrice(), await this.calcSlipQty(), surplusFlags])

return contract.userCmd[callType](HOT_PROXY_IDX, cmd, await this.buildTxArgs(surplusFlags, args.gasEst))
const populatedTx = await contract.userCmd.populateTransaction(HOT_PROXY_IDX, cmd, await this.buildTxArgs(surplusFlags, args.gasEst))
switch (callType) {
case 'estimateGas':
return estimateGas(await this.context, populatedTx);
case 'send':
return sendTransaction(await this.context, populatedTx);
case 'staticCall':
return await staticCall(await this.context, populatedTx, contract, contract.userCmd.fragment);
default:
throw new Error(`Invalid call type: ${callType}`);
}
}

/**
Expand Down
32 changes: 17 additions & 15 deletions src/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Contract, ethers, MaxUint256, TransactionResponse, ZeroAddress } from "ethers";
import { MAX_LIQ } from "./constants";
import { CrocContext, ensureChain } from "./context";
import { CrocContext, ensureChain, estimateGas } from "./context";
import { BlockTag } from "./position";
import { GAS_PADDING } from "./utils";
import { fromDisplayQty, toDisplayQty } from "./utils/token";
import { sendTransaction } from "./vendorEthers";

/* Type representing specified token quantities. This type can either represent the raw non-decimalized
* on-chain value in wei, if passed as a BigNuber. Or it can represent the decimalized value if passed
Expand Down Expand Up @@ -50,18 +51,14 @@ export class CrocTokenView {
const weiQty = approveQty !== undefined ? await this.normQty(approveQty) : MaxUint256

await ensureChain(await this.context)
const populatedTx = await (await this.resolveWrite()).approve.populateTransaction(addr, weiQty, { chainId: (await this.context).chain.chainId })
// We want to hardcode the gas limit, so we can manually pad it from the estimated
// transaction. The default value is low gas calldata, but Metamask and other wallets
// will often ask users to change the approval amount. Without the padding, approval
// transactions can run out of gas.
const gasEst = (await this.resolveWrite()).approve.estimateGas(
addr,
weiQty
);

return (await this.resolveWrite()).approve(
addr, weiQty, { gasLimit: (await gasEst) + BigInt(15000), chainId: ((await this.context).chain).chainId }
);
const gasEst = await estimateGas((await this.context), populatedTx);
populatedTx.gasLimit = gasEst + BigInt(15000);
return await sendTransaction(await this.context, populatedTx);
}

async approveBypassRouter(): Promise<TransactionResponse | undefined> {
Expand All @@ -75,9 +72,13 @@ export class CrocTokenView {
const HOT_PROXY_IDX = 1
const COLD_PROXY_IDX = 3
const cmd = abiCoder.encode(["uint8", "address", "uint32", "uint16[]"],
[72, router.address, MANY_CALLS, [HOT_PROXY_IDX]])
[72, router.target, MANY_CALLS, [HOT_PROXY_IDX]])
await ensureChain(await this.context)
return (await this.context).dex.userCmd(COLD_PROXY_IDX, cmd, { chainId: ((await this.context).chain).chainId })
const populatedTx = await (await this.context).dex.userCmd.populateTransaction(
COLD_PROXY_IDX, cmd, { chainId: ((await this.context).chain).chainId })
const gasEst = await estimateGas((await this.context), populatedTx);
populatedTx.gasLimit = gasEst + BigInt(15000);
return sendTransaction(await this.context, populatedTx);
}

async wallet (address: string, block: BlockTag = "latest"): Promise<bigint> {
Expand Down Expand Up @@ -172,12 +173,13 @@ export class CrocTokenView {
const cmd = abiCoder.encode(["uint8", "address", "uint128", "address"],
[subCode, recv, await weiQty, this.tokenAddr])

const txArgs = useMsgVal ? { value: await weiQty } : { }
const txArgs = useMsgVal ? { value: await weiQty } : {}
let cntx = await this.context
await ensureChain(cntx)
const gasEst = await cntx.dex.userCmd.estimateGas(cntx.chain.proxyPaths.cold, cmd, txArgs)
Object.assign(txArgs, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return cntx.dex.userCmd(cntx.chain.proxyPaths.cold, cmd, txArgs)
const populatedTx = await cntx.dex.userCmd.populateTransaction(cntx.chain.proxyPaths.cold, cmd, txArgs)
const gasEst = await estimateGas(cntx, populatedTx)
Object.assign(populatedTx, { gasLimit: gasEst + GAS_PADDING, chainId: cntx.chain.chainId })
return sendTransaction(cntx, populatedTx);
}

readonly tokenAddr: string;
Expand Down
13 changes: 9 additions & 4 deletions src/vaults/tempest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Contract, Signer, TransactionResponse, Typed } from "ethers";
import { TEMPEST_VAULT_ABI } from "../abis/external/TempestVaultAbi";
import { CrocContext, ensureChain } from "../context";
import { CrocTokenView, TokenQty } from "../tokens";
import { sendTransaction } from "../vendorEthers";

export type TempestStrategy = 'rswEth' | 'symetricAmbient'

Expand Down Expand Up @@ -29,9 +30,11 @@ export class TempestVault {
await ensureChain(await this.context)
switch (this.strategy) {
case 'symetricAmbient':
return (await this.vaultWrite).deposit(await weiQty, owner, Typed.bool(true), txArgs)
const populatedTx = await (await this.vaultWrite).deposit.populateTransaction(await weiQty, owner, Typed.bool(true), txArgs);
return sendTransaction(await this.context, populatedTx);
case 'rswEth':
return (await this.vaultWrite).deposit(await weiQty, owner, Typed.bytes('0x'), txArgs)
const populatedTxRsw = await (await this.vaultWrite).deposit.populateTransaction(await weiQty, owner, Typed.bytes('0x'), txArgs);
return sendTransaction(await this.context, populatedTxRsw);
}
}

Expand All @@ -45,9 +48,11 @@ export class TempestVault {
await ensureChain(await this.context)
switch (this.strategy) {
case 'symetricAmbient':
return (await this.vaultWrite).redeem(await weiQty, owner, owner, Typed.uint256(await minWeiQty), Typed.bool(true))
const populatedTx = await (await this.vaultWrite).redeem.populateTransaction(await weiQty, owner, owner, Typed.uint256(await minWeiQty), Typed.bool(true));
return sendTransaction(await this.context, populatedTx);
case 'rswEth':
return (await this.vaultWrite).redeem(await weiQty, owner, owner, Typed.bytes('0x'))
const populatedTxRsw = await (await this.vaultWrite).redeem.populateTransaction(await weiQty, owner, owner, Typed.bytes('0x'));
return sendTransaction(await this.context, populatedTxRsw);
}
}

Expand Down
Loading