Skip to content

Commit

Permalink
Add viem lens callers
Browse files Browse the repository at this point in the history
  • Loading branch information
shuhuiluo committed Dec 5, 2023
1 parent 1c862c0 commit 2de9d45
Show file tree
Hide file tree
Showing 9 changed files with 656 additions and 14 deletions.
12 changes: 9 additions & 3 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
name: Publish package
on:
release:
types: [published]
types: [ published ]
jobs:
npm_publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: '18.x'
registry-url: 'https://registry.npmjs.org'
scope: '@aperture_finance'
- run: yarn install --frozen-lockfile
- run: yarn build

- name: Install dependencies 📦
run: yarn install --frozen-lockfile

- name: Generate Typechain types and build
run: yarn build

- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "aperture-lens",
"version": "0.0.1",
"version": "0.1.0",
"description": "Contains ephemeral lens contracts that can be called without deployment and their interfaces in various Web3 libraries.",
"author": "Aperture Finance <engineering@aperture.finance>",
"license": "Apache-2.0",
Expand Down Expand Up @@ -29,6 +29,8 @@
"node": ">=18"
},
"scripts": {
"build": "yarn typechain && tsc --build",
"clean": "tsc --build --clean && forge clean",
"compile": "forge build",
"test": "forge test",
"test:hardhat": "npx hardhat test",
Expand All @@ -45,6 +47,7 @@
"viem": "^1.19.11"
},
"devDependencies": {
"@aperture_finance/uniswap-v3-automation-sdk": "^1.8.4",
"@ethersproject/abi": "5.7.0",
"@ethersproject/providers": "5.7.2",
"@nomicfoundation/hardhat-foundry": "^1.1.1",
Expand All @@ -54,6 +57,7 @@
"@types/chai": "^4.3.11",
"@types/mocha": "^10.0.6",
"@types/node": "^20.10.0",
"chai": "^4.3.10",
"hardhat": "^2.19.1",
"mocha": "^10.2.0",
"prettier": "^3.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as viem from "./viem";
46 changes: 46 additions & 0 deletions src/viem/caller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Abi, AbiFunction } from "abitype";
import {
CallExecutionError,
ContractFunctionResult,
EncodeDeployDataParameters,
Hex,
PublicClient,
decodeFunctionResult,
encodeDeployData,
} from "viem";

/**
* Deploy an ephemeral contract which reverts data in the constructor via `eth_call`.
* @param deployParams The abi, bytecode, and constructor arguments.
* @param publicClient Viem public client.
* @param blockNumber Optional block number to query.
* @returns The result of the contract function call.
*/
export async function callEphemeralContract<TAbi extends Abi>(
deployParams: EncodeDeployDataParameters<TAbi>,
publicClient: PublicClient,
blockNumber?: bigint,
): Promise<ContractFunctionResult<TAbi>> {
try {
await publicClient.call({
data: encodeDeployData(deployParams),
blockNumber,
});
} catch (error) {
const baseError = (error as CallExecutionError).walk();
if ("data" in baseError) {
const abiFunctions = deployParams.abi.filter((x) => x.type === "function");
if (abiFunctions.length === 1) {
return decodeFunctionResult({
abi: abiFunctions as [AbiFunction],
data: baseError.data as Hex,
}) as ContractFunctionResult<TAbi>;
} else {
throw new Error("abi should contain exactly one function");
}
} else {
throw error;
}
}
throw new Error("deployment should revert");
}
2 changes: 2 additions & 0 deletions src/viem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./caller";
export * from "./poolLens";
182 changes: 182 additions & 0 deletions src/viem/poolLens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { AbiParametersToPrimitiveTypes, ExtractAbiFunction } from "abitype";
import { Address, PublicClient } from "viem";
import {
EphemeralAllPositionsByOwner__factory,
EphemeralPoolPositions__factory,
EphemeralPoolSlots__factory,
EphemeralPoolTickBitmap__factory,
EphemeralPoolTicks__factory,
EphemeralGetPopulatedTicksInRange__factory,
EphemeralGetPosition__factory,
EphemeralGetPositions__factory,
} from "../../typechain";
import { callEphemeralContract } from "./caller";

/**
* Fetches the liquidity within the tick range for the specified pool by deploying an ephemeral contract via `eth_call`.
* Each tick consumes about 100k gas, so this method may fail if the number of ticks exceeds 3k assuming the provider
* gas limit is 300m.
* @param pool The liquidity pool to fetch the tick to liquidity map for.
* @param tickLower The lower tick to fetch liquidity for.
* @param tickUpper The upper tick to fetch liquidity for.
* @param publicClient Viem public client.
* @param blockNumber Optional block number to query.
*/
export async function getPopulatedTicksInRange(
pool: Address,
tickLower: number,
tickUpper: number,
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralGetPopulatedTicksInRange__factory.abi,
bytecode: EphemeralGetPopulatedTicksInRange__factory.bytecode,
args: [pool, tickLower, tickUpper],
},
publicClient,
blockNumber,
);
}

/**
* Get the position details in a single call by deploying an ephemeral contract via `eth_call`
* @param npm Nonfungible position manager address.
* @param positionId Position id.
* @param publicClient Viem public client.
* @param blockNumber Optional block number to query.
* @returns The position details.
*/
export async function getPositionDetails(
npm: Address,
positionId: bigint,
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralGetPosition__factory.abi,
bytecode: EphemeralGetPosition__factory.bytecode,
args: [npm, positionId],
},
publicClient,
blockNumber,
);
}

/**
* Get the state and pool for all positions in a single call by deploying an ephemeral contract via `eth_call`.
* @param npm Nonfungible position manager address.
* @param positionIds Position ids.
* @param publicClient Viem public client.
* @param blockNumber Optional block number to query.
* @returns The position details for all positions.
*/
export async function getPositions(
npm: Address,
positionIds: bigint[],
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralGetPositions__factory.abi,
bytecode: EphemeralGetPositions__factory.bytecode,
args: [npm, positionIds],
},
publicClient,
blockNumber,
);
}

/**
* Get the state and pool for all positions of the specified owner by deploying an ephemeral contract via `eth_call`.
* Each position consumes about 200k gas, so this method may fail if the number of positions exceeds 1500 assuming the
* provider gas limit is 300m.
* @param npm Nonfungible position manager address.
* @param owner The owner of the positions.
* @param publicClient Viem public client.
* @param blockNumber Optional block number to query.
* @returns The position details for all positions of the specified owner.
*/
export async function getAllPositionsByOwner(
npm: Address,
owner: Address,
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralAllPositionsByOwner__factory.abi,
bytecode: EphemeralAllPositionsByOwner__factory.bytecode,
args: [npm, owner],
},
publicClient,
blockNumber,
);
}

export async function getStaticSlots(pool: Address, publicClient: PublicClient, blockNumber?: bigint) {
return await callEphemeralContract(
{
abi: EphemeralPoolSlots__factory.abi,
bytecode: EphemeralPoolSlots__factory.bytecode,
args: [pool],
},
publicClient,
blockNumber,
);
}

export async function getTicksSlots(
pool: Address,
tickLower: number,
tickUpper: number,
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralPoolTicks__factory.abi,
bytecode: EphemeralPoolTicks__factory.bytecode,
args: [pool, tickLower, tickUpper],
},
publicClient,
blockNumber,
);
}

export async function getTickBitmapSlots(pool: Address, publicClient: PublicClient, blockNumber?: bigint) {
return await callEphemeralContract(
{
abi: EphemeralPoolTickBitmap__factory.abi,
bytecode: EphemeralPoolTickBitmap__factory.bytecode,
args: [pool],
},
publicClient,
blockNumber,
);
}

export type PositionKey = AbiParametersToPrimitiveTypes<
ExtractAbiFunction<typeof EphemeralPoolPositions__factory.abi, "getPositions">["inputs"],
"inputs"
>[1][0];

export async function getPositionsSlots(
pool: Address,
keys: PositionKey[],
publicClient: PublicClient,
blockNumber?: bigint,
) {
return await callEphemeralContract(
{
abi: EphemeralPoolPositions__factory.abi,
bytecode: EphemeralPoolPositions__factory.bytecode,
args: [pool, keys],
},
publicClient,
blockNumber,
);
}
73 changes: 73 additions & 0 deletions test/hardhat/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ApertureSupportedChainId, getChainInfo, viem } from "@aperture_finance/uniswap-v3-automation-sdk";
import { TickMath } from "@uniswap/v3-sdk";
import { expect } from "chai";
import { config as dotenvConfig } from "dotenv";
import { createPublicClient, getContract, http, toHex } from "viem";
import { getTicksSlots, getPositionsSlots, getStaticSlots, getTickBitmapSlots } from "../../src/viem/poolLens";
import { IUniswapV3Pool__factory } from "../../typechain";

dotenvConfig();

const chainId = ApertureSupportedChainId.ETHEREUM_MAINNET_CHAIN_ID;
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const WETH_ADDRESS = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2";

describe("Pool lens test", () => {
const { chain, uniswap_v3_factory } = getChainInfo(chainId);
const publicClient = createPublicClient({
chain,
transport: http(`https://mainnet.infura.io/v3/${process.env.INFURA_API_KEY}`),
batch: {
multicall: true,
},
});
const blockNumber = 17000000n;
const pool = viem.computePoolAddress(uniswap_v3_factory, USDC_ADDRESS, WETH_ADDRESS, 500);
const poolContract = getContract({
address: pool,
abi: IUniswapV3Pool__factory.abi,
publicClient,
});

it("Test getting static storage slots", async () => {
const slots = await getStaticSlots(pool, publicClient, blockNumber);
expect(slots.some(({ data }) => data > 0)).to.be.true;
const address = pool;
const altSlots = await Promise.all([
publicClient.getStorageAt({ address, slot: toHex(0), blockNumber }),
publicClient.getStorageAt({ address, slot: toHex(1), blockNumber }),
publicClient.getStorageAt({ address, slot: toHex(2), blockNumber }),
publicClient.getStorageAt({ address, slot: toHex(3), blockNumber }),
]);
for (let i = 0; i < 4; i++) {
expect(slots[i].data).to.be.eq(BigInt(altSlots[i]!));
}
});

it("Test getting populated ticks slots", async () => {
const slots = await getTicksSlots(pool, TickMath.MIN_TICK, TickMath.MAX_TICK, publicClient, blockNumber);
expect(slots.some(({ data }) => data > 0)).to.be.true;
});

it("Test getting tick bitmap slots", async () => {
const slots = await getTickBitmapSlots(pool, publicClient, blockNumber);
expect(slots.some(({ data }) => data > 0)).to.be.true;
});

it("Test getting positions mapping slots", async () => {
const logs = await poolContract.getEvents.Mint(
{},
{
fromBlock: blockNumber - 10000n,
toBlock: blockNumber,
},
);
const positions = logs.map(({ args: { owner, tickLower, tickUpper } }) => ({
owner: owner!,
tickLower: tickLower!,
tickUpper: tickUpper!,
}));
const slots = await getPositionsSlots(pool, positions, publicClient, blockNumber);
expect(slots.some(({ data }) => data > 0)).to.be.true;
});
});
9 changes: 5 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
"resolveJsonModule": true,
"outDir": "dist",
"declaration": true,
"declarationDir": "dist"
},
"include": [
"./src",
"./typechain"
],
"files": [
"./hardhat.config.ts"
]
}
Loading

0 comments on commit 2de9d45

Please sign in to comment.