-
Notifications
You must be signed in to change notification settings - Fork 27
feat(epic): add SVM client #985
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
8b3ed44
chore: create skeleton for svm spoke client (#965)
james-a-morris 6b1b65a
feat(svm): Initial SpokeUtils skeleton (#960)
pxrl 1b6939c
fix: epic branch lint (#972)
melisaguevara dff7f94
improve(Address): Support new EVM address from Base58 encoding
pxrl 60ecbce
Add svm/base16 counterpart
pxrl 58f376c
lint
pxrl 1a79e88
Merge remote-tracking branch 'origin/master'
pxrl b8aedad
Add test
pxrl eb9ae20
lint
pxrl fc4c167
Merge remote-tracking branch 'origin/pxrl/base58EVMAddress' into epic…
pxrl 27ce8ec
chore: Unify svm w/ arch/svm (#977)
pxrl 57d86cd
refactor(sdk): transition SVM events client to use blocks instead of …
james-a-morris 06e400d
feat(svmSpokeUtils): get fill deadline buffer implementation (#978)
melisaguevara c9ee25e
improve: allow SVM events client to take in non-spoke IDLs (#982)
bmzig 3e20d7e
improve(svmEventsClient): enable pda events querying (#989)
melisaguevara c5efe4a
feat: create svm spoke _update (#976)
james-a-morris 4f9b54f
improve: add tests for SVM (#1007)
james-a-morris eec28b8
feat(SvmSpokePoolClient): relayFillStatus and fillStatusArray impleme…
melisaguevara 549fee8
fix: svm spoke client missing rpc (#1023)
md0x 5ac115a
feat(SvmSpokeUtils): implement findFillEvent (#998)
melisaguevara ef8b147
chore: reuse SVMProvider type (#1025)
melisaguevara f7f5163
feat(svm): integration tests with test validator (#1015)
md0x 0146d3c
feat: Solana relayFeeCalculator and gasPriceOracle (#980)
bmzig 5bb6a2f
improve: SVM getTimestampForSlot (#1026)
melisaguevara 68280e9
chore: resync epic branch with master (#1029)
bmzig bfd4550
Revert "chore: resync epic branch with master (#1029)"
bmzig 4c1d608
Merge branch 'master' into epic/svm-client
bmzig 9b3bb77
feat: add request slow fill and fill functions and event tests (#1028)
md0x 9d93433
feat(svm-eventsClient): add methods to find deposit and fills from si…
gsteenkamp89 5582642
feat(svm): add svm test node hook at root level (#1037)
md0x fdbf818
chore: Create release v4.1.63-beta.1 (#1040)
gsteenkamp89 da60fb8
feat: expose `getNativeGasCosts` in `svmQuery` (#1039)
dohaki feebf2a
chore: bump 4.1.63-beta.2 (#1041)
dohaki 4041826
improve: generalize block finder interface for EVM (#1038)
bmzig 4151ac4
feat(sdk): implement Solana-optimized deposit finder (#983)
james-a-morris de781d1
chore: bump to next minor version (#1045)
james-a-morris 94d2ff6
Merge branch 'master' into epic/svm-client
james-a-morris e46123f
use named imports only for solana/kit (#1046)
gsteenkamp89 8426ced
feat: Mock SVMSpokePoolClient (#1010)
melisaguevara 809cfba
chore(release): v4.1.63-beta.3 (#1047)
gsteenkamp89 11b39d2
feat: add tests for fill related functions of the svm spoke pool clie…
melisaguevara 1b4dfb1
updates
bmzig File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ dist | |
| /coverage | ||
| /artifacts | ||
| /typechain-types | ||
| .ledger | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,209 @@ | ||
| import assert from "assert"; | ||
| import { Provider, Block as EthersBlock } from "@ethersproject/abstract-provider"; | ||
| import { clamp, sortedIndexBy } from "lodash"; | ||
| import { chainIsOPStack, getNetworkName } from "../../utils/NetworkUtils"; | ||
| import { isDefined } from "../../utils/TypeGuards"; | ||
| import { | ||
| BlockFinder, | ||
| type Block, | ||
| type BlockFinderOpts as Opts, | ||
| type BlockTimeAverage, | ||
| type BlockFinderHints, | ||
| } from "../../utils/BlockFinder"; | ||
| import { getCurrentTime } from "../../utils/TimeUtils"; | ||
| import { CHAIN_IDs } from "../../constants"; | ||
|
|
||
| // Extension of the EthersBlock type which implements `Block`. | ||
| interface EVMBlock extends Block, EthersBlock {} | ||
|
|
||
| // Archive requests typically commence at 128 blocks past the head of the chain. | ||
| // Round down to 120 blocks to avoid slipping into archive territory. | ||
| const defaultBlockRange = 120; | ||
|
|
||
| // Default offset to the high block number. This is subtracted from the block number of the high block | ||
| // when it is queried from the network, rather than having been specified by the caller. This is useful | ||
| // since the supplied Provider instance may be backed by multiple RPC providers, which can lead to some | ||
| // providers running slower than others and taking time to synchronise on the latest block. | ||
| const defaultHighBlockOffset = 10; | ||
|
|
||
| // Retain computations for 15 minutes. | ||
| const cacheTTL = 60 * 15; | ||
| const now = getCurrentTime(); // Seed the cache with initial values. | ||
| const blockTimes: { [chainId: number]: BlockTimeAverage } = { | ||
| [CHAIN_IDs.INK]: { average: 1, timestamp: now, blockRange: 1 }, | ||
| [CHAIN_IDs.LINEA]: { average: 3, timestamp: now, blockRange: 1 }, | ||
| [CHAIN_IDs.MAINNET]: { average: 12.5, timestamp: now, blockRange: 1 }, | ||
| [CHAIN_IDs.OPTIMISM]: { average: 2, timestamp: now, blockRange: 1 }, | ||
| [CHAIN_IDs.UNICHAIN]: { average: 1, timestamp: now, blockRange: 1 }, | ||
| }; | ||
|
|
||
| /** | ||
| * @description Compute the average block time over a block range. | ||
| * @returns Average number of seconds per block. | ||
| */ | ||
| export async function averageBlockTime( | ||
| provider: Provider, | ||
| { highBlock, highBlockOffset, blockRange }: Opts = {} | ||
| ): Promise<Pick<BlockTimeAverage, "average" | "blockRange">> { | ||
| // Does not block for StaticJsonRpcProvider. | ||
| const { chainId } = await provider.getNetwork(); | ||
|
|
||
| // OP stack chains inherit Optimism block times, but can be overridden. | ||
| const cache = blockTimes[chainId] ?? (chainIsOPStack(chainId) ? blockTimes[CHAIN_IDs.OPTIMISM] : undefined); | ||
|
|
||
| const now = getCurrentTime(); | ||
| if (isDefined(cache) && now < cache.timestamp + cacheTTL) { | ||
| return { average: cache.average, blockRange: cache.blockRange }; | ||
| } | ||
|
|
||
| // If the caller was not specific about highBlock, resolve it via the RPC provider. Subtract an offset | ||
| // to account for various RPC provider sync issues that might occur when querting the latest block. | ||
| if (!isDefined(highBlock)) { | ||
| highBlock = await provider.getBlockNumber(); | ||
| highBlock -= highBlockOffset ?? defaultHighBlockOffset; | ||
| } | ||
| blockRange ??= defaultBlockRange; | ||
|
|
||
| const earliestBlockNumber = highBlock - blockRange; | ||
| const [firstBlock, lastBlock] = await Promise.all([ | ||
| provider.getBlock(earliestBlockNumber), | ||
| provider.getBlock(highBlock), | ||
| ]); | ||
| [firstBlock, lastBlock].forEach((block: Block | undefined) => { | ||
| if (!isDefined(block?.timestamp)) { | ||
| const network = getNetworkName(chainId); | ||
| const blockNumber = block === firstBlock ? earliestBlockNumber : highBlock; | ||
| throw new Error(`BlockFinder: Failed to fetch block ${blockNumber} on ${network}`); | ||
| } | ||
| }); | ||
|
|
||
| const average = (lastBlock.timestamp - firstBlock.timestamp) / blockRange; | ||
| blockTimes[chainId] = { timestamp: now, average, blockRange }; | ||
|
|
||
| return { average, blockRange }; | ||
| } | ||
|
|
||
| async function estimateBlocksElapsed(seconds: number, cushionPercentage = 0.0, provider: Provider): Promise<number> { | ||
| const cushionMultiplier = cushionPercentage + 1.0; | ||
| const { average } = await averageBlockTime(provider); | ||
| return Math.floor((seconds * cushionMultiplier) / average); | ||
| } | ||
|
|
||
| export class EVMBlockFinder extends BlockFinder<EVMBlock> { | ||
| constructor( | ||
| private readonly provider: Provider, | ||
| private readonly blocks: EVMBlock[] = [] | ||
| ) { | ||
| super(); | ||
| } | ||
|
|
||
| /** | ||
| * @notice Gets the latest block whose timestamp is <= the provided timestamp. | ||
| * @param number Timestamp timestamp to search. | ||
| * @param hints Optional low and high block to bound the search space. | ||
| */ | ||
| public async getBlockForTimestamp(timestamp: number | string, hints: BlockFinderHints = {}): Promise<EVMBlock> { | ||
| timestamp = Number(timestamp); | ||
| assert(timestamp !== undefined && timestamp !== null, "timestamp must be provided"); | ||
| // If the last block we have stored is too early, grab the latest block. | ||
| if (this.blocks.length === 0 || this.blocks[this.blocks.length - 1].timestamp < timestamp) { | ||
| const block = await this.getLatestBlock(); | ||
| if (timestamp >= block.timestamp) return block; | ||
| } | ||
|
|
||
| // Prime the BlockFinder cache with any supplied hints. | ||
| // If the hint is accurate, then this will bypass the subsequent estimation. | ||
| await Promise.all( | ||
| Object.values(hints) | ||
| .filter((blockNumber) => isDefined(blockNumber)) | ||
| .map((blockNumber) => this.getBlock(blockNumber)) | ||
| ); | ||
|
|
||
| // Check the first block. If it's greater than our timestamp, we need to find an earlier block. | ||
| if (this.blocks[0].timestamp > timestamp) { | ||
| const initialBlock = this.blocks[0]; | ||
| // We use a 2x cushion to reduce the number of iterations in the following loop and increase the chance | ||
| // that the first block we find sets a floor for the target timestamp. The loop converges on the correct block | ||
| // slower than the following incremental search performed by `findBlock`, so we want to minimize the number of | ||
| // loop iterations in favor of searching more blocks over the `findBlock` search. | ||
| const cushion = 1; | ||
| const incrementDistance = Math.max( | ||
| // Ensure the increment block distance is _at least_ a single block to prevent an infinite loop. | ||
| await estimateBlocksElapsed(initialBlock.timestamp - timestamp, cushion, this.provider), | ||
| 1 | ||
| ); | ||
|
|
||
| // Search backwards by a constant increment until we find a block before the timestamp or hit block 0. | ||
| for (let multiplier = 1; ; multiplier++) { | ||
| const distance = multiplier * incrementDistance; | ||
| const blockNumber = Math.max(0, initialBlock.number - distance); | ||
| const block = await this.getBlock(blockNumber); | ||
| if (block.timestamp <= timestamp) break; // Found an earlier block. | ||
| assert(blockNumber > 0, "timestamp is before block 0"); // Block 0 was not earlier than this timestamp. The row. | ||
| } | ||
| } | ||
|
|
||
| // Find the index where the block would be inserted and use that as the end block (since it is >= the timestamp). | ||
| const index = sortedIndexBy(this.blocks, { timestamp } as Block, "timestamp"); | ||
| return this.findBlock(this.blocks[index - 1], this.blocks[index], timestamp); | ||
| } | ||
|
|
||
| // Grabs the most recent block and caches it. | ||
| private async getLatestBlock() { | ||
| const block = await this.provider.getBlock("latest"); | ||
| const index = sortedIndexBy(this.blocks, block, "number"); | ||
| if (this.blocks[index]?.number !== block.number) this.blocks.splice(index, 0, block); | ||
| return this.blocks[index]; | ||
| } | ||
|
|
||
| // Grabs the block for a particular number and caches it. | ||
| private async getBlock(number: number) { | ||
| let index = sortedIndexBy(this.blocks, { number } as Block, "number"); | ||
| if (this.blocks[index]?.number === number) return this.blocks[index]; // Return early if block already exists. | ||
| const block = await this.provider.getBlock(number); | ||
|
|
||
| // Recompute the index after the async call since the state of this.blocks could have changed! | ||
| index = sortedIndexBy(this.blocks, { number } as Block, "number"); | ||
|
|
||
| // Rerun this check to avoid duplicate insertion. | ||
| if (this.blocks[index]?.number === number) return this.blocks[index]; | ||
| this.blocks.splice(index, 0, block); // A simple insert at index. | ||
| return block; | ||
| } | ||
|
|
||
| // Return the latest block, between startBlock and endBlock, whose timestamp is <= timestamp. | ||
| // Effectively, this is an interpolation search algorithm to minimize block requests. | ||
| // Note: startBlock and endBlock _must_ be different blocks. | ||
| private async findBlock(_startBlock: EVMBlock, _endBlock: EVMBlock, timestamp: number): Promise<EVMBlock> { | ||
| const [startBlock, endBlock] = [_startBlock, _endBlock]; | ||
| // In the case of equality, the endBlock is expected to be passed as the one whose timestamp === the requested | ||
| // timestamp. | ||
| if (endBlock.timestamp === timestamp) return endBlock; | ||
|
|
||
| // If there's no equality, but the blocks are adjacent, return the startBlock, since we want the returned block's | ||
| // timestamp to be <= the requested timestamp. | ||
| if (endBlock.number === startBlock.number + 1) return startBlock; | ||
|
|
||
| assert(endBlock.number !== startBlock.number, "startBlock cannot equal endBlock"); | ||
| assert( | ||
| timestamp < endBlock.timestamp && timestamp > startBlock.timestamp, | ||
| "timestamp not in between start and end blocks" | ||
| ); | ||
|
|
||
| // Interpolating the timestamp we're searching for to block numbers. | ||
| const totalTimeDifference = endBlock.timestamp - startBlock.timestamp; | ||
| const totalBlockDistance = endBlock.number - startBlock.number; | ||
| const blockPercentile = (timestamp - startBlock.timestamp) / totalTimeDifference; | ||
| const estimatedBlock = startBlock.number + Math.round(blockPercentile * totalBlockDistance); | ||
|
|
||
| // Clamp ensures the estimated block is strictly greater than the start block and strictly less than the end block. | ||
| const newBlock = await this.getBlock(clamp(estimatedBlock, startBlock.number + 1, endBlock.number - 1)); | ||
|
|
||
| // Depending on whether the new block is below or above the timestamp, narrow the search space accordingly. | ||
| if (newBlock.timestamp < timestamp) { | ||
| return this.findBlock(newBlock, endBlock, timestamp); | ||
| } else { | ||
| return this.findBlock(startBlock, newBlock, timestamp); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| export * from "./SpokeUtils"; | ||
| export * from "./BlockUtils"; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.