From dc8d7bcb8dad7f293f685019ce3d425c8a088d60 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Mon, 17 Jun 2024 21:20:27 -0700 Subject: [PATCH 01/15] wip activity rework --- schema.graphql | 50 +++- schema/otoken.graphql | 44 ++- scripts/parse-tx-activity.ts | 25 -- src/mainnet/processors/uniswap.ts | 10 +- src/model/generated/_oTokenActivityType.ts | 13 + src/model/generated/index.ts | 1 + src/model/generated/oTokenActivity.model.ts | 32 +-- .../daily-stats/daily-stats.ts | 37 +-- src/oeth/processors/ccip.ts | 53 +--- src/oeth/processors/curve-lp.ts | 71 +---- src/oeth/processors/oeth.ts | 52 +++- src/ousd/processors/ousd/ousd.ts | 22 +- src/templates/otoken/activity-processor.ts | 183 ++++++++++++ src/templates/otoken/activity-types.ts | 107 +++++++ src/templates/otoken/otoken.ts | 265 ++++-------------- src/utils/activityFromTx.ts | 125 +++------ src/utils/addresses.ts | 11 +- src/utils/logFilter.ts | 9 +- 18 files changed, 569 insertions(+), 541 deletions(-) delete mode 100644 scripts/parse-tx-activity.ts create mode 100644 src/model/generated/_oTokenActivityType.ts create mode 100644 src/templates/otoken/activity-processor.ts create mode 100644 src/templates/otoken/activity-types.ts diff --git a/schema.graphql b/schema.graphql index 729b0b4e..7b4ef35a 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1071,18 +1071,44 @@ type OTokenActivity @entity { timestamp: DateTime! @index blockNumber: Int! @index txHash: String! @index - callDataLast4Bytes: String! - address: String - sighash: String - - action: String - exchange: String - interface: String - - fromSymbol: String - toSymbol: String - amount: BigInt -} + type: OTokenActivityType + data: JSON +} + + +enum OTokenActivityType { + Approval + Bridge + ClaimRewards + DelegateVote + ExtendStake + Migrate + Redeem + Stake + Swap + Unstake + Vote +} + +#type OTokenActivity @entity { +# id: ID! +# chainId: Int! @index +# otoken: String! @index +# timestamp: DateTime! @index +# blockNumber: Int! @index +# txHash: String! @index +# callDataLast4Bytes: String! +# address: String +# sighash: String +# +# action: String +# exchange: String +# interface: String +# +# fromSymbol: String +# toSymbol: String +# amount: BigInt +#} """ The Vault entity tracks the OUSD vault balance over time. """ diff --git a/schema/otoken.graphql b/schema/otoken.graphql index 16202b71..e82f2a1c 100644 --- a/schema/otoken.graphql +++ b/schema/otoken.graphql @@ -105,15 +105,41 @@ type OTokenActivity @entity { timestamp: DateTime! @index blockNumber: Int! @index txHash: String! @index - callDataLast4Bytes: String! - address: String - sighash: String + type: OTokenActivityType + data: JSON +} - action: String - exchange: String - interface: String - fromSymbol: String - toSymbol: String - amount: BigInt +enum OTokenActivityType { + Approval + Bridge + ClaimRewards + DelegateVote + ExtendStake + Migrate + Redeem + Stake + Swap + Unstake + Vote } + +#type OTokenActivity @entity { +# id: ID! +# chainId: Int! @index +# otoken: String! @index +# timestamp: DateTime! @index +# blockNumber: Int! @index +# txHash: String! @index +# callDataLast4Bytes: String! +# address: String +# sighash: String +# +# action: String +# exchange: String +# interface: String +# +# fromSymbol: String +# toSymbol: String +# amount: BigInt +#} diff --git a/scripts/parse-tx-activity.ts b/scripts/parse-tx-activity.ts deleted file mode 100644 index 3078b142..00000000 --- a/scripts/parse-tx-activity.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * This script takes a transaction hash and parses the activity. Useful for - * debugging the activity parser without having to run Squid. - */ -import { createPublicClient, http } from 'viem' -import { mainnet } from 'viem/chains' - -import { type Transaction, activityFromTx } from '../src/utils/activityFromTx' - -const client = createPublicClient({ - chain: mainnet, - transport: http(process.env.RPC_ENDPOINT), -}) - -async function go(hash: `0x${string}`) { - const transaction = await client.getTransaction({ hash }) - const receipt = await client.getTransactionReceipt({ hash }) - const activity = await activityFromTx( - transaction as Transaction, - receipt.logs, - ) - console.log(activity) -} - -go('0xd3705c324d3e59545a21eb9773108cbb266631251484ace5016b59c25719a02a') diff --git a/src/mainnet/processors/uniswap.ts b/src/mainnet/processors/uniswap.ts index 3c7d2ccd..1e0ae55b 100644 --- a/src/mainnet/processors/uniswap.ts +++ b/src/mainnet/processors/uniswap.ts @@ -1,5 +1,5 @@ import { LiquiditySourceType } from '@model' -import { addresses } from '@utils/addresses' +import { UNISWAP_OETH_WEH_ADDRESS, addresses } from '@utils/addresses' import { addERC20Processing } from './erc20s' import { registerLiquiditySource } from './liquidity-sources' @@ -18,7 +18,7 @@ const pools = [ tokens: ['wstETH', 'WETH'], }, { - address: '0x52299416c469843f4e0d54688099966a6c7d720f', + address: UNISWAP_OETH_WEH_ADDRESS, tokens: ['OETH', 'WETH'], }, ] as const @@ -26,11 +26,7 @@ const pools = [ export const initialize = () => { for (const pool of pools) { for (const token of pool.tokens) { - registerLiquiditySource( - pool.address, - LiquiditySourceType.UniswapPool, - addresses.tokens[token], - ) + registerLiquiditySource(pool.address, LiquiditySourceType.UniswapPool, addresses.tokens[token]) addERC20Processing(token, pool.address) } } diff --git a/src/model/generated/_oTokenActivityType.ts b/src/model/generated/_oTokenActivityType.ts new file mode 100644 index 00000000..c0dc473f --- /dev/null +++ b/src/model/generated/_oTokenActivityType.ts @@ -0,0 +1,13 @@ +export enum OTokenActivityType { + Approval = "Approval", + Bridge = "Bridge", + ClaimRewards = "ClaimRewards", + DelegateVote = "DelegateVote", + ExtendStake = "ExtendStake", + Migrate = "Migrate", + Redeem = "Redeem", + Stake = "Stake", + Swap = "Swap", + Unstake = "Unstake", + Vote = "Vote", +} diff --git a/src/model/generated/index.ts b/src/model/generated/index.ts index 3b92e2a8..8fd8f0e5 100644 --- a/src/model/generated/index.ts +++ b/src/model/generated/index.ts @@ -93,6 +93,7 @@ export * from "./oTokenRebaseOption.model" export * from "./oTokenApy.model" export * from "./oTokenVault.model" export * from "./oTokenActivity.model" +export * from "./_oTokenActivityType" export * from "./ousdVault.model" export * from "./ousdMorphoAave.model" export * from "./ousdMorphoCompound.model" diff --git a/src/model/generated/oTokenActivity.model.ts b/src/model/generated/oTokenActivity.model.ts index 47426ab0..e4557e69 100644 --- a/src/model/generated/oTokenActivity.model.ts +++ b/src/model/generated/oTokenActivity.model.ts @@ -1,4 +1,5 @@ -import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, IntColumn as IntColumn_, Index as Index_, StringColumn as StringColumn_, DateTimeColumn as DateTimeColumn_, BigIntColumn as BigIntColumn_} from "@subsquid/typeorm-store" +import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, IntColumn as IntColumn_, Index as Index_, StringColumn as StringColumn_, DateTimeColumn as DateTimeColumn_, JSONColumn as JSONColumn_} from "@subsquid/typeorm-store" +import {OTokenActivityType} from "./_oTokenActivityType" @Entity_() export class OTokenActivity { @@ -29,30 +30,9 @@ export class OTokenActivity { @StringColumn_({nullable: false}) txHash!: string - @StringColumn_({nullable: false}) - callDataLast4Bytes!: string - - @StringColumn_({nullable: true}) - address!: string | undefined | null - - @StringColumn_({nullable: true}) - sighash!: string | undefined | null - - @StringColumn_({nullable: true}) - action!: string | undefined | null - - @StringColumn_({nullable: true}) - exchange!: string | undefined | null - - @StringColumn_({nullable: true}) - interface!: string | undefined | null - - @StringColumn_({nullable: true}) - fromSymbol!: string | undefined | null - - @StringColumn_({nullable: true}) - toSymbol!: string | undefined | null + @Column_("varchar", {length: 12, nullable: true}) + type!: OTokenActivityType | undefined | null - @BigIntColumn_({nullable: true}) - amount!: bigint | undefined | null + @JSONColumn_({nullable: true}) + data!: unknown | undefined | null } diff --git a/src/oeth/post-processors/daily-stats/daily-stats.ts b/src/oeth/post-processors/daily-stats/daily-stats.ts index 39a54ad1..bd56701a 100644 --- a/src/oeth/post-processors/daily-stats/daily-stats.ts +++ b/src/oeth/post-processors/daily-stats/daily-stats.ts @@ -1,11 +1,6 @@ import dayjs from 'dayjs' import utc from 'dayjs/plugin/utc' -import { - EntityManager, - FindOptionsOrderValue, - LessThanOrEqual, - MoreThanOrEqual, -} from 'typeorm' +import { EntityManager, FindOptionsOrderValue, LessThanOrEqual, MoreThanOrEqual } from 'typeorm' import { ExchangeRate, @@ -26,7 +21,7 @@ import { } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' -import { OETH_ADDRESS } from '@utils/addresses' +import { OETH_ADDRESS, WOETH_ADDRESS } from '@utils/addresses' import { applyCoingeckoData } from '@utils/coingecko' dayjs.extend(utc) @@ -38,8 +33,7 @@ export const setup = async (processor: EvmBatchProcessor) => { } export const process = async (ctx: Context) => { - const firstBlockTimestamp = ctx.blocks.find((b) => b.header.height >= from) - ?.header.timestamp + const firstBlockTimestamp = ctx.blocks.find((b) => b.header.height >= from)?.header.timestamp if (!firstBlockTimestamp) return const firstBlock = ctx.blocks[0] @@ -48,11 +42,7 @@ export const process = async (ctx: Context) => { const endDate = dayjs.utc(lastBlock.header.timestamp).endOf('day') let dates: Date[] = [] - for ( - let date = startDate; - !date.isAfter(endDate); - date = date.add(1, 'day').endOf('day') - ) { + for (let date = startDate; !date.isAfter(endDate); date = date.add(1, 'day').endOf('day')) { // ctx.log.info({ date, startDate, endDate }) dates.push(date.toDate()) } @@ -80,9 +70,7 @@ export const process = async (ctx: Context) => { vsCurrency: 'eth', })) as OETHDailyStat[] const existingIds = dailyStats.map((stat) => stat.id) - dailyStats.push( - ...updatedStats.filter((stat) => existingIds.indexOf(stat.id) < 0), - ) + dailyStats.push(...updatedStats.filter((stat) => existingIds.indexOf(stat.id) < 0)) } await ctx.store.upsert(dailyStats) @@ -142,7 +130,7 @@ async function updateDailyStats(ctx: Context, date: Date) { chainId: ctx.chain.id, otoken: OETH_ADDRESS, timestamp: LessThanOrEqual(date), - address: { address: '0xdcee70654261af21c44c093c300ed3bb97b78192' }, + address: { address: WOETH_ADDRESS }, }, order: { timestamp: 'desc' as FindOptionsOrderValue }, }), @@ -238,10 +226,7 @@ async function updateDailyStats(ctx: Context, date: Date) { // Collateral totals const ETH = lastCurve?.ethOwned || 0n const OETHOwned = lastCurve?.oethOwned || 0n - const WETH = - (lastVault?.weth || 0n) + - (lastMorpho?.weth || 0n) + - (lastBalancer?.weth || 0n) + const WETH = (lastVault?.weth || 0n) + (lastMorpho?.weth || 0n) + (lastBalancer?.weth || 0n) const stETH = lastVault?.stETH || 0n const rETHRaw = (lastVault?.rETH || 0n) + (lastBalancer?.rETH || 0n) @@ -250,15 +235,11 @@ async function updateDailyStats(ctx: Context, date: Date) { const sfrxEthExchangeRate = lastSfrxEthRate?.rate || 1000000000000000000n const sfrxETH = lastFrax?.sfrxETH || 0n - const convertedSfrxEth = - (sfrxETH * sfrxEthExchangeRate) / 1000000000000000000n + const convertedSfrxEth = (sfrxETH * sfrxEthExchangeRate) / 1000000000000000000n // Strategy totals const vaultTotal = - (lastVault?.frxETH || 0n) + - (lastVault?.weth || 0n) + - (lastVault?.stETH || 0n) + - (lastVault?.rETH || 0n) + (lastVault?.frxETH || 0n) + (lastVault?.weth || 0n) + (lastVault?.stETH || 0n) + (lastVault?.rETH || 0n) const balancerTotal = (lastBalancer?.weth || 0n) + (lastBalancer?.rETH || 0n) diff --git a/src/oeth/processors/ccip.ts b/src/oeth/processors/ccip.ts index e267be4e..588d79c2 100644 --- a/src/oeth/processors/ccip.ts +++ b/src/oeth/processors/ccip.ts @@ -5,6 +5,7 @@ import * as erc20Abi from '@abi/erc20' import { BridgeTransfer, BridgeTransferState } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { WOETH_ADDRESS, WOETH_ARBITRUM_ADDRESS } from '@utils/addresses' import { logFilter } from '@utils/logFilter' import { traceFilter } from '@utils/traceFilter' @@ -36,10 +37,9 @@ const ccipConfig = { offRampAddresses: { '42161': '0xefc4a18af59398ff23bfe7325f2401ad44286f4d', }, - tokens: ['0xdcee70654261af21c44c093c300ed3bb97b78192'], + tokens: [WOETH_ADDRESS], tokenMappings: { - '0xdcee70654261af21c44c093c300ed3bb97b78192': - '0xd8724322f44e5c58d7a815f542036fb17dbbf839', // wOETH + [WOETH_ADDRESS]: WOETH_ARBITRUM_ADDRESS, } as Record, }, '42161': { @@ -51,24 +51,16 @@ const ccipConfig = { offRampAddresses: { '1': '0x542ba1902044069330e8c5b36a84ec503863722f', }, - tokens: ['0xd8724322f44e5c58d7a815f542036fb17dbbf839'], + tokens: [WOETH_ARBITRUM_ADDRESS], tokenMappings: { - '0xd8724322f44e5c58d7a815f542036fb17dbbf839': - '0xdcee70654261af21c44c093c300ed3bb97b78192', // wOETH + [WOETH_ARBITRUM_ADDRESS]: WOETH_ADDRESS, } as Record, }, } export const ccip = (params: { chainId: 1 | 42161 }) => { - const { - from, - tokens, - tokenMappings, - tokenPoolAddress, - ccipRouterAddress, - onRampAddress, - offRampAddresses, - } = ccipConfig[params.chainId] + const { from, tokens, tokenMappings, tokenPoolAddress, ccipRouterAddress, onRampAddress, offRampAddresses } = + ccipConfig[params.chainId] const transfersToLockReleasePool = logFilter({ address: tokens, topic0: [erc20Abi.events.Transfer.topic], @@ -136,33 +128,19 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { if (transfersToLockReleasePool.matches(log)) { // console.log('match transfersToOnramp') const logSendRequested = block.logs.find( - (l) => - log.transactionHash === l.transactionHash && - ccipSendRequested.matches(l), + (l) => log.transactionHash === l.transactionHash && ccipSendRequested.matches(l), ) const traceSendRequested = block.traces.find( - (t) => - log.transactionHash === t.transaction?.hash && - ccipSendFunction.matches(t), + (t) => log.transactionHash === t.transaction?.hash && ccipSendFunction.matches(t), ) - if ( - logSendRequested && - traceSendRequested && - traceSendRequested.type === 'call' - ) { + if (logSendRequested && traceSendRequested && traceSendRequested.type === 'call') { // console.log('match ccipSendRequested') - const logData = - ccipOnRampAbi.events.CCIPSendRequested.decode(logSendRequested) + const logData = ccipOnRampAbi.events.CCIPSendRequested.decode(logSendRequested) const message = logData.message - const functionData = ccipRouter.functions.ccipSend.decode( - traceSendRequested.action.input, - ) + const functionData = ccipRouter.functions.ccipSend.decode(traceSendRequested.action.input) // A `BridgeTransferState` may already exist. // If so, we should pull the `state` for it. - const bridgeTransferState = await ctx.store.get( - BridgeTransferState, - message.messageId, - ) + const bridgeTransferState = await ctx.store.get(BridgeTransferState, message.messageId) for (let i = 0; i < message.tokenAmounts.length; i++) { const tokenAmount = message.tokenAmounts[i] @@ -175,10 +153,7 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { messageId: message.messageId, bridge: 'ccip', chainIn: params.chainId, - chainOut: - chainSelectorIdMappings[ - functionData.destinationChainSelector.toString() - ], + chainOut: chainSelectorIdMappings[functionData.destinationChainSelector.toString()], tokenIn: tokenAmount.token.toLowerCase(), tokenOut: tokenMappings[tokenAmount.token.toLowerCase()], amountIn: tokenAmount.amount, diff --git a/src/oeth/processors/curve-lp.ts b/src/oeth/processors/curve-lp.ts index 019ea1d9..7396fbe4 100644 --- a/src/oeth/processors/curve-lp.ts +++ b/src/oeth/processors/curve-lp.ts @@ -5,11 +5,7 @@ import * as curveLpToken from '@abi/curve-lp-token' import { OETHCurveLP } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' -import { - OETH_CONVEX_ADDRESS, - OETH_CURVE_LP_ADDRESS, - OETH_CURVE_REWARD_LP_ADDRESS, -} from '@utils/addresses' +import { CURVE_ETH_OETH_POOL_ADDRESS, OETH_CONVEX_ADDRESS, OETH_CURVE_REWARD_LP_ADDRESS } from '@utils/addresses' import { getLatestEntity } from '@utils/utils' interface ProcessResult { @@ -23,7 +19,7 @@ export const from = Math.min( export const setup = (processor: EvmBatchProcessor) => { processor.addLog({ - address: [OETH_CURVE_LP_ADDRESS], + address: [CURVE_ETH_OETH_POOL_ADDRESS], topic0: [ curveLpToken.events.AddLiquidity.topic, curveLpToken.events.RemoveLiquidity.topic, @@ -35,10 +31,7 @@ export const setup = (processor: EvmBatchProcessor) => { }) processor.addLog({ address: [OETH_CURVE_REWARD_LP_ADDRESS], - topic0: [ - baseRewardPool.events.Staked.topic, - baseRewardPool.events.Withdrawn.topic, - ], + topic0: [baseRewardPool.events.Staked.topic, baseRewardPool.events.Withdrawn.topic], topic1: [pad(OETH_CONVEX_ADDRESS)], range: { from }, }) @@ -50,9 +43,7 @@ export const process = async (ctx: Context) => { } for (const block of ctx.blocks) { - const haveCurveLpEvent = block.logs.find( - (log) => log.address === OETH_CURVE_LP_ADDRESS, - ) + const haveCurveLpEvent = block.logs.find((log) => log.address === CURVE_ETH_OETH_POOL_ADDRESS) if (haveCurveLpEvent) { await updateCurveValues(ctx, result, block) } @@ -66,30 +57,15 @@ export const process = async (ctx: Context) => { await ctx.store.insert(result.curveLPs) } -const updateCurveValues = async ( - ctx: Context, - result: ProcessResult, - block: Context['blocks']['0'], -) => { +const updateCurveValues = async (ctx: Context, result: ProcessResult, block: Context['blocks']['0']) => { const { curveLP } = await getLatestCurveLP(ctx, result, block) - const poolContract = new curveLpToken.Contract( - ctx, - block.header, - OETH_CURVE_LP_ADDRESS, - ) - const [totalSupply, balances] = await Promise.all([ - poolContract.totalSupply(), - poolContract.get_balances(), - ]) + const poolContract = new curveLpToken.Contract(ctx, block.header, CURVE_ETH_OETH_POOL_ADDRESS) + const [totalSupply, balances] = await Promise.all([poolContract.totalSupply(), poolContract.get_balances()]) curveLP.totalSupply = totalSupply curveLP.eth = balances[0] curveLP.oeth = balances[1] - curveLP.ethOwned = curveLP.totalSupply - ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n - curveLP.oethOwned = curveLP.totalSupply - ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n + curveLP.ethOwned = curveLP.totalSupply ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n + curveLP.oethOwned = curveLP.totalSupply ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n } const processCurveRewardEvents = async ( @@ -103,37 +79,20 @@ const processCurveRewardEvents = async ( const { amount } = baseRewardPool.events.Staked.decode(log) const { curveLP } = await getLatestCurveLP(ctx, result, block) curveLP.totalSupplyOwned += amount - curveLP.ethOwned = curveLP.totalSupply - ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n - curveLP.oethOwned = curveLP.totalSupply - ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n + curveLP.ethOwned = curveLP.totalSupply ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n + curveLP.oethOwned = curveLP.totalSupply ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n } else if (log.topics[0] === baseRewardPool.events.Withdrawn.topic) { const { amount } = baseRewardPool.events.Withdrawn.decode(log) const { curveLP } = await getLatestCurveLP(ctx, result, block) curveLP.totalSupplyOwned -= amount - curveLP.ethOwned = curveLP.totalSupply - ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n - curveLP.oethOwned = curveLP.totalSupply - ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply - : 0n + curveLP.ethOwned = curveLP.totalSupply ? (curveLP.eth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n + curveLP.oethOwned = curveLP.totalSupply ? (curveLP.oeth * curveLP.totalSupplyOwned) / curveLP.totalSupply : 0n } } -const getLatestCurveLP = async ( - ctx: Context, - result: ProcessResult, - block: Context['blocks']['0'], -) => { +const getLatestCurveLP = async (ctx: Context, result: ProcessResult, block: Context['blocks']['0']) => { const timestampId = new Date(block.header.timestamp).toISOString() - const { latest, current } = await getLatestEntity( - ctx, - OETHCurveLP, - result.curveLPs, - timestampId, - ) + const { latest, current } = await getLatestEntity(ctx, OETHCurveLP, result.curveLPs, timestampId) let isNew = false let curveLP = current diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 61a99ac7..246bc486 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -1,5 +1,10 @@ -import { createOTokenProcessor, createOTokenSetup } from '@templates/otoken' +import { Context } from '@processor' +import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { createOTokenProcessor } from '@templates/otoken' +import { createOTokenActivityProcessor } from '@templates/otoken/activity-processor' import { + CURVE_ETH_OETH_POOL_ADDRESS, + CURVE_FRXETH_OETH_POOL_ADDRESS, ETH_ADDRESS, FRXETH_ADDRESS, OETH_ADDRESS, @@ -12,20 +17,8 @@ import { WSTETH_ADDRESS, } from '@utils/addresses' -export const from = 16933090 // https://etherscan.io/tx/0x3b4ece4f5fef04bf7ceaec4f6c6edf700540d7597589f8da0e3a8c94264a3b50 - -export const setup = createOTokenSetup({ - address: OETH_ADDRESS, - wrappedAddress: WOETH_ADDRESS, - vaultAddress: OETH_VAULT_ADDRESS, - from, - upgrades: { - rebaseOptEvents: 18872285, - }, -}) - -export const process = createOTokenProcessor({ - from, +const otokenProcessor = createOTokenProcessor({ + from: 16933090, // https://etherscan.io/tx/0x3b4ece4f5fef04bf7ceaec4f6c6edf700540d7597589f8da0e3a8c94264a3b50 vaultFrom: 17084107, otokenAddress: OETH_ADDRESS, wotokenAddress: WOETH_ADDRESS, @@ -40,4 +33,33 @@ export const process = createOTokenProcessor({ { asset: WSTETH_ADDRESS, symbol: 'wstETH' }, { asset: OETH_ADDRESS, symbol: 'OETH' }, ], + upgrades: { + rebaseOptEvents: 18872285, + }, }) + +const otokenActivityProcessor = createOTokenActivityProcessor({ + from: 16933090, + otokenAddress: OETH_ADDRESS, + wotokenAddress: WOETH_ADDRESS, + curvePools: [ + { + address: CURVE_ETH_OETH_POOL_ADDRESS, + tokens: [ETH_ADDRESS, OETH_ADDRESS], + }, + { + address: CURVE_FRXETH_OETH_POOL_ADDRESS, + tokens: [FRXETH_ADDRESS, OETH_ADDRESS], + }, + ], + balancerPools: ['0x7056c8dfa8182859ed0d4fb0ef0886fdf3d2edcf000200000000000000000623'], +}) + +export const from = Math.min(otokenProcessor.from, otokenActivityProcessor.from) +export const setup = (processor: EvmBatchProcessor) => { + otokenProcessor.setup(processor) + otokenActivityProcessor.setup(processor) +} +export const process = async (ctx: Context) => { + await Promise.all([otokenProcessor.process(ctx), otokenActivityProcessor.process(ctx)]) +} diff --git a/src/ousd/processors/ousd/ousd.ts b/src/ousd/processors/ousd/ousd.ts index 3aed8c93..ff3058bf 100644 --- a/src/ousd/processors/ousd/ousd.ts +++ b/src/ousd/processors/ousd/ousd.ts @@ -1,24 +1,12 @@ -import { createOTokenProcessor, createOTokenSetup } from '@templates/otoken' -import { - DAI_ADDRESS, - OUSD_ADDRESS, - OUSD_VAULT_ADDRESS, - USDC_ADDRESS, - USDT_ADDRESS, -} from '@utils/addresses' +import { createOTokenProcessor } from '@templates/otoken' +import { DAI_ADDRESS, OUSD_ADDRESS, OUSD_VAULT_ADDRESS, USDC_ADDRESS, USDT_ADDRESS } from '@utils/addresses' // export const from = 10884563 // https://etherscan.io/tx/0x9141921f5ebf072e58c00fe56332b6bee0c02f0ae4f54c42999b8a3a88662681 -export const from = 11585978 // OUSDReset +// export const from = 11585978 // OUSDReset // export const from = 13533937 // https://etherscan.io/tx/0xc9b6fc6a4fad18dad197ff7d0636f74bf066671d75656849a1c45122e00d54cf -export const setup = createOTokenSetup({ - address: OUSD_ADDRESS, - vaultAddress: OUSD_VAULT_ADDRESS, - from, -}) - -export const process = createOTokenProcessor({ - from, +export const { from, setup, process } = createOTokenProcessor({ + from: 11585978, // OUSDReset vaultFrom: 11596942, Upgrade_CreditsBalanceOfHighRes: 13533937, // https://etherscan.io/tx/0xc9b6fc6a4fad18dad197ff7d0636f74bf066671d75656849a1c45122e00d54cf otokenAddress: OUSD_ADDRESS, diff --git a/src/templates/otoken/activity-processor.ts b/src/templates/otoken/activity-processor.ts new file mode 100644 index 00000000..e31ae848 --- /dev/null +++ b/src/templates/otoken/activity-processor.ts @@ -0,0 +1,183 @@ +import { groupBy, keyBy, wrap } from 'lodash' + +import * as balancerVault from '@abi/balancer-vault' +import * as curveLp from '@abi/curve-lp-token' +import * as woeth from '@abi/woeth' +import { OTokenActivity, OTokenActivityType } from '@model' +import { Block, Context, Log } from '@processor' +import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { Activity, SwapActivity } from '@templates/otoken/activity-types' +import { BALANCER_VAULT_ADDRESS } from '@utils/addresses' +import { LogFilter, logFilter } from '@utils/logFilter' + +interface Input { + from: number + otokenAddress: string + wotokenAddress?: string + curvePools: { + address: string + tokens: string[] + }[] + balancerPools: string[] +} + +export interface ActivityProcessor { + name: string + filters: LogFilter[] + process: (ctx: Context, block: Block, logs: Log[]) => Promise +} + +export const createOTokenActivityProcessor = (params: Input) => { + const processors: ActivityProcessor[] = [ + ...params.curvePools.map((pool) => curveActivityProcessor(pool)), + balancerActivityProcessor({ pools: params.balancerPools }), + ] + + const from = params.from + const setup = (processor: EvmBatchProcessor) => { + for (const p of processors) { + for (const filter of p.filters) { + processor.addLog(filter.value) + } + } + } + const process = async (ctx: Context) => { + const activities: Activity[] = [] + for (const block of ctx.blocks) { + const transactions = groupBy(block.logs, (l) => l.transactionHash) + for (const logs of Object.values(transactions)) { + for (const p of processors) { + for (const filter of p.filters) { + if (logs.find((log) => filter.matches(log))) { + const results = await p.process(ctx, block, logs) + activities.push(...results) + } + } + } + } + } + await ctx.store.insert( + activities.map( + (activity) => + new OTokenActivity({ + id: activity.id, + chainId: activity.chainId, + type: OTokenActivityType[activity.type], + txHash: activity.txHash, + blockNumber: activity.blockNumber, + timestamp: new Date(activity.timestamp), + otoken: params.otokenAddress, + data: activity, + }), + ), + ) + } + + return { from, setup, process } +} + +const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: string[] }): ActivityProcessor => { + return { + name: 'Curve Pool Processor', + filters: [ + logFilter({ + address: [address], + topic0: [curveLp.events.TokenExchange.topic], + }), + ], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [tokenExchangeFilter] = this.filters + return logs + .filter((l) => tokenExchangeFilter.matches(l)) + .map((log) => { + const tokenExchange = curveLp.events.TokenExchange.decode(log) + return { + id: `${ctx.chain.id}:${log.id}`, + chainId: ctx.chain.id, + type: 'Swap', + exchange: 'Curve', + blockNumber: block.header.height, + timestamp: block.header.timestamp, + status: 'success', + pool: log.address, + txHash: log.transactionHash, + tokenIn: tokens[Number(tokenExchange.sold_id)], + tokenOut: tokens[Number(tokenExchange.bought_id)], + amountIn: tokenExchange.tokens_sold, + amountOut: tokenExchange.tokens_bought, + } + }) + }, + } +} + +const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProcessor => { + return { + name: 'Balancer Pool Processor', + filters: [ + logFilter({ + address: [BALANCER_VAULT_ADDRESS], + topic0: [balancerVault.events.Swap.topic], + topic1: pools, + }), + ], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [swapFilter] = this.filters + return logs + .filter((l) => swapFilter.matches(l)) + .map((log) => { + const swap = balancerVault.events.Swap.decode(log) + return { + id: `${ctx.chain.id}:${log.id}`, + chainId: ctx.chain.id, + type: 'Swap', + exchange: 'Balancer', + blockNumber: block.header.height, + timestamp: block.header.timestamp, + status: 'success', + txHash: log.transactionHash, + pool: swap.poolId, + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + amountIn: swap.amountIn, + amountOut: swap.amountOut, + } + }) + }, + } +} + +const wrappedActivityProcessor = (wrappedAddress: string) => { + return { + name: 'Wrapped Processor', + filters: [ + logFilter({ + address: [wrappedAddress], + topic0: [woeth.events.Deposit.topic], + }), + ], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [depositFilter] = this.filters + return logs + .filter((l) => depositFilter.matches(l)) + .map((log) => { + const swap = balancerVault.events.Swap.decode(log) + return { + id: `${ctx.chain.id}:${log.id}`, + chainId: ctx.chain.id, + type: 'Swap', + exchange: 'Balancer', + blockNumber: block.header.height, + timestamp: block.header.timestamp, + status: 'success', + txHash: log.transactionHash, + pool: swap.poolId, + tokenIn: swap.tokenIn, + tokenOut: swap.tokenOut, + amountIn: swap.amountIn, + amountOut: swap.amountOut, + } + }) + }, + } +} diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts new file mode 100644 index 00000000..978848d0 --- /dev/null +++ b/src/templates/otoken/activity-types.ts @@ -0,0 +1,107 @@ +export type ActivityStatus = 'idle' | 'pending' | 'signed' | 'success' | 'error' + +export interface ActivityBase { + id: string + blockNumber: number + timestamp: number + status: ActivityStatus + txHash: string + chainId: number +} + +export interface ApprovalActivity extends ActivityBase { + type: 'Approval' + tokenIn: string + amountIn: bigint +} + +export interface BridgeActivity extends ActivityBase { + type: 'Bridge' + chainIn: number + chainOut: number + tokenIn: string + tokenOut: string + amountIn: bigint +} + +export interface ClaimRewardsActivity extends ActivityBase { + type: 'ClaimRewards' + tokenIn: string + amountIn: bigint +} + +export interface DelegateVoteActivity extends ActivityBase { + type: 'DelegateVote' + tokenIn: string + votingPower: bigint + delegateTo: string +} + +export interface ExtendStakeActivity extends ActivityBase { + type: 'ExtendStake' + amountIn: bigint + tokenIn: string + monthDuration: number + lockupId: string +} + +export interface MigrateActivity extends ActivityBase { + type: 'Migrate' + amountIn: bigint + tokenIn: string + tokenIdStaked: string + tokenIdLiquid: string + liquid?: bigint + staked?: bigint +} + +export interface RedeemActivity extends ActivityBase { + type: 'Redeem' + tokenIn: string + tokenOut: string + amountIn: bigint +} + +export interface StakeActivity extends ActivityBase { + type: 'Stake' + tokenIn: string + amountIn: bigint + monthDuration: number +} + +export interface SwapActivity extends ActivityBase { + type: 'Swap' + exchange: 'Curve' | 'Balancer' + pool: string + tokenIn: string + tokenOut: string + amountIn: bigint + amountOut: bigint +} + +export interface UnstakeActivity extends ActivityBase { + type: 'Unstake' + tokenIn: string + tokenOut: string + lockupId: string +} + +export interface VoteActivity extends ActivityBase { + type: 'Vote' + tokenIn: string + choice: string + proposalId: string +} + +export type Activity = + | ApprovalActivity + | BridgeActivity + | ClaimRewardsActivity + | DelegateVoteActivity + | ExtendStakeActivity + | MigrateActivity + | RedeemActivity + | StakeActivity + | SwapActivity + | UnstakeActivity + | VoteActivity diff --git a/src/templates/otoken/otoken.ts b/src/templates/otoken/otoken.ts index 2b7e891a..f0a8d674 100644 --- a/src/templates/otoken/otoken.ts +++ b/src/templates/otoken/otoken.ts @@ -1,6 +1,3 @@ -import { groupBy } from 'lodash' -import { GetTransactionReceiptReturnType } from 'viem' - import * as erc20 from '@abi/erc20' import * as otoken from '@abi/otoken' import * as otokenVault from '@abi/otoken-vault' @@ -19,12 +16,8 @@ import { } from '@model' import { Context } from '@processor' import { ensureExchangeRate } from '@shared/post-processors/exchange-rates' -import { - CurrencyAddress, - CurrencySymbol, -} from '@shared/post-processors/exchange-rates/currencies' +import { CurrencyAddress, CurrencySymbol } from '@shared/post-processors/exchange-rates/currencies' import { EvmBatchProcessor } from '@subsquid/evm-processor' -import { type Transaction, activityFromTx } from '@utils/activityFromTx' import { ADDRESS_ZERO } from '@utils/addresses' import { blockFrequencyUpdater } from '@utils/blockFrequencyUpdater' import { DECIMALS_18 } from '@utils/constants' @@ -33,35 +26,28 @@ import { getLatestEntity } from '@utils/utils' import { createAddress, createRebaseAPY } from './utils' -export const createOTokenSetup = - ({ - address, - wrappedAddress, - vaultAddress, - from, - upgrades, - }: { - address: string - wrappedAddress?: string - vaultAddress: string - from: number - upgrades?: { - rebaseOptEvents: number - } - }) => - (processor: EvmBatchProcessor) => { +export const createOTokenProcessor = (params: { + from: number + vaultFrom: number + Upgrade_CreditsBalanceOfHighRes?: number + otokenAddress: string + wotokenAddress?: string + otokenVaultAddress: string + oTokenAssets: { asset: CurrencyAddress; symbol: CurrencySymbol }[] + upgrades?: { + rebaseOptEvents: number + } +}) => { + const setup = (processor: EvmBatchProcessor) => { processor.addTrace({ type: ['call'], - callTo: [address], - callSighash: [ - otoken.functions.rebaseOptOut.selector, - otoken.functions.rebaseOptIn.selector, - ], + callTo: [params.otokenAddress], + callSighash: [otoken.functions.rebaseOptOut.selector, otoken.functions.rebaseOptIn.selector], transaction: true, - range: { from, to: upgrades?.rebaseOptEvents }, // First AccountRebasing appears on 18872285, on OETH + range: { from: params.from, to: params.upgrades?.rebaseOptEvents }, // First AccountRebasing appears on 18872285, on OETH }) processor.addLog({ - address: [address], + address: [params.otokenAddress], topic0: [ otoken.events.Transfer.topic, otoken.events.TotalSupplyUpdatedHighres.topic, @@ -69,31 +55,22 @@ export const createOTokenSetup = otoken.events.AccountRebasingDisabled.topic, ], transaction: true, - range: { from }, + range: { from: params.from }, }) - if (wrappedAddress) { + if (params.wotokenAddress) { processor.addLog({ - address: [wrappedAddress], + address: [params.wotokenAddress], topic0: [erc20.events.Transfer.topic], - range: { from }, + range: { from: params.from }, }) } processor.addLog({ - address: [vaultAddress], + address: [params.otokenVaultAddress], topic0: [otokenVault.events.YieldDistribution.topic], - range: { from }, + range: { from: params.from }, }) } -export const createOTokenProcessor = (params: { - from: number - vaultFrom: number - Upgrade_CreditsBalanceOfHighRes?: number - otokenAddress: string - wotokenAddress?: string - otokenVaultAddress: string - oTokenAssets: { asset: CurrencyAddress; symbol: CurrencySymbol }[] -}) => { interface ProcessResult { initialized: boolean initialize: () => Promise @@ -176,15 +153,10 @@ export const createOTokenProcessor = (params: { await processTotalSupplyUpdatedHighres(ctx, result, block, log) await processRebaseOptEvent(ctx, result, block, log) } - await processActivity(ctx, result, block) } await frequencyUpdate(ctx, async (ctx, block) => { - const vaultContract = new otokenVault.Contract( - ctx, - block.header, - params.otokenVaultAddress, - ) + const vaultContract = new otokenVault.Contract(ctx, block.header, params.otokenVaultAddress) result.vaults.push( new OTokenVault({ id: `${ctx.chain.id}-${params.otokenAddress}-${block.header.height}-${params.otokenVaultAddress}`, @@ -247,13 +219,8 @@ export const createOTokenProcessor = (params: { return entity } - const afterHighResUpgrade = - block.header.height >= (params.Upgrade_CreditsBalanceOfHighRes ?? 0) - const [ - addressSub, - addressAdd, - [fromCreditsBalanceOf, toCreditsBalanceOf], - ] = await Promise.all([ + const afterHighResUpgrade = block.header.height >= (params.Upgrade_CreditsBalanceOfHighRes ?? 0) + const [addressSub, addressAdd, [fromCreditsBalanceOf, toCreditsBalanceOf]] = await Promise.all([ ensureAddress(data.from), ensureAddress(data.to), multicall( @@ -261,8 +228,7 @@ export const createOTokenProcessor = (params: { block.header, afterHighResUpgrade ? otoken.functions.creditsBalanceOfHighres - : (otoken.functions - .creditsBalanceOf as unknown as typeof otoken.functions.creditsBalanceOfHighres), + : (otoken.functions.creditsBalanceOf as unknown as typeof otoken.functions.creditsBalanceOfHighres), params.otokenAddress, [{ _account: data.from }, { _account: data.to }], ).then((results) => { @@ -279,24 +245,15 @@ export const createOTokenProcessor = (params: { * "0017708038-000327-29fec:0xd2cdf18b60a5cdb634180d5615df7a58a597247c:Sent","0","49130257489166670","2023-07-16T19:50:11.000Z",17708038,"0x0e3ac28945d45993e3d8e1f716b6e9ec17bfc000418a1091a845b7a00c7e3280","Sent","0xd2cdf18b60a5cdb634180d5615df7a58a597247c", */ - const updateAddressBalance = ({ - address, - credits, - }: { - address: OTokenAddress - credits: [bigint, bigint] - }) => { + const updateAddressBalance = ({ address, credits }: { address: OTokenAddress; credits: [bigint, bigint] }) => { const newBalance = (credits[0] * DECIMALS_18) / credits[1] const change = newBalance - address.balance if (change === 0n) return - const type = - addressSub === address ? HistoryType.Sent : HistoryType.Received + const type = addressSub === address ? HistoryType.Sent : HistoryType.Received result.history.push( new OTokenHistory({ // we can't use {t.id} because it's not unique - id: getUniqueId( - `${ctx.chain.id}-${params.otokenAddress}-${log.id}-${address.address}`, - ), + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.id}-${address.address}`), chainId: ctx.chain.id, otoken: params.otokenAddress, address: address, @@ -321,17 +278,11 @@ export const createOTokenProcessor = (params: { credits: toCreditsBalanceOf, }) - if ( - addressAdd.rebasingOption === RebasingOption.OptOut && - data.from === ADDRESS_ZERO - ) { + if (addressAdd.rebasingOption === RebasingOption.OptOut && data.from === ADDRESS_ZERO) { // If it's a mint and minter has opted out of rebasing, // add to non-rebasing supply otokenObject.nonRebasingSupply += data.value - } else if ( - data.to === ADDRESS_ZERO && - addressSub.rebasingOption === RebasingOption.OptOut - ) { + } else if (data.to === ADDRESS_ZERO && addressSub.rebasingOption === RebasingOption.OptOut) { // If it's a redeem and redeemer has opted out of rebasing, // subtract non-rebasing supply otokenObject.nonRebasingSupply -= data.value @@ -352,55 +303,7 @@ export const createOTokenProcessor = (params: { } // Update rebasing supply in all cases - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply - } - } - - const processActivity = async ( - ctx: Context, - result: ProcessResult, - block: Context['blocks']['0'], - ) => { - await result.initialize() - const logs = block.logs - const groupedLogs = groupBy(logs, (log) => log.transactionHash) - for (const [txHash, logs] of Object.entries(groupedLogs)) { - const log = logs.find((l) => l.address === params.otokenAddress) - const transaction = log?.transaction as unknown as Transaction - if (log && transaction) { - // We need to get the whole transaction receipt for all related logs. - // This shouldn't slow things down too much since it's only done for - // OToken transactions. - const txReceipt = await ctx._chain.client.call( - 'eth_getTransactionReceipt', - [log.transactionHash], - ) - - const activity = await activityFromTx( - transaction, - txReceipt.logs as unknown as GetTransactionReceiptReturnType['logs'], - ) - if (activity) { - for (const item of activity) { - result.activity.push( - new OTokenActivity({ - id: getUniqueId( - `${ctx.chain.id}-${params.otokenAddress}-${log.id}`, - ), - chainId: ctx.chain.id, - otoken: params.otokenAddress, - timestamp: new Date(block.header.timestamp), - blockNumber: block.header.height, - txHash: log.transactionHash, - address: transaction.to, - sighash: transaction.input.slice(0, 10), - ...item, - }), - ) - } - } - } + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply } } @@ -419,8 +322,7 @@ export const createOTokenProcessor = (params: { // OToken Object const otokenObject = await getLatestOTokenObject(ctx, result, block) otokenObject.totalSupply = data.totalSupply - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply if (!result.lastYieldDistributionEvent) { throw new Error('lastYieldDistributionEvent is not set') @@ -444,22 +346,16 @@ export const createOTokenProcessor = (params: { ) for (const address of owners!.values()) { - if ( - !address.credits || - address.rebasingOption === RebasingOption.OptOut - ) { + if (!address.credits || address.rebasingOption === RebasingOption.OptOut) { continue } - const newBalance = - (address.credits * DECIMALS_18) / data.rebasingCreditsPerToken + const newBalance = (address.credits * DECIMALS_18) / data.rebasingCreditsPerToken const earned = newBalance - address.balance if (earned === 0n) continue result.history.push( new OTokenHistory({ - id: getUniqueId( - `${ctx.chain.id}-${params.otokenAddress}-${log.id}-${address.address}`, - ), + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.id}-${address.address}`), // we can't use {t.id} because it's not unique chainId: ctx.chain.id, otoken: params.otokenAddress, @@ -503,8 +399,7 @@ export const createOTokenProcessor = (params: { if ( trace.type === 'call' && params.otokenAddress === trace.action.to && - (trace.action.sighash === - otoken.functions.governanceRebaseOptIn.selector || + (trace.action.sighash === otoken.functions.governanceRebaseOptIn.selector || trace.action.sighash === otoken.functions.rebaseOptIn.selector || trace.action.sighash === otoken.functions.rebaseOptOut.selector) ) { @@ -513,26 +408,17 @@ export const createOTokenProcessor = (params: { const blockNumber = block.header.height const address = trace.action.sighash === otoken.functions.governanceRebaseOptIn.selector - ? otoken.functions.governanceRebaseOptIn.decode(trace.action.input) - ._account + ? otoken.functions.governanceRebaseOptIn.decode(trace.action.input)._account : trace.action.from.toLowerCase() const otokenObject = await getLatestOTokenObject(ctx, result, block) let owner = owners!.get(address) if (!owner) { - owner = await createAddress( - ctx, - params.otokenAddress, - address, - timestamp, - ) + owner = await createAddress(ctx, params.otokenAddress, address, timestamp) owners!.set(address, owner) } const rebaseOption = new OTokenRebaseOption({ - id: getUniqueId( - `${ctx.chain.id}-${params.otokenAddress}-${trace.transaction - ?.hash!}-${owner.address}`, - ), + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${trace.transaction?.hash!}-${owner.address}`), chainId: ctx.chain.id, otoken: params.otokenAddress, timestamp, @@ -543,41 +429,28 @@ export const createOTokenProcessor = (params: { }) result.rebaseOptions.push(rebaseOption) if (trace.action.sighash === otoken.functions.rebaseOptIn.selector) { - const afterHighResUpgrade = - block.header.height >= (params.Upgrade_CreditsBalanceOfHighRes ?? 0) - const otokenContract = new otoken.Contract( - ctx, - block.header, - params.otokenAddress, - ) + const afterHighResUpgrade = block.header.height >= (params.Upgrade_CreditsBalanceOfHighRes ?? 0) + const otokenContract = new otoken.Contract(ctx, block.header, params.otokenAddress) owner.credits = afterHighResUpgrade - ? await otokenContract - .creditsBalanceOfHighres(owner.address) - .then((c) => c._0) - : await otokenContract - .creditsBalanceOf(owner.address) - .then((c) => c._0 * 1000000000n) + ? await otokenContract.creditsBalanceOfHighres(owner.address).then((c) => c._0) + : await otokenContract.creditsBalanceOf(owner.address).then((c) => c._0 * 1000000000n) owner.rebasingOption = RebasingOption.OptIn rebaseOption.status = RebasingOption.OptIn otokenObject.nonRebasingSupply -= owner.balance - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply } if (trace.action.sighash === otoken.functions.rebaseOptOut.selector) { owner.rebasingOption = RebasingOption.OptOut rebaseOption.status = RebasingOption.OptOut otokenObject.nonRebasingSupply += owner.balance - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply } } } const rebaseEventTopics = { - [otoken.events.AccountRebasingEnabled.topic]: - otoken.events.AccountRebasingEnabled, - [otoken.events.AccountRebasingDisabled.topic]: - otoken.events.AccountRebasingDisabled, + [otoken.events.AccountRebasingEnabled.topic]: otoken.events.AccountRebasingEnabled, + [otoken.events.AccountRebasingDisabled.topic]: otoken.events.AccountRebasingDisabled, } const processRebaseOptEvent = async ( ctx: Context, @@ -595,21 +468,12 @@ export const createOTokenProcessor = (params: { const address = data.account.toLowerCase() let owner = owners!.get(address) if (!owner) { - owner = await createAddress( - ctx, - params.otokenAddress, - address, - timestamp, - ) + owner = await createAddress(ctx, params.otokenAddress, address, timestamp) owners!.set(address, owner) } const rebaseOption = new OTokenRebaseOption({ - id: getUniqueId( - `${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash!}-${ - owner.address - }`, - ), + id: getUniqueId(`${ctx.chain.id}-${params.otokenAddress}-${log.transactionHash!}-${owner.address}`), chainId: ctx.chain.id, otoken: params.otokenAddress, timestamp, @@ -623,33 +487,24 @@ export const createOTokenProcessor = (params: { owner.rebasingOption = RebasingOption.OptIn rebaseOption.status = RebasingOption.OptIn otokenObject.nonRebasingSupply -= owner.balance - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply } if (log.topics[0] === otoken.events.AccountRebasingDisabled.topic) { owner.rebasingOption = RebasingOption.OptOut rebaseOption.status = RebasingOption.OptOut otokenObject.nonRebasingSupply += owner.balance - otokenObject.rebasingSupply = - otokenObject.totalSupply - otokenObject.nonRebasingSupply + otokenObject.rebasingSupply = otokenObject.totalSupply - otokenObject.nonRebasingSupply } } } - const getLatestOTokenObject = async ( - ctx: Context, - result: ProcessResult, - block: Context['blocks']['0'], - ) => { + const getLatestOTokenObject = async (ctx: Context, result: ProcessResult, block: Context['blocks']['0']) => { const timestamp = new Date(block.header.timestamp).toISOString() const otokenId = `${ctx.chain.id}-${params.otokenAddress}-${timestamp}` - const { latest, current } = await getLatestEntity( - ctx, - OToken, - result.otokens, - otokenId, - { chainId: ctx.chain.id, otoken: params.otokenAddress }, - ) + const { latest, current } = await getLatestEntity(ctx, OToken, result.otokens, otokenId, { + chainId: ctx.chain.id, + otoken: params.otokenAddress, + }) let otokenObject = current if (!otokenObject) { @@ -669,5 +524,5 @@ export const createOTokenProcessor = (params: { return otokenObject } - return process + return { from: params.from, setup, process } } diff --git a/src/utils/activityFromTx.ts b/src/utils/activityFromTx.ts index 3eaafdb1..8f33397c 100644 --- a/src/utils/activityFromTx.ts +++ b/src/utils/activityFromTx.ts @@ -2,22 +2,31 @@ import { compact } from 'lodash' import { GetTransactionReceiptReturnType, decodeEventLog, parseAbi } from 'viem' import * as balancerVaultAbi from '@abi/balancer-vault.abi' +import * as curveLp from '@abi/curve-lp-token' import * as curveLpAbi from '@abi/curve-lp-token.abi' import * as oethAbi from '@abi/oeth.abi' import * as oethVaultAbi from '@abi/otoken-vault.abi' +import { Block, Context, Log } from '@processor' +import { Activity, SwapActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' import { + BALANCER_VAULT_ADDRESS, + COWSWAP_SETTLEMENT_ADDRESS, + CURVE_ETH_OETH_POOL_ADDRESS, + CURVE_FRXETH_OETH_POOL_ADDRESS, OETH_ADDRESS, OETH_VAULT_ADDRESS, OETH_ZAPPER_ADDRESS, + ONEINCH_AGGREGATION_ROUTER_ADDRESS, + UNISWAP_OETH_WEH_ADDRESS, + WOETH_ADDRESS, } from './addresses' const UniswapV3SwapAbi = parseAbi([ 'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)', ]) -const GnosisSafeAbi = parseAbi([ - 'event ExecutionSuccess(bytes32 txHash, uint256 payment)', -]) +const GnosisSafeAbi = parseAbi(['event ExecutionSuccess(bytes32 txHash, uint256 payment)']) const WrappedOETHAbi = parseAbi([ 'event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares)', 'event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)', @@ -51,27 +60,7 @@ type NetTransfers = { [address: string]: bigint } -export async function activityFromTx( - transaction: Transaction, - logs: GetTransactionReceiptReturnType['logs'], -) { - const activity = [] - if (!transaction) return - - const curveEvents = compact( - logs - .filter((l) => - addressEq(l.address, '0x94B17476A93b3262d87B9a326965D1E91f9c13E7'), - ) - .map((log) => - tryDecodeEventLog({ - abi: curveLpAbi.ABI_JSON, - data: log.data, - topics: log.topics, - }), - ), - ) - +export async function activityFromTx(ctx: Context, block: Block, log: Log) { const sansGnosisSafeEvents = logs.filter(({ data, topics }) => { try { decodeEventLog({ @@ -98,20 +87,6 @@ export async function activityFromTx( }), ) - const balancerVaultEvents = compact( - logs - .filter((l) => - addressEq(l.address, '0xBA12222222228d8Ba445958a75a0704d566BF2C8'), - ) - .map((log) => { - return tryDecodeEventLog({ - abi: balancerVaultAbi.ABI_JSON, - data: log.data, - topics: log.topics, - }) - }), - ) - const oethVaultEvents = compact( logs .filter((l) => l.address === OETH_VAULT_ADDRESS) @@ -126,22 +101,14 @@ export async function activityFromTx( const woethEvents = compact( logs - .filter((l) => - addressEq(l.address, '0xdcee70654261af21c44c093c300ed3bb97b78192'), - ) - .map(({ data, topics }) => - tryDecodeEventLog({ abi: WrappedOETHAbi, data, topics }), - ), + .filter((l) => l.address === WOETH_ADDRESS) + .map(({ data, topics }) => tryDecodeEventLog({ abi: WrappedOETHAbi, data, topics })), ) - const oneInchEvents = logs.filter((l) => - addressEq(l.address, '0x1111111254eeb25477b68fb85ed929f73a960582'), - ) + const oneInchEvents = logs.filter((l) => l.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) const uniswapWethEvents = compact( logs - .filter((l) => - addressEq(l.address, '0x52299416c469843f4e0d54688099966a6c7d720f'), - ) + .filter((l) => l.address === UNISWAP_OETH_WEH_ADDRESS) .map((log) => tryDecodeEventLog({ abi: UniswapV3SwapAbi, @@ -151,25 +118,14 @@ export async function activityFromTx( ), ) - const frxEthOETHCurvePoolEvents = logs.filter((l) => - addressEq(l.address, '0xfa0bbb0a5815f6648241c9221027b70914dd8949'), - ) + const oethTransfers = compact(oethEvents).filter((log) => log.eventName === 'Transfer') as Transfer[] - const oethTransfers = compact(oethEvents).filter( - (log) => log.eventName === 'Transfer', - ) as Transfer[] - - const netTransfers = oethTransfers.reduce( - (acc, { args: { from, to, value } }) => { - acc[from] = (acc[from] || 0n) - value - acc[to] = (acc[to] || 0n) + value - return acc - }, - {}, - ) - const nonZeroBalances: { [address: string]: bigint } = Object.keys( - netTransfers, - ).reduce( + const netTransfers = oethTransfers.reduce((acc, { args: { from, to, value } }) => { + acc[from] = (acc[from] || 0n) - value + acc[to] = (acc[to] || 0n) + value + return acc + }, {}) + const nonZeroBalances: { [address: string]: bigint } = Object.keys(netTransfers).reduce( (acc, key) => { if (netTransfers[key] !== 0n) { acc[key] = netTransfers[key] @@ -179,9 +135,7 @@ export async function activityFromTx( {} as { [address: string]: bigint }, ) - const sumPositiveBalances = (balances: { - [address: string]: bigint - }): bigint => { + const sumPositiveBalances = (balances: { [address: string]: bigint }): bigint => { return Object.values(balances).reduce((sum, value) => { if (value > 0n) { return sum + value @@ -209,32 +163,23 @@ export async function activityFromTx( if (oethVaultEvents.some((e) => e.eventName === 'Redeem')) { data.action = 'Redeem' - } else if ( - oethEvents.some((e) => e.eventName === 'TotalSupplyUpdatedHighres') - ) { + } else if (oethEvents.some((e) => e.eventName === 'TotalSupplyUpdatedHighres')) { data.action = 'Rebase' } - if ( - addressEq(transaction.to, '0x1111111254EEB25477B68fb85Ed929f73A960582') || - oneInchEvents.length > 0 - ) { + if (transaction.to === ONEINCH_AGGREGATION_ROUTER_ADDRESS || oneInchEvents.length > 0) { data.action = 'Swap' data.interface = '1inch' } - if (addressEq(transaction.to, '0x9008d19f58aabd9ed0d60971565aa8510560ab41')) { + if (transaction.to === COWSWAP_SETTLEMENT_ADDRESS) { data.action = 'Swap' data.exchange = 'CoW Swap' } - if (addressEq(transaction.to, '0x6131b5fae19ea4f9d964eac0408e4408b66337b5')) { + if (transaction.to === '0x6131b5fae19ea4f9d964eac0408e4408b66337b5') { data.action = 'Swap' data.exchange = 'Kyber Swap' } - if (curveEvents.length > 0) { - data.action = 'Swap' - data.exchange = 'Curve' - } if (frxEthOETHCurvePoolEvents.length > 0) { data.action = 'Swap' data.exchange = 'Curve' @@ -259,16 +204,16 @@ export async function activityFromTx( if ( oethTransfers.some( (t) => - addressEq(t.args.from, '0x9008d19f58aabd9ed0d60971565aa8510560ab41') || - addressEq(t.args.to, '0x9008d19f58aabd9ed0d60971565aa8510560ab41'), + t.args.from.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS || + t.args.to.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS, ) ) { data.exchange = 'CoW Swap' } else if ( oethTransfers.some( (t) => - addressEq(t.args.from, '0x94b17476a93b3262d87b9a326965d1e91f9c13e7') || - addressEq(t.args.to, '0x94b17476a93b3262d87b9a326965d1e91f9c13e7'), + t.args.from.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS || + t.args.to.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS, ) ) { data.exchange = 'Curve' @@ -295,7 +240,3 @@ function decodeOethZapperTx(transaction: Transaction) { toSymbol: 'OETH', } } - -function addressEq(a: string | undefined, b: string | undefined) { - return a?.toLowerCase() === b?.toLowerCase() -} diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index 8d2d71b6..13a20f74 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -2,7 +2,11 @@ export const ADDRESS_ZERO = '0x0000000000000000000000000000000000000000' export const ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' -export const CURVE_EXCHANGE_ADDRESS = `0x99a58482bd75cbab83b27ec03ca68ff489b5788f` +export const COWSWAP_SETTLEMENT_ADDRESS = '0x9008d19f58aabd9ed0d60971565aa8510560ab41' +export const ONEINCH_AGGREGATION_ROUTER_ADDRESS = '0x1111111254eeb25477b68fb85ed929f73a960582' +export const UNISWAP_OETH_WEH_ADDRESS = '0x52299416c469843f4e0d54688099966a6c7d720f' +export const BALANCER_VAULT_ADDRESS = '0xba12222222228d8ba445958a75a0704d566bf2c8' +export const CURVE_FRXETH_OETH_POOL_ADDRESS = '0xfa0bbb0a5815f6648241c9221027b70914dd8949' export const OUSD_ADDRESS = '0x2a8e1e676ec238d8a992307b495b45b3feaa5e86' export const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f' @@ -39,10 +43,7 @@ export const OETH_VAULT_ERC20_ADDRESSES = [WETH_ADDRESS, STETH_ADDRESS, RETH_ADD export const OETH_CONVEX_ADDRESS = '0x1827f9ea98e0bf96550b2fc20f7233277fcd7e63' export const OETH_CURVE_REWARD_LP_ADDRESS = '0x24b65dc1cf053a8d96872c323d29e86ec43eb33a' -export const OETH_CURVE_LP_ADDRESS = '0x94b17476a93b3262d87b9a326965d1e91f9c13e7' - -// export const OETH_CURVE_LP_OWNER_ADDRESS = '0xd03be91b1932715709e18021734fcb91bb431715' -// export const CONVEX_DEPOSIT = '0xf403c135812408bfbe8713b5a23a04b3d48aae31' +export const CURVE_ETH_OETH_POOL_ADDRESS = '0x94b17476a93b3262d87b9a326965d1e91f9c13e7' export const OETH_FRAX_STAKING_ADDRESS = '0x3ff8654d633d4ea0fae24c52aec73b4a20d0d0e5' diff --git a/src/utils/logFilter.ts b/src/utils/logFilter.ts index 83b1df99..278203b5 100644 --- a/src/utils/logFilter.ts +++ b/src/utils/logFilter.ts @@ -10,9 +10,7 @@ const prepare = (hex: string) => pad(lower(hex)) /** * Helper to create and match logs, ensuring hex values are lowercase and properly padded. */ -export const logFilter = ( - filter: Parameters[0], -) => { +export const logFilter = (filter: Parameters[0]) => { filter = { address: filter.address?.map(lower), topic0: filter.topic0?.map(prepare), @@ -21,6 +19,8 @@ export const logFilter = ( topic3: filter.topic3?.map(prepare), range: filter.range, transaction: filter.transaction, + transactionLogs: filter.transactionLogs, + transactionTraces: filter.transactionTraces, } return { value: filter, @@ -42,8 +42,7 @@ export const logFilter = ( } if ( filter.range && - (log.block.height < filter.range.from || - (filter.range.to && log.block.height > filter.range.to)) + (log.block.height < filter.range.from || (filter.range.to && log.block.height > filter.range.to)) ) { return false } From 47a11ecb3c4c3b32fdab1f2b579a9dbb9c0338b5 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 18 Jun 2024 10:44:51 -0700 Subject: [PATCH 02/15] wip activity rework --- schema.graphql | 5 +- schema/otoken.graphql | 5 +- src/model/generated/_oTokenActivityType.ts | 5 +- src/oeth/processors/oeth.ts | 1 + src/templates/otoken/activity-processor.ts | 234 ++++++++++++++++----- src/templates/otoken/activity-types.ts | 59 ++++-- src/utils/activityFromTx.ts | 6 - 7 files changed, 238 insertions(+), 77 deletions(-) diff --git a/schema.graphql b/schema.graphql index 7b4ef35a..7c92ca3f 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1083,9 +1083,12 @@ enum OTokenActivityType { DelegateVote ExtendStake Migrate - Redeem Stake Swap + Wrap + Unwrap + Mint + Redeem Unstake Vote } diff --git a/schema/otoken.graphql b/schema/otoken.graphql index e82f2a1c..03bcb878 100644 --- a/schema/otoken.graphql +++ b/schema/otoken.graphql @@ -117,9 +117,12 @@ enum OTokenActivityType { DelegateVote ExtendStake Migrate - Redeem Stake Swap + Wrap + Unwrap + Mint + Redeem Unstake Vote } diff --git a/src/model/generated/_oTokenActivityType.ts b/src/model/generated/_oTokenActivityType.ts index c0dc473f..40e72d16 100644 --- a/src/model/generated/_oTokenActivityType.ts +++ b/src/model/generated/_oTokenActivityType.ts @@ -5,9 +5,12 @@ export enum OTokenActivityType { DelegateVote = "DelegateVote", ExtendStake = "ExtendStake", Migrate = "Migrate", - Redeem = "Redeem", Stake = "Stake", Swap = "Swap", + Wrap = "Wrap", + Unwrap = "Unwrap", + Mint = "Mint", + Redeem = "Redeem", Unstake = "Unstake", Vote = "Vote", } diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 246bc486..a90717fe 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -41,6 +41,7 @@ const otokenProcessor = createOTokenProcessor({ const otokenActivityProcessor = createOTokenActivityProcessor({ from: 16933090, otokenAddress: OETH_ADDRESS, + vaultAddress: OETH_VAULT_ADDRESS, wotokenAddress: WOETH_ADDRESS, curvePools: [ { diff --git a/src/templates/otoken/activity-processor.ts b/src/templates/otoken/activity-processor.ts index e31ae848..89dc4abb 100644 --- a/src/templates/otoken/activity-processor.ts +++ b/src/templates/otoken/activity-processor.ts @@ -1,18 +1,27 @@ -import { groupBy, keyBy, wrap } from 'lodash' +import { compact, groupBy, uniq } from 'lodash' -import * as balancerVault from '@abi/balancer-vault' -import * as curveLp from '@abi/curve-lp-token' -import * as woeth from '@abi/woeth' +import * as balancerVaultAbi from '@abi/balancer-vault' +import * as curvePoolAbi from '@abi/curve-lp-token' +import * as otokenVaultAbi from '@abi/otoken-vault' +import * as wotokenAbi from '@abi/woeth' import { OTokenActivity, OTokenActivityType } from '@model' import { Block, Context, Log } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' -import { Activity, SwapActivity } from '@templates/otoken/activity-types' +import { + Activity, + MintActivity, + RedeemActivity, + SwapActivity, + UnwrapActivity, + WrapActivity, +} from '@templates/otoken/activity-types' import { BALANCER_VAULT_ADDRESS } from '@utils/addresses' import { LogFilter, logFilter } from '@utils/logFilter' interface Input { from: number otokenAddress: string + vaultAddress: string wotokenAddress?: string curvePools: { address: string @@ -21,17 +30,19 @@ interface Input { balancerPools: string[] } -export interface ActivityProcessor { +export interface ActivityProcessor { name: string filters: LogFilter[] - process: (ctx: Context, block: Block, logs: Log[]) => Promise + process: (ctx: Context, block: Block, logs: Log[]) => Promise } export const createOTokenActivityProcessor = (params: Input) => { - const processors: ActivityProcessor[] = [ + const processors: ActivityProcessor[] = compact([ ...params.curvePools.map((pool) => curveActivityProcessor(pool)), balancerActivityProcessor({ pools: params.balancerPools }), - ] + params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), + vaultActivityProcessor({ otokenAddress: params.otokenAddress, vaultAddress: params.vaultAddress }), + ]) const from = params.from const setup = (processor: EvmBatchProcessor) => { @@ -47,12 +58,16 @@ export const createOTokenActivityProcessor = (params: Input) => { const transactions = groupBy(block.logs, (l) => l.transactionHash) for (const logs of Object.values(transactions)) { for (const p of processors) { + let hit = false for (const filter of p.filters) { if (logs.find((log) => filter.matches(log))) { const results = await p.process(ctx, block, logs) activities.push(...results) + hit = true + break } } + if (hit) break } } } @@ -72,7 +87,6 @@ export const createOTokenActivityProcessor = (params: Input) => { ), ) } - return { from, setup, process } } @@ -82,7 +96,7 @@ const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: filters: [ logFilter({ address: [address], - topic0: [curveLp.events.TokenExchange.topic], + topic0: [curvePoolAbi.events.TokenExchange.topic], }), ], async process(ctx: Context, block: Block, logs: Log[]): Promise { @@ -90,22 +104,17 @@ const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: return logs .filter((l) => tokenExchangeFilter.matches(l)) .map((log) => { - const tokenExchange = curveLp.events.TokenExchange.decode(log) - return { - id: `${ctx.chain.id}:${log.id}`, - chainId: ctx.chain.id, + const tokenExchange = curvePoolAbi.events.TokenExchange.decode(log) + return createActivity(ctx, block, log, { type: 'Swap', + account: tokenExchange.buyer, exchange: 'Curve', - blockNumber: block.header.height, - timestamp: block.header.timestamp, - status: 'success', - pool: log.address, - txHash: log.transactionHash, + contract: log.address, tokenIn: tokens[Number(tokenExchange.sold_id)], tokenOut: tokens[Number(tokenExchange.bought_id)], amountIn: tokenExchange.tokens_sold, amountOut: tokenExchange.tokens_bought, - } + }) }) }, } @@ -117,8 +126,9 @@ const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProc filters: [ logFilter({ address: [BALANCER_VAULT_ADDRESS], - topic0: [balancerVault.events.Swap.topic], + topic0: [balancerVaultAbi.events.Swap.topic], topic1: pools, + transaction: true, }), ], async process(ctx: Context, block: Block, logs: Log[]): Promise { @@ -126,58 +136,172 @@ const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProc return logs .filter((l) => swapFilter.matches(l)) .map((log) => { - const swap = balancerVault.events.Swap.decode(log) - return { - id: `${ctx.chain.id}:${log.id}`, - chainId: ctx.chain.id, + const swap = balancerVaultAbi.events.Swap.decode(log) + return createActivity(ctx, block, log, { type: 'Swap', + account: log.transaction!.from, exchange: 'Balancer', - blockNumber: block.header.height, - timestamp: block.header.timestamp, - status: 'success', - txHash: log.transactionHash, - pool: swap.poolId, + contract: swap.poolId, tokenIn: swap.tokenIn, tokenOut: swap.tokenOut, amountIn: swap.amountIn, amountOut: swap.amountOut, - } + }) }) }, } } -const wrappedActivityProcessor = (wrappedAddress: string) => { +const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor => { return { name: 'Wrapped Processor', filters: [ logFilter({ address: [wrappedAddress], - topic0: [woeth.events.Deposit.topic], + topic0: [wotokenAbi.events.Deposit.topic], + }), + logFilter({ + address: [wrappedAddress], + topic0: [wotokenAbi.events.Withdraw.topic], }), ], - async process(ctx: Context, block: Block, logs: Log[]): Promise { - const [depositFilter] = this.filters - return logs - .filter((l) => depositFilter.matches(l)) - .map((log) => { - const swap = balancerVault.events.Swap.decode(log) - return { - id: `${ctx.chain.id}:${log.id}`, - chainId: ctx.chain.id, - type: 'Swap', - exchange: 'Balancer', - blockNumber: block.header.height, - timestamp: block.header.timestamp, - status: 'success', - txHash: log.transactionHash, - pool: swap.poolId, - tokenIn: swap.tokenIn, - tokenOut: swap.tokenOut, - amountIn: swap.amountIn, - amountOut: swap.amountOut, - } + async process(ctx: Context, block: Block, logs: Log[]) { + const result: (WrapActivity | UnwrapActivity)[] = [] + const [depositFilter, withdrawFilter] = this.filters + // Deposits + const depositLogs = logs.filter((l) => depositFilter.matches(l)) + if (depositLogs.length) { + const transferInFilter = logFilter({ + topic0: [wotokenAbi.events.Transfer.topic], + topic2: [wrappedAddress], + }) + const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) + result.push( + ...depositLogs.map((log) => { + const data = wotokenAbi.events.Deposit.decode(log) + const tokenIn = transferInLogs[0].address + const amountIn = transferInLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + return createActivity(ctx, block, log, { + type: 'Wrap', + contract: wrappedAddress, + account: data.owner, + tokenIn, + tokenOut: wrappedAddress, + amountIn, + amountOut: data.shares, + }) + }), + ) + } + // Withdrawals + const withdrawLogs = logs.filter((l) => withdrawFilter.matches(l)) + if (withdrawLogs.length) { + const transferOutFilter = logFilter({ + topic0: [wotokenAbi.events.Transfer.topic], + topic1: [wrappedAddress], }) + const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) + result.push( + ...withdrawLogs.map((log) => { + const data = wotokenAbi.events.Withdraw.decode(log) + const tokenOut = transferOutLogs[0].address + const amountOut = transferOutLogs.reduce((sum, l) => sum + wotokenAbi.events.Withdraw.decode(l).assets, 0n) + return createActivity(ctx, block, log, { + type: 'Unwrap', + contract: wrappedAddress, + account: data.owner, + tokenIn: wrappedAddress, + tokenOut, + amountIn: data.shares, + amountOut, + }) + }), + ) + } + return result }, } } + +const vaultActivityProcessor = ({ + otokenAddress, + vaultAddress, +}: { + otokenAddress: string + vaultAddress: string +}): ActivityProcessor => { + const mintFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Mint.topic] }) + const redeemFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Redeem.topic] }) + return { + name: 'Vault Processor', + filters: [mintFilter, redeemFilter], + process: async (ctx, block, logs) => { + const result: (MintActivity | RedeemActivity)[] = [] + const mintLogs = logs.filter((l) => mintFilter.matches(l)) + if (mintLogs.length) { + const transferInFilter = logFilter({ + topic0: [wotokenAbi.events.Transfer.topic], + topic2: [vaultAddress], + }) + const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) + const tokenIn = transferInLogs[0].address + const amountIn = transferInLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + result.push( + ...mintLogs.map((log) => { + const data = otokenVaultAbi.events.Mint.decode(log) + return createActivity(ctx, block, log, { + type: 'Mint', + contract: log.address, + account: data._addr, + tokenIn, + amountIn, + tokenOut: otokenAddress, + amountOut: data._value, + }) + }), + ) + } + const redeemLogs = logs.filter((l) => redeemFilter.matches(l)) + if (redeemLogs.length) { + const transferOutFilter = logFilter({ + topic0: [wotokenAbi.events.Transfer.topic], + topic1: [vaultAddress], + }) + const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) + const tokensOut = uniq(transferOutLogs.map((l) => l.address)) + const amountOut = transferOutLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + result.push( + ...mintLogs.map((log) => { + const data = otokenVaultAbi.events.Redeem.decode(log) + return createActivity(ctx, block, log, { + type: 'Redeem', + contract: log.address, + account: data._addr, + tokenIn: otokenAddress, + amountIn: data._value, + tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0], + amountOut, + }) + }), + ) + } + return result + }, + } +} + +const createActivity = ( + ctx: Context, + block: Block, + log: Log, + partial: Omit, +) => + ({ + id: `${ctx.chain.id}:${log.id}`, + chainId: ctx.chain.id, + blockNumber: block.header.height, + timestamp: block.header.timestamp, + status: 'success', + txHash: log.transactionHash, + ...partial, + }) as T diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 978848d0..4ae210ca 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -7,6 +7,7 @@ export interface ActivityBase { status: ActivityStatus txHash: string chainId: number + account: string } export interface ApprovalActivity extends ActivityBase { @@ -55,13 +56,6 @@ export interface MigrateActivity extends ActivityBase { staked?: bigint } -export interface RedeemActivity extends ActivityBase { - type: 'Redeem' - tokenIn: string - tokenOut: string - amountIn: bigint -} - export interface StakeActivity extends ActivityBase { type: 'Stake' tokenIn: string @@ -69,21 +63,57 @@ export interface StakeActivity extends ActivityBase { monthDuration: number } +export interface UnstakeActivity extends ActivityBase { + type: 'Unstake' + tokenIn: string + tokenOut: string + lockupId: string +} + export interface SwapActivity extends ActivityBase { type: 'Swap' exchange: 'Curve' | 'Balancer' - pool: string + contract: string tokenIn: string tokenOut: string amountIn: bigint amountOut: bigint } -export interface UnstakeActivity extends ActivityBase { - type: 'Unstake' +export interface WrapActivity extends ActivityBase { + type: 'Wrap' + contract: string tokenIn: string tokenOut: string - lockupId: string + amountIn: bigint + amountOut: bigint +} + +export interface UnwrapActivity extends ActivityBase { + type: 'Unwrap' + contract: string + tokenIn: string + tokenOut: string + amountIn: bigint + amountOut: bigint +} + +export interface MintActivity extends ActivityBase { + type: 'Mint' + contract: string + tokenIn: string + tokenOut: string + amountIn: bigint + amountOut: bigint +} + +export interface RedeemActivity extends ActivityBase { + type: 'Redeem' + contract: string + tokenIn: string + tokenOut: string + amountIn: bigint + amountOut: bigint } export interface VoteActivity extends ActivityBase { @@ -100,8 +130,11 @@ export type Activity = | DelegateVoteActivity | ExtendStakeActivity | MigrateActivity - | RedeemActivity | StakeActivity - | SwapActivity | UnstakeActivity + | SwapActivity + | WrapActivity + | UnwrapActivity + | MintActivity + | RedeemActivity | VoteActivity diff --git a/src/utils/activityFromTx.ts b/src/utils/activityFromTx.ts index 8f33397c..ec2ad180 100644 --- a/src/utils/activityFromTx.ts +++ b/src/utils/activityFromTx.ts @@ -99,12 +99,6 @@ export async function activityFromTx(ctx: Context, block: Block, log: Log) { }), ) - const woethEvents = compact( - logs - .filter((l) => l.address === WOETH_ADDRESS) - .map(({ data, topics }) => tryDecodeEventLog({ abi: WrappedOETHAbi, data, topics })), - ) - const oneInchEvents = logs.filter((l) => l.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) const uniswapWethEvents = compact( logs From b476bb0ecfd22305921786f88c32876dfd58466f Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 18 Jun 2024 18:41:04 -0700 Subject: [PATCH 03/15] wip activity rework --- ...45844084-Data.js => 1718760142436-Data.js} | 6 +- schema.graphql | 1 + schema/otoken.graphql | 1 + src/main-oeth.ts | 30 +- src/model/generated/_oTokenActivityType.ts | 1 + src/oeth/processors/oeth.ts | 7 +- src/templates/otoken/activity-processor.ts | 206 +++++--- src/templates/otoken/activity-types.ts | 63 ++- src/utils/activityFromTx.ts | 459 +++++++++--------- 9 files changed, 438 insertions(+), 336 deletions(-) rename db/migrations/{1717645844084-Data.js => 1718760142436-Data.js} (99%) diff --git a/db/migrations/1717645844084-Data.js b/db/migrations/1718760142436-Data.js similarity index 99% rename from db/migrations/1717645844084-Data.js rename to db/migrations/1718760142436-Data.js index f528b2fa..a45f783e 100644 --- a/db/migrations/1717645844084-Data.js +++ b/db/migrations/1718760142436-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1717645844084 { - name = 'Data1717645844084' +module.exports = class Data1718760142436 { + name = 'Data1718760142436' async up(db) { await db.query(`CREATE TABLE "es_token" ("id" character varying NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "circulating" numeric NOT NULL, "staked" numeric NOT NULL, "total" numeric NOT NULL, CONSTRAINT "PK_69bef9eb94d9a5d42d726d1e661" PRIMARY KEY ("id"))`) @@ -303,7 +303,7 @@ module.exports = class Data1717645844084 { await db.query(`CREATE INDEX "IDX_091cfbe0d977006e05144bd1fe" ON "o_token_vault" ("timestamp") `) await db.query(`CREATE INDEX "IDX_fa92b36d011441a02d9a231860" ON "o_token_vault" ("block_number") `) await db.query(`CREATE INDEX "IDX_3a2bfb2808c1d7cbb0a568910c" ON "o_token_vault" ("address") `) - await db.query(`CREATE TABLE "o_token_activity" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "tx_hash" text NOT NULL, "call_data_last4_bytes" text NOT NULL, "address" text, "sighash" text, "action" text, "exchange" text, "interface" text, "from_symbol" text, "to_symbol" text, "amount" numeric, CONSTRAINT "PK_a6e9207d04e252b4de591283276" PRIMARY KEY ("id"))`) + await db.query(`CREATE TABLE "o_token_activity" ("id" character varying NOT NULL, "chain_id" integer NOT NULL, "otoken" text NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "tx_hash" text NOT NULL, "type" character varying(12), "data" jsonb, CONSTRAINT "PK_a6e9207d04e252b4de591283276" PRIMARY KEY ("id"))`) await db.query(`CREATE INDEX "IDX_9c617918c3dc521bed5220a185" ON "o_token_activity" ("chain_id") `) await db.query(`CREATE INDEX "IDX_62bfeb1dee3bcefffdbd10172a" ON "o_token_activity" ("otoken") `) await db.query(`CREATE INDEX "IDX_57bb1f7d2fd6fe063b9cd434b2" ON "o_token_activity" ("timestamp") `) diff --git a/schema.graphql b/schema.graphql index 7c92ca3f..760b8d95 100644 --- a/schema.graphql +++ b/schema.graphql @@ -1084,6 +1084,7 @@ enum OTokenActivityType { ExtendStake Migrate Stake + Transfer Swap Wrap Unwrap diff --git a/schema/otoken.graphql b/schema/otoken.graphql index 03bcb878..3f02fa01 100644 --- a/schema/otoken.graphql +++ b/schema/otoken.graphql @@ -118,6 +118,7 @@ enum OTokenActivityType { ExtendStake Migrate Stake + Transfer Swap Wrap Unwrap diff --git a/src/main-oeth.ts b/src/main-oeth.ts index 5aebb80d..4201f7ea 100644 --- a/src/main-oeth.ts +++ b/src/main-oeth.ts @@ -20,23 +20,23 @@ import * as validateOeth from './oeth/validators/validate-oeth' export const processor = { stateSchema: 'oeth-processor', processors: [ - ccip({ chainId: 1 }), + // ccip({ chainId: 1 }), oeth, - vault, - fraxStaking, - morphoAave, - dripper, - curveLp, - balancerMetaPoolStrategy, - strategies, - exchangeRates, + // vault, + // fraxStaking, + // morphoAave, + // dripper, + // curveLp, + // balancerMetaPoolStrategy, + // strategies, + // exchangeRates, ], - postProcessors: [ - exchangeRatesPostProcessor, - dailyStats, - processStatus('oeth'), - ], - validators: [validateOeth], + // postProcessors: [ + // exchangeRatesPostProcessor, + // dailyStats, + // processStatus('oeth'), + // ], + // validators: [validateOeth], } export default processor diff --git a/src/model/generated/_oTokenActivityType.ts b/src/model/generated/_oTokenActivityType.ts index 40e72d16..543dfdfd 100644 --- a/src/model/generated/_oTokenActivityType.ts +++ b/src/model/generated/_oTokenActivityType.ts @@ -6,6 +6,7 @@ export enum OTokenActivityType { ExtendStake = "ExtendStake", Migrate = "Migrate", Stake = "Stake", + Transfer = "Transfer", Swap = "Swap", Wrap = "Wrap", Unwrap = "Unwrap", diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index a90717fe..71004013 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -58,9 +58,12 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ export const from = Math.min(otokenProcessor.from, otokenActivityProcessor.from) export const setup = (processor: EvmBatchProcessor) => { - otokenProcessor.setup(processor) + // otokenProcessor.setup(processor) otokenActivityProcessor.setup(processor) } export const process = async (ctx: Context) => { - await Promise.all([otokenProcessor.process(ctx), otokenActivityProcessor.process(ctx)]) + await Promise.all([ + // otokenProcessor.process(ctx), + otokenActivityProcessor.process(ctx), + ]) } diff --git a/src/templates/otoken/activity-processor.ts b/src/templates/otoken/activity-processor.ts index 89dc4abb..f138ee48 100644 --- a/src/templates/otoken/activity-processor.ts +++ b/src/templates/otoken/activity-processor.ts @@ -2,6 +2,7 @@ import { compact, groupBy, uniq } from 'lodash' import * as balancerVaultAbi from '@abi/balancer-vault' import * as curvePoolAbi from '@abi/curve-lp-token' +import * as otokenAbi from '@abi/otoken' import * as otokenVaultAbi from '@abi/otoken-vault' import * as wotokenAbi from '@abi/woeth' import { OTokenActivity, OTokenActivityType } from '@model' @@ -12,10 +13,11 @@ import { MintActivity, RedeemActivity, SwapActivity, + TransferActivity, UnwrapActivity, WrapActivity, } from '@templates/otoken/activity-types' -import { BALANCER_VAULT_ADDRESS } from '@utils/addresses' +import { BALANCER_VAULT_ADDRESS, ETH_ADDRESS, ONEINCH_AGGREGATION_ROUTER_ADDRESS } from '@utils/addresses' import { LogFilter, logFilter } from '@utils/logFilter' interface Input { @@ -38,10 +40,20 @@ export interface ActivityProcessor { export const createOTokenActivityProcessor = (params: Input) => { const processors: ActivityProcessor[] = compact([ + // Swaps ...params.curvePools.map((pool) => curveActivityProcessor(pool)), + + // Swaps balancerActivityProcessor({ pools: params.balancerPools }), + + // Wraps & Unwraps params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), + + // Mints & Redeems vaultActivityProcessor({ otokenAddress: params.otokenAddress, vaultAddress: params.vaultAddress }), + + // Transfers & Swaps + transferActivityProcessor({ otokenAddress: params.otokenAddress }), ]) const from = params.from @@ -93,12 +105,7 @@ export const createOTokenActivityProcessor = (params: Input) => { const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: string[] }): ActivityProcessor => { return { name: 'Curve Pool Processor', - filters: [ - logFilter({ - address: [address], - topic0: [curvePoolAbi.events.TokenExchange.topic], - }), - ], + filters: [logFilter({ address: [address], topic0: [curvePoolAbi.events.TokenExchange.topic] })], async process(ctx: Context, block: Block, logs: Log[]): Promise { const [tokenExchangeFilter] = this.filters return logs @@ -112,8 +119,8 @@ const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: contract: log.address, tokenIn: tokens[Number(tokenExchange.sold_id)], tokenOut: tokens[Number(tokenExchange.bought_id)], - amountIn: tokenExchange.tokens_sold, - amountOut: tokenExchange.tokens_bought, + amountIn: tokenExchange.tokens_sold.toString(), + amountOut: tokenExchange.tokens_bought.toString(), }) }) }, @@ -144,8 +151,8 @@ const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProc contract: swap.poolId, tokenIn: swap.tokenIn, tokenOut: swap.tokenOut, - amountIn: swap.amountIn, - amountOut: swap.amountOut, + amountIn: swap.amountIn.toString(), + amountOut: swap.amountOut.toString(), }) }) }, @@ -153,67 +160,51 @@ const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProc } const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor => { + const depositFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Deposit.topic] }) + const withdrawFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Withdraw.topic] }) + const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [wrappedAddress] }) + const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [wrappedAddress] }) return { name: 'Wrapped Processor', - filters: [ - logFilter({ - address: [wrappedAddress], - topic0: [wotokenAbi.events.Deposit.topic], - }), - logFilter({ - address: [wrappedAddress], - topic0: [wotokenAbi.events.Withdraw.topic], - }), - ], + filters: [depositFilter, withdrawFilter, transferInFilter, transferOutFilter], async process(ctx: Context, block: Block, logs: Log[]) { const result: (WrapActivity | UnwrapActivity)[] = [] - const [depositFilter, withdrawFilter] = this.filters - // Deposits + // Wrap const depositLogs = logs.filter((l) => depositFilter.matches(l)) if (depositLogs.length) { - const transferInFilter = logFilter({ - topic0: [wotokenAbi.events.Transfer.topic], - topic2: [wrappedAddress], - }) - const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) + const transferInLog = logs.find((l) => transferInFilter.matches(l)) result.push( ...depositLogs.map((log) => { const data = wotokenAbi.events.Deposit.decode(log) - const tokenIn = transferInLogs[0].address - const amountIn = transferInLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + const tokenIn = transferInLog?.address ?? 'unknown' return createActivity(ctx, block, log, { type: 'Wrap', contract: wrappedAddress, account: data.owner, tokenIn, tokenOut: wrappedAddress, - amountIn, - amountOut: data.shares, + amountIn: data.assets.toString(), + amountOut: data.shares.toString(), }) }), ) } - // Withdrawals + // Unwrap const withdrawLogs = logs.filter((l) => withdrawFilter.matches(l)) if (withdrawLogs.length) { - const transferOutFilter = logFilter({ - topic0: [wotokenAbi.events.Transfer.topic], - topic1: [wrappedAddress], - }) - const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) + const transferOutLog = logs.find((l) => transferOutFilter.matches(l)) result.push( ...withdrawLogs.map((log) => { const data = wotokenAbi.events.Withdraw.decode(log) - const tokenOut = transferOutLogs[0].address - const amountOut = transferOutLogs.reduce((sum, l) => sum + wotokenAbi.events.Withdraw.decode(l).assets, 0n) + const tokenOut = transferOutLog?.address ?? 'unknown' return createActivity(ctx, block, log, { type: 'Unwrap', contract: wrappedAddress, account: data.owner, tokenIn: wrappedAddress, tokenOut, - amountIn: data.shares, - amountOut, + amountIn: data.shares.toString(), + amountOut: data.assets.toString(), }) }), ) @@ -232,20 +223,19 @@ const vaultActivityProcessor = ({ }): ActivityProcessor => { const mintFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Mint.topic] }) const redeemFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Redeem.topic] }) + const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [vaultAddress] }) + const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [vaultAddress] }) return { name: 'Vault Processor', - filters: [mintFilter, redeemFilter], + filters: [mintFilter, redeemFilter, transferInFilter, transferOutFilter], process: async (ctx, block, logs) => { const result: (MintActivity | RedeemActivity)[] = [] + // Mint const mintLogs = logs.filter((l) => mintFilter.matches(l)) if (mintLogs.length) { - const transferInFilter = logFilter({ - topic0: [wotokenAbi.events.Transfer.topic], - topic2: [vaultAddress], - }) const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) - const tokenIn = transferInLogs[0].address - const amountIn = transferInLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + const tokenIn = transferInLogs[0]?.address ?? ETH_ADDRESS + const amountIn = mintLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Mint.decode(l)._value, 0n) result.push( ...mintLogs.map((log) => { const data = otokenVaultAbi.events.Mint.decode(log) @@ -254,33 +244,30 @@ const vaultActivityProcessor = ({ contract: log.address, account: data._addr, tokenIn, - amountIn, + amountIn: amountIn.toString(), tokenOut: otokenAddress, - amountOut: data._value, + amountOut: data._value.toString(), }) }), ) } + // Redeem const redeemLogs = logs.filter((l) => redeemFilter.matches(l)) if (redeemLogs.length) { - const transferOutFilter = logFilter({ - topic0: [wotokenAbi.events.Transfer.topic], - topic1: [vaultAddress], - }) const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) const tokensOut = uniq(transferOutLogs.map((l) => l.address)) - const amountOut = transferOutLogs.reduce((sum, l) => sum + wotokenAbi.events.Deposit.decode(l).assets, 0n) + const amountOut = redeemLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Redeem.decode(l)._value, 0n) result.push( - ...mintLogs.map((log) => { + ...redeemLogs.map((log) => { const data = otokenVaultAbi.events.Redeem.decode(log) return createActivity(ctx, block, log, { type: 'Redeem', contract: log.address, account: data._addr, tokenIn: otokenAddress, - amountIn: data._value, - tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0], - amountOut, + amountIn: data._value.toString(), + tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0] ?? ETH_ADDRESS, + amountOut: amountOut.toString(), }) }), ) @@ -290,6 +277,105 @@ const vaultActivityProcessor = ({ } } +const transferActivityProcessor = ({ + otokenAddress, +}: { + otokenAddress: string +}): ActivityProcessor => { + const transferFilter = logFilter({ + address: [otokenAddress], + topic0: [otokenAbi.events.Transfer.topic], + }) + return { + name: 'Transfer Activity', + filters: [transferFilter], + process: async (ctx, block, logs) => { + const transferLogs = logs + .filter((l) => transferFilter.matches(l)) + .map((log) => ({ + log, + data: otokenAbi.events.Transfer.decode(log), + })) + + const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs, ONEINCH_AGGREGATION_ROUTER_ADDRESS) + if (swapActivity) return swapActivity + + return transferLogs.map(({ log, data }) => { + return createActivity(ctx, block, log, { + type: 'Transfer', + token: log.address, + from: data.from.toLowerCase(), + to: data.to.toLowerCase(), + amount: data.value.toString(), + }) + }) + }, + } +} + +const calculateTransferActivityAsSwap = ( + ctx: Context, + block: Block, + logs: { + log: Log + data: ReturnType + }[], + swapperAddress: string, +) => { + const swapperLogs = logs.filter( + ({ log, data }) => data.from.toLowerCase() === swapperAddress || data.to.toLowerCase() === swapperAddress, + ) + if (swapperLogs.length === 2) { + const changes = calculateBalanceChanges(swapperLogs) + let account: string = '' + let tokenIn: string = '' + let tokenOut: string = '' + let amountIn: bigint = 0n + let amountOut: bigint = 0n + for (const { token, from, to, value } of changes) { + if (from === swapperAddress) { + account = to + tokenOut = token + amountOut = value + } else if (to === swapperAddress) { + tokenIn = token + amountIn = value + } + } + return [ + createActivity(ctx, block, swapperLogs[0].log, { + type: 'Swap', + account, + exchange: 'Curve', + contract: swapperAddress, + tokenIn, + tokenOut, + amountIn: amountIn.toString(), + amountOut: amountOut.toString(), + }), + ] + } + return undefined +} + +const calculateBalanceChanges = ( + logs: { + log: Log + data: ReturnType + }[], +) => { + return Object.entries( + logs.reduce>((acc, { log, data }) => { + const key = `${log.address}:${data.from.toLowerCase()}:${data.to.toLowerCase()}` + acc[key] = (acc[key] || 0n) + data.value + return acc + }, {}), + ).map(([data, value]) => { + const [token, from, to] = data.split(':') + return { token, from, to, value } + }) +} + const createActivity = ( ctx: Context, block: Block, diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 4ae210ca..20ad08f1 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -7,40 +7,45 @@ export interface ActivityBase { status: ActivityStatus txHash: string chainId: number - account: string } export interface ApprovalActivity extends ActivityBase { type: 'Approval' + account: string tokenIn: string - amountIn: bigint + amountIn: string } export interface BridgeActivity extends ActivityBase { type: 'Bridge' + from: string + to: string chainIn: number chainOut: number tokenIn: string tokenOut: string - amountIn: bigint + amountIn: string } export interface ClaimRewardsActivity extends ActivityBase { type: 'ClaimRewards' + account: string tokenIn: string - amountIn: bigint + amountIn: string } export interface DelegateVoteActivity extends ActivityBase { type: 'DelegateVote' + account: string tokenIn: string - votingPower: bigint + votingPower: string delegateTo: string } export interface ExtendStakeActivity extends ActivityBase { type: 'ExtendStake' - amountIn: bigint + account: string + amountIn: string tokenIn: string monthDuration: number lockupId: string @@ -48,23 +53,26 @@ export interface ExtendStakeActivity extends ActivityBase { export interface MigrateActivity extends ActivityBase { type: 'Migrate' - amountIn: bigint + account: string + amountIn: string tokenIn: string tokenIdStaked: string tokenIdLiquid: string - liquid?: bigint - staked?: bigint + liquid?: string + staked?: string } export interface StakeActivity extends ActivityBase { type: 'Stake' + account: string tokenIn: string - amountIn: bigint + amountIn: string monthDuration: number } export interface UnstakeActivity extends ActivityBase { type: 'Unstake' + account: string tokenIn: string tokenOut: string lockupId: string @@ -72,52 +80,66 @@ export interface UnstakeActivity extends ActivityBase { export interface SwapActivity extends ActivityBase { type: 'Swap' + account: string exchange: 'Curve' | 'Balancer' contract: string tokenIn: string tokenOut: string - amountIn: bigint - amountOut: bigint + amountIn: string + amountOut: string +} + +export interface TransferActivity extends ActivityBase { + type: 'Transfer' + token: string + from: string + to: string + amount: string } export interface WrapActivity extends ActivityBase { type: 'Wrap' + account: string contract: string tokenIn: string tokenOut: string - amountIn: bigint - amountOut: bigint + amountIn: string + amountOut: string } export interface UnwrapActivity extends ActivityBase { type: 'Unwrap' + account: string contract: string tokenIn: string tokenOut: string - amountIn: bigint - amountOut: bigint + amountIn: string + amountOut: string } export interface MintActivity extends ActivityBase { type: 'Mint' + account: string contract: string tokenIn: string tokenOut: string - amountIn: bigint - amountOut: bigint + amountIn: string + amountOut: string } export interface RedeemActivity extends ActivityBase { type: 'Redeem' + account: string contract: string tokenIn: string tokenOut: string - amountIn: bigint - amountOut: bigint + amountIn: string + amountOut: string } export interface VoteActivity extends ActivityBase { type: 'Vote' + account: string tokenIn: string choice: string proposalId: string @@ -132,6 +154,7 @@ export type Activity = | MigrateActivity | StakeActivity | UnstakeActivity + | TransferActivity | SwapActivity | WrapActivity | UnwrapActivity diff --git a/src/utils/activityFromTx.ts b/src/utils/activityFromTx.ts index ec2ad180..a0db584d 100644 --- a/src/utils/activityFromTx.ts +++ b/src/utils/activityFromTx.ts @@ -1,236 +1,223 @@ -import { compact } from 'lodash' -import { GetTransactionReceiptReturnType, decodeEventLog, parseAbi } from 'viem' - -import * as balancerVaultAbi from '@abi/balancer-vault.abi' -import * as curveLp from '@abi/curve-lp-token' -import * as curveLpAbi from '@abi/curve-lp-token.abi' -import * as oethAbi from '@abi/oeth.abi' -import * as oethVaultAbi from '@abi/otoken-vault.abi' -import { Block, Context, Log } from '@processor' -import { Activity, SwapActivity } from '@templates/otoken/activity-types' -import { logFilter } from '@utils/logFilter' - -import { - BALANCER_VAULT_ADDRESS, - COWSWAP_SETTLEMENT_ADDRESS, - CURVE_ETH_OETH_POOL_ADDRESS, - CURVE_FRXETH_OETH_POOL_ADDRESS, - OETH_ADDRESS, - OETH_VAULT_ADDRESS, - OETH_ZAPPER_ADDRESS, - ONEINCH_AGGREGATION_ROUTER_ADDRESS, - UNISWAP_OETH_WEH_ADDRESS, - WOETH_ADDRESS, -} from './addresses' - -const UniswapV3SwapAbi = parseAbi([ - 'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)', -]) -const GnosisSafeAbi = parseAbi(['event ExecutionSuccess(bytes32 txHash, uint256 payment)']) -const WrappedOETHAbi = parseAbi([ - 'event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares)', - 'event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)', -]) - -const tryDecodeEventLog = (...params: Parameters) => { - try { - return decodeEventLog(...params) - } catch (err: unknown) { - return undefined - } -} - -export interface Transaction { - to: string - from: string - input: string - value: bigint -} - -type Transfer = { - eventName: string - args: { - from: string - to: string - value: bigint - } -} - -type NetTransfers = { - [address: string]: bigint -} - -export async function activityFromTx(ctx: Context, block: Block, log: Log) { - const sansGnosisSafeEvents = logs.filter(({ data, topics }) => { - try { - decodeEventLog({ - abi: GnosisSafeAbi, - data, - topics, - strict: false, - }) - return false - } catch (e) { - return true - } - }) - - const oethEvents = compact( - logs - .filter((l) => l.address === OETH_ADDRESS) - .map((log) => { - return tryDecodeEventLog({ - abi: oethAbi.ABI_JSON, - data: log.data, - topics: log.topics, - }) - }), - ) - - const oethVaultEvents = compact( - logs - .filter((l) => l.address === OETH_VAULT_ADDRESS) - .map((log) => { - return tryDecodeEventLog({ - abi: oethVaultAbi.ABI_JSON, - data: log.data, - topics: log.topics, - }) - }), - ) - - const oneInchEvents = logs.filter((l) => l.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) - const uniswapWethEvents = compact( - logs - .filter((l) => l.address === UNISWAP_OETH_WEH_ADDRESS) - .map((log) => - tryDecodeEventLog({ - abi: UniswapV3SwapAbi, - data: log.data, - topics: log.topics, - }), - ), - ) - - const oethTransfers = compact(oethEvents).filter((log) => log.eventName === 'Transfer') as Transfer[] - - const netTransfers = oethTransfers.reduce((acc, { args: { from, to, value } }) => { - acc[from] = (acc[from] || 0n) - value - acc[to] = (acc[to] || 0n) + value - return acc - }, {}) - const nonZeroBalances: { [address: string]: bigint } = Object.keys(netTransfers).reduce( - (acc, key) => { - if (netTransfers[key] !== 0n) { - acc[key] = netTransfers[key] - } - return acc - }, - {} as { [address: string]: bigint }, - ) - - const sumPositiveBalances = (balances: { [address: string]: bigint }): bigint => { - return Object.values(balances).reduce((sum, value) => { - if (value > 0n) { - return sum + value - } - return sum - }, 0n) - } - - const totalPositiveBalance = sumPositiveBalances(nonZeroBalances) - - let data: - | { - callDataLast4Bytes?: string - exchange?: string - action?: string - fromSymbol?: string - toSymbol?: string - interface?: string - amount?: bigint - } - | undefined = {} - - data.callDataLast4Bytes = transaction?.input.slice(-8) - data.amount = totalPositiveBalance - - if (oethVaultEvents.some((e) => e.eventName === 'Redeem')) { - data.action = 'Redeem' - } else if (oethEvents.some((e) => e.eventName === 'TotalSupplyUpdatedHighres')) { - data.action = 'Rebase' - } - - if (transaction.to === ONEINCH_AGGREGATION_ROUTER_ADDRESS || oneInchEvents.length > 0) { - data.action = 'Swap' - data.interface = '1inch' - } - if (transaction.to === COWSWAP_SETTLEMENT_ADDRESS) { - data.action = 'Swap' - data.exchange = 'CoW Swap' - } - if (transaction.to === '0x6131b5fae19ea4f9d964eac0408e4408b66337b5') { - data.action = 'Swap' - data.exchange = 'Kyber Swap' - } - - if (frxEthOETHCurvePoolEvents.length > 0) { - data.action = 'Swap' - data.exchange = 'Curve' - } - if (balancerVaultEvents.length > 0) { - data.action = 'Swap' - data.exchange = 'Balancer' - } - - if (uniswapWethEvents.length > 0) { - data.action = 'Swap' - data.exchange = 'UniswapV3' - } - if (woethEvents.find((e) => e?.eventName === 'Deposit')) { - data.action = 'Wrap' - data.exchange = 'WOETH' - } else if (woethEvents.find((e) => e?.eventName === 'Withdraw')) { - data.action = 'Un-wrap' - data.exchange = 'WOETH' - } - - if ( - oethTransfers.some( - (t) => - t.args.from.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS || - t.args.to.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS, - ) - ) { - data.exchange = 'CoW Swap' - } else if ( - oethTransfers.some( - (t) => - t.args.from.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS || - t.args.to.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS, - ) - ) { - data.exchange = 'Curve' - } - if (oethTransfers.length === 1 && sansGnosisSafeEvents.length === 1) { - data.action = 'Transfer' - } - - if (transaction.to === OETH_ZAPPER_ADDRESS) { - data = decodeOethZapperTx(transaction) - } - - activity.push(data) - - return activity -} - -function decodeOethZapperTx(transaction: Transaction) { - return { - callDataLast4Bytes: transaction?.input.slice(-8), - exchange: 'OETHZapper', - action: 'Swap', - fromSymbol: 'ETH', - toSymbol: 'OETH', - } -} +// import { compact } from 'lodash' +// import { GetTransactionReceiptReturnType, decodeEventLog, parseAbi } from 'viem' +// +// import * as balancerVaultAbi from '@abi/balancer-vault.abi' +// import * as curveLp from '@abi/curve-lp-token' +// import * as curveLpAbi from '@abi/curve-lp-token.abi' +// import * as oethAbi from '@abi/oeth.abi' +// import * as oethVaultAbi from '@abi/otoken-vault.abi' +// import { Block, Context, Log } from '@processor' +// import { Activity, SwapActivity } from '@templates/otoken/activity-types' +// import { logFilter } from '@utils/logFilter' +// +// import { +// BALANCER_VAULT_ADDRESS, +// COWSWAP_SETTLEMENT_ADDRESS, +// CURVE_ETH_OETH_POOL_ADDRESS, +// CURVE_FRXETH_OETH_POOL_ADDRESS, +// OETH_ADDRESS, +// OETH_VAULT_ADDRESS, +// OETH_ZAPPER_ADDRESS, +// ONEINCH_AGGREGATION_ROUTER_ADDRESS, +// UNISWAP_OETH_WEH_ADDRESS, +// WOETH_ADDRESS, +// } from './addresses' +// +// const UniswapV3SwapAbi = parseAbi([ +// 'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)', +// ]) +// const GnosisSafeAbi = parseAbi(['event ExecutionSuccess(bytes32 txHash, uint256 payment)']) +// const WrappedOETHAbi = parseAbi([ +// 'event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares)', +// 'event Withdraw(address indexed caller, address indexed receiver, address indexed owner, uint256 assets, uint256 shares)', +// ]) +// +// const tryDecodeEventLog = (...params: Parameters) => { +// try { +// return decodeEventLog(...params) +// } catch (err: unknown) { +// return undefined +// } +// } +// +// export interface Transaction { +// to: string +// from: string +// input: string +// value: bigint +// } +// +// type Transfer = { +// eventName: string +// args: { +// from: string +// to: string +// value: bigint +// } +// } +// +// type NetTransfers = { +// [address: string]: bigint +// } +// +// export async function activityFromTx(ctx: Context, block: Block, log: Log) { +// const sansGnosisSafeEvents = logs.filter(({ data, topics }) => { +// try { +// decodeEventLog({ +// abi: GnosisSafeAbi, +// data, +// topics, +// strict: false, +// }) +// return false +// } catch (e) { +// return true +// } +// }) +// +// const oethEvents = compact( +// logs +// .filter((l) => l.address === OETH_ADDRESS) +// .map((log) => { +// return tryDecodeEventLog({ +// abi: oethAbi.ABI_JSON, +// data: log.data, +// topics: log.topics, +// }) +// }), +// ) +// +// const oethVaultEvents = compact( +// logs +// .filter((l) => l.address === OETH_VAULT_ADDRESS) +// .map((log) => { +// return tryDecodeEventLog({ +// abi: oethVaultAbi.ABI_JSON, +// data: log.data, +// topics: log.topics, +// }) +// }), +// ) +// +// const oneInchEvents = logs.filter((l) => l.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) +// const uniswapWethEvents = compact( +// logs +// .filter((l) => l.address === UNISWAP_OETH_WEH_ADDRESS) +// .map((log) => +// tryDecodeEventLog({ +// abi: UniswapV3SwapAbi, +// data: log.data, +// topics: log.topics, +// }), +// ), +// ) +// +// const oethTransfers = compact(oethEvents).filter((log) => log.eventName === 'Transfer') as Transfer[] +// +// const netTransfers = oethTransfers.reduce((acc, { args: { from, to, value } }) => { +// acc[from] = (acc[from] || 0n) - value +// acc[to] = (acc[to] || 0n) + value +// return acc +// }, {}) +// const nonZeroBalances: { [address: string]: bigint } = Object.keys(netTransfers).reduce( +// (acc, key) => { +// if (netTransfers[key] !== 0n) { +// acc[key] = netTransfers[key] +// } +// return acc +// }, +// {} as { [address: string]: bigint }, +// ) +// +// const sumPositiveBalances = (balances: { [address: string]: bigint }): bigint => { +// return Object.values(balances).reduce((sum, value) => { +// if (value > 0n) { +// return sum + value +// } +// return sum +// }, 0n) +// } +// +// const totalPositiveBalance = sumPositiveBalances(nonZeroBalances) +// +// let data: +// | { +// callDataLast4Bytes?: string +// exchange?: string +// action?: string +// fromSymbol?: string +// toSymbol?: string +// interface?: string +// amount?: bigint +// } +// | undefined = {} +// +// data.callDataLast4Bytes = transaction?.input.slice(-8) +// data.amount = totalPositiveBalance +// +// if (transaction.to === ONEINCH_AGGREGATION_ROUTER_ADDRESS || oneInchEvents.length > 0) { +// data.action = 'Swap' +// data.interface = '1inch' +// } +// if (transaction.to === COWSWAP_SETTLEMENT_ADDRESS) { +// data.action = 'Swap' +// data.exchange = 'CoW Swap' +// } +// if (transaction.to === '0x6131b5fae19ea4f9d964eac0408e4408b66337b5') { +// data.action = 'Swap' +// data.exchange = 'Kyber Swap' +// } +// +// if (frxEthOETHCurvePoolEvents.length > 0) { +// data.action = 'Swap' +// data.exchange = 'Curve' +// } +// if (balancerVaultEvents.length > 0) { +// data.action = 'Swap' +// data.exchange = 'Balancer' +// } +// +// if (uniswapWethEvents.length > 0) { +// data.action = 'Swap' +// data.exchange = 'UniswapV3' +// } +// +// if ( +// oethTransfers.some( +// (t) => +// t.args.from.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS || +// t.args.to.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS, +// ) +// ) { +// data.exchange = 'CoW Swap' +// } else if ( +// oethTransfers.some( +// (t) => +// t.args.from.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS || +// t.args.to.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS, +// ) +// ) { +// data.exchange = 'Curve' +// } +// if (oethTransfers.length === 1 && sansGnosisSafeEvents.length === 1) { +// data.action = 'Transfer' +// } +// +// if (transaction.to === OETH_ZAPPER_ADDRESS) { +// data = decodeOethZapperTx(transaction) +// } +// +// activity.push(data) +// +// return activity +// } +// +// function decodeOethZapperTx(transaction: Transaction) { +// return { +// callDataLast4Bytes: transaction?.input.slice(-8), +// exchange: 'OETHZapper', +// action: 'Swap', +// fromSymbol: 'ETH', +// toSymbol: 'OETH', +// } +// } From a754092b811f1b123fb28f8ce266577c74bc7eb6 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 18 Jun 2024 20:38:23 -0700 Subject: [PATCH 04/15] wip activity rework --- abi/cow-swap-settlement.json | 581 ++++++ abi/one-inch-aggregation-router.json | 2188 ++++++++++++++++++++ src/abi/cow-swap-settlement.ts | 114 + src/templates/otoken/activity-processor.ts | 306 +-- src/templates/otoken/activity-types.ts | 8 +- 5 files changed, 3071 insertions(+), 126 deletions(-) create mode 100644 abi/cow-swap-settlement.json create mode 100644 abi/one-inch-aggregation-router.json create mode 100644 src/abi/cow-swap-settlement.ts diff --git a/abi/cow-swap-settlement.json b/abi/cow-swap-settlement.json new file mode 100644 index 00000000..683bda5e --- /dev/null +++ b/abi/cow-swap-settlement.json @@ -0,0 +1,581 @@ +[ + { + "inputs": [ + { + "internalType": "contract GPv2Authentication", + "name": "authenticator_", + "type": "address" + }, + { + "internalType": "contract IVault", + "name": "vault_", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes4", + "name": "selector", + "type": "bytes4" + } + ], + "name": "Interaction", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "OrderInvalidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "indexed": false, + "internalType": "bool", + "name": "signed", + "type": "bool" + } + ], + "name": "PreSignature", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "solver", + "type": "address" + } + ], + "name": "Settlement", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "sellToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "contract IERC20", + "name": "buyToken", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "Trade", + "type": "event" + }, + { + "inputs": [], + "name": "authenticator", + "outputs": [ + { + "internalType": "contract GPv2Authentication", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "domainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "filledAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "orderUids", + "type": "bytes[]" + } + ], + "name": "freeFilledAmountStorage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "orderUids", + "type": "bytes[]" + } + ], + "name": "freePreSignatureStorage", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offset", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "getStorageAt", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + } + ], + "name": "invalidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "preSignature", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "orderUid", + "type": "bytes" + }, + { + "internalType": "bool", + "name": "signed", + "type": "bool" + } + ], + "name": "setPreSignature", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "clearingPrices", + "type": "uint256[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "sellTokenIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyTokenIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "flags", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executedAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct GPv2Trade.Data[]", + "name": "trades", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callData", + "type": "bytes" + } + ], + "internalType": "struct GPv2Interaction.Data[][3]", + "name": "interactions", + "type": "tuple[][3]" + } + ], + "name": "settle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { + "internalType": "bytes", + "name": "calldataPayload", + "type": "bytes" + } + ], + "name": "simulateDelegatecall", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "targetContract", + "type": "address" + }, + { + "internalType": "bytes", + "name": "calldataPayload", + "type": "bytes" + } + ], + "name": "simulateDelegatecallInternal", + "outputs": [ + { + "internalType": "bytes", + "name": "response", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "poolId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "assetInIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "assetOutIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "userData", + "type": "bytes" + } + ], + "internalType": "struct IVault.BatchSwapStep[]", + "name": "swaps", + "type": "tuple[]" + }, + { + "internalType": "contract IERC20[]", + "name": "tokens", + "type": "address[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "sellTokenIndex", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyTokenIndex", + "type": "uint256" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "sellAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyAmount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "validTo", + "type": "uint32" + }, + { + "internalType": "bytes32", + "name": "appData", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "flags", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executedAmount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + } + ], + "internalType": "struct GPv2Trade.Data", + "name": "trade", + "type": "tuple" + } + ], + "name": "swap", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract IVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "vaultRelayer", + "outputs": [ + { + "internalType": "contract GPv2VaultRelayer", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/abi/one-inch-aggregation-router.json b/abi/one-inch-aggregation-router.json new file mode 100644 index 00000000..080dfd11 --- /dev/null +++ b/abi/one-inch-aggregation-router.json @@ -0,0 +1,2188 @@ +[ + { + "inputs": [ + { + "internalType": "contract IWETH", + "name": "weth", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "AccessDenied", + "type": "error" + }, + { + "inputs": [], + "name": "AdvanceNonceFailed", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyFilled", + "type": "error" + }, + { + "inputs": [], + "name": "ArbitraryStaticCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "BadPool", + "type": "error" + }, + { + "inputs": [], + "name": "BadSignature", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "ETHTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyPools", + "type": "error" + }, + { + "inputs": [], + "name": "EthDepositRejected", + "type": "error" + }, + { + "inputs": [], + "name": "GetAmountCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "IncorrectDataLength", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidMsgValue", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidMsgValue", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidatedOrder", + "type": "error" + }, + { + "inputs": [], + "name": "MakingAmountExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "MakingAmountTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "OnlyOneAmountShouldBeZero", + "type": "error" + }, + { + "inputs": [], + "name": "OrderExpired", + "type": "error" + }, + { + "inputs": [], + "name": "PermitLengthTooLow", + "type": "error" + }, + { + "inputs": [], + "name": "PredicateIsNotTrue", + "type": "error" + }, + { + "inputs": [], + "name": "PrivateOrder", + "type": "error" + }, + { + "inputs": [], + "name": "RFQBadSignature", + "type": "error" + }, + { + "inputs": [], + "name": "RFQPrivateOrder", + "type": "error" + }, + { + "inputs": [], + "name": "RFQSwapWithZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "RFQZeroTargetIsForbidden", + "type": "error" + }, + { + "inputs": [], + "name": "ReentrancyDetected", + "type": "error" + }, + { + "inputs": [], + "name": "RemainingAmountIsZero", + "type": "error" + }, + { + "inputs": [], + "name": "ReservesCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "ReturnAmountIsNotEnough", + "type": "error" + }, + { + "inputs": [], + "name": "SafePermitBadLength", + "type": "error" + }, + { + "inputs": [], + "name": "SafeTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "SafeTransferFromFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "res", + "type": "bytes" + } + ], + "name": "SimulationResults", + "type": "error" + }, + { + "inputs": [], + "name": "SwapAmountTooLarge", + "type": "error" + }, + { + "inputs": [], + "name": "SwapWithZeroAmount", + "type": "error" + }, + { + "inputs": [], + "name": "TakingAmountExceeded", + "type": "error" + }, + { + "inputs": [], + "name": "TakingAmountIncreased", + "type": "error" + }, + { + "inputs": [], + "name": "TakingAmountTooHigh", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFromMakerToTakerFailed", + "type": "error" + }, + { + "inputs": [], + "name": "TransferFromTakerToMakerFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownOrder", + "type": "error" + }, + { + "inputs": [], + "name": "WrongAmount", + "type": "error" + }, + { + "inputs": [], + "name": "WrongGetter", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroMinReturn", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroReturnAmount", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroTargetIsForbidden", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newNonce", + "type": "uint256" + } + ], + "name": "NonceIncreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "remainingRaw", + "type": "uint256" + } + ], + "name": "OrderCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "remaining", + "type": "uint256" + } + ], + "name": "OrderFilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + } + ], + "name": "OrderFilledRFQ", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "amount", + "type": "uint8" + } + ], + "name": "advanceNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "and", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "arbitraryStaticCall", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "cancelOrder", + "outputs": [ + { + "internalType": "uint256", + "name": "orderRemaining", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "orderInfo", + "type": "uint256" + } + ], + "name": "cancelOrderRFQ", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "orderInfo", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "additionalMask", + "type": "uint256" + } + ], + "name": "cancelOrderRFQ", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "checkPredicate", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IClipperExchangeInterface", + "name": "clipperExchange", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "dstToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "inputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "goodUntil", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "vs", + "type": "bytes32" + } + ], + "name": "clipperSwap", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IClipperExchangeInterface", + "name": "clipperExchange", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "dstToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "inputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "goodUntil", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "vs", + "type": "bytes32" + } + ], + "name": "clipperSwapTo", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IClipperExchangeInterface", + "name": "clipperExchange", + "type": "address" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "dstToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "inputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "outputAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "goodUntil", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "vs", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "name": "clipperSwapToWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "destroy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "eq", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "interaction", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "skipPermitAndThresholdAmount", + "type": "uint256" + } + ], + "name": "fillOrder", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + } + ], + "name": "fillOrderRFQ", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "vs", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + } + ], + "name": "fillOrderRFQCompact", + "outputs": [ + { + "internalType": "uint256", + "name": "filledMakingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "filledTakingAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "fillOrderRFQTo", + "outputs": [ + { + "internalType": "uint256", + "name": "filledMakingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "filledTakingAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "info", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + } + ], + "internalType": "struct OrderRFQLib.OrderRFQ", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "flagsAndAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "name": "fillOrderRFQToWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order_", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "interaction", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "skipPermitAndThresholdAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "fillOrderTo", + "outputs": [ + { + "internalType": "uint256", + "name": "actualMakingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actualTakingAmount", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "interaction", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "skipPermitAndThresholdAmount", + "type": "uint256" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "name": "fillOrderToWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "gt", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "makerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "takerAsset", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "address", + "name": "allowedSender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "takingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "interactions", + "type": "bytes" + } + ], + "internalType": "struct OrderLib.Order", + "name": "order", + "type": "tuple" + } + ], + "name": "hashOrder", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "increaseNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "uint256", + "name": "slot", + "type": "uint256" + } + ], + "name": "invalidatorForOrderRFQ", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "lt", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "makerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerNonce", + "type": "uint256" + } + ], + "name": "nonceEquals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "offsets", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "or", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "remaining", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "remainingRaw", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "orderHashes", + "type": "bytes32[]" + } + ], + "name": "remainingsRaw", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "rescueFunds", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "simulate", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAggregationExecutor", + "name": "executor", + "type": "address" + }, + { + "components": [ + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "dstToken", + "type": "address" + }, + { + "internalType": "address payable", + "name": "srcReceiver", + "type": "address" + }, + { + "internalType": "address payable", + "name": "dstReceiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturnAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "flags", + "type": "uint256" + } + ], + "internalType": "struct GenericRouter.SwapDescription", + "name": "desc", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "spentAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "time", + "type": "uint256" + } + ], + "name": "timestampBelow", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "timeNonceAccount", + "type": "uint256" + } + ], + "name": "timestampBelowAndNonceEquals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + } + ], + "name": "uniswapV3Swap", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + } + ], + "name": "uniswapV3SwapTo", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "name": "uniswapV3SwapToWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + } + ], + "name": "unoswap", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + } + ], + "name": "unoswapTo", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address payable", + "name": "recipient", + "type": "address" + }, + { + "internalType": "contract IERC20", + "name": "srcToken", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "minReturn", + "type": "uint256" + }, + { + "internalType": "uint256[]", + "name": "pools", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "name": "unoswapToWithPermit", + "outputs": [ + { + "internalType": "uint256", + "name": "returnAmount", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] \ No newline at end of file diff --git a/src/abi/cow-swap-settlement.ts b/src/abi/cow-swap-settlement.ts new file mode 100644 index 00000000..fc41da69 --- /dev/null +++ b/src/abi/cow-swap-settlement.ts @@ -0,0 +1,114 @@ +import * as p from '@subsquid/evm-codec' +import { event, fun, indexed, ContractBase } from '@subsquid/evm-abi' +import type { EventParams as EParams, FunctionArguments, FunctionReturn } from '@subsquid/evm-abi' + +export const events = { + Interaction: event("0xed99827efb37016f2275f98c4bcf71c7551c75d59e9b450f79fa32e60be672c2", {"target": indexed(p.address), "value": p.uint256, "selector": p.bytes4}), + OrderInvalidated: event("0x875b6cb035bbd4ac6500fabc6d1e4ca5bdc58a3e2b424ccb5c24cdbebeb009a9", {"owner": indexed(p.address), "orderUid": p.bytes}), + PreSignature: event("0x01bf7c8b0ca55deecbea89d7e58295b7ffbf685fd0d96801034ba8c6ffe1c68d", {"owner": indexed(p.address), "orderUid": p.bytes, "signed": p.bool}), + Settlement: event("0x40338ce1a7c49204f0099533b1e9a7ee0a3d261f84974ab7af36105b8c4e9db4", {"solver": indexed(p.address)}), + Trade: event("0xa07a543ab8a018198e99ca0184c93fe9050a79400a0a723441f84de1d972cc17", {"owner": indexed(p.address), "sellToken": p.address, "buyToken": p.address, "sellAmount": p.uint256, "buyAmount": p.uint256, "feeAmount": p.uint256, "orderUid": p.bytes}), +} + +export const functions = { + authenticator: fun("0x2335c76b", {}, p.address), + domainSeparator: fun("0xf698da25", {}, p.bytes32), + filledAmount: fun("0x2479fb6e", {"_0": p.bytes}, p.uint256), + freeFilledAmountStorage: fun("0xed9f35ce", {"orderUids": p.array(p.bytes)}, ), + freePreSignatureStorage: fun("0xa2a7d51b", {"orderUids": p.array(p.bytes)}, ), + getStorageAt: fun("0x5624b25b", {"offset": p.uint256, "length": p.uint256}, p.bytes), + invalidateOrder: fun("0x15337bc0", {"orderUid": p.bytes}, ), + preSignature: fun("0xd08d33d1", {"_0": p.bytes}, p.uint256), + setPreSignature: fun("0xec6cb13f", {"orderUid": p.bytes, "signed": p.bool}, ), + settle: fun("0x13d79a0b", {"tokens": p.array(p.address), "clearingPrices": p.array(p.uint256), "trades": p.array(p.struct({"sellTokenIndex": p.uint256, "buyTokenIndex": p.uint256, "receiver": p.address, "sellAmount": p.uint256, "buyAmount": p.uint256, "validTo": p.uint32, "appData": p.bytes32, "feeAmount": p.uint256, "flags": p.uint256, "executedAmount": p.uint256, "signature": p.bytes})), "interactions": p.fixedSizeArray(p.array(p.struct({"target": p.address, "value": p.uint256, "callData": p.bytes})), 3)}, ), + simulateDelegatecall: fun("0xf84436bd", {"targetContract": p.address, "calldataPayload": p.bytes}, p.bytes), + simulateDelegatecallInternal: fun("0x43218e19", {"targetContract": p.address, "calldataPayload": p.bytes}, p.bytes), + swap: fun("0x845a101f", {"swaps": p.array(p.struct({"poolId": p.bytes32, "assetInIndex": p.uint256, "assetOutIndex": p.uint256, "amount": p.uint256, "userData": p.bytes})), "tokens": p.array(p.address), "trade": p.struct({"sellTokenIndex": p.uint256, "buyTokenIndex": p.uint256, "receiver": p.address, "sellAmount": p.uint256, "buyAmount": p.uint256, "validTo": p.uint32, "appData": p.bytes32, "feeAmount": p.uint256, "flags": p.uint256, "executedAmount": p.uint256, "signature": p.bytes})}, ), + vault: fun("0xfbfa77cf", {}, p.address), + vaultRelayer: fun("0x9b552cc2", {}, p.address), +} + +export class Contract extends ContractBase { + + authenticator() { + return this.eth_call(functions.authenticator, {}) + } + + domainSeparator() { + return this.eth_call(functions.domainSeparator, {}) + } + + filledAmount(_0: FilledAmountParams["_0"]) { + return this.eth_call(functions.filledAmount, {_0}) + } + + getStorageAt(offset: GetStorageAtParams["offset"], length: GetStorageAtParams["length"]) { + return this.eth_call(functions.getStorageAt, {offset, length}) + } + + preSignature(_0: PreSignatureParams["_0"]) { + return this.eth_call(functions.preSignature, {_0}) + } + + vault() { + return this.eth_call(functions.vault, {}) + } + + vaultRelayer() { + return this.eth_call(functions.vaultRelayer, {}) + } +} + +/// Event types +export type InteractionEventArgs = EParams +export type OrderInvalidatedEventArgs = EParams +export type PreSignatureEventArgs = EParams +export type SettlementEventArgs = EParams +export type TradeEventArgs = EParams + +/// Function types +export type AuthenticatorParams = FunctionArguments +export type AuthenticatorReturn = FunctionReturn + +export type DomainSeparatorParams = FunctionArguments +export type DomainSeparatorReturn = FunctionReturn + +export type FilledAmountParams = FunctionArguments +export type FilledAmountReturn = FunctionReturn + +export type FreeFilledAmountStorageParams = FunctionArguments +export type FreeFilledAmountStorageReturn = FunctionReturn + +export type FreePreSignatureStorageParams = FunctionArguments +export type FreePreSignatureStorageReturn = FunctionReturn + +export type GetStorageAtParams = FunctionArguments +export type GetStorageAtReturn = FunctionReturn + +export type InvalidateOrderParams = FunctionArguments +export type InvalidateOrderReturn = FunctionReturn + +export type PreSignatureParams = FunctionArguments +export type PreSignatureReturn = FunctionReturn + +export type SetPreSignatureParams = FunctionArguments +export type SetPreSignatureReturn = FunctionReturn + +export type SettleParams = FunctionArguments +export type SettleReturn = FunctionReturn + +export type SimulateDelegatecallParams = FunctionArguments +export type SimulateDelegatecallReturn = FunctionReturn + +export type SimulateDelegatecallInternalParams = FunctionArguments +export type SimulateDelegatecallInternalReturn = FunctionReturn + +export type SwapParams = FunctionArguments +export type SwapReturn = FunctionReturn + +export type VaultParams = FunctionArguments +export type VaultReturn = FunctionReturn + +export type VaultRelayerParams = FunctionArguments +export type VaultRelayerReturn = FunctionReturn + diff --git a/src/templates/otoken/activity-processor.ts b/src/templates/otoken/activity-processor.ts index f138ee48..42d0fabf 100644 --- a/src/templates/otoken/activity-processor.ts +++ b/src/templates/otoken/activity-processor.ts @@ -1,6 +1,7 @@ -import { compact, groupBy, uniq } from 'lodash' +import { add, compact, groupBy, uniq } from 'lodash' import * as balancerVaultAbi from '@abi/balancer-vault' +import * as cowswapSettlementAbi from '@abi/cow-swap-settlement' import * as curvePoolAbi from '@abi/curve-lp-token' import * as otokenAbi from '@abi/otoken' import * as otokenVaultAbi from '@abi/otoken-vault' @@ -17,7 +18,12 @@ import { UnwrapActivity, WrapActivity, } from '@templates/otoken/activity-types' -import { BALANCER_VAULT_ADDRESS, ETH_ADDRESS, ONEINCH_AGGREGATION_ROUTER_ADDRESS } from '@utils/addresses' +import { + BALANCER_VAULT_ADDRESS, + COWSWAP_SETTLEMENT_ADDRESS, + ETH_ADDRESS, + ONEINCH_AGGREGATION_ROUTER_ADDRESS, +} from '@utils/addresses' import { LogFilter, logFilter } from '@utils/logFilter' interface Input { @@ -45,6 +51,8 @@ export const createOTokenActivityProcessor = (params: Input) => { // Swaps balancerActivityProcessor({ pools: params.balancerPools }), + cowSwapActivityProcessor({ address: params.otokenAddress }), + params.wotokenAddress && cowSwapActivityProcessor({ address: params.wotokenAddress }), // Wraps & Unwraps params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), @@ -112,16 +120,24 @@ const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: .filter((l) => tokenExchangeFilter.matches(l)) .map((log) => { const tokenExchange = curvePoolAbi.events.TokenExchange.decode(log) - return createActivity(ctx, block, log, { - type: 'Swap', - account: tokenExchange.buyer, - exchange: 'Curve', - contract: log.address, - tokenIn: tokens[Number(tokenExchange.sold_id)], - tokenOut: tokens[Number(tokenExchange.bought_id)], - amountIn: tokenExchange.tokens_sold.toString(), - amountOut: tokenExchange.tokens_bought.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: tokenExchange.buyer, + exchange: 'Curve', + contract: log.address, + tokensIn: [ + { token: tokens[Number(tokenExchange.sold_id)], amount: tokenExchange.tokens_sold.toString() }, + ], + tokensOut: [ + { + token: tokens[Number(tokenExchange.bought_id)], + amount: tokenExchange.tokens_bought.toString(), + }, + ], + }, + ) }) }, } @@ -144,16 +160,52 @@ const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProc .filter((l) => swapFilter.matches(l)) .map((log) => { const swap = balancerVaultAbi.events.Swap.decode(log) - return createActivity(ctx, block, log, { - type: 'Swap', - account: log.transaction!.from, - exchange: 'Balancer', - contract: swap.poolId, - tokenIn: swap.tokenIn, - tokenOut: swap.tokenOut, - amountIn: swap.amountIn.toString(), - amountOut: swap.amountOut.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: log.transaction!.from, + exchange: 'Balancer', + contract: swap.poolId, + tokensIn: [{ token: swap.tokenIn, amount: swap.amountIn.toString() }], + tokensOut: [{ token: swap.tokenOut, amount: swap.amountOut.toString() }], + }, + ) + }) + }, + } +} + +const cowSwapActivityProcessor = ({ address }: { address: string }): ActivityProcessor => { + const tradeFilter = logFilter({ + address: [COWSWAP_SETTLEMENT_ADDRESS], + topic0: [cowswapSettlementAbi.events.Trade.topic], + transaction: true, + }) + return { + name: 'Cowswap Activity Processor', + filters: [tradeFilter], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const tradeLogs = logs + .filter((l) => tradeFilter.matches(l)) + .map((log) => ({ + log, + data: cowswapSettlementAbi.events.Trade.decode(log), + })) + return tradeLogs + .filter(({ data }) => data.buyToken.toLowerCase() === address || data.sellToken.toLowerCase() === address) + .map(({ log, data }) => { + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: log.transaction!.from, + exchange: 'Balancer', + contract: COWSWAP_SETTLEMENT_ADDRESS, + tokensIn: [{ token: data.sellToken, amount: data.sellAmount.toString() }], + tokensOut: [{ token: data.buyToken, amount: data.buyAmount.toString() }], + }, + ) }) }, } @@ -177,15 +229,18 @@ const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor { const data = wotokenAbi.events.Deposit.decode(log) const tokenIn = transferInLog?.address ?? 'unknown' - return createActivity(ctx, block, log, { - type: 'Wrap', - contract: wrappedAddress, - account: data.owner, - tokenIn, - tokenOut: wrappedAddress, - amountIn: data.assets.toString(), - amountOut: data.shares.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Wrap', + contract: wrappedAddress, + account: data.owner, + tokenIn, + tokenOut: wrappedAddress, + amountIn: data.assets.toString(), + amountOut: data.shares.toString(), + }, + ) }), ) } @@ -197,15 +252,18 @@ const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor { const data = wotokenAbi.events.Withdraw.decode(log) const tokenOut = transferOutLog?.address ?? 'unknown' - return createActivity(ctx, block, log, { - type: 'Unwrap', - contract: wrappedAddress, - account: data.owner, - tokenIn: wrappedAddress, - tokenOut, - amountIn: data.shares.toString(), - amountOut: data.assets.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Unwrap', + contract: wrappedAddress, + account: data.owner, + tokenIn: wrappedAddress, + tokenOut, + amountIn: data.shares.toString(), + amountOut: data.assets.toString(), + }, + ) }), ) } @@ -239,15 +297,18 @@ const vaultActivityProcessor = ({ result.push( ...mintLogs.map((log) => { const data = otokenVaultAbi.events.Mint.decode(log) - return createActivity(ctx, block, log, { - type: 'Mint', - contract: log.address, - account: data._addr, - tokenIn, - amountIn: amountIn.toString(), - tokenOut: otokenAddress, - amountOut: data._value.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Mint', + contract: log.address, + account: data._addr, + tokenIn, + amountIn: amountIn.toString(), + tokenOut: otokenAddress, + amountOut: data._value.toString(), + }, + ) }), ) } @@ -260,15 +321,18 @@ const vaultActivityProcessor = ({ result.push( ...redeemLogs.map((log) => { const data = otokenVaultAbi.events.Redeem.decode(log) - return createActivity(ctx, block, log, { - type: 'Redeem', - contract: log.address, - account: data._addr, - tokenIn: otokenAddress, - amountIn: data._value.toString(), - tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0] ?? ETH_ADDRESS, - amountOut: amountOut.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Redeem', + contract: log.address, + account: data._addr, + tokenIn: otokenAddress, + amountIn: data._value.toString(), + tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0] ?? ETH_ADDRESS, + amountOut: amountOut.toString(), + }, + ) }), ) } @@ -297,17 +361,20 @@ const transferActivityProcessor = ({ data: otokenAbi.events.Transfer.decode(log), })) - const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs, ONEINCH_AGGREGATION_ROUTER_ADDRESS) + const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs) if (swapActivity) return swapActivity return transferLogs.map(({ log, data }) => { - return createActivity(ctx, block, log, { - type: 'Transfer', - token: log.address, - from: data.from.toLowerCase(), - to: data.to.toLowerCase(), - amount: data.value.toString(), - }) + return createActivity( + { ctx, block, log }, + { + type: 'Transfer', + token: log.address, + from: data.from.toLowerCase(), + to: data.to.toLowerCase(), + amount: data.value.toString(), + }, + ) }) }, } @@ -320,70 +387,67 @@ const calculateTransferActivityAsSwap = ( log: Log data: ReturnType }[], - swapperAddress: string, ) => { - const swapperLogs = logs.filter( - ({ log, data }) => data.from.toLowerCase() === swapperAddress || data.to.toLowerCase() === swapperAddress, - ) - if (swapperLogs.length === 2) { - const changes = calculateBalanceChanges(swapperLogs) - let account: string = '' - let tokenIn: string = '' - let tokenOut: string = '' - let amountIn: bigint = 0n - let amountOut: bigint = 0n - for (const { token, from, to, value } of changes) { - if (from === swapperAddress) { - account = to - tokenOut = token - amountOut = value - } else if (to === swapperAddress) { - tokenIn = token - amountIn = value - } - } - return [ - createActivity(ctx, block, swapperLogs[0].log, { - type: 'Swap', - account, - exchange: 'Curve', - contract: swapperAddress, - tokenIn, - tokenOut, - amountIn: amountIn.toString(), - amountOut: amountOut.toString(), - }), - ] + if (logs.length === 1) return undefined + const resultMap: Record = {} + const tokens = new Set() + for (const { log, data } of logs) { + tokens.add(log.address) + // To + resultMap[data.to.toLowerCase()] = + resultMap[data.to.toLowerCase()] ?? + createActivity( + { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.to.toLowerCase()}` }, + { + type: 'Swap', + exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + contract: log.address, + account: data.to.toLowerCase(), + tokensOut: [], + tokensIn: [], + }, + ) + resultMap[data.to.toLowerCase()].tokensIn.push({ token: log.address, amount: data.value.toString() }) + + // From + resultMap[data.from.toLowerCase()] = + resultMap[data.from.toLowerCase()] ?? + createActivity( + { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.from.toLowerCase()}` }, + { + type: 'Swap', + exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + contract: log.address, + account: data.to.toLowerCase(), + tokensOut: [], + tokensIn: [], + }, + ) + resultMap[data.from.toLowerCase()].tokensOut.push({ token: log.address, amount: data.value.toString() }) } + if (tokens.size <= 1) return undefined + // We are a swap if we sent and received more than one token + const results = Object.values(resultMap).filter((r) => r.tokensIn.length > 0 && r.tokensOut.length > 0) + if (results.length > 0) return results return undefined } -const calculateBalanceChanges = ( - logs: { - log: Log - data: ReturnType - }[], -) => { - return Object.entries( - logs.reduce>((acc, { log, data }) => { - const key = `${log.address}:${data.from.toLowerCase()}:${data.to.toLowerCase()}` - acc[key] = (acc[key] || 0n) + data.value - return acc - }, {}), - ).map(([data, value]) => { - const [token, from, to] = data.split(':') - return { token, from, to, value } - }) -} - const createActivity = ( - ctx: Context, - block: Block, - log: Log, + { + ctx, + block, + log, + id, + }: { + ctx: Context + block: Block + log: Log + id?: string + }, partial: Omit, ) => ({ - id: `${ctx.chain.id}:${log.id}`, + id: id ?? `${ctx.chain.id}:${log.id}`, chainId: ctx.chain.id, blockNumber: block.header.height, timestamp: block.header.timestamp, diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 20ad08f1..0590e1ce 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -81,12 +81,10 @@ export interface UnstakeActivity extends ActivityBase { export interface SwapActivity extends ActivityBase { type: 'Swap' account: string - exchange: 'Curve' | 'Balancer' + exchange: 'Curve' | 'Balancer' | '1inch' | 'other' contract: string - tokenIn: string - tokenOut: string - amountIn: string - amountOut: string + tokensIn: { token: string; amount: string }[] + tokensOut: { token: string; amount: string }[] } export interface TransferActivity extends ActivityBase { From 7e4c2d022e3e7d6fbcfd1e0cee5e89402a56db02 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Wed, 17 Jul 2024 15:18:24 -0700 Subject: [PATCH 05/15] review & reorganize --- src/oeth/processors/oeth.ts | 2 +- src/templates/otoken/activity-processor.ts | 457 ------------------ .../activity-processor/activity-processor.ts | 89 ++++ .../otoken/activity-processor/index.ts | 8 + .../otoken/activity-processor/sub/balancer.ts | 40 ++ .../otoken/activity-processor/sub/cow-swap.ts | 42 ++ .../otoken/activity-processor/sub/curve.ts | 45 ++ .../otoken/activity-processor/sub/transfer.ts | 99 ++++ .../otoken/activity-processor/sub/vault.ts | 78 +++ .../otoken/activity-processor/sub/wrapped.ts | 67 +++ .../otoken/activity-processor/types.ts | 9 + .../otoken/activity-processor/utils.ts | 26 + 12 files changed, 504 insertions(+), 458 deletions(-) delete mode 100644 src/templates/otoken/activity-processor.ts create mode 100644 src/templates/otoken/activity-processor/activity-processor.ts create mode 100644 src/templates/otoken/activity-processor/index.ts create mode 100644 src/templates/otoken/activity-processor/sub/balancer.ts create mode 100644 src/templates/otoken/activity-processor/sub/cow-swap.ts create mode 100644 src/templates/otoken/activity-processor/sub/curve.ts create mode 100644 src/templates/otoken/activity-processor/sub/transfer.ts create mode 100644 src/templates/otoken/activity-processor/sub/vault.ts create mode 100644 src/templates/otoken/activity-processor/sub/wrapped.ts create mode 100644 src/templates/otoken/activity-processor/types.ts create mode 100644 src/templates/otoken/activity-processor/utils.ts diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 71004013..b558cc2b 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -1,7 +1,7 @@ import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' import { createOTokenProcessor } from '@templates/otoken' -import { createOTokenActivityProcessor } from '@templates/otoken/activity-processor' +import { createOTokenActivityProcessor } from '@templates/otoken/activity-processor/activity-processor' import { CURVE_ETH_OETH_POOL_ADDRESS, CURVE_FRXETH_OETH_POOL_ADDRESS, diff --git a/src/templates/otoken/activity-processor.ts b/src/templates/otoken/activity-processor.ts deleted file mode 100644 index 42d0fabf..00000000 --- a/src/templates/otoken/activity-processor.ts +++ /dev/null @@ -1,457 +0,0 @@ -import { add, compact, groupBy, uniq } from 'lodash' - -import * as balancerVaultAbi from '@abi/balancer-vault' -import * as cowswapSettlementAbi from '@abi/cow-swap-settlement' -import * as curvePoolAbi from '@abi/curve-lp-token' -import * as otokenAbi from '@abi/otoken' -import * as otokenVaultAbi from '@abi/otoken-vault' -import * as wotokenAbi from '@abi/woeth' -import { OTokenActivity, OTokenActivityType } from '@model' -import { Block, Context, Log } from '@processor' -import { EvmBatchProcessor } from '@subsquid/evm-processor' -import { - Activity, - MintActivity, - RedeemActivity, - SwapActivity, - TransferActivity, - UnwrapActivity, - WrapActivity, -} from '@templates/otoken/activity-types' -import { - BALANCER_VAULT_ADDRESS, - COWSWAP_SETTLEMENT_ADDRESS, - ETH_ADDRESS, - ONEINCH_AGGREGATION_ROUTER_ADDRESS, -} from '@utils/addresses' -import { LogFilter, logFilter } from '@utils/logFilter' - -interface Input { - from: number - otokenAddress: string - vaultAddress: string - wotokenAddress?: string - curvePools: { - address: string - tokens: string[] - }[] - balancerPools: string[] -} - -export interface ActivityProcessor { - name: string - filters: LogFilter[] - process: (ctx: Context, block: Block, logs: Log[]) => Promise -} - -export const createOTokenActivityProcessor = (params: Input) => { - const processors: ActivityProcessor[] = compact([ - // Swaps - ...params.curvePools.map((pool) => curveActivityProcessor(pool)), - - // Swaps - balancerActivityProcessor({ pools: params.balancerPools }), - cowSwapActivityProcessor({ address: params.otokenAddress }), - params.wotokenAddress && cowSwapActivityProcessor({ address: params.wotokenAddress }), - - // Wraps & Unwraps - params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), - - // Mints & Redeems - vaultActivityProcessor({ otokenAddress: params.otokenAddress, vaultAddress: params.vaultAddress }), - - // Transfers & Swaps - transferActivityProcessor({ otokenAddress: params.otokenAddress }), - ]) - - const from = params.from - const setup = (processor: EvmBatchProcessor) => { - for (const p of processors) { - for (const filter of p.filters) { - processor.addLog(filter.value) - } - } - } - const process = async (ctx: Context) => { - const activities: Activity[] = [] - for (const block of ctx.blocks) { - const transactions = groupBy(block.logs, (l) => l.transactionHash) - for (const logs of Object.values(transactions)) { - for (const p of processors) { - let hit = false - for (const filter of p.filters) { - if (logs.find((log) => filter.matches(log))) { - const results = await p.process(ctx, block, logs) - activities.push(...results) - hit = true - break - } - } - if (hit) break - } - } - } - await ctx.store.insert( - activities.map( - (activity) => - new OTokenActivity({ - id: activity.id, - chainId: activity.chainId, - type: OTokenActivityType[activity.type], - txHash: activity.txHash, - blockNumber: activity.blockNumber, - timestamp: new Date(activity.timestamp), - otoken: params.otokenAddress, - data: activity, - }), - ), - ) - } - return { from, setup, process } -} - -const curveActivityProcessor = ({ address, tokens }: { address: string; tokens: string[] }): ActivityProcessor => { - return { - name: 'Curve Pool Processor', - filters: [logFilter({ address: [address], topic0: [curvePoolAbi.events.TokenExchange.topic] })], - async process(ctx: Context, block: Block, logs: Log[]): Promise { - const [tokenExchangeFilter] = this.filters - return logs - .filter((l) => tokenExchangeFilter.matches(l)) - .map((log) => { - const tokenExchange = curvePoolAbi.events.TokenExchange.decode(log) - return createActivity( - { ctx, block, log }, - { - type: 'Swap', - account: tokenExchange.buyer, - exchange: 'Curve', - contract: log.address, - tokensIn: [ - { token: tokens[Number(tokenExchange.sold_id)], amount: tokenExchange.tokens_sold.toString() }, - ], - tokensOut: [ - { - token: tokens[Number(tokenExchange.bought_id)], - amount: tokenExchange.tokens_bought.toString(), - }, - ], - }, - ) - }) - }, - } -} - -const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProcessor => { - return { - name: 'Balancer Pool Processor', - filters: [ - logFilter({ - address: [BALANCER_VAULT_ADDRESS], - topic0: [balancerVaultAbi.events.Swap.topic], - topic1: pools, - transaction: true, - }), - ], - async process(ctx: Context, block: Block, logs: Log[]): Promise { - const [swapFilter] = this.filters - return logs - .filter((l) => swapFilter.matches(l)) - .map((log) => { - const swap = balancerVaultAbi.events.Swap.decode(log) - return createActivity( - { ctx, block, log }, - { - type: 'Swap', - account: log.transaction!.from, - exchange: 'Balancer', - contract: swap.poolId, - tokensIn: [{ token: swap.tokenIn, amount: swap.amountIn.toString() }], - tokensOut: [{ token: swap.tokenOut, amount: swap.amountOut.toString() }], - }, - ) - }) - }, - } -} - -const cowSwapActivityProcessor = ({ address }: { address: string }): ActivityProcessor => { - const tradeFilter = logFilter({ - address: [COWSWAP_SETTLEMENT_ADDRESS], - topic0: [cowswapSettlementAbi.events.Trade.topic], - transaction: true, - }) - return { - name: 'Cowswap Activity Processor', - filters: [tradeFilter], - async process(ctx: Context, block: Block, logs: Log[]): Promise { - const tradeLogs = logs - .filter((l) => tradeFilter.matches(l)) - .map((log) => ({ - log, - data: cowswapSettlementAbi.events.Trade.decode(log), - })) - return tradeLogs - .filter(({ data }) => data.buyToken.toLowerCase() === address || data.sellToken.toLowerCase() === address) - .map(({ log, data }) => { - return createActivity( - { ctx, block, log }, - { - type: 'Swap', - account: log.transaction!.from, - exchange: 'Balancer', - contract: COWSWAP_SETTLEMENT_ADDRESS, - tokensIn: [{ token: data.sellToken, amount: data.sellAmount.toString() }], - tokensOut: [{ token: data.buyToken, amount: data.buyAmount.toString() }], - }, - ) - }) - }, - } -} - -const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor => { - const depositFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Deposit.topic] }) - const withdrawFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Withdraw.topic] }) - const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [wrappedAddress] }) - const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [wrappedAddress] }) - return { - name: 'Wrapped Processor', - filters: [depositFilter, withdrawFilter, transferInFilter, transferOutFilter], - async process(ctx: Context, block: Block, logs: Log[]) { - const result: (WrapActivity | UnwrapActivity)[] = [] - // Wrap - const depositLogs = logs.filter((l) => depositFilter.matches(l)) - if (depositLogs.length) { - const transferInLog = logs.find((l) => transferInFilter.matches(l)) - result.push( - ...depositLogs.map((log) => { - const data = wotokenAbi.events.Deposit.decode(log) - const tokenIn = transferInLog?.address ?? 'unknown' - return createActivity( - { ctx, block, log }, - { - type: 'Wrap', - contract: wrappedAddress, - account: data.owner, - tokenIn, - tokenOut: wrappedAddress, - amountIn: data.assets.toString(), - amountOut: data.shares.toString(), - }, - ) - }), - ) - } - // Unwrap - const withdrawLogs = logs.filter((l) => withdrawFilter.matches(l)) - if (withdrawLogs.length) { - const transferOutLog = logs.find((l) => transferOutFilter.matches(l)) - result.push( - ...withdrawLogs.map((log) => { - const data = wotokenAbi.events.Withdraw.decode(log) - const tokenOut = transferOutLog?.address ?? 'unknown' - return createActivity( - { ctx, block, log }, - { - type: 'Unwrap', - contract: wrappedAddress, - account: data.owner, - tokenIn: wrappedAddress, - tokenOut, - amountIn: data.shares.toString(), - amountOut: data.assets.toString(), - }, - ) - }), - ) - } - return result - }, - } -} - -const vaultActivityProcessor = ({ - otokenAddress, - vaultAddress, -}: { - otokenAddress: string - vaultAddress: string -}): ActivityProcessor => { - const mintFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Mint.topic] }) - const redeemFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Redeem.topic] }) - const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [vaultAddress] }) - const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [vaultAddress] }) - return { - name: 'Vault Processor', - filters: [mintFilter, redeemFilter, transferInFilter, transferOutFilter], - process: async (ctx, block, logs) => { - const result: (MintActivity | RedeemActivity)[] = [] - // Mint - const mintLogs = logs.filter((l) => mintFilter.matches(l)) - if (mintLogs.length) { - const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) - const tokenIn = transferInLogs[0]?.address ?? ETH_ADDRESS - const amountIn = mintLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Mint.decode(l)._value, 0n) - result.push( - ...mintLogs.map((log) => { - const data = otokenVaultAbi.events.Mint.decode(log) - return createActivity( - { ctx, block, log }, - { - type: 'Mint', - contract: log.address, - account: data._addr, - tokenIn, - amountIn: amountIn.toString(), - tokenOut: otokenAddress, - amountOut: data._value.toString(), - }, - ) - }), - ) - } - // Redeem - const redeemLogs = logs.filter((l) => redeemFilter.matches(l)) - if (redeemLogs.length) { - const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) - const tokensOut = uniq(transferOutLogs.map((l) => l.address)) - const amountOut = redeemLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Redeem.decode(l)._value, 0n) - result.push( - ...redeemLogs.map((log) => { - const data = otokenVaultAbi.events.Redeem.decode(log) - return createActivity( - { ctx, block, log }, - { - type: 'Redeem', - contract: log.address, - account: data._addr, - tokenIn: otokenAddress, - amountIn: data._value.toString(), - tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0] ?? ETH_ADDRESS, - amountOut: amountOut.toString(), - }, - ) - }), - ) - } - return result - }, - } -} - -const transferActivityProcessor = ({ - otokenAddress, -}: { - otokenAddress: string -}): ActivityProcessor => { - const transferFilter = logFilter({ - address: [otokenAddress], - topic0: [otokenAbi.events.Transfer.topic], - }) - return { - name: 'Transfer Activity', - filters: [transferFilter], - process: async (ctx, block, logs) => { - const transferLogs = logs - .filter((l) => transferFilter.matches(l)) - .map((log) => ({ - log, - data: otokenAbi.events.Transfer.decode(log), - })) - - const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs) - if (swapActivity) return swapActivity - - return transferLogs.map(({ log, data }) => { - return createActivity( - { ctx, block, log }, - { - type: 'Transfer', - token: log.address, - from: data.from.toLowerCase(), - to: data.to.toLowerCase(), - amount: data.value.toString(), - }, - ) - }) - }, - } -} - -const calculateTransferActivityAsSwap = ( - ctx: Context, - block: Block, - logs: { - log: Log - data: ReturnType - }[], -) => { - if (logs.length === 1) return undefined - const resultMap: Record = {} - const tokens = new Set() - for (const { log, data } of logs) { - tokens.add(log.address) - // To - resultMap[data.to.toLowerCase()] = - resultMap[data.to.toLowerCase()] ?? - createActivity( - { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.to.toLowerCase()}` }, - { - type: 'Swap', - exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', - contract: log.address, - account: data.to.toLowerCase(), - tokensOut: [], - tokensIn: [], - }, - ) - resultMap[data.to.toLowerCase()].tokensIn.push({ token: log.address, amount: data.value.toString() }) - - // From - resultMap[data.from.toLowerCase()] = - resultMap[data.from.toLowerCase()] ?? - createActivity( - { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.from.toLowerCase()}` }, - { - type: 'Swap', - exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', - contract: log.address, - account: data.to.toLowerCase(), - tokensOut: [], - tokensIn: [], - }, - ) - resultMap[data.from.toLowerCase()].tokensOut.push({ token: log.address, amount: data.value.toString() }) - } - if (tokens.size <= 1) return undefined - // We are a swap if we sent and received more than one token - const results = Object.values(resultMap).filter((r) => r.tokensIn.length > 0 && r.tokensOut.length > 0) - if (results.length > 0) return results - return undefined -} - -const createActivity = ( - { - ctx, - block, - log, - id, - }: { - ctx: Context - block: Block - log: Log - id?: string - }, - partial: Omit, -) => - ({ - id: id ?? `${ctx.chain.id}:${log.id}`, - chainId: ctx.chain.id, - blockNumber: block.header.height, - timestamp: block.header.timestamp, - status: 'success', - txHash: log.transactionHash, - ...partial, - }) as T diff --git a/src/templates/otoken/activity-processor/activity-processor.ts b/src/templates/otoken/activity-processor/activity-processor.ts new file mode 100644 index 00000000..4b4ffd41 --- /dev/null +++ b/src/templates/otoken/activity-processor/activity-processor.ts @@ -0,0 +1,89 @@ +import { compact, groupBy } from 'lodash' + +import { OTokenActivity, OTokenActivityType } from '@model' +import { Context } from '@processor' +import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { balancerActivityProcessor } from '@templates/otoken/activity-processor/sub/balancer' +import { cowSwapActivityProcessor } from '@templates/otoken/activity-processor/sub/cow-swap' +import { curveActivityProcessor } from '@templates/otoken/activity-processor/sub/curve' +import { transferActivityProcessor } from '@templates/otoken/activity-processor/sub/transfer' +import { vaultActivityProcessor } from '@templates/otoken/activity-processor/sub/vault' +import { wrappedActivityProcessor } from '@templates/otoken/activity-processor/sub/wrapped' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { Activity } from '@templates/otoken/activity-types' + +export const createOTokenActivityProcessor = (params: { + from: number + otokenAddress: string + vaultAddress: string + wotokenAddress?: string + curvePools: { + address: string + tokens: string[] + }[] + balancerPools: string[] +}) => { + const processors: ActivityProcessor[] = compact([ + // Swaps + ...params.curvePools.map((pool) => curveActivityProcessor(pool)), + + // Swaps + balancerActivityProcessor({ pools: params.balancerPools }), + cowSwapActivityProcessor({ address: params.otokenAddress }), + params.wotokenAddress && cowSwapActivityProcessor({ address: params.wotokenAddress }), + + // Wraps & Unwraps + params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), + + // Mints & Redeems + vaultActivityProcessor({ otokenAddress: params.otokenAddress, vaultAddress: params.vaultAddress }), + + // Transfers & Swaps + transferActivityProcessor({ otokenAddress: params.otokenAddress }), + ]) + + const from = params.from + const setup = (processor: EvmBatchProcessor) => { + for (const p of processors) { + for (const filter of p.filters) { + processor.addLog(filter.value) + } + } + } + const process = async (ctx: Context) => { + const activities: Activity[] = [] + for (const block of ctx.blocks) { + const transactions = groupBy(block.logs, (l) => l.transactionHash) + for (const logs of Object.values(transactions)) { + for (const p of processors) { + let hit = false + for (const filter of p.filters) { + if (logs.find((log) => filter.matches(log))) { + const results = await p.process(ctx, block, logs) + activities.push(...results) + hit = true + break + } + } + if (hit) break + } + } + } + await ctx.store.insert( + activities.map( + (activity) => + new OTokenActivity({ + id: activity.id, + chainId: activity.chainId, + type: OTokenActivityType[activity.type], + txHash: activity.txHash, + blockNumber: activity.blockNumber, + timestamp: new Date(activity.timestamp), + otoken: params.otokenAddress, + data: activity, + }), + ), + ) + } + return { from, setup, process } +} diff --git a/src/templates/otoken/activity-processor/index.ts b/src/templates/otoken/activity-processor/index.ts new file mode 100644 index 00000000..f3809c4c --- /dev/null +++ b/src/templates/otoken/activity-processor/index.ts @@ -0,0 +1,8 @@ +export * from './activity-processor' +export { createActivity } from '@templates/otoken/activity-processor/utils' +export { transferActivityProcessor } from '@templates/otoken/activity-processor/sub/transfer' +export { vaultActivityProcessor } from '@templates/otoken/activity-processor/sub/vault' +export { wrappedActivityProcessor } from '@templates/otoken/activity-processor/sub/wrapped' +export { cowSwapActivityProcessor } from '@templates/otoken/activity-processor/sub/cow-swap' +export { balancerActivityProcessor } from '@templates/otoken/activity-processor/sub/balancer' +export { curveActivityProcessor } from '@templates/otoken/activity-processor/sub/curve' diff --git a/src/templates/otoken/activity-processor/sub/balancer.ts b/src/templates/otoken/activity-processor/sub/balancer.ts new file mode 100644 index 00000000..1e0253e1 --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/balancer.ts @@ -0,0 +1,40 @@ +import * as balancerVaultAbi from '@abi/balancer-vault' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { SwapActivity } from '@templates/otoken/activity-types' +import { BALANCER_VAULT_ADDRESS } from '@utils/addresses' +import { logFilter } from '@utils/logFilter' + +export const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProcessor => { + return { + name: 'Balancer Pool Processor', + filters: [ + logFilter({ + address: [BALANCER_VAULT_ADDRESS], + topic0: [balancerVaultAbi.events.Swap.topic], + topic1: pools, + transaction: true, + }), + ], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [swapFilter] = this.filters + return logs + .filter((l) => swapFilter.matches(l)) + .map((log) => { + const swap = balancerVaultAbi.events.Swap.decode(log) + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: log.transaction!.from, + exchange: 'Balancer', + contract: swap.poolId, + tokensIn: [{ token: swap.tokenIn, amount: swap.amountIn.toString() }], + tokensOut: [{ token: swap.tokenOut, amount: swap.amountOut.toString() }], + }, + ) + }) + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/cow-swap.ts b/src/templates/otoken/activity-processor/sub/cow-swap.ts new file mode 100644 index 00000000..8caa9ddc --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/cow-swap.ts @@ -0,0 +1,42 @@ +import * as cowswapSettlementAbi from '@abi/cow-swap-settlement' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { SwapActivity } from '@templates/otoken/activity-types' +import { COWSWAP_SETTLEMENT_ADDRESS } from '@utils/addresses' +import { logFilter } from '@utils/logFilter' + +export const cowSwapActivityProcessor = ({ address }: { address: string }): ActivityProcessor => { + const tradeFilter = logFilter({ + address: [COWSWAP_SETTLEMENT_ADDRESS], + topic0: [cowswapSettlementAbi.events.Trade.topic], + transaction: true, + }) + return { + name: 'Cowswap Activity Processor', + filters: [tradeFilter], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const tradeLogs = logs + .filter((l) => tradeFilter.matches(l)) + .map((log) => ({ + log, + data: cowswapSettlementAbi.events.Trade.decode(log), + })) + return tradeLogs + .filter(({ data }) => data.buyToken.toLowerCase() === address || data.sellToken.toLowerCase() === address) + .map(({ log, data }) => { + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: log.transaction!.from, + exchange: 'Balancer', + contract: COWSWAP_SETTLEMENT_ADDRESS, + tokensIn: [{ token: data.sellToken, amount: data.sellAmount.toString() }], + tokensOut: [{ token: data.buyToken, amount: data.buyAmount.toString() }], + }, + ) + }) + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/curve.ts b/src/templates/otoken/activity-processor/sub/curve.ts new file mode 100644 index 00000000..33f716cb --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/curve.ts @@ -0,0 +1,45 @@ +import * as curvePoolAbi from '@abi/curve-lp-token' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { SwapActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' + +export const curveActivityProcessor = ({ + address, + tokens, +}: { + address: string + tokens: string[] +}): ActivityProcessor => { + return { + name: 'Curve Pool Processor', + filters: [logFilter({ address: [address], topic0: [curvePoolAbi.events.TokenExchange.topic] })], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [tokenExchangeFilter] = this.filters + return logs + .filter((l) => tokenExchangeFilter.matches(l)) + .map((log) => { + const tokenExchange = curvePoolAbi.events.TokenExchange.decode(log) + return createActivity( + { ctx, block, log }, + { + type: 'Swap', + account: tokenExchange.buyer, + exchange: 'Curve', + contract: log.address, + tokensIn: [ + { token: tokens[Number(tokenExchange.sold_id)], amount: tokenExchange.tokens_sold.toString() }, + ], + tokensOut: [ + { + token: tokens[Number(tokenExchange.bought_id)], + amount: tokenExchange.tokens_bought.toString(), + }, + ], + }, + ) + }) + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/transfer.ts b/src/templates/otoken/activity-processor/sub/transfer.ts new file mode 100644 index 00000000..e06f523c --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/transfer.ts @@ -0,0 +1,99 @@ +import * as otokenAbi from '@abi/otoken' +import * as wotokenAbi from '@abi/woeth' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { SwapActivity, TransferActivity } from '@templates/otoken/activity-types' +import { ONEINCH_AGGREGATION_ROUTER_ADDRESS } from '@utils/addresses' +import { logFilter } from '@utils/logFilter' + +export const transferActivityProcessor = ({ + otokenAddress, +}: { + otokenAddress: string +}): ActivityProcessor => { + const transferFilter = logFilter({ + address: [otokenAddress], + topic0: [otokenAbi.events.Transfer.topic], + }) + return { + name: 'Transfer Activity', + filters: [transferFilter], + process: async (ctx, block, logs) => { + const transferLogs = logs + .filter((l) => transferFilter.matches(l)) + .map((log) => ({ + log, + data: otokenAbi.events.Transfer.decode(log), + })) + + const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs) + if (swapActivity) return swapActivity + + return transferLogs.map(({ log, data }) => { + return createActivity( + { ctx, block, log }, + { + type: 'Transfer', + token: log.address, + from: data.from.toLowerCase(), + to: data.to.toLowerCase(), + amount: data.value.toString(), + }, + ) + }) + }, + } +} + +const calculateTransferActivityAsSwap = ( + ctx: Context, + block: Block, + logs: { + log: Log + data: ReturnType + }[], +) => { + if (logs.length === 1) return undefined + const resultMap: Record = {} + const tokens = new Set() + for (const { log, data } of logs) { + tokens.add(log.address) + // To + resultMap[data.to.toLowerCase()] = + resultMap[data.to.toLowerCase()] ?? + createActivity( + { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.to.toLowerCase()}` }, + { + type: 'Swap', + exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + contract: log.address, + account: data.to.toLowerCase(), + tokensOut: [], + tokensIn: [], + }, + ) + resultMap[data.to.toLowerCase()].tokensIn.push({ token: log.address, amount: data.value.toString() }) + + // From + resultMap[data.from.toLowerCase()] = + resultMap[data.from.toLowerCase()] ?? + createActivity( + { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.from.toLowerCase()}` }, + { + type: 'Swap', + exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + contract: log.address, + account: data.to.toLowerCase(), + tokensOut: [], + tokensIn: [], + }, + ) + resultMap[data.from.toLowerCase()].tokensOut.push({ token: log.address, amount: data.value.toString() }) + } + if (tokens.size <= 1) return undefined + // We are a swap if we sent and received more than one token + const results = Object.values(resultMap).filter((r) => r.tokensIn.length > 0 && r.tokensOut.length > 0) + if (results.length > 0) return results + return undefined +} diff --git a/src/templates/otoken/activity-processor/sub/vault.ts b/src/templates/otoken/activity-processor/sub/vault.ts new file mode 100644 index 00000000..89cc3b29 --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/vault.ts @@ -0,0 +1,78 @@ +import { uniq } from 'lodash' + +import * as otokenVaultAbi from '@abi/otoken-vault' +import * as wotokenAbi from '@abi/woeth' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { MintActivity, RedeemActivity } from '@templates/otoken/activity-types' +import { ETH_ADDRESS } from '@utils/addresses' +import { logFilter } from '@utils/logFilter' + +export const vaultActivityProcessor = ({ + otokenAddress, + vaultAddress, +}: { + otokenAddress: string + vaultAddress: string +}): ActivityProcessor => { + const mintFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Mint.topic] }) + const redeemFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Redeem.topic] }) + const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [vaultAddress] }) + const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [vaultAddress] }) + return { + name: 'Vault Processor', + filters: [mintFilter, redeemFilter, transferInFilter, transferOutFilter], + process: async (ctx, block, logs) => { + const result: (MintActivity | RedeemActivity)[] = [] + // Mint + const mintLogs = logs.filter((l) => mintFilter.matches(l)) + if (mintLogs.length) { + const transferInLogs = logs.filter((l) => transferInFilter.matches(l)) + const tokenIn = transferInLogs[0]?.address ?? ETH_ADDRESS + const amountIn = mintLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Mint.decode(l)._value, 0n) + result.push( + ...mintLogs.map((log) => { + const data = otokenVaultAbi.events.Mint.decode(log) + return createActivity( + { ctx, block, log }, + { + type: 'Mint', + contract: log.address, + account: data._addr, + tokenIn, + amountIn: amountIn.toString(), + tokenOut: otokenAddress, + amountOut: data._value.toString(), + }, + ) + }), + ) + } + // Redeem + const redeemLogs = logs.filter((l) => redeemFilter.matches(l)) + if (redeemLogs.length) { + const transferOutLogs = logs.filter((l) => transferOutFilter.matches(l)) + const tokensOut = uniq(transferOutLogs.map((l) => l.address)) + const amountOut = redeemLogs.reduce((sum, l) => sum + otokenVaultAbi.events.Redeem.decode(l)._value, 0n) + result.push( + ...redeemLogs.map((log) => { + const data = otokenVaultAbi.events.Redeem.decode(log) + return createActivity( + { ctx, block, log }, + { + type: 'Redeem', + contract: log.address, + account: data._addr, + tokenIn: otokenAddress, + amountIn: data._value.toString(), + tokenOut: tokensOut.length > 1 ? 'MIX' : tokensOut[0] ?? ETH_ADDRESS, + amountOut: amountOut.toString(), + }, + ) + }), + ) + } + return result + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/wrapped.ts b/src/templates/otoken/activity-processor/sub/wrapped.ts new file mode 100644 index 00000000..79d3a4b5 --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/wrapped.ts @@ -0,0 +1,67 @@ +import * as wotokenAbi from '@abi/woeth' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { UnwrapActivity, WrapActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' + +export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor => { + const depositFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Deposit.topic] }) + const withdrawFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Withdraw.topic] }) + const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [wrappedAddress] }) + const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [wrappedAddress] }) + return { + name: 'Wrapped Processor', + filters: [depositFilter, withdrawFilter, transferInFilter, transferOutFilter], + async process(ctx: Context, block: Block, logs: Log[]) { + const result: (WrapActivity | UnwrapActivity)[] = [] + // Wrap + const depositLogs = logs.filter((l) => depositFilter.matches(l)) + if (depositLogs.length) { + const transferInLog = logs.find((l) => transferInFilter.matches(l)) + result.push( + ...depositLogs.map((log) => { + const data = wotokenAbi.events.Deposit.decode(log) + const tokenIn = transferInLog?.address ?? 'unknown' + return createActivity( + { ctx, block, log }, + { + type: 'Wrap', + contract: wrappedAddress, + account: data.owner, + tokenIn, + tokenOut: wrappedAddress, + amountIn: data.assets.toString(), + amountOut: data.shares.toString(), + }, + ) + }), + ) + } + // Unwrap + const withdrawLogs = logs.filter((l) => withdrawFilter.matches(l)) + if (withdrawLogs.length) { + const transferOutLog = logs.find((l) => transferOutFilter.matches(l)) + result.push( + ...withdrawLogs.map((log) => { + const data = wotokenAbi.events.Withdraw.decode(log) + const tokenOut = transferOutLog?.address ?? 'unknown' + return createActivity( + { ctx, block, log }, + { + type: 'Unwrap', + contract: wrappedAddress, + account: data.owner, + tokenIn: wrappedAddress, + tokenOut, + amountIn: data.shares.toString(), + amountOut: data.assets.toString(), + }, + ) + }), + ) + } + return result + }, + } +} diff --git a/src/templates/otoken/activity-processor/types.ts b/src/templates/otoken/activity-processor/types.ts new file mode 100644 index 00000000..dfd8917f --- /dev/null +++ b/src/templates/otoken/activity-processor/types.ts @@ -0,0 +1,9 @@ +import { Block, Context, Log } from '@processor' +import { Activity } from '@templates/otoken/activity-types' +import { LogFilter } from '@utils/logFilter' + +export interface ActivityProcessor { + name: string + filters: LogFilter[] + process: (ctx: Context, block: Block, logs: Log[]) => Promise +} diff --git a/src/templates/otoken/activity-processor/utils.ts b/src/templates/otoken/activity-processor/utils.ts new file mode 100644 index 00000000..e4196f7a --- /dev/null +++ b/src/templates/otoken/activity-processor/utils.ts @@ -0,0 +1,26 @@ +import { Block, Context, Log } from '@processor' +import { Activity } from '@templates/otoken/activity-types' + +export const createActivity = ( + { + ctx, + block, + log, + id, + }: { + ctx: Context + block: Block + log: Log + id?: string + }, + partial: Omit, +) => + ({ + id: id ?? `${ctx.chain.id}:${log.id}`, + chainId: ctx.chain.id, + blockNumber: block.header.height, + timestamp: block.header.timestamp, + status: 'success', + txHash: log.transactionHash, + ...partial, + }) as T From 715d6132718fa6e7f19f42ff05ea4c4fa406d39e Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 18 Jul 2024 17:52:10 -0700 Subject: [PATCH 06/15] zapper, uniswap --- abi/uniswap-v3.json | 988 ++++++++++++++++++ ...52086993-Data.js => 1721327933049-Data.js} | 4 +- schema.graphql | 1 + schema/otoken.graphql | 1 + src/abi/uniswap-v3.ts | 206 ++++ src/mainnet/processors/uniswap.ts | 4 +- src/model/generated/_oTokenActivityType.ts | 1 + src/oeth/processors/oeth.ts | 7 + .../activity-processor/activity-processor.ts | 29 +- .../otoken/activity-processor/sub/balancer.ts | 1 + .../otoken/activity-processor/sub/cow-swap.ts | 1 + .../otoken/activity-processor/sub/curve.ts | 1 + .../otoken/activity-processor/sub/transfer.ts | 18 +- .../activity-processor/sub/uniswap-v3.ts | 62 ++ .../otoken/activity-processor/sub/vault.ts | 2 + .../otoken/activity-processor/sub/wrapped.ts | 4 +- .../otoken/activity-processor/sub/zapper.ts | 46 + .../otoken/activity-processor/utils.ts | 6 + src/templates/otoken/activity-types.ts | 15 +- src/utils/activityFromTx.ts | 147 --- src/utils/addresses.ts | 2 +- 21 files changed, 1379 insertions(+), 167 deletions(-) create mode 100644 abi/uniswap-v3.json rename db/migrations/{1721252086993-Data.js => 1721327933049-Data.js} (99%) create mode 100644 src/abi/uniswap-v3.ts create mode 100644 src/templates/otoken/activity-processor/sub/uniswap-v3.ts create mode 100644 src/templates/otoken/activity-processor/sub/zapper.ts diff --git a/abi/uniswap-v3.json b/abi/uniswap-v3.json new file mode 100644 index 00000000..49cc338e --- /dev/null +++ b/abi/uniswap-v3.json @@ -0,0 +1,988 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "name": "Collect", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "name": "CollectProtocol", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "paid1", + "type": "uint256" + } + ], + "name": "Flash", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "observationCardinalityNextOld", + "type": "uint16" + }, + { + "indexed": false, + "internalType": "uint16", + "name": "observationCardinalityNextNew", + "type": "uint16" + } + ], + "name": "IncreaseObservationCardinalityNext", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Initialize", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "indexed": true, + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol0Old", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol1Old", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol0New", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "feeProtocol1New", + "type": "uint8" + } + ], + "name": "SetFeeProtocol", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "amount0", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "amount1", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "indexed": false, + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "indexed": false, + "internalType": "int24", + "name": "tick", + "type": "int24" + } + ], + "name": "Swap", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + } + ], + "name": "burn", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount0Requested", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Requested", + "type": "uint128" + } + ], + "name": "collect", + "outputs": [ + { + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint128", + "name": "amount0Requested", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1Requested", + "type": "uint128" + } + ], + "name": "collectProtocol", + "outputs": [ + { + "internalType": "uint128", + "name": "amount0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "amount1", + "type": "uint128" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "factory", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "fee", + "outputs": [ + { + "internalType": "uint24", + "name": "", + "type": "uint24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeGrowthGlobal0X128", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeGrowthGlobal1X128", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "flash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "observationCardinalityNext", + "type": "uint16" + } + ], + "name": "increaseObservationCardinalityNext", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidity", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxLiquidityPerTick", + "outputs": [ + { + "internalType": "uint128", + "name": "", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + }, + { + "internalType": "uint128", + "name": "amount", + "type": "uint128" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "mint", + "outputs": [ + { + "internalType": "uint256", + "name": "amount0", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount1", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "observations", + "outputs": [ + { + "internalType": "uint32", + "name": "blockTimestamp", + "type": "uint32" + }, + { + "internalType": "int56", + "name": "tickCumulative", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityCumulativeX128", + "type": "uint160" + }, + { + "internalType": "bool", + "name": "initialized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint32[]", + "name": "secondsAgos", + "type": "uint32[]" + } + ], + "name": "observe", + "outputs": [ + { + "internalType": "int56[]", + "name": "tickCumulatives", + "type": "int56[]" + }, + { + "internalType": "uint160[]", + "name": "secondsPerLiquidityCumulativeX128s", + "type": "uint160[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "positions", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidity", + "type": "uint128" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside0LastX128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthInside1LastX128", + "type": "uint256" + }, + { + "internalType": "uint128", + "name": "tokensOwed0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "tokensOwed1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "protocolFees", + "outputs": [ + { + "internalType": "uint128", + "name": "token0", + "type": "uint128" + }, + { + "internalType": "uint128", + "name": "token1", + "type": "uint128" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "feeProtocol0", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "feeProtocol1", + "type": "uint8" + } + ], + "name": "setFeeProtocol", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "slot0", + "outputs": [ + { + "internalType": "uint160", + "name": "sqrtPriceX96", + "type": "uint160" + }, + { + "internalType": "int24", + "name": "tick", + "type": "int24" + }, + { + "internalType": "uint16", + "name": "observationIndex", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "observationCardinality", + "type": "uint16" + }, + { + "internalType": "uint16", + "name": "observationCardinalityNext", + "type": "uint16" + }, + { + "internalType": "uint8", + "name": "feeProtocol", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "unlocked", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "tickLower", + "type": "int24" + }, + { + "internalType": "int24", + "name": "tickUpper", + "type": "int24" + } + ], + "name": "snapshotCumulativesInside", + "outputs": [ + { + "internalType": "int56", + "name": "tickCumulativeInside", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityInsideX128", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "secondsInside", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "internalType": "bool", + "name": "zeroForOne", + "type": "bool" + }, + { + "internalType": "int256", + "name": "amountSpecified", + "type": "int256" + }, + { + "internalType": "uint160", + "name": "sqrtPriceLimitX96", + "type": "uint160" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "swap", + "outputs": [ + { + "internalType": "int256", + "name": "amount0", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1", + "type": "int256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "name": "tickBitmap", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "tickSpacing", + "outputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int24", + "name": "", + "type": "int24" + } + ], + "name": "ticks", + "outputs": [ + { + "internalType": "uint128", + "name": "liquidityGross", + "type": "uint128" + }, + { + "internalType": "int128", + "name": "liquidityNet", + "type": "int128" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside0X128", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "feeGrowthOutside1X128", + "type": "uint256" + }, + { + "internalType": "int56", + "name": "tickCumulativeOutside", + "type": "int56" + }, + { + "internalType": "uint160", + "name": "secondsPerLiquidityOutsideX128", + "type": "uint160" + }, + { + "internalType": "uint32", + "name": "secondsOutside", + "type": "uint32" + }, + { + "internalType": "bool", + "name": "initialized", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token0", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "token1", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/db/migrations/1721252086993-Data.js b/db/migrations/1721327933049-Data.js similarity index 99% rename from db/migrations/1721252086993-Data.js rename to db/migrations/1721327933049-Data.js index 018f55f5..ef658f51 100644 --- a/db/migrations/1721252086993-Data.js +++ b/db/migrations/1721327933049-Data.js @@ -1,5 +1,5 @@ -module.exports = class Data1721252086993 { - name = 'Data1721252086993' +module.exports = class Data1721327933049 { + name = 'Data1721327933049' async up(db) { await db.query(`CREATE TABLE "es_token" ("id" character varying NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "block_number" integer NOT NULL, "circulating" numeric NOT NULL, "staked" numeric NOT NULL, "total" numeric NOT NULL, CONSTRAINT "PK_69bef9eb94d9a5d42d726d1e661" PRIMARY KEY ("id"))`) diff --git a/schema.graphql b/schema.graphql index dc6f580f..cd993d9d 100644 --- a/schema.graphql +++ b/schema.graphql @@ -985,6 +985,7 @@ enum OTokenActivityType { Unwrap Mint Redeem + Zap Unstake Vote } diff --git a/schema/otoken.graphql b/schema/otoken.graphql index 3f02fa01..6ad10ae6 100644 --- a/schema/otoken.graphql +++ b/schema/otoken.graphql @@ -124,6 +124,7 @@ enum OTokenActivityType { Unwrap Mint Redeem + Zap Unstake Vote } diff --git a/src/abi/uniswap-v3.ts b/src/abi/uniswap-v3.ts new file mode 100644 index 00000000..a2dae7ea --- /dev/null +++ b/src/abi/uniswap-v3.ts @@ -0,0 +1,206 @@ +import * as p from '@subsquid/evm-codec' +import { event, fun, viewFun, indexed, ContractBase } from '@subsquid/evm-abi' +import type { EventParams as EParams, FunctionArguments, FunctionReturn } from '@subsquid/evm-abi' + +export const events = { + Burn: event("0x0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c", "Burn(address,int24,int24,uint128,uint256,uint256)", {"owner": indexed(p.address), "tickLower": indexed(p.int24), "tickUpper": indexed(p.int24), "amount": p.uint128, "amount0": p.uint256, "amount1": p.uint256}), + Collect: event("0x70935338e69775456a85ddef226c395fb668b63fa0115f5f20610b388e6ca9c0", "Collect(address,address,int24,int24,uint128,uint128)", {"owner": indexed(p.address), "recipient": p.address, "tickLower": indexed(p.int24), "tickUpper": indexed(p.int24), "amount0": p.uint128, "amount1": p.uint128}), + CollectProtocol: event("0x596b573906218d3411850b26a6b437d6c4522fdb43d2d2386263f86d50b8b151", "CollectProtocol(address,address,uint128,uint128)", {"sender": indexed(p.address), "recipient": indexed(p.address), "amount0": p.uint128, "amount1": p.uint128}), + Flash: event("0xbdbdb71d7860376ba52b25a5028beea23581364a40522f6bcfb86bb1f2dca633", "Flash(address,address,uint256,uint256,uint256,uint256)", {"sender": indexed(p.address), "recipient": indexed(p.address), "amount0": p.uint256, "amount1": p.uint256, "paid0": p.uint256, "paid1": p.uint256}), + IncreaseObservationCardinalityNext: event("0xac49e518f90a358f652e4400164f05a5d8f7e35e7747279bc3a93dbf584e125a", "IncreaseObservationCardinalityNext(uint16,uint16)", {"observationCardinalityNextOld": p.uint16, "observationCardinalityNextNew": p.uint16}), + Initialize: event("0x98636036cb66a9c19a37435efc1e90142190214e8abeb821bdba3f2990dd4c95", "Initialize(uint160,int24)", {"sqrtPriceX96": p.uint160, "tick": p.int24}), + Mint: event("0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde", "Mint(address,address,int24,int24,uint128,uint256,uint256)", {"sender": p.address, "owner": indexed(p.address), "tickLower": indexed(p.int24), "tickUpper": indexed(p.int24), "amount": p.uint128, "amount0": p.uint256, "amount1": p.uint256}), + SetFeeProtocol: event("0x973d8d92bb299f4af6ce49b52a8adb85ae46b9f214c4c4fc06ac77401237b133", "SetFeeProtocol(uint8,uint8,uint8,uint8)", {"feeProtocol0Old": p.uint8, "feeProtocol1Old": p.uint8, "feeProtocol0New": p.uint8, "feeProtocol1New": p.uint8}), + Swap: event("0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67", "Swap(address,address,int256,int256,uint160,uint128,int24)", {"sender": indexed(p.address), "recipient": indexed(p.address), "amount0": p.int256, "amount1": p.int256, "sqrtPriceX96": p.uint160, "liquidity": p.uint128, "tick": p.int24}), +} + +export const functions = { + burn: fun("0xa34123a7", "burn(int24,int24,uint128)", {"tickLower": p.int24, "tickUpper": p.int24, "amount": p.uint128}, {"amount0": p.uint256, "amount1": p.uint256}), + collect: fun("0x4f1eb3d8", "collect(address,int24,int24,uint128,uint128)", {"recipient": p.address, "tickLower": p.int24, "tickUpper": p.int24, "amount0Requested": p.uint128, "amount1Requested": p.uint128}, {"amount0": p.uint128, "amount1": p.uint128}), + collectProtocol: fun("0x85b66729", "collectProtocol(address,uint128,uint128)", {"recipient": p.address, "amount0Requested": p.uint128, "amount1Requested": p.uint128}, {"amount0": p.uint128, "amount1": p.uint128}), + factory: viewFun("0xc45a0155", "factory()", {}, p.address), + fee: viewFun("0xddca3f43", "fee()", {}, p.uint24), + feeGrowthGlobal0X128: viewFun("0xf3058399", "feeGrowthGlobal0X128()", {}, p.uint256), + feeGrowthGlobal1X128: viewFun("0x46141319", "feeGrowthGlobal1X128()", {}, p.uint256), + flash: fun("0x490e6cbc", "flash(address,uint256,uint256,bytes)", {"recipient": p.address, "amount0": p.uint256, "amount1": p.uint256, "data": p.bytes}, ), + increaseObservationCardinalityNext: fun("0x32148f67", "increaseObservationCardinalityNext(uint16)", {"observationCardinalityNext": p.uint16}, ), + initialize: fun("0xf637731d", "initialize(uint160)", {"sqrtPriceX96": p.uint160}, ), + liquidity: viewFun("0x1a686502", "liquidity()", {}, p.uint128), + maxLiquidityPerTick: viewFun("0x70cf754a", "maxLiquidityPerTick()", {}, p.uint128), + mint: fun("0x3c8a7d8d", "mint(address,int24,int24,uint128,bytes)", {"recipient": p.address, "tickLower": p.int24, "tickUpper": p.int24, "amount": p.uint128, "data": p.bytes}, {"amount0": p.uint256, "amount1": p.uint256}), + observations: viewFun("0x252c09d7", "observations(uint256)", {"_0": p.uint256}, {"blockTimestamp": p.uint32, "tickCumulative": p.int56, "secondsPerLiquidityCumulativeX128": p.uint160, "initialized": p.bool}), + observe: viewFun("0x883bdbfd", "observe(uint32[])", {"secondsAgos": p.array(p.uint32)}, {"tickCumulatives": p.array(p.int56), "secondsPerLiquidityCumulativeX128s": p.array(p.uint160)}), + positions: viewFun("0x514ea4bf", "positions(bytes32)", {"_0": p.bytes32}, {"liquidity": p.uint128, "feeGrowthInside0LastX128": p.uint256, "feeGrowthInside1LastX128": p.uint256, "tokensOwed0": p.uint128, "tokensOwed1": p.uint128}), + protocolFees: viewFun("0x1ad8b03b", "protocolFees()", {}, {"token0": p.uint128, "token1": p.uint128}), + setFeeProtocol: fun("0x8206a4d1", "setFeeProtocol(uint8,uint8)", {"feeProtocol0": p.uint8, "feeProtocol1": p.uint8}, ), + slot0: viewFun("0x3850c7bd", "slot0()", {}, {"sqrtPriceX96": p.uint160, "tick": p.int24, "observationIndex": p.uint16, "observationCardinality": p.uint16, "observationCardinalityNext": p.uint16, "feeProtocol": p.uint8, "unlocked": p.bool}), + snapshotCumulativesInside: viewFun("0xa38807f2", "snapshotCumulativesInside(int24,int24)", {"tickLower": p.int24, "tickUpper": p.int24}, {"tickCumulativeInside": p.int56, "secondsPerLiquidityInsideX128": p.uint160, "secondsInside": p.uint32}), + swap: fun("0x128acb08", "swap(address,bool,int256,uint160,bytes)", {"recipient": p.address, "zeroForOne": p.bool, "amountSpecified": p.int256, "sqrtPriceLimitX96": p.uint160, "data": p.bytes}, {"amount0": p.int256, "amount1": p.int256}), + tickBitmap: viewFun("0x5339c296", "tickBitmap(int16)", {"_0": p.int16}, p.uint256), + tickSpacing: viewFun("0xd0c93a7c", "tickSpacing()", {}, p.int24), + ticks: viewFun("0xf30dba93", "ticks(int24)", {"_0": p.int24}, {"liquidityGross": p.uint128, "liquidityNet": p.int128, "feeGrowthOutside0X128": p.uint256, "feeGrowthOutside1X128": p.uint256, "tickCumulativeOutside": p.int56, "secondsPerLiquidityOutsideX128": p.uint160, "secondsOutside": p.uint32, "initialized": p.bool}), + token0: viewFun("0x0dfe1681", "token0()", {}, p.address), + token1: viewFun("0xd21220a7", "token1()", {}, p.address), +} + +export class Contract extends ContractBase { + + factory() { + return this.eth_call(functions.factory, {}) + } + + fee() { + return this.eth_call(functions.fee, {}) + } + + feeGrowthGlobal0X128() { + return this.eth_call(functions.feeGrowthGlobal0X128, {}) + } + + feeGrowthGlobal1X128() { + return this.eth_call(functions.feeGrowthGlobal1X128, {}) + } + + liquidity() { + return this.eth_call(functions.liquidity, {}) + } + + maxLiquidityPerTick() { + return this.eth_call(functions.maxLiquidityPerTick, {}) + } + + observations(_0: ObservationsParams["_0"]) { + return this.eth_call(functions.observations, {_0}) + } + + observe(secondsAgos: ObserveParams["secondsAgos"]) { + return this.eth_call(functions.observe, {secondsAgos}) + } + + positions(_0: PositionsParams["_0"]) { + return this.eth_call(functions.positions, {_0}) + } + + protocolFees() { + return this.eth_call(functions.protocolFees, {}) + } + + slot0() { + return this.eth_call(functions.slot0, {}) + } + + snapshotCumulativesInside(tickLower: SnapshotCumulativesInsideParams["tickLower"], tickUpper: SnapshotCumulativesInsideParams["tickUpper"]) { + return this.eth_call(functions.snapshotCumulativesInside, {tickLower, tickUpper}) + } + + tickBitmap(_0: TickBitmapParams["_0"]) { + return this.eth_call(functions.tickBitmap, {_0}) + } + + tickSpacing() { + return this.eth_call(functions.tickSpacing, {}) + } + + ticks(_0: TicksParams["_0"]) { + return this.eth_call(functions.ticks, {_0}) + } + + token0() { + return this.eth_call(functions.token0, {}) + } + + token1() { + return this.eth_call(functions.token1, {}) + } +} + +/// Event types +export type BurnEventArgs = EParams +export type CollectEventArgs = EParams +export type CollectProtocolEventArgs = EParams +export type FlashEventArgs = EParams +export type IncreaseObservationCardinalityNextEventArgs = EParams +export type InitializeEventArgs = EParams +export type MintEventArgs = EParams +export type SetFeeProtocolEventArgs = EParams +export type SwapEventArgs = EParams + +/// Function types +export type BurnParams = FunctionArguments +export type BurnReturn = FunctionReturn + +export type CollectParams = FunctionArguments +export type CollectReturn = FunctionReturn + +export type CollectProtocolParams = FunctionArguments +export type CollectProtocolReturn = FunctionReturn + +export type FactoryParams = FunctionArguments +export type FactoryReturn = FunctionReturn + +export type FeeParams = FunctionArguments +export type FeeReturn = FunctionReturn + +export type FeeGrowthGlobal0X128Params = FunctionArguments +export type FeeGrowthGlobal0X128Return = FunctionReturn + +export type FeeGrowthGlobal1X128Params = FunctionArguments +export type FeeGrowthGlobal1X128Return = FunctionReturn + +export type FlashParams = FunctionArguments +export type FlashReturn = FunctionReturn + +export type IncreaseObservationCardinalityNextParams = FunctionArguments +export type IncreaseObservationCardinalityNextReturn = FunctionReturn + +export type InitializeParams = FunctionArguments +export type InitializeReturn = FunctionReturn + +export type LiquidityParams = FunctionArguments +export type LiquidityReturn = FunctionReturn + +export type MaxLiquidityPerTickParams = FunctionArguments +export type MaxLiquidityPerTickReturn = FunctionReturn + +export type MintParams = FunctionArguments +export type MintReturn = FunctionReturn + +export type ObservationsParams = FunctionArguments +export type ObservationsReturn = FunctionReturn + +export type ObserveParams = FunctionArguments +export type ObserveReturn = FunctionReturn + +export type PositionsParams = FunctionArguments +export type PositionsReturn = FunctionReturn + +export type ProtocolFeesParams = FunctionArguments +export type ProtocolFeesReturn = FunctionReturn + +export type SetFeeProtocolParams = FunctionArguments +export type SetFeeProtocolReturn = FunctionReturn + +export type Slot0Params = FunctionArguments +export type Slot0Return = FunctionReturn + +export type SnapshotCumulativesInsideParams = FunctionArguments +export type SnapshotCumulativesInsideReturn = FunctionReturn + +export type SwapParams = FunctionArguments +export type SwapReturn = FunctionReturn + +export type TickBitmapParams = FunctionArguments +export type TickBitmapReturn = FunctionReturn + +export type TickSpacingParams = FunctionArguments +export type TickSpacingReturn = FunctionReturn + +export type TicksParams = FunctionArguments +export type TicksReturn = FunctionReturn + +export type Token0Params = FunctionArguments +export type Token0Return = FunctionReturn + +export type Token1Params = FunctionArguments +export type Token1Return = FunctionReturn + diff --git a/src/mainnet/processors/uniswap.ts b/src/mainnet/processors/uniswap.ts index 1e0ae55b..029f38f6 100644 --- a/src/mainnet/processors/uniswap.ts +++ b/src/mainnet/processors/uniswap.ts @@ -1,5 +1,5 @@ import { LiquiditySourceType } from '@model' -import { UNISWAP_OETH_WEH_ADDRESS, addresses } from '@utils/addresses' +import { UNISWAP_V3_OETH_WEH_ADDRESS, addresses } from '@utils/addresses' import { addERC20Processing } from './erc20s' import { registerLiquiditySource } from './liquidity-sources' @@ -18,7 +18,7 @@ const pools = [ tokens: ['wstETH', 'WETH'], }, { - address: UNISWAP_OETH_WEH_ADDRESS, + address: UNISWAP_V3_OETH_WEH_ADDRESS, tokens: ['OETH', 'WETH'], }, ] as const diff --git a/src/model/generated/_oTokenActivityType.ts b/src/model/generated/_oTokenActivityType.ts index 543dfdfd..8b482960 100644 --- a/src/model/generated/_oTokenActivityType.ts +++ b/src/model/generated/_oTokenActivityType.ts @@ -12,6 +12,7 @@ export enum OTokenActivityType { Unwrap = "Unwrap", Mint = "Mint", Redeem = "Redeem", + Zap = "Zap", Unstake = "Unstake", Vote = "Vote", } diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index b558cc2b..6f6a36dd 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -9,9 +9,11 @@ import { FRXETH_ADDRESS, OETH_ADDRESS, OETH_VAULT_ADDRESS, + OETH_ZAPPER_ADDRESS, RETH_ADDRESS, SFRXETH_ADDRESS, STETH_ADDRESS, + UNISWAP_V3_OETH_WEH_ADDRESS, WETH_ADDRESS, WOETH_ADDRESS, WSTETH_ADDRESS, @@ -43,6 +45,7 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ otokenAddress: OETH_ADDRESS, vaultAddress: OETH_VAULT_ADDRESS, wotokenAddress: WOETH_ADDRESS, + zapperAddress: OETH_ZAPPER_ADDRESS, curvePools: [ { address: CURVE_ETH_OETH_POOL_ADDRESS, @@ -54,6 +57,10 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ }, ], balancerPools: ['0x7056c8dfa8182859ed0d4fb0ef0886fdf3d2edcf000200000000000000000623'], + uniswapV3: { + address: UNISWAP_V3_OETH_WEH_ADDRESS, + tokens: [WETH_ADDRESS, OETH_ADDRESS], + }, }) export const from = Math.min(otokenProcessor.from, otokenActivityProcessor.from) diff --git a/src/templates/otoken/activity-processor/activity-processor.ts b/src/templates/otoken/activity-processor/activity-processor.ts index 4b4ffd41..1daa889d 100644 --- a/src/templates/otoken/activity-processor/activity-processor.ts +++ b/src/templates/otoken/activity-processor/activity-processor.ts @@ -7,8 +7,10 @@ import { balancerActivityProcessor } from '@templates/otoken/activity-processor/ import { cowSwapActivityProcessor } from '@templates/otoken/activity-processor/sub/cow-swap' import { curveActivityProcessor } from '@templates/otoken/activity-processor/sub/curve' import { transferActivityProcessor } from '@templates/otoken/activity-processor/sub/transfer' +import { uniswapV3ActivityProcessor } from '@templates/otoken/activity-processor/sub/uniswap-v3' import { vaultActivityProcessor } from '@templates/otoken/activity-processor/sub/vault' import { wrappedActivityProcessor } from '@templates/otoken/activity-processor/sub/wrapped' +import { zapperActivityProcessor } from '@templates/otoken/activity-processor/sub/zapper' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { Activity } from '@templates/otoken/activity-types' @@ -17,17 +19,23 @@ export const createOTokenActivityProcessor = (params: { otokenAddress: string vaultAddress: string wotokenAddress?: string + zapperAddress: string curvePools: { address: string tokens: string[] }[] balancerPools: string[] + uniswapV3: { address: string; tokens: [string, string] } }) => { const processors: ActivityProcessor[] = compact([ - // Swaps - ...params.curvePools.map((pool) => curveActivityProcessor(pool)), + // TODO: Morpho Blue: https://etherscan.io/tx/0xde3e7e991f70979ffdfaf0652b4c2722773416341ca78dcdaabd3cae98f8204d#eventlog + + // Zaps + zapperActivityProcessor({ otokenAddress: params.otokenAddress, zapperAddress: params.zapperAddress }), // Swaps + uniswapV3ActivityProcessor(params.uniswapV3), + ...params.curvePools.map((pool) => curveActivityProcessor(pool)), balancerActivityProcessor({ pools: params.balancerPools }), cowSwapActivityProcessor({ address: params.otokenAddress }), params.wotokenAddress && cowSwapActivityProcessor({ address: params.wotokenAddress }), @@ -52,23 +60,22 @@ export const createOTokenActivityProcessor = (params: { } const process = async (ctx: Context) => { const activities: Activity[] = [] + // Loop through each block for (const block of ctx.blocks) { + // Group logs by transaction const transactions = groupBy(block.logs, (l) => l.transactionHash) + // Loop through each transaction's set of logs. for (const logs of Object.values(transactions)) { for (const p of processors) { - let hit = false - for (const filter of p.filters) { - if (logs.find((log) => filter.matches(log))) { - const results = await p.process(ctx, block, logs) - activities.push(...results) - hit = true - break - } + const filterMatch = p.filters.find((f) => logs.find((l) => f.matches(l))) + if (filterMatch) { + const results = await p.process(ctx, block, logs) + activities.push(...results) } - if (hit) break } } } + debugger await ctx.store.insert( activities.map( (activity) => diff --git a/src/templates/otoken/activity-processor/sub/balancer.ts b/src/templates/otoken/activity-processor/sub/balancer.ts index 1e0253e1..8f7fca83 100644 --- a/src/templates/otoken/activity-processor/sub/balancer.ts +++ b/src/templates/otoken/activity-processor/sub/balancer.ts @@ -26,6 +26,7 @@ export const balancerActivityProcessor = ({ pools }: { pools: string[] }): Activ return createActivity( { ctx, block, log }, { + processor: 'balancer', type: 'Swap', account: log.transaction!.from, exchange: 'Balancer', diff --git a/src/templates/otoken/activity-processor/sub/cow-swap.ts b/src/templates/otoken/activity-processor/sub/cow-swap.ts index 8caa9ddc..bbeb5434 100644 --- a/src/templates/otoken/activity-processor/sub/cow-swap.ts +++ b/src/templates/otoken/activity-processor/sub/cow-swap.ts @@ -28,6 +28,7 @@ export const cowSwapActivityProcessor = ({ address }: { address: string }): Acti return createActivity( { ctx, block, log }, { + processor: 'cow-swap', type: 'Swap', account: log.transaction!.from, exchange: 'Balancer', diff --git a/src/templates/otoken/activity-processor/sub/curve.ts b/src/templates/otoken/activity-processor/sub/curve.ts index 33f716cb..bc4143aa 100644 --- a/src/templates/otoken/activity-processor/sub/curve.ts +++ b/src/templates/otoken/activity-processor/sub/curve.ts @@ -24,6 +24,7 @@ export const curveActivityProcessor = ({ return createActivity( { ctx, block, log }, { + processor: 'curve', type: 'Swap', account: tokenExchange.buyer, exchange: 'Curve', diff --git a/src/templates/otoken/activity-processor/sub/transfer.ts b/src/templates/otoken/activity-processor/sub/transfer.ts index e06f523c..6b31d6b1 100644 --- a/src/templates/otoken/activity-processor/sub/transfer.ts +++ b/src/templates/otoken/activity-processor/sub/transfer.ts @@ -34,6 +34,7 @@ export const transferActivityProcessor = ({ return createActivity( { ctx, block, log }, { + processor: 'transfer', type: 'Transfer', token: log.address, from: data.from.toLowerCase(), @@ -46,6 +47,16 @@ export const transferActivityProcessor = ({ } } +const getExchangeName = ( + logs: { + log: Log + data: ReturnType + }[], +) => { + if (logs[0].log.transaction?.to === '0x6131b5fae19ea4f9d964eac0408e4408b66337b5') return 'Kyber Swap' + return logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other' +} + const calculateTransferActivityAsSwap = ( ctx: Context, block: Block, @@ -57,6 +68,7 @@ const calculateTransferActivityAsSwap = ( if (logs.length === 1) return undefined const resultMap: Record = {} const tokens = new Set() + const exchange = getExchangeName(logs) for (const { log, data } of logs) { tokens.add(log.address) // To @@ -65,8 +77,9 @@ const calculateTransferActivityAsSwap = ( createActivity( { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.to.toLowerCase()}` }, { + processor: 'transfer', type: 'Swap', - exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + exchange, contract: log.address, account: data.to.toLowerCase(), tokensOut: [], @@ -81,8 +94,9 @@ const calculateTransferActivityAsSwap = ( createActivity( { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.from.toLowerCase()}` }, { + processor: 'transfer', type: 'Swap', - exchange: logs.find((l) => l.log.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) ? '1inch' : 'other', + exchange, contract: log.address, account: data.to.toLowerCase(), tokensOut: [], diff --git a/src/templates/otoken/activity-processor/sub/uniswap-v3.ts b/src/templates/otoken/activity-processor/sub/uniswap-v3.ts new file mode 100644 index 00000000..8dd511fc --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/uniswap-v3.ts @@ -0,0 +1,62 @@ +import * as uniswapV3Abi from '@abi/uniswap-v3' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { SwapActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' + +export const uniswapV3ActivityProcessor = ({ + address, + tokens, +}: { + address: string + tokens: [string, string] +}): ActivityProcessor => { + const tradeFilter = logFilter({ + address: [address], + topic0: [uniswapV3Abi.events.Swap.topic], + transaction: true, + }) + return { + name: 'UniswapV3 Activity Processor', + filters: [tradeFilter], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const tradeLogs = logs + .filter((l) => tradeFilter.matches(l)) + .map((log) => ({ + log, + data: uniswapV3Abi.events.Swap.decode(log), + })) + return tradeLogs.flatMap(({ log, data }) => { + const senderTokens0 = { token: tokens[0], amount: data.amount0.toString() } + const senderTokens1 = { token: tokens[1], amount: data.amount1.toString() } + return [ + createActivity( + { ctx, block, log }, + { + processor: 'uniswap-v3', + type: 'Swap', + account: data.sender.toLowerCase(), + exchange: 'UniswapV3', + contract: address, + tokensIn: [data.amount0 < 0n ? senderTokens0 : senderTokens1], + tokensOut: [data.amount0 > 0n ? senderTokens0 : senderTokens1], + }, + ), + createActivity( + { ctx, block, log }, + { + processor: 'cow-swap', + type: 'Swap', + account: data.recipient.toLowerCase(), + exchange: 'Balancer', + contract: address, + tokensIn: [data.amount0 > 0n ? senderTokens0 : senderTokens1], + tokensOut: [data.amount0 < 0n ? senderTokens0 : senderTokens1], + }, + ), + ] + }) + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/vault.ts b/src/templates/otoken/activity-processor/sub/vault.ts index 89cc3b29..cbdcb6ae 100644 --- a/src/templates/otoken/activity-processor/sub/vault.ts +++ b/src/templates/otoken/activity-processor/sub/vault.ts @@ -36,6 +36,7 @@ export const vaultActivityProcessor = ({ return createActivity( { ctx, block, log }, { + processor: 'vault', type: 'Mint', contract: log.address, account: data._addr, @@ -60,6 +61,7 @@ export const vaultActivityProcessor = ({ return createActivity( { ctx, block, log }, { + processor: 'vault', type: 'Redeem', contract: log.address, account: data._addr, diff --git a/src/templates/otoken/activity-processor/sub/wrapped.ts b/src/templates/otoken/activity-processor/sub/wrapped.ts index 79d3a4b5..472242bc 100644 --- a/src/templates/otoken/activity-processor/sub/wrapped.ts +++ b/src/templates/otoken/activity-processor/sub/wrapped.ts @@ -1,7 +1,7 @@ import * as wotokenAbi from '@abi/woeth' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' -import { createActivity } from '@templates/otoken/activity-processor/utils' +import { createActivity, useActivityState } from '@templates/otoken/activity-processor/utils' import { UnwrapActivity, WrapActivity } from '@templates/otoken/activity-types' import { logFilter } from '@utils/logFilter' @@ -26,6 +26,7 @@ export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProces return createActivity( { ctx, block, log }, { + processor: 'wrapped', type: 'Wrap', contract: wrappedAddress, account: data.owner, @@ -49,6 +50,7 @@ export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProces return createActivity( { ctx, block, log }, { + processor: 'wrapped', type: 'Unwrap', contract: wrappedAddress, account: data.owner, diff --git a/src/templates/otoken/activity-processor/sub/zapper.ts b/src/templates/otoken/activity-processor/sub/zapper.ts new file mode 100644 index 00000000..14db4dfe --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/zapper.ts @@ -0,0 +1,46 @@ +import * as zapperAbi from '@abi/oeth-zapper' +import { Block, Context, Log } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { ZapActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' + +export const zapperActivityProcessor = ({ + zapperAddress, + otokenAddress, +}: { + zapperAddress: string + otokenAddress: string +}): ActivityProcessor => { + return { + name: 'Zapper Processor', + filters: [ + logFilter({ + address: [zapperAddress], + topic0: [zapperAbi.events.Zap.topic], + transaction: true, + }), + ], + async process(ctx: Context, block: Block, logs: Log[]): Promise { + const [zapFilter] = this.filters + return logs + .filter((l) => zapFilter.matches(l)) + .map((log) => { + const zap = zapperAbi.events.Zap.decode(log) + return createActivity( + { ctx, block, log }, + { + processor: 'zapper', + type: 'Zap', + account: zap.minter, + contract: zapperAddress, + tokenIn: zap.asset, + amountIn: zap.amount.toString(), + tokenOut: otokenAddress, + amountOut: zap.amount.toString(), + }, + ) + }) + }, + } +} diff --git a/src/templates/otoken/activity-processor/utils.ts b/src/templates/otoken/activity-processor/utils.ts index e4196f7a..1d7050dd 100644 --- a/src/templates/otoken/activity-processor/utils.ts +++ b/src/templates/otoken/activity-processor/utils.ts @@ -1,5 +1,11 @@ import { Block, Context, Log } from '@processor' import { Activity } from '@templates/otoken/activity-types' +import { useProcessorState } from '@utils/state' + +export const useActivityState = (ctx: Context) => + useProcessorState(ctx, 'activity-state', { + processedLogs: new Set(), + }) export const createActivity = ( { diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 0590e1ce..50d6a99c 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -1,4 +1,5 @@ export type ActivityStatus = 'idle' | 'pending' | 'signed' | 'success' | 'error' +export type Exchange = 'Curve' | 'Balancer' | '1inch' | 'other' | 'UniswapV2' | 'UniswapV3' | 'Kyber Swap' export interface ActivityBase { id: string @@ -7,6 +8,7 @@ export interface ActivityBase { status: ActivityStatus txHash: string chainId: number + processor: string } export interface ApprovalActivity extends ActivityBase { @@ -81,7 +83,7 @@ export interface UnstakeActivity extends ActivityBase { export interface SwapActivity extends ActivityBase { type: 'Swap' account: string - exchange: 'Curve' | 'Balancer' | '1inch' | 'other' + exchange: Exchange contract: string tokensIn: { token: string; amount: string }[] tokensOut: { token: string; amount: string }[] @@ -135,6 +137,16 @@ export interface RedeemActivity extends ActivityBase { amountOut: string } +export interface ZapActivity extends ActivityBase { + type: 'Zap' + account: string + contract: string + tokenIn: string + tokenOut: string + amountIn: string + amountOut: string +} + export interface VoteActivity extends ActivityBase { type: 'Vote' account: string @@ -158,4 +170,5 @@ export type Activity = | UnwrapActivity | MintActivity | RedeemActivity + | ZapActivity | VoteActivity diff --git a/src/utils/activityFromTx.ts b/src/utils/activityFromTx.ts index a0db584d..1fec7e36 100644 --- a/src/utils/activityFromTx.ts +++ b/src/utils/activityFromTx.ts @@ -74,150 +74,3 @@ // return true // } // }) -// -// const oethEvents = compact( -// logs -// .filter((l) => l.address === OETH_ADDRESS) -// .map((log) => { -// return tryDecodeEventLog({ -// abi: oethAbi.ABI_JSON, -// data: log.data, -// topics: log.topics, -// }) -// }), -// ) -// -// const oethVaultEvents = compact( -// logs -// .filter((l) => l.address === OETH_VAULT_ADDRESS) -// .map((log) => { -// return tryDecodeEventLog({ -// abi: oethVaultAbi.ABI_JSON, -// data: log.data, -// topics: log.topics, -// }) -// }), -// ) -// -// const oneInchEvents = logs.filter((l) => l.address === ONEINCH_AGGREGATION_ROUTER_ADDRESS) -// const uniswapWethEvents = compact( -// logs -// .filter((l) => l.address === UNISWAP_OETH_WEH_ADDRESS) -// .map((log) => -// tryDecodeEventLog({ -// abi: UniswapV3SwapAbi, -// data: log.data, -// topics: log.topics, -// }), -// ), -// ) -// -// const oethTransfers = compact(oethEvents).filter((log) => log.eventName === 'Transfer') as Transfer[] -// -// const netTransfers = oethTransfers.reduce((acc, { args: { from, to, value } }) => { -// acc[from] = (acc[from] || 0n) - value -// acc[to] = (acc[to] || 0n) + value -// return acc -// }, {}) -// const nonZeroBalances: { [address: string]: bigint } = Object.keys(netTransfers).reduce( -// (acc, key) => { -// if (netTransfers[key] !== 0n) { -// acc[key] = netTransfers[key] -// } -// return acc -// }, -// {} as { [address: string]: bigint }, -// ) -// -// const sumPositiveBalances = (balances: { [address: string]: bigint }): bigint => { -// return Object.values(balances).reduce((sum, value) => { -// if (value > 0n) { -// return sum + value -// } -// return sum -// }, 0n) -// } -// -// const totalPositiveBalance = sumPositiveBalances(nonZeroBalances) -// -// let data: -// | { -// callDataLast4Bytes?: string -// exchange?: string -// action?: string -// fromSymbol?: string -// toSymbol?: string -// interface?: string -// amount?: bigint -// } -// | undefined = {} -// -// data.callDataLast4Bytes = transaction?.input.slice(-8) -// data.amount = totalPositiveBalance -// -// if (transaction.to === ONEINCH_AGGREGATION_ROUTER_ADDRESS || oneInchEvents.length > 0) { -// data.action = 'Swap' -// data.interface = '1inch' -// } -// if (transaction.to === COWSWAP_SETTLEMENT_ADDRESS) { -// data.action = 'Swap' -// data.exchange = 'CoW Swap' -// } -// if (transaction.to === '0x6131b5fae19ea4f9d964eac0408e4408b66337b5') { -// data.action = 'Swap' -// data.exchange = 'Kyber Swap' -// } -// -// if (frxEthOETHCurvePoolEvents.length > 0) { -// data.action = 'Swap' -// data.exchange = 'Curve' -// } -// if (balancerVaultEvents.length > 0) { -// data.action = 'Swap' -// data.exchange = 'Balancer' -// } -// -// if (uniswapWethEvents.length > 0) { -// data.action = 'Swap' -// data.exchange = 'UniswapV3' -// } -// -// if ( -// oethTransfers.some( -// (t) => -// t.args.from.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS || -// t.args.to.toLowerCase() === COWSWAP_SETTLEMENT_ADDRESS, -// ) -// ) { -// data.exchange = 'CoW Swap' -// } else if ( -// oethTransfers.some( -// (t) => -// t.args.from.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS || -// t.args.to.toLowerCase() === CURVE_ETH_OETH_POOL_ADDRESS, -// ) -// ) { -// data.exchange = 'Curve' -// } -// if (oethTransfers.length === 1 && sansGnosisSafeEvents.length === 1) { -// data.action = 'Transfer' -// } -// -// if (transaction.to === OETH_ZAPPER_ADDRESS) { -// data = decodeOethZapperTx(transaction) -// } -// -// activity.push(data) -// -// return activity -// } -// -// function decodeOethZapperTx(transaction: Transaction) { -// return { -// callDataLast4Bytes: transaction?.input.slice(-8), -// exchange: 'OETHZapper', -// action: 'Swap', -// fromSymbol: 'ETH', -// toSymbol: 'OETH', -// } -// } diff --git a/src/utils/addresses.ts b/src/utils/addresses.ts index f3ce2df1..6c932877 100644 --- a/src/utils/addresses.ts +++ b/src/utils/addresses.ts @@ -4,7 +4,7 @@ export const ETH_ADDRESS = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' export const COWSWAP_SETTLEMENT_ADDRESS = '0x9008d19f58aabd9ed0d60971565aa8510560ab41' export const ONEINCH_AGGREGATION_ROUTER_ADDRESS = '0x1111111254eeb25477b68fb85ed929f73a960582' -export const UNISWAP_OETH_WEH_ADDRESS = '0x52299416c469843f4e0d54688099966a6c7d720f' +export const UNISWAP_V3_OETH_WEH_ADDRESS = '0x52299416c469843f4e0d54688099966a6c7d720f' export const BALANCER_VAULT_ADDRESS = '0xba12222222228d8ba445958a75a0704d566bf2c8' export const CURVE_FRXETH_OETH_POOL_ADDRESS = '0xfa0bbb0a5815f6648241c9221027b70914dd8949' From e45a5497089f782b3453c9d8222fe4ecada0b97b Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Mon, 22 Jul 2024 16:42:47 -0700 Subject: [PATCH 07/15] uncomment --- src/main-oeth.ts | 26 +++++++++++--------------- src/oeth/processors/oeth.ts | 7 ++----- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/main-oeth.ts b/src/main-oeth.ts index 4201f7ea..6b8fcb3d 100644 --- a/src/main-oeth.ts +++ b/src/main-oeth.ts @@ -20,23 +20,19 @@ import * as validateOeth from './oeth/validators/validate-oeth' export const processor = { stateSchema: 'oeth-processor', processors: [ - // ccip({ chainId: 1 }), + ccip({ chainId: 1 }), oeth, - // vault, - // fraxStaking, - // morphoAave, - // dripper, - // curveLp, - // balancerMetaPoolStrategy, - // strategies, - // exchangeRates, + vault, + fraxStaking, + morphoAave, + dripper, + curveLp, + balancerMetaPoolStrategy, + strategies, + exchangeRates, ], - // postProcessors: [ - // exchangeRatesPostProcessor, - // dailyStats, - // processStatus('oeth'), - // ], - // validators: [validateOeth], + postProcessors: [exchangeRatesPostProcessor, dailyStats, processStatus('oeth')], + validators: [validateOeth], } export default processor diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 6f6a36dd..963671eb 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -65,12 +65,9 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ export const from = Math.min(otokenProcessor.from, otokenActivityProcessor.from) export const setup = (processor: EvmBatchProcessor) => { - // otokenProcessor.setup(processor) + otokenProcessor.setup(processor) otokenActivityProcessor.setup(processor) } export const process = async (ctx: Context) => { - await Promise.all([ - // otokenProcessor.process(ctx), - otokenActivityProcessor.process(ctx), - ]) + await Promise.all([otokenProcessor.process(ctx), otokenActivityProcessor.process(ctx)]) } From e338fe386f57843969804d1f3a72e2e882d10549 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 23 Jul 2024 18:36:30 -0700 Subject: [PATCH 08/15] feat: activity-rework --- src/oeth/processors/ccip.ts | 12 +++-- .../activity-processor/activity-processor.ts | 5 ++- .../activity-processor/sub/ccip-bridge.ts | 45 +++++++++++++++++++ .../otoken/activity-processor/utils.ts | 15 ++++--- src/templates/otoken/activity-types.ts | 11 ++++- src/utils/state.ts | 8 ++++ 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/templates/otoken/activity-processor/sub/ccip-bridge.ts diff --git a/src/oeth/processors/ccip.ts b/src/oeth/processors/ccip.ts index 588d79c2..5734e855 100644 --- a/src/oeth/processors/ccip.ts +++ b/src/oeth/processors/ccip.ts @@ -3,10 +3,11 @@ import * as ccipOnRampAbi from '@abi/ccip-evm2evmonramp' import * as ccipRouter from '@abi/ccip-router' import * as erc20Abi from '@abi/erc20' import { BridgeTransfer, BridgeTransferState } from '@model' -import { Context } from '@processor' +import { Block, Context, Log } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' import { WOETH_ADDRESS, WOETH_ARBITRUM_ADDRESS } from '@utils/addresses' import { logFilter } from '@utils/logFilter' +import { publishProcessorState } from '@utils/state' import { traceFilter } from '@utils/traceFilter' // Code Reference: https://github.com/smartcontractkit/smart-contract-examples/tree/main/ccip-offchain @@ -17,8 +18,9 @@ import { traceFilter } from '@utils/traceFilter' // These can be retrieved using the chain selector id on the router function `getOffRamps` // States: https://github.com/smartcontractkit/smart-contract-examples/blob/main/ccip-offchain/config/messageState.json -interface ProcessResult { +export interface CCIPProcessorResult { transfers: Map + transfersWithLogs: Map bridgeTransferStates: Map } @@ -96,8 +98,9 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { } const process = async (ctx: Context) => { - const result: ProcessResult = { + const result: CCIPProcessorResult = { transfers: new Map(), + transfersWithLogs: new Map(), bridgeTransferStates: new Map(), } @@ -122,6 +125,7 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { bridgeTransfer.txHashOut = log.transactionHash bridgeTransfer.state = data.state result.transfers.set(state.id, bridgeTransfer) + result.transfersWithLogs.set(state.id, { block, log, transfer: bridgeTransfer }) } // console.log(state) } @@ -165,12 +169,14 @@ export const ccip = (params: { chainId: 1 | 42161 }) => { }) // console.log(transfer) result.transfers.set(transfer.id, transfer) + result.transfersWithLogs.set(transfer.id, { block, log, transfer }) } } } } } + publishProcessorState(ctx, 'ccip', result) await ctx.store.upsert([...result.transfers.values()]) await ctx.store.upsert([...result.bridgeTransferStates.values()]) } diff --git a/src/templates/otoken/activity-processor/activity-processor.ts b/src/templates/otoken/activity-processor/activity-processor.ts index 1daa889d..0a7ff8d6 100644 --- a/src/templates/otoken/activity-processor/activity-processor.ts +++ b/src/templates/otoken/activity-processor/activity-processor.ts @@ -4,6 +4,7 @@ import { OTokenActivity, OTokenActivityType } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' import { balancerActivityProcessor } from '@templates/otoken/activity-processor/sub/balancer' +import { ccipBridgeActivityProcessor } from '@templates/otoken/activity-processor/sub/ccip-bridge' import { cowSwapActivityProcessor } from '@templates/otoken/activity-processor/sub/cow-swap' import { curveActivityProcessor } from '@templates/otoken/activity-processor/sub/curve' import { transferActivityProcessor } from '@templates/otoken/activity-processor/sub/transfer' @@ -30,6 +31,9 @@ export const createOTokenActivityProcessor = (params: { const processors: ActivityProcessor[] = compact([ // TODO: Morpho Blue: https://etherscan.io/tx/0xde3e7e991f70979ffdfaf0652b4c2722773416341ca78dcdaabd3cae98f8204d#eventlog + // Bridges + ccipBridgeActivityProcessor(), + // Zaps zapperActivityProcessor({ otokenAddress: params.otokenAddress, zapperAddress: params.zapperAddress }), @@ -75,7 +79,6 @@ export const createOTokenActivityProcessor = (params: { } } } - debugger await ctx.store.insert( activities.map( (activity) => diff --git a/src/templates/otoken/activity-processor/sub/ccip-bridge.ts b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts new file mode 100644 index 00000000..83a802a9 --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts @@ -0,0 +1,45 @@ +import { Context } from '@processor' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { BridgeActivity } from '@templates/otoken/activity-types' +import { waitForProcessorState } from '@utils/state' + +import { CCIPProcessorResult } from '../../../../oeth/processors/ccip' + +export const ccipBridgeActivityProcessor = (): ActivityProcessor => { + return { + name: 'CCIP Bridge Processor', + filters: [], + async process(ctx: Context): Promise { + const results: BridgeActivity[] = [] + const ccipResult = await waitForProcessorState(ctx, 'ccip') + for (const { block, log, transfer } of ccipResult.transfersWithLogs.values()) { + results.push( + createActivity( + { ctx, block, log }, + { + processor: 'ccip-bridge', + type: 'Bridge', + txHashIn: transfer.txHashIn, + txHashOut: transfer.txHashOut, + messageId: transfer.messageId, + bridge: transfer.bridge, + transactor: transfer.transactor, + sender: transfer.sender, + receiver: transfer.receiver, + chainIn: transfer.chainIn, + chainOut: transfer.chainOut, + tokenIn: transfer.tokenIn, + tokenOut: transfer.tokenOut, + amountIn: transfer.amountIn.toString(), + amountOut: transfer.amountOut.toString(), + state: transfer.state, + }, + ), + ) + } + + return results + }, + } +} diff --git a/src/templates/otoken/activity-processor/utils.ts b/src/templates/otoken/activity-processor/utils.ts index 1d7050dd..5c71b96e 100644 --- a/src/templates/otoken/activity-processor/utils.ts +++ b/src/templates/otoken/activity-processor/utils.ts @@ -1,3 +1,5 @@ +import crypto from 'crypto' + import { Block, Context, Log } from '@processor' import { Activity } from '@templates/otoken/activity-types' import { useProcessorState } from '@utils/state' @@ -19,14 +21,17 @@ export const createActivity = ( log: Log id?: string }, - partial: Omit, -) => - ({ - id: id ?? `${ctx.chain.id}:${log.id}`, + partial: { processor: string } & Omit, +) => { + const activity = { + id: id ?? `${partial.processor}:${ctx.chain.id}:${log.id}`, chainId: ctx.chain.id, blockNumber: block.header.height, timestamp: block.header.timestamp, status: 'success', txHash: log.transactionHash, ...partial, - }) as T + } as T + activity.id += `:${crypto.createHash('sha256').update(JSON.stringify(activity)).digest('hex').substring(0, 8)}` + return activity +} diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 50d6a99c..6e9761f7 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -20,13 +20,20 @@ export interface ApprovalActivity extends ActivityBase { export interface BridgeActivity extends ActivityBase { type: 'Bridge' - from: string - to: string + txHashIn: string + txHashOut: string | undefined | null + messageId: string + bridge: string + transactor: string + sender: string + receiver: string chainIn: number chainOut: number tokenIn: string tokenOut: string amountIn: string + amountOut: string + state: number } export interface ClaimRewardsActivity extends ActivityBase { diff --git a/src/utils/state.ts b/src/utils/state.ts index 1f6305fe..6b99381c 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -15,6 +15,11 @@ export const useProcessorState = (ctx: Context, key: string, defaultValue?: T ] as const } +/** + * Not for continuously updating state within a single context. + * Use this to distribute state throughout processors one time. + * *Not for gradual/continuous update within the context.* + */ export const publishProcessorState = (ctx: Context, key: string, state: T) => { const [, setState] = useProcessorState(ctx, `waitForProcessorState:${key}`) const [listeners] = useProcessorState<((state: T) => void)[]>(ctx, `waitForProcessorState-listeners:${key}`, []) @@ -22,6 +27,9 @@ export const publishProcessorState = (ctx: Context, key: string, state: T) => listeners.forEach((listener) => listener(state)) } +/** + * Wait for processor state to be set and retrieve it. + */ export const waitForProcessorState = (ctx: Context, key: string) => { return new Promise((resolve) => { const [state] = useProcessorState(ctx, `waitForProcessorState:${key}`) From 3565d488558e0ee13697d220d00ab71476f266e5 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Tue, 23 Jul 2024 19:43:39 -0700 Subject: [PATCH 09/15] feat: activity-rework --- src/oeth/processors/oeth.ts | 2 + .../activity-processor/activity-processor.ts | 11 +++++- .../otoken/activity-processor/sub/approval.ts | 38 +++++++++++++++++++ .../activity-processor/sub/ccip-bridge.ts | 12 +++++- .../otoken/activity-processor/utils.ts | 5 ++- src/templates/otoken/activity-types.ts | 13 +++++-- 6 files changed, 74 insertions(+), 7 deletions(-) create mode 100644 src/templates/otoken/activity-processor/sub/approval.ts diff --git a/src/oeth/processors/oeth.ts b/src/oeth/processors/oeth.ts index 963671eb..283dec6c 100644 --- a/src/oeth/processors/oeth.ts +++ b/src/oeth/processors/oeth.ts @@ -16,6 +16,7 @@ import { UNISWAP_V3_OETH_WEH_ADDRESS, WETH_ADDRESS, WOETH_ADDRESS, + WOETH_ARBITRUM_ADDRESS, WSTETH_ADDRESS, } from '@utils/addresses' @@ -45,6 +46,7 @@ const otokenActivityProcessor = createOTokenActivityProcessor({ otokenAddress: OETH_ADDRESS, vaultAddress: OETH_VAULT_ADDRESS, wotokenAddress: WOETH_ADDRESS, + wotokenArbitrumAddress: WOETH_ARBITRUM_ADDRESS, zapperAddress: OETH_ZAPPER_ADDRESS, curvePools: [ { diff --git a/src/templates/otoken/activity-processor/activity-processor.ts b/src/templates/otoken/activity-processor/activity-processor.ts index 0a7ff8d6..69a4b83b 100644 --- a/src/templates/otoken/activity-processor/activity-processor.ts +++ b/src/templates/otoken/activity-processor/activity-processor.ts @@ -3,6 +3,7 @@ import { compact, groupBy } from 'lodash' import { OTokenActivity, OTokenActivityType } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' +import { approvalActivityProcessor } from '@templates/otoken/activity-processor/sub/approval' import { balancerActivityProcessor } from '@templates/otoken/activity-processor/sub/balancer' import { ccipBridgeActivityProcessor } from '@templates/otoken/activity-processor/sub/ccip-bridge' import { cowSwapActivityProcessor } from '@templates/otoken/activity-processor/sub/cow-swap' @@ -20,6 +21,8 @@ export const createOTokenActivityProcessor = (params: { otokenAddress: string vaultAddress: string wotokenAddress?: string + wotokenArbitrumAddress?: string + zapperAddress: string curvePools: { address: string @@ -31,8 +34,14 @@ export const createOTokenActivityProcessor = (params: { const processors: ActivityProcessor[] = compact([ // TODO: Morpho Blue: https://etherscan.io/tx/0xde3e7e991f70979ffdfaf0652b4c2722773416341ca78dcdaabd3cae98f8204d#eventlog + // Approvals + approvalActivityProcessor(params), + // Bridges - ccipBridgeActivityProcessor(), + params.wotokenAddress && + ccipBridgeActivityProcessor({ + wotokenAddresses: compact([params.wotokenAddress, params.wotokenArbitrumAddress]), + }), // Zaps zapperActivityProcessor({ otokenAddress: params.otokenAddress, zapperAddress: params.zapperAddress }), diff --git a/src/templates/otoken/activity-processor/sub/approval.ts b/src/templates/otoken/activity-processor/sub/approval.ts new file mode 100644 index 00000000..c18e596c --- /dev/null +++ b/src/templates/otoken/activity-processor/sub/approval.ts @@ -0,0 +1,38 @@ +import * as otokenAbi from '@abi/otoken' +import { ActivityProcessor } from '@templates/otoken/activity-processor/types' +import { createActivity } from '@templates/otoken/activity-processor/utils' +import { ApprovalActivity } from '@templates/otoken/activity-types' +import { logFilter } from '@utils/logFilter' + +export const approvalActivityProcessor = ({ + otokenAddress, +}: { + otokenAddress: string +}): ActivityProcessor => { + const approvalFilter = logFilter({ address: [otokenAddress], topic0: [otokenAbi.events.Approval.topic] }) + return { + name: 'Approval Processor', + filters: [approvalFilter], + process: async (ctx, block, logs) => { + const result: ApprovalActivity[] = [] + const approvalLogs = logs.filter((l) => approvalFilter.matches(l)) + result.push( + ...approvalLogs.map((log) => { + const data = otokenAbi.events.Approval.decode(log) + return createActivity( + { ctx, block, log }, + { + processor: 'approval', + type: 'Approval', + owner: data.owner, + spender: data.spender, + token: otokenAddress, + value: data.value.toString(), + }, + ) + }), + ) + return result + }, + } +} diff --git a/src/templates/otoken/activity-processor/sub/ccip-bridge.ts b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts index 83a802a9..530aee96 100644 --- a/src/templates/otoken/activity-processor/sub/ccip-bridge.ts +++ b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts @@ -1,12 +1,12 @@ import { Context } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' -import { BridgeActivity } from '@templates/otoken/activity-types' +import { ActivityStatus, BridgeActivity } from '@templates/otoken/activity-types' import { waitForProcessorState } from '@utils/state' import { CCIPProcessorResult } from '../../../../oeth/processors/ccip' -export const ccipBridgeActivityProcessor = (): ActivityProcessor => { +export const ccipBridgeActivityProcessor = (params: { wotokenAddresses: string[] }): ActivityProcessor => { return { name: 'CCIP Bridge Processor', filters: [], @@ -14,12 +14,20 @@ export const ccipBridgeActivityProcessor = (): ActivityProcessor => { const results: BridgeActivity[] = [] const ccipResult = await waitForProcessorState(ctx, 'ccip') for (const { block, log, transfer } of ccipResult.transfersWithLogs.values()) { + if ( + !params.wotokenAddresses.includes(transfer.tokenIn) && + !params.wotokenAddresses.includes(transfer.tokenOut) + ) { + // Skip the transfer if it is not one of our wotoken addresses. + continue + } results.push( createActivity( { ctx, block, log }, { processor: 'ccip-bridge', type: 'Bridge', + status: (['signed', 'signed', 'success', 'error'] as const)[transfer.state], txHashIn: transfer.txHashIn, txHashOut: transfer.txHashOut, messageId: transfer.messageId, diff --git a/src/templates/otoken/activity-processor/utils.ts b/src/templates/otoken/activity-processor/utils.ts index 5c71b96e..a822ca4c 100644 --- a/src/templates/otoken/activity-processor/utils.ts +++ b/src/templates/otoken/activity-processor/utils.ts @@ -21,7 +21,10 @@ export const createActivity = ( log: Log id?: string }, - partial: { processor: string } & Omit, + partial: { + processor: string + status?: T['status'] + } & Omit, ) => { const activity = { id: id ?? `${partial.processor}:${ctx.chain.id}:${log.id}`, diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index 6e9761f7..ea9ce96d 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -13,9 +13,10 @@ export interface ActivityBase { export interface ApprovalActivity extends ActivityBase { type: 'Approval' - account: string - tokenIn: string - amountIn: string + owner: string + spender: string + token: string + value: string } export interface BridgeActivity extends ActivityBase { @@ -33,6 +34,12 @@ export interface BridgeActivity extends ActivityBase { tokenOut: string amountIn: string amountOut: string + /** + * 0 = untouched + * 1 = processing + * 2 = complete + * 3 = failed + */ state: number } From d375691cd546b979f4b4b8c19be5a8a5152b25e4 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Wed, 24 Jul 2024 16:16:07 -0700 Subject: [PATCH 10/15] feat: activity-rework --- .../activity-processor/activity-processor.ts | 41 ++++++++----------- .../otoken/activity-processor/sub/approval.ts | 11 ++--- .../otoken/activity-processor/sub/balancer.ts | 13 ++++-- .../activity-processor/sub/ccip-bridge.ts | 14 ++++--- .../otoken/activity-processor/sub/cow-swap.ts | 13 ++++-- .../otoken/activity-processor/sub/curve.ts | 7 +++- .../otoken/activity-processor/sub/transfer.ts | 35 +++++++++------- .../activity-processor/sub/uniswap-v3.ts | 11 +++-- .../otoken/activity-processor/sub/vault.ts | 9 ++-- .../otoken/activity-processor/sub/wrapped.ts | 31 ++++++++------ .../otoken/activity-processor/sub/zapper.ts | 5 ++- .../otoken/activity-processor/types.ts | 6 +-- .../otoken/activity-processor/utils.ts | 28 +++++++++---- src/templates/otoken/activity-types.ts | 25 +++++++---- 14 files changed, 148 insertions(+), 101 deletions(-) diff --git a/src/templates/otoken/activity-processor/activity-processor.ts b/src/templates/otoken/activity-processor/activity-processor.ts index 69a4b83b..66d5d290 100644 --- a/src/templates/otoken/activity-processor/activity-processor.ts +++ b/src/templates/otoken/activity-processor/activity-processor.ts @@ -40,6 +40,7 @@ export const createOTokenActivityProcessor = (params: { // Bridges params.wotokenAddress && ccipBridgeActivityProcessor({ + otokenAddress: params.otokenAddress, wotokenAddresses: compact([params.wotokenAddress, params.wotokenArbitrumAddress]), }), @@ -47,21 +48,29 @@ export const createOTokenActivityProcessor = (params: { zapperActivityProcessor({ otokenAddress: params.otokenAddress, zapperAddress: params.zapperAddress }), // Swaps - uniswapV3ActivityProcessor(params.uniswapV3), - ...params.curvePools.map((pool) => curveActivityProcessor(pool)), - balancerActivityProcessor({ pools: params.balancerPools }), - cowSwapActivityProcessor({ address: params.otokenAddress }), - params.wotokenAddress && cowSwapActivityProcessor({ address: params.wotokenAddress }), + uniswapV3ActivityProcessor({ otokenAddress: params.otokenAddress, ...params.uniswapV3 }), + ...params.curvePools.map((pool) => curveActivityProcessor({ otokenAddress: params.otokenAddress, ...pool })), + balancerActivityProcessor({ otokenAddress: params.otokenAddress, pools: params.balancerPools }), + cowSwapActivityProcessor({ otokenAddress: params.otokenAddress, address: params.otokenAddress }), + params.wotokenAddress && + cowSwapActivityProcessor({ + otokenAddress: params.otokenAddress, + address: params.wotokenAddress, + }), // Wraps & Unwraps - params.wotokenAddress && wrappedActivityProcessor(params.wotokenAddress), + params.wotokenAddress && + wrappedActivityProcessor({ + otokenAddress: params.otokenAddress, + wotokenAddress: params.wotokenAddress, + }), // Mints & Redeems vaultActivityProcessor({ otokenAddress: params.otokenAddress, vaultAddress: params.vaultAddress }), // Transfers & Swaps transferActivityProcessor({ otokenAddress: params.otokenAddress }), - ]) + ]).filter((p) => p.name.includes('Approval')) const from = params.from const setup = (processor: EvmBatchProcessor) => { @@ -72,7 +81,7 @@ export const createOTokenActivityProcessor = (params: { } } const process = async (ctx: Context) => { - const activities: Activity[] = [] + const activities: OTokenActivity[] = [] // Loop through each block for (const block of ctx.blocks) { // Group logs by transaction @@ -88,21 +97,7 @@ export const createOTokenActivityProcessor = (params: { } } } - await ctx.store.insert( - activities.map( - (activity) => - new OTokenActivity({ - id: activity.id, - chainId: activity.chainId, - type: OTokenActivityType[activity.type], - txHash: activity.txHash, - blockNumber: activity.blockNumber, - timestamp: new Date(activity.timestamp), - otoken: params.otokenAddress, - data: activity, - }), - ), - ) + await ctx.store.insert(activities) } return { from, setup, process } } diff --git a/src/templates/otoken/activity-processor/sub/approval.ts b/src/templates/otoken/activity-processor/sub/approval.ts index c18e596c..893fdf4e 100644 --- a/src/templates/otoken/activity-processor/sub/approval.ts +++ b/src/templates/otoken/activity-processor/sub/approval.ts @@ -1,26 +1,23 @@ import * as otokenAbi from '@abi/otoken' +import { OTokenActivity } from '@model' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' import { ApprovalActivity } from '@templates/otoken/activity-types' import { logFilter } from '@utils/logFilter' -export const approvalActivityProcessor = ({ - otokenAddress, -}: { - otokenAddress: string -}): ActivityProcessor => { +export const approvalActivityProcessor = ({ otokenAddress }: { otokenAddress: string }): ActivityProcessor => { const approvalFilter = logFilter({ address: [otokenAddress], topic0: [otokenAbi.events.Approval.topic] }) return { name: 'Approval Processor', filters: [approvalFilter], process: async (ctx, block, logs) => { - const result: ApprovalActivity[] = [] + const result: OTokenActivity[] = [] const approvalLogs = logs.filter((l) => approvalFilter.matches(l)) result.push( ...approvalLogs.map((log) => { const data = otokenAbi.events.Approval.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'approval', type: 'Approval', diff --git a/src/templates/otoken/activity-processor/sub/balancer.ts b/src/templates/otoken/activity-processor/sub/balancer.ts index 8f7fca83..f7a9a542 100644 --- a/src/templates/otoken/activity-processor/sub/balancer.ts +++ b/src/templates/otoken/activity-processor/sub/balancer.ts @@ -1,4 +1,5 @@ import * as balancerVaultAbi from '@abi/balancer-vault' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -6,7 +7,13 @@ import { SwapActivity } from '@templates/otoken/activity-types' import { BALANCER_VAULT_ADDRESS } from '@utils/addresses' import { logFilter } from '@utils/logFilter' -export const balancerActivityProcessor = ({ pools }: { pools: string[] }): ActivityProcessor => { +export const balancerActivityProcessor = ({ + otokenAddress, + pools, +}: { + otokenAddress: string + pools: string[] +}): ActivityProcessor => { return { name: 'Balancer Pool Processor', filters: [ @@ -17,14 +24,14 @@ export const balancerActivityProcessor = ({ pools }: { pools: string[] }): Activ transaction: true, }), ], - async process(ctx: Context, block: Block, logs: Log[]): Promise { + async process(ctx: Context, block: Block, logs: Log[]): Promise { const [swapFilter] = this.filters return logs .filter((l) => swapFilter.matches(l)) .map((log) => { const swap = balancerVaultAbi.events.Swap.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'balancer', type: 'Swap', diff --git a/src/templates/otoken/activity-processor/sub/ccip-bridge.ts b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts index 530aee96..77cb9ae9 100644 --- a/src/templates/otoken/activity-processor/sub/ccip-bridge.ts +++ b/src/templates/otoken/activity-processor/sub/ccip-bridge.ts @@ -1,17 +1,21 @@ +import { OTokenActivity } from '@model' import { Context } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' -import { ActivityStatus, BridgeActivity } from '@templates/otoken/activity-types' +import { BridgeActivity } from '@templates/otoken/activity-types' import { waitForProcessorState } from '@utils/state' import { CCIPProcessorResult } from '../../../../oeth/processors/ccip' -export const ccipBridgeActivityProcessor = (params: { wotokenAddresses: string[] }): ActivityProcessor => { +export const ccipBridgeActivityProcessor = (params: { + otokenAddress: string + wotokenAddresses: string[] +}): ActivityProcessor => { return { name: 'CCIP Bridge Processor', filters: [], - async process(ctx: Context): Promise { - const results: BridgeActivity[] = [] + async process(ctx: Context): Promise { + const results: OTokenActivity[] = [] const ccipResult = await waitForProcessorState(ctx, 'ccip') for (const { block, log, transfer } of ccipResult.transfersWithLogs.values()) { if ( @@ -23,7 +27,7 @@ export const ccipBridgeActivityProcessor = (params: { wotokenAddresses: string[] } results.push( createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress: params.otokenAddress }, { processor: 'ccip-bridge', type: 'Bridge', diff --git a/src/templates/otoken/activity-processor/sub/cow-swap.ts b/src/templates/otoken/activity-processor/sub/cow-swap.ts index bbeb5434..6b29134c 100644 --- a/src/templates/otoken/activity-processor/sub/cow-swap.ts +++ b/src/templates/otoken/activity-processor/sub/cow-swap.ts @@ -1,4 +1,5 @@ import * as cowswapSettlementAbi from '@abi/cow-swap-settlement' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -6,7 +7,13 @@ import { SwapActivity } from '@templates/otoken/activity-types' import { COWSWAP_SETTLEMENT_ADDRESS } from '@utils/addresses' import { logFilter } from '@utils/logFilter' -export const cowSwapActivityProcessor = ({ address }: { address: string }): ActivityProcessor => { +export const cowSwapActivityProcessor = ({ + otokenAddress, + address, +}: { + otokenAddress: string + address: string +}): ActivityProcessor => { const tradeFilter = logFilter({ address: [COWSWAP_SETTLEMENT_ADDRESS], topic0: [cowswapSettlementAbi.events.Trade.topic], @@ -15,7 +22,7 @@ export const cowSwapActivityProcessor = ({ address }: { address: string }): Acti return { name: 'Cowswap Activity Processor', filters: [tradeFilter], - async process(ctx: Context, block: Block, logs: Log[]): Promise { + async process(ctx: Context, block: Block, logs: Log[]): Promise { const tradeLogs = logs .filter((l) => tradeFilter.matches(l)) .map((log) => ({ @@ -26,7 +33,7 @@ export const cowSwapActivityProcessor = ({ address }: { address: string }): Acti .filter(({ data }) => data.buyToken.toLowerCase() === address || data.sellToken.toLowerCase() === address) .map(({ log, data }) => { return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'cow-swap', type: 'Swap', diff --git a/src/templates/otoken/activity-processor/sub/curve.ts b/src/templates/otoken/activity-processor/sub/curve.ts index bc4143aa..199729fe 100644 --- a/src/templates/otoken/activity-processor/sub/curve.ts +++ b/src/templates/otoken/activity-processor/sub/curve.ts @@ -1,4 +1,5 @@ import * as curvePoolAbi from '@abi/curve-lp-token' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -6,23 +7,25 @@ import { SwapActivity } from '@templates/otoken/activity-types' import { logFilter } from '@utils/logFilter' export const curveActivityProcessor = ({ + otokenAddress, address, tokens, }: { + otokenAddress: string address: string tokens: string[] }): ActivityProcessor => { return { name: 'Curve Pool Processor', filters: [logFilter({ address: [address], topic0: [curvePoolAbi.events.TokenExchange.topic] })], - async process(ctx: Context, block: Block, logs: Log[]): Promise { + async process(ctx: Context, block: Block, logs: Log[]): Promise { const [tokenExchangeFilter] = this.filters return logs .filter((l) => tokenExchangeFilter.matches(l)) .map((log) => { const tokenExchange = curvePoolAbi.events.TokenExchange.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'curve', type: 'Swap', diff --git a/src/templates/otoken/activity-processor/sub/transfer.ts b/src/templates/otoken/activity-processor/sub/transfer.ts index 6b31d6b1..036c246e 100644 --- a/src/templates/otoken/activity-processor/sub/transfer.ts +++ b/src/templates/otoken/activity-processor/sub/transfer.ts @@ -1,5 +1,6 @@ import * as otokenAbi from '@abi/otoken' import * as wotokenAbi from '@abi/woeth' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -7,11 +8,7 @@ import { SwapActivity, TransferActivity } from '@templates/otoken/activity-types import { ONEINCH_AGGREGATION_ROUTER_ADDRESS } from '@utils/addresses' import { logFilter } from '@utils/logFilter' -export const transferActivityProcessor = ({ - otokenAddress, -}: { - otokenAddress: string -}): ActivityProcessor => { +export const transferActivityProcessor = ({ otokenAddress }: { otokenAddress: string }): ActivityProcessor => { const transferFilter = logFilter({ address: [otokenAddress], topic0: [otokenAbi.events.Transfer.topic], @@ -27,12 +24,12 @@ export const transferActivityProcessor = ({ data: otokenAbi.events.Transfer.decode(log), })) - const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs) + const swapActivity = calculateTransferActivityAsSwap(ctx, block, transferLogs, otokenAddress) if (swapActivity) return swapActivity return transferLogs.map(({ log, data }) => { return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'transfer', type: 'Transfer', @@ -64,18 +61,19 @@ const calculateTransferActivityAsSwap = ( log: Log data: ReturnType }[], + otokenAddress: string, ) => { if (logs.length === 1) return undefined - const resultMap: Record = {} + const resultMap: Record = {} const tokens = new Set() const exchange = getExchangeName(logs) for (const { log, data } of logs) { tokens.add(log.address) // To - resultMap[data.to.toLowerCase()] = + const toActivity = resultMap[data.to.toLowerCase()] ?? createActivity( - { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.to.toLowerCase()}` }, + { ctx, block, log: logs[0].log, otokenAddress }, { processor: 'transfer', type: 'Swap', @@ -86,13 +84,15 @@ const calculateTransferActivityAsSwap = ( tokensIn: [], }, ) - resultMap[data.to.toLowerCase()].tokensIn.push({ token: log.address, amount: data.value.toString() }) + const toSwapActivity = toActivity.data as SwapActivity + toSwapActivity.tokensIn.push({ token: log.address, amount: data.value.toString() }) + resultMap[data.to.toLowerCase()] = toActivity // From - resultMap[data.from.toLowerCase()] = + const fromActivity = resultMap[data.from.toLowerCase()] ?? createActivity( - { ctx, block, log: logs[0].log, id: `${ctx.chain.id}:${log.id}:${data.from.toLowerCase()}` }, + { ctx, block, log: logs[0].log, otokenAddress }, { processor: 'transfer', type: 'Swap', @@ -103,11 +103,16 @@ const calculateTransferActivityAsSwap = ( tokensIn: [], }, ) - resultMap[data.from.toLowerCase()].tokensOut.push({ token: log.address, amount: data.value.toString() }) + const fromSwapActivity = fromActivity.data as SwapActivity + resultMap[data.from.toLowerCase()] = fromActivity + fromSwapActivity.tokensOut.push({ token: log.address, amount: data.value.toString() }) } if (tokens.size <= 1) return undefined // We are a swap if we sent and received more than one token - const results = Object.values(resultMap).filter((r) => r.tokensIn.length > 0 && r.tokensOut.length > 0) + const results = Object.values(resultMap).filter((r) => { + const activity = r.data as SwapActivity + return activity.tokensIn.length > 0 && activity.tokensOut.length > 0 + }) if (results.length > 0) return results return undefined } diff --git a/src/templates/otoken/activity-processor/sub/uniswap-v3.ts b/src/templates/otoken/activity-processor/sub/uniswap-v3.ts index 8dd511fc..5573638f 100644 --- a/src/templates/otoken/activity-processor/sub/uniswap-v3.ts +++ b/src/templates/otoken/activity-processor/sub/uniswap-v3.ts @@ -1,4 +1,5 @@ import * as uniswapV3Abi from '@abi/uniswap-v3' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -6,12 +7,14 @@ import { SwapActivity } from '@templates/otoken/activity-types' import { logFilter } from '@utils/logFilter' export const uniswapV3ActivityProcessor = ({ + otokenAddress, address, tokens, }: { + otokenAddress: string address: string tokens: [string, string] -}): ActivityProcessor => { +}): ActivityProcessor => { const tradeFilter = logFilter({ address: [address], topic0: [uniswapV3Abi.events.Swap.topic], @@ -20,7 +23,7 @@ export const uniswapV3ActivityProcessor = ({ return { name: 'UniswapV3 Activity Processor', filters: [tradeFilter], - async process(ctx: Context, block: Block, logs: Log[]): Promise { + async process(ctx: Context, block: Block, logs: Log[]): Promise { const tradeLogs = logs .filter((l) => tradeFilter.matches(l)) .map((log) => ({ @@ -32,7 +35,7 @@ export const uniswapV3ActivityProcessor = ({ const senderTokens1 = { token: tokens[1], amount: data.amount1.toString() } return [ createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'uniswap-v3', type: 'Swap', @@ -44,7 +47,7 @@ export const uniswapV3ActivityProcessor = ({ }, ), createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'cow-swap', type: 'Swap', diff --git a/src/templates/otoken/activity-processor/sub/vault.ts b/src/templates/otoken/activity-processor/sub/vault.ts index cbdcb6ae..9b4834de 100644 --- a/src/templates/otoken/activity-processor/sub/vault.ts +++ b/src/templates/otoken/activity-processor/sub/vault.ts @@ -2,6 +2,7 @@ import { uniq } from 'lodash' import * as otokenVaultAbi from '@abi/otoken-vault' import * as wotokenAbi from '@abi/woeth' +import { OTokenActivity } from '@model' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' import { MintActivity, RedeemActivity } from '@templates/otoken/activity-types' @@ -14,7 +15,7 @@ export const vaultActivityProcessor = ({ }: { otokenAddress: string vaultAddress: string -}): ActivityProcessor => { +}): ActivityProcessor => { const mintFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Mint.topic] }) const redeemFilter = logFilter({ address: [vaultAddress], topic0: [otokenVaultAbi.events.Redeem.topic] }) const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [vaultAddress] }) @@ -23,7 +24,7 @@ export const vaultActivityProcessor = ({ name: 'Vault Processor', filters: [mintFilter, redeemFilter, transferInFilter, transferOutFilter], process: async (ctx, block, logs) => { - const result: (MintActivity | RedeemActivity)[] = [] + const result: OTokenActivity[] = [] // Mint const mintLogs = logs.filter((l) => mintFilter.matches(l)) if (mintLogs.length) { @@ -34,7 +35,7 @@ export const vaultActivityProcessor = ({ ...mintLogs.map((log) => { const data = otokenVaultAbi.events.Mint.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'vault', type: 'Mint', @@ -59,7 +60,7 @@ export const vaultActivityProcessor = ({ ...redeemLogs.map((log) => { const data = otokenVaultAbi.events.Redeem.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'vault', type: 'Redeem', diff --git a/src/templates/otoken/activity-processor/sub/wrapped.ts b/src/templates/otoken/activity-processor/sub/wrapped.ts index 472242bc..3f46cee7 100644 --- a/src/templates/otoken/activity-processor/sub/wrapped.ts +++ b/src/templates/otoken/activity-processor/sub/wrapped.ts @@ -1,20 +1,27 @@ import * as wotokenAbi from '@abi/woeth' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity, useActivityState } from '@templates/otoken/activity-processor/utils' import { UnwrapActivity, WrapActivity } from '@templates/otoken/activity-types' import { logFilter } from '@utils/logFilter' -export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProcessor => { - const depositFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Deposit.topic] }) - const withdrawFilter = logFilter({ address: [wrappedAddress], topic0: [wotokenAbi.events.Withdraw.topic] }) - const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [wrappedAddress] }) - const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [wrappedAddress] }) +export const wrappedActivityProcessor = ({ + otokenAddress, + wotokenAddress, +}: { + otokenAddress: string + wotokenAddress: string +}): ActivityProcessor => { + const depositFilter = logFilter({ address: [wotokenAddress], topic0: [wotokenAbi.events.Deposit.topic] }) + const withdrawFilter = logFilter({ address: [wotokenAddress], topic0: [wotokenAbi.events.Withdraw.topic] }) + const transferInFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic2: [wotokenAddress] }) + const transferOutFilter = logFilter({ topic0: [wotokenAbi.events.Transfer.topic], topic1: [wotokenAddress] }) return { name: 'Wrapped Processor', filters: [depositFilter, withdrawFilter, transferInFilter, transferOutFilter], async process(ctx: Context, block: Block, logs: Log[]) { - const result: (WrapActivity | UnwrapActivity)[] = [] + const result: OTokenActivity[] = [] // Wrap const depositLogs = logs.filter((l) => depositFilter.matches(l)) if (depositLogs.length) { @@ -24,14 +31,14 @@ export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProces const data = wotokenAbi.events.Deposit.decode(log) const tokenIn = transferInLog?.address ?? 'unknown' return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'wrapped', type: 'Wrap', - contract: wrappedAddress, + contract: wotokenAddress, account: data.owner, tokenIn, - tokenOut: wrappedAddress, + tokenOut: wotokenAddress, amountIn: data.assets.toString(), amountOut: data.shares.toString(), }, @@ -48,13 +55,13 @@ export const wrappedActivityProcessor = (wrappedAddress: string): ActivityProces const data = wotokenAbi.events.Withdraw.decode(log) const tokenOut = transferOutLog?.address ?? 'unknown' return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'wrapped', type: 'Unwrap', - contract: wrappedAddress, + contract: wotokenAddress, account: data.owner, - tokenIn: wrappedAddress, + tokenIn: wotokenAddress, tokenOut, amountIn: data.shares.toString(), amountOut: data.assets.toString(), diff --git a/src/templates/otoken/activity-processor/sub/zapper.ts b/src/templates/otoken/activity-processor/sub/zapper.ts index 14db4dfe..4fb660aa 100644 --- a/src/templates/otoken/activity-processor/sub/zapper.ts +++ b/src/templates/otoken/activity-processor/sub/zapper.ts @@ -1,4 +1,5 @@ import * as zapperAbi from '@abi/oeth-zapper' +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' import { ActivityProcessor } from '@templates/otoken/activity-processor/types' import { createActivity } from '@templates/otoken/activity-processor/utils' @@ -21,14 +22,14 @@ export const zapperActivityProcessor = ({ transaction: true, }), ], - async process(ctx: Context, block: Block, logs: Log[]): Promise { + async process(ctx: Context, block: Block, logs: Log[]): Promise { const [zapFilter] = this.filters return logs .filter((l) => zapFilter.matches(l)) .map((log) => { const zap = zapperAbi.events.Zap.decode(log) return createActivity( - { ctx, block, log }, + { ctx, block, log, otokenAddress }, { processor: 'zapper', type: 'Zap', diff --git a/src/templates/otoken/activity-processor/types.ts b/src/templates/otoken/activity-processor/types.ts index dfd8917f..a7da88f5 100644 --- a/src/templates/otoken/activity-processor/types.ts +++ b/src/templates/otoken/activity-processor/types.ts @@ -1,9 +1,9 @@ +import { OTokenActivity } from '@model' import { Block, Context, Log } from '@processor' -import { Activity } from '@templates/otoken/activity-types' import { LogFilter } from '@utils/logFilter' -export interface ActivityProcessor { +export interface ActivityProcessor { name: string filters: LogFilter[] - process: (ctx: Context, block: Block, logs: Log[]) => Promise + process: (ctx: Context, block: Block, logs: Log[]) => Promise } diff --git a/src/templates/otoken/activity-processor/utils.ts b/src/templates/otoken/activity-processor/utils.ts index a822ca4c..d0c47960 100644 --- a/src/templates/otoken/activity-processor/utils.ts +++ b/src/templates/otoken/activity-processor/utils.ts @@ -1,5 +1,6 @@ import crypto from 'crypto' +import { OTokenActivity, OTokenActivityType } from '@model' import { Block, Context, Log } from '@processor' import { Activity } from '@templates/otoken/activity-types' import { useProcessorState } from '@utils/state' @@ -14,27 +15,36 @@ export const createActivity = ( ctx, block, log, - id, + otokenAddress, }: { ctx: Context block: Block log: Log - id?: string + otokenAddress: string }, partial: { processor: string status?: T['status'] } & Omit, ) => { - const activity = { - id: id ?? `${partial.processor}:${ctx.chain.id}:${log.id}`, + const activity = new OTokenActivity({ chainId: ctx.chain.id, blockNumber: block.header.height, - timestamp: block.header.timestamp, - status: 'success', + timestamp: new Date(block.header.timestamp), txHash: log.transactionHash, - ...partial, - } as T - activity.id += `:${crypto.createHash('sha256').update(JSON.stringify(activity)).digest('hex').substring(0, 8)}` + type: OTokenActivityType[partial.type], + otoken: otokenAddress, + data: { + status: 'success', + ...partial, + } as T, + }) + + activity.id = `${partial.processor}:${ctx.chain.id}:${log.id}:${crypto + .createHash('sha256') + .update(JSON.stringify(activity)) + .digest('hex') + .substring(0, 8)}` + return activity } diff --git a/src/templates/otoken/activity-types.ts b/src/templates/otoken/activity-types.ts index ea9ce96d..b199d26c 100644 --- a/src/templates/otoken/activity-types.ts +++ b/src/templates/otoken/activity-types.ts @@ -43,6 +43,7 @@ export interface BridgeActivity extends ActivityBase { state: number } +// TODO export interface ClaimRewardsActivity extends ActivityBase { type: 'ClaimRewards' account: string @@ -50,6 +51,7 @@ export interface ClaimRewardsActivity extends ActivityBase { amountIn: string } +// TODO export interface DelegateVoteActivity extends ActivityBase { type: 'DelegateVote' account: string @@ -58,15 +60,7 @@ export interface DelegateVoteActivity extends ActivityBase { delegateTo: string } -export interface ExtendStakeActivity extends ActivityBase { - type: 'ExtendStake' - account: string - amountIn: string - tokenIn: string - monthDuration: number - lockupId: string -} - +// TODO export interface MigrateActivity extends ActivityBase { type: 'Migrate' account: string @@ -78,6 +72,7 @@ export interface MigrateActivity extends ActivityBase { staked?: string } +// TODO export interface StakeActivity extends ActivityBase { type: 'Stake' account: string @@ -86,6 +81,7 @@ export interface StakeActivity extends ActivityBase { monthDuration: number } +// TODO export interface UnstakeActivity extends ActivityBase { type: 'Unstake' account: string @@ -94,6 +90,16 @@ export interface UnstakeActivity extends ActivityBase { lockupId: string } +// TODO +export interface ExtendStakeActivity extends ActivityBase { + type: 'ExtendStake' + account: string + amountIn: string + tokenIn: string + monthDuration: number + lockupId: string +} + export interface SwapActivity extends ActivityBase { type: 'Swap' account: string @@ -161,6 +167,7 @@ export interface ZapActivity extends ActivityBase { amountOut: string } +// TODO export interface VoteActivity extends ActivityBase { type: 'Vote' account: string From b75b88b49f0b8318674372971ca870672827f1a8 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 25 Jul 2024 10:17:26 -0700 Subject: [PATCH 11/15] v40 --- squid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squid.yaml b/squid.yaml index 60c25a70..0099830f 100644 --- a/squid.yaml +++ b/squid.yaml @@ -1,6 +1,6 @@ manifestVersion: subsquid.io/v0.1 name: origin-squid -version: 999 +version: 40 description: 'Origin Protocol 🦑' build: deploy: From ee2d4979740649670bd2d0dca8bc1bedd33ed40b Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 25 Jul 2024 12:50:08 -0700 Subject: [PATCH 12/15] add custom resolver for upcoming analytics changes --- src/oeth/processors/strategies.ts | 5 +- src/server-extension/resolvers/index.ts | 1 + src/server-extension/strategies.ts | 87 +++++++++++++++++++ .../strategy/strategy-native-staking.ts | 1 - src/templates/strategy/strategy.ts | 6 +- 5 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/server-extension/strategies.ts diff --git a/src/oeth/processors/strategies.ts b/src/oeth/processors/strategies.ts index c1ffdae1..61226b56 100644 --- a/src/oeth/processors/strategies.ts +++ b/src/oeth/processors/strategies.ts @@ -15,7 +15,6 @@ import { WETH_ADDRESS, addresses, } from '@utils/addresses' -import { logFilter } from '@utils/logFilter' export const oethStrategies: readonly IStrategyData[] = [ { @@ -132,7 +131,7 @@ export const oethStrategies: readonly IStrategyData[] = [ }, { from: 20046251, - name: 'OETH Native Staking', + name: 'OETH Native Staking 1', contractName: 'NativeStakingSSVStrategy', address: OETH_NATIVE_STRATEGY_ADDRESSES[0], oTokenAddress: OETH_ADDRESS, @@ -143,7 +142,7 @@ export const oethStrategies: readonly IStrategyData[] = [ }, { from: 20290461, - name: 'OETH Native Staking', + name: 'OETH Native Staking 2', contractName: 'NativeStakingSSVStrategy', address: OETH_NATIVE_STRATEGY_ADDRESSES[1], oTokenAddress: OETH_ADDRESS, diff --git a/src/server-extension/resolvers/index.ts b/src/server-extension/resolvers/index.ts index 88561fe4..fa6f4bae 100644 --- a/src/server-extension/resolvers/index.ts +++ b/src/server-extension/resolvers/index.ts @@ -1,3 +1,4 @@ import 'tsconfig-paths/register' export { OGNStatsResolver } from '../ogn-stats' +export { StrategyResolver } from '../strategies' diff --git a/src/server-extension/strategies.ts b/src/server-extension/strategies.ts new file mode 100644 index 00000000..145f5f0a --- /dev/null +++ b/src/server-extension/strategies.ts @@ -0,0 +1,87 @@ +import { GraphQLResolveInfo } from 'graphql' +import { compact } from 'lodash' +import { Arg, Field, Info, Int, ObjectType, Query, Resolver } from 'type-graphql' +import { EntityManager, LessThanOrEqual } from 'typeorm' + +import { StrategyBalance } from '@model' + +import { oethStrategies } from '../oeth/processors/strategies' + +@ObjectType() +export class Strategy { + @Field(() => String, { nullable: false }) + name!: string + @Field(() => String, { nullable: false }) + contractName!: string + @Field(() => String, { nullable: false }) + address!: string + @Field(() => String, { nullable: false }) + oTokenAddress!: string + @Field(() => String, { nullable: false }) + kind!: string + @Field(() => [Balance], { nullable: false }) + balances!: Balance[] + + constructor(props: Partial) { + Object.assign(this, props) + } +} + +@ObjectType() +export class Balance { + @Field(() => String, { nullable: false }) + asset!: string + @Field(() => Date, { nullable: false }) + timestamp!: Date + @Field(() => Int, { nullable: false }) + blockNumber!: number + @Field(() => BigInt, { nullable: false }) + balance!: bigint + + constructor(props: Partial) { + Object.assign(this, props) + } +} + +@Resolver() +export class StrategyResolver { + constructor(private tx: () => Promise) {} + + @Query(() => [Strategy]) + async strategies( + @Arg('timestamp', () => String, { nullable: true }) timestamp: string | null, + @Info() info: GraphQLResolveInfo, + ): Promise { + const manager = await this.tx() + return Promise.all( + oethStrategies.map(async (s) => { + return { + name: s.name, + contractName: s.contractName, + address: s.address, + oTokenAddress: s.oTokenAddress, + kind: s.kind, + balances: compact( + await Promise.all( + s.assets.map((asset) => + manager.getRepository(StrategyBalance).findOne({ + order: { timestamp: 'desc' }, + where: { + strategy: s.address, + asset: asset.address, + timestamp: LessThanOrEqual(timestamp ? new Date(timestamp) : new Date()), + }, + }), + ), + ), + ).map((b) => ({ + asset: b.asset, + timestamp: b.timestamp, + blockNumber: b.blockNumber, + balance: b.balance, + })), + } + }), + ) + } +} diff --git a/src/templates/strategy/strategy-native-staking.ts b/src/templates/strategy/strategy-native-staking.ts index 62c63ce9..dbd3d91c 100644 --- a/src/templates/strategy/strategy-native-staking.ts +++ b/src/templates/strategy/strategy-native-staking.ts @@ -1,5 +1,4 @@ import * as abstractStrategyAbi from '@abi/initializable-abstract-strategy' -import * as nativeStakingStrategyAbi from '@abi/strategy-native-staking' import { StrategyBalance } from '@model' import { Context } from '@processor' import { EvmBatchProcessor } from '@subsquid/evm-processor' diff --git a/src/templates/strategy/strategy.ts b/src/templates/strategy/strategy.ts index b29ffaed..bb184887 100644 --- a/src/templates/strategy/strategy.ts +++ b/src/templates/strategy/strategy.ts @@ -30,7 +30,7 @@ export type IStrategyData = { name: string contractName: string address: string - kind: 'Generic' | 'Vault' | 'CurveAMO' | 'BalancerMetaStablePool' | 'BalancerComposableStablePool' | 'NativeStaking' + kind: 'Generic' | 'Vault' | 'CurveAMO' | 'BalancerMetaStablePool' | 'NativeStaking' base: { address: string decimals: number @@ -69,10 +69,6 @@ const processors: Record< Vault: strategyVault, CurveAMO: strategyCurveAMO, BalancerMetaStablePool: strategyBalancer, - BalancerComposableStablePool: { - setup: () => Promise.reject('Not implemented.'), - process: () => Promise.reject('Not implemented.'), - }, NativeStaking: strategyNativeStaking, } From c3e0e19b31e965cf40b5195f09218440cb16c8a6 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 25 Jul 2024 12:50:41 -0700 Subject: [PATCH 13/15] add custom resolver for upcoming analytics changes --- src/server-extension/strategies.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/server-extension/strategies.ts b/src/server-extension/strategies.ts index 145f5f0a..b3a1d907 100644 --- a/src/server-extension/strategies.ts +++ b/src/server-extension/strategies.ts @@ -7,6 +7,10 @@ import { StrategyBalance } from '@model' import { oethStrategies } from '../oeth/processors/strategies' +/** + * This is used by the OETH analytics Balance Sheet (marketing site) + */ + @ObjectType() export class Strategy { @Field(() => String, { nullable: false }) From 349fa0dfdd370240be30c91d0ebee325a97f3cc4 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Thu, 25 Jul 2024 12:53:33 -0700 Subject: [PATCH 14/15] v999 --- squid.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/squid.yaml b/squid.yaml index 0099830f..60c25a70 100644 --- a/squid.yaml +++ b/squid.yaml @@ -1,6 +1,6 @@ manifestVersion: subsquid.io/v0.1 name: origin-squid -version: 40 +version: 999 description: 'Origin Protocol 🦑' build: deploy: From 5e0841039d73efdb1658cfd9a90f74e06be4d8b5 Mon Sep 17 00:00:00 2001 From: Chris Jacobs Date: Fri, 26 Jul 2024 12:38:25 -0700 Subject: [PATCH 15/15] create erc20 custom resolver with `erc20StateByDay` --- src/server-extension/erc20.ts | 116 ++++++++++++++++++++++++ src/server-extension/resolvers/index.ts | 1 + 2 files changed, 117 insertions(+) create mode 100644 src/server-extension/erc20.ts diff --git a/src/server-extension/erc20.ts b/src/server-extension/erc20.ts new file mode 100644 index 00000000..6fab9c40 --- /dev/null +++ b/src/server-extension/erc20.ts @@ -0,0 +1,116 @@ +import { GraphQLResolveInfo } from 'graphql' +import { Arg, Field, Info, Int, ObjectType, Query, Resolver } from 'type-graphql' +import { EntityManager } from 'typeorm' + +@ObjectType() +export class ERC20StateByDay { + @Field(() => String, { nullable: false }) + chainId!: number + @Field(() => String, { nullable: false }) + address!: string + @Field(() => Date, { nullable: false }) + day!: Date + @Field(() => BigInt, { nullable: false }) + totalSupply!: bigint + @Field(() => Int, { nullable: false }) + holderCount!: number + + constructor(props: Partial) { + Object.assign(this, props) + } +} + +@Resolver() +export class ERC20Resolver { + constructor(private tx: () => Promise) {} + + @Query(() => [ERC20StateByDay]) + async erc20StateByDay( + @Arg('address', () => String, { nullable: false }) address: string, + @Arg('from', () => String, { nullable: false }) from: string, + @Arg('to', () => String, { nullable: true }) to: string | null, + @Info() info: GraphQLResolveInfo, + ): Promise { + const manager = await this.tx() + const results = await manager.query( + ` + WITH RECURSIVE date_series AS ( + SELECT + DATE_TRUNC('day', $2::timestamp) AS day + UNION + SELECT + day + INTERVAL '1 day' + FROM + date_series + WHERE + day + INTERVAL '1 day' <= COALESCE($3::timestamp, NOW()) + ), + latest_daily_data AS ( + SELECT DISTINCT ON (DATE_TRUNC('day', timestamp)) + DATE_TRUNC('day', timestamp) AS day, + chain_id, + address, + total_supply, + holder_count + FROM + erc20_state + WHERE + address = $1 + AND timestamp >= $2 + AND ($3::timestamp IS NULL OR timestamp <= $3) + ORDER BY + DATE_TRUNC('day', timestamp), timestamp DESC + ), + data_with_gaps AS ( + SELECT + ds.day, + ldd.chain_id, + ldd.address, + ldd.total_supply, + ldd.holder_count + FROM + date_series ds + LEFT JOIN + latest_daily_data ldd ON ds.day = ldd.day + ), + filled_data AS ( + SELECT + day, + chain_id, + address, + total_supply, + holder_count, + COALESCE(total_supply, LAG(total_supply) OVER (PARTITION BY address ORDER BY day)) AS filled_total_supply, + COALESCE(holder_count, LAG(holder_count) OVER (PARTITION BY address ORDER BY day)) AS filled_holder_count + FROM + data_with_gaps + ) + SELECT + chain_id, + address, + day, + filled_total_supply AS total_supply, + filled_holder_count AS holder_count + FROM + filled_data + WHERE + chain_id is not null + ORDER BY + day; + + `, + [address, from, to], + ) + + return results.map( + (row: any) => + new ERC20StateByDay({ + chainId: row.chain_id, + address: row.address, + day: row.day, + totalSupply: BigInt(row.total_supply), + holderCount: row.holder_count, + }), + ) + } +} diff --git a/src/server-extension/resolvers/index.ts b/src/server-extension/resolvers/index.ts index fa6f4bae..3981fde0 100644 --- a/src/server-extension/resolvers/index.ts +++ b/src/server-extension/resolvers/index.ts @@ -2,3 +2,4 @@ import 'tsconfig-paths/register' export { OGNStatsResolver } from '../ogn-stats' export { StrategyResolver } from '../strategies' +export { ERC20Resolver } from '../erc20'