From d9d9f3e3c197b06177248ac4db128b20c5cb7bdc Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 22 Jul 2024 16:34:29 -0500 Subject: [PATCH 01/49] Match type to actual value --- .../src/contract/utils/createSimulateContractParameters.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts b/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts index 3ae91c95..47325417 100644 --- a/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts +++ b/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts @@ -40,6 +40,5 @@ type SimulateContractParameters = { value?: bigint; } & ( | { gasPrice?: bigint } - | { maxFeePerGas?: bigint } - | { maxPriorityFeePerGas?: bigint } + | { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint } ); From fa2b1f54cecf89624dac5f99c7c3bdac41f9bf09 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 15:57:50 -0500 Subject: [PATCH 02/49] New name, new README --- .github/CONTRIBUTING.md | 19 ++ .prettierrc.mjs => .prettierrc.js | 4 +- .vscode/settings.json | 1 + README.md | 499 ++++++++++++++++++++++++++++-- packages/evm-client/package.json | 5 +- 5 files changed, 499 insertions(+), 29 deletions(-) create mode 100644 .github/CONTRIBUTING.md rename .prettierrc.mjs => .prettierrc.js (87%) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..c051bd81 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,19 @@ +# Contributing + +## Creating a release + +This repo uses [changesets](https://github.com/changesets/changesets) to manage +versioning and changelogs. This means you shouldn't need to manually change any +of the internal package versions. + +Before opening a PR, run `yarn changeset` and follow the prompts to describe the +changes you've made. This will create a changeset file that should be committed. + +As changesets are committed to the `main` branch, the [changesets github +action](https://github.com/changesets/action) in the release workflow will +automatically keep track of the pending `package.json` and `CHANGELOG.md` +updates in an open PR titled `chore: version packages`. + +Once this PR is merged, the release workflow will be triggered, creating new +tags and github releases, and publishing the updated packages to NPM. **These +PRs should be carefully reviewed!** diff --git a/.prettierrc.mjs b/.prettierrc.js similarity index 87% rename from .prettierrc.mjs rename to .prettierrc.js index 6372900a..5a0f71e3 100644 --- a/.prettierrc.mjs +++ b/.prettierrc.js @@ -1,7 +1,7 @@ // A minimal config for extensions when in languages not supported by biome. // https://biomejs.dev/internals/language-support/ /** @type {import("prettier").Config} */ -const config = { +module.exports = { tabWidth: 2, useTabs: false, trailingComma: "all", @@ -9,5 +9,3 @@ const config = { semi: true, printWidth: 80, }; - -export default config; diff --git a/.vscode/settings.json b/.vscode/settings.json index f0f2ef75..1a24d91b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { + "prettier.configPath": ".prettierrc.js", "biome.lspBin": "./node_modules/.bin/biome", "[javascript]": { "editor.defaultFormatter": "biomejs.biome" diff --git a/README.md b/README.md index 380f438f..d7e192aa 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,484 @@ -# @delvtech/evm-client +# Drift -Useful EVM client abstractions for TypeScript projects that want to remain web3 -library agnostic. +**Write once, run anywhere:** Simplify Ethereum contract interactions with +built-in caching, type-safe APIs, and support for multiple web3 libraries β€” +without the headache of managing multiple hooks or redundant network requests. -## Packages +Drift is a TypeScript library that lets you write a single implementation for +interacting with Ethereum smart contracts, while seamlessly supporting multiple +web3 libraries like `ethers.js`, `viem`, and more. With built-in caching, +type-safe contract APIs, and easy test mocks, Drift helps you build efficient +and reliable applications without overthinking call optimizations or juggling +countless hooks. -- **[@delvtech/evm-client](./packages/evm-client):** Core abstractions, utils, - and stubs. -- **[@delvtech/evm-client-viem](./packages/evm-client-viem):** Bindings for - [Viem](https://viem.sh/). -- **[@delvtech/evm-client-ethers](./packages/evm-client-ethers):** Bindings for - [Ethers](https://ethers.org/). +## Why Drift? -## Creating a release +Building on Ethereum often means: -This repo uses [changesets](https://github.com/changesets/changesets) to manage -versioning and changelogs. This means you shouldn't need to manually change any -of the internal package versions. +- **Juggling Different Web3 Libraries:** Choosing between `ethers.js`, `viem`, + or others can feel like vendor lock-in, and rewrites are time-consuming. +- **Managing Multiple Hooks:** Each contract call often needs its own hook and + query key to prevent redundant network requests. +- **Optimizing Network Calls:** Manually caching calls and optimizing queries to + minimize RPC requests slows down development. +- **Complex Testing:** Setting up mocks for contract interactions can be + cumbersome and error-prone. -Before opening a PR, run `yarn changeset` and follow the prompts to describe the -changes you've made. This will create a changeset file that should be committed. +Drift abstracts away these complexities: -As changesets are committed to the `main` branch, the [changesets github -action](https://github.com/changesets/action) in the release workflow will -automatically keep track of the pending `package.json` and `CHANGELOG.md` -updates in an open PR titled `chore: version packages`. +- **Unified Interface:** Write your contract logic once and use it across + different web3 providers. +- **Built-in Caching:** Automatically reduce redundant RPC calls β€” no need to + manage hooks for each call. +- **Type-Safe APIs:** Benefit from TypeScript with type-checked contract + interactions. +- **Easy Testing:** Built-in mocks simplify testing your contract interactions. -Once this PR is merged, the release workflow will be triggered, creating new -tags and github releases, and publishing the updated packages to NPM. **These -PRs should be carefully reviewed!** +## Features + +- 🌐 **Multi-Library Support:** Compatible with `ethers.js`, `viem`, and soon + `web3.js`. +- ⚑ **Optimized Performance:** Built-in caching for fewer network calls + without manual management. +- πŸ”’ **Type Safety:** Catch errors at compile time with type-checked APIs. +- πŸ§ͺ **Testing Made Easy:** Use built-in mocks for reliable and straightforward + testing. +- πŸ”„ **Extensible:** Designed to grow with your project's needs. + +## Installation + +Install Drift and the adapter for your preferred web3 library: + +```bash +npm install @delvtech/drift +# For ethers.js +npm install @delvtech/drift-ethers +# For viem +npm install @delvtech/drift-viem +``` + +## Start Drifting + +### 1. Initialize Drift with your chosen adapter + +```typescript +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { createPublicClient, http } from "viem"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +// optionally, create a wallet client +const walletClient = createWalletClient({ + transport: http(), + // ...other options +}); + +const drift = new Drift(viemAdapter({ publicClient, walletClient })); +``` + +### 2. Interact with your Contracts + +#### Read Operations with Caching + +```typescript +import { VaultAbi } from "./abis/VaultAbi"; + +// No need to wrap in separate hooks; Drift handles caching internally +const balance = await drift.read({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "balanceOf", + args: { + account: "0xUserAddress", + }, +}); +``` + +#### Write Operations + +If Drift was initialized with a wallet client, you can perform write operations: + +```typescript +const txHash = await drift.write({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "deposit", + args: { + amount: BigInt(100e18), + receiver: "0xReceiverAddress", + }, + + // Optionally wait for the transaction to be mined and invalidate cache + onMined: () => { + drift.cache.invalidateRead({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "balanceOf", + args: { + account: "0xReceiverAddress", + }, + }); + }, +}); +``` + +#### Contract Instances + +Create contract instances to write your options once and get a streamlined, +type-safe API to re-use across your application. + +```typescript +const vault = drift.contract({ + abi: VaultAbi, + address: "0xYourVaultAddress", + // ...other options +}); + +const balance = await vault.read("balanceOf", { account }); + +const txHash = await vault.write( + "deposit", + { + amount: BigInt(100e18), + receiver: "0xReceiverAddress", + }, + { + onMined: () => { + vault.invalidateRead("balanceOf", { account: "0xReceiverAddress" }); + }, + }, +); +``` + +Drift optimizes your contract interactions behind the scenes, so you don't have +to sacrifice code readability for performance or manage hooks and query keys +manually. + +## Example: Building Vault Clients + +Let's build a simple library agnostic SDK with `ReadVault` and `ReadWriteVault` +clients using Drift. + +### 1. Define core vault clients + +In your core SDK package, define the `ReadVault` and `ReadWriteVault` clients +using Drift's `ReadContract` and `ReadWriteContract` abstractions. + +```typescript +// sdk-core/src/VaultClient.ts +import { + ContractEvent, + Drift, + ReadContract, + ReadWriteAdapter, + ReadWriteContract, +} from "@delvtech/drift"; +import { vaultAbi } from "../abis/VaultAbi"; + +type VaultAbi = typeof vaultAbi; + +export class ReadVault { + contract: ReadContract; + + constructor(address: string, drift: Drift) { + this.contract = drift.contract({ + abi: vaultAbi, + address, + }); + } + + // Read balance with internal caching + async getBalance(account: string): Promise { + return this.contract.read("balanceOf", { account }); + } + + // Get all deposit events for an account with internal caching + async getDeposits( + account?: string, + ): Promise[]> { + return this.contract.getEvents("Deposit", { + filter: { + depositor: account, + recipient: account, + }, + }); + } +} + +export class ReadWriteVault extends ReadVault { + declare contract: ReadWriteContract; + + constructor(address: string, drift: Drift) { + super(wallet, drift); + } + + // Make a deposit + async deposit(amount: bigint, recipient: string): Promise { + const txHash = await this.contract.write( + "deposit", + { amount, recipient }, + { + // Optionally wait for the transaction to be mined and invalidate cache + onMined: () => { + this.contract.invalidateRead("balanceOf", { recipient }); + }, + }, + ); + + return txHash; + } +} +``` + +### 2. Use the clients in your application + +Using an adapter, you can integrate Drift with your chosen web3 library. Here's +an example using `viem`: + +```typescript +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { createPublicClient, http } from "viem"; +import { ReadVault } from "sdk-core"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +const drift = new Drift(viemAdapter({ publicClient })); + +// Instantiate the ReadVault client +const readVault = new ReadVault("0xYourVaultAddress", drift); + +// Fetch user balance +const userBalance = await readVault.getBalance("0xUserAddress"); + +// Get deposit history +const deposits = await readVault.getDeposits("0xUserAddress"); +``` + +#### Benefits of This Architecture + +- **Modularity:** Your core logic remains untouched when switching web3 + libraries. +- **Reusability:** Write your business logic once and reuse it across different + environments. +- **Flexibility:** Easily extend support to new web3 libraries by creating small + adapter packages. +- **Simplicity:** Your application code stays clean and focused on business + logic rather than on handling different web3 providers. + +### 3. Extend core clients for library-specific clients + +To provide library specific client packages, e.g., `sdk-viem`, extend the core +clients and overwrite their constructors to accept `viem` clients. + +```typescript +// sdk-viem/src/VaultClient.ts +import { + ReadVault as CoreReadVault, + ReadWriteVault as CoreReadWriteVault, +} from "sdk-core"; +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { PublicClient, WalletClient } from "viem"; + +export class ReadVault extends CoreReadVault { + constructor(address: string, publicClient: PublicClient) { + const drift = new Drift(viemAdapter({ publicClient })); + super(address, drift); + } +} + +export class ReadWriteVault extends CoreReadWriteVault { + constructor( + address: string, + publicClient: PublicClient, + walletClient: WalletClient, + ) { + const drift = new Drift(viemAdapter({ publicClient, walletClient })); + super(address, drift); + } +} +``` + +### 4. Test Your Clients with Drift's Built-in Mocks + +Testing smart contract interactions can be complex and time-consuming. Drift +simplifies this process by providing built-in mocks that allow you to stub +responses and focus on testing your application logic. + +#### Example: Testing Contract Interactions with Mocks + +Suppose you have a method `getShortAccruedYield` in your `ReadVault` client that +calculates the accrued yield for a mature position. You want to test this method +without making actual RPC calls. + +Here's how you can use Drift's mocks to stub contract calls and test your +method: + +```typescript +// test/ReadVault.test.ts +import { MockDrift } from "@delvtech/drift/testing"; +import { parseBigInt } from "parse-bigint"; +import { ReadVault } from "sdk-core"; +import { vaultAbi } from "../abis/VaultAbi"; + +test("getShortAccruedYield should return the amount of yield a mature position has earned", async () => { + // Set up mocks + const mockDrift = new MockDrift(); + const mockContract = drift.contract({ + abi: vaultAbi, + address: "0xVaultAddress", + }); + + // Stub the getBlock method + mockDrift.onGetBlock().returns({ number: 1n, timestamp: 1699503565n }); + + // Stub contract reads for getPoolConfig + mockContract.onRead("getPoolConfig").returns({ + positionDuration: 86400n, // one day in seconds + checkpointDuration: 86400n, // one day in seconds + // ...other config values + }); + + // Stub the checkpoint when the short was opened + mockContract.onRead("getCheckpoint", { _checkpointTime: 1n }).returns({ + vaultSharePrice: parseBigInt("1.008e18"), + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + + // Stub the checkpoint when the short matured + mockContract.onRead("getCheckpoint", { _checkpointTime: 86401n }).returns({ + vaultSharePrice: parseBigInt("1.01e18"), + weightedSpotPrice: 0n, + lastWeightedSpotPriceUpdateTime: 0n, + }); + + // Instantiate your client with the mocked Drift instance + const readVault = new ReadVault("0xYourVaultAddress", mockDrift); + + // Call the method you want to test + const accruedYield = await readVault.getShortAccruedYield({ + checkpointTime: 1n, + bondAmount: parseBigInt("100e18"), + }); + + // Assert the expected result + // If you opened a short position on 100 bonds at a previous checkpoint price + // of 1.008 and the price was 1.01 at maturity, your accrued profit would be 0.20. + expect(accruedYield).toEqual(parseBigInt("0.20e18")); +}); +``` + +#### Benefits + +- **No Network Calls:** Tests run faster and more reliably without actual + network interactions. +- **Focus on Logic:** Concentrate on testing your application's business logic. +- **Easy Setup:** Minimal configuration required to get started with testing. + +## Simplifying React Hook Management + +### The Problem Without Drift + +In traditional setups, you might rely on data-fetching libraries like React +Query. However, to prevent redundant network requests, each contract call would +need: + +- Its own hook (e.g., `useBalanceOf`, `useTokenSymbol`). +- Unique query keys for caching. + +Composing multiple calls becomes cumbersome, as you have to manage each hook's +result separately. + +### How Drift Helps + +Drift's internal caching means you don't need to wrap every contract call in a +separate hook. You can perform multiple contract interactions within a single +function or hook without worrying about redundant requests. + +#### Example Using React Query + +```typescript +import { useQuery } from "@tanstack/react-query"; +import { ReadVault } from "sdk-core"; + +function useVaultData(readVault: ReadVault, userAddress: string) { + return useQuery(["vaultData", userAddress], async () => { + // Perform multiple reads without separate hooks or query keys + const [balance, symbol, deposits] = await Promise.all([ + readVault.getBalance(userAddress), + readVault.contract.read("symbol"), + readVault.getDeposits(userAddress), + ]); + + return { balance, symbol, deposits }; + }); +} +``` + +No need to manage multiple hooks or query keys β€” Drift handles caching +internally, simplifying your code and development process. + +## Caching in Action + +Drift's caching mechanism ensures that repeated calls with the same parameters +don't result in unnecessary network requests, even when composed within the same +function. + +```typescript +// Both calls use the cache; only one network request is made +const balance1 = await contract.read("balanceOf", { account }); +const balance2 = await contract.read("balanceOf", { account }); +``` + +You can also manually invalidate the cache if needed: + +```typescript +// Invalidate the cache for a specific read +contract.invalidateRead("balanceOf", { account }); + +// Invalidate all reads matching partial arguments +contract.invalidateReadsMatching("balanceOf"); +``` + +## Advanced Usage + +### Custom Cache Implementation + +If you have specific caching needs, you can provide your own cache +implementation: + +```typescript +import { LRUCache } from "lru-cache"; + +const customCache = new LRUCache({ max: 500 }); +const drift = new Drift(viemAdapter(publicClient, { cache: customCache })); +``` + +### Extending Drift for Your Needs + +Drift is designed to be extensible. You can build additional abstractions or +utilities on top of it to suit your project's requirements. + +## Contributing + +Got ideas or found a bug? Check the [Contributing +Guide](./.github/CONTRIBUTING.md) to get started. + +## License + +Drift is open-source software licensed under the [TODO License](LICENSE). + +--- + +Build smarter, not harder β€” let Drift handle caching and call optimization so +you can focus on what matters, without the hassle of managing multiple hooks or +redundant network requests. diff --git a/packages/evm-client/package.json b/packages/evm-client/package.json index 5ab65ef5..246af59c 100644 --- a/packages/evm-client/package.json +++ b/packages/evm-client/package.json @@ -1,6 +1,6 @@ { - "name": "@delvtech/evm-client", - "version": "0.5.1", + "name": "@delvtech/drift", + "version": "0.0.0", "license": "MIT", "type": "module", "exports": { @@ -67,7 +67,6 @@ "dotenv": "^16.4.2", "fast-json-stable-stringify": "^2.1.0", "sinon": "^17.0.1", - "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "tsup": "^8.0.2", "typescript": "^5.4.5", From 5a05f1e6e6282adf5e9706ff32fb2bc47a4fbf7b Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 15:59:54 -0500 Subject: [PATCH 03/49] Reorder README --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d7e192aa..305fe370 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,17 @@ type-safe contract APIs, and easy test mocks, Drift helps you build efficient and reliable applications without overthinking call optimizations or juggling countless hooks. +## Features + +- 🌐 **Multi-Library Support:** Compatible with `ethers.js`, `viem`, and soon + `web3.js`. +- ⚑ **Optimized Performance:** Built-in caching for fewer network calls + without manual management. +- πŸ”’ **Type Safety:** Catch errors at compile time with type-checked APIs. +- πŸ§ͺ **Testing Made Easy:** Use built-in mocks for reliable and straightforward + testing. +- πŸ”„ **Extensible:** Designed to grow with your project's needs. + ## Why Drift? Building on Ethereum often means: @@ -34,17 +45,6 @@ Drift abstracts away these complexities: interactions. - **Easy Testing:** Built-in mocks simplify testing your contract interactions. -## Features - -- 🌐 **Multi-Library Support:** Compatible with `ethers.js`, `viem`, and soon - `web3.js`. -- ⚑ **Optimized Performance:** Built-in caching for fewer network calls - without manual management. -- πŸ”’ **Type Safety:** Catch errors at compile time with type-checked APIs. -- πŸ§ͺ **Testing Made Easy:** Use built-in mocks for reliable and straightforward - testing. -- πŸ”„ **Extensible:** Designed to grow with your project's needs. - ## Installation Install Drift and the adapter for your preferred web3 library: From 7fc9d061db0d8bed00792d47354ac2477854266b Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 16:06:36 -0500 Subject: [PATCH 04/49] Reorder README --- README.md | 48 ++++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 305fe370..c57f7903 100644 --- a/README.md +++ b/README.md @@ -22,29 +22,6 @@ countless hooks. testing. - πŸ”„ **Extensible:** Designed to grow with your project's needs. -## Why Drift? - -Building on Ethereum often means: - -- **Juggling Different Web3 Libraries:** Choosing between `ethers.js`, `viem`, - or others can feel like vendor lock-in, and rewrites are time-consuming. -- **Managing Multiple Hooks:** Each contract call often needs its own hook and - query key to prevent redundant network requests. -- **Optimizing Network Calls:** Manually caching calls and optimizing queries to - minimize RPC requests slows down development. -- **Complex Testing:** Setting up mocks for contract interactions can be - cumbersome and error-prone. - -Drift abstracts away these complexities: - -- **Unified Interface:** Write your contract logic once and use it across - different web3 providers. -- **Built-in Caching:** Automatically reduce redundant RPC calls β€” no need to - manage hooks for each call. -- **Type-Safe APIs:** Benefit from TypeScript with type-checked contract - interactions. -- **Easy Testing:** Built-in mocks simplify testing your contract interactions. - ## Installation Install Drift and the adapter for your preferred web3 library: @@ -154,9 +131,28 @@ const txHash = await vault.write( ); ``` -Drift optimizes your contract interactions behind the scenes, so you don't have -to sacrifice code readability for performance or manage hooks and query keys -manually. +## Why Drift? + +Building on Ethereum often means: + +- **Juggling Different Web3 Libraries:** Choosing between `ethers.js`, `viem`, + or others can feel like vendor lock-in, and rewrites are time-consuming. +- **Managing Multiple Hooks:** Each contract call often needs its own hook and + query key to prevent redundant network requests. +- **Optimizing Network Calls:** Manually caching calls and optimizing queries to + minimize RPC requests slows down development. +- **Complex Testing:** Setting up mocks for contract interactions can be + cumbersome and error-prone. + +Drift abstracts away these complexities: + +- **Unified Interface:** Write your contract logic once and use it across + different web3 providers. +- **Built-in Caching:** Automatically reduce redundant RPC calls β€” no need to + manage hooks for each call. +- **Type-Safe APIs:** Benefit from TypeScript with type-checked contract + interactions. +- **Easy Testing:** Built-in mocks simplify testing your contract interactions. ## Example: Building Vault Clients From 557248f45ffaac4ab4115798b26fbc7d1c5fb187 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 16:14:29 -0500 Subject: [PATCH 05/49] Remove redundancy --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index c57f7903..5eb030d5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Drift -**Write once, run anywhere:** Simplify Ethereum contract interactions with -built-in caching, type-safe APIs, and support for multiple web3 libraries β€” -without the headache of managing multiple hooks or redundant network requests. - -Drift is a TypeScript library that lets you write a single implementation for +**Write once, run anywhere:** Drift lets you write a single implementation for interacting with Ethereum smart contracts, while seamlessly supporting multiple web3 libraries like `ethers.js`, `viem`, and more. With built-in caching, type-safe contract APIs, and easy test mocks, Drift helps you build efficient From 85315fe002a49efafdc7166aca2e53f26ec966fa Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 16:18:30 -0500 Subject: [PATCH 06/49] Edit intro --- README.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5eb030d5..25f5faac 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,22 @@ # Drift -**Write once, run anywhere:** Drift lets you write a single implementation for -interacting with Ethereum smart contracts, while seamlessly supporting multiple -web3 libraries like `ethers.js`, `viem`, and more. With built-in caching, -type-safe contract APIs, and easy test mocks, Drift helps you build efficient -and reliable applications without overthinking call optimizations or juggling -countless hooks. +**Effortless Ethereum Development Across Web3 Libraries** + +Write Ethereum smart contract interactions once with Drift and run them +anywhere. Seamlessly support multiple web3 libraries like ethers.js, viem, and +moreβ€”without getting locked into a single provider or rewriting code. + +With built-in caching, type-safe contract APIs, and easy-to-use testing mocks, +Drift lets you build efficient and reliable applications without worrying about +call optimizations or juggling countless hooks. Focus on what matters: creating +great features and user experiences. ## Features - 🌐 **Multi-Library Support:** Compatible with `ethers.js`, `viem`, and soon `web3.js`. -- ⚑ **Optimized Performance:** Built-in caching for fewer network calls - without manual management. +- ⚑ **Optimized Performance:** Built-in caching for fewer network calls without + manual management. - πŸ”’ **Type Safety:** Catch errors at compile time with type-checked APIs. - πŸ§ͺ **Testing Made Easy:** Use built-in mocks for reliable and straightforward testing. From 80cd8453ad249461138ee27eed75d2e2f8c3821e Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 19:08:18 -0500 Subject: [PATCH 07/49] Remove ending --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 25f5faac..efebfdc7 100644 --- a/README.md +++ b/README.md @@ -472,9 +472,3 @@ Guide](./.github/CONTRIBUTING.md) to get started. ## License Drift is open-source software licensed under the [TODO License](LICENSE). - ---- - -Build smarter, not harder β€” let Drift handle caching and call optimization so -you can focus on what matters, without the hassle of managing multiple hooks or -redundant network requests. From 5d3ab8269cbf8041721e17dc16fa34330042060d Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 22:00:49 -0500 Subject: [PATCH 08/49] Incorporate feedback --- README.md | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index efebfdc7..43058a27 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,22 @@ Drift lets you build efficient and reliable applications without worrying about call optimizations or juggling countless hooks. Focus on what matters: creating great features and user experiences. -## Features +## Why Drift? + +Building on Ethereum often means dealing with: + +- **Hard Dependency on a Specific Web3 Library:** There are several competing options, like ethers.js, viem, or web3.js. Tying your business logic to a specific one creates vendor lock-in and makes it harder to switch down the road. +- **Managing Multiple Hooks:** Each contract call often needs its own hook and query key to prevent redundant network requests. +- **Optimizing Network Calls:** Manually caching calls and optimizing queries to minimize RPC requests slows down development. +- **Complex Testing:** Setting up mocks for contract interactions can be cumbersome and error-prone. + +## Drift Solves These Problems -- 🌐 **Multi-Library Support:** Compatible with `ethers.js`, `viem`, and soon - `web3.js`. -- ⚑ **Optimized Performance:** Built-in caching for fewer network calls without - manual management. -- πŸ”’ **Type Safety:** Catch errors at compile time with type-checked APIs. -- πŸ§ͺ **Testing Made Easy:** Use built-in mocks for reliable and straightforward - testing. -- πŸ”„ **Extensible:** Designed to grow with your project's needs. +- 🌐 **Multi-Library Support:** Drift provides a unified interface compatible with multiple web3 libraries. Write your contract logic once and use it across different providers. +- ⚑ **Optimized Performance:** Automatically reduces redundant RPC calls with built-in caching. No need to manage hooks or query keys for each call. +- πŸ”’ **Type Safety:** Drift's type-checked APIs help catch errors at compile time. If your ABI changes, your mocks will reflect that at compile time, keeping your tests in sync. +- πŸ§ͺ **Testing Made Easy:** Built-in mocks simplify testing your contract interactions. Drift's testing mocks are also type-safe, ensuring your tests are always in sync with your contracts. +- πŸ”„ **Extensibility:** Designed to grow with your project's needs, Drift allows you to easily extend support to new web3 libraries by creating small adapter packages. ## Installation @@ -131,29 +137,6 @@ const txHash = await vault.write( ); ``` -## Why Drift? - -Building on Ethereum often means: - -- **Juggling Different Web3 Libraries:** Choosing between `ethers.js`, `viem`, - or others can feel like vendor lock-in, and rewrites are time-consuming. -- **Managing Multiple Hooks:** Each contract call often needs its own hook and - query key to prevent redundant network requests. -- **Optimizing Network Calls:** Manually caching calls and optimizing queries to - minimize RPC requests slows down development. -- **Complex Testing:** Setting up mocks for contract interactions can be - cumbersome and error-prone. - -Drift abstracts away these complexities: - -- **Unified Interface:** Write your contract logic once and use it across - different web3 providers. -- **Built-in Caching:** Automatically reduce redundant RPC calls β€” no need to - manage hooks for each call. -- **Type-Safe APIs:** Benefit from TypeScript with type-checked contract - interactions. -- **Easy Testing:** Built-in mocks simplify testing your contract interactions. - ## Example: Building Vault Clients Let's build a simple library agnostic SDK with `ReadVault` and `ReadWriteVault` From 59d98acb7ef07ca558c668af55ef8265d0472855 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 22:16:59 -0500 Subject: [PATCH 09/49] Format README, edit cache example --- README.md | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 43058a27..20ca6b25 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,33 @@ great features and user experiences. Building on Ethereum often means dealing with: -- **Hard Dependency on a Specific Web3 Library:** There are several competing options, like ethers.js, viem, or web3.js. Tying your business logic to a specific one creates vendor lock-in and makes it harder to switch down the road. -- **Managing Multiple Hooks:** Each contract call often needs its own hook and query key to prevent redundant network requests. -- **Optimizing Network Calls:** Manually caching calls and optimizing queries to minimize RPC requests slows down development. -- **Complex Testing:** Setting up mocks for contract interactions can be cumbersome and error-prone. +- **Hard Dependency on a Specific Web3 Library:** There are several competing + options, like ethers.js, viem, or web3.js. Tying your business logic to a + specific one creates vendor lock-in and makes it harder to switch down the + road. +- **Managing Multiple Hooks:** Each contract call often needs its own hook and + query key to prevent redundant network requests. +- **Optimizing Network Calls:** Manually caching calls and optimizing queries to + minimize RPC requests slows down development. +- **Complex Testing:** Setting up mocks for contract interactions can be + cumbersome and error-prone. ## Drift Solves These Problems -- 🌐 **Multi-Library Support:** Drift provides a unified interface compatible with multiple web3 libraries. Write your contract logic once and use it across different providers. -- ⚑ **Optimized Performance:** Automatically reduces redundant RPC calls with built-in caching. No need to manage hooks or query keys for each call. -- πŸ”’ **Type Safety:** Drift's type-checked APIs help catch errors at compile time. If your ABI changes, your mocks will reflect that at compile time, keeping your tests in sync. -- πŸ§ͺ **Testing Made Easy:** Built-in mocks simplify testing your contract interactions. Drift's testing mocks are also type-safe, ensuring your tests are always in sync with your contracts. -- πŸ”„ **Extensibility:** Designed to grow with your project's needs, Drift allows you to easily extend support to new web3 libraries by creating small adapter packages. +- 🌐 **Multi-Library Support:** Drift provides a unified interface compatible + with multiple web3 libraries. Write your contract logic once and use it across + different providers. +- ⚑ **Optimized Performance:** Automatically reduces redundant RPC calls with + built-in caching. No need to manage hooks or query keys for each call. +- πŸ”’ **Type Safety:** Drift's type-checked APIs help catch errors at compile + time. If your ABI changes, your mocks will reflect that at compile time, + keeping your tests in sync. +- πŸ§ͺ **Testing Made Easy:** Built-in mocks simplify testing your contract + interactions. Drift's testing mocks are also type-safe, ensuring your tests + are always in sync with your contracts. +- πŸ”„ **Extensibility:** Designed to grow with your project's needs, Drift allows + you to easily extend support to new web3 libraries by creating small adapter + packages. ## Installation @@ -439,7 +454,7 @@ implementation: import { LRUCache } from "lru-cache"; const customCache = new LRUCache({ max: 500 }); -const drift = new Drift(viemAdapter(publicClient, { cache: customCache })); +const drift = new Drift(viemAdapter({ publicClient }), { cache: customCache }); ``` ### Extending Drift for Your Needs From 0f7a0b82ed27ecc76b16cca6c68c6da50eb36899 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 28 Sep 2024 22:36:43 -0500 Subject: [PATCH 10/49] Rename repos to drift --- package.json | 2 +- .../.gitignore | 0 .../CHANGELOG.md | 0 .../README.md | 0 .../integration-tests/artifacts/CoreVoting.ts | 0 .../createReadContract.test.ts | 0 .../package.json | 6 +- .../src/contract/createCachedReadContract.ts | 2 +- .../contract/createCachedReadWriteContract.ts | 2 +- .../src/contract/createReadContract.ts | 10 ++- .../src/contract/createReadWriteContract.ts | 11 ++- .../src/index.ts | 10 +-- .../src/network/createNetwork.ts | 2 +- packages/drift-ethers/src/stubs.ts | 1 + .../tsconfig.json | 0 .../tsup.config.ts | 0 .../vite.config.ts | 0 .../.gitignore | 0 .../CHANGELOG.md | 0 .../{evm-client-viem => drift-viem}/README.md | 0 .../integration-tests/artifacts/CoreVoting.ts | 0 .../createReadContract.test.ts | 0 .../package.json | 6 +- .../src/contract/createCachedReadContract.ts | 2 +- .../contract/createCachedReadWriteContract.ts | 2 +- .../src/contract/createReadContract.ts | 2 +- .../src/contract/createReadWriteContract.ts | 6 +- .../utils/createSimulateContractParameters.ts | 2 +- .../src/contract/utils/outputToFriendly.ts | 2 +- .../src/index.ts | 10 +-- .../src/network/createNetwork.ts | 2 +- packages/drift-viem/src/stubs.ts | 1 + .../tsconfig.json | 0 .../tsup.config.ts | 0 .../vite.config.ts | 0 packages/{evm-client => drift}/.gitignore | 0 packages/{evm-client => drift}/CHANGELOG.md | 0 packages/{evm-client => drift}/README.md | 0 packages/{evm-client => drift}/package.json | 0 .../src/base/testing/IERC20.ts | 0 .../src/base/testing/accounts.ts | 0 .../{evm-client => drift}/src/base/types.ts | 0 .../cache/factories/createLruSimpleCache.ts | 0 .../src/cache/types/SimpleCache.ts | 0 .../src/cache/utils/createSimpleCacheKey.ts | 0 .../createCachedReadContract.test.ts | 0 .../factories/createCachedReadContract.ts | 0 .../createCachedReadWriteContract.ts | 0 .../contract/stubs/ReadContractStub.test.ts | 0 .../src/contract/stubs/ReadContractStub.ts | 0 .../stubs/ReadWriteContractStub.test.ts | 0 .../contract/stubs/ReadWriteContractStub.ts | 0 .../src/contract/types/AbiEntry.ts | 0 .../src/contract/types/CachedContract.ts | 0 .../src/contract/types/Contract.ts | 0 .../src/contract/types/Event.ts | 0 .../src/contract/types/Function.ts | 5 +- .../contract/utils/arrayToFriendly.test.ts | 0 .../src/contract/utils/arrayToFriendly.ts | 0 .../src/contract/utils/arrayToObject.test.ts | 0 .../src/contract/utils/arrayToObject.ts | 0 .../src/contract/utils/getAbiEntry.ts | 0 .../src/contract/utils/objectToArray.test.ts | 0 .../src/contract/utils/objectToArray.ts | 0 .../src/errors/AbiEntryNotFound.ts | 0 .../src/exports/cache.ts | 0 .../src/exports/contract.ts | 0 .../src/exports/errors.ts | 0 .../src/exports/index.ts | 0 .../src/exports/network.ts | 0 .../src/exports/stubs.ts | 0 .../src/network/stubs/NetworkStub.test.ts | 0 .../src/network/stubs/NetworkStub.ts | 5 +- .../src/network/types/Block.ts | 0 .../src/network/types/Network.ts | 5 +- .../src/network/types/Transaction.ts | 0 packages/{evm-client => drift}/tsconfig.json | 0 packages/{evm-client => drift}/tsup.config.ts | 0 packages/{evm-client => drift}/vite.config.ts | 0 packages/evm-client-ethers/src/stubs.ts | 1 - packages/evm-client-viem/src/stubs.ts | 1 - yarn.lock | 90 +------------------ 82 files changed, 62 insertions(+), 126 deletions(-) rename packages/{evm-client-ethers => drift-ethers}/.gitignore (100%) rename packages/{evm-client-ethers => drift-ethers}/CHANGELOG.md (100%) rename packages/{evm-client-ethers => drift-ethers}/README.md (100%) rename packages/{evm-client-ethers => drift-ethers}/integration-tests/artifacts/CoreVoting.ts (100%) rename packages/{evm-client-ethers => drift-ethers}/integration-tests/createReadContract.test.ts (100%) rename packages/{evm-client-ethers => drift-ethers}/package.json (90%) rename packages/{evm-client-ethers => drift-ethers}/src/contract/createCachedReadContract.ts (95%) rename packages/{evm-client-ethers => drift-ethers}/src/contract/createCachedReadWriteContract.ts (96%) rename packages/{evm-client-ethers => drift-ethers}/src/contract/createReadContract.ts (97%) rename packages/{evm-client-ethers => drift-ethers}/src/contract/createReadWriteContract.ts (92%) rename packages/{evm-client-ethers => drift-ethers}/src/index.ts (85%) rename packages/{evm-client-ethers => drift-ethers}/src/network/createNetwork.ts (98%) create mode 100644 packages/drift-ethers/src/stubs.ts rename packages/{evm-client-ethers => drift-ethers}/tsconfig.json (100%) rename packages/{evm-client-ethers => drift-ethers}/tsup.config.ts (100%) rename packages/{evm-client-ethers => drift-ethers}/vite.config.ts (100%) rename packages/{evm-client-viem => drift-viem}/.gitignore (100%) rename packages/{evm-client-viem => drift-viem}/CHANGELOG.md (100%) rename packages/{evm-client-viem => drift-viem}/README.md (100%) rename packages/{evm-client-viem => drift-viem}/integration-tests/artifacts/CoreVoting.ts (100%) rename packages/{evm-client-viem => drift-viem}/integration-tests/createReadContract.test.ts (100%) rename packages/{evm-client-viem => drift-viem}/package.json (92%) rename packages/{evm-client-viem => drift-viem}/src/contract/createCachedReadContract.ts (96%) rename packages/{evm-client-viem => drift-viem}/src/contract/createCachedReadWriteContract.ts (96%) rename packages/{evm-client-viem => drift-viem}/src/contract/createReadContract.ts (99%) rename packages/{evm-client-viem => drift-viem}/src/contract/createReadWriteContract.ts (98%) rename packages/{evm-client-viem => drift-viem}/src/contract/utils/createSimulateContractParameters.ts (93%) rename packages/{evm-client-viem => drift-viem}/src/contract/utils/outputToFriendly.ts (96%) rename packages/{evm-client-viem => drift-viem}/src/index.ts (85%) rename packages/{evm-client-viem => drift-viem}/src/network/createNetwork.ts (97%) create mode 100644 packages/drift-viem/src/stubs.ts rename packages/{evm-client-viem => drift-viem}/tsconfig.json (100%) rename packages/{evm-client-viem => drift-viem}/tsup.config.ts (100%) rename packages/{evm-client-viem => drift-viem}/vite.config.ts (100%) rename packages/{evm-client => drift}/.gitignore (100%) rename packages/{evm-client => drift}/CHANGELOG.md (100%) rename packages/{evm-client => drift}/README.md (100%) rename packages/{evm-client => drift}/package.json (100%) rename packages/{evm-client => drift}/src/base/testing/IERC20.ts (100%) rename packages/{evm-client => drift}/src/base/testing/accounts.ts (100%) rename packages/{evm-client => drift}/src/base/types.ts (100%) rename packages/{evm-client => drift}/src/cache/factories/createLruSimpleCache.ts (100%) rename packages/{evm-client => drift}/src/cache/types/SimpleCache.ts (100%) rename packages/{evm-client => drift}/src/cache/utils/createSimpleCacheKey.ts (100%) rename packages/{evm-client => drift}/src/contract/factories/createCachedReadContract.test.ts (100%) rename packages/{evm-client => drift}/src/contract/factories/createCachedReadContract.ts (100%) rename packages/{evm-client => drift}/src/contract/factories/createCachedReadWriteContract.ts (100%) rename packages/{evm-client => drift}/src/contract/stubs/ReadContractStub.test.ts (100%) rename packages/{evm-client => drift}/src/contract/stubs/ReadContractStub.ts (100%) rename packages/{evm-client => drift}/src/contract/stubs/ReadWriteContractStub.test.ts (100%) rename packages/{evm-client => drift}/src/contract/stubs/ReadWriteContractStub.ts (100%) rename packages/{evm-client => drift}/src/contract/types/AbiEntry.ts (100%) rename packages/{evm-client => drift}/src/contract/types/CachedContract.ts (100%) rename packages/{evm-client => drift}/src/contract/types/Contract.ts (100%) rename packages/{evm-client => drift}/src/contract/types/Event.ts (100%) rename packages/{evm-client => drift}/src/contract/types/Function.ts (94%) rename packages/{evm-client => drift}/src/contract/utils/arrayToFriendly.test.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/arrayToFriendly.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/arrayToObject.test.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/arrayToObject.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/getAbiEntry.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/objectToArray.test.ts (100%) rename packages/{evm-client => drift}/src/contract/utils/objectToArray.ts (100%) rename packages/{evm-client => drift}/src/errors/AbiEntryNotFound.ts (100%) rename packages/{evm-client => drift}/src/exports/cache.ts (100%) rename packages/{evm-client => drift}/src/exports/contract.ts (100%) rename packages/{evm-client => drift}/src/exports/errors.ts (100%) rename packages/{evm-client => drift}/src/exports/index.ts (100%) rename packages/{evm-client => drift}/src/exports/network.ts (100%) rename packages/{evm-client => drift}/src/exports/stubs.ts (100%) rename packages/{evm-client => drift}/src/network/stubs/NetworkStub.test.ts (100%) rename packages/{evm-client => drift}/src/network/stubs/NetworkStub.ts (98%) rename packages/{evm-client => drift}/src/network/types/Block.ts (100%) rename packages/{evm-client => drift}/src/network/types/Network.ts (95%) rename packages/{evm-client => drift}/src/network/types/Transaction.ts (100%) rename packages/{evm-client => drift}/tsconfig.json (100%) rename packages/{evm-client => drift}/tsup.config.ts (100%) rename packages/{evm-client => drift}/vite.config.ts (100%) delete mode 100644 packages/evm-client-ethers/src/stubs.ts delete mode 100644 packages/evm-client-viem/src/stubs.ts diff --git a/package.json b/package.json index a697d788..aa56e6f0 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "evm-client", + "name": "drift", "private": true, "scripts": { "build:packages": "turbo build --filter=./packages/*", diff --git a/packages/evm-client-ethers/.gitignore b/packages/drift-ethers/.gitignore similarity index 100% rename from packages/evm-client-ethers/.gitignore rename to packages/drift-ethers/.gitignore diff --git a/packages/evm-client-ethers/CHANGELOG.md b/packages/drift-ethers/CHANGELOG.md similarity index 100% rename from packages/evm-client-ethers/CHANGELOG.md rename to packages/drift-ethers/CHANGELOG.md diff --git a/packages/evm-client-ethers/README.md b/packages/drift-ethers/README.md similarity index 100% rename from packages/evm-client-ethers/README.md rename to packages/drift-ethers/README.md diff --git a/packages/evm-client-ethers/integration-tests/artifacts/CoreVoting.ts b/packages/drift-ethers/integration-tests/artifacts/CoreVoting.ts similarity index 100% rename from packages/evm-client-ethers/integration-tests/artifacts/CoreVoting.ts rename to packages/drift-ethers/integration-tests/artifacts/CoreVoting.ts diff --git a/packages/evm-client-ethers/integration-tests/createReadContract.test.ts b/packages/drift-ethers/integration-tests/createReadContract.test.ts similarity index 100% rename from packages/evm-client-ethers/integration-tests/createReadContract.test.ts rename to packages/drift-ethers/integration-tests/createReadContract.test.ts diff --git a/packages/evm-client-ethers/package.json b/packages/drift-ethers/package.json similarity index 90% rename from packages/evm-client-ethers/package.json rename to packages/drift-ethers/package.json index 6da7e0dc..b1633565 100644 --- a/packages/evm-client-ethers/package.json +++ b/packages/drift-ethers/package.json @@ -1,6 +1,6 @@ { - "name": "@delvtech/evm-client-ethers", - "version": "0.5.1", + "name": "@delvtech/drift-ethers", + "version": "0.0.0", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -33,7 +33,7 @@ "ethers": "^6" }, "dependencies": { - "@delvtech/evm-client": "0.5.1" + "@delvtech/drift": "0.0.0" }, "devDependencies": { "@repo/typescript-config": "*", diff --git a/packages/evm-client-ethers/src/contract/createCachedReadContract.ts b/packages/drift-ethers/src/contract/createCachedReadContract.ts similarity index 95% rename from packages/evm-client-ethers/src/contract/createCachedReadContract.ts rename to packages/drift-ethers/src/contract/createCachedReadContract.ts index 5e4d1e5e..4abc18f4 100644 --- a/packages/evm-client-ethers/src/contract/createCachedReadContract.ts +++ b/packages/drift-ethers/src/contract/createCachedReadContract.ts @@ -2,7 +2,7 @@ import { type CachedReadContract, type SimpleCache, createCachedReadContract as baseFactory, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import type { Abi } from "abitype"; import { type CreateReadContractOptions, diff --git a/packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts b/packages/drift-ethers/src/contract/createCachedReadWriteContract.ts similarity index 96% rename from packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts rename to packages/drift-ethers/src/contract/createCachedReadWriteContract.ts index 2f13720b..006f0e46 100644 --- a/packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts +++ b/packages/drift-ethers/src/contract/createCachedReadWriteContract.ts @@ -2,7 +2,7 @@ import { type CachedReadWriteContract, type SimpleCache, createCachedReadWriteContract as baseFactory, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import type { Abi } from "abitype"; import { type ReadWriteContractOptions, diff --git a/packages/evm-client-ethers/src/contract/createReadContract.ts b/packages/drift-ethers/src/contract/createReadContract.ts similarity index 97% rename from packages/evm-client-ethers/src/contract/createReadContract.ts rename to packages/drift-ethers/src/contract/createReadContract.ts index 058919c8..279ee7ab 100644 --- a/packages/evm-client-ethers/src/contract/createReadContract.ts +++ b/packages/drift-ethers/src/contract/createReadContract.ts @@ -7,9 +7,15 @@ import { arrayToFriendly, arrayToObject, objectToArray, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import type { Abi } from "abitype"; -import { Contract, type EventLog, type InterfaceAbi, type Provider, type Signer } from "ethers"; +import { + Contract, + type EventLog, + type InterfaceAbi, + type Provider, + type Signer, +} from "ethers"; import { createReadWriteContract } from "src/contract/createReadWriteContract"; export interface CreateReadContractOptions { diff --git a/packages/evm-client-ethers/src/contract/createReadWriteContract.ts b/packages/drift-ethers/src/contract/createReadWriteContract.ts similarity index 92% rename from packages/evm-client-ethers/src/contract/createReadWriteContract.ts rename to packages/drift-ethers/src/contract/createReadWriteContract.ts index 3ad329b0..9e90f4eb 100644 --- a/packages/evm-client-ethers/src/contract/createReadWriteContract.ts +++ b/packages/drift-ethers/src/contract/createReadWriteContract.ts @@ -1,10 +1,15 @@ import { - objectToArray, type ReadContract, type ReadWriteContract, -} from "@delvtech/evm-client"; + objectToArray, +} from "@delvtech/drift"; import type { Abi } from "abitype"; -import { Contract, type InterfaceAbi, type Provider, type Signer } from "ethers"; +import { + Contract, + type InterfaceAbi, + type Provider, + type Signer, +} from "ethers"; import { createReadContract } from "src/contract/createReadContract"; export interface ReadWriteContractOptions { diff --git a/packages/evm-client-ethers/src/index.ts b/packages/drift-ethers/src/index.ts similarity index 85% rename from packages/evm-client-ethers/src/index.ts rename to packages/drift-ethers/src/index.ts index 7775a6cd..8a1f1db5 100644 --- a/packages/evm-client-ethers/src/index.ts +++ b/packages/drift-ethers/src/index.ts @@ -21,14 +21,14 @@ export { export { createNetwork } from "src/network/createNetwork"; // Re-exports -export * from "@delvtech/evm-client/cache"; +export * from "@delvtech/drift/cache"; export { arrayToFriendly, arrayToObject, getAbiEntry, objectToArray, -} from "@delvtech/evm-client/contract"; +} from "@delvtech/drift/contract"; export type { AbiArrayType, AbiEntry, @@ -57,7 +57,7 @@ export type { FunctionReturn, ReadContract, ReadWriteContract, -} from "@delvtech/evm-client/contract"; +} from "@delvtech/drift/contract"; -export * from "@delvtech/evm-client/errors"; -export * from "@delvtech/evm-client/network"; +export * from "@delvtech/drift/errors"; +export * from "@delvtech/drift/network"; diff --git a/packages/evm-client-ethers/src/network/createNetwork.ts b/packages/drift-ethers/src/network/createNetwork.ts similarity index 98% rename from packages/evm-client-ethers/src/network/createNetwork.ts rename to packages/drift-ethers/src/network/createNetwork.ts index 2c51c904..91e0ad0e 100644 --- a/packages/evm-client-ethers/src/network/createNetwork.ts +++ b/packages/drift-ethers/src/network/createNetwork.ts @@ -1,4 +1,4 @@ -import type { Network } from "@delvtech/evm-client"; +import type { Network } from "@delvtech/drift"; import type { Provider } from "ethers"; export function createNetwork(provider: Provider): Network { diff --git a/packages/drift-ethers/src/stubs.ts b/packages/drift-ethers/src/stubs.ts new file mode 100644 index 00000000..4c9072d4 --- /dev/null +++ b/packages/drift-ethers/src/stubs.ts @@ -0,0 +1 @@ +export * from "@delvtech/drift/stubs"; diff --git a/packages/evm-client-ethers/tsconfig.json b/packages/drift-ethers/tsconfig.json similarity index 100% rename from packages/evm-client-ethers/tsconfig.json rename to packages/drift-ethers/tsconfig.json diff --git a/packages/evm-client-ethers/tsup.config.ts b/packages/drift-ethers/tsup.config.ts similarity index 100% rename from packages/evm-client-ethers/tsup.config.ts rename to packages/drift-ethers/tsup.config.ts diff --git a/packages/evm-client-ethers/vite.config.ts b/packages/drift-ethers/vite.config.ts similarity index 100% rename from packages/evm-client-ethers/vite.config.ts rename to packages/drift-ethers/vite.config.ts diff --git a/packages/evm-client-viem/.gitignore b/packages/drift-viem/.gitignore similarity index 100% rename from packages/evm-client-viem/.gitignore rename to packages/drift-viem/.gitignore diff --git a/packages/evm-client-viem/CHANGELOG.md b/packages/drift-viem/CHANGELOG.md similarity index 100% rename from packages/evm-client-viem/CHANGELOG.md rename to packages/drift-viem/CHANGELOG.md diff --git a/packages/evm-client-viem/README.md b/packages/drift-viem/README.md similarity index 100% rename from packages/evm-client-viem/README.md rename to packages/drift-viem/README.md diff --git a/packages/evm-client-viem/integration-tests/artifacts/CoreVoting.ts b/packages/drift-viem/integration-tests/artifacts/CoreVoting.ts similarity index 100% rename from packages/evm-client-viem/integration-tests/artifacts/CoreVoting.ts rename to packages/drift-viem/integration-tests/artifacts/CoreVoting.ts diff --git a/packages/evm-client-viem/integration-tests/createReadContract.test.ts b/packages/drift-viem/integration-tests/createReadContract.test.ts similarity index 100% rename from packages/evm-client-viem/integration-tests/createReadContract.test.ts rename to packages/drift-viem/integration-tests/createReadContract.test.ts diff --git a/packages/evm-client-viem/package.json b/packages/drift-viem/package.json similarity index 92% rename from packages/evm-client-viem/package.json rename to packages/drift-viem/package.json index 600ce549..938527ef 100644 --- a/packages/evm-client-viem/package.json +++ b/packages/drift-viem/package.json @@ -1,6 +1,6 @@ { - "name": "@delvtech/evm-client-viem", - "version": "0.6.3", + "name": "@delvtech/drift-viem", + "version": "0.0.0", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -39,7 +39,7 @@ } }, "dependencies": { - "@delvtech/evm-client": "0.5.1" + "@delvtech/drift": "0.0.0" }, "devDependencies": { "@repo/typescript-config": "*", diff --git a/packages/evm-client-viem/src/contract/createCachedReadContract.ts b/packages/drift-viem/src/contract/createCachedReadContract.ts similarity index 96% rename from packages/evm-client-viem/src/contract/createCachedReadContract.ts rename to packages/drift-viem/src/contract/createCachedReadContract.ts index 2fba9c7e..04b8b98b 100644 --- a/packages/evm-client-viem/src/contract/createCachedReadContract.ts +++ b/packages/drift-viem/src/contract/createCachedReadContract.ts @@ -2,7 +2,7 @@ import { type CachedReadContract, type SimpleCache, createCachedReadContract as baseFactory, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import { type CreateReadContractOptions, createReadContract, diff --git a/packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts b/packages/drift-viem/src/contract/createCachedReadWriteContract.ts similarity index 96% rename from packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts rename to packages/drift-viem/src/contract/createCachedReadWriteContract.ts index 5da447d5..7ada2f65 100644 --- a/packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts +++ b/packages/drift-viem/src/contract/createCachedReadWriteContract.ts @@ -2,7 +2,7 @@ import { type CachedReadWriteContract, type SimpleCache, createCachedReadWriteContract as baseFactory, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import { type ReadWriteContractOptions, createReadWriteContract, diff --git a/packages/evm-client-viem/src/contract/createReadContract.ts b/packages/drift-viem/src/contract/createReadContract.ts similarity index 99% rename from packages/evm-client-viem/src/contract/createReadContract.ts rename to packages/drift-viem/src/contract/createReadContract.ts index 6cd0b3e8..d05237f9 100644 --- a/packages/evm-client-viem/src/contract/createReadContract.ts +++ b/packages/drift-viem/src/contract/createReadContract.ts @@ -7,7 +7,7 @@ import { type ReadWriteContract, arrayToObject, objectToArray, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import { createSimulateContractParameters } from "src/contract/utils/createSimulateContractParameters"; import { type Abi, diff --git a/packages/evm-client-viem/src/contract/createReadWriteContract.ts b/packages/drift-viem/src/contract/createReadWriteContract.ts similarity index 98% rename from packages/evm-client-viem/src/contract/createReadWriteContract.ts rename to packages/drift-viem/src/contract/createReadWriteContract.ts index 03c69efc..31abe18d 100644 --- a/packages/evm-client-viem/src/contract/createReadWriteContract.ts +++ b/packages/drift-viem/src/contract/createReadWriteContract.ts @@ -1,11 +1,11 @@ import { - objectToArray, type ReadContract, type ReadWriteContract, -} from "@delvtech/evm-client"; + objectToArray, +} from "@delvtech/drift"; import { - createReadContract, type CreateReadContractOptions, + createReadContract, } from "src/contract/createReadContract"; import { createSimulateContractParameters } from "src/contract/utils/createSimulateContractParameters"; import type { Abi, WalletClient } from "viem"; diff --git a/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts b/packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts similarity index 93% rename from packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts rename to packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts index 47325417..d18b736b 100644 --- a/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts +++ b/packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts @@ -1,4 +1,4 @@ -import type { ContractWriteOptions } from "@delvtech/evm-client"; +import type { ContractWriteOptions } from "@delvtech/drift"; /** * Get parameters for `simulateContract` from `ContractWriteOptions` diff --git a/packages/evm-client-viem/src/contract/utils/outputToFriendly.ts b/packages/drift-viem/src/contract/utils/outputToFriendly.ts similarity index 96% rename from packages/evm-client-viem/src/contract/utils/outputToFriendly.ts rename to packages/drift-viem/src/contract/utils/outputToFriendly.ts index ea06b066..4a2d5a73 100644 --- a/packages/evm-client-viem/src/contract/utils/outputToFriendly.ts +++ b/packages/drift-viem/src/contract/utils/outputToFriendly.ts @@ -2,7 +2,7 @@ import { type FunctionReturn, arrayToFriendly, getAbiEntry, -} from "@delvtech/evm-client"; +} from "@delvtech/drift"; import type { Abi } from "viem"; export function outputToFriendly({ diff --git a/packages/evm-client-viem/src/index.ts b/packages/drift-viem/src/index.ts similarity index 85% rename from packages/evm-client-viem/src/index.ts rename to packages/drift-viem/src/index.ts index 27bb04bc..3c898f90 100644 --- a/packages/evm-client-viem/src/index.ts +++ b/packages/drift-viem/src/index.ts @@ -21,14 +21,14 @@ export { export { createNetwork } from "src/network/createNetwork"; // Re-exports -export * from "@delvtech/evm-client/cache"; +export * from "@delvtech/drift/cache"; export { arrayToFriendly, arrayToObject, getAbiEntry, objectToArray, -} from "@delvtech/evm-client/contract"; +} from "@delvtech/drift/contract"; export type { AbiArrayType, AbiEntry, @@ -57,7 +57,7 @@ export type { FunctionReturn, ReadContract, ReadWriteContract, -} from "@delvtech/evm-client/contract"; +} from "@delvtech/drift/contract"; -export * from "@delvtech/evm-client/errors"; -export * from "@delvtech/evm-client/network"; +export * from "@delvtech/drift/errors"; +export * from "@delvtech/drift/network"; diff --git a/packages/evm-client-viem/src/network/createNetwork.ts b/packages/drift-viem/src/network/createNetwork.ts similarity index 97% rename from packages/evm-client-viem/src/network/createNetwork.ts rename to packages/drift-viem/src/network/createNetwork.ts index 672fc396..850f392a 100644 --- a/packages/evm-client-viem/src/network/createNetwork.ts +++ b/packages/drift-viem/src/network/createNetwork.ts @@ -1,4 +1,4 @@ -import type { Network } from "@delvtech/evm-client"; +import type { Network } from "@delvtech/drift"; import { type GetBalanceParameters, type PublicClient, diff --git a/packages/drift-viem/src/stubs.ts b/packages/drift-viem/src/stubs.ts new file mode 100644 index 00000000..4c9072d4 --- /dev/null +++ b/packages/drift-viem/src/stubs.ts @@ -0,0 +1 @@ +export * from "@delvtech/drift/stubs"; diff --git a/packages/evm-client-viem/tsconfig.json b/packages/drift-viem/tsconfig.json similarity index 100% rename from packages/evm-client-viem/tsconfig.json rename to packages/drift-viem/tsconfig.json diff --git a/packages/evm-client-viem/tsup.config.ts b/packages/drift-viem/tsup.config.ts similarity index 100% rename from packages/evm-client-viem/tsup.config.ts rename to packages/drift-viem/tsup.config.ts diff --git a/packages/evm-client-viem/vite.config.ts b/packages/drift-viem/vite.config.ts similarity index 100% rename from packages/evm-client-viem/vite.config.ts rename to packages/drift-viem/vite.config.ts diff --git a/packages/evm-client/.gitignore b/packages/drift/.gitignore similarity index 100% rename from packages/evm-client/.gitignore rename to packages/drift/.gitignore diff --git a/packages/evm-client/CHANGELOG.md b/packages/drift/CHANGELOG.md similarity index 100% rename from packages/evm-client/CHANGELOG.md rename to packages/drift/CHANGELOG.md diff --git a/packages/evm-client/README.md b/packages/drift/README.md similarity index 100% rename from packages/evm-client/README.md rename to packages/drift/README.md diff --git a/packages/evm-client/package.json b/packages/drift/package.json similarity index 100% rename from packages/evm-client/package.json rename to packages/drift/package.json diff --git a/packages/evm-client/src/base/testing/IERC20.ts b/packages/drift/src/base/testing/IERC20.ts similarity index 100% rename from packages/evm-client/src/base/testing/IERC20.ts rename to packages/drift/src/base/testing/IERC20.ts diff --git a/packages/evm-client/src/base/testing/accounts.ts b/packages/drift/src/base/testing/accounts.ts similarity index 100% rename from packages/evm-client/src/base/testing/accounts.ts rename to packages/drift/src/base/testing/accounts.ts diff --git a/packages/evm-client/src/base/types.ts b/packages/drift/src/base/types.ts similarity index 100% rename from packages/evm-client/src/base/types.ts rename to packages/drift/src/base/types.ts diff --git a/packages/evm-client/src/cache/factories/createLruSimpleCache.ts b/packages/drift/src/cache/factories/createLruSimpleCache.ts similarity index 100% rename from packages/evm-client/src/cache/factories/createLruSimpleCache.ts rename to packages/drift/src/cache/factories/createLruSimpleCache.ts diff --git a/packages/evm-client/src/cache/types/SimpleCache.ts b/packages/drift/src/cache/types/SimpleCache.ts similarity index 100% rename from packages/evm-client/src/cache/types/SimpleCache.ts rename to packages/drift/src/cache/types/SimpleCache.ts diff --git a/packages/evm-client/src/cache/utils/createSimpleCacheKey.ts b/packages/drift/src/cache/utils/createSimpleCacheKey.ts similarity index 100% rename from packages/evm-client/src/cache/utils/createSimpleCacheKey.ts rename to packages/drift/src/cache/utils/createSimpleCacheKey.ts diff --git a/packages/evm-client/src/contract/factories/createCachedReadContract.test.ts b/packages/drift/src/contract/factories/createCachedReadContract.test.ts similarity index 100% rename from packages/evm-client/src/contract/factories/createCachedReadContract.test.ts rename to packages/drift/src/contract/factories/createCachedReadContract.test.ts diff --git a/packages/evm-client/src/contract/factories/createCachedReadContract.ts b/packages/drift/src/contract/factories/createCachedReadContract.ts similarity index 100% rename from packages/evm-client/src/contract/factories/createCachedReadContract.ts rename to packages/drift/src/contract/factories/createCachedReadContract.ts diff --git a/packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts b/packages/drift/src/contract/factories/createCachedReadWriteContract.ts similarity index 100% rename from packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts rename to packages/drift/src/contract/factories/createCachedReadWriteContract.ts diff --git a/packages/evm-client/src/contract/stubs/ReadContractStub.test.ts b/packages/drift/src/contract/stubs/ReadContractStub.test.ts similarity index 100% rename from packages/evm-client/src/contract/stubs/ReadContractStub.test.ts rename to packages/drift/src/contract/stubs/ReadContractStub.test.ts diff --git a/packages/evm-client/src/contract/stubs/ReadContractStub.ts b/packages/drift/src/contract/stubs/ReadContractStub.ts similarity index 100% rename from packages/evm-client/src/contract/stubs/ReadContractStub.ts rename to packages/drift/src/contract/stubs/ReadContractStub.ts diff --git a/packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts b/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts similarity index 100% rename from packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts rename to packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts diff --git a/packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts b/packages/drift/src/contract/stubs/ReadWriteContractStub.ts similarity index 100% rename from packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts rename to packages/drift/src/contract/stubs/ReadWriteContractStub.ts diff --git a/packages/evm-client/src/contract/types/AbiEntry.ts b/packages/drift/src/contract/types/AbiEntry.ts similarity index 100% rename from packages/evm-client/src/contract/types/AbiEntry.ts rename to packages/drift/src/contract/types/AbiEntry.ts diff --git a/packages/evm-client/src/contract/types/CachedContract.ts b/packages/drift/src/contract/types/CachedContract.ts similarity index 100% rename from packages/evm-client/src/contract/types/CachedContract.ts rename to packages/drift/src/contract/types/CachedContract.ts diff --git a/packages/evm-client/src/contract/types/Contract.ts b/packages/drift/src/contract/types/Contract.ts similarity index 100% rename from packages/evm-client/src/contract/types/Contract.ts rename to packages/drift/src/contract/types/Contract.ts diff --git a/packages/evm-client/src/contract/types/Event.ts b/packages/drift/src/contract/types/Event.ts similarity index 100% rename from packages/evm-client/src/contract/types/Event.ts rename to packages/drift/src/contract/types/Event.ts diff --git a/packages/evm-client/src/contract/types/Function.ts b/packages/drift/src/contract/types/Function.ts similarity index 94% rename from packages/evm-client/src/contract/types/Function.ts rename to packages/drift/src/contract/types/Function.ts index f0e5a0ce..00f2e63d 100644 --- a/packages/evm-client/src/contract/types/Function.ts +++ b/packages/drift/src/contract/types/Function.ts @@ -1,5 +1,8 @@ import type { Abi, AbiStateMutability } from "abitype"; -import type { AbiFriendlyType, AbiObjectType } from "src/contract/types/AbiEntry"; +import type { + AbiFriendlyType, + AbiObjectType, +} from "src/contract/types/AbiEntry"; /** * Get a union of function names from an abi diff --git a/packages/evm-client/src/contract/utils/arrayToFriendly.test.ts b/packages/drift/src/contract/utils/arrayToFriendly.test.ts similarity index 100% rename from packages/evm-client/src/contract/utils/arrayToFriendly.test.ts rename to packages/drift/src/contract/utils/arrayToFriendly.test.ts diff --git a/packages/evm-client/src/contract/utils/arrayToFriendly.ts b/packages/drift/src/contract/utils/arrayToFriendly.ts similarity index 100% rename from packages/evm-client/src/contract/utils/arrayToFriendly.ts rename to packages/drift/src/contract/utils/arrayToFriendly.ts diff --git a/packages/evm-client/src/contract/utils/arrayToObject.test.ts b/packages/drift/src/contract/utils/arrayToObject.test.ts similarity index 100% rename from packages/evm-client/src/contract/utils/arrayToObject.test.ts rename to packages/drift/src/contract/utils/arrayToObject.test.ts diff --git a/packages/evm-client/src/contract/utils/arrayToObject.ts b/packages/drift/src/contract/utils/arrayToObject.ts similarity index 100% rename from packages/evm-client/src/contract/utils/arrayToObject.ts rename to packages/drift/src/contract/utils/arrayToObject.ts diff --git a/packages/evm-client/src/contract/utils/getAbiEntry.ts b/packages/drift/src/contract/utils/getAbiEntry.ts similarity index 100% rename from packages/evm-client/src/contract/utils/getAbiEntry.ts rename to packages/drift/src/contract/utils/getAbiEntry.ts diff --git a/packages/evm-client/src/contract/utils/objectToArray.test.ts b/packages/drift/src/contract/utils/objectToArray.test.ts similarity index 100% rename from packages/evm-client/src/contract/utils/objectToArray.test.ts rename to packages/drift/src/contract/utils/objectToArray.test.ts diff --git a/packages/evm-client/src/contract/utils/objectToArray.ts b/packages/drift/src/contract/utils/objectToArray.ts similarity index 100% rename from packages/evm-client/src/contract/utils/objectToArray.ts rename to packages/drift/src/contract/utils/objectToArray.ts diff --git a/packages/evm-client/src/errors/AbiEntryNotFound.ts b/packages/drift/src/errors/AbiEntryNotFound.ts similarity index 100% rename from packages/evm-client/src/errors/AbiEntryNotFound.ts rename to packages/drift/src/errors/AbiEntryNotFound.ts diff --git a/packages/evm-client/src/exports/cache.ts b/packages/drift/src/exports/cache.ts similarity index 100% rename from packages/evm-client/src/exports/cache.ts rename to packages/drift/src/exports/cache.ts diff --git a/packages/evm-client/src/exports/contract.ts b/packages/drift/src/exports/contract.ts similarity index 100% rename from packages/evm-client/src/exports/contract.ts rename to packages/drift/src/exports/contract.ts diff --git a/packages/evm-client/src/exports/errors.ts b/packages/drift/src/exports/errors.ts similarity index 100% rename from packages/evm-client/src/exports/errors.ts rename to packages/drift/src/exports/errors.ts diff --git a/packages/evm-client/src/exports/index.ts b/packages/drift/src/exports/index.ts similarity index 100% rename from packages/evm-client/src/exports/index.ts rename to packages/drift/src/exports/index.ts diff --git a/packages/evm-client/src/exports/network.ts b/packages/drift/src/exports/network.ts similarity index 100% rename from packages/evm-client/src/exports/network.ts rename to packages/drift/src/exports/network.ts diff --git a/packages/evm-client/src/exports/stubs.ts b/packages/drift/src/exports/stubs.ts similarity index 100% rename from packages/evm-client/src/exports/stubs.ts rename to packages/drift/src/exports/stubs.ts diff --git a/packages/evm-client/src/network/stubs/NetworkStub.test.ts b/packages/drift/src/network/stubs/NetworkStub.test.ts similarity index 100% rename from packages/evm-client/src/network/stubs/NetworkStub.test.ts rename to packages/drift/src/network/stubs/NetworkStub.test.ts diff --git a/packages/evm-client/src/network/stubs/NetworkStub.ts b/packages/drift/src/network/stubs/NetworkStub.ts similarity index 98% rename from packages/evm-client/src/network/stubs/NetworkStub.ts rename to packages/drift/src/network/stubs/NetworkStub.ts index 0b091120..b43b8bb4 100644 --- a/packages/evm-client/src/network/stubs/NetworkStub.ts +++ b/packages/drift/src/network/stubs/NetworkStub.ts @@ -7,7 +7,10 @@ import type { NetworkGetTransactionArgs, NetworkWaitForTransactionArgs, } from "src/network/types/Network"; -import type { Transaction, TransactionReceipt } from "src/network/types/Transaction"; +import type { + Transaction, + TransactionReceipt, +} from "src/network/types/Transaction"; /** * A mock implementation of a `Network` designed to facilitate unit diff --git a/packages/evm-client/src/network/types/Block.ts b/packages/drift/src/network/types/Block.ts similarity index 100% rename from packages/evm-client/src/network/types/Block.ts rename to packages/drift/src/network/types/Block.ts diff --git a/packages/evm-client/src/network/types/Network.ts b/packages/drift/src/network/types/Network.ts similarity index 95% rename from packages/evm-client/src/network/types/Network.ts rename to packages/drift/src/network/types/Network.ts index de200e58..8e318859 100644 --- a/packages/evm-client/src/network/types/Network.ts +++ b/packages/drift/src/network/types/Network.ts @@ -1,5 +1,8 @@ import type { Block, BlockTag } from "src/network/types/Block"; -import type { Transaction, TransactionReceipt } from "src/network/types/Transaction"; +import type { + Transaction, + TransactionReceipt, +} from "src/network/types/Transaction"; // https://ethereum.github.io/execution-apis/api-documentation/ diff --git a/packages/evm-client/src/network/types/Transaction.ts b/packages/drift/src/network/types/Transaction.ts similarity index 100% rename from packages/evm-client/src/network/types/Transaction.ts rename to packages/drift/src/network/types/Transaction.ts diff --git a/packages/evm-client/tsconfig.json b/packages/drift/tsconfig.json similarity index 100% rename from packages/evm-client/tsconfig.json rename to packages/drift/tsconfig.json diff --git a/packages/evm-client/tsup.config.ts b/packages/drift/tsup.config.ts similarity index 100% rename from packages/evm-client/tsup.config.ts rename to packages/drift/tsup.config.ts diff --git a/packages/evm-client/vite.config.ts b/packages/drift/vite.config.ts similarity index 100% rename from packages/evm-client/vite.config.ts rename to packages/drift/vite.config.ts diff --git a/packages/evm-client-ethers/src/stubs.ts b/packages/evm-client-ethers/src/stubs.ts deleted file mode 100644 index 363f08fb..00000000 --- a/packages/evm-client-ethers/src/stubs.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@delvtech/evm-client/stubs"; diff --git a/packages/evm-client-viem/src/stubs.ts b/packages/evm-client-viem/src/stubs.ts deleted file mode 100644 index 363f08fb..00000000 --- a/packages/evm-client-viem/src/stubs.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@delvtech/evm-client/stubs"; diff --git a/yarn.lock b/yarn.lock index 0dc27c75..8962a931 100644 --- a/yarn.lock +++ b/yarn.lock @@ -294,13 +294,6 @@ human-id "^1.0.2" prettier "^2.7.1" -"@cspotcode/source-map-support@^0.8.0": - version "0.8.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" - integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== - dependencies: - "@jridgewell/trace-mapping" "0.3.9" - "@esbuild/aix-ppc64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" @@ -444,7 +437,7 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/resolve-uri@^3.0.3", "@jridgewell/resolve-uri@^3.1.0": +"@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== @@ -459,14 +452,6 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@0.3.9": - version "0.3.9" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" - integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== - dependencies: - "@jridgewell/resolve-uri" "^3.0.3" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping@^0.3.9": version "0.3.22" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" @@ -667,26 +652,6 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" - integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== - "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -790,12 +755,12 @@ abitype@1.0.0, abitype@^1.0.0: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== -acorn-walk@^8.1.1, acorn-walk@^8.3.2: +acorn-walk@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== -acorn@^8.10.0, acorn@^8.11.3, acorn@^8.4.1: +acorn@^8.10.0, acorn@^8.11.3: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -857,11 +822,6 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1109,11 +1069,6 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1220,11 +1175,6 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - diff@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" @@ -2106,11 +2056,6 @@ magic-string@^0.30.5: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -3046,25 +2991,6 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== -ts-node@^10.9.2: - version "10.9.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" - integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== - dependencies: - "@cspotcode/source-map-support" "^0.8.0" - "@tsconfig/node10" "^1.0.7" - "@tsconfig/node12" "^1.0.7" - "@tsconfig/node14" "^1.0.0" - "@tsconfig/node16" "^1.0.2" - acorn "^8.4.1" - acorn-walk "^8.1.1" - arg "^4.1.0" - create-require "^1.1.0" - diff "^4.0.1" - make-error "^1.1.1" - v8-compile-cache-lib "^3.0.1" - yn "3.1.1" - tsconfck@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.2.tgz#d8e279f7a049d55f207f528d13fa493e1d8e7ceb" @@ -3243,11 +3169,6 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== -v8-compile-cache-lib@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" - integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -3511,11 +3432,6 @@ yargs@^17.7.1: y18n "^5.0.5" yargs-parser "^21.1.1" -yn@3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From be1d0e76dc5edad74d8935f980485c3d40c94b47 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 29 Sep 2024 01:19:57 -0500 Subject: [PATCH 11/49] Remove repeated sentance --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 20ca6b25..e15a8c28 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,7 @@ Building on Ethereum often means dealing with: - ⚑ **Optimized Performance:** Automatically reduces redundant RPC calls with built-in caching. No need to manage hooks or query keys for each call. - πŸ”’ **Type Safety:** Drift's type-checked APIs help catch errors at compile - time. If your ABI changes, your mocks will reflect that at compile time, - keeping your tests in sync. + time. - πŸ§ͺ **Testing Made Easy:** Built-in mocks simplify testing your contract interactions. Drift's testing mocks are also type-safe, ensuring your tests are always in sync with your contracts. From 2bd56b4e78cf4b8c328b9ecab519f28332b8737f Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 29 Sep 2024 01:20:17 -0500 Subject: [PATCH 12/49] Add wip drift client --- packages/drift/src/drift.ts | 136 ++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 packages/drift/src/drift.ts diff --git a/packages/drift/src/drift.ts b/packages/drift/src/drift.ts new file mode 100644 index 00000000..61bd777d --- /dev/null +++ b/packages/drift/src/drift.ts @@ -0,0 +1,136 @@ +import type { Abi } from "abitype"; +import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; +import type { SimpleCache } from "src/cache/types/SimpleCache"; +import { createCachedReadContract } from "src/contract/factories/createCachedReadContract"; +import { createCachedReadWriteContract } from "src/contract/factories/createCachedReadWriteContract"; +import type { + CachedReadContract, + CachedReadWriteContract, +} from "src/contract/types/CachedContract"; +import type { + ContractReadOptions, + ReadContract, + ReadWriteContract, +} from "src/contract/types/Contract"; +import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; +import type { Network } from "src/network/types/Network"; + +export interface ReadAdapter { + network: Network; + createReadContract(options: { + abi: TAbi; + address: string; + }): ReadContract; +} + +export interface ReadWriteAdapter extends ReadAdapter { + createReadWriteContract(options: { + abi: TAbi; + address: string; + }): ReadWriteContract; +} + +export class Drift { + adapter: TAdapter; + cache: DriftCache; + + constructor( + adapter: TAdapter, + { + cache = createLruSimpleCache({ max: 500 }), + }: { + cache?: SimpleCache; + } = {}, + ) { + this.adapter = adapter; + this.cache = createDriftCache(cache, adapter); + } + + contract({ + abi, + address, + cache, + namespace, + }: ContractOptions): DriftContract { + if (isReadWriteAdapter(this.adapter)) { + return createCachedReadWriteContract({ + contract: this.adapter.createReadWriteContract({ abi, address }), + cache, + namespace, + }); + } + + return createCachedReadContract({ + contract: this.adapter.createReadContract({ abi, address }), + cache, + namespace, + }) as DriftContract; + } +} + +export type DriftContract< + TAbi extends Abi, + TAdapter extends ReadAdapter | ReadWriteAdapter = ReadAdapter, +> = TAdapter extends ReadWriteAdapter + ? CachedReadWriteContract + : CachedReadContract; + +function isReadWriteAdapter( + adapter: ReadAdapter | ReadWriteAdapter, +): adapter is ReadWriteAdapter { + return "createReadWriteContract" in adapter; +} + +export interface ContractOptions { + abi: TAbi; + address: string; + cache?: SimpleCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; +} + +interface DriftCache extends SimpleCache { + invalidateRead>( + params: DriftReadParams, + ): void; +} + +function createDriftCache( + cache: SimpleCache, + adapter: ReadAdapter, +): DriftCache { + const cachePrototype = Object.getPrototypeOf(cache); + const newCache: SimpleCache = Object.create(cachePrototype); + return Object.assign(newCache, cache, { + invalidateRead>({ + abi, + address, + args, + fn, + ...options + }: DriftReadParams) { + const contract = createCachedReadContract({ + contract: adapter.createReadContract({ + abi, + address, + }), + cache, + }); + + contract.deleteRead(fn, args, options); + }, + }); +} + +type DriftReadParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = { + abi: TAbi; + address: string; + fn: TFunctionName; + args: FunctionArgs; +} & ContractReadOptions; From e3441e3f2871d868016be12f8a94b1b83bbebfe2 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 29 Sep 2024 16:17:12 -0500 Subject: [PATCH 13/49] Simplify testing example --- README.md | 78 +++++++++++++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index e15a8c28..fbe6d592 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ import { ReadWriteAdapter, ReadWriteContract, } from "@delvtech/drift"; -import { vaultAbi } from "../abis/VaultAbi"; +import { vaultAbi } from "./abis/VaultAbi"; type VaultAbi = typeof vaultAbi; @@ -300,73 +300,67 @@ export class ReadWriteVault extends CoreReadWriteVault { } ``` +Then, in your app: + +```typescript +import { ReadVault } from "sdk-viem"; +import { createPublicClient, http } from "viem"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +// Instantiate the ReadVault client with viem directly +const readVault = new ReadVault("0xYourVaultAddress", publicClient); +``` + ### 4. Test Your Clients with Drift's Built-in Mocks Testing smart contract interactions can be complex and time-consuming. Drift simplifies this process by providing built-in mocks that allow you to stub responses and focus on testing your application logic. -#### Example: Testing Contract Interactions with Mocks +#### Example: Testing Client Methods with Multiple RPC Calls -Suppose you have a method `getShortAccruedYield` in your `ReadVault` client that -calculates the accrued yield for a mature position. You want to test this method -without making actual RPC calls. +Suppose you have a method `getAccountValue` in your `ReadVault` client that +get's the total asset value for an account by fetching their vault balance and +converting it to assets. Under the hood, this method makes multiple RPC +requests. Here's how you can use Drift's mocks to stub contract calls and test your method: ```typescript -// test/ReadVault.test.ts +// sdk-core/src/ReadVault.test.ts import { MockDrift } from "@delvtech/drift/testing"; -import { parseBigInt } from "parse-bigint"; -import { ReadVault } from "sdk-core"; -import { vaultAbi } from "../abis/VaultAbi"; +import { vaultAbi } from "./abis/VaultAbi"; +import { ReadVault } from "./VaultClient"; -test("getShortAccruedYield should return the amount of yield a mature position has earned", async () => { +test("getUserAssetValue should return the total asset value for a user", async () => { // Set up mocks const mockDrift = new MockDrift(); - const mockContract = drift.contract({ + const mockContract = mockDrift.contract({ abi: vaultAbi, address: "0xVaultAddress", }); - // Stub the getBlock method - mockDrift.onGetBlock().returns({ number: 1n, timestamp: 1699503565n }); - - // Stub contract reads for getPoolConfig - mockContract.onRead("getPoolConfig").returns({ - positionDuration: 86400n, // one day in seconds - checkpointDuration: 86400n, // one day in seconds - // ...other config values - }); - - // Stub the checkpoint when the short was opened - mockContract.onRead("getCheckpoint", { _checkpointTime: 1n }).returns({ - vaultSharePrice: parseBigInt("1.008e18"), - weightedSpotPrice: 0n, - lastWeightedSpotPriceUpdateTime: 0n, - }); - - // Stub the checkpoint when the short matured - mockContract.onRead("getCheckpoint", { _checkpointTime: 86401n }).returns({ - vaultSharePrice: parseBigInt("1.01e18"), - weightedSpotPrice: 0n, - lastWeightedSpotPriceUpdateTime: 0n, - }); + // Stub the vault's return values + mockContract.onRead("balanceOf", { account: "0xUserAddress" }).returns( + BigInt(100e18), // User has 100 vault shares + ); + mockContract.onRead("convertToAssets", { shares: BigInt(100e18) }).returns( + BigInt(150e18), // 100 vault shares are worth 150 in assets + ); // Instantiate your client with the mocked Drift instance - const readVault = new ReadVault("0xYourVaultAddress", mockDrift); + const readVault = new ReadVault("0xVaultAddress", mockDrift); // Call the method you want to test - const accruedYield = await readVault.getShortAccruedYield({ - checkpointTime: 1n, - bondAmount: parseBigInt("100e18"), - }); + const accountAssetValue = await readVault.getAccountValue("0xUserAddress"); // Assert the expected result - // If you opened a short position on 100 bonds at a previous checkpoint price - // of 1.008 and the price was 1.01 at maturity, your accrued profit would be 0.20. - expect(accruedYield).toEqual(parseBigInt("0.20e18")); + expect(accountAssetValue).toEqual(BigInt(150e18)); }); ``` From 962e2cf909dcfdff110ce2d749e7cfa8c08b86ba Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 30 Sep 2024 14:07:46 -0500 Subject: [PATCH 14/49] Wip MockAdapter and MockDrift --- .../drift/src/adapter/MockAdapter.test.ts | 40 ++++ packages/drift/src/adapter/MockAdapter.ts | 12 + packages/drift/src/adapter/types.ts | 24 ++ packages/drift/src/drift.ts | 136 ----------- packages/drift/src/drift/Drift.ts | 221 ++++++++++++++++++ packages/drift/src/drift/MockDrift.test.ts | 27 +++ packages/drift/src/drift/MockDrift.ts | 15 ++ 7 files changed, 339 insertions(+), 136 deletions(-) create mode 100644 packages/drift/src/adapter/MockAdapter.test.ts create mode 100644 packages/drift/src/adapter/MockAdapter.ts create mode 100644 packages/drift/src/adapter/types.ts delete mode 100644 packages/drift/src/drift.ts create mode 100644 packages/drift/src/drift/Drift.ts create mode 100644 packages/drift/src/drift/MockDrift.test.ts create mode 100644 packages/drift/src/drift/MockDrift.ts diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts new file mode 100644 index 00000000..8c80a113 --- /dev/null +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -0,0 +1,40 @@ +import { MockAdapter } from "src/adapter/MockAdapter"; +import { IERC20 } from "src/base/testing/IERC20"; +import { describe, expect, it } from "vitest"; + +describe("MockAdapter", () => { + it("Includes a mock network", async () => { + const adapter = new MockAdapter(); + const blockStub = { + blockNumber: 100n, + timestamp: 200n, + }; + adapter.network.stubGetBlock({ + value: blockStub, + }); + const block = await adapter.network.getBlock(); + expect(block).toBe(blockStub); + }); + + it("Creates mock read contracts", async () => { + const mockAdapter = new MockAdapter(); + const contract = mockAdapter.readContract(IERC20.abi); + contract.stubRead({ + functionName: "balanceOf", + value: 100n, + }); + const balance = await contract.read("balanceOf", { owner: "0xMe" }); + expect(balance).toBe(100n); + }); + + it("Creates mock read-write contracts", async () => { + const mockAdapter = new MockAdapter(); + const contract = mockAdapter.readWriteContract(IERC20.abi); + contract.stubWrite("approve", "0xDone"); + const txHash = await contract.write("approve", { + spender: "0xYou", + value: 100n, + }); + expect(txHash).toBe("0xDone"); + }); +}); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts new file mode 100644 index 00000000..89bd7b94 --- /dev/null +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -0,0 +1,12 @@ +import type { Abi } from "abitype"; +import type { ReadWriteAdapter } from "src/adapter/types"; +import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +import { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; +import { NetworkStub } from "src/network/stubs/NetworkStub"; + +export class MockAdapter implements ReadWriteAdapter { + network = new NetworkStub(); + readContract = (abi: TAbi) => new ReadContractStub(abi); + readWriteContract = (abi: TAbi) => + new ReadWriteContractStub(abi); +} diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types.ts new file mode 100644 index 00000000..56a86190 --- /dev/null +++ b/packages/drift/src/adapter/types.ts @@ -0,0 +1,24 @@ +import type { Abi } from "abitype"; +import type { Prettify } from "src/base/types"; +import type { + ReadContract, + ReadWriteContract, +} from "src/contract/types/Contract"; +import type { Network } from "src/network/types/Network"; + +export interface Adapter { + network: Network; + readContract: ( + abi: TAbi, + address: string, + ) => ReadContract; + readWriteContract?: ( + abi: TAbi, + address: string, + ) => ReadWriteContract; +} + +export type ReadWriteAdapter = Prettify< + Omit & + Pick, "readWriteContract"> +>; diff --git a/packages/drift/src/drift.ts b/packages/drift/src/drift.ts deleted file mode 100644 index 61bd777d..00000000 --- a/packages/drift/src/drift.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { Abi } from "abitype"; -import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; -import type { SimpleCache } from "src/cache/types/SimpleCache"; -import { createCachedReadContract } from "src/contract/factories/createCachedReadContract"; -import { createCachedReadWriteContract } from "src/contract/factories/createCachedReadWriteContract"; -import type { - CachedReadContract, - CachedReadWriteContract, -} from "src/contract/types/CachedContract"; -import type { - ContractReadOptions, - ReadContract, - ReadWriteContract, -} from "src/contract/types/Contract"; -import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; -import type { Network } from "src/network/types/Network"; - -export interface ReadAdapter { - network: Network; - createReadContract(options: { - abi: TAbi; - address: string; - }): ReadContract; -} - -export interface ReadWriteAdapter extends ReadAdapter { - createReadWriteContract(options: { - abi: TAbi; - address: string; - }): ReadWriteContract; -} - -export class Drift { - adapter: TAdapter; - cache: DriftCache; - - constructor( - adapter: TAdapter, - { - cache = createLruSimpleCache({ max: 500 }), - }: { - cache?: SimpleCache; - } = {}, - ) { - this.adapter = adapter; - this.cache = createDriftCache(cache, adapter); - } - - contract({ - abi, - address, - cache, - namespace, - }: ContractOptions): DriftContract { - if (isReadWriteAdapter(this.adapter)) { - return createCachedReadWriteContract({ - contract: this.adapter.createReadWriteContract({ abi, address }), - cache, - namespace, - }); - } - - return createCachedReadContract({ - contract: this.adapter.createReadContract({ abi, address }), - cache, - namespace, - }) as DriftContract; - } -} - -export type DriftContract< - TAbi extends Abi, - TAdapter extends ReadAdapter | ReadWriteAdapter = ReadAdapter, -> = TAdapter extends ReadWriteAdapter - ? CachedReadWriteContract - : CachedReadContract; - -function isReadWriteAdapter( - adapter: ReadAdapter | ReadWriteAdapter, -): adapter is ReadWriteAdapter { - return "createReadWriteContract" in adapter; -} - -export interface ContractOptions { - abi: TAbi; - address: string; - cache?: SimpleCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -interface DriftCache extends SimpleCache { - invalidateRead>( - params: DriftReadParams, - ): void; -} - -function createDriftCache( - cache: SimpleCache, - adapter: ReadAdapter, -): DriftCache { - const cachePrototype = Object.getPrototypeOf(cache); - const newCache: SimpleCache = Object.create(cachePrototype); - return Object.assign(newCache, cache, { - invalidateRead>({ - abi, - address, - args, - fn, - ...options - }: DriftReadParams) { - const contract = createCachedReadContract({ - contract: adapter.createReadContract({ - abi, - address, - }), - cache, - }); - - contract.deleteRead(fn, args, options); - }, - }); -} - -type DriftReadParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = { - abi: TAbi; - address: string; - fn: TFunctionName; - args: FunctionArgs; -} & ContractReadOptions; diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts new file mode 100644 index 00000000..525b144e --- /dev/null +++ b/packages/drift/src/drift/Drift.ts @@ -0,0 +1,221 @@ +import type { Abi } from "abitype"; +import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; +import type { EmptyObject } from "src/base/types"; +import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; +import type { SimpleCache } from "src/cache/types/SimpleCache"; +import { createCachedReadContract } from "src/contract/factories/createCachedReadContract"; +import { createCachedReadWriteContract } from "src/contract/factories/createCachedReadWriteContract"; +import type { + CachedReadContract, + CachedReadWriteContract, +} from "src/contract/types/CachedContract"; +import type { + ContractReadOptions, + ContractWriteOptions, +} from "src/contract/types/Contract"; +import type { + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/contract/types/Function"; +import type { TransactionReceipt } from "src/network/types/Transaction"; + +// Drift Client // + +export interface IDrift< + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> { + adapter: TAdapter; + cache: DriftCache; + readonly isReadWrite: TAdapter extends ReadWriteAdapter ? true : false; + + // methods // + + contract: ( + params: ContractParams, + ) => DriftContract; + + read: >( + params: DriftReadParams, + ) => Promise>; + + write: TAdapter extends ReadWriteAdapter + ? < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: DriftWriteParams, + ) => Promise + : undefined; +} + +export interface DriftOptions { + cache?: TCache; +} + +export class Drift< + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> implements IDrift +{ + readonly adapter: TAdapter; + cache: DriftCache; + write: TAdapter extends ReadWriteAdapter + ? < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: DriftWriteParams, + ) => Promise + : undefined; + + constructor( + adapter: TAdapter, + { + cache = createLruSimpleCache({ max: 500 }) as TCache, + }: DriftOptions = {}, + ) { + this.adapter = adapter; + this.cache = createDriftCache(cache, adapter); + + this.write = isReadWriteAdapter(adapter) + ? async ({ abi, address, fn, args, onMined, ...writeOptions }) => { + const txHash = await createCachedReadWriteContract({ + contract: adapter.readWriteContract(abi, address), + cache, + }).write(fn, args, writeOptions); + + if (onMined) { + adapter.network.waitForTransaction(txHash).then(onMined); + } + + return txHash; + } + : (undefined as any); + } + + get isReadWrite(): TAdapter extends ReadWriteAdapter ? true : false { + return isReadWriteAdapter(this.adapter) as any; + } + + // Static properties // + + contract = ({ + abi, + address, + cache, + namespace, + }: ContractParams): DriftContract => + isReadWriteAdapter(this.adapter) + ? createCachedReadWriteContract({ + contract: this.adapter.readWriteContract(abi, address), + cache, + namespace, + }) + : (createCachedReadContract({ + contract: this.adapter.readContract(abi, address), + cache, + namespace, + }) as DriftContract); + + read = async >({ + abi, + address, + fn, + args, + ...readOptions + }: DriftReadParams): Promise< + FunctionReturn + > => { + return createCachedReadContract({ + contract: this.adapter.readContract(abi, address), + cache: this.cache, + }).read(fn, args, readOptions); + }; +} + +export interface ContractParams { + abi: TAbi; + address: string; + cache?: SimpleCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; +} + +export type DriftContract< + TAbi extends Abi, + TAdapter extends Adapter = Adapter, +> = TAdapter extends ReadWriteAdapter + ? CachedReadWriteContract + : CachedReadContract; + +type DriftReadParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = ContractReadOptions & { + abi: TAbi; + address: string; + fn: TFunctionName; +} & (FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }); + +type DriftWriteParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = ContractWriteOptions & { + abi: TAbi; + address: string; + fn: TFunctionName; + onMined?: (receipt?: TransactionReceipt) => void; +} & (FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }); + +function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { + return "readWriteContract" in adapter; +} + +// Drift Cache // + +type DriftCache = T & { + invalidateRead>( + params: DriftReadParams, + ): void; +}; + +function createDriftCache( + cache: T, + adapter: Adapter, +): DriftCache { + const cachePrototype = Object.getPrototypeOf(cache); + const newCache: T = Object.create(cachePrototype); + return Object.assign(newCache, cache, { + invalidateRead>({ + abi, + address, + args, + fn, + ...options + }: DriftReadParams) { + // TODO: Untie cache key schema from factory function to avoid creating a + // whole contract just to delete a cache entry. + createCachedReadContract({ + contract: adapter.readContract(abi, address), + cache, + }).deleteRead(fn, args, options); + }, + }); +} diff --git a/packages/drift/src/drift/MockDrift.test.ts b/packages/drift/src/drift/MockDrift.test.ts new file mode 100644 index 00000000..b723fe98 --- /dev/null +++ b/packages/drift/src/drift/MockDrift.test.ts @@ -0,0 +1,27 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { MockDrift } from "src/drift/MockDrift"; +import { describe, expect, it } from "vitest"; + +describe("MockDrift", () => { + it("Creates mock read-write contracts", async () => { + const mockDrift = new MockDrift(); + const mockContract = mockDrift.contract({ + abi: IERC20.abi, + address: "0xVaultAddress", + }); + + mockContract.stubRead({ + functionName: "symbol", + value: "FOO", + }); + expect(await mockContract.read("symbol")).toBe("FOO"); + + mockContract.stubWrite("approve", "0xHash"); + expect( + await mockContract.write("approve", { + spender: "0x1", + value: 100n, + }), + ).toBe("0xHash"); + }); +}); diff --git a/packages/drift/src/drift/MockDrift.ts b/packages/drift/src/drift/MockDrift.ts new file mode 100644 index 00000000..fd8334d7 --- /dev/null +++ b/packages/drift/src/drift/MockDrift.ts @@ -0,0 +1,15 @@ +import type { Abi } from "abitype"; +import { MockAdapter } from "src/adapter/MockAdapter"; +import type { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; +import type { CachedReadWriteContract } from "src/contract/types/CachedContract"; +import { type ContractParams, Drift } from "src/drift/Drift"; + +export class MockDrift extends Drift { + constructor() { + super(new MockAdapter()); + } + + declare contract: ( + params: ContractParams, + ) => CachedReadWriteContract & ReadWriteContractStub; +} From 800c76bff02b8ed5042bc2fabd8d19cadca65574 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 30 Sep 2024 22:37:55 -0500 Subject: [PATCH 15/49] Rename base to utils, add extendInstance, add RequiredKeys --- .../drift/src/adapter/MockAdapter.test.ts | 2 +- packages/drift/src/adapter/types.ts | 8 +++---- packages/drift/src/base/types.ts | 7 ------ .../createCachedReadContract.test.ts | 4 ++-- .../contract/stubs/ReadContractStub.test.ts | 4 ++-- .../stubs/ReadWriteContractStub.test.ts | 2 +- .../contract/stubs/ReadWriteContractStub.ts | 2 +- packages/drift/src/contract/types/AbiEntry.ts | 4 ++-- .../src/contract/types/CachedContract.ts | 6 +---- packages/drift/src/contract/types/Contract.ts | 2 +- .../contract/utils/arrayToFriendly.test.ts | 2 +- .../src/contract/utils/arrayToObject.test.ts | 2 +- .../src/contract/utils/objectToArray.test.ts | 2 +- packages/drift/src/drift/MockDrift.test.ts | 2 +- .../src/network/stubs/NetworkStub.test.ts | 2 +- packages/drift/src/utils/extendInstance.ts | 11 ++++++++++ .../src/{base => utils}/testing/IERC20.ts | 0 .../src/{base => utils}/testing/accounts.ts | 0 packages/drift/src/utils/types.ts | 22 +++++++++++++++++++ 19 files changed, 52 insertions(+), 32 deletions(-) delete mode 100644 packages/drift/src/base/types.ts create mode 100644 packages/drift/src/utils/extendInstance.ts rename packages/drift/src/{base => utils}/testing/IERC20.ts (100%) rename packages/drift/src/{base => utils}/testing/accounts.ts (100%) create mode 100644 packages/drift/src/utils/types.ts diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index 8c80a113..eb4e6d23 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -1,5 +1,5 @@ import { MockAdapter } from "src/adapter/MockAdapter"; -import { IERC20 } from "src/base/testing/IERC20"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockAdapter", () => { diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types.ts index 56a86190..5b4a561d 100644 --- a/packages/drift/src/adapter/types.ts +++ b/packages/drift/src/adapter/types.ts @@ -1,10 +1,10 @@ import type { Abi } from "abitype"; -import type { Prettify } from "src/base/types"; import type { ReadContract, ReadWriteContract, } from "src/contract/types/Contract"; import type { Network } from "src/network/types/Network"; +import type { RequiredKeys } from "src/utils/types"; export interface Adapter { network: Network; @@ -18,7 +18,5 @@ export interface Adapter { ) => ReadWriteContract; } -export type ReadWriteAdapter = Prettify< - Omit & - Pick, "readWriteContract"> ->; +export type ReadAdapter = Omit; +export type ReadWriteAdapter = RequiredKeys; diff --git a/packages/drift/src/base/types.ts b/packages/drift/src/base/types.ts deleted file mode 100644 index 31da0da4..00000000 --- a/packages/drift/src/base/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type EmptyObject = Record; - -/** - * Combines members of an intersection into a readable type. - * @see https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg - */ -export type Prettify = { [K in keyof T]: T[K] } & unknown; diff --git a/packages/drift/src/contract/factories/createCachedReadContract.test.ts b/packages/drift/src/contract/factories/createCachedReadContract.test.ts index 06e26808..69860ca2 100644 --- a/packages/drift/src/contract/factories/createCachedReadContract.test.ts +++ b/packages/drift/src/contract/factories/createCachedReadContract.test.ts @@ -1,7 +1,7 @@ -import { IERC20 } from "src/base/testing/IERC20"; -import { ALICE, BOB } from "src/base/testing/accounts"; import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; import type { Event } from "src/contract/types/Event"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { ALICE, BOB } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; import { createCachedReadContract } from "./createCachedReadContract"; diff --git a/packages/drift/src/contract/stubs/ReadContractStub.test.ts b/packages/drift/src/contract/stubs/ReadContractStub.test.ts index 89f6ced9..8ce35cf7 100644 --- a/packages/drift/src/contract/stubs/ReadContractStub.test.ts +++ b/packages/drift/src/contract/stubs/ReadContractStub.test.ts @@ -1,7 +1,7 @@ -import { IERC20 } from "src/base/testing/IERC20"; -import { ALICE, BOB, NANCY } from "src/base/testing/accounts"; import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; import type { Event } from "src/contract/types/Event"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { ALICE, BOB, NANCY } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; const ERC20ABI = IERC20.abi; diff --git a/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts b/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts index 73589b2d..dd207c54 100644 --- a/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts +++ b/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts @@ -1,5 +1,5 @@ -import { IERC20 } from "src/base/testing/IERC20"; import { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; const ERC20ABI = IERC20.abi; diff --git a/packages/drift/src/contract/stubs/ReadWriteContractStub.ts b/packages/drift/src/contract/stubs/ReadWriteContractStub.ts index a2cae13f..89df7449 100644 --- a/packages/drift/src/contract/stubs/ReadWriteContractStub.ts +++ b/packages/drift/src/contract/stubs/ReadWriteContractStub.ts @@ -1,6 +1,5 @@ import type { Abi } from "abitype"; import { type SinonStub, stub } from "sinon"; -import { BOB } from "src/base/testing/accounts"; import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; import type { ContractWriteArgs, @@ -8,6 +7,7 @@ import type { ReadWriteContract, } from "src/contract/types/Contract"; import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; +import { BOB } from "src/utils/testing/accounts"; /** * A mock implementation of a writable Ethereum contract designed for unit diff --git a/packages/drift/src/contract/types/AbiEntry.ts b/packages/drift/src/contract/types/AbiEntry.ts index f1698b1c..11010a48 100644 --- a/packages/drift/src/contract/types/AbiEntry.ts +++ b/packages/drift/src/contract/types/AbiEntry.ts @@ -3,11 +3,11 @@ import type { AbiItemType, AbiParameter, AbiParameterKind, - AbiParametersToPrimitiveTypes, AbiParameterToPrimitiveType, + AbiParametersToPrimitiveTypes, AbiStateMutability, } from "abitype"; -import type { EmptyObject, Prettify } from "src/base/types"; +import type { EmptyObject, Prettify } from "src/utils/types"; // https://docs.soliditylang.org/en/latest/abi-spec.html#json diff --git a/packages/drift/src/contract/types/CachedContract.ts b/packages/drift/src/contract/types/CachedContract.ts index 8f116935..80dd463b 100644 --- a/packages/drift/src/contract/types/CachedContract.ts +++ b/packages/drift/src/contract/types/CachedContract.ts @@ -6,6 +6,7 @@ import type { } from "src/contract/types/Contract"; import type { FunctionName } from "src/contract/types/Function"; import type { SimpleCache } from "src/exports"; +import type { DeepPartial } from "src/utils/types"; export interface CachedReadContract extends ReadContract { @@ -25,8 +26,3 @@ export interface CachedReadContract export interface CachedReadWriteContract extends CachedReadContract, ReadWriteContract {} - -/** Recursively make all properties in T partial. */ -type DeepPartial = { - [K in keyof T]?: DeepPartial; -}; diff --git a/packages/drift/src/contract/types/Contract.ts b/packages/drift/src/contract/types/Contract.ts index f485217f..3ae72efa 100644 --- a/packages/drift/src/contract/types/Contract.ts +++ b/packages/drift/src/contract/types/Contract.ts @@ -1,5 +1,4 @@ import type { Abi } from "abitype"; -import type { EmptyObject } from "src/base/types"; import type { Event, EventFilter, EventName } from "src/contract/types/Event"; import type { DecodedFunctionData, @@ -8,6 +7,7 @@ import type { FunctionReturn, } from "src/contract/types/Function"; import type { BlockTag } from "src/network/types/Block"; +import type { EmptyObject } from "src/utils/types"; // https://ethereum.github.io/execution-apis/api-documentation/ diff --git a/packages/drift/src/contract/utils/arrayToFriendly.test.ts b/packages/drift/src/contract/utils/arrayToFriendly.test.ts index 1b00fbe3..2be277a2 100644 --- a/packages/drift/src/contract/utils/arrayToFriendly.test.ts +++ b/packages/drift/src/contract/utils/arrayToFriendly.test.ts @@ -1,5 +1,5 @@ -import { IERC20 } from "src/base/testing/IERC20"; import { arrayToFriendly } from "src/contract/utils/arrayToFriendly"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("arrayToFriendly", () => { diff --git a/packages/drift/src/contract/utils/arrayToObject.test.ts b/packages/drift/src/contract/utils/arrayToObject.test.ts index 732a8110..f7095a1c 100644 --- a/packages/drift/src/contract/utils/arrayToObject.test.ts +++ b/packages/drift/src/contract/utils/arrayToObject.test.ts @@ -1,5 +1,5 @@ -import { IERC20 } from "src/base/testing/IERC20"; import { arrayToObject } from "src/contract/utils/arrayToObject"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("arrayToObject", () => { diff --git a/packages/drift/src/contract/utils/objectToArray.test.ts b/packages/drift/src/contract/utils/objectToArray.test.ts index bcaaa8b1..d2ded5ca 100644 --- a/packages/drift/src/contract/utils/objectToArray.test.ts +++ b/packages/drift/src/contract/utils/objectToArray.test.ts @@ -1,5 +1,5 @@ -import { IERC20 } from "src/base/testing/IERC20"; import { objectToArray } from "src/contract/utils/objectToArray"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("objectToArray", () => { diff --git a/packages/drift/src/drift/MockDrift.test.ts b/packages/drift/src/drift/MockDrift.test.ts index b723fe98..7b511f25 100644 --- a/packages/drift/src/drift/MockDrift.test.ts +++ b/packages/drift/src/drift/MockDrift.test.ts @@ -1,5 +1,5 @@ -import { IERC20 } from "src/base/testing/IERC20"; import { MockDrift } from "src/drift/MockDrift"; +import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockDrift", () => { diff --git a/packages/drift/src/network/stubs/NetworkStub.test.ts b/packages/drift/src/network/stubs/NetworkStub.test.ts index 2613579d..aee4dc4c 100644 --- a/packages/drift/src/network/stubs/NetworkStub.test.ts +++ b/packages/drift/src/network/stubs/NetworkStub.test.ts @@ -1,8 +1,8 @@ -import { ALICE } from "src/base/testing/accounts"; import { NetworkStub, transactionToReceipt, } from "src/network/stubs/NetworkStub"; +import { ALICE } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; import type { Transaction } from "../types/Transaction"; diff --git a/packages/drift/src/utils/extendInstance.ts b/packages/drift/src/utils/extendInstance.ts new file mode 100644 index 00000000..9b1b2d59 --- /dev/null +++ b/packages/drift/src/utils/extendInstance.ts @@ -0,0 +1,11 @@ +/** + * Extends an object instance with additional properties, returning a new object + * that maintains the prototype chain of the original instance. + */ +export function extendInstance(instance: T, extension: U): T & U { + return Object.assign( + Object.create(Object.getPrototypeOf(instance)), + instance, + extension, + ); +} diff --git a/packages/drift/src/base/testing/IERC20.ts b/packages/drift/src/utils/testing/IERC20.ts similarity index 100% rename from packages/drift/src/base/testing/IERC20.ts rename to packages/drift/src/utils/testing/IERC20.ts diff --git a/packages/drift/src/base/testing/accounts.ts b/packages/drift/src/utils/testing/accounts.ts similarity index 100% rename from packages/drift/src/base/testing/accounts.ts rename to packages/drift/src/utils/testing/accounts.ts diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts new file mode 100644 index 00000000..d5b8c4bd --- /dev/null +++ b/packages/drift/src/utils/types.ts @@ -0,0 +1,22 @@ +export type EmptyObject = Record; + +/** + * Combines members of an intersection into a readable type. + * @see https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg + */ +export type Prettify = { [K in keyof T]: T[K] } & unknown; + +/** Recursively make all properties in T partial. */ +export type DeepPartial = { + [K in keyof T]?: DeepPartial; +}; + +/** + * Make all properties in `T` whose keys are in the union `K` required and + * non-nullable. + */ +export type RequiredKeys = Prettify< + Omit & { + [P in K]-?: NonNullable; + } +>; From e6147c07d4b2b3beba7a768f4bab3cc8cdef1616 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 30 Sep 2024 22:40:53 -0500 Subject: [PATCH 16/49] Wip Drift refactors, DriftCache --- packages/drift/src/drift/Drift.ts | 192 ++++---------------- packages/drift/src/drift/DriftCache.test.ts | 51 ++++++ packages/drift/src/drift/DriftCache.ts | 76 ++++++++ packages/drift/src/drift/types.ts | 96 ++++++++++ 4 files changed, 263 insertions(+), 152 deletions(-) create mode 100644 packages/drift/src/drift/DriftCache.test.ts create mode 100644 packages/drift/src/drift/DriftCache.ts create mode 100644 packages/drift/src/drift/types.ts diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts index 525b144e..967279bf 100644 --- a/packages/drift/src/drift/Drift.ts +++ b/packages/drift/src/drift/Drift.ts @@ -1,66 +1,35 @@ import type { Abi } from "abitype"; import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; -import type { EmptyObject } from "src/base/types"; -import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; import type { SimpleCache } from "src/cache/types/SimpleCache"; import { createCachedReadContract } from "src/contract/factories/createCachedReadContract"; import { createCachedReadWriteContract } from "src/contract/factories/createCachedReadWriteContract"; +import type { FunctionName, FunctionReturn } from "src/contract/types/Function"; +import { type DriftCache, createDriftCache } from "src/drift/DriftCache"; import type { - CachedReadContract, - CachedReadWriteContract, -} from "src/contract/types/CachedContract"; -import type { - ContractReadOptions, - ContractWriteOptions, -} from "src/contract/types/Contract"; -import type { - FunctionArgs, - FunctionName, - FunctionReturn, -} from "src/contract/types/Function"; -import type { TransactionReceipt } from "src/network/types/Transaction"; - -// Drift Client // - -export interface IDrift< - TAdapter extends Adapter = Adapter, - TCache extends SimpleCache = SimpleCache, -> { - adapter: TAdapter; - cache: DriftCache; - readonly isReadWrite: TAdapter extends ReadWriteAdapter ? true : false; - - // methods // - - contract: ( - params: ContractParams, - ) => DriftContract; - - read: >( - params: DriftReadParams, - ) => Promise>; - - write: TAdapter extends ReadWriteAdapter - ? < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: DriftWriteParams, - ) => Promise - : undefined; -} + DriftContract, + DriftContractParams, + DriftReadParams, + DriftWriteParams, +} from "src/drift/types"; export interface DriftOptions { cache?: TCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; } +// This is the one place where the Read/ReadWrite distinction is skipped in +// favor of a unified entrypoint to the Drift API. export class Drift< TAdapter extends Adapter = Adapter, TCache extends SimpleCache = SimpleCache, -> implements IDrift -{ +> { readonly adapter: TAdapter; cache: DriftCache; + namespace?: string; write: TAdapter extends ReadWriteAdapter ? < TAbi extends Abi, @@ -72,42 +41,39 @@ export class Drift< constructor( adapter: TAdapter, - { - cache = createLruSimpleCache({ max: 500 }) as TCache, - }: DriftOptions = {}, + { cache, namespace }: DriftOptions = {}, ) { this.adapter = adapter; - this.cache = createDriftCache(cache, adapter); - - this.write = isReadWriteAdapter(adapter) + this.cache = createDriftCache(cache); + this.namespace = namespace; + this.write = this.isReadWrite() ? async ({ abi, address, fn, args, onMined, ...writeOptions }) => { - const txHash = await createCachedReadWriteContract({ - contract: adapter.readWriteContract(abi, address), - cache, - }).write(fn, args, writeOptions); + if (isReadWriteAdapter(this.adapter)) { + const txHash = await createCachedReadWriteContract({ + contract: this.adapter.readWriteContract(abi, address), + cache: this.cache, + }).write(fn, args, writeOptions); - if (onMined) { - adapter.network.waitForTransaction(txHash).then(onMined); - } + if (onMined) { + this.adapter.network.waitForTransaction(txHash).then(onMined); + } - return txHash; + return txHash; + } } : (undefined as any); } - get isReadWrite(): TAdapter extends ReadWriteAdapter ? true : false { - return isReadWriteAdapter(this.adapter) as any; - } - - // Static properties // + isReadWrite = (): this is Drift => + isReadWriteAdapter(this.adapter); contract = ({ abi, address, - cache, - namespace, - }: ContractParams): DriftContract => - isReadWriteAdapter(this.adapter) + cache = this.cache, + namespace = this.namespace, + }: DriftContractParams): DriftContract => + this.isReadWrite() ? createCachedReadWriteContract({ contract: this.adapter.readWriteContract(abi, address), cache, @@ -119,7 +85,10 @@ export class Drift< namespace, }) as DriftContract); - read = async >({ + read = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ abi, address, fn, @@ -135,87 +104,6 @@ export class Drift< }; } -export interface ContractParams { - abi: TAbi; - address: string; - cache?: SimpleCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -export type DriftContract< - TAbi extends Abi, - TAdapter extends Adapter = Adapter, -> = TAdapter extends ReadWriteAdapter - ? CachedReadWriteContract - : CachedReadContract; - -type DriftReadParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = ContractReadOptions & { - abi: TAbi; - address: string; - fn: TFunctionName; -} & (FunctionArgs extends EmptyObject - ? { - args?: FunctionArgs; - } - : { - args: FunctionArgs; - }); - -type DriftWriteParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = ContractWriteOptions & { - abi: TAbi; - address: string; - fn: TFunctionName; - onMined?: (receipt?: TransactionReceipt) => void; -} & (FunctionArgs extends EmptyObject - ? { - args?: FunctionArgs; - } - : { - args: FunctionArgs; - }); - function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { return "readWriteContract" in adapter; } - -// Drift Cache // - -type DriftCache = T & { - invalidateRead>( - params: DriftReadParams, - ): void; -}; - -function createDriftCache( - cache: T, - adapter: Adapter, -): DriftCache { - const cachePrototype = Object.getPrototypeOf(cache); - const newCache: T = Object.create(cachePrototype); - return Object.assign(newCache, cache, { - invalidateRead>({ - abi, - address, - args, - fn, - ...options - }: DriftReadParams) { - // TODO: Untie cache key schema from factory function to avoid creating a - // whole contract just to delete a cache entry. - createCachedReadContract({ - contract: adapter.readContract(abi, address), - cache, - }).deleteRead(fn, args, options); - }, - }); -} diff --git a/packages/drift/src/drift/DriftCache.test.ts b/packages/drift/src/drift/DriftCache.test.ts new file mode 100644 index 00000000..9989989e --- /dev/null +++ b/packages/drift/src/drift/DriftCache.test.ts @@ -0,0 +1,51 @@ +import { createDriftCache } from "src/drift/DriftCache"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { describe, expect, it } from "vitest"; + +describe("createDriftCache", () => { + it("Invalidates reads by their read key", () => { + const driftCache = createDriftCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + spender: "0xSpender", + }, + } as const; + + const key = driftCache.readKey(params); + const value = 100n; + driftCache.set(key, value); + + expect(driftCache.get(key)).toEqual(value); + + driftCache.invalidateRead(params); + + expect(driftCache.get(key)).toBeUndefined(); + }); + + it("Invalidates reads matching a partial read key", () => { + const driftCache = createDriftCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + spender: "0xSpender", + }, + } as const; + + const key = driftCache.readKey(params); + const value = 100n; + driftCache.set(key, value); + + expect(driftCache.get(key)).toEqual(value); + + driftCache.invalidateReadsMatching({ address: "0xContract" }); + + expect(driftCache.get(key)).toBeUndefined(); + }); +}); diff --git a/packages/drift/src/drift/DriftCache.ts b/packages/drift/src/drift/DriftCache.ts new file mode 100644 index 00000000..8fbbf6f4 --- /dev/null +++ b/packages/drift/src/drift/DriftCache.ts @@ -0,0 +1,76 @@ +import type { Abi } from "abitype"; +import isMatch from "lodash.ismatch"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; +import { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; +import type { EventName } from "src/contract/types/Event"; +import type { FunctionName } from "src/contract/types/Function"; +import type { DriftGetEventsParams, DriftReadParams } from "src/drift/types"; +import { createLruSimpleCache } from "src/exports"; +import { extendInstance } from "src/utils/extendInstance"; +import type { DeepPartial } from "src/utils/types"; + +export type DriftCache = T & { + // Key Management // + + partialReadKey>( + params: DeepPartial, "cache">>, + ): SimpleCacheKey; + + readKey>( + params: Omit, "cache">, + ): SimpleCacheKey; + + eventsKey>( + params: Omit, "cache">, + ): SimpleCacheKey; + + // Cache Management // + + invalidateRead>( + params: DriftReadParams, + ): void; + + invalidateReadsMatching< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: DeepPartial>): void; +}; + +/** + * Extends a {@linkcode SimpleCache} with additional API methods for use with + * Drift clients. + */ +export function createDriftCache( + cache: T = createLruSimpleCache({ max: 500 }) as T, +): DriftCache { + const driftCache: DriftCache = extendInstance< + T, + Omit + >(cache, { + partialReadKey: ({ abi, namespace, ...params }) => + createSimpleCacheKey([namespace, "read", params]), + + readKey: (params) => driftCache.partialReadKey(params), + + eventsKey: ({ abi, namespace, ...params }) => + createSimpleCacheKey([namespace, "events", params]), + + invalidateRead: ({ cache: targetCache = cache, ...params }) => + targetCache.delete(driftCache.readKey(params)), + + invalidateReadsMatching({ cache: targetCache = cache, ...params }) { + const sourceKey = driftCache.partialReadKey(params); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SimpleCacheKey[]) + ) { + cache.delete(key); + } + } + }, + }); + + return driftCache; +} diff --git a/packages/drift/src/drift/types.ts b/packages/drift/src/drift/types.ts new file mode 100644 index 00000000..3f418659 --- /dev/null +++ b/packages/drift/src/drift/types.ts @@ -0,0 +1,96 @@ +import type { Abi } from "abitype"; +import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; +import type { SimpleCache } from "src/cache/types/SimpleCache"; +import type { + CachedReadContract, + CachedReadWriteContract, +} from "src/contract/types/CachedContract"; +import type { + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, +} from "src/contract/types/Contract"; +import type { EventName } from "src/contract/types/Event"; +import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; +import type { TransactionReceipt } from "src/network/types/Transaction"; +import type { EmptyObject } from "src/utils/types"; + +export type DriftContract< + TAbi extends Abi, + TAdapter extends Adapter = Adapter, +> = TAdapter extends ReadWriteAdapter + ? CachedReadWriteContract + : CachedReadContract; + +export interface DriftContractParams { + abi: TAbi; + address: string; + cache?: SimpleCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; +} + +export type DriftReadParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = { + fn: TFunctionName; +} & (FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }) & + ContractReadOptions & + DriftContractParams; + +export interface DriftGetEventsParams< + TAbi extends Abi, + TEventName extends EventName, +> extends ContractGetEventsOptions, + DriftContractParams { + event: TEventName; +} + +export type DriftWriteParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = ContractWriteOptions & { + abi: TAbi; + address: string; + fn: TFunctionName; + onMined?: (receipt?: TransactionReceipt) => void; +} & (FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }); + +export type DriftEncodeFunctionDataParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = { + abi: TAbi; + fn: TFunctionName; +} & (FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }); + +export interface DriftDecodeFunctionDataParams< + TAbi extends Abi, + TFunctionName extends FunctionName, +> { + abi: TAbi; + data: string; + fn?: TFunctionName; +} From 96deb065a82e5f7e0458830157e2a41894493319 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 30 Sep 2024 23:25:17 -0500 Subject: [PATCH 17/49] Add preload methods to DriftCache --- packages/drift/src/drift/DriftCache.test.ts | 65 ++++++++++++++++----- packages/drift/src/drift/DriftCache.ts | 48 ++++++++++++--- 2 files changed, 91 insertions(+), 22 deletions(-) diff --git a/packages/drift/src/drift/DriftCache.test.ts b/packages/drift/src/drift/DriftCache.test.ts index 9989989e..613e47fa 100644 --- a/packages/drift/src/drift/DriftCache.test.ts +++ b/packages/drift/src/drift/DriftCache.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest"; describe("createDriftCache", () => { it("Invalidates reads by their read key", () => { - const driftCache = createDriftCache(); + const cache = createDriftCache(); const params = { abi: IERC20.abi, address: "0xContract", @@ -14,20 +14,18 @@ describe("createDriftCache", () => { spender: "0xSpender", }, } as const; - - const key = driftCache.readKey(params); + const key = cache.readKey(params); const value = 100n; - driftCache.set(key, value); - - expect(driftCache.get(key)).toEqual(value); - driftCache.invalidateRead(params); + cache.set(key, value); + expect(cache.get(key)).toEqual(value); - expect(driftCache.get(key)).toBeUndefined(); + cache.invalidateRead(params); + expect(cache.get(key)).toBeUndefined(); }); it("Invalidates reads matching a partial read key", () => { - const driftCache = createDriftCache(); + const cache = createDriftCache(); const params = { abi: IERC20.abi, address: "0xContract", @@ -37,15 +35,54 @@ describe("createDriftCache", () => { spender: "0xSpender", }, } as const; + const key = cache.readKey(params); + const value = 100n; + + cache.set(key, value); + expect(cache.get(key)).toEqual(value); + + cache.invalidateReadsMatching({ address: "0xContract" }); + expect(cache.get(key)).toBeUndefined(); + }); - const key = driftCache.readKey(params); + it("Preloads reads by their key", async () => { + const cache = createDriftCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + spender: "0xSpender", + }, + } as const; + const key = cache.readKey(params); const value = 100n; - driftCache.set(key, value); - expect(driftCache.get(key)).toEqual(value); + cache.preloadRead({ value, ...params }); + expect(cache.get(key)).toEqual(value); + }); - driftCache.invalidateReadsMatching({ address: "0xContract" }); + it("Preloads events by their key", async () => { + const cache = createDriftCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + event: "Approval", + } as const; + const key = cache.eventsKey(params); + const value = [ + { + eventName: "Approval", + args: { + owner: "0xOwner", + spender: "0xSpender", + value: 100n, + }, + }, + ] as const; - expect(driftCache.get(key)).toBeUndefined(); + cache.preloadEvents({ value, ...params }); + expect(cache.get(key)).toEqual(value); }); }); diff --git a/packages/drift/src/drift/DriftCache.ts b/packages/drift/src/drift/DriftCache.ts index 8fbbf6f4..8ae0e476 100644 --- a/packages/drift/src/drift/DriftCache.ts +++ b/packages/drift/src/drift/DriftCache.ts @@ -2,40 +2,64 @@ import type { Abi } from "abitype"; import isMatch from "lodash.ismatch"; import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; import { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; -import type { EventName } from "src/contract/types/Event"; -import type { FunctionName } from "src/contract/types/Function"; +import type { Event, EventName } from "src/contract/types/Event"; +import type { FunctionName, FunctionReturn } from "src/contract/types/Function"; import type { DriftGetEventsParams, DriftReadParams } from "src/drift/types"; import { createLruSimpleCache } from "src/exports"; import { extendInstance } from "src/utils/extendInstance"; import type { DeepPartial } from "src/utils/types"; export type DriftCache = T & { - // Key Management // + // Key Generators // partialReadKey>( - params: DeepPartial, "cache">>, + params: DeepPartial>, ): SimpleCacheKey; readKey>( - params: Omit, "cache">, + params: DriftReadKeyParams, ): SimpleCacheKey; eventsKey>( - params: Omit, "cache">, + params: DriftEventsKeyParams, ): SimpleCacheKey; // Cache Management // + preloadRead>( + params: DriftReadParams & { + value: FunctionReturn; + }, + ): void | Promise; + invalidateRead>( params: DriftReadParams, - ): void; + ): void | Promise; invalidateReadsMatching< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: DeepPartial>): void; + >( + params: DeepPartial>, + ): void | Promise; + + preloadEvents>( + params: DriftGetEventsParams & { + value: readonly Event[]; + }, + ): void | Promise; }; +export type DriftReadKeyParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = Omit, "cache">; + +export type DriftEventsKeyParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = Omit, "cache">; + /** * Extends a {@linkcode SimpleCache} with additional API methods for use with * Drift clients. @@ -55,6 +79,10 @@ export function createDriftCache( eventsKey: ({ abi, namespace, ...params }) => createSimpleCacheKey([namespace, "events", params]), + preloadRead: ({ cache: targetCache = cache, value, ...params }) => { + targetCache.set(driftCache.readKey(params as DriftReadKeyParams), value); + }, + invalidateRead: ({ cache: targetCache = cache, ...params }) => targetCache.delete(driftCache.readKey(params)), @@ -70,6 +98,10 @@ export function createDriftCache( } } }, + + preloadEvents: ({ cache: targetCache = cache, value, ...params }) => { + targetCache.set(driftCache.eventsKey(params), value); + }, }); return driftCache; From 640a7f3d3db76b13aa5d736d6ebbd17db96ac0a9 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 30 Sep 2024 23:33:32 -0500 Subject: [PATCH 18/49] Use key params for all cache methods --- packages/drift/src/drift/DriftCache.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/drift/src/drift/DriftCache.ts b/packages/drift/src/drift/DriftCache.ts index 8ae0e476..707fbd42 100644 --- a/packages/drift/src/drift/DriftCache.ts +++ b/packages/drift/src/drift/DriftCache.ts @@ -27,24 +27,24 @@ export type DriftCache = T & { // Cache Management // preloadRead>( - params: DriftReadParams & { + params: DriftReadKeyParams & { value: FunctionReturn; }, ): void | Promise; invalidateRead>( - params: DriftReadParams, + params: DriftReadKeyParams, ): void | Promise; invalidateReadsMatching< TAbi extends Abi, TFunctionName extends FunctionName, >( - params: DeepPartial>, + params: DeepPartial>, ): void | Promise; preloadEvents>( - params: DriftGetEventsParams & { + params: DriftEventsKeyParams & { value: readonly Event[]; }, ): void | Promise; @@ -79,14 +79,12 @@ export function createDriftCache( eventsKey: ({ abi, namespace, ...params }) => createSimpleCacheKey([namespace, "events", params]), - preloadRead: ({ cache: targetCache = cache, value, ...params }) => { - targetCache.set(driftCache.readKey(params as DriftReadKeyParams), value); - }, + preloadRead: ({ value, ...params }) => + cache.set(driftCache.readKey(params as DriftReadKeyParams), value), - invalidateRead: ({ cache: targetCache = cache, ...params }) => - targetCache.delete(driftCache.readKey(params)), + invalidateRead: (params) => cache.delete(driftCache.readKey(params)), - invalidateReadsMatching({ cache: targetCache = cache, ...params }) { + invalidateReadsMatching(params) { const sourceKey = driftCache.partialReadKey(params); for (const [key] of cache.entries) { @@ -99,9 +97,8 @@ export function createDriftCache( } }, - preloadEvents: ({ cache: targetCache = cache, value, ...params }) => { - targetCache.set(driftCache.eventsKey(params), value); - }, + preloadEvents: ({ value, ...params }) => + cache.set(driftCache.eventsKey(params), value), }); return driftCache; From 6cd5165c8505258ef8da400866e12b720e851470 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Tue, 1 Oct 2024 13:52:51 -0500 Subject: [PATCH 19/49] Wip --- packages/drift/src/adapter/MockAdapter.ts | 8 +- .../contract/stubs/ReadContractStub.test.ts | 4 +- .../contract/stubs/ReadContractStub.ts | 10 +- .../stubs/ReadWriteContractStub.test.ts | 2 +- .../contract/stubs/ReadWriteContractStub.ts | 13 +- .../{ => adapter}/contract/types/AbiEntry.ts | 0 .../{ => adapter}/contract/types/Contract.ts | 20 +-- .../src/{ => adapter}/contract/types/Event.ts | 2 +- .../{ => adapter}/contract/types/Function.ts | 2 +- .../contract/utils/arrayToFriendly.test.ts | 2 +- .../contract/utils/arrayToFriendly.ts | 4 +- .../contract/utils/arrayToObject.test.ts | 2 +- .../contract/utils/arrayToObject.ts | 4 +- .../contract/utils/getAbiEntry.ts | 5 +- .../contract/utils/objectToArray.test.ts | 2 +- .../contract/utils/objectToArray.ts | 4 +- .../network/AdapterNetwork.ts} | 6 +- .../types => adapter/network}/Block.ts | 0 .../network/MockNetwork.test.ts} | 20 +-- .../network/MockNetwork.ts} | 10 +- .../types => adapter/network}/Transaction.ts | 0 packages/drift/src/adapter/types.ts | 14 +- .../DriftCache/createDriftCache.test.ts} | 2 +- .../src/cache/DriftCache/createDriftCache.ts | 53 ++++++++ .../DriftCache/types.ts} | 62 ++------- .../createLruSimpleCache.ts | 2 +- .../createSimpleCacheKey.ts | 2 +- .../SimpleCache.ts => SimpleCache/types.ts} | 0 packages/drift/src/cache/utils/DriftCache.ts | 22 ++++ .../utils}/createCachedReadContract.test.ts | 6 +- .../utils}/createCachedReadContract.ts | 12 +- .../utils}/createCachedReadWriteContract.ts | 10 +- packages/drift/src/cache/utils/eventsKey.ts | 16 +++ .../drift/src/cache/utils/invalidateRead.ts | 15 +++ .../cache/utils/invalidateReadsMatching.ts | 26 ++++ .../drift/src/cache/utils/partialReadKey.ts | 17 +++ packages/drift/src/cache/utils/preloadRead.ts | 20 +++ packages/drift/src/cache/utils/readKey.ts | 12 ++ .../contract/{types => }/CachedContract.ts | 12 +- packages/drift/src/drift/Drift.ts | 121 ++++++++++++++++-- packages/drift/src/drift/MockDrift.test.ts | 8 ++ packages/drift/src/drift/MockDrift.ts | 17 ++- packages/drift/src/exports/cache.ts | 6 +- packages/drift/src/exports/contract.ts | 6 +- packages/drift/src/{drift => }/types.ts | 41 +++--- 45 files changed, 435 insertions(+), 187 deletions(-) rename packages/drift/src/{ => adapter}/contract/stubs/ReadContractStub.test.ts (97%) rename packages/drift/src/{ => adapter}/contract/stubs/ReadContractStub.ts (97%) rename packages/drift/src/{ => adapter}/contract/stubs/ReadWriteContractStub.test.ts (87%) rename packages/drift/src/{ => adapter}/contract/stubs/ReadWriteContractStub.ts (91%) rename packages/drift/src/{ => adapter}/contract/types/AbiEntry.ts (100%) rename packages/drift/src/{ => adapter}/contract/types/Contract.ts (92%) rename packages/drift/src/{ => adapter}/contract/types/Event.ts (97%) rename packages/drift/src/{ => adapter}/contract/types/Function.ts (97%) rename packages/drift/src/{ => adapter}/contract/utils/arrayToFriendly.test.ts (95%) rename packages/drift/src/{ => adapter}/contract/utils/arrayToFriendly.ts (95%) rename packages/drift/src/{ => adapter}/contract/utils/arrayToObject.test.ts (94%) rename packages/drift/src/{ => adapter}/contract/utils/arrayToObject.ts (95%) rename packages/drift/src/{ => adapter}/contract/utils/getAbiEntry.ts (89%) rename packages/drift/src/{ => adapter}/contract/utils/objectToArray.test.ts (94%) rename packages/drift/src/{ => adapter}/contract/utils/objectToArray.ts (95%) rename packages/drift/src/{network/types/Network.ts => adapter/network/AdapterNetwork.ts} (92%) rename packages/drift/src/{network/types => adapter/network}/Block.ts (100%) rename packages/drift/src/{network/stubs/NetworkStub.test.ts => adapter/network/MockNetwork.test.ts} (84%) rename packages/drift/src/{network/stubs/NetworkStub.ts => adapter/network/MockNetwork.ts} (95%) rename packages/drift/src/{network/types => adapter/network}/Transaction.ts (100%) rename packages/drift/src/{drift/DriftCache.test.ts => cache/DriftCache/createDriftCache.test.ts} (96%) create mode 100644 packages/drift/src/cache/DriftCache/createDriftCache.ts rename packages/drift/src/{drift/DriftCache.ts => cache/DriftCache/types.ts} (50%) rename packages/drift/src/cache/{factories => SimpleCache}/createLruSimpleCache.ts (95%) rename packages/drift/src/cache/{utils => SimpleCache}/createSimpleCacheKey.ts (96%) rename packages/drift/src/cache/{types/SimpleCache.ts => SimpleCache/types.ts} (100%) create mode 100644 packages/drift/src/cache/utils/DriftCache.ts rename packages/drift/src/{contract/factories => cache/utils}/createCachedReadContract.test.ts (95%) rename packages/drift/src/{contract/factories => cache/utils}/createCachedReadContract.ts (91%) rename packages/drift/src/{contract/factories => cache/utils}/createCachedReadWriteContract.ts (83%) create mode 100644 packages/drift/src/cache/utils/eventsKey.ts create mode 100644 packages/drift/src/cache/utils/invalidateRead.ts create mode 100644 packages/drift/src/cache/utils/invalidateReadsMatching.ts create mode 100644 packages/drift/src/cache/utils/partialReadKey.ts create mode 100644 packages/drift/src/cache/utils/preloadRead.ts create mode 100644 packages/drift/src/cache/utils/readKey.ts rename packages/drift/src/contract/{types => }/CachedContract.ts (74%) rename packages/drift/src/{drift => }/types.ts (62%) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 89bd7b94..9b66f68e 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -1,11 +1,11 @@ import type { Abi } from "abitype"; +import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; +import { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; +import { MockNetwork } from "src/adapter/network/MockNetwork"; import type { ReadWriteAdapter } from "src/adapter/types"; -import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; -import { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; -import { NetworkStub } from "src/network/stubs/NetworkStub"; export class MockAdapter implements ReadWriteAdapter { - network = new NetworkStub(); + network = new MockNetwork(); readContract = (abi: TAbi) => new ReadContractStub(abi); readWriteContract = (abi: TAbi) => new ReadWriteContractStub(abi); diff --git a/packages/drift/src/contract/stubs/ReadContractStub.test.ts b/packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts similarity index 97% rename from packages/drift/src/contract/stubs/ReadContractStub.test.ts rename to packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts index 8ce35cf7..ce5d2253 100644 --- a/packages/drift/src/contract/stubs/ReadContractStub.test.ts +++ b/packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts @@ -1,5 +1,5 @@ -import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; -import type { Event } from "src/contract/types/Event"; +import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; +import type { Event } from "src/adapter/contract/types/Event"; import { IERC20 } from "src/utils/testing/IERC20"; import { ALICE, BOB, NANCY } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/contract/stubs/ReadContractStub.ts b/packages/drift/src/adapter/contract/stubs/ReadContractStub.ts similarity index 97% rename from packages/drift/src/contract/stubs/ReadContractStub.ts rename to packages/drift/src/adapter/contract/stubs/ReadContractStub.ts index 813428b6..1fa981f6 100644 --- a/packages/drift/src/contract/stubs/ReadContractStub.ts +++ b/packages/drift/src/adapter/contract/stubs/ReadContractStub.ts @@ -2,6 +2,7 @@ import type { Abi } from "abitype"; import stringify from "fast-safe-stringify"; import { type SinonStub, stub } from "sinon"; import type { + AdapterReadContract, ContractDecodeFunctionDataArgs, ContractEncodeFunctionDataArgs, ContractGetEventsArgs, @@ -10,15 +11,14 @@ import type { ContractReadOptions, ContractWriteArgs, ContractWriteOptions, - ReadContract, -} from "src/contract/types/Contract"; -import type { Event, EventName } from "src/contract/types/Event"; +} from "src/adapter/contract/types/Contract"; +import type { Event, EventName } from "src/adapter/contract/types/Event"; import type { DecodedFunctionData, FunctionArgs, FunctionName, FunctionReturn, -} from "src/contract/types/Function"; +} from "src/adapter/contract/types/Function"; /** * A mock implementation of a `ReadContract` designed to facilitate unit @@ -34,7 +34,7 @@ import type { * */ export class ReadContractStub - implements ReadContract + implements AdapterReadContract { abi; address = "0x0000000000000000000000000000000000000000" as const; diff --git a/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts similarity index 87% rename from packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts rename to packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts index dd207c54..b33b28df 100644 --- a/packages/drift/src/contract/stubs/ReadWriteContractStub.test.ts +++ b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts @@ -1,4 +1,4 @@ -import { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; +import { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/contract/stubs/ReadWriteContractStub.ts b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts similarity index 91% rename from packages/drift/src/contract/stubs/ReadWriteContractStub.ts rename to packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts index 89df7449..e0fb0b28 100644 --- a/packages/drift/src/contract/stubs/ReadWriteContractStub.ts +++ b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts @@ -1,12 +1,15 @@ import type { Abi } from "abitype"; import { type SinonStub, stub } from "sinon"; -import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; import type { + AdapterReadWriteContract, ContractWriteArgs, ContractWriteOptions, - ReadWriteContract, -} from "src/contract/types/Contract"; -import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; +} from "src/adapter/contract/types/Contract"; +import type { + FunctionArgs, + FunctionName, +} from "src/adapter/contract/types/Function"; import { BOB } from "src/utils/testing/accounts"; /** @@ -27,7 +30,7 @@ import { BOB } from "src/utils/testing/accounts"; */ export class ReadWriteContractStub extends ReadContractStub - implements ReadWriteContract + implements AdapterReadWriteContract { protected writeStubMap = new Map< FunctionName, diff --git a/packages/drift/src/contract/types/AbiEntry.ts b/packages/drift/src/adapter/contract/types/AbiEntry.ts similarity index 100% rename from packages/drift/src/contract/types/AbiEntry.ts rename to packages/drift/src/adapter/contract/types/AbiEntry.ts diff --git a/packages/drift/src/contract/types/Contract.ts b/packages/drift/src/adapter/contract/types/Contract.ts similarity index 92% rename from packages/drift/src/contract/types/Contract.ts rename to packages/drift/src/adapter/contract/types/Contract.ts index 3ae72efa..eb9497f4 100644 --- a/packages/drift/src/contract/types/Contract.ts +++ b/packages/drift/src/adapter/contract/types/Contract.ts @@ -1,12 +1,16 @@ import type { Abi } from "abitype"; -import type { Event, EventFilter, EventName } from "src/contract/types/Event"; +import type { + Event, + EventFilter, + EventName, +} from "src/adapter/contract/types/Event"; import type { DecodedFunctionData, FunctionArgs, FunctionName, FunctionReturn, -} from "src/contract/types/Function"; -import type { BlockTag } from "src/network/types/Block"; +} from "src/adapter/contract/types/Function"; +import type { BlockTag } from "src/adapter/network/Block"; import type { EmptyObject } from "src/utils/types"; // https://ethereum.github.io/execution-apis/api-documentation/ @@ -15,7 +19,7 @@ import type { EmptyObject } from "src/utils/types"; * Interface representing a readable contract with specified ABI. Provides type * safe methods to read and simulate write operations on the contract. */ -export interface ReadContract { +export interface AdapterReadContract { abi: TAbi; address: `0x${string}`; @@ -61,11 +65,11 @@ export interface ReadContract { } /** - * Interface representing a writable contract with specified ABI. - * Extends IReadContract to also include write operations. + * Interface representing a writable contract with specified ABI. Extends + * IReadContract to also include write operations. */ -export interface ReadWriteContract - extends ReadContract { +export interface AdapterReadWriteContract + extends AdapterReadContract { /** * Get the address of the signer for this contract. */ diff --git a/packages/drift/src/contract/types/Event.ts b/packages/drift/src/adapter/contract/types/Event.ts similarity index 97% rename from packages/drift/src/contract/types/Event.ts rename to packages/drift/src/adapter/contract/types/Event.ts index a4070bed..0d73256e 100644 --- a/packages/drift/src/contract/types/Event.ts +++ b/packages/drift/src/adapter/contract/types/Event.ts @@ -5,7 +5,7 @@ import type { AbiParameters, AbiParametersToObject, NamedAbiParameter, -} from "src/contract/types/AbiEntry"; +} from "src/adapter/contract/types/AbiEntry"; /** * Get a union of event names from an abi diff --git a/packages/drift/src/contract/types/Function.ts b/packages/drift/src/adapter/contract/types/Function.ts similarity index 97% rename from packages/drift/src/contract/types/Function.ts rename to packages/drift/src/adapter/contract/types/Function.ts index 00f2e63d..f2eaadf1 100644 --- a/packages/drift/src/contract/types/Function.ts +++ b/packages/drift/src/adapter/contract/types/Function.ts @@ -2,7 +2,7 @@ import type { Abi, AbiStateMutability } from "abitype"; import type { AbiFriendlyType, AbiObjectType, -} from "src/contract/types/AbiEntry"; +} from "src/adapter/contract/types/AbiEntry"; /** * Get a union of function names from an abi diff --git a/packages/drift/src/contract/utils/arrayToFriendly.test.ts b/packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts similarity index 95% rename from packages/drift/src/contract/utils/arrayToFriendly.test.ts rename to packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts index 2be277a2..ccfda0b4 100644 --- a/packages/drift/src/contract/utils/arrayToFriendly.test.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts @@ -1,4 +1,4 @@ -import { arrayToFriendly } from "src/contract/utils/arrayToFriendly"; +import { arrayToFriendly } from "src/adapter/contract/utils/arrayToFriendly"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/contract/utils/arrayToFriendly.ts b/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts similarity index 95% rename from packages/drift/src/contract/utils/arrayToFriendly.ts rename to packages/drift/src/adapter/contract/utils/arrayToFriendly.ts index a4c99b2a..b5fb887b 100644 --- a/packages/drift/src/contract/utils/arrayToFriendly.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiFriendlyType, -} from "src/contract/types/AbiEntry"; -import { getAbiEntry } from "src/contract/utils/getAbiEntry"; +} from "src/adapter/contract/types/AbiEntry"; +import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** * Converts an array of input or output values into an diff --git a/packages/drift/src/contract/utils/arrayToObject.test.ts b/packages/drift/src/adapter/contract/utils/arrayToObject.test.ts similarity index 94% rename from packages/drift/src/contract/utils/arrayToObject.test.ts rename to packages/drift/src/adapter/contract/utils/arrayToObject.test.ts index f7095a1c..1c520c5f 100644 --- a/packages/drift/src/contract/utils/arrayToObject.test.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToObject.test.ts @@ -1,4 +1,4 @@ -import { arrayToObject } from "src/contract/utils/arrayToObject"; +import { arrayToObject } from "src/adapter/contract/utils/arrayToObject"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/contract/utils/arrayToObject.ts b/packages/drift/src/adapter/contract/utils/arrayToObject.ts similarity index 95% rename from packages/drift/src/contract/utils/arrayToObject.ts rename to packages/drift/src/adapter/contract/utils/arrayToObject.ts index d9399b95..66d9eaba 100644 --- a/packages/drift/src/contract/utils/arrayToObject.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToObject.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/contract/types/AbiEntry"; -import { getAbiEntry } from "src/contract/utils/getAbiEntry"; +} from "src/adapter/contract/types/AbiEntry"; +import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** * Converts an array of input or output values into an object typ, ensuring the diff --git a/packages/drift/src/contract/utils/getAbiEntry.ts b/packages/drift/src/adapter/contract/utils/getAbiEntry.ts similarity index 89% rename from packages/drift/src/contract/utils/getAbiEntry.ts rename to packages/drift/src/adapter/contract/utils/getAbiEntry.ts index d9ae2fc5..3936d014 100644 --- a/packages/drift/src/contract/utils/getAbiEntry.ts +++ b/packages/drift/src/adapter/contract/utils/getAbiEntry.ts @@ -1,5 +1,8 @@ import type { Abi, AbiItemType } from "abitype"; -import type { AbiEntry, AbiEntryName } from "src/contract/types/AbiEntry"; +import type { + AbiEntry, + AbiEntryName, +} from "src/adapter/contract/types/AbiEntry"; import { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; /** diff --git a/packages/drift/src/contract/utils/objectToArray.test.ts b/packages/drift/src/adapter/contract/utils/objectToArray.test.ts similarity index 94% rename from packages/drift/src/contract/utils/objectToArray.test.ts rename to packages/drift/src/adapter/contract/utils/objectToArray.test.ts index d2ded5ca..e3f77513 100644 --- a/packages/drift/src/contract/utils/objectToArray.test.ts +++ b/packages/drift/src/adapter/contract/utils/objectToArray.test.ts @@ -1,4 +1,4 @@ -import { objectToArray } from "src/contract/utils/objectToArray"; +import { objectToArray } from "src/adapter/contract/utils/objectToArray"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/contract/utils/objectToArray.ts b/packages/drift/src/adapter/contract/utils/objectToArray.ts similarity index 95% rename from packages/drift/src/contract/utils/objectToArray.ts rename to packages/drift/src/adapter/contract/utils/objectToArray.ts index bce6edab..5eb6e1ec 100644 --- a/packages/drift/src/contract/utils/objectToArray.ts +++ b/packages/drift/src/adapter/contract/utils/objectToArray.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/contract/types/AbiEntry"; -import { getAbiEntry } from "src/contract/utils/getAbiEntry"; +} from "src/adapter/contract/types/AbiEntry"; +import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** * Converts an object into an array of input or output values, ensuring the the diff --git a/packages/drift/src/network/types/Network.ts b/packages/drift/src/adapter/network/AdapterNetwork.ts similarity index 92% rename from packages/drift/src/network/types/Network.ts rename to packages/drift/src/adapter/network/AdapterNetwork.ts index 8e318859..83b3af4a 100644 --- a/packages/drift/src/network/types/Network.ts +++ b/packages/drift/src/adapter/network/AdapterNetwork.ts @@ -1,15 +1,15 @@ -import type { Block, BlockTag } from "src/network/types/Block"; +import type { Block, BlockTag } from "src/adapter/network/Block"; import type { Transaction, TransactionReceipt, -} from "src/network/types/Transaction"; +} from "src/adapter/network/Transaction"; // https://ethereum.github.io/execution-apis/api-documentation/ /** * An interface representing data the SDK needs to get from the network. */ -export interface Network { +export interface AdapterNetwork { /** * Get the balance of native currency for an account. */ diff --git a/packages/drift/src/network/types/Block.ts b/packages/drift/src/adapter/network/Block.ts similarity index 100% rename from packages/drift/src/network/types/Block.ts rename to packages/drift/src/adapter/network/Block.ts diff --git a/packages/drift/src/network/stubs/NetworkStub.test.ts b/packages/drift/src/adapter/network/MockNetwork.test.ts similarity index 84% rename from packages/drift/src/network/stubs/NetworkStub.test.ts rename to packages/drift/src/adapter/network/MockNetwork.test.ts index aee4dc4c..4f4b697e 100644 --- a/packages/drift/src/network/stubs/NetworkStub.test.ts +++ b/packages/drift/src/adapter/network/MockNetwork.test.ts @@ -1,14 +1,14 @@ import { - NetworkStub, + MockNetwork, transactionToReceipt, -} from "src/network/stubs/NetworkStub"; +} from "src/adapter/network/MockNetwork"; import { ALICE } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; -import type { Transaction } from "../types/Transaction"; +import type { Transaction } from "./Transaction"; -describe("NetworkStub", () => { +describe("MockNetwork", () => { it("stubs getBalance", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); network.stubGetBalance({ args: [ALICE], @@ -21,7 +21,7 @@ describe("NetworkStub", () => { }); it("stubs getBlock", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); const block = { blockNumber: 1n, @@ -38,7 +38,7 @@ describe("NetworkStub", () => { }); it("stubs getChainId", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); network.stubGetChainId(42069); @@ -48,7 +48,7 @@ describe("NetworkStub", () => { }); it("stubs getTransaction", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); const txHash = "0x123abc"; const tx: Transaction = { @@ -71,7 +71,7 @@ describe("NetworkStub", () => { }); it("waits for stubbed transactions", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); const txHash = "0x123abc"; const stubbedTx = { @@ -101,7 +101,7 @@ describe("NetworkStub", () => { }); it("reaches timeout when waiting for transactions that are never stubbed", async () => { - const network = new NetworkStub(); + const network = new MockNetwork(); const waitPromise = await network.waitForTransaction("0x123abc", { timeout: 1000, diff --git a/packages/drift/src/network/stubs/NetworkStub.ts b/packages/drift/src/adapter/network/MockNetwork.ts similarity index 95% rename from packages/drift/src/network/stubs/NetworkStub.ts rename to packages/drift/src/adapter/network/MockNetwork.ts index b43b8bb4..b234be6f 100644 --- a/packages/drift/src/network/stubs/NetworkStub.ts +++ b/packages/drift/src/adapter/network/MockNetwork.ts @@ -1,22 +1,22 @@ import { type SinonStub, stub } from "sinon"; -import type { Block } from "src/network/types/Block"; import type { - Network, + AdapterNetwork, NetworkGetBalanceArgs, NetworkGetBlockArgs, NetworkGetTransactionArgs, NetworkWaitForTransactionArgs, -} from "src/network/types/Network"; +} from "src/adapter/network/AdapterNetwork"; +import type { Block } from "src/adapter/network/Block"; import type { Transaction, TransactionReceipt, -} from "src/network/types/Transaction"; +} from "src/adapter/network/Transaction"; /** * A mock implementation of a `Network` designed to facilitate unit * testing. */ -export class NetworkStub implements Network { +export class MockNetwork implements AdapterNetwork { protected getBalanceStub: | SinonStub<[NetworkGetBalanceArgs?], Promise> | undefined; diff --git a/packages/drift/src/network/types/Transaction.ts b/packages/drift/src/adapter/network/Transaction.ts similarity index 100% rename from packages/drift/src/network/types/Transaction.ts rename to packages/drift/src/adapter/network/Transaction.ts diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types.ts index 5b4a561d..5fb9d428 100644 --- a/packages/drift/src/adapter/types.ts +++ b/packages/drift/src/adapter/types.ts @@ -1,21 +1,21 @@ import type { Abi } from "abitype"; import type { - ReadContract, - ReadWriteContract, -} from "src/contract/types/Contract"; -import type { Network } from "src/network/types/Network"; + AdapterReadContract, + AdapterReadWriteContract, +} from "src/adapter/contract/types/Contract"; +import type { AdapterNetwork } from "src/adapter/network/AdapterNetwork"; import type { RequiredKeys } from "src/utils/types"; export interface Adapter { - network: Network; + network: AdapterNetwork; readContract: ( abi: TAbi, address: string, - ) => ReadContract; + ) => AdapterReadContract; readWriteContract?: ( abi: TAbi, address: string, - ) => ReadWriteContract; + ) => AdapterReadWriteContract; } export type ReadAdapter = Omit; diff --git a/packages/drift/src/drift/DriftCache.test.ts b/packages/drift/src/cache/DriftCache/createDriftCache.test.ts similarity index 96% rename from packages/drift/src/drift/DriftCache.test.ts rename to packages/drift/src/cache/DriftCache/createDriftCache.test.ts index 613e47fa..c31f20ae 100644 --- a/packages/drift/src/drift/DriftCache.test.ts +++ b/packages/drift/src/cache/DriftCache/createDriftCache.test.ts @@ -1,4 +1,4 @@ -import { createDriftCache } from "src/drift/DriftCache"; +import { createDriftCache } from "src/cache/DriftCache/createDriftCache"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/cache/DriftCache/createDriftCache.ts b/packages/drift/src/cache/DriftCache/createDriftCache.ts new file mode 100644 index 00000000..9e67142e --- /dev/null +++ b/packages/drift/src/cache/DriftCache/createDriftCache.ts @@ -0,0 +1,53 @@ +import isMatch from "lodash.ismatch"; +import type { + DriftCache, + DriftReadKeyParams, +} from "src/cache/DriftCache/types"; +import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; +import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +import { extendInstance } from "src/utils/extendInstance"; + +/** + * Extends a {@linkcode SimpleCache} with additional API methods for use with + * Drift clients. + */ +export function createDriftCache( + cache: T = createLruSimpleCache({ max: 500 }) as T, +): DriftCache { + const driftCache: DriftCache = extendInstance< + T, + Omit + >(cache, { + partialReadKey: ({ abi, namespace, ...params }) => + createSimpleCacheKey([namespace, "read", params]), + + readKey: (params) => driftCache.partialReadKey(params), + + eventsKey: ({ abi, namespace, ...params }) => + createSimpleCacheKey([namespace, "events", params]), + + preloadRead: ({ value, ...params }) => + cache.set(driftCache.readKey(params as DriftReadKeyParams), value), + + invalidateRead: (params) => cache.delete(driftCache.readKey(params)), + + invalidateReadsMatching(params) { + const sourceKey = driftCache.partialReadKey(params); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SimpleCacheKey[]) + ) { + cache.delete(key); + } + } + }, + + preloadEvents: ({ value, ...params }) => + cache.set(driftCache.eventsKey(params), value), + }); + + return driftCache; +} diff --git a/packages/drift/src/drift/DriftCache.ts b/packages/drift/src/cache/DriftCache/types.ts similarity index 50% rename from packages/drift/src/drift/DriftCache.ts rename to packages/drift/src/cache/DriftCache/types.ts index 707fbd42..2a87c35d 100644 --- a/packages/drift/src/drift/DriftCache.ts +++ b/packages/drift/src/cache/DriftCache/types.ts @@ -1,12 +1,14 @@ import type { Abi } from "abitype"; -import isMatch from "lodash.ismatch"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; -import { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; -import type { Event, EventName } from "src/contract/types/Event"; -import type { FunctionName, FunctionReturn } from "src/contract/types/Function"; -import type { DriftGetEventsParams, DriftReadParams } from "src/drift/types"; -import { createLruSimpleCache } from "src/exports"; -import { extendInstance } from "src/utils/extendInstance"; +import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { + FunctionName, + FunctionReturn, +} from "src/adapter/contract/types/Function"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +import type { + DriftGetEventsParams, + DriftReadParams, +} from "src/drift/types/DriftContract"; import type { DeepPartial } from "src/utils/types"; export type DriftCache = T & { @@ -59,47 +61,3 @@ export type DriftEventsKeyParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, > = Omit, "cache">; - -/** - * Extends a {@linkcode SimpleCache} with additional API methods for use with - * Drift clients. - */ -export function createDriftCache( - cache: T = createLruSimpleCache({ max: 500 }) as T, -): DriftCache { - const driftCache: DriftCache = extendInstance< - T, - Omit - >(cache, { - partialReadKey: ({ abi, namespace, ...params }) => - createSimpleCacheKey([namespace, "read", params]), - - readKey: (params) => driftCache.partialReadKey(params), - - eventsKey: ({ abi, namespace, ...params }) => - createSimpleCacheKey([namespace, "events", params]), - - preloadRead: ({ value, ...params }) => - cache.set(driftCache.readKey(params as DriftReadKeyParams), value), - - invalidateRead: (params) => cache.delete(driftCache.readKey(params)), - - invalidateReadsMatching(params) { - const sourceKey = driftCache.partialReadKey(params); - - for (const [key] of cache.entries) { - if ( - typeof key === "object" && - isMatch(key, sourceKey as SimpleCacheKey[]) - ) { - cache.delete(key); - } - } - }, - - preloadEvents: ({ value, ...params }) => - cache.set(driftCache.eventsKey(params), value), - }); - - return driftCache; -} diff --git a/packages/drift/src/cache/factories/createLruSimpleCache.ts b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts similarity index 95% rename from packages/drift/src/cache/factories/createLruSimpleCache.ts rename to packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts index 70db363d..3a55e7ae 100644 --- a/packages/drift/src/cache/factories/createLruSimpleCache.ts +++ b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts @@ -1,6 +1,6 @@ import stringify from "fast-json-stable-stringify"; import { LRUCache } from "lru-cache"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; /** * An LRU (Least Recently Used) implementation of the `SimpleCache` interface. diff --git a/packages/drift/src/cache/utils/createSimpleCacheKey.ts b/packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts similarity index 96% rename from packages/drift/src/cache/utils/createSimpleCacheKey.ts rename to packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts index 76a57bec..5fa4a8fd 100644 --- a/packages/drift/src/cache/utils/createSimpleCacheKey.ts +++ b/packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts @@ -1,4 +1,4 @@ -import type { SimpleCacheKey } from "src/cache/types/SimpleCache"; +import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; type DefinedValue = NonNullable< Record | string | number | boolean | symbol diff --git a/packages/drift/src/cache/types/SimpleCache.ts b/packages/drift/src/cache/SimpleCache/types.ts similarity index 100% rename from packages/drift/src/cache/types/SimpleCache.ts rename to packages/drift/src/cache/SimpleCache/types.ts diff --git a/packages/drift/src/cache/utils/DriftCache.ts b/packages/drift/src/cache/utils/DriftCache.ts new file mode 100644 index 00000000..f2c3da12 --- /dev/null +++ b/packages/drift/src/cache/utils/DriftCache.ts @@ -0,0 +1,22 @@ +import type { Abi } from "abitype"; +import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { + DriftEventsKeyParams +} from "src/cache/DriftCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { eventsKey } from "src/cache/utils/eventsKey"; + +export function preloadEvents< + TAbi extends Abi, + TEventName extends EventName, +>( + cache: SimpleCache, + { + value, + ...params + }: DriftEventsKeyParams & { + value: readonly Event[]; + }, +): void | Promise { + return cache.set(eventsKey(params), value); +} diff --git a/packages/drift/src/contract/factories/createCachedReadContract.test.ts b/packages/drift/src/cache/utils/createCachedReadContract.test.ts similarity index 95% rename from packages/drift/src/contract/factories/createCachedReadContract.test.ts rename to packages/drift/src/cache/utils/createCachedReadContract.test.ts index 69860ca2..9a296d2a 100644 --- a/packages/drift/src/contract/factories/createCachedReadContract.test.ts +++ b/packages/drift/src/cache/utils/createCachedReadContract.test.ts @@ -1,9 +1,9 @@ -import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; -import type { Event } from "src/contract/types/Event"; +import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; +import type { Event } from "src/adapter/contract/types/Event"; +import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; import { IERC20 } from "src/utils/testing/IERC20"; import { ALICE, BOB } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; -import { createCachedReadContract } from "./createCachedReadContract"; const ERC20ABI = IERC20.abi; diff --git a/packages/drift/src/contract/factories/createCachedReadContract.ts b/packages/drift/src/cache/utils/createCachedReadContract.ts similarity index 91% rename from packages/drift/src/contract/factories/createCachedReadContract.ts rename to packages/drift/src/cache/utils/createCachedReadContract.ts index ae51e5e9..6c219299 100644 --- a/packages/drift/src/contract/factories/createCachedReadContract.ts +++ b/packages/drift/src/cache/utils/createCachedReadContract.ts @@ -1,16 +1,16 @@ import type { Abi } from "abitype"; import isMatch from "lodash.ismatch"; -import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; -import { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; -import type { CachedReadContract } from "src/contract/types/CachedContract"; -import type { ReadContract } from "src/contract/types/Contract"; +import type { AdapterReadContract } from "src/adapter/contract/types/Contract"; +import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; +import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +import type { CachedReadContract } from "src/contract/CachedContract"; // TODO: Figure out a good default cache size const DEFAULT_CACHE_SIZE = 100; export interface CreateCachedReadContractOptions { - contract: ReadContract; + contract: AdapterReadContract; cache?: SimpleCache; /** * A namespace to distinguish this instance from others in the cache by diff --git a/packages/drift/src/contract/factories/createCachedReadWriteContract.ts b/packages/drift/src/cache/utils/createCachedReadWriteContract.ts similarity index 83% rename from packages/drift/src/contract/factories/createCachedReadWriteContract.ts rename to packages/drift/src/cache/utils/createCachedReadWriteContract.ts index 4f4a9368..099f4dac 100644 --- a/packages/drift/src/contract/factories/createCachedReadWriteContract.ts +++ b/packages/drift/src/cache/utils/createCachedReadWriteContract.ts @@ -1,14 +1,14 @@ import type { Abi } from "abitype"; +import type { AdapterReadWriteContract } from "src/adapter/contract/types/Contract"; +import type { CachedReadWriteContract } from "src/cache/types/CachedContract"; import { type CreateCachedReadContractOptions, createCachedReadContract, -} from "src/contract/factories/createCachedReadContract"; -import type { CachedReadWriteContract } from "src/contract/types/CachedContract"; -import type { ReadWriteContract } from "src/contract/types/Contract"; +} from "src/cache/utils/createCachedReadContract"; export interface CreateCachedReadWriteContractOptions extends CreateCachedReadContractOptions { - contract: ReadWriteContract; + contract: AdapterReadWriteContract; } /** @@ -40,7 +40,7 @@ export function createCachedReadWriteContract({ } function isCached( - contract: ReadWriteContract, + contract: AdapterReadWriteContract, ): contract is CachedReadWriteContract { return "clearCache" in contract; } diff --git a/packages/drift/src/cache/utils/eventsKey.ts b/packages/drift/src/cache/utils/eventsKey.ts new file mode 100644 index 00000000..1202d252 --- /dev/null +++ b/packages/drift/src/cache/utils/eventsKey.ts @@ -0,0 +1,16 @@ +import type { Abi } from "abitype"; +import type { EventName } from "src/adapter/contract/types/Event"; +import type { DriftEventsKeyParams } from "src/cache/DriftCache/types"; +import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; +import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; + +export function eventsKey< + TAbi extends Abi, + TEventName extends EventName, +>({ + abi, + namespace, + ...params +}: DriftEventsKeyParams): SimpleCacheKey { + return createSimpleCacheKey([namespace, "events", params]); +} diff --git a/packages/drift/src/cache/utils/invalidateRead.ts b/packages/drift/src/cache/utils/invalidateRead.ts new file mode 100644 index 00000000..13b1a12f --- /dev/null +++ b/packages/drift/src/cache/utils/invalidateRead.ts @@ -0,0 +1,15 @@ +import type { Abi } from "abitype"; +import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { readKey } from "src/cache/utils/readKey"; + +export function invalidateRead< + TAbi extends Abi, + TFunctionName extends FunctionName, +>( + cache: SimpleCache, + params: DriftReadKeyParams, +): void | Promise { + return cache.delete(readKey(params)); +} diff --git a/packages/drift/src/cache/utils/invalidateReadsMatching.ts b/packages/drift/src/cache/utils/invalidateReadsMatching.ts new file mode 100644 index 00000000..073df1b7 --- /dev/null +++ b/packages/drift/src/cache/utils/invalidateReadsMatching.ts @@ -0,0 +1,26 @@ +import type { Abi } from "abitype"; +import isMatch from "lodash.ismatch"; +import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +import { partialReadKey } from "src/cache/utils/partialReadKey"; +import type { DeepPartial } from "src/utils/types"; + +export function invalidateReadsMatching< + TAbi extends Abi, + TFunctionName extends FunctionName, +>( + cache: SimpleCache, + params: DeepPartial>, +): void | Promise { + const sourceKey = partialReadKey(params); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SimpleCacheKey[]) + ) { + cache.delete(key); + } + } +} diff --git a/packages/drift/src/cache/utils/partialReadKey.ts b/packages/drift/src/cache/utils/partialReadKey.ts new file mode 100644 index 00000000..933dd59d --- /dev/null +++ b/packages/drift/src/cache/utils/partialReadKey.ts @@ -0,0 +1,17 @@ +import type { Abi } from "abitype"; +import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; +import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; +import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; +import type { DeepPartial } from "src/utils/types"; + +export function partialReadKey< + TAbi extends Abi, + TFunctionName extends FunctionName, +>({ + abi, + namespace, + ...params +}: DeepPartial>): SimpleCacheKey { + return createSimpleCacheKey([namespace, "read", params]); +} diff --git a/packages/drift/src/cache/utils/preloadRead.ts b/packages/drift/src/cache/utils/preloadRead.ts new file mode 100644 index 00000000..012d1edc --- /dev/null +++ b/packages/drift/src/cache/utils/preloadRead.ts @@ -0,0 +1,20 @@ +import type { Abi } from "abitype"; +import type { FunctionName, FunctionReturn } from "src/adapter/contract/types/Function"; +import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { readKey } from "src/cache/utils/readKey"; + +export function preloadRead< + TAbi extends Abi, + TFunctionName extends FunctionName, +>( + cache: SimpleCache, + { + value, + ...params + }: DriftReadKeyParams & { + value: FunctionReturn; + }, +): void | Promise { + return cache.set(readKey(params as DriftReadKeyParams), value); +} diff --git a/packages/drift/src/cache/utils/readKey.ts b/packages/drift/src/cache/utils/readKey.ts new file mode 100644 index 00000000..7d4f41c5 --- /dev/null +++ b/packages/drift/src/cache/utils/readKey.ts @@ -0,0 +1,12 @@ +import type { Abi } from "abitype"; +import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; +import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; +import { partialReadKey } from "src/cache/utils/partialReadKey"; + +export function readKey< + TAbi extends Abi, + TFunctionName extends FunctionName, +>(params: DriftReadKeyParams): SimpleCacheKey { + return partialReadKey(params); +} diff --git a/packages/drift/src/contract/types/CachedContract.ts b/packages/drift/src/contract/CachedContract.ts similarity index 74% rename from packages/drift/src/contract/types/CachedContract.ts rename to packages/drift/src/contract/CachedContract.ts index 80dd463b..528fe5f4 100644 --- a/packages/drift/src/contract/types/CachedContract.ts +++ b/packages/drift/src/contract/CachedContract.ts @@ -1,15 +1,15 @@ import type { Abi } from "abitype"; import type { + AdapterReadContract, + AdapterReadWriteContract, ContractReadArgs, - ReadContract, - ReadWriteContract, -} from "src/contract/types/Contract"; -import type { FunctionName } from "src/contract/types/Function"; +} from "src/adapter/contract/types/Contract"; +import type { FunctionName } from "src/adapter/contract/types/Function"; import type { SimpleCache } from "src/exports"; import type { DeepPartial } from "src/utils/types"; export interface CachedReadContract - extends ReadContract { + extends AdapterReadContract { cache: SimpleCache; namespace?: string; clearCache(): void; @@ -25,4 +25,4 @@ export interface CachedReadContract export interface CachedReadWriteContract extends CachedReadContract, - ReadWriteContract {} + AdapterReadWriteContract {} diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts index 967279bf..281d35da 100644 --- a/packages/drift/src/drift/Drift.ts +++ b/packages/drift/src/drift/Drift.ts @@ -1,16 +1,35 @@ import type { Abi } from "abitype"; +import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { + DecodedFunctionData, + FunctionName, + FunctionReturn, +} from "src/adapter/contract/types/Function"; import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; -import type { SimpleCache } from "src/cache/types/SimpleCache"; -import { createCachedReadContract } from "src/contract/factories/createCachedReadContract"; -import { createCachedReadWriteContract } from "src/contract/factories/createCachedReadWriteContract"; -import type { FunctionName, FunctionReturn } from "src/contract/types/Function"; -import { type DriftCache, createDriftCache } from "src/drift/DriftCache"; +import { createDriftCache } from "src/cache/DriftCache/createDriftCache"; +import type { DriftCache } from "src/cache/DriftCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; +import { createCachedReadWriteContract } from "src/cache/utils/createCachedReadWriteContract"; +import type { + CachedReadContract, + CachedReadWriteContract, +} from "src/contract/CachedContract"; import type { - DriftContract, - DriftContractParams, - DriftReadParams, - DriftWriteParams, -} from "src/drift/types"; + ContractParams, + DecodeFunctionDataParams, + EncodeFunctionDataParams, + GetEventsParams, + ReadParams, + WriteParams, +} from "src/types"; + +export type DriftContract< + TAbi extends Abi, + TAdapter extends Adapter = Adapter, +> = TAdapter extends ReadWriteAdapter + ? CachedReadWriteContract + : CachedReadContract; export interface DriftOptions { cache?: TCache; @@ -35,7 +54,7 @@ export class Drift< TAbi extends Abi, TFunctionName extends FunctionName, >( - params: DriftWriteParams, + params: WriteParams, ) => Promise : undefined; @@ -72,7 +91,7 @@ export class Drift< address, cache = this.cache, namespace = this.namespace, - }: DriftContractParams): DriftContract => + }: ContractParams): DriftContract => this.isReadWrite() ? createCachedReadWriteContract({ contract: this.adapter.readWriteContract(abi, address), @@ -85,6 +104,9 @@ export class Drift< namespace, }) as DriftContract); + /** + * Reads a specified function from a contract. + */ read = async < TAbi extends Abi, TFunctionName extends FunctionName, @@ -94,7 +116,7 @@ export class Drift< fn, args, ...readOptions - }: DriftReadParams): Promise< + }: ReadParams): Promise< FunctionReturn > => { return createCachedReadContract({ @@ -102,6 +124,79 @@ export class Drift< cache: this.cache, }).read(fn, args, readOptions); }; + + /** + * Simulates a write operation on a specified function of a contract. + */ + simulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + address, + fn, + args, + ...writeOptions + }: WriteParams): Promise< + FunctionReturn + > { + return createCachedReadContract({ + contract: this.adapter.readContract(abi, address), + cache: this.cache, + }).simulateWrite(fn, args, writeOptions); + } + + /** + * Retrieves specified events from a contract. + */ + getEvents>({ + abi, + address, + event, + ...params + }: GetEventsParams): Promise[]> { + return createCachedReadContract({ + contract: this.adapter.readContract(abi, address), + cache: this.cache, + }).getEvents(event, params); + } + + /** + * Encodes a function call into calldata. + */ + encodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + fn, + args, + }: EncodeFunctionDataParams): `0x${string}` { + return createCachedReadContract({ + contract: this.adapter.readContract(abi, "0x0"), + cache: this.cache, + }).encodeFunctionData(fn, args); + } + + /** + * Decodes a string of function calldata into it's arguments and function + * name. + */ + decodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName = FunctionName, + >({ + abi, + data, + }: DecodeFunctionDataParams): DecodedFunctionData< + TAbi, + TFunctionName + > { + return createCachedReadContract({ + contract: this.adapter.readContract(abi, "0x0"), + cache: this.cache, + }).decodeFunctionData(data as `0x${string}`); + } } function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { diff --git a/packages/drift/src/drift/MockDrift.test.ts b/packages/drift/src/drift/MockDrift.test.ts index 7b511f25..39607970 100644 --- a/packages/drift/src/drift/MockDrift.test.ts +++ b/packages/drift/src/drift/MockDrift.test.ts @@ -16,6 +16,14 @@ describe("MockDrift", () => { }); expect(await mockContract.read("symbol")).toBe("FOO"); + // expect( + // await mockDrift.read({ + // abi: IERC20.abi, + // address: "0xVaultAddress", + // fn: "symbol", + // }), + // ).toBe("FOO"); + mockContract.stubWrite("approve", "0xHash"); expect( await mockContract.write("approve", { diff --git a/packages/drift/src/drift/MockDrift.ts b/packages/drift/src/drift/MockDrift.ts index fd8334d7..024e59dd 100644 --- a/packages/drift/src/drift/MockDrift.ts +++ b/packages/drift/src/drift/MockDrift.ts @@ -1,12 +1,17 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; -import type { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; -import type { CachedReadWriteContract } from "src/contract/types/CachedContract"; -import { type ContractParams, Drift } from "src/drift/Drift"; +import type { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; +import type { CachedReadWriteContract } from "src/contract/CachedContract"; +import { Drift, type DriftOptions } from "src/drift/Drift"; +import type { SimpleCache } from "src/exports"; +import type { ContractParams } from "src/types"; -export class MockDrift extends Drift { - constructor() { - super(new MockAdapter()); +export class MockDrift extends Drift< + MockAdapter, + TCache +> { + constructor(options?: DriftOptions) { + super(new MockAdapter(), options); } declare contract: ( diff --git a/packages/drift/src/exports/cache.ts b/packages/drift/src/exports/cache.ts index e4ea412a..1f2c8064 100644 --- a/packages/drift/src/exports/cache.ts +++ b/packages/drift/src/exports/cache.ts @@ -1,3 +1,3 @@ -export { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; -export type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; -export { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; +export { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; +export type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +export { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; diff --git a/packages/drift/src/exports/contract.ts b/packages/drift/src/exports/contract.ts index 69867527..4239fba2 100644 --- a/packages/drift/src/exports/contract.ts +++ b/packages/drift/src/exports/contract.ts @@ -2,11 +2,11 @@ export { createCachedReadContract, type CreateCachedReadContractOptions, -} from "src/contract/factories/createCachedReadContract"; +} from "src/cache/utils/createCachedReadContract"; export { createCachedReadWriteContract, type CreateCachedReadWriteContractOptions, -} from "src/contract/factories/createCachedReadWriteContract"; +} from "src/cache/utils/createCachedReadWriteContract"; // Types export type { @@ -20,7 +20,7 @@ export type { export type { CachedReadContract, CachedReadWriteContract, -} from "src/contract/types/CachedContract"; +} from "src/cache/types/CachedContract"; export type { ContractDecodeFunctionDataArgs, ContractEncodeFunctionDataArgs, diff --git a/packages/drift/src/drift/types.ts b/packages/drift/src/types.ts similarity index 62% rename from packages/drift/src/drift/types.ts rename to packages/drift/src/types.ts index 3f418659..2b66ee1c 100644 --- a/packages/drift/src/drift/types.ts +++ b/packages/drift/src/types.ts @@ -1,28 +1,19 @@ import type { Abi } from "abitype"; -import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; -import type { SimpleCache } from "src/cache/types/SimpleCache"; -import type { - CachedReadContract, - CachedReadWriteContract, -} from "src/contract/types/CachedContract"; import type { ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, -} from "src/contract/types/Contract"; -import type { EventName } from "src/contract/types/Event"; -import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; -import type { TransactionReceipt } from "src/network/types/Transaction"; +} from "src/adapter/contract/types/Contract"; +import type { EventName } from "src/adapter/contract/types/Event"; +import type { + FunctionArgs, + FunctionName, +} from "src/adapter/contract/types/Function"; +import type { TransactionReceipt } from "src/adapter/network/Transaction"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; import type { EmptyObject } from "src/utils/types"; -export type DriftContract< - TAbi extends Abi, - TAdapter extends Adapter = Adapter, -> = TAdapter extends ReadWriteAdapter - ? CachedReadWriteContract - : CachedReadContract; - -export interface DriftContractParams { +export interface ContractParams { abi: TAbi; address: string; cache?: SimpleCache; @@ -33,7 +24,7 @@ export interface DriftContractParams { namespace?: string; } -export type DriftReadParams< +export type ReadParams< TAbi extends Abi, TFunctionName extends FunctionName, > = { @@ -46,17 +37,17 @@ export type DriftReadParams< args: FunctionArgs; }) & ContractReadOptions & - DriftContractParams; + ContractParams; -export interface DriftGetEventsParams< +export interface GetEventsParams< TAbi extends Abi, TEventName extends EventName, > extends ContractGetEventsOptions, - DriftContractParams { + ContractParams { event: TEventName; } -export type DriftWriteParams< +export type WriteParams< TAbi extends Abi, TFunctionName extends FunctionName, > = ContractWriteOptions & { @@ -72,7 +63,7 @@ export type DriftWriteParams< args: FunctionArgs; }); -export type DriftEncodeFunctionDataParams< +export type EncodeFunctionDataParams< TAbi extends Abi, TFunctionName extends FunctionName, > = { @@ -86,7 +77,7 @@ export type DriftEncodeFunctionDataParams< args: FunctionArgs; }); -export interface DriftDecodeFunctionDataParams< +export interface DecodeFunctionDataParams< TAbi extends Abi, TFunctionName extends FunctionName, > { From 52d1a3bc6e300ae24337b6d823a2aaf1c60e20f8 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Tue, 1 Oct 2024 14:13:15 -0500 Subject: [PATCH 20/49] Add `getSignerAddress` to `Drift` client --- .../drift/src/adapter/MockAdapter.test.ts | 6 +++++ packages/drift/src/adapter/MockAdapter.ts | 1 + packages/drift/src/adapter/types.ts | 13 ++++++---- packages/drift/src/drift/Drift.ts | 26 ++++++++++++++++--- 4 files changed, 37 insertions(+), 9 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index eb4e6d23..24e1255d 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -16,6 +16,12 @@ describe("MockAdapter", () => { expect(block).toBe(blockStub); }); + it("Stubs the signer address", async () => { + const adapter = new MockAdapter(); + const signer = await adapter.getSignerAddress(); + expect(signer).toBeTypeOf("string"); + }); + it("Creates mock read contracts", async () => { const mockAdapter = new MockAdapter(); const contract = mockAdapter.readContract(IERC20.abi); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 9b66f68e..f808cb2b 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -6,6 +6,7 @@ import type { ReadWriteAdapter } from "src/adapter/types"; export class MockAdapter implements ReadWriteAdapter { network = new MockNetwork(); + getSignerAddress = async () => "0xMockSigner"; readContract = (abi: TAbi) => new ReadContractStub(abi); readWriteContract = (abi: TAbi) => new ReadWriteContractStub(abi); diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types.ts index 5fb9d428..986e6ab9 100644 --- a/packages/drift/src/adapter/types.ts +++ b/packages/drift/src/adapter/types.ts @@ -4,19 +4,22 @@ import type { AdapterReadWriteContract, } from "src/adapter/contract/types/Contract"; import type { AdapterNetwork } from "src/adapter/network/AdapterNetwork"; -import type { RequiredKeys } from "src/utils/types"; -export interface Adapter { +export interface ReadAdapter { network: AdapterNetwork; readContract: ( abi: TAbi, address: string, ) => AdapterReadContract; - readWriteContract?: ( +} + +export interface ReadWriteAdapter extends ReadAdapter { + // Write-only properties + getSignerAddress: () => Promise; + readWriteContract: ( abi: TAbi, address: string, ) => AdapterReadWriteContract; } -export type ReadAdapter = Omit; -export type ReadWriteAdapter = RequiredKeys; +export type Adapter = ReadAdapter | ReadWriteAdapter; diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts index 281d35da..6e87cdbe 100644 --- a/packages/drift/src/drift/Drift.ts +++ b/packages/drift/src/drift/Drift.ts @@ -40,8 +40,8 @@ export interface DriftOptions { namespace?: string; } -// This is the one place where the Read/ReadWrite distinction is skipped in -// favor of a unified entrypoint to the Drift API. +// This is the one place where the Read/ReadWrite concepts are combined into a +// single class in favor of a unified entrypoint to the Drift API. export class Drift< TAdapter extends Adapter = Adapter, TCache extends SimpleCache = SimpleCache, @@ -49,6 +49,13 @@ export class Drift< readonly adapter: TAdapter; cache: DriftCache; namespace?: string; + + // Write-only property definitions // + + getSignerAddress: TAdapter extends ReadWriteAdapter + ? () => Promise + : undefined; + write: TAdapter extends ReadWriteAdapter ? < TAbi extends Abi, @@ -58,6 +65,8 @@ export class Drift< ) => Promise : undefined; + // Implementation // + constructor( adapter: TAdapter, { cache, namespace }: DriftOptions = {}, @@ -65,7 +74,16 @@ export class Drift< this.adapter = adapter; this.cache = createDriftCache(cache); this.namespace = namespace; - this.write = this.isReadWrite() + + // Write-only property assignment // + + const isReadWrite = this.isReadWrite(); + + this.getSignerAddress = isReadWrite + ? () => this.adapter.getSignerAddress() + : (undefined as any); + + this.write = isReadWrite ? async ({ abi, address, fn, args, onMined, ...writeOptions }) => { if (isReadWriteAdapter(this.adapter)) { const txHash = await createCachedReadWriteContract({ @@ -83,7 +101,7 @@ export class Drift< : (undefined as any); } - isReadWrite = (): this is Drift => + isReadWrite = (): this is Drift => isReadWriteAdapter(this.adapter); contract = ({ From 66ccec56a8900cd4740f9d28c3aaca8ffb71bc63 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Wed, 2 Oct 2024 21:49:52 -0500 Subject: [PATCH 21/49] Broken wip --- .../drift/src/adapter/MockAdapter.test.ts | 563 +++++++++++++++++- packages/drift/src/adapter/MockAdapter.ts | 395 +++++++++++- .../{stubs => mocks}/ReadContractStub.test.ts | 4 +- .../{stubs => mocks}/ReadContractStub.ts | 6 +- .../ReadWriteContractStub.test.ts | 2 +- .../contract/mocks/ReadWriteContractStub.ts | 105 ++++ .../contract/stubs/ReadWriteContractStub.ts | 6 +- .../src/adapter/contract/types/Contract.ts | 49 +- .../drift/src/adapter/contract/types/Event.ts | 2 +- .../src/adapter/contract/types/Function.ts | 2 +- .../contract/types/{AbiEntry.ts => abi.ts} | 2 +- .../adapter/contract/utils/arrayToFriendly.ts | 2 +- .../adapter/contract/utils/arrayToObject.ts | 2 +- .../src/adapter/contract/utils/getAbiEntry.ts | 2 +- .../adapter/contract/utils/objectToArray.ts | 2 +- .../src/adapter/network/MockNetwork.test.ts | 2 +- .../drift/src/adapter/network/MockNetwork.ts | 10 +- .../src/adapter/network/{ => types}/Block.ts | 0 .../NetworkAdapter.ts} | 15 +- .../network/{ => types}/Transaction.ts | 0 packages/drift/src/adapter/types.ts | 134 ++++- .../src/cache/DriftCache/createDriftCache.ts | 6 +- packages/drift/src/cache/DriftCache/types.ts | 37 +- packages/drift/src/cache/utils/DriftCache.ts | 2 +- .../utils/createCachedReadContract.test.ts | 4 +- .../cache/utils/createCachedReadContract.ts | 8 +- .../utils/createCachedReadWriteContract.ts | 2 +- packages/drift/src/cache/utils/eventsKey.ts | 2 +- .../drift/src/cache/utils/invalidateRead.ts | 2 +- .../cache/utils/invalidateReadsMatching.ts | 2 +- .../drift/src/cache/utils/partialReadKey.ts | 2 +- packages/drift/src/cache/utils/preloadRead.ts | 2 +- packages/drift/src/cache/utils/readKey.ts | 2 +- packages/drift/src/contract/CachedContract.ts | 28 - .../drift/src/contract/createReadContract.ts | 63 ++ packages/drift/src/contract/types.ts | 90 +++ packages/drift/src/drift/Drift.ts | 29 +- packages/drift/src/drift/MockDrift.ts | 6 +- packages/drift/src/types.ts | 91 +-- packages/drift/src/utils/types.ts | 13 +- 40 files changed, 1413 insertions(+), 283 deletions(-) rename packages/drift/src/adapter/contract/{stubs => mocks}/ReadContractStub.test.ts (97%) rename packages/drift/src/adapter/contract/{stubs => mocks}/ReadContractStub.ts (98%) rename packages/drift/src/adapter/contract/{stubs => mocks}/ReadWriteContractStub.test.ts (90%) create mode 100644 packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts rename packages/drift/src/adapter/contract/types/{AbiEntry.ts => abi.ts} (99%) rename packages/drift/src/adapter/network/{ => types}/Block.ts (100%) rename packages/drift/src/adapter/network/{AdapterNetwork.ts => types/NetworkAdapter.ts} (81%) rename packages/drift/src/adapter/network/{ => types}/Transaction.ts (100%) delete mode 100644 packages/drift/src/contract/CachedContract.ts create mode 100644 packages/drift/src/contract/createReadContract.ts create mode 100644 packages/drift/src/contract/types.ts diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index 24e1255d..8dbd37b0 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -1,46 +1,549 @@ import { MockAdapter } from "src/adapter/MockAdapter"; +import type { Event } from "src/adapter/contract/types/event"; +import type { Block } from "src/adapter/network/types/Block"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/network/types/Transaction"; +import type { + DecodeFunctionDataParams, + EncodeFunctionDataParams, + GetEventsParams, + ReadParams, + WriteParams, +} from "src/adapter/types"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockAdapter", () => { - it("Includes a mock network", async () => { - const adapter = new MockAdapter(); - const blockStub = { - blockNumber: 100n, - timestamp: 200n, - }; - adapter.network.stubGetBlock({ - value: blockStub, + describe("getBalance", () => { + it("Resolves to a default value", async () => { + const adapter = new MockAdapter(); + expect(await adapter.getBalance("0x0")).toBeTypeOf("bigint"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter.onGetBalance().resolves(123n); + expect(await adapter.getBalance("0x")).toBe(123n); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const defaultValue = await adapter.getBalance("0x"); + adapter.onGetBalance("0xAlice").resolves(defaultValue + 1n); + adapter.onGetBalance("0xBob").resolves(defaultValue + 2n); + expect(await adapter.getBalance("0xAlice")).toBe(defaultValue + 1n); + expect(await adapter.getBalance("0xBob")).toBe(defaultValue + 2n); + }); + }); + + describe("getBlock", () => { + it("Resolves to a default value", async () => { + const adapter = new MockAdapter(); + expect(adapter.getBlock()).resolves.toEqual({ + blockNumber: expect.any(BigInt), + timestamp: expect.any(BigInt), + }); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + const block: Block = { + blockNumber: 123n, + timestamp: 123n, + }; + adapter.onGetBlock().resolves(block); + expect(await adapter.getBlock()).toBe(block); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const { blockNumber, timestamp } = await adapter.getBlock(); + const block1: Block = { + blockNumber: blockNumber ?? 0n + 1n, + timestamp: timestamp + 1n, + }; + const block2: Block = { + blockNumber: blockNumber ?? 0n + 2n, + timestamp: timestamp + 2n, + }; + adapter.onGetBlock({ blockNumber: 1n }).resolves(block1); + adapter.onGetBlock({ blockNumber: 2n }).resolves(block2); + expect(await adapter.getBlock({ blockNumber: 1n })).toBe(block1); + expect(await adapter.getBlock({ blockNumber: 2n })).toBe(block2); + }); + }); + + describe("getChainId", () => { + it("Resolves to a default value", async () => { + const adapter = new MockAdapter(); + expect(await adapter.getChainId()).toBeTypeOf("number"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter.onGetChainId().resolves(123); + expect(await adapter.getChainId()).toBe(123); + }); + }); + + describe("getTransaction", () => { + it("Resolves to undefined by default", async () => { + const adapter = new MockAdapter(); + expect(await adapter.getTransaction("0x")).toBeUndefined(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + const transaction: Transaction = { + blockNumber: 123n, + gas: 123n, + gasPrice: 123n, + input: "0x", + nonce: 123, + type: "0x123", + value: 123n, + }; + adapter.onGetTransaction().resolves(transaction); + expect(await adapter.getTransaction("0x")).toBe(transaction); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const transaction1: Transaction = { + input: "0x1", + blockNumber: 123n, + gas: 123n, + gasPrice: 123n, + nonce: 123, + type: "0x123", + value: 123n, + }; + const transaction2: Transaction = { + ...transaction1, + input: "0x2", + }; + adapter.onGetTransaction("0x1").resolves(transaction1); + adapter.onGetTransaction("0x2").resolves(transaction2); + expect(await adapter.getTransaction("0x1")).toBe(transaction1); + expect(await adapter.getTransaction("0x2")).toBe(transaction2); }); - const block = await adapter.network.getBlock(); - expect(block).toBe(blockStub); }); - it("Stubs the signer address", async () => { - const adapter = new MockAdapter(); - const signer = await adapter.getSignerAddress(); - expect(signer).toBeTypeOf("string"); + describe("waitForTransaction", () => { + it("Resolves to undefined by default", async () => { + const adapter = new MockAdapter(); + expect(adapter.waitForTransaction("0x")).resolves.toBeUndefined(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + const receipt: TransactionReceipt = { + blockNumber: 123n, + blockHash: "0x", + cumulativeGasUsed: 123n, + effectiveGasPrice: 123n, + from: "0x", + gasUsed: 123n, + logsBloom: "0x", + status: "success", + to: "0x", + transactionHash: "0x", + transactionIndex: 123, + }; + adapter.onWaitForTransaction().resolves(receipt); + expect(await adapter.waitForTransaction("0x")).toBe(receipt); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const receipt1: TransactionReceipt = { + transactionHash: "0x1", + blockNumber: 123n, + blockHash: "0x", + cumulativeGasUsed: 123n, + effectiveGasPrice: 123n, + from: "0x", + gasUsed: 123n, + logsBloom: "0x", + status: "success", + to: "0x", + transactionIndex: 123, + }; + const receipt2: TransactionReceipt = { + ...receipt1, + transactionHash: "0x2", + }; + adapter.onWaitForTransaction("0x1").resolves(receipt1); + adapter.onWaitForTransaction("0x2").resolves(receipt2); + expect(await adapter.waitForTransaction("0x1")).toBe(receipt1); + expect(await adapter.waitForTransaction("0x2")).toBe(receipt2); + }); }); - it("Creates mock read contracts", async () => { - const mockAdapter = new MockAdapter(); - const contract = mockAdapter.readContract(IERC20.abi); - contract.stubRead({ - functionName: "balanceOf", - value: 100n, + describe("encodeFunctionData", () => { + it("Returns a default value", async () => { + const adapter = new MockAdapter(); + expect( + adapter.encodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter + .onEncodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }) + .returns("0x123"); + expect( + adapter.encodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBe("0x123"); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: EncodeFunctionDataParams = + { + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x1" }, + }; + const params2: EncodeFunctionDataParams = + { + ...params1, + args: { owner: "0x2" }, + }; + adapter.onEncodeFunctionData(params1).returns("0x1"); + adapter.onEncodeFunctionData(params2).returns("0x2"); + expect(adapter.encodeFunctionData(params1)).toBe("0x1"); + expect(adapter.encodeFunctionData(params2)).toBe("0x2"); }); - const balance = await contract.read("balanceOf", { owner: "0xMe" }); - expect(balance).toBe(100n); }); - it("Creates mock read-write contracts", async () => { - const mockAdapter = new MockAdapter(); - const contract = mockAdapter.readWriteContract(IERC20.abi); - contract.stubWrite("approve", "0xDone"); - const txHash = await contract.write("approve", { - spender: "0xYou", - value: 100n, + describe("decodeFunctionData", () => { + it("Throws an error by default", async () => { + const adapter = new MockAdapter(); + expect( + (async () => + adapter.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }))(), + ).rejects.toThrowError(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter + .onDecodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }) + .returns(123n); + expect( + adapter.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }), + ).toBe(123n); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: DecodeFunctionDataParams = + { + abi: IERC20.abi, + fn: "balanceOf", + data: "0x1", + }; + const params2: DecodeFunctionDataParams = + { + ...params1, + data: "0x2", + }; + adapter.onDecodeFunctionData(params1).returns(1n); + adapter.onDecodeFunctionData(params2).returns(2n); + expect(adapter.decodeFunctionData(params1)).toBe(1n); + expect(adapter.decodeFunctionData(params2)).toBe(2n); + }); + }); + + describe("getEvents", () => { + it("Rejects with an error by default", async () => { + const adapter = new MockAdapter(); + expect( + adapter.getEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }), + ).rejects.toThrowError(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + const events: Event[] = [ + { + eventName: "Transfer", + args: { + from: "0x", + to: "0x", + value: 123n, + }, + }, + ]; + adapter + .onGetEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }) + .resolves(events); + expect( + await adapter.getEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }), + ).toBe(events); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: GetEventsParams = { + abi: IERC20.abi, + address: "0x1", + event: "Transfer", + filter: { from: "0x1" }, + }; + const params2: GetEventsParams = { + ...params1, + filter: { from: "0x2" }, + }; + const events1: Event[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: Event[] = [ + { + eventName: "Transfer", + args: { + from: "0x2", + to: "0x2", + value: 123n, + }, + }, + ]; + adapter.onGetEvents(params1).resolves(events1); + adapter.onGetEvents(params2).resolves(events2); + expect(await adapter.getEvents(params1)).toBe(events1); + expect(await adapter.getEvents(params2)).toBe(events2); + }); + }); + + describe("read", () => { + it("Rejects with an error by default", async () => { + const adapter = new MockAdapter(); + expect( + adapter.read({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }), + ).rejects.toThrowError(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter + .onRead({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }) + .resolves("ABC"); + expect( + await adapter.read({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }), + ).toBe("ABC"); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: ReadParams = { + abi: IERC20.abi, + address: "0x1", + fn: "allowance", + args: { owner: "0x1", spender: "0x1" }, + }; + const params2: ReadParams = { + ...params1, + args: { owner: "0x2", spender: "0x2" }, + }; + adapter.onRead(params1).resolves(1n); + adapter.onRead(params2).resolves(2n); + expect(await adapter.read(params1)).toBe(1n); + expect(await adapter.read(params2)).toBe(2n); + }); + + it.todo("Can be stubbed with partial args", async () => { + const adapter = new MockAdapter(); + adapter + .onRead({ + abi: IERC20.abi, + address: "0x", + fn: "balanceOf", + }) + .resolves(123n); + expect( + await adapter.read({ + abi: IERC20.abi, + address: "0x", + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBe(123n); + }); + }); + + describe("simulateWrite", () => { + it("Rejects with an error by default", async () => { + const adapter = new MockAdapter(); + expect( + adapter.simulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).rejects.toThrowError(); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter + .onSimulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }) + .resolves(true); + expect( + await adapter.simulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBe(true); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: WriteParams = { + abi: IERC20.abi, + address: "0x1", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: WriteParams = { + ...params1, + args: { to: "0x2", value: 123n }, + }; + adapter.onSimulateWrite(params1).resolves(true); + adapter.onSimulateWrite(params2).resolves(false); + expect(await adapter.simulateWrite(params1)).toBe(true); + expect(await adapter.simulateWrite(params2)).toBe(false); + }); + }); + + describe("write", () => { + it("Returns a default value", async () => { + const adapter = new MockAdapter(); + expect( + await adapter.write({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter + .onWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }) + .returns("0x123"); + expect( + await adapter.write({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBe("0x123"); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: WriteParams = { + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: WriteParams = { + ...params1, + args: { to: "0x2", value: 123n }, + }; + adapter.onWrite(params1).returns("0x1"); + adapter.onWrite(params2).returns("0x2"); + expect(await adapter.write(params1)).toBe("0x1"); + expect(await adapter.write(params2)).toBe("0x2"); + }); + }); + + describe("getSignerAddress", () => { + it("Returns a default value", async () => { + const adapter = new MockAdapter(); + expect(await adapter.getSignerAddress()).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + adapter.onGetSignerAddress().resolves("0x123"); + expect(await adapter.getSignerAddress()).toBe("0x123"); }); - expect(txHash).toBe("0xDone"); }); }); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index f808cb2b..2dd8c3fa 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -1,13 +1,390 @@ import type { Abi } from "abitype"; -import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; -import { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; -import { MockNetwork } from "src/adapter/network/MockNetwork"; -import type { ReadWriteAdapter } from "src/adapter/types"; +import { type SinonStub, stub as createStub } from "sinon"; +import type { Event, EventName } from "src/adapter/contract/types/event"; +import type { + FunctionName, + FunctionReturn, +} from "src/adapter/contract/types/function"; +import type { + NetworkGetBalanceArgs, + NetworkGetBlockArgs, + NetworkGetTransactionArgs, + NetworkWaitForTransactionArgs, +} from "src/adapter/network/types/NetworkAdapter"; +import type { + DecodeFunctionDataParams, + EncodeFunctionDataParams, + GetEventsParams, + ReadParams, + ReadWriteAdapter, + WriteParams, +} from "src/adapter/types"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import type { OptionalKeys } from "src/utils/types"; +// TODO: Allow configuration of error throwing/default return value behavior export class MockAdapter implements ReadWriteAdapter { - network = new MockNetwork(); - getSignerAddress = async () => "0xMockSigner"; - readContract = (abi: TAbi) => new ReadContractStub(abi); - readWriteContract = (abi: TAbi) => - new ReadWriteContractStub(abi); + // stubs // + + protected stubs = new Map(); + + protected getStub SinonStub)>({ + key, + create, + }: { + key: string; + create?: TInsert; + }): TInsert extends false | undefined ? SinonStub | undefined : SinonStub { + let stub = this.stubs.get(key); + if (!stub && create) { + stub = typeof create === "function" ? create() : createStub(); + this.stubs.set(key, stub); + } + return stub as any; + } + + reset() { + this.stubs.clear(); + } + + // getBalance // + + protected get getBalanceStub() { + return this.getStub({ + key: "getBalance", + create: () => createStub().resolves(0n), + }); + } + + getBalance(...args: NetworkGetBalanceArgs) { + return this.getBalanceStub(...args); + } + + onGetBalance(...args: Partial) { + return this.getBalanceStub.withArgs(...args); + } + + // getBlock // + + protected get getBlockStub() { + return this.getStub({ + key: "getBlock", + create: () => + createStub().resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }); + } + + getBlock(...args: NetworkGetBlockArgs) { + return this.getBlockStub(...args); + } + + onGetBlock(...args: Partial) { + return this.getBlockStub.withArgs(...args); + } + + // getChainId // + + protected get getChainIdStub() { + return this.getStub({ + key: "getChainId", + create: () => createStub().resolves(0), + }); + } + + getChainId() { + return this.getChainIdStub(); + } + + onGetChainId() { + return this.getChainIdStub; + } + + // getTransaction // + + protected get getTransactionStub() { + return this.getStub({ + key: "getTransaction", + create: () => createStub().resolves(undefined), + }); + } + + getTransaction(...args: NetworkGetTransactionArgs) { + return this.getTransactionStub(...args); + } + + onGetTransaction(...args: Partial) { + return this.getTransactionStub.withArgs(...args); + } + + // waitForTransaction // + + protected get waitForTransactionStub() { + return this.getStub({ + key: "waitForTransaction", + create: () => createStub().resolves(undefined), + }); + } + + waitForTransaction(...args: NetworkWaitForTransactionArgs) { + return this.waitForTransactionStub(...args); + } + + onWaitForTransaction(...args: Partial) { + return this.waitForTransactionStub.withArgs(...args); + } + + // encodeFunction // + + protected get encodeFunctionDataStub() { + return this.getStub({ + key: "encodeFunctionData", + create: () => createStub().returns("0x0"), + }); + } + + encodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: EncodeFunctionDataParams): Bytes { + return this.encodeFunctionDataStub(params); + } + + onEncodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: EncodeFunctionDataStubParams) { + return this.encodeFunctionDataStub.withArgs(params); + } + + // decodeFunction // + + // TODO: This should be specific to the abi to ensure the correct return type. + protected decodeFunctionDataStubKey({ + fn, + }: Partial) { + return `decodeFunctionData:${fn}`; + } + + decodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: DecodeFunctionDataParams, + ): FunctionReturn { + const stub = this.getStub({ + key: this.decodeFunctionDataStubKey(params), + }); + if (!stub) { + throw new NotImplementedError({ + name: params.fn || params.data, + method: "decodeFunctionData", + stubMethod: "onDecodeFunctionData", + }); + } + return stub(params); + } + + // TODO: Does calling `onDecodeFunctionData` without calling any methods on + // it, e.g. `returns`, break the error behavior? + onDecodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: DecodeFunctionDataStubParams) { + return this.getStub({ + key: this.decodeFunctionDataStubKey(params), + create: true, + }).withArgs(params); + } + + // getEvents // + + protected getEventsStubKey({ + address, + event, + }: Partial>): string { + return `getEvents:${address}:${event}`; + } + + getEvents>( + params: GetEventsParams, + ): Promise[]> { + const stub = this.stubs.get(this.getEventsStubKey(params)); + if (!stub) { + return Promise.reject( + new NotImplementedError({ + name: params.event, + method: "getEvents", + stubMethod: "onGetEvents", + }), + ); + } + return Promise.resolve(stub(params)); + } + + onGetEvents>( + params: GetEventsParams, + ) { + return this.getStub< + [GetEventsParams], + Promise[]> + >({ + key: this.getEventsStubKey(params), + args: [params], + }); + } + + // read // + + protected readStubKey({ address, fn }: ReadStubParams) { + return `read:${address}:${fn}`; + } + + read< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: ReadParams, + ): Promise> { + const stub = this.stubs.get(this.readStubKey(params)); + if (!stub) { + return Promise.reject( + new NotImplementedError({ + name: params.fn, + method: "read", + stubMethod: "onRead", + }), + ); + } + return Promise.resolve(stub(params)); + } + + onRead< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: ReadStubParams) { + return this.getStub< + [ReadStubParams], + Promise> + >({ + key: this.readStubKey(params), + args: [params], + }); + } + + // simulateWrite // + + protected simulateWriteStubKey({ address, fn }: WriteStubParams) { + return `simulateWrite:${address}:${fn}`; + } + + simulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: WriteParams, + ): Promise> { + const stub = this.stubs.get(this.simulateWriteStubKey(params)); + if (!stub) { + return Promise.reject( + new NotImplementedError({ + name: params.fn, + method: "simulateWrite", + stubMethod: "onSimulateWrite", + }), + ); + } + return Promise.resolve(stub(params)); + } + + onSimulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: WriteStubParams) { + return this.getStub< + [WriteStubParams], + Promise> + >({ + key: this.simulateWriteStubKey(params), + args: [params], + }); + } + + // write // + + protected get writeStub() { + return this.getStub<[WriteStubParams], Bytes>({ + key: "write", + }).returns("0x0"); + } + + write< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: WriteParams): Promise { + return Promise.resolve(this.writeStub(params)); + } + + onWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: WriteStubParams) { + return this.writeStub.withArgs(params); + } + + // getSignerAddress // + + protected get getSignerAddressStub() { + const key = "getSignerAddress"; + let stub = this.stubs.get(key); + if (!stub) { + stub = createStub().resolves("0xMockSigner"); + this.stubs.set(key, stub); + } + } + + onGetSignerAddress() { + return this.getSignerAddressStub; + } + + getSignerAddress(): Promise
{ + return Promise.resolve(this.getSignerAddressStub()); + } +} + +// TODO: Make address optional and create a key from the abi entry and fn name. +export type ReadStubParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = OptionalKeys, "args">; + +export type WriteStubParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = OptionalKeys, "args" | "abi">; + +export type EncodeFunctionDataStubParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = OptionalKeys, "args">; + +export type DecodeFunctionDataStubParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = OptionalKeys, "data" | "abi">; + +class NotImplementedError extends Error { + constructor({ + method, + stubMethod, + name, + }: { method: string; stubMethod: string; name?: string }) { + // TODO: This error message is not accurate. + super( + `Called ${method}${name ? ` for "${name}"` : ""} on a MockAdapter without a return value. The function must be stubbed first:\n\tadapter.${stubMethod}("${name}").resolves(value)`, + ); + this.name = "NotImplementedError"; + } } diff --git a/packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts b/packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts similarity index 97% rename from packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts rename to packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts index ce5d2253..ad721ef7 100644 --- a/packages/drift/src/adapter/contract/stubs/ReadContractStub.test.ts +++ b/packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts @@ -1,5 +1,5 @@ -import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; -import type { Event } from "src/adapter/contract/types/Event"; +import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; +import type { Event } from "src/adapter/contract/types/event"; import { IERC20 } from "src/utils/testing/IERC20"; import { ALICE, BOB, NANCY } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/adapter/contract/stubs/ReadContractStub.ts b/packages/drift/src/adapter/contract/mocks/ReadContractStub.ts similarity index 98% rename from packages/drift/src/adapter/contract/stubs/ReadContractStub.ts rename to packages/drift/src/adapter/contract/mocks/ReadContractStub.ts index 1fa981f6..67c20846 100644 --- a/packages/drift/src/adapter/contract/stubs/ReadContractStub.ts +++ b/packages/drift/src/adapter/contract/mocks/ReadContractStub.ts @@ -11,14 +11,14 @@ import type { ContractReadOptions, ContractWriteArgs, ContractWriteOptions, -} from "src/adapter/contract/types/Contract"; -import type { Event, EventName } from "src/adapter/contract/types/Event"; +} from "src/adapter/contract/types/contract"; +import type { Event, EventName } from "src/adapter/contract/types/event"; import type { DecodedFunctionData, FunctionArgs, FunctionName, FunctionReturn, -} from "src/adapter/contract/types/Function"; +} from "src/adapter/contract/types/function"; /** * A mock implementation of a `ReadContract` designed to facilitate unit diff --git a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts similarity index 90% rename from packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts rename to packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts index b33b28df..cfa6b0fa 100644 --- a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.test.ts +++ b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts @@ -1,4 +1,4 @@ -import { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; +import { ReadWriteContractStub } from "src/adapter/contract/mocks/ReadWriteContractStub"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts new file mode 100644 index 00000000..cdd469fc --- /dev/null +++ b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts @@ -0,0 +1,105 @@ +import type { Abi } from "abitype"; +import { type SinonStub, stub } from "sinon"; +import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; +import type { + AdapterReadWriteContract, + ContractWriteArgs, + ContractWriteOptions, +} from "src/adapter/contract/types/contract"; +import type { + FunctionArgs, + FunctionName, +} from "src/adapter/contract/types/function"; +import { BOB } from "src/utils/testing/accounts"; + +/** + * A mock implementation of a writable Ethereum contract designed for unit + * testing purposes. The `ReadWriteContractStub` extends the functionalities of + * `ReadContractStub` and provides capabilities to stub out specific + * contract write behaviors. This makes it a valuable tool when testing + * scenarios that involve contract writing operations, without actually + * interacting with a real Ethereum contract. + * + * @example + * const contract = new ReadWriteContractStub(ERC20ABI); + * contract.stubWrite("addLiquidity", 100n); + * + * const result = await contract.write("addLiquidity", []); // 100n + * @extends {ReadContractStub} + * @implements {ReadWriteContract} + */ +export class ReadWriteContractStub + extends ReadContractStub + implements AdapterReadWriteContract +{ + protected writeStubMap = new Map< + FunctionName, + WriteStub> + >(); + + getSignerAddress = stub().resolves(BOB); + + /** + * Simulates a contract write operation for a given function. If the function + * is not previously stubbed using `stubWrite` from the parent class, an error + * will be thrown. + */ + async write< + TFunctionName extends FunctionName, + >( + ...[functionName, args, options]: ContractWriteArgs + ): Promise<`0x${string}`> { + const stub = this.getWriteStub(functionName); + if (!stub) { + throw new Error( + `Called write for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, + ); + } + return stub(args, options); + } + + /** + * Stubs the return value for a given function when `simulateWrite` is called + * with that function name. This method overrides any previously stubbed + * values for the same function. + * + * *Note: The stub doesn't account for dynamic values based on provided + * arguments/options.* + */ + stubWrite>( + functionName: TFunctionName, + value: `0x${string}`, + ): void { + let writeStub = this.writeStubMap.get(functionName); + if (!writeStub) { + writeStub = stub(); + this.writeStubMap.set(functionName, writeStub); + } + writeStub.resolves(value); + } + + /** + * Retrieves the stub associated with a write function name. + * Useful for assertions in testing, such as checking call counts. + */ + getWriteStub< + TFunctionName extends FunctionName, + >(functionName: TFunctionName): WriteStub | undefined { + return this.writeStubMap.get(functionName) as WriteStub< + TAbi, + TFunctionName + >; + } +} + +/** + * Type representing a stub for the "write" and "simulateWrite" functions of a + * contract. + */ +type WriteStub< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = SinonStub< + [args?: FunctionArgs, options?: ContractWriteOptions], + `0x${string}` +>; diff --git a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts index e0fb0b28..cdd469fc 100644 --- a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts +++ b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts @@ -1,15 +1,15 @@ import type { Abi } from "abitype"; import { type SinonStub, stub } from "sinon"; -import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; +import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; import type { AdapterReadWriteContract, ContractWriteArgs, ContractWriteOptions, -} from "src/adapter/contract/types/Contract"; +} from "src/adapter/contract/types/contract"; import type { FunctionArgs, FunctionName, -} from "src/adapter/contract/types/Function"; +} from "src/adapter/contract/types/function"; import { BOB } from "src/utils/testing/accounts"; /** diff --git a/packages/drift/src/adapter/contract/types/Contract.ts b/packages/drift/src/adapter/contract/types/Contract.ts index eb9497f4..5833114c 100644 --- a/packages/drift/src/adapter/contract/types/Contract.ts +++ b/packages/drift/src/adapter/contract/types/Contract.ts @@ -3,14 +3,14 @@ import type { Event, EventFilter, EventName, -} from "src/adapter/contract/types/Event"; +} from "src/adapter/contract/types/event"; import type { DecodedFunctionData, FunctionArgs, FunctionName, FunctionReturn, -} from "src/adapter/contract/types/Function"; -import type { BlockTag } from "src/adapter/network/Block"; +} from "src/adapter/contract/types/function"; +import type { BlockTag } from "src/adapter/network/types/Block"; import type { EmptyObject } from "src/utils/types"; // https://ethereum.github.io/execution-apis/api-documentation/ @@ -21,7 +21,7 @@ import type { EmptyObject } from "src/utils/types"; */ export interface AdapterReadContract { abi: TAbi; - address: `0x${string}`; + address: string; /** * Reads a specified function from the contract. @@ -51,7 +51,7 @@ export interface AdapterReadContract { */ encodeFunctionData>( ...args: ContractEncodeFunctionDataArgs - ): `0x${string}`; + ): string; /** * Decodes a string of function calldata into it's arguments and function @@ -73,7 +73,7 @@ export interface AdapterReadWriteContract /** * Get the address of the signer for this contract. */ - getSignerAddress(): Promise<`0x${string}`>; + getSignerAddress(): Promise; /** * Writes to a specified function on the contract. @@ -81,7 +81,7 @@ export interface AdapterReadWriteContract */ write>( ...args: ContractWriteArgs - ): Promise<`0x${string}`>; + ): Promise; } // https://github.com/ethereum/execution-apis/blob/main/src/eth/execute.yaml#L1 @@ -99,8 +99,8 @@ export type ContractReadOptions = }; export type ContractReadArgs< - TAbi extends Abi, - TFunctionName extends FunctionName, + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, > = FunctionArgs extends EmptyObject ? [ functionName: TFunctionName, @@ -114,7 +114,7 @@ export type ContractReadArgs< ]; export interface ContractGetEventsOptions< - TAbi extends Abi, + TAbi extends Abi = Abi, TEventName extends EventName = EventName, > { filter?: EventFilter; @@ -123,8 +123,8 @@ export interface ContractGetEventsOptions< } export type ContractGetEventsArgs< - TAbi extends Abi, - TEventName extends EventName, + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, > = [ eventName: TEventName, options?: ContractGetEventsOptions, @@ -132,16 +132,16 @@ export type ContractGetEventsArgs< // https://github.com/ethereum/execution-apis/blob/main/src/schemas/transaction.yaml#L274 export interface ContractWriteOptions { - type?: `0x${string}`; + type?: string; nonce?: bigint; - to?: `0x${string}`; - from?: `0x${string}`; + to?: string; + from?: string; /** * Gas limit */ gas?: bigint; value?: bigint; - input?: `0x${string}`; + input?: string; /** * The gas price willing to be paid by the sender in wei */ @@ -159,8 +159,8 @@ export interface ContractWriteOptions { * EIP-2930 access list */ accessList?: { - address: `0x${string}`; - storageKeys: `0x${string}`[]; + address: string; + storageKeys: string[]; }[]; /** * Chain ID that this transaction is valid on. @@ -169,8 +169,11 @@ export interface ContractWriteOptions { } export type ContractWriteArgs< - TAbi extends Abi, - TFunctionName extends FunctionName, + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, > = FunctionArgs extends EmptyObject ? [ functionName: TFunctionName, @@ -184,10 +187,10 @@ export type ContractWriteArgs< ]; export type ContractEncodeFunctionDataArgs< - TAbi extends Abi, - TFunctionName extends FunctionName, + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, > = FunctionArgs extends EmptyObject ? [functionName: TFunctionName, args?: FunctionArgs] : [functionName: TFunctionName, args: FunctionArgs]; -export type ContractDecodeFunctionDataArgs = [data: `0x${string}`]; +export type ContractDecodeFunctionDataArgs = [data: string]; diff --git a/packages/drift/src/adapter/contract/types/Event.ts b/packages/drift/src/adapter/contract/types/Event.ts index 0d73256e..d524721f 100644 --- a/packages/drift/src/adapter/contract/types/Event.ts +++ b/packages/drift/src/adapter/contract/types/Event.ts @@ -5,7 +5,7 @@ import type { AbiParameters, AbiParametersToObject, NamedAbiParameter, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; /** * Get a union of event names from an abi diff --git a/packages/drift/src/adapter/contract/types/Function.ts b/packages/drift/src/adapter/contract/types/Function.ts index f2eaadf1..a9b16926 100644 --- a/packages/drift/src/adapter/contract/types/Function.ts +++ b/packages/drift/src/adapter/contract/types/Function.ts @@ -2,7 +2,7 @@ import type { Abi, AbiStateMutability } from "abitype"; import type { AbiFriendlyType, AbiObjectType, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; /** * Get a union of function names from an abi diff --git a/packages/drift/src/adapter/contract/types/AbiEntry.ts b/packages/drift/src/adapter/contract/types/abi.ts similarity index 99% rename from packages/drift/src/adapter/contract/types/AbiEntry.ts rename to packages/drift/src/adapter/contract/types/abi.ts index 11010a48..2b1c6aa5 100644 --- a/packages/drift/src/adapter/contract/types/AbiEntry.ts +++ b/packages/drift/src/adapter/contract/types/abi.ts @@ -137,7 +137,7 @@ type NamedParametersToObject< // key, so we have to use `number` as the key for any parameters that have // empty names ("") in arrays Extract extends never - ? unknown // <- No parameters with empty names + ? any // <- No parameters with empty names : { [index: number]: AbiParameterToPrimitiveType< Extract, diff --git a/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts b/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts index b5fb887b..20172eb1 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts @@ -3,7 +3,7 @@ import type { AbiArrayType, AbiEntryName, AbiFriendlyType, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** diff --git a/packages/drift/src/adapter/contract/utils/arrayToObject.ts b/packages/drift/src/adapter/contract/utils/arrayToObject.ts index 66d9eaba..37c9f56b 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToObject.ts +++ b/packages/drift/src/adapter/contract/utils/arrayToObject.ts @@ -3,7 +3,7 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** diff --git a/packages/drift/src/adapter/contract/utils/getAbiEntry.ts b/packages/drift/src/adapter/contract/utils/getAbiEntry.ts index 3936d014..4775629b 100644 --- a/packages/drift/src/adapter/contract/utils/getAbiEntry.ts +++ b/packages/drift/src/adapter/contract/utils/getAbiEntry.ts @@ -2,7 +2,7 @@ import type { Abi, AbiItemType } from "abitype"; import type { AbiEntry, AbiEntryName, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; import { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; /** diff --git a/packages/drift/src/adapter/contract/utils/objectToArray.ts b/packages/drift/src/adapter/contract/utils/objectToArray.ts index 5eb6e1ec..97f938e1 100644 --- a/packages/drift/src/adapter/contract/utils/objectToArray.ts +++ b/packages/drift/src/adapter/contract/utils/objectToArray.ts @@ -3,7 +3,7 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/adapter/contract/types/AbiEntry"; +} from "src/adapter/contract/types/abi"; import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; /** diff --git a/packages/drift/src/adapter/network/MockNetwork.test.ts b/packages/drift/src/adapter/network/MockNetwork.test.ts index 4f4b697e..edf49b30 100644 --- a/packages/drift/src/adapter/network/MockNetwork.test.ts +++ b/packages/drift/src/adapter/network/MockNetwork.test.ts @@ -4,7 +4,7 @@ import { } from "src/adapter/network/MockNetwork"; import { ALICE } from "src/utils/testing/accounts"; import { describe, expect, it } from "vitest"; -import type { Transaction } from "./Transaction"; +import type { Transaction } from "./types/Transaction"; describe("MockNetwork", () => { it("stubs getBalance", async () => { diff --git a/packages/drift/src/adapter/network/MockNetwork.ts b/packages/drift/src/adapter/network/MockNetwork.ts index b234be6f..c6af2e29 100644 --- a/packages/drift/src/adapter/network/MockNetwork.ts +++ b/packages/drift/src/adapter/network/MockNetwork.ts @@ -1,22 +1,22 @@ import { type SinonStub, stub } from "sinon"; +import type { Block } from "src/adapter/network/types/Block"; import type { - AdapterNetwork, + NetworkAdapter, NetworkGetBalanceArgs, NetworkGetBlockArgs, NetworkGetTransactionArgs, NetworkWaitForTransactionArgs, -} from "src/adapter/network/AdapterNetwork"; -import type { Block } from "src/adapter/network/Block"; +} from "src/adapter/network/types/NetworkAdapter"; import type { Transaction, TransactionReceipt, -} from "src/adapter/network/Transaction"; +} from "src/adapter/network/types/Transaction"; /** * A mock implementation of a `Network` designed to facilitate unit * testing. */ -export class MockNetwork implements AdapterNetwork { +export class MockNetwork implements NetworkAdapter { protected getBalanceStub: | SinonStub<[NetworkGetBalanceArgs?], Promise> | undefined; diff --git a/packages/drift/src/adapter/network/Block.ts b/packages/drift/src/adapter/network/types/Block.ts similarity index 100% rename from packages/drift/src/adapter/network/Block.ts rename to packages/drift/src/adapter/network/types/Block.ts diff --git a/packages/drift/src/adapter/network/AdapterNetwork.ts b/packages/drift/src/adapter/network/types/NetworkAdapter.ts similarity index 81% rename from packages/drift/src/adapter/network/AdapterNetwork.ts rename to packages/drift/src/adapter/network/types/NetworkAdapter.ts index 83b3af4a..ea6e1360 100644 --- a/packages/drift/src/adapter/network/AdapterNetwork.ts +++ b/packages/drift/src/adapter/network/types/NetworkAdapter.ts @@ -1,15 +1,16 @@ -import type { Block, BlockTag } from "src/adapter/network/Block"; +import type { Block, BlockTag } from "src/adapter/network/types/Block"; import type { Transaction, TransactionReceipt, -} from "src/adapter/network/Transaction"; +} from "src/adapter/network/types/Transaction"; +import type { Address, HexString, TransactionHash } from "src/types"; // https://ethereum.github.io/execution-apis/api-documentation/ /** * An interface representing data the SDK needs to get from the network. */ -export interface AdapterNetwork { +export interface NetworkAdapter { /** * Get the balance of native currency for an account. */ @@ -43,7 +44,7 @@ export interface AdapterNetwork { export type NetworkGetBlockOptions = | { - blockHash?: `0x${string}`; + blockHash?: HexString; blockNumber?: never; blockTag?: never; } @@ -59,16 +60,16 @@ export type NetworkGetBlockOptions = }; export type NetworkGetBalanceArgs = [ - address: `0x${string}`, + address: Address, block?: NetworkGetBlockOptions, ]; export type NetworkGetBlockArgs = [options?: NetworkGetBlockOptions]; -export type NetworkGetTransactionArgs = [hash: `0x${string}`]; +export type NetworkGetTransactionArgs = [hash: TransactionHash]; export type NetworkWaitForTransactionArgs = [ - hash: `0x${string}`, + hash: TransactionHash, options?: { /** * The number of milliseconds to wait for the transaction until rejecting diff --git a/packages/drift/src/adapter/network/Transaction.ts b/packages/drift/src/adapter/network/types/Transaction.ts similarity index 100% rename from packages/drift/src/adapter/network/Transaction.ts rename to packages/drift/src/adapter/network/types/Transaction.ts diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types.ts index 986e6ab9..9a1018e4 100644 --- a/packages/drift/src/adapter/types.ts +++ b/packages/drift/src/adapter/types.ts @@ -1,25 +1,125 @@ import type { Abi } from "abitype"; import type { - AdapterReadContract, - AdapterReadWriteContract, -} from "src/adapter/contract/types/Contract"; -import type { AdapterNetwork } from "src/adapter/network/AdapterNetwork"; - -export interface ReadAdapter { - network: AdapterNetwork; - readContract: ( - abi: TAbi, - address: string, - ) => AdapterReadContract; + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, +} from "src/adapter/contract/types/contract"; +import type { Event, EventName } from "src/adapter/contract/types/event"; +import type { + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/contract/types/function"; +import type { NetworkAdapter } from "src/adapter/network/types/NetworkAdapter"; +import type { TransactionReceipt } from "src/adapter/network/types/Transaction"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import type { EmptyObject } from "src/utils/types"; + +export interface ReadAdapter extends NetworkAdapter { + read< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: ReadParams, + ): Promise>; + + getEvents>( + params: GetEventsParams, + ): Promise[]>; + + simulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: WriteParams, + ): Promise>; + + encodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: EncodeFunctionDataParams): Bytes; + + decodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: DecodeFunctionDataParams, + ): FunctionReturn; } export interface ReadWriteAdapter extends ReadAdapter { - // Write-only properties - getSignerAddress: () => Promise; - readWriteContract: ( - abi: TAbi, - address: string, - ) => AdapterReadWriteContract; + getSignerAddress(): Promise
; + + write< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: WriteParams): Promise; } export type Adapter = ReadAdapter | ReadWriteAdapter; + +export type ArgsParam< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = FunctionArgs extends EmptyObject + ? { + args?: FunctionArgs; + } + : Abi extends TAbi + ? { + args?: FunctionArgs; + } + : { + args: FunctionArgs; + }; + +export type ReadParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = ContractReadOptions & { + abi: TAbi; + address: Address; + fn: TFunctionName; +} & ArgsParam; + +export interface GetEventsParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> extends ContractGetEventsOptions { + abi: TAbi; + address: Address; + event: TEventName; +} + +export type WriteParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, +> = ContractWriteOptions & { + abi: TAbi; + address: Address; + fn: TFunctionName; + onMined?: (receipt?: TransactionReceipt) => void; +} & ArgsParam; + +export type EncodeFunctionDataParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = { + abi: TAbi; + fn: TFunctionName; +} & ArgsParam; + +export interface DecodeFunctionDataParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> { + abi: TAbi; + data: Bytes; + // TODO: This is optional and only used to determine the return type, but is + // there another way to get the return type based on the function selector in + // the data? + fn?: TFunctionName; +} diff --git a/packages/drift/src/cache/DriftCache/createDriftCache.ts b/packages/drift/src/cache/DriftCache/createDriftCache.ts index 9e67142e..f6079d2c 100644 --- a/packages/drift/src/cache/DriftCache/createDriftCache.ts +++ b/packages/drift/src/cache/DriftCache/createDriftCache.ts @@ -30,6 +30,9 @@ export function createDriftCache( preloadRead: ({ value, ...params }) => cache.set(driftCache.readKey(params as DriftReadKeyParams), value), + preloadEvents: ({ value, ...params }) => + cache.set(driftCache.eventsKey(params), value), + invalidateRead: (params) => cache.delete(driftCache.readKey(params)), invalidateReadsMatching(params) { @@ -44,9 +47,6 @@ export function createDriftCache( } } }, - - preloadEvents: ({ value, ...params }) => - cache.set(driftCache.eventsKey(params), value), }); return driftCache; diff --git a/packages/drift/src/cache/DriftCache/types.ts b/packages/drift/src/cache/DriftCache/types.ts index 2a87c35d..f7133eae 100644 --- a/packages/drift/src/cache/DriftCache/types.ts +++ b/packages/drift/src/cache/DriftCache/types.ts @@ -1,18 +1,15 @@ import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { Event, EventName } from "src/adapter/contract/types/event"; import type { FunctionName, FunctionReturn, -} from "src/adapter/contract/types/Function"; +} from "src/adapter/contract/types/function"; +import type { GetEventsParams, ReadParams } from "src/adapter/types"; import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import type { - DriftGetEventsParams, - DriftReadParams, -} from "src/drift/types/DriftContract"; -import type { DeepPartial } from "src/utils/types"; +import type { DeepPartial, MaybePromise } from "src/utils/types"; export type DriftCache = T & { - // Key Generators // + // Key generation // partialReadKey>( params: DeepPartial>, @@ -26,38 +23,38 @@ export type DriftCache = T & { params: DriftEventsKeyParams, ): SimpleCacheKey; - // Cache Management // + // Cache management // preloadRead>( params: DriftReadKeyParams & { value: FunctionReturn; }, - ): void | Promise; + ): MaybePromise; + + preloadEvents>( + params: DriftEventsKeyParams & { + value: readonly Event[]; + }, + ): MaybePromise; invalidateRead>( params: DriftReadKeyParams, - ): void | Promise; + ): MaybePromise; invalidateReadsMatching< TAbi extends Abi, TFunctionName extends FunctionName, >( params: DeepPartial>, - ): void | Promise; - - preloadEvents>( - params: DriftEventsKeyParams & { - value: readonly Event[]; - }, - ): void | Promise; + ): MaybePromise; }; export type DriftReadKeyParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = Omit, "cache">; +> = Omit, "cache">; export type DriftEventsKeyParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, -> = Omit, "cache">; +> = Omit, "cache">; diff --git a/packages/drift/src/cache/utils/DriftCache.ts b/packages/drift/src/cache/utils/DriftCache.ts index f2c3da12..3e6bba2c 100644 --- a/packages/drift/src/cache/utils/DriftCache.ts +++ b/packages/drift/src/cache/utils/DriftCache.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { Event, EventName } from "src/adapter/contract/types/event"; import type { DriftEventsKeyParams } from "src/cache/DriftCache/types"; diff --git a/packages/drift/src/cache/utils/createCachedReadContract.test.ts b/packages/drift/src/cache/utils/createCachedReadContract.test.ts index 9a296d2a..2d4c20b1 100644 --- a/packages/drift/src/cache/utils/createCachedReadContract.test.ts +++ b/packages/drift/src/cache/utils/createCachedReadContract.test.ts @@ -1,5 +1,5 @@ -import { ReadContractStub } from "src/adapter/contract/stubs/ReadContractStub"; -import type { Event } from "src/adapter/contract/types/Event"; +import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; +import type { Event } from "src/adapter/contract/types/event"; import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; import { IERC20 } from "src/utils/testing/IERC20"; import { ALICE, BOB } from "src/utils/testing/accounts"; diff --git a/packages/drift/src/cache/utils/createCachedReadContract.ts b/packages/drift/src/cache/utils/createCachedReadContract.ts index 6c219299..a60a3d87 100644 --- a/packages/drift/src/cache/utils/createCachedReadContract.ts +++ b/packages/drift/src/cache/utils/createCachedReadContract.ts @@ -1,10 +1,10 @@ import type { Abi } from "abitype"; import isMatch from "lodash.ismatch"; -import type { AdapterReadContract } from "src/adapter/contract/types/Contract"; +import type { AdapterReadContract } from "src/adapter/contract/types/contract"; import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import type { CachedReadContract } from "src/contract/CachedContract"; +import type { ReadContract } from "src/contract/types"; // TODO: Figure out a good default cache size const DEFAULT_CACHE_SIZE = 100; @@ -33,7 +33,7 @@ export function createCachedReadContract({ contract, cache = createLruSimpleCache({ max: DEFAULT_CACHE_SIZE }), namespace, -}: CreateCachedReadContractOptions): CachedReadContract { +}: CreateCachedReadContractOptions): ReadContract { // Because this is part of the public API, we won't know if the original // contract is a plain object or a class instance, so we use Object.create to // preserve the original contract's prototype chain when extending, ensuring @@ -42,7 +42,7 @@ export function createCachedReadContract({ const contractPrototype = Object.getPrototypeOf(contract); const newContract = Object.create(contractPrototype); - const overrides: Partial> = { + const overrides: Partial> = { cache, /** diff --git a/packages/drift/src/cache/utils/createCachedReadWriteContract.ts b/packages/drift/src/cache/utils/createCachedReadWriteContract.ts index 099f4dac..10733359 100644 --- a/packages/drift/src/cache/utils/createCachedReadWriteContract.ts +++ b/packages/drift/src/cache/utils/createCachedReadWriteContract.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { AdapterReadWriteContract } from "src/adapter/contract/types/Contract"; +import type { AdapterReadWriteContract } from "src/adapter/contract/types/contract"; import type { CachedReadWriteContract } from "src/cache/types/CachedContract"; import { type CreateCachedReadContractOptions, diff --git a/packages/drift/src/cache/utils/eventsKey.ts b/packages/drift/src/cache/utils/eventsKey.ts index 1202d252..37aefec5 100644 --- a/packages/drift/src/cache/utils/eventsKey.ts +++ b/packages/drift/src/cache/utils/eventsKey.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { EventName } from "src/adapter/contract/types/Event"; +import type { EventName } from "src/adapter/contract/types/event"; import type { DriftEventsKeyParams } from "src/cache/DriftCache/types"; import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; diff --git a/packages/drift/src/cache/utils/invalidateRead.ts b/packages/drift/src/cache/utils/invalidateRead.ts index 13b1a12f..a6dc5883 100644 --- a/packages/drift/src/cache/utils/invalidateRead.ts +++ b/packages/drift/src/cache/utils/invalidateRead.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { FunctionName } from "src/adapter/contract/types/function"; import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; import type { SimpleCache } from "src/cache/SimpleCache/types"; import { readKey } from "src/cache/utils/readKey"; diff --git a/packages/drift/src/cache/utils/invalidateReadsMatching.ts b/packages/drift/src/cache/utils/invalidateReadsMatching.ts index 073df1b7..b2498532 100644 --- a/packages/drift/src/cache/utils/invalidateReadsMatching.ts +++ b/packages/drift/src/cache/utils/invalidateReadsMatching.ts @@ -1,6 +1,6 @@ import type { Abi } from "abitype"; import isMatch from "lodash.ismatch"; -import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { FunctionName } from "src/adapter/contract/types/function"; import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; import { partialReadKey } from "src/cache/utils/partialReadKey"; diff --git a/packages/drift/src/cache/utils/partialReadKey.ts b/packages/drift/src/cache/utils/partialReadKey.ts index 933dd59d..1e9bd920 100644 --- a/packages/drift/src/cache/utils/partialReadKey.ts +++ b/packages/drift/src/cache/utils/partialReadKey.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { FunctionName } from "src/adapter/contract/types/function"; import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; diff --git a/packages/drift/src/cache/utils/preloadRead.ts b/packages/drift/src/cache/utils/preloadRead.ts index 012d1edc..d058a2af 100644 --- a/packages/drift/src/cache/utils/preloadRead.ts +++ b/packages/drift/src/cache/utils/preloadRead.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { FunctionName, FunctionReturn } from "src/adapter/contract/types/Function"; +import type { FunctionName, FunctionReturn } from "src/adapter/contract/types/function"; import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; import type { SimpleCache } from "src/cache/SimpleCache/types"; import { readKey } from "src/cache/utils/readKey"; diff --git a/packages/drift/src/cache/utils/readKey.ts b/packages/drift/src/cache/utils/readKey.ts index 7d4f41c5..958f65d7 100644 --- a/packages/drift/src/cache/utils/readKey.ts +++ b/packages/drift/src/cache/utils/readKey.ts @@ -1,5 +1,5 @@ import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/Function"; +import type { FunctionName } from "src/adapter/contract/types/function"; import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; import { partialReadKey } from "src/cache/utils/partialReadKey"; diff --git a/packages/drift/src/contract/CachedContract.ts b/packages/drift/src/contract/CachedContract.ts deleted file mode 100644 index 528fe5f4..00000000 --- a/packages/drift/src/contract/CachedContract.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Abi } from "abitype"; -import type { - AdapterReadContract, - AdapterReadWriteContract, - ContractReadArgs, -} from "src/adapter/contract/types/Contract"; -import type { FunctionName } from "src/adapter/contract/types/Function"; -import type { SimpleCache } from "src/exports"; -import type { DeepPartial } from "src/utils/types"; - -export interface CachedReadContract - extends AdapterReadContract { - cache: SimpleCache; - namespace?: string; - clearCache(): void; - deleteRead>( - ...[functionName, args, options]: ContractReadArgs - ): void; - deleteReadsMatching>( - ...[functionName, args, options]: DeepPartial< - ContractReadArgs - > - ): void; -} - -export interface CachedReadWriteContract - extends CachedReadContract, - AdapterReadWriteContract {} diff --git a/packages/drift/src/contract/createReadContract.ts b/packages/drift/src/contract/createReadContract.ts new file mode 100644 index 00000000..15f0ff86 --- /dev/null +++ b/packages/drift/src/contract/createReadContract.ts @@ -0,0 +1,63 @@ +import type { Abi } from "abitype"; +import type { AdapterReadContract } from "src/adapter/contract/types/contract"; +import type { DriftCache } from "src/cache/DriftCache/types"; +import type { ReadContract } from "src/contract/types"; +import { extendInstance } from "src/utils/extendInstance"; + +interface CreateReadContractParams< +TAbi extends Abi, +TContract extends AdapterReadContract, +TCache extends DriftCache, +> { + contract: TContract; + cache: TCache; + namespace: string; +} + +/** + * Extends an {@linkcode AdapterReadContract} with additional API methods for + * use with Drift clients. + */ +export function createReadContract< + TAbi extends Abi, + TContract extends AdapterReadContract, + TCache extends DriftCache, +>(contract: TContract, cache: TCache): ReadContract { + const readContract: ReadContract = extendInstance< + TContract, + Omit + >(contract, { + cache, + + partialReadKey: (fn, args, options) => + cache.partialReadKey({ abi: contract.abi, fn, args, address: contract.address, namespace, }), + + readKey: (params) => driftCache.partialReadKey(params), + + eventsKey: ({ abi, namespace, ...params }) => + createSimpleCacheKey([namespace, "events", params]), + + preloadRead: ({ value, ...params }) => + cache.set(driftCache.readKey(params as DriftReadKeyParams), value), + + preloadEvents: ({ value, ...params }) => + cache.set(driftCache.eventsKey(params), value), + + invalidateRead: (params) => cache.delete(driftCache.readKey(params)), + + invalidateReadsMatching(params) { + const sourceKey = driftCache.partialReadKey(params); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SimpleCacheKey[]) + ) { + cache.delete(key); + } + } + }, + }); + + return readContract; +} diff --git a/packages/drift/src/contract/types.ts b/packages/drift/src/contract/types.ts new file mode 100644 index 00000000..d4500b61 --- /dev/null +++ b/packages/drift/src/contract/types.ts @@ -0,0 +1,90 @@ +import type { Abi } from "abitype"; +import type { + AdapterReadContract, + AdapterReadWriteContract, + ContractGetEventsArgs, + ContractReadArgs, +} from "src/adapter/contract/types/contract"; +import type { EventName } from "src/adapter/contract/types/event"; +import type { Event } from "src/adapter/contract/types/event"; +import type { + FunctionName, + FunctionReturn, +} from "src/adapter/contract/types/function"; +import type { + DriftCache, + DriftReadKeyParams, +} from "src/cache/DriftCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import type { Address } from "src/types"; +import type { MaybePromise } from "src/utils/types"; + +export interface ContractParams { + abi: TAbi; + address: Address; + cache?: SimpleCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; +} + +export type ReadContract< + TAbi extends Abi = Abi, + TAdapterContract extends + AdapterReadContract = AdapterReadContract, + TCache extends DriftCache = DriftCache, +> = TAdapterContract & { + cache: TCache; + + // Key generation // + + partialReadKey>( + ...args: Partial> + ): string; + + readKey>( + ...args: ContractReadArgs + ): string; + + eventsKey>( + ...args: ContractGetEventsArgs + ): string; + + // Cache management // + + preloadRead>( + params: ContractReadKeyParams & { + value: FunctionReturn; + }, + ): MaybePromise; + + preloadEvents>( + ...args: ContractGetEventsArgs & { + value: readonly Event[]; + } + ): MaybePromise; + + invalidateRead>( + ...args: ContractReadArgs + ): MaybePromise; + + invalidateReadsMatching>( + ...args: Partial> + ): MaybePromise; + + invalidateAllReads(): void; +}; + +export interface ReadWriteContract + extends ReadContract, + AdapterReadWriteContract {} + +export type ContractReadKeyParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName< + TAbi, + "pure" | "view" + >, +> = Omit, keyof ContractParams>; diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts index 6e87cdbe..b6074d62 100644 --- a/packages/drift/src/drift/Drift.ts +++ b/packages/drift/src/drift/Drift.ts @@ -1,10 +1,10 @@ import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/Event"; +import type { Event, EventName } from "src/adapter/contract/types/event"; import type { DecodedFunctionData, FunctionName, FunctionReturn, -} from "src/adapter/contract/types/Function"; +} from "src/adapter/contract/types/function"; import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; import { createDriftCache } from "src/cache/DriftCache/createDriftCache"; import type { DriftCache } from "src/cache/DriftCache/types"; @@ -12,9 +12,9 @@ import type { SimpleCache } from "src/cache/SimpleCache/types"; import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; import { createCachedReadWriteContract } from "src/cache/utils/createCachedReadWriteContract"; import type { - CachedReadContract, - CachedReadWriteContract, -} from "src/contract/CachedContract"; + ReadContract, + ReadWriteContract, +} from "src/contract/types"; import type { ContractParams, DecodeFunctionDataParams, @@ -28,8 +28,8 @@ export type DriftContract< TAbi extends Abi, TAdapter extends Adapter = Adapter, > = TAdapter extends ReadWriteAdapter - ? CachedReadWriteContract - : CachedReadContract; + ? ReadWriteContract + : ReadContract; export interface DriftOptions { cache?: TCache; @@ -109,18 +109,9 @@ export class Drift< address, cache = this.cache, namespace = this.namespace, - }: ContractParams): DriftContract => - this.isReadWrite() - ? createCachedReadWriteContract({ - contract: this.adapter.readWriteContract(abi, address), - cache, - namespace, - }) - : (createCachedReadContract({ - contract: this.adapter.readContract(abi, address), - cache, - namespace, - }) as DriftContract); + }: ContractParams): DriftContract => { + con + } /** * Reads a specified function from a contract. diff --git a/packages/drift/src/drift/MockDrift.ts b/packages/drift/src/drift/MockDrift.ts index 024e59dd..16e2973e 100644 --- a/packages/drift/src/drift/MockDrift.ts +++ b/packages/drift/src/drift/MockDrift.ts @@ -1,7 +1,7 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; -import type { ReadWriteContractStub } from "src/adapter/contract/stubs/ReadWriteContractStub"; -import type { CachedReadWriteContract } from "src/contract/CachedContract"; +import type { ReadWriteContractStub } from "src/adapter/contract/mocks/ReadWriteContractStub"; +import type { ReadWriteContract } from "src/contract/types"; import { Drift, type DriftOptions } from "src/drift/Drift"; import type { SimpleCache } from "src/exports"; import type { ContractParams } from "src/types"; @@ -16,5 +16,5 @@ export class MockDrift extends Drift< declare contract: ( params: ContractParams, - ) => CachedReadWriteContract & ReadWriteContractStub; + ) => ReadWriteContract & ReadWriteContractStub; } diff --git a/packages/drift/src/types.ts b/packages/drift/src/types.ts index 2b66ee1c..3435f0dc 100644 --- a/packages/drift/src/types.ts +++ b/packages/drift/src/types.ts @@ -1,87 +1,4 @@ -import type { Abi } from "abitype"; -import type { - ContractGetEventsOptions, - ContractReadOptions, - ContractWriteOptions, -} from "src/adapter/contract/types/Contract"; -import type { EventName } from "src/adapter/contract/types/Event"; -import type { - FunctionArgs, - FunctionName, -} from "src/adapter/contract/types/Function"; -import type { TransactionReceipt } from "src/adapter/network/Transaction"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import type { EmptyObject } from "src/utils/types"; - -export interface ContractParams { - abi: TAbi; - address: string; - cache?: SimpleCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -export type ReadParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = { - fn: TFunctionName; -} & (FunctionArgs extends EmptyObject - ? { - args?: FunctionArgs; - } - : { - args: FunctionArgs; - }) & - ContractReadOptions & - ContractParams; - -export interface GetEventsParams< - TAbi extends Abi, - TEventName extends EventName, -> extends ContractGetEventsOptions, - ContractParams { - event: TEventName; -} - -export type WriteParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = ContractWriteOptions & { - abi: TAbi; - address: string; - fn: TFunctionName; - onMined?: (receipt?: TransactionReceipt) => void; -} & (FunctionArgs extends EmptyObject - ? { - args?: FunctionArgs; - } - : { - args: FunctionArgs; - }); - -export type EncodeFunctionDataParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = { - abi: TAbi; - fn: TFunctionName; -} & (FunctionArgs extends EmptyObject - ? { - args?: FunctionArgs; - } - : { - args: FunctionArgs; - }); - -export interface DecodeFunctionDataParams< - TAbi extends Abi, - TFunctionName extends FunctionName, -> { - abi: TAbi; - data: string; - fn?: TFunctionName; -} +export type HexString = string; +export type Address = HexString; +export type Bytes = HexString; +export type TransactionHash = HexString; diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index d5b8c4bd..5b254c6d 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -1,4 +1,6 @@ -export type EmptyObject = Record; +export type EmptyObject = Record; + +export type MaybePromise = T | Promise; /** * Combines members of an intersection into a readable type. @@ -20,3 +22,12 @@ export type RequiredKeys = Prettify< [P in K]-?: NonNullable; } >; + +/** + * Make all properties in `T` whose keys are in the union `K` optional. + */ +export type OptionalKeys = Prettify< + Omit & { + [P in K]?: T[P]; + } +>; From ff35296fb7d1b6a92f51768d65e7df0b9cd5b5ba Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Thu, 3 Oct 2024 01:14:12 -0500 Subject: [PATCH 22/49] Fix MockAdapter and tests --- .../drift/src/adapter/MockAdapter.test.ts | 57 ++- packages/drift/src/adapter/MockAdapter.ts | 414 ++++++++---------- 2 files changed, 222 insertions(+), 249 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index 8dbd37b0..f3a26388 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -59,7 +59,7 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const { blockNumber, timestamp } = await adapter.getBlock(); + const { blockNumber, timestamp = 0n } = (await adapter.getBlock()) ?? {}; const block1: Block = { blockNumber: blockNumber ?? 0n + 1n, timestamp: timestamp + 1n, @@ -235,14 +235,17 @@ describe("MockAdapter", () => { describe("decodeFunctionData", () => { it("Throws an error by default", async () => { const adapter = new MockAdapter(); - expect( - (async () => - adapter.decodeFunctionData({ - abi: IERC20.abi, - fn: "balanceOf", - data: "0x", - }))(), - ).rejects.toThrowError(); + let error: unknown; + try { + adapter.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); }); it("Can be stubbed", async () => { @@ -286,13 +289,17 @@ describe("MockAdapter", () => { describe("getEvents", () => { it("Rejects with an error by default", async () => { const adapter = new MockAdapter(); - expect( - adapter.getEvents({ + let error: unknown; + try { + await adapter.getEvents({ abi: IERC20.abi, address: "0x", event: "Transfer", - }), - ).rejects.toThrowError(); + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); }); it("Can be stubbed", async () => { @@ -365,13 +372,17 @@ describe("MockAdapter", () => { describe("read", () => { it("Rejects with an error by default", async () => { const adapter = new MockAdapter(); - expect( - adapter.read({ + let error: unknown; + try { + await adapter.read({ abi: IERC20.abi, address: "0x", fn: "symbol", - }), - ).rejects.toThrowError(); + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); }); it("Can be stubbed", async () => { @@ -433,14 +444,18 @@ describe("MockAdapter", () => { describe("simulateWrite", () => { it("Rejects with an error by default", async () => { const adapter = new MockAdapter(); - expect( - adapter.simulateWrite({ + let error: unknown; + try { + await adapter.simulateWrite({ abi: IERC20.abi, address: "0x", fn: "transfer", args: { to: "0x", value: 123n }, - }), - ).rejects.toThrowError(); + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); }); it("Can be stubbed", async () => { diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 2dd8c3fa..dd6397b7 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -1,16 +1,22 @@ import type { Abi } from "abitype"; -import { type SinonStub, stub as createStub } from "sinon"; +import stringify from "fast-json-stable-stringify"; +import { type SinonStub, stub as sinonStub } from "sinon"; import type { Event, EventName } from "src/adapter/contract/types/event"; import type { FunctionName, FunctionReturn, } from "src/adapter/contract/types/function"; +import type { Block } from "src/adapter/network/types/Block"; import type { NetworkGetBalanceArgs, NetworkGetBlockArgs, NetworkGetTransactionArgs, NetworkWaitForTransactionArgs, } from "src/adapter/network/types/NetworkAdapter"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/network/types/Transaction"; import type { DecodeFunctionDataParams, EncodeFunctionDataParams, @@ -19,6 +25,7 @@ import type { ReadWriteAdapter, WriteParams, } from "src/adapter/types"; +import type { SimpleCacheKey } from "src/exports"; import type { Address, Bytes, TransactionHash } from "src/types"; import type { OptionalKeys } from "src/utils/types"; @@ -26,146 +33,165 @@ import type { OptionalKeys } from "src/utils/types"; export class MockAdapter implements ReadWriteAdapter { // stubs // - protected stubs = new Map(); + protected mocks = new Map(); - protected getStub SinonStub)>({ + protected getMock({ + method, key, create, }: { - key: string; - create?: TInsert; - }): TInsert extends false | undefined ? SinonStub | undefined : SinonStub { - let stub = this.stubs.get(key); - if (!stub && create) { - stub = typeof create === "function" ? create() : createStub(); - this.stubs.set(key, stub); + method: keyof ReadWriteAdapter; + key?: SimpleCacheKey; + create?: () => SinonStub; + }): SinonStub { + let mockKey = method; + if (key) { + mockKey += `:${stringify(key)}`; } - return stub as any; + let mock = this.mocks.get(mockKey); + if (!mock) { + mock = create + ? create() + : // Throws an error by default if no explicit return value is set. + sinonStub().throws( + new NotImplementedError({ + method, + mockKey, + }), + ); + this.mocks.set(mockKey, mock); + } + return mock; } reset() { - this.stubs.clear(); + this.mocks.clear(); } // getBalance // - protected get getBalanceStub() { - return this.getStub({ - key: "getBalance", - create: () => createStub().resolves(0n), - }); + onGetBalance(...args: Partial) { + return this.getMock({ + method: "getBalance", + create: () => sinonStub().resolves(0n), + }).withArgs(...args); } - getBalance(...args: NetworkGetBalanceArgs) { - return this.getBalanceStub(...args); - } - - onGetBalance(...args: Partial) { - return this.getBalanceStub.withArgs(...args); + getBalance(...args: NetworkGetBalanceArgs): Promise { + return this.getMock({ + method: "getBalance", + create: () => sinonStub().resolves(0n), + })(...args); } // getBlock // - protected get getBlockStub() { - return this.getStub({ - key: "getBlock", + onGetBlock(...args: Partial) { + return this.getMock({ + method: "getBlock", create: () => - createStub().resolves({ + sinonStub().resolves({ blockNumber: 0n, timestamp: 0n, }), - }); + }).withArgs(...args); } - getBlock(...args: NetworkGetBlockArgs) { - return this.getBlockStub(...args); - } - - onGetBlock(...args: Partial) { - return this.getBlockStub.withArgs(...args); + async getBlock(...args: NetworkGetBlockArgs): Promise { + return this.getMock({ + method: "getBlock", + create: () => + sinonStub().resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + })(...args); } // getChainId // - protected get getChainIdStub() { - return this.getStub({ - key: "getChainId", - create: () => createStub().resolves(0), + onGetChainId() { + return this.getMock({ + method: "getChainId", + create: () => sinonStub().resolves(96024), }); } - getChainId() { - return this.getChainIdStub(); - } - - onGetChainId() { - return this.getChainIdStub; + async getChainId(): Promise { + return this.getMock({ + method: "getChainId", + create: () => sinonStub().resolves(96024), + })(); } // getTransaction // - protected get getTransactionStub() { - return this.getStub({ - key: "getTransaction", - create: () => createStub().resolves(undefined), - }); - } - - getTransaction(...args: NetworkGetTransactionArgs) { - return this.getTransactionStub(...args); + onGetTransaction(...args: Partial) { + return this.getMock({ + method: "getTransaction", + create: () => sinonStub().resolves(undefined), + }).withArgs(...args); } - onGetTransaction(...args: Partial) { - return this.getTransactionStub.withArgs(...args); + async getTransaction( + ...args: NetworkGetTransactionArgs + ): Promise { + return this.getMock({ + method: "getTransaction", + create: () => sinonStub().resolves(undefined), + })(...args); } // waitForTransaction // - protected get waitForTransactionStub() { - return this.getStub({ - key: "waitForTransaction", - create: () => createStub().resolves(undefined), - }); + onWaitForTransaction(...args: Partial) { + return this.getMock({ + method: "waitForTransaction", + create: () => sinonStub().resolves(undefined), + }).withArgs(...args); } - waitForTransaction(...args: NetworkWaitForTransactionArgs) { - return this.waitForTransactionStub(...args); - } - - onWaitForTransaction(...args: Partial) { - return this.waitForTransactionStub.withArgs(...args); + async waitForTransaction( + ...args: NetworkWaitForTransactionArgs + ): Promise { + return this.getMock({ + method: "waitForTransaction", + create: () => sinonStub().resolves(undefined), + })(...args); } // encodeFunction // - protected get encodeFunctionDataStub() { - return this.getStub({ - key: "encodeFunctionData", - create: () => createStub().returns("0x0"), - }); - } - - encodeFunctionData< + onEncodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: EncodeFunctionDataParams): Bytes { - return this.encodeFunctionDataStub(params); + >(params: OnEncodeFunctionDataParams) { + return this.getMock({ + method: "encodeFunctionData", + create: () => sinonStub().returns("0x0"), + }).withArgs(params); } - onEncodeFunctionData< + encodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: EncodeFunctionDataStubParams) { - return this.encodeFunctionDataStub.withArgs(params); + >(params: EncodeFunctionDataParams): Bytes { + return this.getMock({ + method: "encodeFunctionData", + create: () => sinonStub().returns("0x0"), + })(params); } // decodeFunction // - // TODO: This should be specific to the abi to ensure the correct return type. - protected decodeFunctionDataStubKey({ - fn, - }: Partial) { - return `decodeFunctionData:${fn}`; + onDecodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: OnDecodeFunctionDataParams) { + return this.getMock({ + method: "decodeFunctionData", + key: params.fn, + }).withArgs(params); } decodeFunctionData< @@ -174,216 +200,148 @@ export class MockAdapter implements ReadWriteAdapter { >( params: DecodeFunctionDataParams, ): FunctionReturn { - const stub = this.getStub({ - key: this.decodeFunctionDataStubKey(params), - }); - if (!stub) { - throw new NotImplementedError({ - name: params.fn || params.data, - method: "decodeFunctionData", - stubMethod: "onDecodeFunctionData", - }); - } - return stub(params); - } - - // TODO: Does calling `onDecodeFunctionData` without calling any methods on - // it, e.g. `returns`, break the error behavior? - onDecodeFunctionData< - TAbi extends Abi, - TFunctionName extends FunctionName, - >(params: DecodeFunctionDataStubParams) { - return this.getStub({ - key: this.decodeFunctionDataStubKey(params), - create: true, - }).withArgs(params); + // TODO: This should be specific to the abi to ensure the correct return type. + return this.getMock({ + method: "decodeFunctionData", + key: params.fn, + })(params); } // getEvents // - protected getEventsStubKey({ - address, - event, - }: Partial>): string { - return `getEvents:${address}:${event}`; - } - - getEvents>( + onGetEvents>( params: GetEventsParams, - ): Promise[]> { - const stub = this.stubs.get(this.getEventsStubKey(params)); - if (!stub) { - return Promise.reject( - new NotImplementedError({ - name: params.event, - method: "getEvents", - stubMethod: "onGetEvents", - }), - ); - } - return Promise.resolve(stub(params)); + ) { + return this.getMock({ + method: "getEvents", + key: params.event, + }).withArgs(params); } - onGetEvents>( + async getEvents>( params: GetEventsParams, - ) { - return this.getStub< - [GetEventsParams], - Promise[]> - >({ - key: this.getEventsStubKey(params), - args: [params], - }); + ): Promise[]> { + return this.getMock({ + method: "getEvents", + key: params.event, + })(params); } // read // - protected readStubKey({ address, fn }: ReadStubParams) { - return `read:${address}:${fn}`; + onRead< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: OnReadParams) { + return this.getMock({ + method: "read", + key: params.fn, + }).withArgs(params); } - read< + async read< TAbi extends Abi, TFunctionName extends FunctionName, >( params: ReadParams, ): Promise> { - const stub = this.stubs.get(this.readStubKey(params)); - if (!stub) { - return Promise.reject( - new NotImplementedError({ - name: params.fn, - method: "read", - stubMethod: "onRead", - }), - ); - } - return Promise.resolve(stub(params)); - } - - onRead< - TAbi extends Abi, - TFunctionName extends FunctionName, - >(params: ReadStubParams) { - return this.getStub< - [ReadStubParams], - Promise> - >({ - key: this.readStubKey(params), - args: [params], - }); + return this.getMock({ + method: "read", + key: params.fn, + })(params); } // simulateWrite // - protected simulateWriteStubKey({ address, fn }: WriteStubParams) { - return `simulateWrite:${address}:${fn}`; + onSimulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: OnWriteParams) { + return this.getMock({ + method: "simulateWrite", + key: params.fn, + }).withArgs(params); } - simulateWrite< + async simulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, >( params: WriteParams, ): Promise> { - const stub = this.stubs.get(this.simulateWriteStubKey(params)); - if (!stub) { - return Promise.reject( - new NotImplementedError({ - name: params.fn, - method: "simulateWrite", - stubMethod: "onSimulateWrite", - }), - ); - } - return Promise.resolve(stub(params)); - } - - onSimulateWrite< - TAbi extends Abi, - TFunctionName extends FunctionName, - >(params: WriteStubParams) { - return this.getStub< - [WriteStubParams], - Promise> - >({ - key: this.simulateWriteStubKey(params), - args: [params], - }); + return this.getMock({ + method: "simulateWrite", + key: params.fn, + })(params); } // write // - protected get writeStub() { - return this.getStub<[WriteStubParams], Bytes>({ - key: "write", - }).returns("0x0"); - } - - write< + onWrite< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteParams): Promise { - return Promise.resolve(this.writeStub(params)); + >(params: OnWriteParams) { + return this.getMock({ + method: "write", + key: params.fn, + create: () => sinonStub().resolves("0x0"), + }).withArgs(params); } - onWrite< + async write< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteStubParams) { - return this.writeStub.withArgs(params); + >(params: WriteParams): Promise { + return this.getMock({ + method: "write", + key: params.fn, + create: () => sinonStub().resolves("0x0"), + })(params); } // getSignerAddress // - protected get getSignerAddressStub() { - const key = "getSignerAddress"; - let stub = this.stubs.get(key); - if (!stub) { - stub = createStub().resolves("0xMockSigner"); - this.stubs.set(key, stub); - } - } - onGetSignerAddress() { - return this.getSignerAddressStub; + return this.getMock({ + method: "getSignerAddress", + create: () => sinonStub().resolves("0xMockSigner"), + }); } - getSignerAddress(): Promise
{ - return Promise.resolve(this.getSignerAddressStub()); + async getSignerAddress(): Promise
{ + return this.getMock({ + method: "getSignerAddress", + create: () => sinonStub().resolves("0xMockSigner"), + })(); } } // TODO: Make address optional and create a key from the abi entry and fn name. -export type ReadStubParams< +export type OnReadParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args">; +> = OptionalKeys, "args" | "address">; -export type WriteStubParams< +export type OnWriteParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args" | "abi">; +> = OptionalKeys, "args" | "address">; -export type EncodeFunctionDataStubParams< +export type OnEncodeFunctionDataParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, > = OptionalKeys, "args">; -export type DecodeFunctionDataStubParams< +export type OnDecodeFunctionDataParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "data" | "abi">; +> = OptionalKeys, "data">; class NotImplementedError extends Error { - constructor({ - method, - stubMethod, - name, - }: { method: string; stubMethod: string; name?: string }) { - // TODO: This error message is not accurate. + constructor({ method, mockKey }: { method: string; mockKey: string }) { super( - `Called ${method}${name ? ` for "${name}"` : ""} on a MockAdapter without a return value. The function must be stubbed first:\n\tadapter.${stubMethod}("${name}").resolves(value)`, + `Called ${method} on a MockAdapter without a return value. No mock found with key "${mockKey}". Stub the return value first: + adapter.on${method.replace(/^./, (c) => c.toUpperCase())}(...args).resolves(value)`, ); this.name = "NotImplementedError"; } From ffd5ea6cdb26ad0c4f6fdef0d1e7ea06792bbe5f Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Thu, 3 Oct 2024 17:10:33 -0500 Subject: [PATCH 23/49] Improve MockAdapter, centralize default stub behavior --- .../drift/src/adapter/MockAdapter.test.ts | 6 +- packages/drift/src/adapter/MockAdapter.ts | 204 +++++++++++------- 2 files changed, 124 insertions(+), 86 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index f3a26388..ce25faca 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -519,7 +519,7 @@ describe("MockAdapter", () => { fn: "transfer", args: { to: "0x", value: 123n }, }) - .returns("0x123"); + .resolves("0x123"); expect( await adapter.write({ abi: IERC20.abi, @@ -542,8 +542,8 @@ describe("MockAdapter", () => { ...params1, args: { to: "0x2", value: 123n }, }; - adapter.onWrite(params1).returns("0x1"); - adapter.onWrite(params2).returns("0x2"); + adapter.onWrite(params1).resolves("0x1"); + adapter.onWrite(params2).resolves("0x2"); expect(await adapter.write(params1)).toBe("0x1"); expect(await adapter.write(params2)).toBe("0x2"); }); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index dd6397b7..f0a74020 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -35,33 +35,34 @@ export class MockAdapter implements ReadWriteAdapter { protected mocks = new Map(); - protected getMock({ + protected getMock({ method, key, create, }: { method: keyof ReadWriteAdapter; key?: SimpleCacheKey; - create?: () => SinonStub; - }): SinonStub { - let mockKey = method; + create?: (mock: SinonStub) => SinonStub; + }): SinonStub { + let mockKey: string = method; if (key) { mockKey += `:${stringify(key)}`; } let mock = this.mocks.get(mockKey); if (!mock) { - mock = create - ? create() - : // Throws an error by default if no explicit return value is set. - sinonStub().throws( - new NotImplementedError({ - method, - mockKey, - }), - ); + mock = sinonStub().throws( + new NotImplementedError({ + method, + mockKey, + }), + ); + if (create) { + // TODO: Cleanup type casting + mock = create(mock as any); + } this.mocks.set(mockKey, mock); } - return mock; + return mock as any; } reset() { @@ -71,34 +72,34 @@ export class MockAdapter implements ReadWriteAdapter { // getBalance // onGetBalance(...args: Partial) { - return this.getMock({ + return this.getMock>({ method: "getBalance", - create: () => sinonStub().resolves(0n), + create: (mock) => mock.resolves(0n), }).withArgs(...args); } - getBalance(...args: NetworkGetBalanceArgs): Promise { - return this.getMock({ + async getBalance(...args: NetworkGetBalanceArgs) { + return this.getMock>({ method: "getBalance", - create: () => sinonStub().resolves(0n), + create: (mock) => mock.resolves(0n), })(...args); } // getBlock // onGetBlock(...args: Partial) { - return this.getMock({ + return this.getMock>({ method: "getBlock", - create: () => - sinonStub().resolves({ + create: (mock) => + mock.resolves({ blockNumber: 0n, timestamp: 0n, }), }).withArgs(...args); } - async getBlock(...args: NetworkGetBlockArgs): Promise { - return this.getMock({ + async getBlock(...args: NetworkGetBlockArgs) { + return this.getMock>({ method: "getBlock", create: () => sinonStub().resolves({ @@ -111,52 +112,60 @@ export class MockAdapter implements ReadWriteAdapter { // getChainId // onGetChainId() { - return this.getMock({ + return this.getMock<[], number>({ method: "getChainId", - create: () => sinonStub().resolves(96024), + create: (mock) => mock.resolves(96024), }); } - async getChainId(): Promise { - return this.getMock({ + async getChainId() { + return this.getMock<[], number>({ method: "getChainId", - create: () => sinonStub().resolves(96024), + create: (mock) => mock.resolves(96024), })(); } // getTransaction // onGetTransaction(...args: Partial) { - return this.getMock({ + return this.getMock< + NetworkGetTransactionArgs, + Promise + >({ method: "getTransaction", - create: () => sinonStub().resolves(undefined), + create: (mock) => mock.resolves(undefined), }).withArgs(...args); } - async getTransaction( - ...args: NetworkGetTransactionArgs - ): Promise { - return this.getMock({ + async getTransaction(...args: NetworkGetTransactionArgs) { + return this.getMock< + NetworkGetTransactionArgs, + Promise + >({ method: "getTransaction", - create: () => sinonStub().resolves(undefined), + create: (mock) => mock.resolves(undefined), })(...args); } // waitForTransaction // onWaitForTransaction(...args: Partial) { - return this.getMock({ + return this.getMock< + NetworkWaitForTransactionArgs, + Promise + >({ method: "waitForTransaction", - create: () => sinonStub().resolves(undefined), + create: (mock) => mock.resolves(undefined), }).withArgs(...args); } - async waitForTransaction( - ...args: NetworkWaitForTransactionArgs - ): Promise { - return this.getMock({ + async waitForTransaction(...args: NetworkWaitForTransactionArgs) { + return this.getMock< + NetworkWaitForTransactionArgs, + Promise + >({ method: "waitForTransaction", - create: () => sinonStub().resolves(undefined), + create: (mock) => mock.resolves(undefined), })(...args); } @@ -166,19 +175,19 @@ export class MockAdapter implements ReadWriteAdapter { TAbi extends Abi, TFunctionName extends FunctionName, >(params: OnEncodeFunctionDataParams) { - return this.getMock({ + return this.getMock<[EncodeFunctionDataParams], Bytes>({ method: "encodeFunctionData", - create: () => sinonStub().returns("0x0"), + create: (mock) => mock.returns("0x0"), }).withArgs(params); } encodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: EncodeFunctionDataParams): Bytes { - return this.getMock({ + >(params: EncodeFunctionDataParams) { + return this.getMock<[EncodeFunctionDataParams], Bytes>({ method: "encodeFunctionData", - create: () => sinonStub().returns("0x0"), + create: (mock) => mock.returns("0x0"), })(params); } @@ -188,7 +197,10 @@ export class MockAdapter implements ReadWriteAdapter { TAbi extends Abi, TFunctionName extends FunctionName, >(params: OnDecodeFunctionDataParams) { - return this.getMock({ + return this.getMock< + [DecodeFunctionDataParams], + FunctionReturn + >({ method: "decodeFunctionData", key: params.fn, }).withArgs(params); @@ -197,12 +209,14 @@ export class MockAdapter implements ReadWriteAdapter { decodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: DecodeFunctionDataParams, - ): FunctionReturn { - // TODO: This should be specific to the abi to ensure the correct return type. - return this.getMock({ + >(params: DecodeFunctionDataParams) { + return this.getMock< + [DecodeFunctionDataParams], + FunctionReturn + >({ method: "decodeFunctionData", + // TODO: This should be specific to the abi to ensure the correct return + // type. key: params.fn, })(params); } @@ -210,9 +224,12 @@ export class MockAdapter implements ReadWriteAdapter { // getEvents // onGetEvents>( - params: GetEventsParams, + params: OnGetEventsParams, ) { - return this.getMock({ + return this.getMock< + [GetEventsParams], + Promise[]> + >({ method: "getEvents", key: params.event, }).withArgs(params); @@ -220,8 +237,11 @@ export class MockAdapter implements ReadWriteAdapter { async getEvents>( params: GetEventsParams, - ): Promise[]> { - return this.getMock({ + ) { + return this.getMock< + [GetEventsParams], + Promise[]> + >({ method: "getEvents", key: params.event, })(params); @@ -233,19 +253,23 @@ export class MockAdapter implements ReadWriteAdapter { TAbi extends Abi, TFunctionName extends FunctionName, >(params: OnReadParams) { - return this.getMock({ + return this.getMock< + [ReadParams], + FunctionReturn + >({ method: "read", key: params.fn, - }).withArgs(params); + }).withArgs(params as Partial>); } async read< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: ReadParams, - ): Promise> { - return this.getMock({ + >(params: ReadParams) { + return this.getMock< + [ReadParams], + FunctionReturn + >({ method: "read", key: params.fn, })(params); @@ -257,19 +281,23 @@ export class MockAdapter implements ReadWriteAdapter { TAbi extends Abi, TFunctionName extends FunctionName, >(params: OnWriteParams) { - return this.getMock({ + return this.getMock< + [WriteParams], + Promise> + >({ method: "simulateWrite", key: params.fn, - }).withArgs(params); + }).withArgs(params as Partial>); } async simulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: WriteParams, - ): Promise> { - return this.getMock({ + >(params: WriteParams) { + return this.getMock< + [WriteParams], + Promise> + >({ method: "simulateWrite", key: params.fn, })(params); @@ -281,42 +309,52 @@ export class MockAdapter implements ReadWriteAdapter { TAbi extends Abi, TFunctionName extends FunctionName, >(params: OnWriteParams) { - return this.getMock({ + return this.getMock< + [WriteParams], + Promise + >({ method: "write", key: params.fn, - create: () => sinonStub().resolves("0x0"), - }).withArgs(params); + create: (mock) => mock.resolves("0x0"), + }).withArgs(params as Partial>); } async write< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteParams): Promise { - return this.getMock({ + >(params: WriteParams) { + return this.getMock< + [WriteParams], + Promise + >({ method: "write", key: params.fn, - create: () => sinonStub().resolves("0x0"), + create: (mock) => mock.resolves("0x0"), })(params); } // getSignerAddress // onGetSignerAddress() { - return this.getMock({ + return this.getMock<[], Address>({ method: "getSignerAddress", - create: () => sinonStub().resolves("0xMockSigner"), + create: (mock) => mock.resolves("0xMockSigner"), }); } - async getSignerAddress(): Promise
{ - return this.getMock({ + async getSignerAddress() { + return this.getMock<[], Address>({ method: "getSignerAddress", - create: () => sinonStub().resolves("0xMockSigner"), + create: (mock) => mock.resolves("0xMockSigner"), })(); } } -// TODO: Make address optional and create a key from the abi entry and fn name. +export type OnGetEventsParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = OptionalKeys, "address">; + export type OnReadParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, @@ -337,7 +375,7 @@ export type OnDecodeFunctionDataParams< TFunctionName extends FunctionName = FunctionName, > = OptionalKeys, "data">; -class NotImplementedError extends Error { +export class NotImplementedError extends Error { constructor({ method, mockKey }: { method: string; mockKey: string }) { super( `Called ${method} on a MockAdapter without a return value. No mock found with key "${mockKey}". Stub the return value first: From 15a625f4992b6ae22e687168d7826f8ffbdf657f Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Thu, 3 Oct 2024 17:12:23 -0500 Subject: [PATCH 24/49] Fix create fn --- packages/drift/src/adapter/MockAdapter.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index f0a74020..543a8ae3 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -101,8 +101,8 @@ export class MockAdapter implements ReadWriteAdapter { async getBlock(...args: NetworkGetBlockArgs) { return this.getMock>({ method: "getBlock", - create: () => - sinonStub().resolves({ + create: (mock) => + mock.resolves({ blockNumber: 0n, timestamp: 0n, }), From b86a0a5c4a8a629f65286d142c6c2f821bcd5f15 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Thu, 3 Oct 2024 17:18:03 -0500 Subject: [PATCH 25/49] nit --- packages/drift/src/adapter/MockAdapter.ts | 25 ++++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 543a8ae3..287d7e99 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -49,19 +49,20 @@ export class MockAdapter implements ReadWriteAdapter { mockKey += `:${stringify(key)}`; } let mock = this.mocks.get(mockKey); - if (!mock) { - mock = sinonStub().throws( - new NotImplementedError({ - method, - mockKey, - }), - ); - if (create) { - // TODO: Cleanup type casting - mock = create(mock as any); - } - this.mocks.set(mockKey, mock); + if (mock) { + return mock as any; + } + mock = sinonStub().throws( + new NotImplementedError({ + method, + mockKey, + }), + ); + if (create) { + // TODO: Cleanup type casting + mock = create(mock as any); } + this.mocks.set(mockKey, mock); return mock as any; } From d759d359f43017c2c428b32ad9100787e471997c Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Fri, 4 Oct 2024 05:40:45 -0500 Subject: [PATCH 26/49] Add Contract, MockContract, wip reorg --- biome.jsonc | 3 + .../drift/src/adapter/MockAdapter.test.ts | 94 ++-- packages/drift/src/adapter/MockAdapter.ts | 359 ++++++++-------- .../contract/mocks/ReadContractStub.test.ts | 180 -------- .../contract/mocks/ReadContractStub.ts | 289 ------------- .../mocks/ReadWriteContractStub.test.ts | 23 - .../contract/mocks/ReadWriteContractStub.ts | 105 ----- .../contract/stubs/ReadWriteContractStub.ts | 105 ----- .../src/adapter/contract/types/Contract.ts | 196 --------- .../src/adapter/network/MockNetwork.test.ts | 112 ----- .../drift/src/adapter/network/MockNetwork.ts | 182 -------- .../{contract/types/abi.ts => types/Abi.ts} | 4 +- .../adapter/{types.ts => types/Adapter.ts} | 78 ++-- .../src/adapter/{network => }/types/Block.ts | 0 packages/drift/src/adapter/types/Contract.ts | 60 +++ .../src/adapter/{contract => }/types/Event.ts | 4 +- .../adapter/{contract => }/types/Function.ts | 16 +- .../NetworkAdapter.ts => types/Network.ts} | 6 +- .../{network => }/types/Transaction.ts | 0 .../utils/arrayToFriendly.test.ts | 2 +- .../{contract => }/utils/arrayToFriendly.ts | 4 +- .../utils/arrayToObject.test.ts | 2 +- .../{contract => }/utils/arrayToObject.ts | 4 +- .../{contract => }/utils/getAbiEntry.ts | 6 +- .../utils/objectToArray.test.ts | 2 +- .../{contract => }/utils/objectToArray.ts | 4 +- .../createClientCache.test.ts} | 12 +- .../cache/ClientCache/createClientCache.ts | 56 +++ .../{DriftCache => ClientCache}/types.ts | 64 +-- .../src/cache/DriftCache/createDriftCache.ts | 53 --- .../cache/SimpleCache/createLruSimpleCache.ts | 9 +- packages/drift/src/cache/SimpleCache/types.ts | 24 +- packages/drift/src/cache/utils/DriftCache.ts | 22 - .../utils/createCachedReadContract.test.ts | 174 -------- .../cache/utils/createCachedReadContract.ts | 168 -------- .../utils/createCachedReadWriteContract.ts | 46 -- packages/drift/src/cache/utils/eventsKey.ts | 16 - .../drift/src/cache/utils/invalidateRead.ts | 15 - .../cache/utils/invalidateReadsMatching.ts | 26 -- .../drift/src/cache/utils/partialReadKey.ts | 17 - packages/drift/src/cache/utils/preloadRead.ts | 20 - packages/drift/src/cache/utils/readKey.ts | 12 - .../drift/src/client/Contract/Contract.ts | 405 ++++++++++++++++++ .../src/client/Contract/MockContract.test.ts | 271 ++++++++++++ .../drift/src/client/Contract/MockContract.ts | 251 +++++++++++ .../drift/src/client/Contract/MockErc20.ts | 18 + packages/drift/src/client/Drift/Drift.ts | 245 +++++++++++ .../{drift => client/Drift}/MockDrift.test.ts | 2 +- .../src/{drift => client/Drift}/MockDrift.ts | 7 +- packages/drift/src/constants.ts | 1 + .../drift/src/contract/createReadContract.ts | 63 --- packages/drift/src/contract/types.ts | 90 ---- packages/drift/src/drift/Drift.ts | 213 --------- packages/drift/src/exports/cache.ts | 3 - packages/drift/src/exports/contract.ts | 54 --- packages/drift/src/exports/errors.ts | 1 - packages/drift/src/exports/index.ts | 4 - packages/drift/src/exports/network.ts | 15 - packages/drift/src/exports/stubs.ts | 6 - packages/drift/src/utils/MockStore.ts | 55 +++ .../createSerializableKey.ts} | 37 +- packages/drift/src/utils/types.ts | 1 + 62 files changed, 1753 insertions(+), 2563 deletions(-) delete mode 100644 packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts delete mode 100644 packages/drift/src/adapter/contract/mocks/ReadContractStub.ts delete mode 100644 packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts delete mode 100644 packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts delete mode 100644 packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts delete mode 100644 packages/drift/src/adapter/contract/types/Contract.ts delete mode 100644 packages/drift/src/adapter/network/MockNetwork.test.ts delete mode 100644 packages/drift/src/adapter/network/MockNetwork.ts rename packages/drift/src/adapter/{contract/types/abi.ts => types/Abi.ts} (99%) rename packages/drift/src/adapter/{types.ts => types/Adapter.ts} (58%) rename packages/drift/src/adapter/{network => }/types/Block.ts (100%) create mode 100644 packages/drift/src/adapter/types/Contract.ts rename packages/drift/src/adapter/{contract => }/types/Event.ts (95%) rename packages/drift/src/adapter/{contract => }/types/Function.ts (86%) rename packages/drift/src/adapter/{network/types/NetworkAdapter.ts => types/Network.ts} (92%) rename packages/drift/src/adapter/{network => }/types/Transaction.ts (100%) rename packages/drift/src/adapter/{contract => }/utils/arrayToFriendly.test.ts (95%) rename packages/drift/src/adapter/{contract => }/utils/arrayToFriendly.ts (95%) rename packages/drift/src/adapter/{contract => }/utils/arrayToObject.test.ts (94%) rename packages/drift/src/adapter/{contract => }/utils/arrayToObject.ts (95%) rename packages/drift/src/adapter/{contract => }/utils/getAbiEntry.ts (91%) rename packages/drift/src/adapter/{contract => }/utils/objectToArray.test.ts (94%) rename packages/drift/src/adapter/{contract => }/utils/objectToArray.ts (95%) rename packages/drift/src/cache/{DriftCache/createDriftCache.test.ts => ClientCache/createClientCache.test.ts} (87%) create mode 100644 packages/drift/src/cache/ClientCache/createClientCache.ts rename packages/drift/src/cache/{DriftCache => ClientCache}/types.ts (53%) delete mode 100644 packages/drift/src/cache/DriftCache/createDriftCache.ts delete mode 100644 packages/drift/src/cache/utils/DriftCache.ts delete mode 100644 packages/drift/src/cache/utils/createCachedReadContract.test.ts delete mode 100644 packages/drift/src/cache/utils/createCachedReadContract.ts delete mode 100644 packages/drift/src/cache/utils/createCachedReadWriteContract.ts delete mode 100644 packages/drift/src/cache/utils/eventsKey.ts delete mode 100644 packages/drift/src/cache/utils/invalidateRead.ts delete mode 100644 packages/drift/src/cache/utils/invalidateReadsMatching.ts delete mode 100644 packages/drift/src/cache/utils/partialReadKey.ts delete mode 100644 packages/drift/src/cache/utils/preloadRead.ts delete mode 100644 packages/drift/src/cache/utils/readKey.ts create mode 100644 packages/drift/src/client/Contract/Contract.ts create mode 100644 packages/drift/src/client/Contract/MockContract.test.ts create mode 100644 packages/drift/src/client/Contract/MockContract.ts create mode 100644 packages/drift/src/client/Contract/MockErc20.ts create mode 100644 packages/drift/src/client/Drift/Drift.ts rename packages/drift/src/{drift => client/Drift}/MockDrift.test.ts (93%) rename packages/drift/src/{drift => client/Drift}/MockDrift.ts (70%) create mode 100644 packages/drift/src/constants.ts delete mode 100644 packages/drift/src/contract/createReadContract.ts delete mode 100644 packages/drift/src/contract/types.ts delete mode 100644 packages/drift/src/drift/Drift.ts delete mode 100644 packages/drift/src/exports/cache.ts delete mode 100644 packages/drift/src/exports/contract.ts delete mode 100644 packages/drift/src/exports/errors.ts delete mode 100644 packages/drift/src/exports/index.ts delete mode 100644 packages/drift/src/exports/network.ts delete mode 100644 packages/drift/src/exports/stubs.ts create mode 100644 packages/drift/src/utils/MockStore.ts rename packages/drift/src/{cache/SimpleCache/createSimpleCacheKey.ts => utils/createSerializableKey.ts} (61%) diff --git a/biome.jsonc b/biome.jsonc index 226325db..b1c74ef3 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -21,6 +21,9 @@ "enabled": true, "rules": { + "complexity": { + "noBannedTypes": "off" + }, "style": { "noNonNullAssertion": { "level": "info", diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index ce25faca..cca8d434 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -1,17 +1,17 @@ import { MockAdapter } from "src/adapter/MockAdapter"; -import type { Event } from "src/adapter/contract/types/event"; -import type { Block } from "src/adapter/network/types/Block"; +import type { + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, +} from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; +import type { ContactEvent } from "src/adapter/types/Event"; import type { Transaction, TransactionReceipt, -} from "src/adapter/network/types/Transaction"; -import type { - DecodeFunctionDataParams, - EncodeFunctionDataParams, - GetEventsParams, - ReadParams, - WriteParams, -} from "src/adapter/types"; +} from "src/adapter/types/Transaction"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; @@ -214,17 +214,21 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: EncodeFunctionDataParams = - { - abi: IERC20.abi, - fn: "balanceOf", - args: { owner: "0x1" }, - }; - const params2: EncodeFunctionDataParams = - { - ...params1, - args: { owner: "0x2" }, - }; + const params1: AdapterEncodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x1" }, + }; + const params2: AdapterEncodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + ...params1, + args: { owner: "0x2" }, + }; adapter.onEncodeFunctionData(params1).returns("0x1"); adapter.onEncodeFunctionData(params2).returns("0x2"); expect(adapter.encodeFunctionData(params1)).toBe("0x1"); @@ -268,17 +272,21 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: DecodeFunctionDataParams = - { - abi: IERC20.abi, - fn: "balanceOf", - data: "0x1", - }; - const params2: DecodeFunctionDataParams = - { - ...params1, - data: "0x2", - }; + const params1: AdapterDecodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + abi: IERC20.abi, + fn: "balanceOf", + data: "0x1", + }; + const params2: AdapterDecodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + ...params1, + data: "0x2", + }; adapter.onDecodeFunctionData(params1).returns(1n); adapter.onDecodeFunctionData(params2).returns(2n); expect(adapter.decodeFunctionData(params1)).toBe(1n); @@ -304,7 +312,7 @@ describe("MockAdapter", () => { it("Can be stubbed", async () => { const adapter = new MockAdapter(); - const events: Event[] = [ + const events: ContactEvent[] = [ { eventName: "Transfer", args: { @@ -332,17 +340,17 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: GetEventsParams = { + const params1: AdapterGetEventsParams = { abi: IERC20.abi, address: "0x1", event: "Transfer", filter: { from: "0x1" }, }; - const params2: GetEventsParams = { + const params2: AdapterGetEventsParams = { ...params1, filter: { from: "0x2" }, }; - const events1: Event[] = [ + const events1: ContactEvent[] = [ { eventName: "Transfer", args: { @@ -352,7 +360,7 @@ describe("MockAdapter", () => { }, }, ]; - const events2: Event[] = [ + const events2: ContactEvent[] = [ { eventName: "Transfer", args: { @@ -405,13 +413,13 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: ReadParams = { + const params1: AdapterReadParams = { abi: IERC20.abi, address: "0x1", fn: "allowance", args: { owner: "0x1", spender: "0x1" }, }; - const params2: ReadParams = { + const params2: AdapterReadParams = { ...params1, args: { owner: "0x2", spender: "0x2" }, }; @@ -480,13 +488,13 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: WriteParams = { + const params1: AdapterWriteParams = { abi: IERC20.abi, address: "0x1", fn: "transfer", args: { to: "0x1", value: 123n }, }; - const params2: WriteParams = { + const params2: AdapterWriteParams = { ...params1, args: { to: "0x2", value: 123n }, }; @@ -532,13 +540,13 @@ describe("MockAdapter", () => { it("Can be stubbed with specific args", async () => { const adapter = new MockAdapter(); - const params1: WriteParams = { + const params1: AdapterWriteParams = { abi: IERC20.abi, address: "0x", fn: "transfer", args: { to: "0x1", value: 123n }, }; - const params2: WriteParams = { + const params2: AdapterWriteParams = { ...params1, args: { to: "0x2", value: 123n }, }; diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 287d7e99..9dc627a5 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -1,86 +1,52 @@ import type { Abi } from "abitype"; -import stringify from "fast-json-stable-stringify"; -import { type SinonStub, stub as sinonStub } from "sinon"; -import type { Event, EventName } from "src/adapter/contract/types/event"; import type { + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, FunctionName, FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { Block } from "src/adapter/network/types/Block"; +} from "src/adapter/types/Function"; import type { NetworkGetBalanceArgs, NetworkGetBlockArgs, NetworkGetTransactionArgs, NetworkWaitForTransactionArgs, -} from "src/adapter/network/types/NetworkAdapter"; +} from "src/adapter/types/Network"; import type { Transaction, TransactionReceipt, -} from "src/adapter/network/types/Transaction"; -import type { - DecodeFunctionDataParams, - EncodeFunctionDataParams, - GetEventsParams, - ReadParams, - ReadWriteAdapter, - WriteParams, -} from "src/adapter/types"; -import type { SimpleCacheKey } from "src/exports"; +} from "src/adapter/types/Transaction"; import type { Address, Bytes, TransactionHash } from "src/types"; +import { MockStore } from "src/utils/MockStore"; import type { OptionalKeys } from "src/utils/types"; // TODO: Allow configuration of error throwing/default return value behavior export class MockAdapter implements ReadWriteAdapter { - // stubs // - - protected mocks = new Map(); - - protected getMock({ - method, - key, - create, - }: { - method: keyof ReadWriteAdapter; - key?: SimpleCacheKey; - create?: (mock: SinonStub) => SinonStub; - }): SinonStub { - let mockKey: string = method; - if (key) { - mockKey += `:${stringify(key)}`; - } - let mock = this.mocks.get(mockKey); - if (mock) { - return mock as any; - } - mock = sinonStub().throws( - new NotImplementedError({ - method, - mockKey, - }), - ); - if (create) { - // TODO: Cleanup type casting - mock = create(mock as any); - } - this.mocks.set(mockKey, mock); - return mock as any; - } + mocks = new MockStore(); - reset() { - this.mocks.clear(); - } + reset = () => this.mocks.reset(); // getBalance // onGetBalance(...args: Partial) { - return this.getMock>({ - method: "getBalance", - create: (mock) => mock.resolves(0n), - }).withArgs(...args); + return this.mocks + .get>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), + }) + .withArgs(...args); } async getBalance(...args: NetworkGetBalanceArgs) { - return this.getMock>({ + return this.mocks.get>({ method: "getBalance", create: (mock) => mock.resolves(0n), })(...args); @@ -89,18 +55,20 @@ export class MockAdapter implements ReadWriteAdapter { // getBlock // onGetBlock(...args: Partial) { - return this.getMock>({ - method: "getBlock", - create: (mock) => - mock.resolves({ - blockNumber: 0n, - timestamp: 0n, - }), - }).withArgs(...args); + return this.mocks + .get>({ + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }) + .withArgs(...args); } async getBlock(...args: NetworkGetBlockArgs) { - return this.getMock>({ + return this.mocks.get>({ method: "getBlock", create: (mock) => mock.resolves({ @@ -113,14 +81,14 @@ export class MockAdapter implements ReadWriteAdapter { // getChainId // onGetChainId() { - return this.getMock<[], number>({ + return this.mocks.get<[], number>({ method: "getChainId", create: (mock) => mock.resolves(96024), }); } async getChainId() { - return this.getMock<[], number>({ + return this.mocks.get<[], number>({ method: "getChainId", create: (mock) => mock.resolves(96024), })(); @@ -129,17 +97,16 @@ export class MockAdapter implements ReadWriteAdapter { // getTransaction // onGetTransaction(...args: Partial) { - return this.getMock< - NetworkGetTransactionArgs, - Promise - >({ - method: "getTransaction", - create: (mock) => mock.resolves(undefined), - }).withArgs(...args); + return this.mocks + .get>({ + method: "getTransaction", + create: (mock) => mock.resolves(undefined), + }) + .withArgs(...args); } async getTransaction(...args: NetworkGetTransactionArgs) { - return this.getMock< + return this.mocks.get< NetworkGetTransactionArgs, Promise >({ @@ -151,17 +118,19 @@ export class MockAdapter implements ReadWriteAdapter { // waitForTransaction // onWaitForTransaction(...args: Partial) { - return this.getMock< - NetworkWaitForTransactionArgs, - Promise - >({ - method: "waitForTransaction", - create: (mock) => mock.resolves(undefined), - }).withArgs(...args); + return this.mocks + .get< + NetworkWaitForTransactionArgs, + Promise + >({ + method: "waitForTransaction", + create: (mock) => mock.resolves(undefined), + }) + .withArgs(...args); } async waitForTransaction(...args: NetworkWaitForTransactionArgs) { - return this.getMock< + return this.mocks.get< NetworkWaitForTransactionArgs, Promise >({ @@ -175,18 +144,25 @@ export class MockAdapter implements ReadWriteAdapter { onEncodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: OnEncodeFunctionDataParams) { - return this.getMock<[EncodeFunctionDataParams], Bytes>({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - }).withArgs(params); + >( + params: OptionalKeys< + AdapterEncodeFunctionDataParams, + "args" + >, + ) { + return this.mocks + .get<[AdapterEncodeFunctionDataParams], Bytes>({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + }) + .withArgs(params); } encodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: EncodeFunctionDataParams) { - return this.getMock<[EncodeFunctionDataParams], Bytes>({ + >(params: AdapterEncodeFunctionDataParams) { + return this.mocks.get<[AdapterEncodeFunctionDataParams], Bytes>({ method: "encodeFunctionData", create: (mock) => mock.returns("0x0"), })(params); @@ -197,23 +173,30 @@ export class MockAdapter implements ReadWriteAdapter { onDecodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: OnDecodeFunctionDataParams) { - return this.getMock< - [DecodeFunctionDataParams], - FunctionReturn - >({ - method: "decodeFunctionData", - key: params.fn, - }).withArgs(params); + >( + params: OptionalKeys< + AdapterDecodeFunctionDataParams, + "data" + >, + ) { + return this.mocks + .get< + [AdapterDecodeFunctionDataParams], + FunctionReturn + >({ + method: "decodeFunctionData", + key: params.fn, + }) + .withArgs(params); } decodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: DecodeFunctionDataParams) { - return this.getMock< - [DecodeFunctionDataParams], - FunctionReturn + >(params: AdapterDecodeFunctionDataParams) { + return this.mocks.get< + [AdapterDecodeFunctionDataParams], + DecodedFunctionData >({ method: "decodeFunctionData", // TODO: This should be specific to the abi to ensure the correct return @@ -225,23 +208,25 @@ export class MockAdapter implements ReadWriteAdapter { // getEvents // onGetEvents>( - params: OnGetEventsParams, + params: OptionalKeys, "address">, ) { - return this.getMock< - [GetEventsParams], - Promise[]> - >({ - method: "getEvents", - key: params.event, - }).withArgs(params); + return this.mocks + .get< + [AdapterGetEventsParams], + Promise[]> + >({ + method: "getEvents", + key: params.event, + }) + .withArgs(params); } async getEvents>( - params: GetEventsParams, + params: AdapterGetEventsParams, ) { - return this.getMock< - [GetEventsParams], - Promise[]> + return this.mocks.get< + [AdapterGetEventsParams], + Promise[]> >({ method: "getEvents", key: params.event, @@ -253,23 +238,30 @@ export class MockAdapter implements ReadWriteAdapter { onRead< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: OnReadParams) { - return this.getMock< - [ReadParams], - FunctionReturn - >({ - method: "read", - key: params.fn, - }).withArgs(params as Partial>); + >( + params: OptionalKeys< + AdapterReadParams, + "args" | "address" + >, + ) { + return this.mocks + .get< + [AdapterReadParams], + Promise> + >({ + method: "read", + key: params.fn, + }) + .withArgs(params as Partial>); } async read< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: ReadParams) { - return this.getMock< - [ReadParams], - FunctionReturn + >(params: AdapterReadParams) { + return this.mocks.get< + [AdapterReadParams], + Promise> >({ method: "read", key: params.fn, @@ -281,22 +273,29 @@ export class MockAdapter implements ReadWriteAdapter { onSimulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: OnWriteParams) { - return this.getMock< - [WriteParams], - Promise> - >({ - method: "simulateWrite", - key: params.fn, - }).withArgs(params as Partial>); + >( + params: OptionalKeys< + AdapterWriteParams, + "args" | "address" + >, + ) { + return this.mocks + .get< + [AdapterWriteParams], + Promise> + >({ + method: "simulateWrite", + key: params.fn, + }) + .withArgs(params as Partial>); } async simulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteParams) { - return this.getMock< - [WriteParams], + >(params: AdapterWriteParams) { + return this.mocks.get< + [AdapterWriteParams], Promise> >({ method: "simulateWrite", @@ -309,72 +308,70 @@ export class MockAdapter implements ReadWriteAdapter { onWrite< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: OnWriteParams) { - return this.getMock< - [WriteParams], - Promise - >({ - method: "write", - key: params.fn, - create: (mock) => mock.resolves("0x0"), - }).withArgs(params as Partial>); + >( + params: OptionalKeys< + AdapterWriteParams, + "args" | "address" + >, + ) { + return this.mocks + .get<[AdapterWriteParams], Promise>( + { + method: "write", + key: params.fn, + create: (mock) => mock.resolves("0x0"), + }, + ) + .withArgs(params as Partial>); } async write< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteParams) { - return this.getMock< - [WriteParams], - Promise - >({ - method: "write", - key: params.fn, - create: (mock) => mock.resolves("0x0"), - })(params); + >(params: AdapterWriteParams) { + const writePromise = Promise.resolve( + this.mocks.get< + [AdapterWriteParams], + Promise + >({ + method: "write", + key: params.fn, + create: (mock) => mock.resolves("0x0"), + })(params), + ); + + // TODO: unit test + if (params.onMined) { + writePromise.then((hash) => { + this.waitForTransaction(hash).then(params.onMined); + return hash; + }); + } + + return writePromise; } // getSignerAddress // onGetSignerAddress() { - return this.getMock<[], Address>({ + return this.mocks.get<[], Address>({ method: "getSignerAddress", create: (mock) => mock.resolves("0xMockSigner"), }); } async getSignerAddress() { - return this.getMock<[], Address>({ + return this.mocks.get<[], Address>({ method: "getSignerAddress", create: (mock) => mock.resolves("0xMockSigner"), })(); } } -export type OnGetEventsParams< - TAbi extends Abi = Abi, - TEventName extends EventName = EventName, -> = OptionalKeys, "address">; - -export type OnReadParams< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args" | "address">; - -export type OnWriteParams< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args" | "address">; - -export type OnEncodeFunctionDataParams< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args">; - -export type OnDecodeFunctionDataParams< +export type AdapterOnWriteParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "data">; +> = OptionalKeys, "args" | "address">; export class NotImplementedError extends Error { constructor({ method, mockKey }: { method: string; mockKey: string }) { diff --git a/packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts b/packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts deleted file mode 100644 index ad721ef7..00000000 --- a/packages/drift/src/adapter/contract/mocks/ReadContractStub.test.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; -import type { Event } from "src/adapter/contract/types/event"; -import { IERC20 } from "src/utils/testing/IERC20"; -import { ALICE, BOB, NANCY } from "src/utils/testing/accounts"; -import { describe, expect, it } from "vitest"; - -const ERC20ABI = IERC20.abi; - -describe("ReadContractStub", () => { - it("stubs the read function without args, but with options", async () => { - const contract = new ReadContractStub(IERC20.abi); - - // stub total supply - contract.stubRead({ - functionName: "totalSupply", - value: 30n, - // options can be specfied as well - options: { blockNumber: 12n }, - }); - contract.stubRead({ - functionName: "totalSupply", - value: 40n, - // options can be specfied as well - options: { blockNumber: 16n }, - }); - // Now try and read them based on their args - const totalSupplyAtBlock12 = await contract.read("totalSupply", undefined, { - blockNumber: 12n, - }); - expect(totalSupplyAtBlock12).toBe(30n); - - const totalSupplyAtBlock16 = await contract.read("totalSupply", undefined, { - blockNumber: 16n, - }); - expect(totalSupplyAtBlock16).toBe(40n); - }); - - it("stubs the read function", async () => { - const contract = new ReadContractStub(IERC20.abi); - - expect(contract.read("balanceOf", { owner: NANCY })).rejects.toThrowError(); - - // Stub bob and alice's balances first - const bobValue = 10n; - contract.stubRead({ - functionName: "balanceOf", - args: { owner: BOB }, - value: bobValue, - }); - - const aliceValue = 20n; - contract.stubRead({ - functionName: "balanceOf", - args: { owner: ALICE }, - value: aliceValue, - // options can be specfied as well - options: { blockNumber: 10n }, - }); - - // Now try and read them based on their args - const bobResult = await contract.read("balanceOf", { owner: BOB }); - const aliceResult = await contract.read( - "balanceOf", - { owner: ALICE }, - { blockNumber: 10n }, - ); - expect(bobResult).toBe(bobValue); - expect(aliceResult).toBe(aliceValue); - - // Now stub w/out any args and see if we get the default value back - const defaultValue = 30n; - contract.stubRead({ - functionName: "balanceOf", - value: defaultValue, - }); - const defaultResult = await contract.read("balanceOf", { owner: NANCY }); - expect(defaultResult).toBe(defaultValue); - - const stub = contract.getReadStub("balanceOf"); - expect(stub?.callCount).toBe(3); - }); - - it("stubs the simulateWrite function", async () => { - const contract = new ReadContractStub(ERC20ABI); - - expect( - contract.simulateWrite("transferFrom", { - from: ALICE, - to: BOB, - value: 100n, - }), - ).rejects.toThrowError(); - - const stubbedResult = true; - contract.stubSimulateWrite("transferFrom", stubbedResult); - - const result = await contract.simulateWrite("transferFrom", { - from: ALICE, - to: BOB, - value: 100n, - }); - - expect(result).toStrictEqual(stubbedResult); - - const stub = contract.getSimulateWriteStub("transferFrom"); - expect(stub?.callCount).toBe(1); - }); - - it("stubs the getEvents function", async () => { - const contract = new ReadContractStub(ERC20ABI); - - // throws an error if you forget to stub the event your requesting - expect(contract.getEvents("Transfer")).rejects.toThrowError(); - - // Stub out the events when calling `getEvents` without any filter args - const stubbedAllEvents: Event[] = [ - { - eventName: "Transfer", - args: { - to: ALICE, - from: BOB, - value: 100n, - }, - blockNumber: 1n, - data: "0x123abc", - transactionHash: "0x123abc", - }, - { - eventName: "Transfer", - args: { - from: ALICE, - to: BOB, - value: 100n, - }, - blockNumber: 1n, - data: "0x123abc", - transactionHash: "0x123abc", - }, - ]; - contract.stubEvents("Transfer", undefined, stubbedAllEvents); - - // Stub out the events when calling `getEvents` *with* filter args - const stubbedFilteredEvents: Event[] = [ - { - eventName: "Transfer", - args: { - to: ALICE, - from: BOB, - value: 100n, - }, - blockNumber: 1n, - data: "0x123abc", - transactionHash: "0x123abc", - }, - ]; - contract.stubEvents( - "Transfer", - { filter: { from: BOB } }, - stubbedFilteredEvents, - ); - - // getting events without any filter args should return the stub that was - // specified without any filter args - const events = await contract.getEvents("Transfer"); - expect(events).toBe(stubbedAllEvents); - const stub = contract.getEventsStub("Transfer"); - expect(stub?.callCount).toBe(1); - - // getting events with filter args should return the stub that was specified - // *with* filter args - const filteredEvents = await contract.getEvents("Transfer", { - filter: { from: BOB }, - }); - expect(filteredEvents).toBe(stubbedFilteredEvents); - const filteredStub = contract.getEventsStub("Transfer", { - filter: { from: BOB }, - }); - expect(filteredStub?.callCount).toBe(1); - }); -}); diff --git a/packages/drift/src/adapter/contract/mocks/ReadContractStub.ts b/packages/drift/src/adapter/contract/mocks/ReadContractStub.ts deleted file mode 100644 index 67c20846..00000000 --- a/packages/drift/src/adapter/contract/mocks/ReadContractStub.ts +++ /dev/null @@ -1,289 +0,0 @@ -import type { Abi } from "abitype"; -import stringify from "fast-safe-stringify"; -import { type SinonStub, stub } from "sinon"; -import type { - AdapterReadContract, - ContractDecodeFunctionDataArgs, - ContractEncodeFunctionDataArgs, - ContractGetEventsArgs, - ContractGetEventsOptions, - ContractReadArgs, - ContractReadOptions, - ContractWriteArgs, - ContractWriteOptions, -} from "src/adapter/contract/types/contract"; -import type { Event, EventName } from "src/adapter/contract/types/event"; -import type { - DecodedFunctionData, - FunctionArgs, - FunctionName, - FunctionReturn, -} from "src/adapter/contract/types/function"; - -/** - * A mock implementation of a `ReadContract` designed to facilitate unit - * testing. The `ReadContractStub` provides a way to stub out specific - * contract read, write, and event-fetching behaviors, allowing tests to focus - * on the business logic of the SDK. - * - * @example - * const contract = new ReadContractStub(ERC20ABI); - * contract.stubRead("baseToken", "0x123abc"); - * - * const value = await contract.read("baseToken", []); // "0x123abc" - * - */ -export class ReadContractStub - implements AdapterReadContract -{ - abi; - address = "0x0000000000000000000000000000000000000000" as const; - - // Maps to store stubs for different contract methods based on their name. - protected readStubMap = new Map< - FunctionName, - ReadStub> - >(); - protected eventsStubMap = new Map< - EventName, - EventsStub> - >(); - protected simulateWriteStubMap = new Map< - FunctionName, - SimulateWriteStub> - >(); - - constructor(abi: TAbi = [] as any) { - this.abi = abi; - } - - /** - * Simulates a contract read operation for a given function. If the function - * is not previously stubbed using `stubRead`, an error will be thrown. - */ - async read>( - ...[functionName, args, options]: ContractReadArgs - ): Promise> { - const stub = this.getReadStub(functionName); - if (!stub) { - throw new Error( - `Called read for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubRead("${functionName}", value)`, - ); - } - return stub(args, options); - } - - /** - * Simulates a contract write operation for a given function. If the function - * is not previously stubbed using `stubWrite`, an error will be thrown. - */ - async simulateWrite< - TFunctionName extends FunctionName, - >( - ...[functionName, args, options]: ContractWriteArgs - ): Promise> { - const stub = this.getSimulateWriteStub(functionName); - if (!stub) { - throw new Error( - `Called simulateWrite for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, - ); - } - return stub(args, options); - } - - /** - * Simulates fetching events for a given event name from the contract. If the - * event name is not previously stubbed using `stubEvents`, an error will be - * thrown. - */ - async getEvents>( - ...[eventName, options]: ContractGetEventsArgs - ): Promise[]> { - const stub = this.getEventsStub(eventName, options); - if (!stub) { - throw new Error( - `Called getEvents for ${eventName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubEvents("${eventName}", value)`, - ); - } - return stub(options); - } - - /** - * Stubs the return value for a given function when `read` is called with that - * function name. This method overrides any previously stubbed values for the - * same function. - */ - stubRead>({ - functionName, - args, - value, - options, - }: { - functionName: TFunctionName; - args?: FunctionArgs; - value: FunctionReturn; - options?: ContractReadOptions; - }): void { - let readStub = this.readStubMap.get(functionName); - if (!readStub) { - readStub = stub(); - this.readStubMap.set(functionName, readStub); - } - - // Account for dynamic args if provided - if (args || options) { - // The stub returned from the map doesn't have a strong FunctionName type - // so we have to cast to avoid contravariance errors with the args. - (readStub as ReadStub) - .withArgs(args, options) - .resolves(value); - return; - } - - readStub.resolves(value); - } - - /** - * Stubs the return value for a given function when `simulateWrite` is called - * with that function name. This method overrides any previously stubbed - * values for the same function. - * - * *Note: The stub doesn't account for dynamic values based on provided - * arguments/options.* - */ - stubSimulateWrite< - TFunctionName extends FunctionName, - >( - functionName: TFunctionName, - value: FunctionReturn, - ): void { - let simulateWriteStub = this.simulateWriteStubMap.get(functionName); - if (!simulateWriteStub) { - simulateWriteStub = stub(); - this.simulateWriteStubMap.set(functionName, simulateWriteStub); - } - simulateWriteStub.resolves(value); - } - - /** - * Stubs the return value for a given event name when `getEvents` is called - * with that event name. This method overrides any previously stubbed values - * for the same event. - */ - stubEvents>( - eventName: TEventName, - args: ContractGetEventsOptions | undefined, - value: Event[], - ): void { - const stubKey = stableStringify({ eventName, args }); - if (this.eventsStubMap.has(stubKey)) { - this.getEventsStub(eventName, args)!.resolves(value as any); - } else { - this.eventsStubMap.set(stubKey, stub().resolves(value) as any); - } - } - - /** - * Retrieves the stub associated with a read function name. - * Useful for assertions in testing, such as checking call counts. - */ - getReadStub>( - functionName: TFunctionName, - ): ReadStub | undefined { - return this.readStubMap.get(functionName) as - | ReadStub - | undefined; - } - - /** - * Retrieves the stub associated with a write function name. - * Useful for assertions in testing, such as checking call counts. - */ - getSimulateWriteStub< - TFunctionName extends FunctionName, - >( - functionName: TFunctionName, - ): SimulateWriteStub | undefined { - return this.simulateWriteStubMap.get(functionName) as - | SimulateWriteStub - | undefined; - } - - /** - * Retrieves the stub associated with an event name. - * Useful for assertions in testing, such as checking call counts. - */ - getEventsStub>( - eventName: TEventName, - args?: ContractGetEventsOptions | undefined, - ): EventsStub | undefined { - const stubKey = stableStringify({ eventName, args }); - return this.eventsStubMap.get(stubKey) as - | EventsStub - | undefined; - } - - // TODO: - decodeFunctionData< - TFunctionName extends FunctionName = FunctionName, - >( - ...args: ContractDecodeFunctionDataArgs - ): DecodedFunctionData { - throw new Error("Method not implemented."); - } - - // TODO: - encodeFunctionData< - TFunctionName extends FunctionName = FunctionName, - >( - ...args: ContractEncodeFunctionDataArgs - ): `0x${string}` { - throw new Error("Method not implemented."); - } -} - -/** - * Type representing a stub for the "read" function of a contract. - */ -type ReadStub< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = SinonStub< - [args?: FunctionArgs, options?: ContractReadOptions], - Promise> ->; - -/** - * Type representing a stub for the "getEvents" function of a contract. - */ -type EventsStub< - TAbi extends Abi, - TEventName extends EventName, -> = SinonStub< - [options?: ContractGetEventsOptions], - Promise[]> ->; - -/** - * Type representing a stub for the "write" and "simulateWrite" functions of a - * contract. - */ -type SimulateWriteStub< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = SinonStub< - [ - args?: FunctionArgs | undefined, - options?: ContractWriteOptions, - ], - Promise> ->; - -function stableStringify(obj: Record) { - // simple non-recursive stringify replacer for bigints - function replacer(_: any, v: any) { - return typeof v === "bigint" ? v.toString() : v; - } - - return stringify.stableStringify(obj, replacer); -} diff --git a/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts deleted file mode 100644 index cfa6b0fa..00000000 --- a/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ReadWriteContractStub } from "src/adapter/contract/mocks/ReadWriteContractStub"; -import { IERC20 } from "src/utils/testing/IERC20"; -import { describe, expect, it } from "vitest"; - -const ERC20ABI = IERC20.abi; - -describe("ReadWriteContractStub", () => { - it("stubs the write function", async () => { - const contract = new ReadWriteContractStub(ERC20ABI); - - const stubbedValue = "0x01234"; - contract.stubWrite("transfer", stubbedValue); - - const value = await contract.write("transfer", { - to: "0x123abc", - value: 100n, - }); - expect(value).toBe(stubbedValue); - - const stub = contract.getWriteStub("transfer"); - expect(stub?.callCount).toBe(1); - }); -}); diff --git a/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts b/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts deleted file mode 100644 index cdd469fc..00000000 --- a/packages/drift/src/adapter/contract/mocks/ReadWriteContractStub.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { Abi } from "abitype"; -import { type SinonStub, stub } from "sinon"; -import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; -import type { - AdapterReadWriteContract, - ContractWriteArgs, - ContractWriteOptions, -} from "src/adapter/contract/types/contract"; -import type { - FunctionArgs, - FunctionName, -} from "src/adapter/contract/types/function"; -import { BOB } from "src/utils/testing/accounts"; - -/** - * A mock implementation of a writable Ethereum contract designed for unit - * testing purposes. The `ReadWriteContractStub` extends the functionalities of - * `ReadContractStub` and provides capabilities to stub out specific - * contract write behaviors. This makes it a valuable tool when testing - * scenarios that involve contract writing operations, without actually - * interacting with a real Ethereum contract. - * - * @example - * const contract = new ReadWriteContractStub(ERC20ABI); - * contract.stubWrite("addLiquidity", 100n); - * - * const result = await contract.write("addLiquidity", []); // 100n - * @extends {ReadContractStub} - * @implements {ReadWriteContract} - */ -export class ReadWriteContractStub - extends ReadContractStub - implements AdapterReadWriteContract -{ - protected writeStubMap = new Map< - FunctionName, - WriteStub> - >(); - - getSignerAddress = stub().resolves(BOB); - - /** - * Simulates a contract write operation for a given function. If the function - * is not previously stubbed using `stubWrite` from the parent class, an error - * will be thrown. - */ - async write< - TFunctionName extends FunctionName, - >( - ...[functionName, args, options]: ContractWriteArgs - ): Promise<`0x${string}`> { - const stub = this.getWriteStub(functionName); - if (!stub) { - throw new Error( - `Called write for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, - ); - } - return stub(args, options); - } - - /** - * Stubs the return value for a given function when `simulateWrite` is called - * with that function name. This method overrides any previously stubbed - * values for the same function. - * - * *Note: The stub doesn't account for dynamic values based on provided - * arguments/options.* - */ - stubWrite>( - functionName: TFunctionName, - value: `0x${string}`, - ): void { - let writeStub = this.writeStubMap.get(functionName); - if (!writeStub) { - writeStub = stub(); - this.writeStubMap.set(functionName, writeStub); - } - writeStub.resolves(value); - } - - /** - * Retrieves the stub associated with a write function name. - * Useful for assertions in testing, such as checking call counts. - */ - getWriteStub< - TFunctionName extends FunctionName, - >(functionName: TFunctionName): WriteStub | undefined { - return this.writeStubMap.get(functionName) as WriteStub< - TAbi, - TFunctionName - >; - } -} - -/** - * Type representing a stub for the "write" and "simulateWrite" functions of a - * contract. - */ -type WriteStub< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = SinonStub< - [args?: FunctionArgs, options?: ContractWriteOptions], - `0x${string}` ->; diff --git a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts b/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts deleted file mode 100644 index cdd469fc..00000000 --- a/packages/drift/src/adapter/contract/stubs/ReadWriteContractStub.ts +++ /dev/null @@ -1,105 +0,0 @@ -import type { Abi } from "abitype"; -import { type SinonStub, stub } from "sinon"; -import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; -import type { - AdapterReadWriteContract, - ContractWriteArgs, - ContractWriteOptions, -} from "src/adapter/contract/types/contract"; -import type { - FunctionArgs, - FunctionName, -} from "src/adapter/contract/types/function"; -import { BOB } from "src/utils/testing/accounts"; - -/** - * A mock implementation of a writable Ethereum contract designed for unit - * testing purposes. The `ReadWriteContractStub` extends the functionalities of - * `ReadContractStub` and provides capabilities to stub out specific - * contract write behaviors. This makes it a valuable tool when testing - * scenarios that involve contract writing operations, without actually - * interacting with a real Ethereum contract. - * - * @example - * const contract = new ReadWriteContractStub(ERC20ABI); - * contract.stubWrite("addLiquidity", 100n); - * - * const result = await contract.write("addLiquidity", []); // 100n - * @extends {ReadContractStub} - * @implements {ReadWriteContract} - */ -export class ReadWriteContractStub - extends ReadContractStub - implements AdapterReadWriteContract -{ - protected writeStubMap = new Map< - FunctionName, - WriteStub> - >(); - - getSignerAddress = stub().resolves(BOB); - - /** - * Simulates a contract write operation for a given function. If the function - * is not previously stubbed using `stubWrite` from the parent class, an error - * will be thrown. - */ - async write< - TFunctionName extends FunctionName, - >( - ...[functionName, args, options]: ContractWriteArgs - ): Promise<`0x${string}`> { - const stub = this.getWriteStub(functionName); - if (!stub) { - throw new Error( - `Called write for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, - ); - } - return stub(args, options); - } - - /** - * Stubs the return value for a given function when `simulateWrite` is called - * with that function name. This method overrides any previously stubbed - * values for the same function. - * - * *Note: The stub doesn't account for dynamic values based on provided - * arguments/options.* - */ - stubWrite>( - functionName: TFunctionName, - value: `0x${string}`, - ): void { - let writeStub = this.writeStubMap.get(functionName); - if (!writeStub) { - writeStub = stub(); - this.writeStubMap.set(functionName, writeStub); - } - writeStub.resolves(value); - } - - /** - * Retrieves the stub associated with a write function name. - * Useful for assertions in testing, such as checking call counts. - */ - getWriteStub< - TFunctionName extends FunctionName, - >(functionName: TFunctionName): WriteStub | undefined { - return this.writeStubMap.get(functionName) as WriteStub< - TAbi, - TFunctionName - >; - } -} - -/** - * Type representing a stub for the "write" and "simulateWrite" functions of a - * contract. - */ -type WriteStub< - TAbi extends Abi, - TFunctionName extends FunctionName, -> = SinonStub< - [args?: FunctionArgs, options?: ContractWriteOptions], - `0x${string}` ->; diff --git a/packages/drift/src/adapter/contract/types/Contract.ts b/packages/drift/src/adapter/contract/types/Contract.ts deleted file mode 100644 index 5833114c..00000000 --- a/packages/drift/src/adapter/contract/types/Contract.ts +++ /dev/null @@ -1,196 +0,0 @@ -import type { Abi } from "abitype"; -import type { - Event, - EventFilter, - EventName, -} from "src/adapter/contract/types/event"; -import type { - DecodedFunctionData, - FunctionArgs, - FunctionName, - FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { BlockTag } from "src/adapter/network/types/Block"; -import type { EmptyObject } from "src/utils/types"; - -// https://ethereum.github.io/execution-apis/api-documentation/ - -/** - * Interface representing a readable contract with specified ABI. Provides type - * safe methods to read and simulate write operations on the contract. - */ -export interface AdapterReadContract { - abi: TAbi; - address: string; - - /** - * Reads a specified function from the contract. - */ - read>( - ...args: ContractReadArgs - ): Promise>; - - /** - * Simulates a write operation on a specified function of the contract. - */ - simulateWrite< - TFunctionName extends FunctionName, - >( - ...args: ContractWriteArgs - ): Promise>; - - /** - * Retrieves specified events from the contract. - */ - getEvents>( - ...args: ContractGetEventsArgs - ): Promise[]>; - - /** - * Encodes a function call into calldata. - */ - encodeFunctionData>( - ...args: ContractEncodeFunctionDataArgs - ): string; - - /** - * Decodes a string of function calldata into it's arguments and function - * name. - */ - decodeFunctionData< - TFunctionName extends FunctionName = FunctionName, - >( - ...args: ContractDecodeFunctionDataArgs - ): DecodedFunctionData; -} - -/** - * Interface representing a writable contract with specified ABI. Extends - * IReadContract to also include write operations. - */ -export interface AdapterReadWriteContract - extends AdapterReadContract { - /** - * Get the address of the signer for this contract. - */ - getSignerAddress(): Promise; - - /** - * Writes to a specified function on the contract. - * @returns The transaction hash of the submitted transaction. - */ - write>( - ...args: ContractWriteArgs - ): Promise; -} - -// https://github.com/ethereum/execution-apis/blob/main/src/eth/execute.yaml#L1 -export type ContractReadOptions = - | { - blockNumber?: bigint; - blockTag?: never; - } - | { - blockNumber?: never; - /** - * @default 'latest' - */ - blockTag?: BlockTag; - }; - -export type ContractReadArgs< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = FunctionArgs extends EmptyObject - ? [ - functionName: TFunctionName, - args?: FunctionArgs, - options?: ContractReadOptions, - ] - : [ - functionName: TFunctionName, - args: FunctionArgs, - options?: ContractReadOptions, - ]; - -export interface ContractGetEventsOptions< - TAbi extends Abi = Abi, - TEventName extends EventName = EventName, -> { - filter?: EventFilter; - fromBlock?: bigint | BlockTag; - toBlock?: bigint | BlockTag; -} - -export type ContractGetEventsArgs< - TAbi extends Abi = Abi, - TEventName extends EventName = EventName, -> = [ - eventName: TEventName, - options?: ContractGetEventsOptions, -]; - -// https://github.com/ethereum/execution-apis/blob/main/src/schemas/transaction.yaml#L274 -export interface ContractWriteOptions { - type?: string; - nonce?: bigint; - to?: string; - from?: string; - /** - * Gas limit - */ - gas?: bigint; - value?: bigint; - input?: string; - /** - * The gas price willing to be paid by the sender in wei - */ - gasPrice?: bigint; - /** - * Maximum fee per gas the sender is willing to pay to miners in wei - */ - maxPriorityFeePerGas?: bigint; - /** - * The maximum total fee per gas the sender is willing to pay (includes the - * network / base fee and miner / priority fee) in wei - */ - maxFeePerGas?: bigint; - /** - * EIP-2930 access list - */ - accessList?: { - address: string; - storageKeys: string[]; - }[]; - /** - * Chain ID that this transaction is valid on. - */ - chainId?: bigint; -} - -export type ContractWriteArgs< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName< - TAbi, - "nonpayable" | "payable" - > = FunctionName, -> = FunctionArgs extends EmptyObject - ? [ - functionName: TFunctionName, - args?: FunctionArgs, - options?: ContractWriteOptions, - ] - : [ - functionName: TFunctionName, - args: FunctionArgs, - options?: ContractWriteOptions, - ]; - -export type ContractEncodeFunctionDataArgs< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = FunctionArgs extends EmptyObject - ? [functionName: TFunctionName, args?: FunctionArgs] - : [functionName: TFunctionName, args: FunctionArgs]; - -export type ContractDecodeFunctionDataArgs = [data: string]; diff --git a/packages/drift/src/adapter/network/MockNetwork.test.ts b/packages/drift/src/adapter/network/MockNetwork.test.ts deleted file mode 100644 index edf49b30..00000000 --- a/packages/drift/src/adapter/network/MockNetwork.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - MockNetwork, - transactionToReceipt, -} from "src/adapter/network/MockNetwork"; -import { ALICE } from "src/utils/testing/accounts"; -import { describe, expect, it } from "vitest"; -import type { Transaction } from "./types/Transaction"; - -describe("MockNetwork", () => { - it("stubs getBalance", async () => { - const network = new MockNetwork(); - - network.stubGetBalance({ - args: [ALICE], - value: 100n, - }); - - const balance = await network.getBalance(ALICE); - - expect(balance).toEqual(100n); - }); - - it("stubs getBlock", async () => { - const network = new MockNetwork(); - - const block = { - blockNumber: 1n, - timestamp: 1000n, - }; - network.stubGetBlock({ - args: [{ blockNumber: 1n }], - value: block, - }); - - const blockResponse = await network.getBlock({ blockNumber: 1n }); - - expect(blockResponse).toEqual(block); - }); - - it("stubs getChainId", async () => { - const network = new MockNetwork(); - - network.stubGetChainId(42069); - - const chainId = await network.getChainId(); - - expect(chainId).toEqual(42069); - }); - - it("stubs getTransaction", async () => { - const network = new MockNetwork(); - - const txHash = "0x123abc"; - const tx: Transaction = { - gas: 100n, - gasPrice: 100n, - input: "0x456def", - nonce: 0, - type: "0x0", - value: 0n, - }; - - network.stubGetTransaction({ - args: [txHash], - value: tx, - }); - - const transaction = await network.getTransaction(txHash); - - expect(transaction).toEqual(tx); - }); - - it("waits for stubbed transactions", async () => { - const network = new MockNetwork(); - - const txHash = "0x123abc"; - const stubbedTx = { - gas: 100n, - gasPrice: 100n, - input: "0x456def", - nonce: 0, - type: "0x0", - value: 0n, - } as const; - - const waitPromise = network.waitForTransaction(txHash); - - await new Promise((resolve) => { - setTimeout(() => { - network.stubGetTransaction({ - args: [txHash], - value: stubbedTx, - }); - resolve(undefined); - }, 1000); - }); - - const tx = await waitPromise; - - expect(tx).toEqual(transactionToReceipt(stubbedTx)); - }); - - it("reaches timeout when waiting for transactions that are never stubbed", async () => { - const network = new MockNetwork(); - - const waitPromise = await network.waitForTransaction("0x123abc", { - timeout: 1000, - }); - - expect(waitPromise).toBe(undefined); - }); -}); diff --git a/packages/drift/src/adapter/network/MockNetwork.ts b/packages/drift/src/adapter/network/MockNetwork.ts deleted file mode 100644 index c6af2e29..00000000 --- a/packages/drift/src/adapter/network/MockNetwork.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { type SinonStub, stub } from "sinon"; -import type { Block } from "src/adapter/network/types/Block"; -import type { - NetworkAdapter, - NetworkGetBalanceArgs, - NetworkGetBlockArgs, - NetworkGetTransactionArgs, - NetworkWaitForTransactionArgs, -} from "src/adapter/network/types/NetworkAdapter"; -import type { - Transaction, - TransactionReceipt, -} from "src/adapter/network/types/Transaction"; - -/** - * A mock implementation of a `Network` designed to facilitate unit - * testing. - */ -export class MockNetwork implements NetworkAdapter { - protected getBalanceStub: - | SinonStub<[NetworkGetBalanceArgs?], Promise> - | undefined; - protected getBlockStub: - | SinonStub<[NetworkGetBlockArgs?], Promise> - | undefined; - protected getChainIdStub: SinonStub<[], Promise> | undefined; - protected getTransactionStub: - | SinonStub<[NetworkGetTransactionArgs?], Promise> - | undefined; - - stubGetBalance({ - args, - value, - }: { - args?: NetworkGetBalanceArgs | undefined; - value: bigint; - }): void { - if (!this.getBalanceStub) { - this.getBalanceStub = stub(); - } - - // Account for dynamic args if provided - if (args) { - this.getBalanceStub.withArgs(args).resolves(value); - return; - } - - this.getBalanceStub.resolves(value); - } - - stubGetBlock({ - args, - value, - }: { - args?: NetworkGetBlockArgs | undefined; - value: Block | undefined; - }): void { - if (!this.getBlockStub) { - this.getBlockStub = stub(); - } - - // Account for dynamic args if provided - if (args) { - this.getBlockStub.withArgs(args).resolves(value); - return; - } - - this.getBlockStub.resolves(value); - } - - stubGetChainId(id: number): void { - if (!this.getChainIdStub) { - this.getChainIdStub = stub(); - } - - this.getChainIdStub.resolves(id); - } - - stubGetTransaction({ - args, - value, - }: { - args?: NetworkGetTransactionArgs; - value: Transaction | undefined; - }): void { - if (!this.getTransactionStub) { - this.getTransactionStub = stub(); - } - - // Account for dynamic args if provided - if (args) { - this.getTransactionStub.withArgs(args).resolves(value); - return; - } - - this.getTransactionStub.resolves(value); - } - - getBalance(...args: NetworkGetBalanceArgs): Promise { - if (!this.getBalanceStub) { - throw new Error( - "The getBalance function must be stubbed first:\n\tcontract.stubGetBalance()", - ); - } - return this.getBalanceStub(args); - } - - getBlock(...args: NetworkGetBlockArgs): Promise { - if (!this.getBlockStub) { - throw new Error( - "The getBlock function must be stubbed first:\n\tcontract.stubGetBlock()", - ); - } - return this.getBlockStub(args); - } - - getChainId(): Promise { - if (!this.getChainIdStub) { - throw new Error( - "The getChainId function must be stubbed first:\n\tcontract.stubGetChainId()", - ); - } - return this.getChainIdStub(); - } - - getTransaction( - ...args: NetworkGetTransactionArgs - ): Promise { - if (!this.getTransactionStub) { - throw new Error( - "The getTransaction function must be stubbed first:\n\tcontract.stubGetTransaction()", - ); - } - return this.getTransactionStub(args); - } - - async waitForTransaction( - ...[hash, { timeout = 60_000 } = {}]: NetworkWaitForTransactionArgs - ): Promise { - // biome-ignore lint/suspicious/noAsyncPromiseExecutor: special case for testing - return new Promise(async (resolve) => { - let transaction: Transaction | undefined; - - transaction = await this.getTransactionStub?.([hash]).catch(); - - if (transaction) { - return resolve(transactionToReceipt(transaction)); - } - - // Poll for the transaction until it's found or the timeout is reached - let waitedTime = 0; - const interval = setInterval(async () => { - waitedTime += 1000; - transaction = await this.getTransactionStub?.([hash]).catch(); - if (transaction || waitedTime >= timeout) { - clearInterval(interval); - resolve(transactionToReceipt(transaction)); - } - }, 1000); - }); - } -} - -export function transactionToReceipt( - transaction: Transaction | undefined, -): TransactionReceipt | undefined { - return transaction - ? { - blockHash: transaction.blockHash!, - blockNumber: transaction.blockNumber!, - from: transaction.from!, - to: transaction.to!, - transactionIndex: transaction.transactionIndex!, - cumulativeGasUsed: 0n, - effectiveGasPrice: 0n, - transactionHash: transaction.hash!, - gasUsed: 0n, - logsBloom: "0x", - status: "success", - } - : undefined; -} diff --git a/packages/drift/src/adapter/contract/types/abi.ts b/packages/drift/src/adapter/types/Abi.ts similarity index 99% rename from packages/drift/src/adapter/contract/types/abi.ts rename to packages/drift/src/adapter/types/Abi.ts index 2b1c6aa5..52c3897e 100644 --- a/packages/drift/src/adapter/contract/types/abi.ts +++ b/packages/drift/src/adapter/types/Abi.ts @@ -137,11 +137,11 @@ type NamedParametersToObject< // key, so we have to use `number` as the key for any parameters that have // empty names ("") in arrays Extract extends never - ? any // <- No parameters with empty names + ? {} // <- No parameters with empty names : { [index: number]: AbiParameterToPrimitiveType< Extract, - "inputs" + TParameterKind >; }) >; diff --git a/packages/drift/src/adapter/types.ts b/packages/drift/src/adapter/types/Adapter.ts similarity index 58% rename from packages/drift/src/adapter/types.ts rename to packages/drift/src/adapter/types/Adapter.ts index 9a1018e4..ffbf3b42 100644 --- a/packages/drift/src/adapter/types.ts +++ b/packages/drift/src/adapter/types/Adapter.ts @@ -3,48 +3,51 @@ import type { ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, -} from "src/adapter/contract/types/contract"; -import type { Event, EventName } from "src/adapter/contract/types/event"; +} from "src/adapter/types/Contract"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; import type { + DecodedFunctionData, FunctionArgs, FunctionName, FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { NetworkAdapter } from "src/adapter/network/types/NetworkAdapter"; -import type { TransactionReceipt } from "src/adapter/network/types/Transaction"; +} from "src/adapter/types/Function"; +import type { Network } from "src/adapter/types/Network"; +import type { TransactionReceipt } from "src/adapter/types/Transaction"; import type { Address, Bytes, TransactionHash } from "src/types"; -import type { EmptyObject } from "src/utils/types"; +import type { AnyObject, EmptyObject } from "src/utils/types"; -export interface ReadAdapter extends NetworkAdapter { +export type Adapter = ReadAdapter | ReadWriteAdapter; + +export interface ReadAdapter extends Network { read< TAbi extends Abi, TFunctionName extends FunctionName, >( - params: ReadParams, + params: AdapterReadParams, ): Promise>; getEvents>( - params: GetEventsParams, - ): Promise[]>; + params: AdapterGetEventsParams, + ): Promise[]>; simulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, >( - params: WriteParams, + params: AdapterWriteParams, ): Promise>; encodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: EncodeFunctionDataParams): Bytes; + >(params: AdapterEncodeFunctionDataParams): Bytes; decodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, >( - params: DecodeFunctionDataParams, - ): FunctionReturn; + params: AdapterDecodeFunctionDataParams, + ): DecodedFunctionData; } export interface ReadWriteAdapter extends ReadAdapter { @@ -53,36 +56,38 @@ export interface ReadWriteAdapter extends ReadAdapter { write< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: WriteParams): Promise; + >(params: AdapterWriteParams): Promise; } -export type Adapter = ReadAdapter | ReadWriteAdapter; - -export type ArgsParam< +export type AdapterArgsParam< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = FunctionArgs extends EmptyObject +> = Abi extends TAbi ? { - args?: FunctionArgs; + args?: AnyObject; } - : Abi extends TAbi + : EmptyObject extends FunctionArgs ? { - args?: FunctionArgs; + args?: EmptyObject; } : { args: FunctionArgs; }; -export type ReadParams< +export type AdapterReadParams< TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = ContractReadOptions & { + TFunctionName extends FunctionName = FunctionName< + TAbi, + "pure" | "view" + >, +> = { abi: TAbi; address: Address; fn: TFunctionName; -} & ArgsParam; +} & AdapterArgsParam & + ContractReadOptions; -export interface GetEventsParams< +export interface AdapterGetEventsParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, > extends ContractGetEventsOptions { @@ -91,28 +96,33 @@ export interface GetEventsParams< event: TEventName; } -export type WriteParams< +export interface OnMinedParam { + onMined?: (receipt?: TransactionReceipt) => void; +} + +export type AdapterWriteParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName< TAbi, "nonpayable" | "payable" > = FunctionName, -> = ContractWriteOptions & { +> = { abi: TAbi; address: Address; fn: TFunctionName; - onMined?: (receipt?: TransactionReceipt) => void; -} & ArgsParam; +} & AdapterArgsParam & + ContractWriteOptions & + OnMinedParam; -export type EncodeFunctionDataParams< +export type AdapterEncodeFunctionDataParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, > = { abi: TAbi; fn: TFunctionName; -} & ArgsParam; +} & AdapterArgsParam; -export interface DecodeFunctionDataParams< +export interface AdapterDecodeFunctionDataParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, > { diff --git a/packages/drift/src/adapter/network/types/Block.ts b/packages/drift/src/adapter/types/Block.ts similarity index 100% rename from packages/drift/src/adapter/network/types/Block.ts rename to packages/drift/src/adapter/types/Block.ts diff --git a/packages/drift/src/adapter/types/Contract.ts b/packages/drift/src/adapter/types/Contract.ts new file mode 100644 index 00000000..e55bb566 --- /dev/null +++ b/packages/drift/src/adapter/types/Contract.ts @@ -0,0 +1,60 @@ +import type { Abi } from "abitype"; +import type { BlockTag } from "src/adapter/types/Block"; +import type { EventFilter, EventName } from "src/adapter/types/Event"; + +// https://ethereum.github.io/execution-apis/api-documentation/ + +// https://github.com/ethereum/execution-apis/blob/main/src/eth/execute.yaml#L1 +export interface ContractReadOptions { + /** + * @default 'latest' + */ + block?: BlockTag | bigint; +} + +export interface ContractGetEventsOptions< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> { + filter?: EventFilter; + fromBlock?: bigint | BlockTag; + toBlock?: bigint | BlockTag; +} + +// https://github.com/ethereum/execution-apis/blob/main/src/schemas/transaction.yaml#L274 +export interface ContractWriteOptions { + type?: string; + nonce?: bigint; + to?: string; + from?: string; + /** + * Gas limit + */ + gas?: bigint; + value?: bigint; + input?: string; + /** + * The gas price willing to be paid by the sender in wei + */ + gasPrice?: bigint; + /** + * Maximum fee per gas the sender is willing to pay to miners in wei + */ + maxPriorityFeePerGas?: bigint; + /** + * The maximum total fee per gas the sender is willing to pay (includes the + * network / base fee and miner / priority fee) in wei + */ + maxFeePerGas?: bigint; + /** + * EIP-2930 access list + */ + accessList?: { + address: string; + storageKeys: string[]; + }[]; + /** + * Chain ID that this transaction is valid on. + */ + chainId?: bigint; +} diff --git a/packages/drift/src/adapter/contract/types/Event.ts b/packages/drift/src/adapter/types/Event.ts similarity index 95% rename from packages/drift/src/adapter/contract/types/Event.ts rename to packages/drift/src/adapter/types/Event.ts index d524721f..be6a3172 100644 --- a/packages/drift/src/adapter/contract/types/Event.ts +++ b/packages/drift/src/adapter/types/Event.ts @@ -5,7 +5,7 @@ import type { AbiParameters, AbiParametersToObject, NamedAbiParameter, -} from "src/adapter/contract/types/abi"; +} from "src/adapter/types/Abi"; /** * Get a union of event names from an abi @@ -52,7 +52,7 @@ export type EventFilter< /** * A strongly typed event object based on an abi */ -export interface Event< +export interface ContactEvent< TAbi extends Abi, TEventName extends EventName = EventName, > { diff --git a/packages/drift/src/adapter/contract/types/Function.ts b/packages/drift/src/adapter/types/Function.ts similarity index 86% rename from packages/drift/src/adapter/contract/types/Function.ts rename to packages/drift/src/adapter/types/Function.ts index a9b16926..23d40aa8 100644 --- a/packages/drift/src/adapter/contract/types/Function.ts +++ b/packages/drift/src/adapter/types/Function.ts @@ -1,8 +1,8 @@ import type { Abi, AbiStateMutability } from "abitype"; import type { - AbiFriendlyType, - AbiObjectType, -} from "src/adapter/contract/types/abi"; + AbiFriendlyType, + AbiObjectType, +} from "src/adapter/types/Abi"; /** * Get a union of function names from an abi @@ -10,10 +10,12 @@ import type { export type FunctionName< TAbi extends Abi, TAbiStateMutability extends AbiStateMutability = AbiStateMutability, -> = Extract< - TAbi[number], - { type: "function"; stateMutability: TAbiStateMutability } ->["name"]; +> = Abi extends TAbi + ? string + : Extract< + TAbi[number], + { type: "function"; stateMutability: TAbiStateMutability } + >["name"]; /** * Get an object type for an abi function's arguments. diff --git a/packages/drift/src/adapter/network/types/NetworkAdapter.ts b/packages/drift/src/adapter/types/Network.ts similarity index 92% rename from packages/drift/src/adapter/network/types/NetworkAdapter.ts rename to packages/drift/src/adapter/types/Network.ts index ea6e1360..18381973 100644 --- a/packages/drift/src/adapter/network/types/NetworkAdapter.ts +++ b/packages/drift/src/adapter/types/Network.ts @@ -1,8 +1,8 @@ -import type { Block, BlockTag } from "src/adapter/network/types/Block"; +import type { Block, BlockTag } from "src/adapter/types/Block"; import type { Transaction, TransactionReceipt, -} from "src/adapter/network/types/Transaction"; +} from "src/adapter/types/Transaction"; import type { Address, HexString, TransactionHash } from "src/types"; // https://ethereum.github.io/execution-apis/api-documentation/ @@ -10,7 +10,7 @@ import type { Address, HexString, TransactionHash } from "src/types"; /** * An interface representing data the SDK needs to get from the network. */ -export interface NetworkAdapter { +export interface Network { /** * Get the balance of native currency for an account. */ diff --git a/packages/drift/src/adapter/network/types/Transaction.ts b/packages/drift/src/adapter/types/Transaction.ts similarity index 100% rename from packages/drift/src/adapter/network/types/Transaction.ts rename to packages/drift/src/adapter/types/Transaction.ts diff --git a/packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts b/packages/drift/src/adapter/utils/arrayToFriendly.test.ts similarity index 95% rename from packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts rename to packages/drift/src/adapter/utils/arrayToFriendly.test.ts index ccfda0b4..47d5822b 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToFriendly.test.ts +++ b/packages/drift/src/adapter/utils/arrayToFriendly.test.ts @@ -1,4 +1,4 @@ -import { arrayToFriendly } from "src/adapter/contract/utils/arrayToFriendly"; +import { arrayToFriendly } from "src/adapter/utils/arrayToFriendly"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts b/packages/drift/src/adapter/utils/arrayToFriendly.ts similarity index 95% rename from packages/drift/src/adapter/contract/utils/arrayToFriendly.ts rename to packages/drift/src/adapter/utils/arrayToFriendly.ts index 20172eb1..bc5bdfa4 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToFriendly.ts +++ b/packages/drift/src/adapter/utils/arrayToFriendly.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiFriendlyType, -} from "src/adapter/contract/types/abi"; -import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; +} from "src/adapter/types/Abi"; +import { getAbiEntry } from "src/adapter/utils/getAbiEntry"; /** * Converts an array of input or output values into an diff --git a/packages/drift/src/adapter/contract/utils/arrayToObject.test.ts b/packages/drift/src/adapter/utils/arrayToObject.test.ts similarity index 94% rename from packages/drift/src/adapter/contract/utils/arrayToObject.test.ts rename to packages/drift/src/adapter/utils/arrayToObject.test.ts index 1c520c5f..ab1738f9 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToObject.test.ts +++ b/packages/drift/src/adapter/utils/arrayToObject.test.ts @@ -1,4 +1,4 @@ -import { arrayToObject } from "src/adapter/contract/utils/arrayToObject"; +import { arrayToObject } from "src/adapter/utils/arrayToObject"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/adapter/contract/utils/arrayToObject.ts b/packages/drift/src/adapter/utils/arrayToObject.ts similarity index 95% rename from packages/drift/src/adapter/contract/utils/arrayToObject.ts rename to packages/drift/src/adapter/utils/arrayToObject.ts index 37c9f56b..b4903a54 100644 --- a/packages/drift/src/adapter/contract/utils/arrayToObject.ts +++ b/packages/drift/src/adapter/utils/arrayToObject.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/adapter/contract/types/abi"; -import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; +} from "src/adapter/types/Abi"; +import { getAbiEntry } from "src/adapter/utils/getAbiEntry"; /** * Converts an array of input or output values into an object typ, ensuring the diff --git a/packages/drift/src/adapter/contract/utils/getAbiEntry.ts b/packages/drift/src/adapter/utils/getAbiEntry.ts similarity index 91% rename from packages/drift/src/adapter/contract/utils/getAbiEntry.ts rename to packages/drift/src/adapter/utils/getAbiEntry.ts index 4775629b..ba1b8609 100644 --- a/packages/drift/src/adapter/contract/utils/getAbiEntry.ts +++ b/packages/drift/src/adapter/utils/getAbiEntry.ts @@ -1,8 +1,8 @@ import type { Abi, AbiItemType } from "abitype"; import type { - AbiEntry, - AbiEntryName, -} from "src/adapter/contract/types/abi"; + AbiEntry, + AbiEntryName, +} from "src/adapter/types/Abi"; import { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; /** diff --git a/packages/drift/src/adapter/contract/utils/objectToArray.test.ts b/packages/drift/src/adapter/utils/objectToArray.test.ts similarity index 94% rename from packages/drift/src/adapter/contract/utils/objectToArray.test.ts rename to packages/drift/src/adapter/utils/objectToArray.test.ts index e3f77513..7f4fa600 100644 --- a/packages/drift/src/adapter/contract/utils/objectToArray.test.ts +++ b/packages/drift/src/adapter/utils/objectToArray.test.ts @@ -1,4 +1,4 @@ -import { objectToArray } from "src/adapter/contract/utils/objectToArray"; +import { objectToArray } from "src/adapter/utils/objectToArray"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/adapter/contract/utils/objectToArray.ts b/packages/drift/src/adapter/utils/objectToArray.ts similarity index 95% rename from packages/drift/src/adapter/contract/utils/objectToArray.ts rename to packages/drift/src/adapter/utils/objectToArray.ts index 97f938e1..e129bd6b 100644 --- a/packages/drift/src/adapter/contract/utils/objectToArray.ts +++ b/packages/drift/src/adapter/utils/objectToArray.ts @@ -3,8 +3,8 @@ import type { AbiArrayType, AbiEntryName, AbiObjectType, -} from "src/adapter/contract/types/abi"; -import { getAbiEntry } from "src/adapter/contract/utils/getAbiEntry"; +} from "src/adapter/types/Abi"; +import { getAbiEntry } from "src/adapter/utils/getAbiEntry"; /** * Converts an object into an array of input or output values, ensuring the the diff --git a/packages/drift/src/cache/DriftCache/createDriftCache.test.ts b/packages/drift/src/cache/ClientCache/createClientCache.test.ts similarity index 87% rename from packages/drift/src/cache/DriftCache/createDriftCache.test.ts rename to packages/drift/src/cache/ClientCache/createClientCache.test.ts index c31f20ae..93b8808e 100644 --- a/packages/drift/src/cache/DriftCache/createDriftCache.test.ts +++ b/packages/drift/src/cache/ClientCache/createClientCache.test.ts @@ -1,10 +1,10 @@ -import { createDriftCache } from "src/cache/DriftCache/createDriftCache"; +import { createClientCache } from "src/cache/ClientCache/createClientCache"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; -describe("createDriftCache", () => { +describe("createClientCache", () => { it("Invalidates reads by their read key", () => { - const cache = createDriftCache(); + const cache = createClientCache(); const params = { abi: IERC20.abi, address: "0xContract", @@ -25,7 +25,7 @@ describe("createDriftCache", () => { }); it("Invalidates reads matching a partial read key", () => { - const cache = createDriftCache(); + const cache = createClientCache(); const params = { abi: IERC20.abi, address: "0xContract", @@ -46,7 +46,7 @@ describe("createDriftCache", () => { }); it("Preloads reads by their key", async () => { - const cache = createDriftCache(); + const cache = createClientCache(); const params = { abi: IERC20.abi, address: "0xContract", @@ -64,7 +64,7 @@ describe("createDriftCache", () => { }); it("Preloads events by their key", async () => { - const cache = createDriftCache(); + const cache = createClientCache(); const params = { abi: IERC20.abi, address: "0xContract", diff --git a/packages/drift/src/cache/ClientCache/createClientCache.ts b/packages/drift/src/cache/ClientCache/createClientCache.ts new file mode 100644 index 00000000..cf7f07bc --- /dev/null +++ b/packages/drift/src/cache/ClientCache/createClientCache.ts @@ -0,0 +1,56 @@ +import isMatch from "lodash.ismatch"; +import type { + ClientCache, + DriftReadKeyParams, +} from "src/cache/ClientCache/types"; +import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { + type SerializableKey, + createSerializableKey, +} from "src/utils/createSerializableKey"; +import { extendInstance } from "src/utils/extendInstance"; + +/** + * Extends a {@linkcode SimpleCache} with additional API methods for use with + * Drift clients. + */ +export function createClientCache( + cache: T = createLruSimpleCache({ max: 500 }) as T, +): ClientCache { + const clientCache: ClientCache = extendInstance< + T, + Omit + >(cache, { + partialReadKey: ({ abi, namespace, ...params }) => + createSerializableKey([namespace, "read", params]), + + readKey: (params) => clientCache.partialReadKey(params), + + eventsKey: ({ abi, namespace, ...params }) => + createSerializableKey([namespace, "events", params]), + + preloadRead: ({ value, ...params }) => + cache.set(clientCache.readKey(params as DriftReadKeyParams), value), + + preloadEvents: ({ value, ...params }) => + cache.set(clientCache.eventsKey(params), value), + + invalidateRead: (params) => cache.delete(clientCache.readKey(params)), + + invalidateReadsMatching(params) { + const sourceKey = clientCache.partialReadKey(params); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SerializableKey[]) + ) { + cache.delete(key); + } + } + }, + }); + + return clientCache; +} diff --git a/packages/drift/src/cache/DriftCache/types.ts b/packages/drift/src/cache/ClientCache/types.ts similarity index 53% rename from packages/drift/src/cache/DriftCache/types.ts rename to packages/drift/src/cache/ClientCache/types.ts index f7133eae..3c36e73c 100644 --- a/packages/drift/src/cache/DriftCache/types.ts +++ b/packages/drift/src/cache/ClientCache/types.ts @@ -1,29 +1,28 @@ import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/event"; import type { - FunctionName, - FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { GetEventsParams, ReadParams } from "src/adapter/types"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import type { DeepPartial, MaybePromise } from "src/utils/types"; + AdapterGetEventsParams, + AdapterReadParams, +} from "src/adapter/types/Adapter"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { FunctionName, FunctionReturn } from "src/adapter/types/Function"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import type { SerializableKey } from "src/utils/createSerializableKey"; +import type { MaybePromise, DeepPartial as Partial } from "src/utils/types"; -export type DriftCache = T & { - // Key generation // +export type ClientCache = T & { + // Events // - partialReadKey>( - params: DeepPartial>, - ): SimpleCacheKey; - - readKey>( - params: DriftReadKeyParams, - ): SimpleCacheKey; + preloadEvents>( + params: DriftEventsKeyParams & { + value: readonly ContactEvent[]; + }, + ): MaybePromise; eventsKey>( params: DriftEventsKeyParams, - ): SimpleCacheKey; + ): SerializableKey; - // Cache management // + // Read // preloadRead>( params: DriftReadKeyParams & { @@ -31,12 +30,6 @@ export type DriftCache = T & { }, ): MaybePromise; - preloadEvents>( - params: DriftEventsKeyParams & { - value: readonly Event[]; - }, - ): MaybePromise; - invalidateRead>( params: DriftReadKeyParams, ): MaybePromise; @@ -45,16 +38,33 @@ export type DriftCache = T & { TAbi extends Abi, TFunctionName extends FunctionName, >( - params: DeepPartial>, + params: Partial>, ): MaybePromise; + + readKey>( + params: DriftReadKeyParams, + ): SerializableKey; + + partialReadKey>( + params: Partial>, + ): SerializableKey; }; +export interface NameSpaceParam { + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + // TODO: This needs unit tests + namespace?: PropertyKey; +} + export type DriftReadKeyParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = Omit, "cache">; +> = NameSpaceParam & AdapterReadParams; export type DriftEventsKeyParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, -> = Omit, "cache">; +> = NameSpaceParam & AdapterGetEventsParams; diff --git a/packages/drift/src/cache/DriftCache/createDriftCache.ts b/packages/drift/src/cache/DriftCache/createDriftCache.ts deleted file mode 100644 index f6079d2c..00000000 --- a/packages/drift/src/cache/DriftCache/createDriftCache.ts +++ /dev/null @@ -1,53 +0,0 @@ -import isMatch from "lodash.ismatch"; -import type { - DriftCache, - DriftReadKeyParams, -} from "src/cache/DriftCache/types"; -import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; -import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import { extendInstance } from "src/utils/extendInstance"; - -/** - * Extends a {@linkcode SimpleCache} with additional API methods for use with - * Drift clients. - */ -export function createDriftCache( - cache: T = createLruSimpleCache({ max: 500 }) as T, -): DriftCache { - const driftCache: DriftCache = extendInstance< - T, - Omit - >(cache, { - partialReadKey: ({ abi, namespace, ...params }) => - createSimpleCacheKey([namespace, "read", params]), - - readKey: (params) => driftCache.partialReadKey(params), - - eventsKey: ({ abi, namespace, ...params }) => - createSimpleCacheKey([namespace, "events", params]), - - preloadRead: ({ value, ...params }) => - cache.set(driftCache.readKey(params as DriftReadKeyParams), value), - - preloadEvents: ({ value, ...params }) => - cache.set(driftCache.eventsKey(params), value), - - invalidateRead: (params) => cache.delete(driftCache.readKey(params)), - - invalidateReadsMatching(params) { - const sourceKey = driftCache.partialReadKey(params); - - for (const [key] of cache.entries) { - if ( - typeof key === "object" && - isMatch(key, sourceKey as SimpleCacheKey[]) - ) { - cache.delete(key); - } - } - }, - }); - - return driftCache; -} diff --git a/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts index 3a55e7ae..9f8fb607 100644 --- a/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts +++ b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts @@ -1,6 +1,7 @@ import stringify from "fast-json-stable-stringify"; import { LRUCache } from "lru-cache"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import type { SerializableKey } from "src/utils/createSerializableKey"; /** * An LRU (Least Recently Used) implementation of the `SimpleCache` interface. @@ -14,7 +15,7 @@ import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; */ export function createLruSimpleCache< TValue extends NonNullable = NonNullable, - TKey extends SimpleCacheKey = SimpleCacheKey, + TKey extends SerializableKey = SerializableKey, >(options: LRUCache.Options): SimpleCache { const cache = new LRUCache(options); @@ -29,6 +30,10 @@ export function createLruSimpleCache< } return { + has(key) { + return cache.has(stringify(key)); + }, + get entries() { // Keys need to be returned in the same format as they were given to the cache return entriesGenerator(cache.entries() as Generator<[TKey, TValue]>); diff --git a/packages/drift/src/cache/SimpleCache/types.ts b/packages/drift/src/cache/SimpleCache/types.ts index 001d6d04..1231bad8 100644 --- a/packages/drift/src/cache/SimpleCache/types.ts +++ b/packages/drift/src/cache/SimpleCache/types.ts @@ -1,3 +1,5 @@ +import type { SerializableKey } from "src/utils/createSerializableKey"; + /** * Represents a simple caching mechanism with basic operations such as * get, set, delete, clear, and find. @@ -8,13 +10,18 @@ */ export interface SimpleCache< TValue = any, - TKey extends SimpleCacheKey = SimpleCacheKey, + TKey extends SerializableKey = SerializableKey, > { /** * Returns an iterable of key-value pairs for every entry in the cache. */ readonly entries: Iterable<[TKey, TValue]>; + /** + * Returns a boolean indicating whether an entry exists for the specified key. + */ + has: (key: TKey) => boolean; + /** * Retrieves the value associated with the specified key. */ @@ -47,18 +54,3 @@ export interface SimpleCache< predicate: (value: TValue, key: TKey) => boolean, ) => TValue | undefined; } - -/** - * Represents possible serializable key types for the SimpleCache. Can be a - * primitive (string, number, boolean), an array of SimpleCache (with possible - * null/undefined values), or a record with string keys and SimpleCache values. - */ -export type SimpleCacheKey = - | KeyPrimitive - | (SimpleCacheKey | null | undefined)[] - | { - [key: string]: SimpleCacheKey; - }; - -/** Primitive types that can be used as part of a cache key. */ -type KeyPrimitive = string | number | boolean; diff --git a/packages/drift/src/cache/utils/DriftCache.ts b/packages/drift/src/cache/utils/DriftCache.ts deleted file mode 100644 index 3e6bba2c..00000000 --- a/packages/drift/src/cache/utils/DriftCache.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/event"; -import type { - DriftEventsKeyParams -} from "src/cache/DriftCache/types"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { eventsKey } from "src/cache/utils/eventsKey"; - -export function preloadEvents< - TAbi extends Abi, - TEventName extends EventName, ->( - cache: SimpleCache, - { - value, - ...params - }: DriftEventsKeyParams & { - value: readonly Event[]; - }, -): void | Promise { - return cache.set(eventsKey(params), value); -} diff --git a/packages/drift/src/cache/utils/createCachedReadContract.test.ts b/packages/drift/src/cache/utils/createCachedReadContract.test.ts deleted file mode 100644 index 2d4c20b1..00000000 --- a/packages/drift/src/cache/utils/createCachedReadContract.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { ReadContractStub } from "src/adapter/contract/mocks/ReadContractStub"; -import type { Event } from "src/adapter/contract/types/event"; -import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; -import { IERC20 } from "src/utils/testing/IERC20"; -import { ALICE, BOB } from "src/utils/testing/accounts"; -import { describe, expect, it } from "vitest"; - -const ERC20ABI = IERC20.abi; - -describe("createCachedReadContract", () => { - it("caches the read function", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - const stubbedValue = "0x123abc"; - contract.stubRead({ - functionName: "name", - value: stubbedValue, - }); - - const value = await cachedContract.read("name"); - expect(value).toBe(stubbedValue); - - const value2 = await cachedContract.read("name"); - expect(value2).toBe(stubbedValue); - - const stub = contract.getReadStub("name"); - expect(stub?.callCount).toBe(1); - }); - - it("caches the getEvents function", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - const stubbedEvents: Event[] = [ - { - eventName: "Transfer", - args: { - from: ALICE, - to: BOB, - value: 100n, - }, - blockNumber: 1n, - data: "0x123abc", - transactionHash: "0x123abc", - }, - ]; - contract.stubEvents("Transfer", undefined, stubbedEvents); - - const events = await cachedContract.getEvents("Transfer"); - expect(events).toBe(stubbedEvents); - - const events2 = await cachedContract.getEvents("Transfer"); - expect(events2).toBe(stubbedEvents); - - const stub = contract.getEventsStub("Transfer"); - expect(stub?.callCount).toBe(1); - }); - - it("deletes cached reads", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - const stubbedValue = 100n; - contract.stubRead({ functionName: "balanceOf", value: stubbedValue }); - - const value = await cachedContract.read("balanceOf", { owner: "0x123abc" }); - expect(value).toBe(stubbedValue); - - cachedContract.deleteRead("balanceOf", { owner: "0x123abc" }); - - const value2 = await cachedContract.read("balanceOf", { - owner: "0x123abc", - }); - expect(value2).toBe(stubbedValue); - - const stub = contract.getReadStub("balanceOf"); - expect(stub?.callCount).toBe(2); - }); - - it("deletes cached reads from function name only", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - contract.stubRead({ - functionName: "balanceOf", - value: 100n, - args: { owner: ALICE }, - }); - contract.stubRead({ - functionName: "balanceOf", - value: 200n, - args: { owner: BOB }, - }); - - // Get both alice and bob's balance - const aliceValue = await cachedContract.read("balanceOf", { owner: ALICE }); - expect(aliceValue).toBe(100n); - - const bobValue = await cachedContract.read("balanceOf", { owner: BOB }); - expect(bobValue).toBe(200n); - - // Deleting anything that matches a balanceOf call - cachedContract.deleteReadsMatching("balanceOf"); - - // Request bob and alice's balance again - const aliceValue2 = await cachedContract.read("balanceOf", { - owner: ALICE, - }); - expect(aliceValue2).toBe(100n); - const bobValue2 = await cachedContract.read("balanceOf", { owner: BOB }); - expect(bobValue2).toBe(200n); - - const stub = contract.getReadStub("balanceOf"); - expect(stub?.callCount).toBe(4); - }); - - it("deletes cached reads with partial args", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - const aliceArgs = { owner: ALICE, spender: BOB } as const; - contract.stubRead({ - functionName: "allowance", - value: 100n, - args: aliceArgs, - }); - - const bobArgs = { owner: BOB, spender: ALICE } as const; - contract.stubRead({ - functionName: "allowance", - value: 200n, - args: bobArgs, - }); - - // Get both alice and bob's allowance - await cachedContract.read("allowance", aliceArgs); - await cachedContract.read("allowance", bobArgs); - - // Deleting any allowance calls where BOB is the spender - cachedContract.deleteReadsMatching("allowance", { spender: BOB }); - - // Request bob and alice's allowance again - await cachedContract.read("allowance", aliceArgs); - await cachedContract.read("allowance", bobArgs); - - const stub = contract.getReadStub("allowance"); - expect(stub?.callCount).toBe(3); - }); - - it("clears the cache", async () => { - const contract = new ReadContractStub(ERC20ABI); - const cachedContract = createCachedReadContract({ contract }); - - contract.stubRead({ functionName: "balanceOf", value: 100n }); - contract.stubRead({ - functionName: "name", - value: "Base Token", - }); - - await cachedContract.read("balanceOf", { owner: "0x123abc" }); - await cachedContract.read("name"); - - cachedContract.clearCache(); - - await cachedContract.read("balanceOf", { owner: "0x123abc" }); - await cachedContract.read("name"); - - const stubA = contract.getReadStub("balanceOf"); - const stubB = contract.getReadStub("name"); - expect(stubA?.callCount).toBe(2); - expect(stubB?.callCount).toBe(2); - }); -}); diff --git a/packages/drift/src/cache/utils/createCachedReadContract.ts b/packages/drift/src/cache/utils/createCachedReadContract.ts deleted file mode 100644 index a60a3d87..00000000 --- a/packages/drift/src/cache/utils/createCachedReadContract.ts +++ /dev/null @@ -1,168 +0,0 @@ -import type { Abi } from "abitype"; -import isMatch from "lodash.ismatch"; -import type { AdapterReadContract } from "src/adapter/contract/types/contract"; -import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; -import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import type { ReadContract } from "src/contract/types"; - -// TODO: Figure out a good default cache size -const DEFAULT_CACHE_SIZE = 100; - -export interface CreateCachedReadContractOptions { - contract: AdapterReadContract; - cache?: SimpleCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -/** - * A wrapped Ethereum contract reader that provides caching capabilities. Useful - * for reducing the number of actual reads from a contract by caching and - * reusing previous read results. - * - * @example - * const cachedContract = new CachedReadContract({ contract: myContract }); - * const result1 = await cachedContract.read("functionName", args); - * const result2 = await cachedContract.read("functionName", args); // Fetched from cache - */ -export function createCachedReadContract({ - contract, - cache = createLruSimpleCache({ max: DEFAULT_CACHE_SIZE }), - namespace, -}: CreateCachedReadContractOptions): ReadContract { - // Because this is part of the public API, we won't know if the original - // contract is a plain object or a class instance, so we use Object.create to - // preserve the original contract's prototype chain when extending, ensuring - // the new contract includes all the original contract's methods and - // instanceof checks will still work. - const contractPrototype = Object.getPrototypeOf(contract); - const newContract = Object.create(contractPrototype); - - const overrides: Partial> = { - cache, - - /** - * Reads data from the contract. First checks the cache, and if not present, - * fetches from the contract and then caches the result. - */ - async read(functionName, args, options) { - return getOrSet({ - cache, - key: createSimpleCacheKey([ - namespace, - "read", - { - address: contract.address, - functionName, - args, - options, - }, - ]), - callback: () => contract.read(functionName, args, options), - }); - }, - - /** - * Deletes a specific read from the cache. - * - * @example - * const cachedContract = new CachedReadContract({ contract: myContract }); - * const result1 = await cachedContract.read("functionName", args); - * const result2 = await cachedContract.read("functionName", args); // Fetched from cache - * - * cachedContract.deleteRead("functionName", args); - * const result3 = await cachedContract.read("functionName", args); // Fetched from contract - */ - deleteRead(functionName, args, options) { - const key = createSimpleCacheKey([ - namespace, - "read", - { - address: contract.address, - functionName, - args, - options, - }, - ]); - - cache.delete(key); - }, - - deleteReadsMatching(...args) { - const [functionName, functionArgs, options] = args; - - const sourceKey = createSimpleCacheKey([ - namespace, - "read", - { - address: contract.address, - functionName, - args: functionArgs, - options, - }, - ]); - - for (const [key] of cache.entries) { - if ( - typeof key === "object" && - isMatch(key, sourceKey as SimpleCacheKey[]) - ) { - cache.delete(key); - } - } - }, - - /** - * Gets events from the contract. First checks the cache, and if not present, - * fetches from the contract and then caches the result. - */ - async getEvents(eventName, options) { - return getOrSet({ - cache, - key: createSimpleCacheKey([ - namespace, - "getEvents", - { - address: contract.address, - eventName, - options, - }, - ]), - callback: () => contract.getEvents(eventName, options), - }); - }, - - /** - * Clears the entire cache. - */ - clearCache() { - cache.clear(); - }, - }; - - return Object.assign(newContract, contract, overrides); -} - -async function getOrSet({ - cache, - key, - callback, -}: { - cache: SimpleCache; - key: SimpleCacheKey; - callback: () => Promise | TValue; -}): Promise { - let value = cache.get(key); - if (typeof value !== "undefined") { - return value; - } - - value = await callback(); - cache.set(key, value); - - return value; -} diff --git a/packages/drift/src/cache/utils/createCachedReadWriteContract.ts b/packages/drift/src/cache/utils/createCachedReadWriteContract.ts deleted file mode 100644 index 10733359..00000000 --- a/packages/drift/src/cache/utils/createCachedReadWriteContract.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Abi } from "abitype"; -import type { AdapterReadWriteContract } from "src/adapter/contract/types/contract"; -import type { CachedReadWriteContract } from "src/cache/types/CachedContract"; -import { - type CreateCachedReadContractOptions, - createCachedReadContract, -} from "src/cache/utils/createCachedReadContract"; - -export interface CreateCachedReadWriteContractOptions - extends CreateCachedReadContractOptions { - contract: AdapterReadWriteContract; -} - -/** - * Provides a cached wrapper around an Ethereum writable contract. This class is - * useful for both reading (with caching) and writing to a contract. It extends - * the functionality provided by CachedReadContract by adding write - * capabilities. - */ -export function createCachedReadWriteContract({ - contract, - cache, - namespace, -}: CreateCachedReadWriteContractOptions): CachedReadWriteContract { - // Avoid double-caching if given a contract that already has a cache. - if (isCached(contract)) { - return contract; - } - // Because this is part of the public API, we won't know if the original - // contract is a plain object or a class instance, so we use Object.create to - // preserve the original contract's prototype chain when extending, ensuring - // the new contract includes all the original contract's methods and - // instanceof checks will still work. - const contractPrototype = Object.getPrototypeOf(contract); - const newContract = Object.create(contractPrototype); - return Object.assign( - newContract, - createCachedReadContract({ contract, cache, namespace }), - ); -} - -function isCached( - contract: AdapterReadWriteContract, -): contract is CachedReadWriteContract { - return "clearCache" in contract; -} diff --git a/packages/drift/src/cache/utils/eventsKey.ts b/packages/drift/src/cache/utils/eventsKey.ts deleted file mode 100644 index 37aefec5..00000000 --- a/packages/drift/src/cache/utils/eventsKey.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { Abi } from "abitype"; -import type { EventName } from "src/adapter/contract/types/event"; -import type { DriftEventsKeyParams } from "src/cache/DriftCache/types"; -import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; -import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; - -export function eventsKey< - TAbi extends Abi, - TEventName extends EventName, ->({ - abi, - namespace, - ...params -}: DriftEventsKeyParams): SimpleCacheKey { - return createSimpleCacheKey([namespace, "events", params]); -} diff --git a/packages/drift/src/cache/utils/invalidateRead.ts b/packages/drift/src/cache/utils/invalidateRead.ts deleted file mode 100644 index a6dc5883..00000000 --- a/packages/drift/src/cache/utils/invalidateRead.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/function"; -import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { readKey } from "src/cache/utils/readKey"; - -export function invalidateRead< - TAbi extends Abi, - TFunctionName extends FunctionName, ->( - cache: SimpleCache, - params: DriftReadKeyParams, -): void | Promise { - return cache.delete(readKey(params)); -} diff --git a/packages/drift/src/cache/utils/invalidateReadsMatching.ts b/packages/drift/src/cache/utils/invalidateReadsMatching.ts deleted file mode 100644 index b2498532..00000000 --- a/packages/drift/src/cache/utils/invalidateReadsMatching.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { Abi } from "abitype"; -import isMatch from "lodash.ismatch"; -import type { FunctionName } from "src/adapter/contract/types/function"; -import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; -import type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -import { partialReadKey } from "src/cache/utils/partialReadKey"; -import type { DeepPartial } from "src/utils/types"; - -export function invalidateReadsMatching< - TAbi extends Abi, - TFunctionName extends FunctionName, ->( - cache: SimpleCache, - params: DeepPartial>, -): void | Promise { - const sourceKey = partialReadKey(params); - - for (const [key] of cache.entries) { - if ( - typeof key === "object" && - isMatch(key, sourceKey as SimpleCacheKey[]) - ) { - cache.delete(key); - } - } -} diff --git a/packages/drift/src/cache/utils/partialReadKey.ts b/packages/drift/src/cache/utils/partialReadKey.ts deleted file mode 100644 index 1e9bd920..00000000 --- a/packages/drift/src/cache/utils/partialReadKey.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/function"; -import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; -import { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; -import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; -import type { DeepPartial } from "src/utils/types"; - -export function partialReadKey< - TAbi extends Abi, - TFunctionName extends FunctionName, ->({ - abi, - namespace, - ...params -}: DeepPartial>): SimpleCacheKey { - return createSimpleCacheKey([namespace, "read", params]); -} diff --git a/packages/drift/src/cache/utils/preloadRead.ts b/packages/drift/src/cache/utils/preloadRead.ts deleted file mode 100644 index d058a2af..00000000 --- a/packages/drift/src/cache/utils/preloadRead.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Abi } from "abitype"; -import type { FunctionName, FunctionReturn } from "src/adapter/contract/types/function"; -import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { readKey } from "src/cache/utils/readKey"; - -export function preloadRead< - TAbi extends Abi, - TFunctionName extends FunctionName, ->( - cache: SimpleCache, - { - value, - ...params - }: DriftReadKeyParams & { - value: FunctionReturn; - }, -): void | Promise { - return cache.set(readKey(params as DriftReadKeyParams), value); -} diff --git a/packages/drift/src/cache/utils/readKey.ts b/packages/drift/src/cache/utils/readKey.ts deleted file mode 100644 index 958f65d7..00000000 --- a/packages/drift/src/cache/utils/readKey.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Abi } from "abitype"; -import type { FunctionName } from "src/adapter/contract/types/function"; -import type { DriftReadKeyParams } from "src/cache/DriftCache/types"; -import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; -import { partialReadKey } from "src/cache/utils/partialReadKey"; - -export function readKey< - TAbi extends Abi, - TFunctionName extends FunctionName, ->(params: DriftReadKeyParams): SimpleCacheKey { - return partialReadKey(params); -} diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts new file mode 100644 index 00000000..8ca3c38d --- /dev/null +++ b/packages/drift/src/client/Contract/Contract.ts @@ -0,0 +1,405 @@ +import type { Abi } from "abitype"; +import isMatch from "lodash.ismatch"; +import type { + Adapter, + ReadAdapter, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +import type { + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, +} from "src/adapter/types/Contract"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +import { createClientCache } from "src/cache/ClientCache/createClientCache"; +import type { + ClientCache, + DriftEventsKeyParams, + DriftReadKeyParams, + NameSpaceParam, +} from "src/cache/ClientCache/types"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import type { SerializableKey } from "src/utils/createSerializableKey"; +import type { AnyObject, EmptyObject, MaybePromise } from "src/utils/types"; + +export interface ContractParams< + TAbi extends Abi = Abi, + TAdapter extends Adapter = Adapter, + TCache extends ClientCache = ClientCache, +> extends NameSpaceParam { + abi: TAbi; + adapter: TAdapter; + address: Address; + cache?: TCache; +} + +export type Contract< + TAbi extends Abi = Abi, + TAdapter extends Adapter = Adapter, + TCache extends ClientCache = ClientCache, +> = TAdapter extends ReadWriteAdapter + ? ReadWriteContract + : ReadContract; + +export interface ReadContractParams< + TAbi extends Abi = Abi, + TAdapter extends ReadAdapter = ReadAdapter, + TCache extends ClientCache = ClientCache, +> extends ContractParams {} + +/** + * Interface representing a readable contract with specified ABI. Provides type + * safe methods to read and simulate write operations on the contract. + */ +export class ReadContract< + TAbi extends Abi = Abi, + TAdapter extends ReadAdapter = ReadAdapter, + TCache extends ClientCache = ClientCache, +> { + abi: TAbi; + adapter: TAdapter; + address: Address; + cache: TCache; + namespace?: PropertyKey; + + constructor({ + abi, + adapter, + address, + cache = createClientCache() as TCache, + namespace, + }: ReadContractParams) { + this.abi = abi; + this.adapter = adapter; + this.address = address; + this.cache = cache; + this.namespace = namespace; + } + + // Events // + + /** + * Retrieves specified events from the contract. + */ + getEvents = async >( + event: TEventName, + options?: ContractGetEventsOptions, + ): Promise[]> => { + const key = this.eventsKey(event, options); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter + .getEvents({ + abi: this.abi, + address: this.address, + event, + ...options, + }) + .then((events) => { + this.cache.set(key, events); + return events; + }); + }; + + preloadEvents = >( + params: Omit< + DriftEventsKeyParams, + keyof ReadContractParams + > & { + value: readonly ContactEvent[]; + }, + ): MaybePromise => { + return this.cache.preloadEvents({ + namespace: this.namespace, + abi: this.abi, + address: this.address, + ...params, + }); + }; + + eventsKey = >( + event: TEventName, + options?: ContractGetEventsOptions, + ): SerializableKey => { + return this.cache.eventsKey({ + namespace: this.namespace, + abi: this.abi, + address: this.address, + event, + ...options, + }); + }; + + // read // + + /** + * Reads a specified function from the contract. + */ + read = >( + ...[fn, args, options]: ContractReadArgs + ): Promise> => { + // TODO: Cleanup type casting required due to an incompatibility between + // distributive types and the conditional args param. + const key = this.readKey(fn, args as any, options); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter + .read({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) + .then((events) => { + this.cache.set(key, events); + return events; + }); + }; + + preloadRead = >( + params: Omit< + DriftReadKeyParams, + keyof ReadContractParams + > & { + value: FunctionReturn; + }, + ): MaybePromise => { + this.cache.preloadRead({ + namespace: this.namespace, + // TODO: Cleanup type casting required due to an incompatibility between + // `Omit` and the conditional args param. + abi: this.abi as Abi, + address: this.address, + ...params, + }); + }; + + invalidateRead>( + ...[fn, args, options]: ContractReadArgs + ): MaybePromise { + return this.cache.invalidateRead({ + namespace: this.namespace, + // TODO: Cleanup type casting required due to an incompatibility between + // `Omit` and the conditional args param. + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); + } + + invalidateReadsMatching = >( + fn?: TFunctionName, + args?: FunctionArgs, + options?: ContractReadOptions, + ): MaybePromise => { + const matchKey = this.cache.partialReadKey({ + namespace: this.namespace, + abi: this.abi, + address: this.address, + fn, + args, + ...options, + }); + + for (const [key] of this.cache.entries) { + if (key === matchKey) { + this.cache.delete(key); + continue; + } + if ( + typeof key === "object" && + typeof matchKey === "object" && + isMatch(key, matchKey) + ) { + this.cache.delete(key); + } + } + }; + + readKey = >( + ...[fn, args, options]: ContractReadArgs + ): SerializableKey => { + return this.cache.readKey({ + namespace: this.namespace, + // TODO: Cleanup type casting required due to an incompatibility between + // `Omit` and the conditional args param. + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); + }; + + partialReadKey = >( + fn?: TFunctionName, + args?: FunctionArgs, + options?: ContractReadOptions, + ): SerializableKey => { + return this.cache.partialReadKey({ + namespace: this.namespace, + abi: this.abi, + address: this.address, + fn, + args, + ...options, + }); + }; + + // ...rest // + + /** + * Simulates a write operation on a specified function of the contract. + */ + simulateWrite = < + TFunctionName extends FunctionName, + >( + ...[fn, args, options]: ContractWriteArgs + ): Promise> => { + return this.adapter.simulateWrite({ + // TODO: Cleanup type casting required due to an incompatibility between + // distributive types and the conditional args param. + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); + }; + + /** + * Encodes a function call into calldata. + */ + encodeFunctionData = >( + ...[fn, args]: ContractEncodeFunctionDataArgs + ): Bytes => { + return this.adapter.encodeFunctionData({ + // TODO: Cleanup type casting required due to an incompatibility between + // distributive types and the conditional args param. + abi: this.abi as Abi, + fn, + args, + }); + }; + + /** + * Decodes a string of function calldata into it's arguments and function + * name. + */ + decodeFunctionData = < + TFunctionName extends FunctionName = FunctionName, + >( + data: Bytes, + ): DecodedFunctionData => { + return this.adapter.decodeFunctionData({ + abi: this.abi, + data, + }); + }; +} + +export type ReadWriteContractParams< + TAbi extends Abi = Abi, + TAdapter extends ReadWriteAdapter = ReadWriteAdapter, + TCache extends ClientCache = ClientCache, +> = ReadContractParams; + +/** + * Interface representing a writable contract with specified ABI. Extends + * IReadContract to also include write operations. + */ +export class ReadWriteContract< + TAbi extends Abi = Abi, + TAdapter extends ReadWriteAdapter = ReadWriteAdapter, + TCache extends ClientCache = ClientCache, +> extends ReadContract { + /** + * Get the address of the signer for this contract. + */ + getSignerAddress = (): Promise
=> { + return this.adapter.getSignerAddress(); + }; + + /** + * Writes to a specified function on the contract. + * @returns The transaction hash of the submitted transaction. + */ + write = >( + ...[fn, args, options]: ContractWriteArgs + ): Promise => { + return this.adapter.write({ + // TODO: Cleanup type casting required due to an incompatibility between + // distributive types and the conditional args param. + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); + }; +} + +export type ContractReadArgs< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = Abi extends TAbi + ? [ + functionName: TFunctionName, + args?: AnyObject, + options?: ContractReadOptions, + ] + : FunctionArgs extends EmptyObject + ? [ + functionName: TFunctionName, + args?: EmptyObject, + options?: ContractReadOptions, + ] + : [ + functionName: TFunctionName, + args: FunctionArgs, + options?: ContractReadOptions, + ]; + +export type ContractWriteArgs< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, +> = Abi extends TAbi + ? [ + functionName: TFunctionName, + args?: AnyObject, + options?: ContractWriteOptions, + ] + : FunctionArgs extends EmptyObject + ? [ + functionName: TFunctionName, + args?: EmptyObject, + options?: ContractWriteOptions, + ] + : [ + functionName: TFunctionName, + args: FunctionArgs, + options?: ContractWriteOptions, + ]; + +export type ContractEncodeFunctionDataArgs< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = Abi extends TAbi + ? [functionName: TFunctionName, args?: AnyObject] + : FunctionArgs extends EmptyObject + ? [functionName: TFunctionName, args?: EmptyObject] + : [functionName: TFunctionName, args: FunctionArgs]; diff --git a/packages/drift/src/client/Contract/MockContract.test.ts b/packages/drift/src/client/Contract/MockContract.test.ts new file mode 100644 index 00000000..7423361e --- /dev/null +++ b/packages/drift/src/client/Contract/MockContract.test.ts @@ -0,0 +1,271 @@ +import type { ContactEvent } from "src/adapter/types/Event"; +import type { DecodedFunctionData } from "src/adapter/types/Function"; +import { MockContract } from "src/client/Contract/MockContract"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { describe, expect, it } from "vitest"; + +const abi = IERC20.abi; +type Erc20Abi = typeof abi; + +describe("MockContract", () => { + describe("getEvents", async () => { + it("Rejects with an error by default", async () => { + const contract = new MockContract({ abi }); + let error: unknown; + try { + await contract.getEvents("Transfer"); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + const events: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x", + to: "0x", + value: 123n, + }, + }, + ]; + contract.onGetEvents("Transfer").resolves(events); + expect(await contract.getEvents("Transfer")).toBe(events); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + const events1: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x2", + to: "0x2", + value: 123n, + }, + }, + ]; + contract + .onGetEvents("Transfer", { filter: { from: "0x1" } }) + .resolves(events1); + contract + .onGetEvents("Transfer", { filter: { from: "0x2" } }) + .resolves(events2); + expect( + await contract.getEvents("Transfer", { filter: { from: "0x1" } }), + ).toBe(events1); + expect( + await contract.getEvents("Transfer", { filter: { from: "0x2" } }), + ).toBe(events2); + }); + }); + + describe("read", () => { + it("Rejects with an error by default", async () => { + const contract = new MockContract({ abi }); + let error: unknown; + try { + await contract.read("symbol"); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + contract.onRead("symbol").resolves("ABC"); + expect(await contract.read("symbol")).toBe("ABC"); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + contract + .onRead("allowance", { owner: "0x1", spender: "0x1" }) + .resolves(1n); + contract + .onRead("allowance", { owner: "0x1", spender: "0x2" }) + .resolves(2n); + expect( + await contract.read("allowance", { owner: "0x1", spender: "0x1" }), + ).toBe(1n); + expect( + await contract.read("allowance", { owner: "0x1", spender: "0x2" }), + ).toBe(2n); + }); + + it.todo("Can be stubbed with partial args", async () => { + const contract = new MockContract({ abi }); + contract.onRead("balanceOf").resolves(123n); + expect(await contract.read("balanceOf", { owner: "0x" })).toBe(123n); + }); + }); + + describe("simulateWrite", () => { + it("Rejects with an error by default", async () => { + const contract = new MockContract({ abi }); + let error: unknown; + try { + await contract.simulateWrite("transfer", { to: "0x", value: 123n }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + contract + .onSimulateWrite("transfer", { to: "0x", value: 123n }) + .resolves(true); + expect( + await contract.simulateWrite("transfer", { to: "0x", value: 123n }), + ).toBe(true); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + contract + .onSimulateWrite("transfer", { to: "0x1", value: 123n }) + .resolves(true); + contract + .onSimulateWrite("transfer", { to: "0x2", value: 123n }) + .resolves(false); + expect( + await contract.simulateWrite("transfer", { to: "0x1", value: 123n }), + ).toBe(true); + expect( + await contract.simulateWrite("transfer", { to: "0x2", value: 123n }), + ).toBe(false); + }); + }); + + describe("encodeFunctionData", () => { + it("Returns a default value", async () => { + const contract = new MockContract({ abi }); + expect( + contract.encodeFunctionData("balanceOf", { owner: "0x" }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + contract + .onEncodeFunctionData("balanceOf", { owner: "0x" }) + .returns("0x123"); + expect(contract.encodeFunctionData("balanceOf", { owner: "0x" })).toBe( + "0x123", + ); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + contract + .onEncodeFunctionData("balanceOf", { owner: "0x1" }) + .returns("0x1"); + contract + .onEncodeFunctionData("balanceOf", { owner: "0x2" }) + .returns("0x2"); + expect(contract.encodeFunctionData("balanceOf", { owner: "0x1" })).toBe( + "0x1", + ); + expect(contract.encodeFunctionData("balanceOf", { owner: "0x2" })).toBe( + "0x2", + ); + }); + }); + + describe("decodeFunctionData", () => { + it("Throws an error by default", async () => { + const contract = new MockContract({ abi }); + let error: unknown; + try { + contract.decodeFunctionData("0x"); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + const decoded: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x" }, + }; + contract.onDecodeFunctionData("0x").returns(decoded); + expect(contract.decodeFunctionData("0x")).toBe(decoded); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + const decoded1: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; + const decoded2: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x2" }, + }; + contract.onDecodeFunctionData("0x1").returns(decoded1); + contract.onDecodeFunctionData("0x2").returns(decoded2); + expect(contract.decodeFunctionData("0x1")).toBe(decoded1); + expect(contract.decodeFunctionData("0x2")).toBe(decoded2); + }); + }); + + describe("getSignerAddress", () => { + it("Returns a default value", async () => { + const contract = new MockContract({ abi }); + expect(await contract.getSignerAddress()).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + contract.onGetSignerAddress().resolves("0x123"); + expect(await contract.getSignerAddress()).toBe("0x123"); + }); + }); + + describe("write", () => { + it("Returns a default value", async () => { + const contract = new MockContract({ abi }); + expect( + await contract.write("transfer", { to: "0x", value: 123n }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const contract = new MockContract({ abi }); + contract.onWrite("transfer", { to: "0x", value: 123n }).resolves("0x123"); + expect(await contract.write("transfer", { to: "0x", value: 123n })).toBe( + "0x123", + ); + }); + + it("Can be stubbed with specific args", async () => { + const contract = new MockContract({ abi }); + contract.onWrite("transfer", { to: "0x1", value: 123n }).resolves("0x1"); + contract.onWrite("transfer", { to: "0x2", value: 123n }).resolves("0x2"); + expect(await contract.write("transfer", { to: "0x1", value: 123n })).toBe( + "0x1", + ); + expect(await contract.write("transfer", { to: "0x2", value: 123n })).toBe( + "0x2", + ); + }); + }); +}); diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts new file mode 100644 index 00000000..5eeff03b --- /dev/null +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -0,0 +1,251 @@ +import type { Abi } from "abitype"; +import { MockAdapter } from "src/adapter/MockAdapter"; +import type { + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, +} from "src/adapter/types/Contract"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +import { + type ContractEncodeFunctionDataArgs, + type ContractParams, + type ContractReadArgs, + type ContractWriteArgs, + ReadWriteContract, +} from "src/client/Contract/Contract"; +import { ZERO_ADDRESS } from "src/constants"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import { MockStore } from "src/utils/MockStore"; +import type { OptionalKeys } from "src/utils/types"; + +export type MockContractParams = Omit< + OptionalKeys, "address">, + "adapter" | "cache" +>; + +export class MockContract extends ReadWriteContract< + TAbi, + MockAdapter +> { + // mocks // + + constructor({ + abi, + address = ZERO_ADDRESS, + namespace, + }: MockContractParams) { + super({ + abi, + adapter: new MockAdapter(), + address, + namespace, + }); + } + + protected mocks = new MockStore>(); + + reset = () => this.mocks.reset(); + + // getEvents // + + onGetEvents>( + event: TEventName, + options?: ContractGetEventsOptions, + ) { + return this.mocks + .get< + [ + event: TEventName, + options?: ContractGetEventsOptions, + ], + Promise[]> + >({ + method: "getEvents", + key: event, + }) + .withArgs(event, options); + } + + getEvents = async >( + event: TEventName, + options?: ContractGetEventsOptions, + ) => { + return this.mocks.get< + [event: TEventName, options?: ContractGetEventsOptions], + Promise[]> + >({ + method: "getEvents", + key: event, + })(event, options); + }; + + // read // + + onRead>( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractReadOptions, + ) { + return this.mocks + .get< + ContractReadArgs, + Promise> + >({ + method: "read", + key: fn, + }) + .withArgs(fn, args as any, options); + } + + read = async >( + ...[fn, args, options]: ContractReadArgs + ) => { + return this.mocks.get< + ContractReadArgs, + Promise> + >({ + method: "read", + key: fn, + })(fn, args as any, options); + }; + + // simulateWrite // + + onSimulateWrite< + TFunctionName extends FunctionName, + >( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractWriteOptions, + ) { + return this.mocks + .get< + ContractWriteArgs, + Promise> + >({ + method: "simulateWrite", + key: fn, + }) + .withArgs(fn, args as any, options); + } + + simulateWrite = async < + TFunctionName extends FunctionName, + >( + ...[fn, args, options]: ContractWriteArgs + ) => { + return this.mocks.get< + ContractWriteArgs, + Promise> + >({ + method: "simulateWrite", + key: fn, + })(fn, args as any, options); + }; + + // encodeFunction // + + onEncodeFunctionData>( + fn?: TFunctionName, + args?: FunctionArgs, + ) { + let mock = this.mocks.get< + ContractEncodeFunctionDataArgs, + Bytes + >({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + }); + if (fn && args) { + // TODO: Cleanup type casting + mock = mock.withArgs(fn as any, args as any); + } + return mock; + } + + encodeFunctionData = >( + ...[fn, args]: ContractEncodeFunctionDataArgs + ) => { + return this.mocks.get< + ContractEncodeFunctionDataArgs, + Bytes + >({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + })(fn, args as any); + }; + + // decodeFunction // + + onDecodeFunctionData>(data?: Bytes) { + return this.mocks + .get<[data: Bytes], DecodedFunctionData>({ + method: "decodeFunctionData", + }) + .withArgs(data); + } + + decodeFunctionData = >( + data: Bytes, + ) => { + return this.mocks.get< + [data: Bytes], + DecodedFunctionData + >({ + method: "decodeFunctionData", + })(data); + }; + + // getSignerAddress // + + onGetSignerAddress() { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + }); + } + + getSignerAddress = async () => { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + })(); + }; + + // write // + + onWrite>( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractWriteOptions, + ) { + return this.mocks + .get, Promise>({ + method: "write", + key: fn, + create: (mock) => mock.resolves("0x0"), + }) + .withArgs(fn, args as any, options); + } + + write = async < + TFunctionName extends FunctionName, + >( + ...[fn, args, options]: ContractWriteArgs + ) => { + return this.mocks.get< + ContractWriteArgs, + Promise + >({ + method: "write", + key: fn, + create: (mock) => mock.resolves("0x0"), + })(fn, args as any, options); + }; +} diff --git a/packages/drift/src/client/Contract/MockErc20.ts b/packages/drift/src/client/Contract/MockErc20.ts new file mode 100644 index 00000000..2933e983 --- /dev/null +++ b/packages/drift/src/client/Contract/MockErc20.ts @@ -0,0 +1,18 @@ +import { + MockContract, + type MockContractParams, +} from "src/client/Contract/MockContract"; +import { IERC20 } from "src/utils/testing/IERC20"; + +type Erc20Abi = typeof IERC20.abi; + +export type MockErc20Params = Omit, "abi">; + +export class MockErc20 extends MockContract { + constructor(params?: MockErc20Params) { + super({ + ...params, + abi: IERC20.abi, + }); + } +} diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts new file mode 100644 index 00000000..48c78a0c --- /dev/null +++ b/packages/drift/src/client/Drift/Drift.ts @@ -0,0 +1,245 @@ +import type { Abi } from "abitype"; +import type { + Adapter, + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +import type { TransactionReceipt } from "src/adapter/types/Transaction"; +import type { SimpleCache } from "src/cache/SimpleCache/types"; +import { createClientCache } from "src/client/ClientCache/createClientCache"; +import type { ClientCache, NameSpaceParam } from "src/client/ClientCache/types"; +import { + type Contract, + type ContractParams, + ReadContract, + ReadWriteContract, +} from "src/client/Contract/Contract"; +import type { Bytes, TransactionHash } from "src/types"; + +export type DriftContract< + TAbi extends Abi, + TAdapter extends Adapter = Adapter, +> = TAdapter extends ReadWriteAdapter + ? ReadWriteContract + : ReadContract; + +export interface DriftOptions + extends NameSpaceParam { + cache?: TCache; +} + +// This is the one implementation that combines the Read/ReadWrite concepts in +// favor of a unified entrypoint to the library's top-level API. +export class Drift< + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> { + readonly adapter: TAdapter; + cache: ClientCache; + namespace?: PropertyKey; + + // Write-only property definitions // + + getSignerAddress: TAdapter extends ReadWriteAdapter + ? () => Promise + : undefined; + + write: TAdapter extends ReadWriteAdapter + ? < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: WriteParams, + ) => Promise + : undefined; + + // Implementation // + + constructor( + adapter: TAdapter, + { cache, namespace }: DriftOptions = {}, + ) { + this.adapter = adapter; + this.cache = createClientCache(cache); + this.namespace = namespace; + + // Write-only property assignment // + + const isReadWrite = this.isReadWrite(); + + this.getSignerAddress = isReadWrite + ? () => this.adapter.getSignerAddress() + : (undefined as any); + + this.write = isReadWrite + ? async (params) => { + const writePromise = this.adapter.write(params); + + if (params.onMined) { + writePromise.then((txHash) => { + this.adapter.waitForTransaction(txHash).then(params.onMined); + return txHash; + }); + } + + return writePromise; + } + : (undefined as any); + } + + // The following functions are defined as arrow function properties rather + // than typical class methods to ensure they maintain the correct `this` + // context when passed as callbacks. + + isReadWrite = (): this is Drift => + isReadWriteAdapter(this.adapter); + + contract = ({ + abi, + address, + cache = this.cache, + namespace = this.namespace, + }: ContractParams): Contract => { + return ( + this.isReadWrite() + ? new ReadWriteContract({ + abi, + adapter: this.adapter, + address, + cache, + namespace, + }) + : new ReadContract({ + abi, + adapter: this.adapter, + address, + cache, + namespace, + }) + ) as Contract; + }; + + /** + * Reads a specified function from a contract. + */ + read = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: ReadParams, + ): Promise> => { + const key = this.cache.readKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.read(params).then((result) => { + this.cache.set(key, result); + return result; + }); + }; + + /** + * Simulates a write operation on a specified function of a contract. + */ + simulateWrite = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: SimulateWriteParams, + ): Promise> => { + return this.adapter.simulateWrite(params); + }; + + /** + * Retrieves specified events from a contract. + */ + getEvents = async >( + params: GetEventsParams, + ): Promise[]> => { + const key = this.cache.eventsKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getEvents(params).then((result) => { + this.cache.set(key, result); + return result; + }); + }; + + /** + * Encodes a function call into calldata. + */ + encodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: EncodeFunctionDataParams, + ): Bytes => { + return this.adapter.encodeFunctionData(params); + }; + + /** + * Decodes a string of function calldata into it's arguments and function + * name. + */ + decodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName = FunctionName, + >( + params: DecodeFunctionDataParams, + ): DecodedFunctionData => { + return this.adapter.decodeFunctionData(params); + }; +} + +export type ReadParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = NameSpaceParam & AdapterReadParams; + +export type SimulateWriteParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, +> = AdapterWriteParams; + +export type WriteParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, +> = AdapterWriteParams & { + onMined?: (receipt?: TransactionReceipt) => void; +}; + +export type GetEventsParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = NameSpaceParam & AdapterGetEventsParams; + +export type EncodeFunctionDataParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = AdapterEncodeFunctionDataParams; + +export type DecodeFunctionDataParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = AdapterDecodeFunctionDataParams; + +function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { + return "readWriteContract" in adapter; +} diff --git a/packages/drift/src/drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts similarity index 93% rename from packages/drift/src/drift/MockDrift.test.ts rename to packages/drift/src/client/Drift/MockDrift.test.ts index 39607970..daa683a2 100644 --- a/packages/drift/src/drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -1,4 +1,4 @@ -import { MockDrift } from "src/drift/MockDrift"; +import { MockDrift } from "src/client/Drift/MockDrift"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift/src/drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts similarity index 70% rename from packages/drift/src/drift/MockDrift.ts rename to packages/drift/src/client/Drift/MockDrift.ts index 16e2973e..df7c5b49 100644 --- a/packages/drift/src/drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -1,10 +1,9 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { ReadWriteContractStub } from "src/adapter/contract/mocks/ReadWriteContractStub"; -import type { ReadWriteContract } from "src/contract/types"; -import { Drift, type DriftOptions } from "src/drift/Drift"; -import type { SimpleCache } from "src/exports"; -import type { ContractParams } from "src/types"; +import type { SimpleCache } from "src/cache/simple-cache/types"; +import type { ReadWriteContract } from "src/client/Contract/Contract"; +import { Drift, type DriftOptions } from "src/client/Drift/Drift"; export class MockDrift extends Drift< MockAdapter, diff --git a/packages/drift/src/constants.ts b/packages/drift/src/constants.ts new file mode 100644 index 00000000..6cfbec2f --- /dev/null +++ b/packages/drift/src/constants.ts @@ -0,0 +1 @@ +export const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; diff --git a/packages/drift/src/contract/createReadContract.ts b/packages/drift/src/contract/createReadContract.ts deleted file mode 100644 index 15f0ff86..00000000 --- a/packages/drift/src/contract/createReadContract.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Abi } from "abitype"; -import type { AdapterReadContract } from "src/adapter/contract/types/contract"; -import type { DriftCache } from "src/cache/DriftCache/types"; -import type { ReadContract } from "src/contract/types"; -import { extendInstance } from "src/utils/extendInstance"; - -interface CreateReadContractParams< -TAbi extends Abi, -TContract extends AdapterReadContract, -TCache extends DriftCache, -> { - contract: TContract; - cache: TCache; - namespace: string; -} - -/** - * Extends an {@linkcode AdapterReadContract} with additional API methods for - * use with Drift clients. - */ -export function createReadContract< - TAbi extends Abi, - TContract extends AdapterReadContract, - TCache extends DriftCache, ->(contract: TContract, cache: TCache): ReadContract { - const readContract: ReadContract = extendInstance< - TContract, - Omit - >(contract, { - cache, - - partialReadKey: (fn, args, options) => - cache.partialReadKey({ abi: contract.abi, fn, args, address: contract.address, namespace, }), - - readKey: (params) => driftCache.partialReadKey(params), - - eventsKey: ({ abi, namespace, ...params }) => - createSimpleCacheKey([namespace, "events", params]), - - preloadRead: ({ value, ...params }) => - cache.set(driftCache.readKey(params as DriftReadKeyParams), value), - - preloadEvents: ({ value, ...params }) => - cache.set(driftCache.eventsKey(params), value), - - invalidateRead: (params) => cache.delete(driftCache.readKey(params)), - - invalidateReadsMatching(params) { - const sourceKey = driftCache.partialReadKey(params); - - for (const [key] of cache.entries) { - if ( - typeof key === "object" && - isMatch(key, sourceKey as SimpleCacheKey[]) - ) { - cache.delete(key); - } - } - }, - }); - - return readContract; -} diff --git a/packages/drift/src/contract/types.ts b/packages/drift/src/contract/types.ts deleted file mode 100644 index d4500b61..00000000 --- a/packages/drift/src/contract/types.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Abi } from "abitype"; -import type { - AdapterReadContract, - AdapterReadWriteContract, - ContractGetEventsArgs, - ContractReadArgs, -} from "src/adapter/contract/types/contract"; -import type { EventName } from "src/adapter/contract/types/event"; -import type { Event } from "src/adapter/contract/types/event"; -import type { - FunctionName, - FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { - DriftCache, - DriftReadKeyParams, -} from "src/cache/DriftCache/types"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import type { Address } from "src/types"; -import type { MaybePromise } from "src/utils/types"; - -export interface ContractParams { - abi: TAbi; - address: Address; - cache?: SimpleCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -export type ReadContract< - TAbi extends Abi = Abi, - TAdapterContract extends - AdapterReadContract = AdapterReadContract, - TCache extends DriftCache = DriftCache, -> = TAdapterContract & { - cache: TCache; - - // Key generation // - - partialReadKey>( - ...args: Partial> - ): string; - - readKey>( - ...args: ContractReadArgs - ): string; - - eventsKey>( - ...args: ContractGetEventsArgs - ): string; - - // Cache management // - - preloadRead>( - params: ContractReadKeyParams & { - value: FunctionReturn; - }, - ): MaybePromise; - - preloadEvents>( - ...args: ContractGetEventsArgs & { - value: readonly Event[]; - } - ): MaybePromise; - - invalidateRead>( - ...args: ContractReadArgs - ): MaybePromise; - - invalidateReadsMatching>( - ...args: Partial> - ): MaybePromise; - - invalidateAllReads(): void; -}; - -export interface ReadWriteContract - extends ReadContract, - AdapterReadWriteContract {} - -export type ContractReadKeyParams< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName< - TAbi, - "pure" | "view" - >, -> = Omit, keyof ContractParams>; diff --git a/packages/drift/src/drift/Drift.ts b/packages/drift/src/drift/Drift.ts deleted file mode 100644 index b6074d62..00000000 --- a/packages/drift/src/drift/Drift.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { Abi } from "abitype"; -import type { Event, EventName } from "src/adapter/contract/types/event"; -import type { - DecodedFunctionData, - FunctionName, - FunctionReturn, -} from "src/adapter/contract/types/function"; -import type { Adapter, ReadWriteAdapter } from "src/adapter/types"; -import { createDriftCache } from "src/cache/DriftCache/createDriftCache"; -import type { DriftCache } from "src/cache/DriftCache/types"; -import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { createCachedReadContract } from "src/cache/utils/createCachedReadContract"; -import { createCachedReadWriteContract } from "src/cache/utils/createCachedReadWriteContract"; -import type { - ReadContract, - ReadWriteContract, -} from "src/contract/types"; -import type { - ContractParams, - DecodeFunctionDataParams, - EncodeFunctionDataParams, - GetEventsParams, - ReadParams, - WriteParams, -} from "src/types"; - -export type DriftContract< - TAbi extends Abi, - TAdapter extends Adapter = Adapter, -> = TAdapter extends ReadWriteAdapter - ? ReadWriteContract - : ReadContract; - -export interface DriftOptions { - cache?: TCache; - /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. - */ - namespace?: string; -} - -// This is the one place where the Read/ReadWrite concepts are combined into a -// single class in favor of a unified entrypoint to the Drift API. -export class Drift< - TAdapter extends Adapter = Adapter, - TCache extends SimpleCache = SimpleCache, -> { - readonly adapter: TAdapter; - cache: DriftCache; - namespace?: string; - - // Write-only property definitions // - - getSignerAddress: TAdapter extends ReadWriteAdapter - ? () => Promise - : undefined; - - write: TAdapter extends ReadWriteAdapter - ? < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: WriteParams, - ) => Promise - : undefined; - - // Implementation // - - constructor( - adapter: TAdapter, - { cache, namespace }: DriftOptions = {}, - ) { - this.adapter = adapter; - this.cache = createDriftCache(cache); - this.namespace = namespace; - - // Write-only property assignment // - - const isReadWrite = this.isReadWrite(); - - this.getSignerAddress = isReadWrite - ? () => this.adapter.getSignerAddress() - : (undefined as any); - - this.write = isReadWrite - ? async ({ abi, address, fn, args, onMined, ...writeOptions }) => { - if (isReadWriteAdapter(this.adapter)) { - const txHash = await createCachedReadWriteContract({ - contract: this.adapter.readWriteContract(abi, address), - cache: this.cache, - }).write(fn, args, writeOptions); - - if (onMined) { - this.adapter.network.waitForTransaction(txHash).then(onMined); - } - - return txHash; - } - } - : (undefined as any); - } - - isReadWrite = (): this is Drift => - isReadWriteAdapter(this.adapter); - - contract = ({ - abi, - address, - cache = this.cache, - namespace = this.namespace, - }: ContractParams): DriftContract => { - con - } - - /** - * Reads a specified function from a contract. - */ - read = async < - TAbi extends Abi, - TFunctionName extends FunctionName, - >({ - abi, - address, - fn, - args, - ...readOptions - }: ReadParams): Promise< - FunctionReturn - > => { - return createCachedReadContract({ - contract: this.adapter.readContract(abi, address), - cache: this.cache, - }).read(fn, args, readOptions); - }; - - /** - * Simulates a write operation on a specified function of a contract. - */ - simulateWrite< - TAbi extends Abi, - TFunctionName extends FunctionName, - >({ - abi, - address, - fn, - args, - ...writeOptions - }: WriteParams): Promise< - FunctionReturn - > { - return createCachedReadContract({ - contract: this.adapter.readContract(abi, address), - cache: this.cache, - }).simulateWrite(fn, args, writeOptions); - } - - /** - * Retrieves specified events from a contract. - */ - getEvents>({ - abi, - address, - event, - ...params - }: GetEventsParams): Promise[]> { - return createCachedReadContract({ - contract: this.adapter.readContract(abi, address), - cache: this.cache, - }).getEvents(event, params); - } - - /** - * Encodes a function call into calldata. - */ - encodeFunctionData< - TAbi extends Abi, - TFunctionName extends FunctionName, - >({ - abi, - fn, - args, - }: EncodeFunctionDataParams): `0x${string}` { - return createCachedReadContract({ - contract: this.adapter.readContract(abi, "0x0"), - cache: this.cache, - }).encodeFunctionData(fn, args); - } - - /** - * Decodes a string of function calldata into it's arguments and function - * name. - */ - decodeFunctionData< - TAbi extends Abi, - TFunctionName extends FunctionName = FunctionName, - >({ - abi, - data, - }: DecodeFunctionDataParams): DecodedFunctionData< - TAbi, - TFunctionName - > { - return createCachedReadContract({ - contract: this.adapter.readContract(abi, "0x0"), - cache: this.cache, - }).decodeFunctionData(data as `0x${string}`); - } -} - -function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { - return "readWriteContract" in adapter; -} diff --git a/packages/drift/src/exports/cache.ts b/packages/drift/src/exports/cache.ts deleted file mode 100644 index 1f2c8064..00000000 --- a/packages/drift/src/exports/cache.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; -export type { SimpleCache, SimpleCacheKey } from "src/cache/SimpleCache/types"; -export { createSimpleCacheKey } from "src/cache/SimpleCache/createSimpleCacheKey"; diff --git a/packages/drift/src/exports/contract.ts b/packages/drift/src/exports/contract.ts deleted file mode 100644 index 4239fba2..00000000 --- a/packages/drift/src/exports/contract.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Factories -export { - createCachedReadContract, - type CreateCachedReadContractOptions, -} from "src/cache/utils/createCachedReadContract"; -export { - createCachedReadWriteContract, - type CreateCachedReadWriteContractOptions, -} from "src/cache/utils/createCachedReadWriteContract"; - -// Types -export type { - AbiArrayType, - AbiEntry, - AbiEntryName, - AbiFriendlyType, - AbiObjectType, - AbiParameters, -} from "src/contract/types/AbiEntry"; -export type { - CachedReadContract, - CachedReadWriteContract, -} from "src/cache/types/CachedContract"; -export type { - ContractDecodeFunctionDataArgs, - ContractEncodeFunctionDataArgs, - ContractGetEventsArgs, - ContractGetEventsOptions, - ContractReadArgs, - ContractReadOptions, - ContractWriteArgs, - ContractWriteOptions, - ReadContract, - ReadWriteContract, -} from "src/contract/types/Contract"; -export type { - Event, - EventArgs, - EventFilter, - EventName, -} from "src/contract/types/Event"; -export type { - ConstructorArgs, - DecodedFunctionData, - FunctionArgs, - FunctionName, - FunctionReturn, -} from "src/contract/types/Function"; - -// Utils -export { arrayToFriendly } from "src/contract/utils/arrayToFriendly"; -export { arrayToObject } from "src/contract/utils/arrayToObject"; -export { getAbiEntry } from "src/contract/utils/getAbiEntry"; -export { objectToArray } from "src/contract/utils/objectToArray"; diff --git a/packages/drift/src/exports/errors.ts b/packages/drift/src/exports/errors.ts deleted file mode 100644 index 4e681a8e..00000000 --- a/packages/drift/src/exports/errors.ts +++ /dev/null @@ -1 +0,0 @@ -export { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; diff --git a/packages/drift/src/exports/index.ts b/packages/drift/src/exports/index.ts deleted file mode 100644 index ef10a96d..00000000 --- a/packages/drift/src/exports/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "src/exports/cache"; -export * from "src/exports/contract"; -export * from "src/exports/errors"; -export * from "src/exports/network"; diff --git a/packages/drift/src/exports/network.ts b/packages/drift/src/exports/network.ts deleted file mode 100644 index 2c822415..00000000 --- a/packages/drift/src/exports/network.ts +++ /dev/null @@ -1,15 +0,0 @@ -export type { Block, BlockTag } from "src/network/types/Block"; -export type { - Network, - NetworkGetBalanceArgs, - NetworkGetBlockArgs, - NetworkGetBlockOptions, - NetworkGetTransactionArgs, - NetworkWaitForTransactionArgs, -} from "src/network/types/Network"; -export type { - MinedTransaction, - Transaction, - TransactionInfo, - TransactionReceipt, -} from "src/network/types/Transaction"; diff --git a/packages/drift/src/exports/stubs.ts b/packages/drift/src/exports/stubs.ts deleted file mode 100644 index f31d75d4..00000000 --- a/packages/drift/src/exports/stubs.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Contract -export { ReadContractStub } from "src/contract/stubs/ReadContractStub"; -export { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; - -// Network -export { NetworkStub } from "src/network/stubs/NetworkStub"; diff --git a/packages/drift/src/utils/MockStore.ts b/packages/drift/src/utils/MockStore.ts new file mode 100644 index 00000000..10902093 --- /dev/null +++ b/packages/drift/src/utils/MockStore.ts @@ -0,0 +1,55 @@ +import stringify from "fast-json-stable-stringify"; +import { type SinonStub, stub as sinonStub } from "sinon"; +import type { SerializableKey } from "src/utils/createSerializableKey"; + +export class MockStore { + protected mocks = new Map(); + + get({ + method, + key, + create, + }: { + method: NonNullable< + { + [K in keyof T]: T[K] extends Function ? K : never; + }[keyof T] + >; + key?: SerializableKey; + create?: (mock: SinonStub) => SinonStub; + }): SinonStub { + let mockKey: string = String(method); + if (key) { + mockKey += `:${stringify(key)}`; + } + let mock = this.mocks.get(mockKey); + if (mock) { + return mock as any; + } + mock = sinonStub().throws( + new NotImplementedError({ + method: String(method), + mockKey, + }), + ); + if (create) { + mock = create(mock as any); + } + this.mocks.set(mockKey, mock); + return mock as any; + } + + reset() { + this.mocks.clear(); + } +} + +export class NotImplementedError extends Error { + constructor({ method, mockKey }: { method: string; mockKey: string }) { + super( + `No mock found with key "${mockKey}". Called ${method} on a Mock without a return value. The value must be stubbed first: + mock.on${method.replace(/^./, (c) => c.toUpperCase())}(...args).resolves(value)`, + ); + this.name = "NotImplementedError"; + } +} diff --git a/packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts b/packages/drift/src/utils/createSerializableKey.ts similarity index 61% rename from packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts rename to packages/drift/src/utils/createSerializableKey.ts index 5fa4a8fd..7b8208c4 100644 --- a/packages/drift/src/cache/SimpleCache/createSimpleCacheKey.ts +++ b/packages/drift/src/utils/createSerializableKey.ts @@ -1,26 +1,24 @@ -import type { SimpleCacheKey } from "src/cache/SimpleCache/types"; - -type DefinedValue = NonNullable< - Record | string | number | boolean | symbol ->; +import type { AnyObject } from "src/utils/types"; /** - * Converts a given raw key into a `SimpleCacheKey``. + * Converts a given raw key into a `SerializableKey``. * * The method ensures that any given raw key, regardless of its structure, is * converted into a format suitable for consistent cache key referencing. * - * - For scalar (string, number, boolean), it returns them directly. + * - For strings, numbers, and booleans it returns them directly. * - For arrays, it recursively processes each element. * - For objects, it sorts the keys and then recursively processes each value, * ensuring consistent key generation. * - For other types, it attempts to convert the raw key to a string. * * @param rawKey - The raw input to be converted into a cache key. - * @returns A standardized cache key suitable for consistent referencing within - * the cache. + * @returns A standardized key suitable for consistent referencing within a + * cache. */ -export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey { +export function createSerializableKey( + rawKey: string | number | boolean | symbol | AnyObject, +): SerializableKey { switch (typeof rawKey) { case "string": case "number": @@ -34,11 +32,11 @@ export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey { // precedent set by JSON.stringify value === undefined || value === null ? null - : createSimpleCacheKey(value), + : createSerializableKey(value), ); } - const processedObject: Record = {}; + const processedObject: Record = {}; // sort keys to ensure consistent key generation for (const key of Object.keys(rawKey).sort()) { @@ -46,7 +44,7 @@ export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey { // ignore properties with undefined or null values if (value !== undefined && value !== null) { - processedObject[key] = createSimpleCacheKey(value); + processedObject[key] = createSerializableKey(value); } } @@ -61,3 +59,16 @@ export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey { } } } + +/** + * Represents possible serializable key types. + */ +export type SerializableKey = + | KeyPrimitive + | (SerializableKey | null | undefined)[] + | { + [key: string | number]: SerializableKey; + }; + +/** Primitive types that can be used as part of a cache key. */ +type KeyPrimitive = string | number | boolean; diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index 5b254c6d..281b3dc5 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -1,4 +1,5 @@ export type EmptyObject = Record; +export type AnyObject = Record; export type MaybePromise = T | Promise; From 9bd98a9deaae107b074ade145eb98c740ada8a79 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 5 Oct 2024 06:13:15 -0500 Subject: [PATCH 27/49] wip --- .../drift/src/adapter/MockAdapter.test.ts | 89 ++- packages/drift/src/adapter/MockAdapter.ts | 156 ++--- packages/drift/src/adapter/types/Abi.ts | 104 +-- packages/drift/src/adapter/types/Adapter.ts | 8 +- packages/drift/src/adapter/types/Function.ts | 11 +- packages/drift/src/adapter/types/Network.ts | 45 +- .../cache/ClientCache/createClientCache.ts | 95 ++- packages/drift/src/cache/ClientCache/types.ts | 83 ++- .../drift/src/client/Contract/Contract.ts | 35 +- .../drift/src/client/Contract/MockContract.ts | 211 ++---- packages/drift/src/client/Drift/Drift.ts | 170 +++-- .../drift/src/client/Drift/MockDrift.test.ts | 613 +++++++++++++++++- packages/drift/src/client/Drift/MockDrift.ts | 400 +++++++++++- packages/drift/src/utils/MockStore.ts | 16 +- packages/drift/src/utils/types.ts | 120 +++- 15 files changed, 1685 insertions(+), 471 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index cca8d434..1adc347e 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -8,6 +8,7 @@ import type { } from "src/adapter/types/Adapter"; import type { Block } from "src/adapter/types/Block"; import type { ContactEvent } from "src/adapter/types/Event"; +import type { DecodedFunctionData } from "src/adapter/types/Function"; import type { Transaction, TransactionReceipt, @@ -16,25 +17,16 @@ import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockAdapter", () => { - describe("getBalance", () => { + describe("getChainId", () => { it("Resolves to a default value", async () => { const adapter = new MockAdapter(); - expect(await adapter.getBalance("0x0")).toBeTypeOf("bigint"); + expect(await adapter.getChainId()).toBeTypeOf("number"); }); it("Can be stubbed", async () => { const adapter = new MockAdapter(); - adapter.onGetBalance().resolves(123n); - expect(await adapter.getBalance("0x")).toBe(123n); - }); - - it("Can be stubbed with specific args", async () => { - const adapter = new MockAdapter(); - const defaultValue = await adapter.getBalance("0x"); - adapter.onGetBalance("0xAlice").resolves(defaultValue + 1n); - adapter.onGetBalance("0xBob").resolves(defaultValue + 2n); - expect(await adapter.getBalance("0xAlice")).toBe(defaultValue + 1n); - expect(await adapter.getBalance("0xBob")).toBe(defaultValue + 2n); + adapter.onGetChainId().resolves(123); + expect(await adapter.getChainId()).toBe(123); }); }); @@ -75,23 +67,36 @@ describe("MockAdapter", () => { }); }); - describe("getChainId", () => { + describe("getBalance", () => { it("Resolves to a default value", async () => { const adapter = new MockAdapter(); - expect(await adapter.getChainId()).toBeTypeOf("number"); + expect(await adapter.getBalance({ address: "0x" })).toBeTypeOf("bigint"); }); it("Can be stubbed", async () => { const adapter = new MockAdapter(); - adapter.onGetChainId().resolves(123); - expect(await adapter.getChainId()).toBe(123); + adapter.onGetBalance().resolves(123n); + expect(await adapter.getBalance({ address: "0x" })).toBe(123n); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const defaultValue = await adapter.getBalance({ address: "0x" }); + adapter.onGetBalance({ address: "0xAlice" }).resolves(defaultValue + 1n); + adapter.onGetBalance({ address: "0xBob" }).resolves(defaultValue + 2n); + expect(await adapter.getBalance({ address: "0xAlice" })).toBe( + defaultValue + 1n, + ); + expect(await adapter.getBalance({ address: "0xBob" })).toBe( + defaultValue + 2n, + ); }); }); describe("getTransaction", () => { it("Resolves to undefined by default", async () => { const adapter = new MockAdapter(); - expect(await adapter.getTransaction("0x")).toBeUndefined(); + expect(await adapter.getTransaction({ hash: "0x" })).toBeUndefined(); }); it("Can be stubbed", async () => { @@ -106,7 +111,7 @@ describe("MockAdapter", () => { value: 123n, }; adapter.onGetTransaction().resolves(transaction); - expect(await adapter.getTransaction("0x")).toBe(transaction); + expect(await adapter.getTransaction({ hash: "0x" })).toBe(transaction); }); it("Can be stubbed with specific args", async () => { @@ -124,17 +129,19 @@ describe("MockAdapter", () => { ...transaction1, input: "0x2", }; - adapter.onGetTransaction("0x1").resolves(transaction1); - adapter.onGetTransaction("0x2").resolves(transaction2); - expect(await adapter.getTransaction("0x1")).toBe(transaction1); - expect(await adapter.getTransaction("0x2")).toBe(transaction2); + adapter.onGetTransaction({ hash: "0x1" }).resolves(transaction1); + adapter.onGetTransaction({ hash: "0x2" }).resolves(transaction2); + expect(await adapter.getTransaction({ hash: "0x1" })).toBe(transaction1); + expect(await adapter.getTransaction({ hash: "0x2" })).toBe(transaction2); }); }); describe("waitForTransaction", () => { it("Resolves to undefined by default", async () => { const adapter = new MockAdapter(); - expect(adapter.waitForTransaction("0x")).resolves.toBeUndefined(); + expect( + adapter.waitForTransaction({ hash: "0x" }), + ).resolves.toBeUndefined(); }); it("Can be stubbed", async () => { @@ -153,7 +160,7 @@ describe("MockAdapter", () => { transactionIndex: 123, }; adapter.onWaitForTransaction().resolves(receipt); - expect(await adapter.waitForTransaction("0x")).toBe(receipt); + expect(await adapter.waitForTransaction({ hash: "0x" })).toBe(receipt); }); it("Can be stubbed with specific args", async () => { @@ -175,10 +182,10 @@ describe("MockAdapter", () => { ...receipt1, transactionHash: "0x2", }; - adapter.onWaitForTransaction("0x1").resolves(receipt1); - adapter.onWaitForTransaction("0x2").resolves(receipt2); - expect(await adapter.waitForTransaction("0x1")).toBe(receipt1); - expect(await adapter.waitForTransaction("0x2")).toBe(receipt2); + adapter.onWaitForTransaction({ hash: "0x1" }).resolves(receipt1); + adapter.onWaitForTransaction({ hash: "0x2" }).resolves(receipt2); + expect(await adapter.waitForTransaction({ hash: "0x1" })).toBe(receipt1); + expect(await adapter.waitForTransaction({ hash: "0x2" })).toBe(receipt2); }); }); @@ -254,20 +261,24 @@ describe("MockAdapter", () => { it("Can be stubbed", async () => { const adapter = new MockAdapter(); + const decoded: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; adapter .onDecodeFunctionData({ abi: IERC20.abi, fn: "balanceOf", data: "0x", }) - .returns(123n); + .returns(decoded); expect( adapter.decodeFunctionData({ abi: IERC20.abi, fn: "balanceOf", data: "0x", }), - ).toBe(123n); + ).toBe(decoded); }); it("Can be stubbed with specific args", async () => { @@ -280,6 +291,10 @@ describe("MockAdapter", () => { fn: "balanceOf", data: "0x1", }; + const return1: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; const params2: AdapterDecodeFunctionDataParams< typeof IERC20.abi, "balanceOf" @@ -287,10 +302,14 @@ describe("MockAdapter", () => { ...params1, data: "0x2", }; - adapter.onDecodeFunctionData(params1).returns(1n); - adapter.onDecodeFunctionData(params2).returns(2n); - expect(adapter.decodeFunctionData(params1)).toBe(1n); - expect(adapter.decodeFunctionData(params2)).toBe(2n); + const return2: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x2" }, + }; + adapter.onDecodeFunctionData(params1).returns(return1); + adapter.onDecodeFunctionData(params2).returns(return2); + expect(adapter.decodeFunctionData(params1)).toBe(return1); + expect(adapter.decodeFunctionData(params2)).toBe(return2); }); }); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 9dc627a5..f466df92 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -15,10 +15,10 @@ import type { FunctionReturn, } from "src/adapter/types/Function"; import type { - NetworkGetBalanceArgs, - NetworkGetBlockArgs, - NetworkGetTransactionArgs, - NetworkWaitForTransactionArgs, + NetworkGetBalanceParams, + NetworkGetBlockParams, + NetworkGetTransactionParams, + NetworkWaitForTransactionParams, } from "src/adapter/types/Network"; import type { Transaction, @@ -30,33 +30,35 @@ import type { OptionalKeys } from "src/utils/types"; // TODO: Allow configuration of error throwing/default return value behavior export class MockAdapter implements ReadWriteAdapter { - mocks = new MockStore(); + mocks: MockStore; + + constructor(store = new MockStore()) { + this.mocks = store; + } reset = () => this.mocks.reset(); - // getBalance // + // getChainId // - onGetBalance(...args: Partial) { - return this.mocks - .get>({ - method: "getBalance", - create: (mock) => mock.resolves(0n), - }) - .withArgs(...args); + onGetChainId() { + return this.mocks.get<[], number>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + }); } - async getBalance(...args: NetworkGetBalanceArgs) { - return this.mocks.get>({ - method: "getBalance", - create: (mock) => mock.resolves(0n), - })(...args); + async getChainId() { + return this.mocks.get<[], number>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + })(); } // getBlock // - onGetBlock(...args: Partial) { + onGetBlock(params?: Partial) { return this.mocks - .get>({ + .get<[NetworkGetBlockParams?], Promise>({ method: "getBlock", create: (mock) => mock.resolves({ @@ -64,79 +66,92 @@ export class MockAdapter implements ReadWriteAdapter { timestamp: 0n, }), }) - .withArgs(...args); + .withArgs(params); } - async getBlock(...args: NetworkGetBlockArgs) { - return this.mocks.get>({ - method: "getBlock", - create: (mock) => - mock.resolves({ - blockNumber: 0n, - timestamp: 0n, - }), - })(...args); + async getBlock(params?: NetworkGetBlockParams) { + return this.mocks.get<[NetworkGetBlockParams?], Promise>( + { + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }, + )(params); } - // getChainId // + // getBalance // - onGetChainId() { - return this.mocks.get<[], number>({ - method: "getChainId", - create: (mock) => mock.resolves(96024), + onGetBalance(params?: Partial) { + let mock = this.mocks.get<[NetworkGetBalanceParams], Promise>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), }); + if (params) { + mock = mock.withArgs(params); + } + return mock; } - async getChainId() { - return this.mocks.get<[], number>({ - method: "getChainId", - create: (mock) => mock.resolves(96024), - })(); + async getBalance(params: NetworkGetBalanceParams) { + return this.mocks.get<[NetworkGetBalanceParams], Promise>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), + })(params); } // getTransaction // - onGetTransaction(...args: Partial) { - return this.mocks - .get>({ - method: "getTransaction", - create: (mock) => mock.resolves(undefined), - }) - .withArgs(...args); + onGetTransaction(params?: Partial) { + let mock = this.mocks.get< + [NetworkGetTransactionParams], + Promise + >({ + method: "getTransaction", + create: (mock) => mock.resolves(undefined), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; } - async getTransaction(...args: NetworkGetTransactionArgs) { + async getTransaction(params: NetworkGetTransactionParams) { return this.mocks.get< - NetworkGetTransactionArgs, + [NetworkGetTransactionParams], Promise >({ method: "getTransaction", create: (mock) => mock.resolves(undefined), - })(...args); + })(params); } // waitForTransaction // - onWaitForTransaction(...args: Partial) { - return this.mocks - .get< - NetworkWaitForTransactionArgs, - Promise - >({ - method: "waitForTransaction", - create: (mock) => mock.resolves(undefined), - }) - .withArgs(...args); + onWaitForTransaction(params?: Partial) { + let mock = this.mocks.get< + [NetworkWaitForTransactionParams], + Promise + >({ + method: "waitForTransaction", + create: (mock) => mock.resolves(undefined), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; } - async waitForTransaction(...args: NetworkWaitForTransactionArgs) { + async waitForTransaction(params: NetworkWaitForTransactionParams) { return this.mocks.get< - NetworkWaitForTransactionArgs, + [NetworkWaitForTransactionParams], Promise >({ method: "waitForTransaction", create: (mock) => mock.resolves(undefined), - })(...args); + })(params); } // encodeFunction // @@ -144,12 +159,7 @@ export class MockAdapter implements ReadWriteAdapter { onEncodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: OptionalKeys< - AdapterEncodeFunctionDataParams, - "args" - >, - ) { + >(params: Partial>) { return this.mocks .get<[AdapterEncodeFunctionDataParams], Bytes>({ method: "encodeFunctionData", @@ -182,7 +192,7 @@ export class MockAdapter implements ReadWriteAdapter { return this.mocks .get< [AdapterDecodeFunctionDataParams], - FunctionReturn + DecodedFunctionData >({ method: "decodeFunctionData", key: params.fn, @@ -193,7 +203,7 @@ export class MockAdapter implements ReadWriteAdapter { decodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: AdapterDecodeFunctionDataParams) { + >(params: Partial>) { return this.mocks.get< [AdapterDecodeFunctionDataParams], DecodedFunctionData @@ -202,7 +212,7 @@ export class MockAdapter implements ReadWriteAdapter { // TODO: This should be specific to the abi to ensure the correct return // type. key: params.fn, - })(params); + })(params as AdapterDecodeFunctionDataParams); } // getEvents // @@ -343,7 +353,7 @@ export class MockAdapter implements ReadWriteAdapter { // TODO: unit test if (params.onMined) { writePromise.then((hash) => { - this.waitForTransaction(hash).then(params.onMined); + this.waitForTransaction({ hash }).then(params.onMined); return hash; }); } diff --git a/packages/drift/src/adapter/types/Abi.ts b/packages/drift/src/adapter/types/Abi.ts index 52c3897e..c7001bae 100644 --- a/packages/drift/src/adapter/types/Abi.ts +++ b/packages/drift/src/adapter/types/Abi.ts @@ -7,11 +7,20 @@ import type { AbiParametersToPrimitiveTypes, AbiStateMutability, } from "abitype"; -import type { EmptyObject, Prettify } from "src/utils/types"; +import type { + EmptyObject, + MergeKeys, + Prettify, + ReplaceKeys, +} from "src/utils/types"; // https://docs.soliditylang.org/en/latest/abi-spec.html#json -export type NamedAbiParameter = AbiParameter & { name: string }; +export type NamedAbiParameter = AbiParameter extends infer TAbiParameter + ? TAbiParameter extends { name: string } + ? TAbiParameter + : ReplaceKeys + : never; /** * Get a union of possible names for an abi item type. @@ -88,10 +97,12 @@ type WithDefaultNames = { AbiParameter ? TParameter extends NamedAbiParameter ? TParameter - : TParameter & { name: `${K}` } + : ReplaceKeys, { name: `${K}` | string }> : never; }; +type foo = Exclude; + /** * Convert an array or tuple of named abi parameters to an object type with the * parameter names as keys and their primitive types as values. If a parameter @@ -106,45 +117,56 @@ type WithDefaultNames = { type NamedParametersToObject< TParameters extends readonly NamedAbiParameter[], TParameterKind extends AbiParameterKind = AbiParameterKind, -> = Prettify< - { - // For every parameter name, excluding empty names, add a key to the object - // for the parameter name - [TName in Exclude< - TParameters[number]["name"], - "" - >]: AbiParameterToPrimitiveType< - Extract, - TParameterKind +> = NamedAbiParameter[] extends TParameters + ? Record + : Prettify< + { + // For every parameter name, excluding empty names, add a key to the + // object for the parameter name + [TName in Exclude< + TParameters[number]["name"], + "" + >]: AbiParameterToPrimitiveType< + Extract, + TParameterKind + > extends infer TPrimitive + ? TPrimitive extends unknown + ? any + : TPrimitive + : never; + // Check if the parameters are in a Tuple. Tuples have known indexes, so + // we can use the index as the key for the nameless parameters + } & (TParameters extends readonly [ + NamedAbiParameter, + ...NamedAbiParameter[], + ] + ? { + // For every key on the parameters type, if it's value is a + // parameter and the parameter's name is empty (""), then add a key + // for the index + [K in keyof TParameters as TParameters[K] extends NamedAbiParameter + ? TParameters[K]["name"] extends "" + ? // Exclude `number` to ensure only the specific index keys are + // included and not `number` itself. TODO: Test that this is + // actually doing what's described. + Exclude + : never // <- Key for named parameters (already handled above) + : never /* <- Prototype key (e.g., `length`, `toString`) */]: TParameters[K] extends NamedAbiParameter + ? AbiParameterToPrimitiveType + : never; // <- Prototype value + } + : // If the parameters are not in a Tuple, then we can't use the index + // as a key, so we have to use `number` as the key for any parameters + // that have empty names ("") in arrays + Extract extends never + ? {} // <- No parameters with empty names + : { + [index: number]: AbiParameterToPrimitiveType< + Extract, + TParameterKind + >; + }) >; - // Check if the parameters are in a Tuple. Tuples have known indexes, so we - // can use the index as the key for the nameless parameters - } & (TParameters extends readonly [NamedAbiParameter, ...NamedAbiParameter[]] - ? { - // For every key on the parameters type, if it's value is a parameter - // and the parameter's name is empty (""), then add a key for the index - [K in keyof TParameters as TParameters[K] extends NamedAbiParameter - ? TParameters[K]["name"] extends "" - ? // Exclude `number` to ensure only the specific index keys are - // included and not `number` itself - Exclude - : never // <- Key for named parameters (already handled above) - : never /* <- Prototype key (e.g., `length`, `toString`) */]: TParameters[K] extends NamedAbiParameter - ? AbiParameterToPrimitiveType - : never; // <- Prototype value - } - : // If the parameters are not in a Tuple, then we can't use the index as a - // key, so we have to use `number` as the key for any parameters that have - // empty names ("") in arrays - Extract extends never - ? {} // <- No parameters with empty names - : { - [index: number]: AbiParameterToPrimitiveType< - Extract, - TParameterKind - >; - }) ->; /** * Convert an array or tuple of abi parameters to an object type. diff --git a/packages/drift/src/adapter/types/Adapter.ts b/packages/drift/src/adapter/types/Adapter.ts index ffbf3b42..d29c4483 100644 --- a/packages/drift/src/adapter/types/Adapter.ts +++ b/packages/drift/src/adapter/types/Adapter.ts @@ -19,6 +19,10 @@ import type { AnyObject, EmptyObject } from "src/utils/types"; export type Adapter = ReadAdapter | ReadWriteAdapter; export interface ReadAdapter extends Network { + getEvents>( + params: AdapterGetEventsParams, + ): Promise[]>; + read< TAbi extends Abi, TFunctionName extends FunctionName, @@ -26,10 +30,6 @@ export interface ReadAdapter extends Network { params: AdapterReadParams, ): Promise>; - getEvents>( - params: AdapterGetEventsParams, - ): Promise[]>; - simulateWrite< TAbi extends Abi, TFunctionName extends FunctionName, diff --git a/packages/drift/src/adapter/types/Function.ts b/packages/drift/src/adapter/types/Function.ts index 23d40aa8..db2c4e3b 100644 --- a/packages/drift/src/adapter/types/Function.ts +++ b/packages/drift/src/adapter/types/Function.ts @@ -1,8 +1,5 @@ import type { Abi, AbiStateMutability } from "abitype"; -import type { - AbiFriendlyType, - AbiObjectType, -} from "src/adapter/types/Abi"; +import type { AbiFriendlyType, AbiObjectType } from "src/adapter/types/Abi"; /** * Get a union of function names from an abi @@ -45,15 +42,15 @@ export type ConstructorArgs = AbiObjectType< */ export type FunctionReturn< TAbi extends Abi, - TFunctionName extends FunctionName, + TFunctionName extends FunctionName = FunctionName, > = AbiFriendlyType; /** * Get an object representing decoded function or constructor data from an ABI. */ export type DecodedFunctionData< - TAbi extends Abi, - TFunctionName extends FunctionName, + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, > = { [K in TFunctionName]: { args: FunctionArgs; diff --git a/packages/drift/src/adapter/types/Network.ts b/packages/drift/src/adapter/types/Network.ts index 18381973..cafdf40b 100644 --- a/packages/drift/src/adapter/types/Network.ts +++ b/packages/drift/src/adapter/types/Network.ts @@ -12,33 +12,33 @@ import type { Address, HexString, TransactionHash } from "src/types"; */ export interface Network { /** - * Get the balance of native currency for an account. + * Get the chain ID of the network. */ - getBalance(...args: NetworkGetBalanceArgs): Promise; + getChainId(): Promise; /** * Get a block from a block tag, number, or hash. If no argument is provided, * the latest block is returned. */ - getBlock(...args: NetworkGetBlockArgs): Promise; + getBlock(params?: NetworkGetBlockParams): Promise; /** - * Get the chain ID of the network. + * Get the balance of native currency for an account. */ - getChainId(): Promise; + getBalance(params: NetworkGetBalanceParams): Promise; /** * Get a transaction from a transaction hash. */ getTransaction( - ...args: NetworkGetTransactionArgs + params: NetworkGetTransactionParams, ): Promise; /** * Wait for a transaction to be mined. */ waitForTransaction( - ...args: NetworkWaitForTransactionArgs + params: NetworkWaitForTransactionParams, ): Promise; } @@ -59,22 +59,21 @@ export type NetworkGetBlockOptions = blockTag?: BlockTag; }; -export type NetworkGetBalanceArgs = [ - address: Address, - block?: NetworkGetBlockOptions, -]; +export type NetworkGetBalanceParams = { + address: Address; +} & NetworkGetBlockOptions; -export type NetworkGetBlockArgs = [options?: NetworkGetBlockOptions]; +export type NetworkGetBlockParams = NetworkGetBlockOptions; -export type NetworkGetTransactionArgs = [hash: TransactionHash]; +export interface NetworkGetTransactionParams { + hash: TransactionHash; +} -export type NetworkWaitForTransactionArgs = [ - hash: TransactionHash, - options?: { - /** - * The number of milliseconds to wait for the transaction until rejecting - * the promise. - */ - timeout?: number; - }, -]; +export interface NetworkWaitForTransactionParams + extends NetworkGetTransactionParams { + /** + * The number of milliseconds to wait for the transaction until rejecting + * the promise. + */ + timeout?: number; +} diff --git a/packages/drift/src/cache/ClientCache/createClientCache.ts b/packages/drift/src/cache/ClientCache/createClientCache.ts index cf7f07bc..4a261dac 100644 --- a/packages/drift/src/cache/ClientCache/createClientCache.ts +++ b/packages/drift/src/cache/ClientCache/createClientCache.ts @@ -1,13 +1,9 @@ import isMatch from "lodash.ismatch"; -import type { - ClientCache, - DriftReadKeyParams, -} from "src/cache/ClientCache/types"; +import type { ClientCache, ReadKeyParams } from "src/cache/ClientCache/types"; import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; import type { SimpleCache } from "src/cache/SimpleCache/types"; import { - type SerializableKey, - createSerializableKey, + createSerializableKey } from "src/utils/createSerializableKey"; import { extendInstance } from "src/utils/extendInstance"; @@ -22,34 +18,95 @@ export function createClientCache( T, Omit >(cache, { - partialReadKey: ({ abi, namespace, ...params }) => - createSerializableKey([namespace, "read", params]), + // Chain ID // - readKey: (params) => clientCache.partialReadKey(params), + preloadChainId(value) { + return cache.set(clientCache.chainIdKey(), value); + }, + + chainIdKey() { + return "chainId"; + }, + + // Block // + + preloadBlock({ value, ...params }) { + return cache.set(clientCache.blockKey(params), value); + }, + + blockKey({ namespace, options } = {}) { + return createSerializableKey([namespace, "block", options]); + }, - eventsKey: ({ abi, namespace, ...params }) => - createSerializableKey([namespace, "events", params]), + // Balance // - preloadRead: ({ value, ...params }) => - cache.set(clientCache.readKey(params as DriftReadKeyParams), value), + preloadBalance({ value, ...params }) { + return cache.set(clientCache.balanceKey(params), value); + }, + + invalidateBalance(params) { + return cache.delete(clientCache.balanceKey(params)); + }, + + balanceKey({ address, cacheNamespace: namespace, options }) { + return createSerializableKey([namespace, "balance", address, options]); + }, + + // Transaction // - preloadEvents: ({ value, ...params }) => - cache.set(clientCache.eventsKey(params), value), + preloadTransaction({ value, ...params }) { + return cache.set(clientCache.transactionKey(params), value); + }, - invalidateRead: (params) => cache.delete(clientCache.readKey(params)), + transactionKey({ hash, cacheNamespace: namespace }) { + return createSerializableKey([namespace, "transaction", hash]); + }, + + // Events // + + preloadEvents({ value, ...params }) { + return cache.set(clientCache.eventsKey(params), value); + }, + + eventsKey({ abi, cacheNamespace: namespace, ...params }) { + return createSerializableKey([namespace, "events", params]); + }, + + // Read // + + preloadRead({ value, ...params }) { + return cache.set(clientCache.readKey(params as ReadKeyParams), value); + }, + + invalidateRead(params) { + return cache.delete(clientCache.readKey(params)); + }, invalidateReadsMatching(params) { - const sourceKey = clientCache.partialReadKey(params); + const matchKey = clientCache.partialReadKey(params); for (const [key] of cache.entries) { + if (key === matchKey) { + clientCache.delete(key); + continue; + } if ( typeof key === "object" && - isMatch(key, sourceKey as SerializableKey[]) + typeof matchKey === "object" && + isMatch(key, matchKey) ) { - cache.delete(key); + clientCache.delete(key); } } }, + + readKey(params) { + return clientCache.partialReadKey(params); + }, + + partialReadKey({ abi, cacheNamespace: namespace, ...params }) { + return createSerializableKey([namespace, "read", params]); + }, }); return clientCache; diff --git a/packages/drift/src/cache/ClientCache/types.ts b/packages/drift/src/cache/ClientCache/types.ts index 3c36e73c..c6a94d78 100644 --- a/packages/drift/src/cache/ClientCache/types.ts +++ b/packages/drift/src/cache/ClientCache/types.ts @@ -3,68 +3,123 @@ import type { AdapterGetEventsParams, AdapterReadParams, } from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; import type { ContactEvent, EventName } from "src/adapter/types/Event"; import type { FunctionName, FunctionReturn } from "src/adapter/types/Function"; +import type { NetworkGetBlockParams } from "src/adapter/types/Network"; +import type { Transaction } from "src/adapter/types/Transaction"; import type { SimpleCache } from "src/cache/SimpleCache/types"; +import type { Address, TransactionHash } from "src/types"; import type { SerializableKey } from "src/utils/createSerializableKey"; import type { MaybePromise, DeepPartial as Partial } from "src/utils/types"; export type ClientCache = T & { + // Chain ID // + + preloadChainId( + params: ChainIdKeyParams & { + value: number; + }, + ): MaybePromise; + + chainIdKey(params?: ChainIdKeyParams): SerializableKey; + + // Block // + + preloadBlock( + params: BlockKeyParams & { + value: Block; + }, + ): MaybePromise; + + blockKey(params?: BlockKeyParams): SerializableKey; + + // Balance // + + preloadBalance( + params: BalanceKeyParams & { + value: bigint; + }, + ): MaybePromise; + + invalidateBalance(params: BalanceKeyParams): MaybePromise; + + balanceKey(params: BalanceKeyParams): SerializableKey; + + // Transaction // + + preloadTransaction( + params: TransactionKeyParams & { + value: Transaction; + }, + ): MaybePromise; + + transactionKey(params: TransactionKeyParams): SerializableKey; + // Events // preloadEvents>( - params: DriftEventsKeyParams & { + params: EventsKeyParams & { value: readonly ContactEvent[]; }, ): MaybePromise; eventsKey>( - params: DriftEventsKeyParams, + params: EventsKeyParams, ): SerializableKey; // Read // preloadRead>( - params: DriftReadKeyParams & { + params: ReadKeyParams & { value: FunctionReturn; }, ): MaybePromise; invalidateRead>( - params: DriftReadKeyParams, + params: ReadKeyParams, ): MaybePromise; invalidateReadsMatching< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: Partial>, - ): MaybePromise; + >(params: Partial>): MaybePromise; readKey>( - params: DriftReadKeyParams, + params: ReadKeyParams, ): SerializableKey; partialReadKey>( - params: Partial>, + params: Partial>, ): SerializableKey; }; export interface NameSpaceParam { /** - * A namespace to distinguish this instance from others in the cache by - * prefixing all cache keys. + * A namespace to distinguish this instance from others in the cache. */ // TODO: This needs unit tests - namespace?: PropertyKey; + cacheNamespace?: PropertyKey; +} + +export interface ChainIdKeyParams extends NameSpaceParam {} + +export type BlockKeyParams = NameSpaceParam & NetworkGetBlockParams; + +export type BalanceKeyParams = BlockKeyParams & { + address: Address; +}; + +export interface TransactionKeyParams extends NameSpaceParam { + hash: TransactionHash; } -export type DriftReadKeyParams< +export type ReadKeyParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, > = NameSpaceParam & AdapterReadParams; -export type DriftEventsKeyParams< +export type EventsKeyParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, > = NameSpaceParam & AdapterGetEventsParams; diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts index 8ca3c38d..e772823e 100644 --- a/packages/drift/src/client/Contract/Contract.ts +++ b/packages/drift/src/client/Contract/Contract.ts @@ -20,9 +20,9 @@ import type { import { createClientCache } from "src/cache/ClientCache/createClientCache"; import type { ClientCache, - DriftEventsKeyParams, - DriftReadKeyParams, + EventsKeyParams, NameSpaceParam, + ReadKeyParams, } from "src/cache/ClientCache/types"; import type { Address, Bytes, TransactionHash } from "src/types"; import type { SerializableKey } from "src/utils/createSerializableKey"; @@ -73,7 +73,7 @@ export class ReadContract< adapter, address, cache = createClientCache() as TCache, - namespace, + cacheNamespace: namespace, }: ReadContractParams) { this.abi = abi; this.adapter = adapter; @@ -88,8 +88,7 @@ export class ReadContract< * Retrieves specified events from the contract. */ getEvents = async >( - event: TEventName, - options?: ContractGetEventsOptions, + ...[event, options]: ContractGetEventsArgs ): Promise[]> => { const key = this.eventsKey(event, options); if (this.cache.has(key)) { @@ -110,14 +109,14 @@ export class ReadContract< preloadEvents = >( params: Omit< - DriftEventsKeyParams, + EventsKeyParams, keyof ReadContractParams > & { value: readonly ContactEvent[]; }, ): MaybePromise => { return this.cache.preloadEvents({ - namespace: this.namespace, + cacheNamespace: this.namespace, abi: this.abi, address: this.address, ...params, @@ -125,11 +124,10 @@ export class ReadContract< }; eventsKey = >( - event: TEventName, - options?: ContractGetEventsOptions, + ...[event, options]: ContractGetEventsArgs ): SerializableKey => { return this.cache.eventsKey({ - namespace: this.namespace, + cacheNamespace: this.namespace, abi: this.abi, address: this.address, event, @@ -167,14 +165,14 @@ export class ReadContract< preloadRead = >( params: Omit< - DriftReadKeyParams, + ReadKeyParams, keyof ReadContractParams > & { value: FunctionReturn; }, ): MaybePromise => { this.cache.preloadRead({ - namespace: this.namespace, + cacheNamespace: this.namespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -187,7 +185,7 @@ export class ReadContract< ...[fn, args, options]: ContractReadArgs ): MaybePromise { return this.cache.invalidateRead({ - namespace: this.namespace, + cacheNamespace: this.namespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -204,7 +202,7 @@ export class ReadContract< options?: ContractReadOptions, ): MaybePromise => { const matchKey = this.cache.partialReadKey({ - namespace: this.namespace, + cacheNamespace: this.namespace, abi: this.abi, address: this.address, fn, @@ -231,7 +229,7 @@ export class ReadContract< ...[fn, args, options]: ContractReadArgs ): SerializableKey => { return this.cache.readKey({ - namespace: this.namespace, + cacheNamespace: this.namespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -248,7 +246,7 @@ export class ReadContract< options?: ContractReadOptions, ): SerializableKey => { return this.cache.partialReadKey({ - namespace: this.namespace, + cacheNamespace: this.namespace, abi: this.abi, address: this.address, fn, @@ -350,6 +348,11 @@ export class ReadWriteContract< }; } +export type ContractGetEventsArgs< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = [event: TEventName, options?: ContractGetEventsOptions]; + export type ContractReadArgs< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 5eeff03b..947881e6 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -1,7 +1,7 @@ import type { Abi } from "abitype"; +import type { SinonStub } from "sinon"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { - ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; @@ -12,16 +12,17 @@ import type { FunctionName, FunctionReturn, } from "src/adapter/types/Function"; +import type { ClientCache } from "src/cache/ClientCache/types"; import { type ContractEncodeFunctionDataArgs, + type ContractGetEventsArgs, type ContractParams, type ContractReadArgs, type ContractWriteArgs, ReadWriteContract, } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; -import type { Address, Bytes, TransactionHash } from "src/types"; -import { MockStore } from "src/utils/MockStore"; +import type { Bytes, TransactionHash } from "src/types"; import type { OptionalKeys } from "src/utils/types"; export type MockContractParams = Omit< @@ -29,61 +30,42 @@ export type MockContractParams = Omit< "adapter" | "cache" >; -export class MockContract extends ReadWriteContract< - TAbi, - MockAdapter -> { +export class MockContract< + TAbi extends Abi = Abi, + TCache extends ClientCache = ClientCache, +> extends ReadWriteContract { // mocks // constructor({ abi, address = ZERO_ADDRESS, - namespace, + cacheNamespace: namespace, }: MockContractParams) { super({ abi, adapter: new MockAdapter(), address, - namespace, + cacheNamespace: namespace, }); } - protected mocks = new MockStore>(); - - reset = () => this.mocks.reset(); + reset = () => this.adapter.reset(); // getEvents // onGetEvents>( - event: TEventName, - options?: ContractGetEventsOptions, + ...[event, options]: ContractGetEventsArgs ) { - return this.mocks - .get< - [ - event: TEventName, - options?: ContractGetEventsOptions, - ], - Promise[]> - >({ - method: "getEvents", - key: event, - }) - .withArgs(event, options); - } - - getEvents = async >( - event: TEventName, - options?: ContractGetEventsOptions, - ) => { - return this.mocks.get< - [event: TEventName, options?: ContractGetEventsOptions], + return this.adapter.onGetEvents({ + abi: this.abi, + address: this.address, + event, + ...options, + }) as SinonStub as SinonStub< + ContractGetEventsArgs, Promise[]> - >({ - method: "getEvents", - key: event, - })(event, options); - }; + >; + } // read // @@ -92,28 +74,17 @@ export class MockContract extends ReadWriteContract< args?: FunctionArgs, options?: ContractReadOptions, ) { - return this.mocks - .get< - ContractReadArgs, - Promise> - >({ - method: "read", - key: fn, - }) - .withArgs(fn, args as any, options); - } - - read = async >( - ...[fn, args, options]: ContractReadArgs - ) => { - return this.mocks.get< + return this.adapter.onRead({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as SinonStub as SinonStub< ContractReadArgs, Promise> - >({ - method: "read", - key: fn, - })(fn, args as any, options); - }; + >; + } // simulateWrite // @@ -124,30 +95,17 @@ export class MockContract extends ReadWriteContract< args?: FunctionArgs, options?: ContractWriteOptions, ) { - return this.mocks - .get< - ContractWriteArgs, - Promise> - >({ - method: "simulateWrite", - key: fn, - }) - .withArgs(fn, args as any, options); - } - - simulateWrite = async < - TFunctionName extends FunctionName, - >( - ...[fn, args, options]: ContractWriteArgs - ) => { - return this.mocks.get< + return this.adapter.onSimulateWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as SinonStub as SinonStub< ContractWriteArgs, Promise> - >({ - method: "simulateWrite", - key: fn, - })(fn, args as any, options); - }; + >; + } // encodeFunction // @@ -155,69 +113,34 @@ export class MockContract extends ReadWriteContract< fn?: TFunctionName, args?: FunctionArgs, ) { - let mock = this.mocks.get< + return this.adapter.onEncodeFunctionData({ + abi: this.abi, + fn, + args, + }) as SinonStub as SinonStub< ContractEncodeFunctionDataArgs, Bytes - >({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - }); - if (fn && args) { - // TODO: Cleanup type casting - mock = mock.withArgs(fn as any, args as any); - } - return mock; + >; } - encodeFunctionData = >( - ...[fn, args]: ContractEncodeFunctionDataArgs - ) => { - return this.mocks.get< - ContractEncodeFunctionDataArgs, - Bytes - >({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - })(fn, args as any); - }; - // decodeFunction // onDecodeFunctionData>(data?: Bytes) { - return this.mocks - .get<[data: Bytes], DecodedFunctionData>({ - method: "decodeFunctionData", - }) - .withArgs(data); - } - - decodeFunctionData = >( - data: Bytes, - ) => { - return this.mocks.get< + return this.adapter.onDecodeFunctionData({ + abi: this.abi, + data, + }) as SinonStub as SinonStub< [data: Bytes], DecodedFunctionData - >({ - method: "decodeFunctionData", - })(data); - }; + >; + } // getSignerAddress // onGetSignerAddress() { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), - }); + return this.adapter.onGetSignerAddress(); } - getSignerAddress = async () => { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), - })(); - }; - // write // onWrite>( @@ -225,27 +148,15 @@ export class MockContract extends ReadWriteContract< args?: FunctionArgs, options?: ContractWriteOptions, ) { - return this.mocks - .get, Promise>({ - method: "write", - key: fn, - create: (mock) => mock.resolves("0x0"), - }) - .withArgs(fn, args as any, options); - } - - write = async < - TFunctionName extends FunctionName, - >( - ...[fn, args, options]: ContractWriteArgs - ) => { - return this.mocks.get< + return this.adapter.onWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as SinonStub as SinonStub< ContractWriteArgs, Promise - >({ - method: "write", - key: fn, - create: (mock) => mock.resolves("0x0"), - })(fn, args as any, options); - }; + >; + } } diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts index 48c78a0c..0ce9ddc3 100644 --- a/packages/drift/src/client/Drift/Drift.ts +++ b/packages/drift/src/client/Drift/Drift.ts @@ -3,21 +3,33 @@ import type { Adapter, AdapterDecodeFunctionDataParams, AdapterEncodeFunctionDataParams, - AdapterGetEventsParams, - AdapterReadParams, AdapterWriteParams, ReadWriteAdapter, } from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; import type { ContactEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionName, FunctionReturn, } from "src/adapter/types/Function"; -import type { TransactionReceipt } from "src/adapter/types/Transaction"; +import type { NetworkWaitForTransactionParams } from "src/adapter/types/Network"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/types/Transaction"; +import { createClientCache } from "src/cache/ClientCache/createClientCache"; +import type { + BalanceKeyParams, + BlockKeyParams, + ChainIdKeyParams, + ClientCache, + EventsKeyParams, + NameSpaceParam, + ReadKeyParams, + TransactionKeyParams, +} from "src/cache/ClientCache/types"; import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { createClientCache } from "src/client/ClientCache/createClientCache"; -import type { ClientCache, NameSpaceParam } from "src/client/ClientCache/types"; import { type Contract, type ContractParams, @@ -26,25 +38,16 @@ import { } from "src/client/Contract/Contract"; import type { Bytes, TransactionHash } from "src/types"; -export type DriftContract< - TAbi extends Abi, - TAdapter extends Adapter = Adapter, -> = TAdapter extends ReadWriteAdapter - ? ReadWriteContract - : ReadContract; - export interface DriftOptions extends NameSpaceParam { cache?: TCache; } -// This is the one implementation that combines the Read/ReadWrite concepts in -// favor of a unified entrypoint to the library's top-level API. export class Drift< TAdapter extends Adapter = Adapter, TCache extends SimpleCache = SimpleCache, > { - readonly adapter: TAdapter; + adapter: TAdapter; cache: ClientCache; namespace?: PropertyKey; @@ -67,7 +70,7 @@ export class Drift< constructor( adapter: TAdapter, - { cache, namespace }: DriftOptions = {}, + { cache, cacheNamespace: namespace }: DriftOptions = {}, ) { this.adapter = adapter; this.cache = createClientCache(cache); @@ -86,9 +89,9 @@ export class Drift< const writePromise = this.adapter.write(params); if (params.onMined) { - writePromise.then((txHash) => { - this.adapter.waitForTransaction(txHash).then(params.onMined); - return txHash; + writePromise.then((hash) => { + this.adapter.waitForTransaction({ hash }).then(params.onMined); + return hash; }); } @@ -108,7 +111,7 @@ export class Drift< abi, address, cache = this.cache, - namespace = this.namespace, + cacheNamespace: namespace = this.namespace, }: ContractParams): Contract => { return ( this.isReadWrite() @@ -117,18 +120,109 @@ export class Drift< adapter: this.adapter, address, cache, - namespace, + cacheNamespace: namespace, }) : new ReadContract({ abi, adapter: this.adapter, address, cache, - namespace, + cacheNamespace: namespace, }) ) as Contract; }; + /** + * Get the chain ID of the network. + */ + getChainId = async (params?: GetChainIdParams): Promise => { + const key = this.cache.chainIdKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getChainId().then((id) => { + this.cache.set(key, id); + return id; + }); + }; + + /** + * Get a block from a block tag, number, or hash. If no argument is provided, + * the latest block is returned. + */ + getBlock = async (params?: GetBlockParams): Promise => { + const key = this.cache.blockKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getBlock(params).then((block) => { + this.cache.set(key, block); + return block; + }); + }; + + /** + * Get the balance of native currency for an account. + */ + getBalance = async (params: GetBalanceParams): Promise => { + const key = this.cache.balanceKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getBalance(params).then((balance) => { + this.cache.set(key, balance); + return balance; + }); + }; + + /** + * Get a transaction from a transaction hash. + */ + getTransaction = ( + params: GetTransactionParams, + ): Promise => { + const key = this.cache.transactionKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getTransaction(params).then((tx) => { + this.cache.set(key, tx); + return tx; + }); + }; + + /** + * Wait for a transaction to be mined. + */ + waitForTransaction = ( + params: WaitForTransactionParams, + ): Promise => { + const key = this.cache.transactionKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.waitForTransaction(params).then((tx) => { + this.cache.set(key, tx); + return tx; + }); + }; + + /** + * Retrieves specified events from a contract. + */ + getEvents = async >( + params: GetEventsParams, + ): Promise[]> => { + const key = this.cache.eventsKey(params); + if (this.cache.has(key)) { + return this.cache.get(key); + } + return this.adapter.getEvents(params).then((result) => { + this.cache.set(key, result); + return result; + }); + }; + /** * Reads a specified function from a contract. */ @@ -160,22 +254,6 @@ export class Drift< return this.adapter.simulateWrite(params); }; - /** - * Retrieves specified events from a contract. - */ - getEvents = async >( - params: GetEventsParams, - ): Promise[]> => { - const key = this.cache.eventsKey(params); - if (this.cache.has(key)) { - return this.cache.get(key); - } - return this.adapter.getEvents(params).then((result) => { - this.cache.set(key, result); - return result; - }); - }; - /** * Encodes a function call into calldata. */ @@ -202,10 +280,22 @@ export class Drift< }; } +export interface GetChainIdParams extends ChainIdKeyParams {} + +export type GetBlockParams = BlockKeyParams; + +export type GetBalanceParams = BalanceKeyParams; + +export interface GetTransactionParams extends TransactionKeyParams {} + +export interface WaitForTransactionParams + extends TransactionKeyParams, + NetworkWaitForTransactionParams {} + export type ReadParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, -> = NameSpaceParam & AdapterReadParams; +> = ReadKeyParams; export type SimulateWriteParams< TAbi extends Abi = Abi, @@ -228,7 +318,7 @@ export type WriteParams< export type GetEventsParams< TAbi extends Abi = Abi, TEventName extends EventName = EventName, -> = NameSpaceParam & AdapterGetEventsParams; +> = EventsKeyParams; export type EncodeFunctionDataParams< TAbi extends Abi = Abi, diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts index daa683a2..3305a5f2 100644 --- a/packages/drift/src/client/Drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -1,21 +1,40 @@ +import type { Block } from "src/adapter/types/Block"; +import type { ContactEvent } from "src/adapter/types/Event"; +import type { DecodedFunctionData } from "src/adapter/types/Function"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/types/Transaction"; +import type { + DecodeFunctionDataParams, + EncodeFunctionDataParams, + GetEventsParams, + ReadParams, + WriteParams, +} from "src/client/Drift/Drift"; import { MockDrift } from "src/client/Drift/MockDrift"; import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockDrift", () => { - it("Creates mock read-write contracts", async () => { + // biome-ignore lint/suspicious/noFocusedTests: + it.only("Creates mock read-write contracts", async () => { const mockDrift = new MockDrift(); const mockContract = mockDrift.contract({ abi: IERC20.abi, address: "0xVaultAddress", }); - mockContract.stubRead({ - functionName: "symbol", - value: "FOO", - }); - expect(await mockContract.read("symbol")).toBe("FOO"); + mockDrift + .onRead({ + abi: IERC20.abi, + address: "0xVaultAddress", + fn: "symbol", + }) + .resolves("FOO"); + // mockContract.onRead("symbol").resolves("FOO"); + expect(await mockContract.read("symbol")).toBe("FOO"); // expect( // await mockDrift.read({ // abi: IERC20.abi, @@ -24,12 +43,580 @@ describe("MockDrift", () => { // }), // ).toBe("FOO"); - mockContract.stubWrite("approve", "0xHash"); - expect( - await mockContract.write("approve", { - spender: "0x1", - value: 100n, - }), - ).toBe("0xHash"); + // mockContract + // .onWrite("approve", { + // spender: "0x1", + // value: 100n, + // }) + // .resolves("0xHash"); + + // expect( + // await mockContract.write("approve", { + // spender: "0x1", + // value: 100n, + // }), + // ).toBe("0xHash"); + }); + + describe("getChainId", () => { + it("Resolves to a default value", async () => { + const drift = new MockDrift(); + expect(await drift.getChainId()).toBeTypeOf("number"); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift.onGetChainId().resolves(123); + expect(await drift.getChainId()).toBe(123); + }); + }); + + describe("getBlock", () => { + it("Resolves to a default value", async () => { + const drift = new MockDrift(); + expect(drift.getBlock()).resolves.toEqual({ + blockNumber: expect.any(BigInt), + timestamp: expect.any(BigInt), + }); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + const block: Block = { + blockNumber: 123n, + timestamp: 123n, + }; + drift.onGetBlock().resolves(block); + expect(await drift.getBlock()).toBe(block); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const { blockNumber, timestamp = 0n } = (await drift.getBlock()) ?? {}; + const block1: Block = { + blockNumber: blockNumber ?? 0n + 1n, + timestamp: timestamp + 1n, + }; + const block2: Block = { + blockNumber: blockNumber ?? 0n + 2n, + timestamp: timestamp + 2n, + }; + drift.onGetBlock({ blockNumber: 1n }).resolves(block1); + drift.onGetBlock({ blockNumber: 2n }).resolves(block2); + expect(await drift.getBlock({ blockNumber: 1n })).toBe(block1); + expect(await drift.getBlock({ blockNumber: 2n })).toBe(block2); + }); + }); + + describe("getBalance", () => { + it("Resolves to a default value", async () => { + const drift = new MockDrift(); + expect(await drift.getBalance({ address: "0x" })).toBeTypeOf("bigint"); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift.onGetBalance().resolves(123n); + expect(await drift.getBalance({ address: "0x" })).toBe(123n); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const defaultValue = await drift.getBalance({ address: "0x" }); + drift.onGetBalance({ address: "0xAlice" }).resolves(defaultValue + 1n); + drift.onGetBalance({ address: "0xBob" }).resolves(defaultValue + 2n); + expect(await drift.getBalance({ address: "0xAlice" })).toBe( + defaultValue + 1n, + ); + expect(await drift.getBalance({ address: "0xBob" })).toBe( + defaultValue + 2n, + ); + }); + }); + + describe("getTransaction", () => { + it("Resolves to undefined by default", async () => { + const drift = new MockDrift(); + expect(await drift.getTransaction({ hash: "0x" })).toBeUndefined(); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + const transaction: Transaction = { + blockNumber: 123n, + gas: 123n, + gasPrice: 123n, + input: "0x", + nonce: 123, + type: "0x123", + value: 123n, + }; + drift.onGetTransaction().resolves(transaction); + expect(await drift.getTransaction({ hash: "0x" })).toBe(transaction); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const transaction1: Transaction = { + input: "0x1", + blockNumber: 123n, + gas: 123n, + gasPrice: 123n, + nonce: 123, + type: "0x123", + value: 123n, + }; + const transaction2: Transaction = { + ...transaction1, + input: "0x2", + }; + drift.onGetTransaction({ hash: "0x1" }).resolves(transaction1); + drift.onGetTransaction({ hash: "0x2" }).resolves(transaction2); + expect(await drift.getTransaction({ hash: "0x1" })).toBe(transaction1); + expect(await drift.getTransaction({ hash: "0x2" })).toBe(transaction2); + }); + }); + + describe("waitForTransaction", () => { + it("Resolves to undefined by default", async () => { + const drift = new MockDrift(); + expect(drift.waitForTransaction({ hash: "0x" })).resolves.toBeUndefined(); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + const receipt: TransactionReceipt = { + blockNumber: 123n, + blockHash: "0x", + cumulativeGasUsed: 123n, + effectiveGasPrice: 123n, + from: "0x", + gasUsed: 123n, + logsBloom: "0x", + status: "success", + to: "0x", + transactionHash: "0x", + transactionIndex: 123, + }; + drift.onWaitForTransaction().resolves(receipt); + expect(await drift.waitForTransaction({ hash: "0x" })).toBe(receipt); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const receipt1: TransactionReceipt = { + transactionHash: "0x1", + blockNumber: 123n, + blockHash: "0x", + cumulativeGasUsed: 123n, + effectiveGasPrice: 123n, + from: "0x", + gasUsed: 123n, + logsBloom: "0x", + status: "success", + to: "0x", + transactionIndex: 123, + }; + const receipt2: TransactionReceipt = { + ...receipt1, + transactionHash: "0x2", + }; + drift.onWaitForTransaction({ hash: "0x1" }).resolves(receipt1); + drift.onWaitForTransaction({ hash: "0x2" }).resolves(receipt2); + expect(await drift.waitForTransaction({ hash: "0x1" })).toBe(receipt1); + expect(await drift.waitForTransaction({ hash: "0x2" })).toBe(receipt2); + }); + }); + + describe("encodeFunctionData", () => { + it("Returns a default value", async () => { + const drift = new MockDrift(); + expect( + drift.encodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift + .onEncodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }) + .returns("0x123"); + expect( + drift.encodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBe("0x123"); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: EncodeFunctionDataParams = + { + abi: IERC20.abi, + fn: "balanceOf", + args: { owner: "0x1" }, + }; + const params2: EncodeFunctionDataParams = + { + ...params1, + args: { owner: "0x2" }, + }; + drift.onEncodeFunctionData(params1).returns("0x1"); + drift.onEncodeFunctionData(params2).returns("0x2"); + expect(drift.encodeFunctionData(params1)).toBe("0x1"); + expect(drift.encodeFunctionData(params2)).toBe("0x2"); + }); + }); + + describe("decodeFunctionData", () => { + it("Throws an error by default", async () => { + const drift = new MockDrift(); + let error: unknown; + try { + drift.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + const decoded: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; + drift + .onDecodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }) + .returns(decoded); + expect( + drift.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }), + ).toBe(decoded); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: DecodeFunctionDataParams = + { + abi: IERC20.abi, + fn: "balanceOf", + data: "0x1", + }; + const return1: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; + const params2: DecodeFunctionDataParams = + { + ...params1, + data: "0x2", + }; + const return2: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x2" }, + }; + drift.onDecodeFunctionData(params1).returns(return1); + drift.onDecodeFunctionData(params2).returns(return2); + expect(drift.decodeFunctionData(params1)).toBe(return1); + expect(drift.decodeFunctionData(params2)).toBe(return2); + }); + }); + + describe("getEvents", () => { + it("Rejects with an error by default", async () => { + const drift = new MockDrift(); + let error: unknown; + try { + await drift.getEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + const events: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x", + to: "0x", + value: 123n, + }, + }, + ]; + drift + .onGetEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }) + .resolves(events); + expect( + await drift.getEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }), + ).toBe(events); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: GetEventsParams = { + abi: IERC20.abi, + address: "0x1", + event: "Transfer", + filter: { from: "0x1" }, + }; + const params2: GetEventsParams = { + ...params1, + filter: { from: "0x2" }, + }; + const events1: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: ContactEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x2", + to: "0x2", + value: 123n, + }, + }, + ]; + drift.onGetEvents(params1).resolves(events1); + drift.onGetEvents(params2).resolves(events2); + expect(await drift.getEvents(params1)).toBe(events1); + expect(await drift.getEvents(params2)).toBe(events2); + }); + }); + + describe("read", () => { + it("Rejects with an error by default", async () => { + const drift = new MockDrift(); + let error: unknown; + try { + await drift.read({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift + .onRead({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }) + .resolves("ABC"); + expect( + await drift.read({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }), + ).toBe("ABC"); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: ReadParams = { + abi: IERC20.abi, + address: "0x1", + fn: "allowance", + args: { owner: "0x1", spender: "0x1" }, + }; + const params2: ReadParams = { + ...params1, + args: { owner: "0x2", spender: "0x2" }, + }; + drift.onRead(params1).resolves(1n); + drift.onRead(params2).resolves(2n); + expect(await drift.read(params1)).toBe(1n); + expect(await drift.read(params2)).toBe(2n); + }); + + it.todo("Can be stubbed with partial args", async () => { + const drift = new MockDrift(); + drift + .onRead({ + abi: IERC20.abi, + address: "0x", + fn: "balanceOf", + }) + .resolves(123n); + expect( + await drift.read({ + abi: IERC20.abi, + address: "0x", + fn: "balanceOf", + args: { owner: "0x" }, + }), + ).toBe(123n); + }); + }); + + describe("simulateWrite", () => { + it("Rejects with an error by default", async () => { + const drift = new MockDrift(); + let error: unknown; + try { + await drift.simulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift + .onSimulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }) + .resolves(true); + expect( + await drift.simulateWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBe(true); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: WriteParams = { + abi: IERC20.abi, + address: "0x1", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: WriteParams = { + ...params1, + args: { to: "0x2", value: 123n }, + }; + drift.onSimulateWrite(params1).resolves(true); + drift.onSimulateWrite(params2).resolves(false); + expect(await drift.simulateWrite(params1)).toBe(true); + expect(await drift.simulateWrite(params2)).toBe(false); + }); + }); + + describe("write", () => { + it("Returns a default value", async () => { + const drift = new MockDrift(); + expect( + await drift.write({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift + .onWrite({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }) + .resolves("0x123"); + expect( + await drift.write({ + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x", value: 123n }, + }), + ).toBe("0x123"); + }); + + it("Can be stubbed with specific args", async () => { + const drift = new MockDrift(); + const params1: WriteParams = { + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: WriteParams = { + ...params1, + args: { to: "0x2", value: 123n }, + }; + drift.onWrite(params1).resolves("0x1"); + drift.onWrite(params2).resolves("0x2"); + expect(await drift.write(params1)).toBe("0x1"); + expect(await drift.write(params2)).toBe("0x2"); + }); + }); + + describe("getSignerAddress", () => { + it("Returns a default value", async () => { + const drift = new MockDrift(); + expect(await drift.getSignerAddress()).toBeTypeOf("string"); + }); + + it("Can be stubbed", async () => { + const drift = new MockDrift(); + drift.onGetSignerAddress().resolves("0x123"); + expect(await drift.getSignerAddress()).toBe("0x123"); + }); }); }); diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index df7c5b49..8034a489 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -1,19 +1,387 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; -import type { ReadWriteContractStub } from "src/adapter/contract/mocks/ReadWriteContractStub"; -import type { SimpleCache } from "src/cache/simple-cache/types"; -import type { ReadWriteContract } from "src/client/Contract/Contract"; -import { Drift, type DriftOptions } from "src/client/Drift/Drift"; - -export class MockDrift extends Drift< - MockAdapter, - TCache -> { - constructor(options?: DriftOptions) { - super(new MockAdapter(), options); - } - - declare contract: ( - params: ContractParams, - ) => ReadWriteContract & ReadWriteContractStub; +import type { Block } from "src/adapter/types/Block"; +import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/types/Transaction"; +import type { ClientCache } from "src/cache/ClientCache/types"; +import { + MockContract, + type MockContractParams, +} from "src/client/Contract/MockContract"; +import { + type DecodeFunctionDataParams, + Drift, + type EncodeFunctionDataParams, + type GetBalanceParams, + type GetBlockParams, + type GetChainIdParams, + type GetEventsParams, + type GetTransactionParams, + type ReadParams, + type WaitForTransactionParams, + type WriteParams, +} from "src/client/Drift/Drift"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import { MockStore } from "src/utils/MockStore"; +import type { OptionalKeys } from "src/utils/types"; + +export class MockDrift extends Drift { + mocks = new MockStore>(); + + constructor() { + super(new MockAdapter()); + } + + reset = () => this.mocks.reset(); + + contract = ( + params: MockContractParams, + ): MockContract => new MockContract(params); + + // getChainId // + + // FIXME: Partial args in `on` methods is not working as expected. Currently, + // you must stub the method with all expected args. + onGetChainId(params?: Partial) { + let mock = this.mocks.get<[GetChainIdParams?], Promise>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; + } + + getChainId = async (params?: GetChainIdParams) => { + return this.mocks.get<[GetChainIdParams?], Promise>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + })(params); + }; + + // getBlock // + + onGetBlock(params?: Partial) { + return this.mocks + .get<[GetBlockParams?], Promise>({ + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }) + .withArgs(params); + } + + getBlock = async (params?: GetBlockParams) => { + return this.mocks.get<[GetBlockParams?], Promise>({ + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + })(params); + }; + + // getBalance // + + onGetBalance(params?: Partial) { + let mock = this.mocks.get<[GetBalanceParams], Promise>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; + } + + getBalance = async (params: GetBalanceParams) => { + return this.mocks.get<[GetBalanceParams], Promise>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), + })(params); + }; + + // getTransaction // + + onGetTransaction(params?: Partial) { + let mock = this.mocks.get< + [GetTransactionParams], + Promise + >({ + method: "getTransaction", + create: (mock) => mock.resolves(undefined), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; + } + + getTransaction = async (params: GetTransactionParams) => { + return this.mocks.get< + [GetTransactionParams], + Promise + >({ + method: "getTransaction", + create: (mock) => mock.resolves(undefined), + })(params); + }; + + // waitForTransaction // + + onWaitForTransaction(params?: Partial) { + let mock = this.mocks.get< + [WaitForTransactionParams], + Promise + >({ + method: "waitForTransaction", + create: (mock) => mock.resolves(undefined), + }); + if (params) { + mock = mock.withArgs(params); + } + return mock; + } + + waitForTransaction = async (params: WaitForTransactionParams) => { + return this.mocks.get< + [WaitForTransactionParams], + Promise + >({ + method: "waitForTransaction", + create: (mock) => mock.resolves(undefined), + })(params); + }; + + // encodeFunction // + + onEncodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args">, + ) { + return this.mocks + .get<[EncodeFunctionDataParams], Bytes>({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + }) + .withArgs(params); + } + + encodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: EncodeFunctionDataParams, + ) => { + return this.mocks.get<[EncodeFunctionDataParams], Bytes>({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + })(params); + }; + + // decodeFunction // + + onDecodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "data">, + ) { + return this.mocks + .get< + [DecodeFunctionDataParams], + DecodedFunctionData + >({ + method: "decodeFunctionData", + key: params.fn, + }) + .withArgs(params); + } + + decodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: DecodeFunctionDataParams, + ) => { + return this.mocks.get< + [DecodeFunctionDataParams], + DecodedFunctionData + >({ + method: "decodeFunctionData", + // TODO: This should be specific to the abi to ensure the correct return + // type. + key: params.fn, + })(params); + }; + + // getEvents // + + onGetEvents>( + params: OptionalKeys, "address">, + ) { + return this.mocks + .get< + [GetEventsParams], + Promise[]> + >({ + method: "getEvents", + key: params.event, + }) + .withArgs(params); + } + + getEvents = async >( + params: GetEventsParams, + ) => { + return this.mocks.get< + [GetEventsParams], + Promise[]> + >({ + method: "getEvents", + key: params.event, + })(params); + }; + + // read // + + onRead< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: OptionalKeys, "args" | "address">) { + return this.mocks + .get< + [ReadParams], + Promise> + >({ + method: "read", + key: params.fn, + }) + .withArgs(params as Partial>); + } + + read = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: ReadParams, + ) => { + return this.mocks.get< + [ReadParams], + Promise> + >({ + method: "read", + key: params.fn, + })(params); + }; + + // simulateWrite // + + onSimulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args" | "address">, + ) { + return this.mocks + .get< + [WriteParams], + Promise> + >({ + method: "simulateWrite", + key: params.fn, + }) + .withArgs(params as Partial>); + } + + simulateWrite = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: WriteParams, + ) => { + return this.mocks.get< + [WriteParams], + Promise> + >({ + method: "simulateWrite", + key: params.fn, + })(params); + }; + + // write // + + onWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args" | "address">, + ) { + return this.mocks + .get<[WriteParams], Promise>({ + method: "write", + key: params.fn, + create: (mock) => mock.resolves("0x0"), + }) + .withArgs(params as Partial>); + } + + write = async < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: WriteParams, + ) => { + const writePromise = Promise.resolve( + this.mocks.get< + [WriteParams], + Promise + >({ + method: "write", + key: params.fn, + create: (mock) => mock.resolves("0x0"), + })(params), + ); + + // TODO: unit test + if (params.onMined) { + writePromise.then((hash) => { + this.waitForTransaction({ hash }).then(params.onMined); + return hash; + }); + } + + return writePromise; + }; + + // getSignerAddress // + + onGetSignerAddress() { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + }); + } + + getSignerAddress = async () => { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + })(); + }; } diff --git a/packages/drift/src/utils/MockStore.ts b/packages/drift/src/utils/MockStore.ts index 10902093..d660ddb9 100644 --- a/packages/drift/src/utils/MockStore.ts +++ b/packages/drift/src/utils/MockStore.ts @@ -1,7 +1,10 @@ import stringify from "fast-json-stable-stringify"; import { type SinonStub, stub as sinonStub } from "sinon"; import type { SerializableKey } from "src/utils/createSerializableKey"; +import type { FunctionKey } from "src/utils/types"; +// TODO: Consider using a similar pattern for the cache or generalized +// plugins/hooks export class MockStore { protected mocks = new Map(); @@ -10,11 +13,7 @@ export class MockStore { key, create, }: { - method: NonNullable< - { - [K in keyof T]: T[K] extends Function ? K : never; - }[keyof T] - >; + method: FunctionKey; key?: SerializableKey; create?: (mock: SinonStub) => SinonStub; }): SinonStub { @@ -22,11 +21,10 @@ export class MockStore { if (key) { mockKey += `:${stringify(key)}`; } - let mock = this.mocks.get(mockKey); - if (mock) { - return mock as any; + if (this.mocks.has(mockKey)) { + return this.mocks.get(mockKey) as any; } - mock = sinonStub().throws( + let mock = sinonStub().throws( new NotImplementedError({ method: String(method), mockKey, diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index 281b3dc5..b83c29a7 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -3,32 +3,130 @@ export type AnyObject = Record; export type MaybePromise = T | Promise; +export type AnyFunction = (...args: any) => any; + /** * Combines members of an intersection into a readable type. * @see https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg */ -export type Prettify = { [K in keyof T]: T[K] } & unknown; +export type Prettify = { [K in keyof T]: T[K] } & {}; -/** Recursively make all properties in T partial. */ -export type DeepPartial = { - [K in keyof T]?: DeepPartial; -}; +/** + * Replace properties in `T` with properties in `U`. + */ +export type ReplaceKeys = Prettify & U>; /** * Make all properties in `T` whose keys are in the union `K` required and * non-nullable. */ -export type RequiredKeys = Prettify< - Omit & { - [P in K]-?: NonNullable; +export type RequiredKeys = ReplaceKeys< + T, + { + [U in K]-?: NonNullable; } >; /** * Make all properties in `T` whose keys are in the union `K` optional. */ -export type OptionalKeys = Prettify< - Omit & { - [P in K]?: T[P]; +export type OptionalKeys = ReplaceKeys< + T, + { + [U in K]?: T[U]; } >; + +/** Recursively make all properties in T partial. */ +export type DeepPartial = { + [K in keyof T]?: DeepPartial; +}; + +/** + * Get a union of all property keys on `T` that are functions + */ +export type FunctionKey = Exclude< + { + [K in keyof T]: T[K] extends AnyFunction ? K : never; + }[keyof T], + undefined +>; + +/** + * Convert members of a union to an intersection. + * + * @example + * ```ts + * type Union = { a: number } | { b: string }; + * type Intersection = UnionToIntersection; + * // { a: number } & { b: string } + * ``` + * + * @privateRemarks + * This works by taking advantage of [distributive conditional + * types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types), + * which allows conditional types to be applied to each member of a union type + * individually, and [contravarience in function argument + * types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-6.html#strict-function-types). + * + * The conditional type `T extends any ? (x: T) => any : never` is used to + * create a function type for each member of the union that takes the member as + * an argument. + * + * Then, the union of function types is checked to see if it can be assigned to + * a single function type with an inferred argument type. TypeScript infers the + * argument type as the intersection of the union members since it's the only + * argument type that satisfies all members of the function type union. + */ +export type UnionToIntersection = ( + T extends any + ? (member: T) => any + : never +) extends (member: infer R) => any + ? R + : never; + +/** + * Merge the keys of a union or intersection of objects into a single type. + * + * @example + * ```ts + * type GetBlockOptions = { + * includeTransactions?: boolean | undefined + * } & ( + * | { + * blockHash?: string | undefined; + * blockNumber?: never | undefined; + * blockTag?: never | undefined; + * } + * | { + * blockHash?: never | undefined; + * blockNumber?: bigint | undefined; + * blockTag?: never | undefined; + * } + * | { + * blockHash?: never | undefined; + * blockNumber?: never | undefined; + * blockTag?: string | undefined; + * } + * ) + * + * type Merged = MergeKeys; + * // { + * // includeTransactions?: boolean | undefined; + * // blockHash?: string | undefined; + * // blockNumber?: bigint | undefined; + * // blockTag?: string | undefined; + * // } + * ``` + */ +export type MergeKeys = UnionToIntersection extends infer I + ? { + // Each key of the intersection is first checked against the union type, + // T. If it exists in every member of T, then T[K] will be a union of + // the value types. Otherwise, I[K] is used. I[K] is the value type of + // the key in the intersection which will be `never` for keys with + // conflicting value types. + [K in keyof I]: K extends keyof T ? T[K] : I[K]; + } + : never; From accf10b08c96132084fcd76b8ae86fea72e01ff6 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 5 Oct 2024 14:57:08 -0500 Subject: [PATCH 28/49] Wip passing tests --- packages/drift/src/adapter/MockAdapter.ts | 10 - .../drift/src/adapter/utils/getAbiEntry.ts | 14 +- .../cache/ClientCache/createClientCache.ts | 61 ++++-- .../drift/src/client/Contract/MockContract.ts | 196 +++++++++++++----- .../drift/src/client/Contract/MockErc20.ts | 18 -- .../drift/src/client/Drift/MockDrift.test.ts | 45 ++-- packages/drift/src/error.ts | 28 +++ packages/drift/src/errors/AbiEntryNotFound.ts | 7 - packages/drift/src/utils/MockStore.ts | 5 +- 9 files changed, 239 insertions(+), 145 deletions(-) delete mode 100644 packages/drift/src/client/Contract/MockErc20.ts create mode 100644 packages/drift/src/error.ts delete mode 100644 packages/drift/src/errors/AbiEntryNotFound.ts diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index f466df92..09f95875 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -382,13 +382,3 @@ export type AdapterOnWriteParams< TAbi extends Abi = Abi, TFunctionName extends FunctionName = FunctionName, > = OptionalKeys, "args" | "address">; - -export class NotImplementedError extends Error { - constructor({ method, mockKey }: { method: string; mockKey: string }) { - super( - `Called ${method} on a MockAdapter without a return value. No mock found with key "${mockKey}". Stub the return value first: - adapter.on${method.replace(/^./, (c) => c.toUpperCase())}(...args).resolves(value)`, - ); - this.name = "NotImplementedError"; - } -} diff --git a/packages/drift/src/adapter/utils/getAbiEntry.ts b/packages/drift/src/adapter/utils/getAbiEntry.ts index ba1b8609..b34e4c7f 100644 --- a/packages/drift/src/adapter/utils/getAbiEntry.ts +++ b/packages/drift/src/adapter/utils/getAbiEntry.ts @@ -1,9 +1,6 @@ import type { Abi, AbiItemType } from "abitype"; -import type { - AbiEntry, - AbiEntryName, -} from "src/adapter/types/Abi"; -import { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; +import type { AbiEntry, AbiEntryName } from "src/adapter/types/Abi"; +import { DriftError } from "src/error"; /** * Get an entry from an ABI by type and name. @@ -34,3 +31,10 @@ export function getAbiEntry< return abiItem; } + +export class AbiEntryNotFoundError extends DriftError { + constructor({ type, name }: { type: AbiItemType; name?: string }) { + super(`No ${type}${name ? ` with name ${name}` : ""} found in ABI.`); + this.name = "AbiEntryNotFoundError"; + } +} diff --git a/packages/drift/src/cache/ClientCache/createClientCache.ts b/packages/drift/src/cache/ClientCache/createClientCache.ts index 4a261dac..ec408a50 100644 --- a/packages/drift/src/cache/ClientCache/createClientCache.ts +++ b/packages/drift/src/cache/ClientCache/createClientCache.ts @@ -1,16 +1,20 @@ import isMatch from "lodash.ismatch"; -import type { ClientCache, ReadKeyParams } from "src/cache/ClientCache/types"; +import type { + ClientCache, + ReadKeyParams +} from "src/cache/ClientCache/types"; import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; import type { SimpleCache } from "src/cache/SimpleCache/types"; -import { - createSerializableKey -} from "src/utils/createSerializableKey"; +import { createSerializableKey } from "src/utils/createSerializableKey"; import { extendInstance } from "src/utils/extendInstance"; /** * Extends a {@linkcode SimpleCache} with additional API methods for use with * Drift clients. */ +// TODO: Consider using a similar pattern as the `MockStore` for the cache or +// implement a generalized plugins/hooks layer that can be used by the cache, +// store, and other plugins. export function createClientCache( cache: T = createLruSimpleCache({ max: 500 }) as T, ): ClientCache { @@ -20,12 +24,12 @@ export function createClientCache( >(cache, { // Chain ID // - preloadChainId(value) { - return cache.set(clientCache.chainIdKey(), value); + preloadChainId({ value, ...params }) { + return cache.set(clientCache.chainIdKey(params), value); }, - chainIdKey() { - return "chainId"; + chainIdKey({ cacheNamespace } = {}) { + return createSerializableKey([cacheNamespace, "chainId"]); }, // Block // @@ -34,8 +38,12 @@ export function createClientCache( return cache.set(clientCache.blockKey(params), value); }, - blockKey({ namespace, options } = {}) { - return createSerializableKey([namespace, "block", options]); + blockKey({ cacheNamespace, blockHash, blockNumber, blockTag } = {}) { + return createSerializableKey([ + cacheNamespace, + "block", + { blockHash, blockNumber, blockTag }, + ]); }, // Balance // @@ -48,8 +56,12 @@ export function createClientCache( return cache.delete(clientCache.balanceKey(params)); }, - balanceKey({ address, cacheNamespace: namespace, options }) { - return createSerializableKey([namespace, "balance", address, options]); + balanceKey({ cacheNamespace, address, blockHash, blockNumber, blockTag }) { + return createSerializableKey([ + cacheNamespace, + "balance", + { address, blockHash, blockNumber, blockTag }, + ]); }, // Transaction // @@ -58,8 +70,8 @@ export function createClientCache( return cache.set(clientCache.transactionKey(params), value); }, - transactionKey({ hash, cacheNamespace: namespace }) { - return createSerializableKey([namespace, "transaction", hash]); + transactionKey({ hash, cacheNamespace }) { + return createSerializableKey([cacheNamespace, "transaction", { hash }]); }, // Events // @@ -68,8 +80,12 @@ export function createClientCache( return cache.set(clientCache.eventsKey(params), value); }, - eventsKey({ abi, cacheNamespace: namespace, ...params }) { - return createSerializableKey([namespace, "events", params]); + eventsKey({ cacheNamespace, address, event, filter, fromBlock, toBlock }) { + return createSerializableKey([ + cacheNamespace, + "events", + { address, event, filter, fromBlock, toBlock }, + ]); }, // Read // @@ -104,8 +120,17 @@ export function createClientCache( return clientCache.partialReadKey(params); }, - partialReadKey({ abi, cacheNamespace: namespace, ...params }) { - return createSerializableKey([namespace, "read", params]); + partialReadKey({ cacheNamespace, address, args, block, fn }) { + return createSerializableKey([ + cacheNamespace, + "read", + { + address, + args, + block, + fn, + }, + ]); }, }); diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 947881e6..e7ae1630 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -1,7 +1,7 @@ import type { Abi } from "abitype"; -import type { SinonStub } from "sinon"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { + ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; @@ -22,9 +22,15 @@ import { ReadWriteContract, } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; -import type { Bytes, TransactionHash } from "src/types"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import { MockStore } from "src/utils/MockStore"; import type { OptionalKeys } from "src/utils/types"; +// TODO: DRY up the mock clients and integrate them better so that modifying a +// mock fn in one client will modify the same mock in another client, even if +// the signatures are different. This might mean replacing the `on` methods with +// specific mock methods to control their behavior. + export type MockContractParams = Omit< OptionalKeys, "address">, "adapter" | "cache" @@ -34,7 +40,7 @@ export class MockContract< TAbi extends Abi = Abi, TCache extends ClientCache = ClientCache, > extends ReadWriteContract { - // mocks // + mocks = new MockStore>(); constructor({ abi, @@ -56,17 +62,33 @@ export class MockContract< onGetEvents>( ...[event, options]: ContractGetEventsArgs ) { - return this.adapter.onGetEvents({ - abi: this.abi, - address: this.address, - event, - ...options, - }) as SinonStub as SinonStub< - ContractGetEventsArgs, - Promise[]> - >; + return this.mocks + .get< + [ + event: TEventName, + options?: ContractGetEventsOptions, + ], + Promise[]> + >({ + method: "getEvents", + key: event, + }) + .withArgs(event, options); } + getEvents = async >( + event: TEventName, + options?: ContractGetEventsOptions, + ) => { + return this.mocks.get< + [event: TEventName, options?: ContractGetEventsOptions], + Promise[]> + >({ + method: "getEvents", + key: event, + })(event, options); + }; + // read // onRead>( @@ -74,17 +96,28 @@ export class MockContract< args?: FunctionArgs, options?: ContractReadOptions, ) { - return this.adapter.onRead({ - abi: this.abi as Abi, - address: this.address, - fn, - args, - ...options, - }) as SinonStub as SinonStub< + return this.mocks + .get< + ContractReadArgs, + Promise> + >({ + method: "read", + key: fn, + }) + .withArgs(fn, args as any, options); + } + + read = async >( + ...[fn, args, options]: ContractReadArgs + ) => { + return this.mocks.get< ContractReadArgs, Promise> - >; - } + >({ + method: "read", + key: fn, + })(fn, args as any, options); + }; // simulateWrite // @@ -95,17 +128,30 @@ export class MockContract< args?: FunctionArgs, options?: ContractWriteOptions, ) { - return this.adapter.onSimulateWrite({ - abi: this.abi as Abi, - address: this.address, - fn, - args, - ...options, - }) as SinonStub as SinonStub< + return this.mocks + .get< + ContractWriteArgs, + Promise> + >({ + method: "simulateWrite", + key: fn, + }) + .withArgs(fn, args as any, options); + } + + simulateWrite = async < + TFunctionName extends FunctionName, + >( + ...[fn, args, options]: ContractWriteArgs + ) => { + return this.mocks.get< ContractWriteArgs, Promise> - >; - } + >({ + method: "simulateWrite", + key: fn, + })(fn, args as any, options); + }; // encodeFunction // @@ -113,34 +159,68 @@ export class MockContract< fn?: TFunctionName, args?: FunctionArgs, ) { - return this.adapter.onEncodeFunctionData({ - abi: this.abi, - fn, - args, - }) as SinonStub as SinonStub< + let mock = this.mocks.get< ContractEncodeFunctionDataArgs, Bytes - >; + >({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + }); + if (fn && args) { + mock = mock.withArgs(fn, args); + } + return mock; } + encodeFunctionData = >( + ...[fn, args]: ContractEncodeFunctionDataArgs + ) => { + return this.mocks.get< + ContractEncodeFunctionDataArgs, + Bytes + >({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + })(fn, args as FunctionArgs); + }; + // decodeFunction // onDecodeFunctionData>(data?: Bytes) { - return this.adapter.onDecodeFunctionData({ - abi: this.abi, - data, - }) as SinonStub as SinonStub< + return this.mocks + .get<[data: Bytes], DecodedFunctionData>({ + method: "decodeFunctionData", + }) + .withArgs(data); + } + + decodeFunctionData = >( + data: Bytes, + ) => { + return this.mocks.get< [data: Bytes], DecodedFunctionData - >; - } + >({ + method: "decodeFunctionData", + })(data); + }; // getSignerAddress // onGetSignerAddress() { - return this.adapter.onGetSignerAddress(); + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + }); } + getSignerAddress = async () => { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + })(); + }; + // write // onWrite>( @@ -148,15 +228,27 @@ export class MockContract< args?: FunctionArgs, options?: ContractWriteOptions, ) { - return this.adapter.onWrite({ - abi: this.abi as Abi, - address: this.address, - fn, - args, - ...options, - }) as SinonStub as SinonStub< + return this.mocks + .get, Promise>({ + method: "write", + key: fn, + create: (mock) => mock.resolves("0x0"), + }) + .withArgs(fn, args as any, options); + } + + write = async < + TFunctionName extends FunctionName, + >( + ...[fn, args, options]: ContractWriteArgs + ) => { + return this.mocks.get< ContractWriteArgs, Promise - >; - } + >({ + method: "write", + key: fn, + create: (mock) => mock.resolves("0x0"), + })(fn, args as any, options); + }; } diff --git a/packages/drift/src/client/Contract/MockErc20.ts b/packages/drift/src/client/Contract/MockErc20.ts deleted file mode 100644 index 2933e983..00000000 --- a/packages/drift/src/client/Contract/MockErc20.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - MockContract, - type MockContractParams, -} from "src/client/Contract/MockContract"; -import { IERC20 } from "src/utils/testing/IERC20"; - -type Erc20Abi = typeof IERC20.abi; - -export type MockErc20Params = Omit, "abi">; - -export class MockErc20 extends MockContract { - constructor(params?: MockErc20Params) { - super({ - ...params, - abi: IERC20.abi, - }); - } -} diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts index 3305a5f2..3cd86d3f 100644 --- a/packages/drift/src/client/Drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -17,45 +17,26 @@ import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockDrift", () => { - // biome-ignore lint/suspicious/noFocusedTests: - it.only("Creates mock read-write contracts", async () => { + it("Creates mock read-write contracts", async () => { const mockDrift = new MockDrift(); const mockContract = mockDrift.contract({ abi: IERC20.abi, address: "0xVaultAddress", }); - mockDrift - .onRead({ - abi: IERC20.abi, - address: "0xVaultAddress", - fn: "symbol", + mockContract + .onWrite("approve", { + spender: "0x1", + value: 100n, }) - .resolves("FOO"); - - // mockContract.onRead("symbol").resolves("FOO"); - expect(await mockContract.read("symbol")).toBe("FOO"); - // expect( - // await mockDrift.read({ - // abi: IERC20.abi, - // address: "0xVaultAddress", - // fn: "symbol", - // }), - // ).toBe("FOO"); - - // mockContract - // .onWrite("approve", { - // spender: "0x1", - // value: 100n, - // }) - // .resolves("0xHash"); - - // expect( - // await mockContract.write("approve", { - // spender: "0x1", - // value: 100n, - // }), - // ).toBe("0xHash"); + .resolves("0xHash"); + + expect( + await mockContract.write("approve", { + spender: "0x1", + value: 100n, + }), + ).toBe("0xHash"); }); describe("getChainId", () => { diff --git a/packages/drift/src/error.ts b/packages/drift/src/error.ts new file mode 100644 index 00000000..2a1e4091 --- /dev/null +++ b/packages/drift/src/error.ts @@ -0,0 +1,28 @@ +/** + * Added to every error name. + * @internal + */ +export class DriftError extends Error { + static prefix = "βœ– Drift:"; + private _name: string; + + constructor(error: any) { + // Ensure the error can be converted into a primitive type which is required + // for the `Error` constructor. + try { + String(error); + } catch { + throw error; + } + super(error?.message || error); + this._name = `${DriftError.prefix}Drift Error`; + } + + // Override the default getter/setter to ensure the prefix is always present + get name() { + return this._name; + } + set name(name: string) { + this._name = `${DriftError.prefix}${name.replace(new RegExp(`^${DriftError.prefix}`), "")}`; + } +} diff --git a/packages/drift/src/errors/AbiEntryNotFound.ts b/packages/drift/src/errors/AbiEntryNotFound.ts deleted file mode 100644 index df9ead4c..00000000 --- a/packages/drift/src/errors/AbiEntryNotFound.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { AbiItemType } from "abitype"; - -export class AbiEntryNotFoundError extends Error { - constructor({ type, name }: { type: AbiItemType; name?: string }) { - super(`No ${type}${name ? ` with name ${name}` : ""} found in ABI.`); - } -} diff --git a/packages/drift/src/utils/MockStore.ts b/packages/drift/src/utils/MockStore.ts index d660ddb9..9c9b2149 100644 --- a/packages/drift/src/utils/MockStore.ts +++ b/packages/drift/src/utils/MockStore.ts @@ -1,10 +1,9 @@ import stringify from "fast-json-stable-stringify"; import { type SinonStub, stub as sinonStub } from "sinon"; +import { DriftError } from "src/error"; import type { SerializableKey } from "src/utils/createSerializableKey"; import type { FunctionKey } from "src/utils/types"; -// TODO: Consider using a similar pattern for the cache or generalized -// plugins/hooks export class MockStore { protected mocks = new Map(); @@ -42,7 +41,7 @@ export class MockStore { } } -export class NotImplementedError extends Error { +export class NotImplementedError extends DriftError { constructor({ method, mockKey }: { method: string; mockKey: string }) { super( `No mock found with key "${mockKey}". Called ${method} on a Mock without a return value. The value must be stubbed first: From 45ba16ae563f7438951c805b5eb95f08f14970fe Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 5 Oct 2024 15:02:24 -0500 Subject: [PATCH 29/49] Minor cleanup --- packages/drift/src/client/Contract/Contract.ts | 8 +++++--- packages/drift/src/error.ts | 4 ---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts index e772823e..c2a45342 100644 --- a/packages/drift/src/client/Contract/Contract.ts +++ b/packages/drift/src/client/Contract/Contract.ts @@ -143,9 +143,11 @@ export class ReadContract< read = >( ...[fn, args, options]: ContractReadArgs ): Promise> => { - // TODO: Cleanup type casting required due to an incompatibility between - // distributive types and the conditional args param. - const key = this.readKey(fn, args as any, options); + const key = this.readKey( + fn, + args as FunctionArgs, + options, + ); if (this.cache.has(key)) { return this.cache.get(key); } diff --git a/packages/drift/src/error.ts b/packages/drift/src/error.ts index 2a1e4091..312b6fce 100644 --- a/packages/drift/src/error.ts +++ b/packages/drift/src/error.ts @@ -1,7 +1,3 @@ -/** - * Added to every error name. - * @internal - */ export class DriftError extends Error { static prefix = "βœ– Drift:"; private _name: string; From b1166d259edaa7d68a8c1231fd8c51d7b32e7536 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 5 Oct 2024 15:04:51 -0500 Subject: [PATCH 30/49] Replace regex --- packages/drift/src/error.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/drift/src/error.ts b/packages/drift/src/error.ts index 312b6fce..ec2bcc0d 100644 --- a/packages/drift/src/error.ts +++ b/packages/drift/src/error.ts @@ -19,6 +19,8 @@ export class DriftError extends Error { return this._name; } set name(name: string) { - this._name = `${DriftError.prefix}${name.replace(new RegExp(`^${DriftError.prefix}`), "")}`; + this._name = name.startsWith(DriftError.prefix) + ? name + : `${DriftError.prefix}${name}`; } } From 1651cfad75dee4f15e7ffd82d3b99d1f6697e80d Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sat, 5 Oct 2024 15:39:42 -0500 Subject: [PATCH 31/49] Fix type, nits --- packages/drift/src/adapter/MockAdapter.ts | 2 +- packages/drift/src/adapter/types/Abi.ts | 6 +++--- packages/drift/src/client/Contract/MockContract.ts | 2 +- packages/drift/src/client/Drift/MockDrift.ts | 2 +- packages/drift/src/utils/{ => testing}/MockStore.ts | 2 +- packages/drift/src/utils/types.ts | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) rename packages/drift/src/utils/{ => testing}/MockStore.ts (91%) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 09f95875..2012360b 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -25,7 +25,7 @@ import type { TransactionReceipt, } from "src/adapter/types/Transaction"; import type { Address, Bytes, TransactionHash } from "src/types"; -import { MockStore } from "src/utils/MockStore"; +import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; // TODO: Allow configuration of error throwing/default return value behavior diff --git a/packages/drift/src/adapter/types/Abi.ts b/packages/drift/src/adapter/types/Abi.ts index c7001bae..dbe74f7b 100644 --- a/packages/drift/src/adapter/types/Abi.ts +++ b/packages/drift/src/adapter/types/Abi.ts @@ -10,7 +10,7 @@ import type { import type { EmptyObject, MergeKeys, - Prettify, + Pretty, ReplaceKeys, } from "src/utils/types"; @@ -119,7 +119,7 @@ type NamedParametersToObject< TParameterKind extends AbiParameterKind = AbiParameterKind, > = NamedAbiParameter[] extends TParameters ? Record - : Prettify< + : Pretty< { // For every parameter name, excluding empty names, add a key to the // object for the parameter name @@ -130,7 +130,7 @@ type NamedParametersToObject< Extract, TParameterKind > extends infer TPrimitive - ? TPrimitive extends unknown + ? unknown extends TPrimitive ? any : TPrimitive : never; diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index e7ae1630..6bc09da9 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -23,7 +23,7 @@ import { } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; import type { Address, Bytes, TransactionHash } from "src/types"; -import { MockStore } from "src/utils/MockStore"; +import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; // TODO: DRY up the mock clients and integrate them better so that modifying a diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index 8034a489..437e4929 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -30,7 +30,7 @@ import { type WriteParams, } from "src/client/Drift/Drift"; import type { Address, Bytes, TransactionHash } from "src/types"; -import { MockStore } from "src/utils/MockStore"; +import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; export class MockDrift extends Drift { diff --git a/packages/drift/src/utils/MockStore.ts b/packages/drift/src/utils/testing/MockStore.ts similarity index 91% rename from packages/drift/src/utils/MockStore.ts rename to packages/drift/src/utils/testing/MockStore.ts index 9c9b2149..dbb59f60 100644 --- a/packages/drift/src/utils/MockStore.ts +++ b/packages/drift/src/utils/testing/MockStore.ts @@ -44,7 +44,7 @@ export class MockStore { export class NotImplementedError extends DriftError { constructor({ method, mockKey }: { method: string; mockKey: string }) { super( - `No mock found with key "${mockKey}". Called ${method} on a Mock without a return value. The value must be stubbed first: + `No mock found with key "${mockKey}". Called \`.${method}\` on a Mock without a return value. The value must be stubbed first: mock.on${method.replace(/^./, (c) => c.toUpperCase())}(...args).resolves(value)`, ); this.name = "NotImplementedError"; diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index b83c29a7..f52c50c5 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -9,12 +9,12 @@ export type AnyFunction = (...args: any) => any; * Combines members of an intersection into a readable type. * @see https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg */ -export type Prettify = { [K in keyof T]: T[K] } & {}; +export type Pretty = { [K in keyof T]: T[K] } & {}; /** * Replace properties in `T` with properties in `U`. */ -export type ReplaceKeys = Prettify & U>; +export type ReplaceKeys = Pretty & U>; /** * Make all properties in `T` whose keys are in the union `K` required and From 486982d088c8ebfb4ffbba605b2f5ca7e2961cb3 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 6 Oct 2024 00:50:11 -0500 Subject: [PATCH 32/49] Add exports --- packages/drift/src/adapter/MockAdapter.ts | 6 +- .../drift/src/client/Contract/Contract.ts | 20 +-- .../drift/src/client/Contract/MockContract.ts | 4 +- packages/drift/src/client/Drift/Drift.ts | 12 +- packages/drift/src/exports.ts | 144 ++++++++++++++++++ 5 files changed, 163 insertions(+), 23 deletions(-) create mode 100644 packages/drift/src/exports.ts diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 2012360b..ac299a96 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -30,11 +30,7 @@ import type { OptionalKeys } from "src/utils/types"; // TODO: Allow configuration of error throwing/default return value behavior export class MockAdapter implements ReadWriteAdapter { - mocks: MockStore; - - constructor(store = new MockStore()) { - this.mocks = store; - } + mocks = new MockStore(); reset = () => this.mocks.reset(); diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts index c2a45342..346d06e6 100644 --- a/packages/drift/src/client/Contract/Contract.ts +++ b/packages/drift/src/client/Contract/Contract.ts @@ -66,20 +66,20 @@ export class ReadContract< adapter: TAdapter; address: Address; cache: TCache; - namespace?: PropertyKey; + cacheNamespace?: PropertyKey; constructor({ abi, adapter, address, cache = createClientCache() as TCache, - cacheNamespace: namespace, + cacheNamespace, }: ReadContractParams) { this.abi = abi; this.adapter = adapter; this.address = address; this.cache = cache; - this.namespace = namespace; + this.cacheNamespace = cacheNamespace; } // Events // @@ -116,7 +116,7 @@ export class ReadContract< }, ): MaybePromise => { return this.cache.preloadEvents({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, abi: this.abi, address: this.address, ...params, @@ -127,7 +127,7 @@ export class ReadContract< ...[event, options]: ContractGetEventsArgs ): SerializableKey => { return this.cache.eventsKey({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, abi: this.abi, address: this.address, event, @@ -174,7 +174,7 @@ export class ReadContract< }, ): MaybePromise => { this.cache.preloadRead({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -187,7 +187,7 @@ export class ReadContract< ...[fn, args, options]: ContractReadArgs ): MaybePromise { return this.cache.invalidateRead({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -204,7 +204,7 @@ export class ReadContract< options?: ContractReadOptions, ): MaybePromise => { const matchKey = this.cache.partialReadKey({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, abi: this.abi, address: this.address, fn, @@ -231,7 +231,7 @@ export class ReadContract< ...[fn, args, options]: ContractReadArgs ): SerializableKey => { return this.cache.readKey({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, // TODO: Cleanup type casting required due to an incompatibility between // `Omit` and the conditional args param. abi: this.abi as Abi, @@ -248,7 +248,7 @@ export class ReadContract< options?: ContractReadOptions, ): SerializableKey => { return this.cache.partialReadKey({ - cacheNamespace: this.namespace, + cacheNamespace: this.cacheNamespace, abi: this.abi, address: this.address, fn, diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 6bc09da9..0d26e2fd 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -45,13 +45,13 @@ export class MockContract< constructor({ abi, address = ZERO_ADDRESS, - cacheNamespace: namespace, + cacheNamespace, }: MockContractParams) { super({ abi, adapter: new MockAdapter(), address, - cacheNamespace: namespace, + cacheNamespace, }); } diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts index 0ce9ddc3..179c69ea 100644 --- a/packages/drift/src/client/Drift/Drift.ts +++ b/packages/drift/src/client/Drift/Drift.ts @@ -49,7 +49,7 @@ export class Drift< > { adapter: TAdapter; cache: ClientCache; - namespace?: PropertyKey; + cacheNamespace?: PropertyKey; // Write-only property definitions // @@ -70,11 +70,11 @@ export class Drift< constructor( adapter: TAdapter, - { cache, cacheNamespace: namespace }: DriftOptions = {}, + { cache, cacheNamespace }: DriftOptions = {}, ) { this.adapter = adapter; this.cache = createClientCache(cache); - this.namespace = namespace; + this.cacheNamespace = cacheNamespace; // Write-only property assignment // @@ -111,7 +111,7 @@ export class Drift< abi, address, cache = this.cache, - cacheNamespace: namespace = this.namespace, + cacheNamespace = this.cacheNamespace, }: ContractParams): Contract => { return ( this.isReadWrite() @@ -120,14 +120,14 @@ export class Drift< adapter: this.adapter, address, cache, - cacheNamespace: namespace, + cacheNamespace, }) : new ReadContract({ abi, adapter: this.adapter, address, cache, - cacheNamespace: namespace, + cacheNamespace, }) ) as Contract; }; diff --git a/packages/drift/src/exports.ts b/packages/drift/src/exports.ts new file mode 100644 index 00000000..01162a8c --- /dev/null +++ b/packages/drift/src/exports.ts @@ -0,0 +1,144 @@ +// adapter // + +export type { + AbiArrayType, + AbiEntry, + AbiEntryName, + AbiFriendlyType, + AbiObjectType, + AbiParameters, + AbiParametersToObject, + NamedAbiParameter, +} from "src/adapter/types/Abi"; +export type { + Adapter, + AdapterArgsParam, + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, + OnMinedParam, + ReadAdapter, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +export type { + Block, + BlockTag, +} from "src/adapter/types/Block"; +export type { + ContractGetEventsOptions, + ContractReadOptions, + ContractWriteOptions, +} from "src/adapter/types/Contract"; +export type { + ContactEvent, + EventArgs, + EventFilter, + EventName, +} from "src/adapter/types/Event"; +export type { + ConstructorArgs, + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +export type { + Network, + NetworkGetBalanceParams, + NetworkGetBlockOptions, + NetworkGetBlockParams, + NetworkGetTransactionParams, + NetworkWaitForTransactionParams, +} from "src/adapter/types/Network"; +export type { + MinedTransaction, + Transaction, + TransactionInfo, + TransactionReceipt, +} from "src/adapter/types/Transaction"; + +export { arrayToFriendly } from "src/adapter/utils/arrayToFriendly"; +export { arrayToObject } from "src/adapter/utils/arrayToObject"; +export { + AbiEntryNotFoundError, + getAbiEntry, +} from "src/adapter/utils/getAbiEntry"; +export { objectToArray } from "src/adapter/utils/objectToArray"; + +// cache // + +export type { + BalanceKeyParams, + BlockKeyParams, + ChainIdKeyParams, + ClientCache, + EventsKeyParams, + NameSpaceParam, + ReadKeyParams, + TransactionKeyParams, +} from "src/cache/ClientCache/types"; +export { createClientCache } from "src/cache/ClientCache/createClientCache"; + +export type { SimpleCache } from "src/cache/SimpleCache/types"; +export { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; + +// clients // + +export { + type Contract, + type ContractEncodeFunctionDataArgs, + type ContractGetEventsArgs, + type ContractParams, + type ContractReadArgs, + type ContractWriteArgs, + type ReadContractParams, + type ReadWriteContractParams, + ReadContract, + ReadWriteContract, +} from "src/client/Contract/Contract"; + +export { + type DecodeFunctionDataParams, + type DriftOptions, + type EncodeFunctionDataParams, + type GetBalanceParams, + type GetBlockParams, + type GetChainIdParams, + type GetEventsParams, + type GetTransactionParams, + type ReadParams, + type SimulateWriteParams, + type WaitForTransactionParams, + type WriteParams, + Drift, +} from "src/client/Drift/Drift"; + +// utils // + +export type { + AnyFunction, + AnyObject, + DeepPartial, + EmptyObject, + FunctionKey, + MaybePromise, + MergeKeys, + OptionalKeys, + Pretty, + ReplaceKeys, + RequiredKeys, + UnionToIntersection, +} from "src/utils/types"; +export { + type SerializableKey, + createSerializableKey, +} from "src/utils/createSerializableKey"; +export { extendInstance } from "src/utils/extendInstance"; + +// ...rest // + +export type { Address, Bytes, HexString, TransactionHash } from "src/types"; +export { ZERO_ADDRESS } from "src/constants"; +export { DriftError } from "src/error"; From 9ca47c54e68f3b2bd4f93f2d3c1a3c916e8fb917 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 6 Oct 2024 12:01:32 -0500 Subject: [PATCH 33/49] Add old packages back for now --- packages/drift-ethers/src/stubs.ts | 1 - packages/drift-viem/src/stubs.ts | 1 - .../.gitignore | 0 .../CHANGELOG.md | 0 .../README.md | 0 .../integration-tests/artifacts/CoreVoting.ts | 0 .../createReadContract.test.ts | 0 .../package.json | 6 +- .../src/contract/createCachedReadContract.ts | 2 +- .../contract/createCachedReadWriteContract.ts | 2 +- .../src/contract/createReadContract.ts | 10 +- .../src/contract/createReadWriteContract.ts | 11 +- .../src/index.ts | 10 +- .../src/network/createNetwork.ts | 2 +- packages/evm-client-ethers/src/stubs.ts | 1 + .../tsconfig.json | 0 .../tsup.config.ts | 0 .../vite.config.ts | 0 .../.gitignore | 0 .../CHANGELOG.md | 0 .../{drift-viem => evm-client-viem}/README.md | 0 .../integration-tests/artifacts/CoreVoting.ts | 0 .../createReadContract.test.ts | 0 .../package.json | 6 +- .../src/contract/createCachedReadContract.ts | 2 +- .../contract/createCachedReadWriteContract.ts | 2 +- .../src/contract/createReadContract.ts | 2 +- .../src/contract/createReadWriteContract.ts | 6 +- .../utils/createSimulateContractParameters.ts | 5 +- .../src/contract/utils/outputToFriendly.ts | 2 +- .../src/index.ts | 10 +- .../src/network/createNetwork.ts | 2 +- packages/evm-client-viem/src/stubs.ts | 1 + .../tsconfig.json | 0 .../tsup.config.ts | 0 .../vite.config.ts | 0 packages/evm-client/.gitignore | 2 + packages/evm-client/CHANGELOG.md | 157 ++++++++++ packages/evm-client/README.md | 129 ++++++++ packages/evm-client/package.json | 81 +++++ .../evm-client/src/base/testing/IERC20.ts | 224 ++++++++++++++ .../evm-client/src/base/testing/accounts.ts | 3 + packages/evm-client/src/base/types.ts | 7 + .../cache/factories/createLruSimpleCache.ts | 57 ++++ .../evm-client/src/cache/types/SimpleCache.ts | 64 ++++ .../src/cache/utils/createSimpleCacheKey.ts | 63 ++++ .../createCachedReadContract.test.ts | 174 +++++++++++ .../factories/createCachedReadContract.ts | 168 ++++++++++ .../createCachedReadWriteContract.ts | 46 +++ .../contract/stubs/ReadContractStub.test.ts | 180 +++++++++++ .../src/contract/stubs/ReadContractStub.ts | 289 ++++++++++++++++++ .../stubs/ReadWriteContractStub.test.ts | 23 ++ .../contract/stubs/ReadWriteContractStub.ts | 102 +++++++ .../evm-client/src/contract/types/AbiEntry.ts | 260 ++++++++++++++++ .../src/contract/types/CachedContract.ts | 32 ++ .../evm-client/src/contract/types/Contract.ts | 189 ++++++++++++ .../evm-client/src/contract/types/Event.ts | 64 ++++ .../evm-client/src/contract/types/Function.ts | 57 ++++ .../contract/utils/arrayToFriendly.test.ts | 67 ++++ .../src/contract/utils/arrayToFriendly.ts | 105 +++++++ .../src/contract/utils/arrayToObject.test.ts | 54 ++++ .../src/contract/utils/arrayToObject.ts | 91 ++++++ .../src/contract/utils/getAbiEntry.ts | 33 ++ .../src/contract/utils/objectToArray.test.ts | 62 ++++ .../src/contract/utils/objectToArray.ts | 89 ++++++ .../evm-client/src/errors/AbiEntryNotFound.ts | 7 + packages/evm-client/src/exports/cache.ts | 3 + packages/evm-client/src/exports/contract.ts | 54 ++++ packages/evm-client/src/exports/errors.ts | 1 + packages/evm-client/src/exports/index.ts | 4 + packages/evm-client/src/exports/network.ts | 15 + packages/evm-client/src/exports/stubs.ts | 6 + .../src/network/stubs/NetworkStub.test.ts | 112 +++++++ .../src/network/stubs/NetworkStub.ts | 179 +++++++++++ .../evm-client/src/network/types/Block.ts | 8 + .../evm-client/src/network/types/Network.ts | 76 +++++ .../src/network/types/Transaction.ts | 57 ++++ packages/evm-client/tsconfig.json | 15 + packages/evm-client/tsup.config.ts | 22 ++ packages/evm-client/vite.config.ts | 6 + 80 files changed, 3474 insertions(+), 47 deletions(-) delete mode 100644 packages/drift-ethers/src/stubs.ts delete mode 100644 packages/drift-viem/src/stubs.ts rename packages/{drift-ethers => evm-client-ethers}/.gitignore (100%) rename packages/{drift-ethers => evm-client-ethers}/CHANGELOG.md (100%) rename packages/{drift-ethers => evm-client-ethers}/README.md (100%) rename packages/{drift-ethers => evm-client-ethers}/integration-tests/artifacts/CoreVoting.ts (100%) rename packages/{drift-ethers => evm-client-ethers}/integration-tests/createReadContract.test.ts (100%) rename packages/{drift-ethers => evm-client-ethers}/package.json (90%) rename packages/{drift-ethers => evm-client-ethers}/src/contract/createCachedReadContract.ts (95%) rename packages/{drift-ethers => evm-client-ethers}/src/contract/createCachedReadWriteContract.ts (96%) rename packages/{drift-ethers => evm-client-ethers}/src/contract/createReadContract.ts (97%) rename packages/{drift-ethers => evm-client-ethers}/src/contract/createReadWriteContract.ts (92%) rename packages/{drift-ethers => evm-client-ethers}/src/index.ts (85%) rename packages/{drift-ethers => evm-client-ethers}/src/network/createNetwork.ts (98%) create mode 100644 packages/evm-client-ethers/src/stubs.ts rename packages/{drift-ethers => evm-client-ethers}/tsconfig.json (100%) rename packages/{drift-ethers => evm-client-ethers}/tsup.config.ts (100%) rename packages/{drift-ethers => evm-client-ethers}/vite.config.ts (100%) rename packages/{drift-viem => evm-client-viem}/.gitignore (100%) rename packages/{drift-viem => evm-client-viem}/CHANGELOG.md (100%) rename packages/{drift-viem => evm-client-viem}/README.md (100%) rename packages/{drift-viem => evm-client-viem}/integration-tests/artifacts/CoreVoting.ts (100%) rename packages/{drift-viem => evm-client-viem}/integration-tests/createReadContract.test.ts (100%) rename packages/{drift-viem => evm-client-viem}/package.json (92%) rename packages/{drift-viem => evm-client-viem}/src/contract/createCachedReadContract.ts (96%) rename packages/{drift-viem => evm-client-viem}/src/contract/createCachedReadWriteContract.ts (96%) rename packages/{drift-viem => evm-client-viem}/src/contract/createReadContract.ts (99%) rename packages/{drift-viem => evm-client-viem}/src/contract/createReadWriteContract.ts (98%) rename packages/{drift-viem => evm-client-viem}/src/contract/utils/createSimulateContractParameters.ts (85%) rename packages/{drift-viem => evm-client-viem}/src/contract/utils/outputToFriendly.ts (96%) rename packages/{drift-viem => evm-client-viem}/src/index.ts (85%) rename packages/{drift-viem => evm-client-viem}/src/network/createNetwork.ts (97%) create mode 100644 packages/evm-client-viem/src/stubs.ts rename packages/{drift-viem => evm-client-viem}/tsconfig.json (100%) rename packages/{drift-viem => evm-client-viem}/tsup.config.ts (100%) rename packages/{drift-viem => evm-client-viem}/vite.config.ts (100%) create mode 100644 packages/evm-client/.gitignore create mode 100644 packages/evm-client/CHANGELOG.md create mode 100644 packages/evm-client/README.md create mode 100644 packages/evm-client/package.json create mode 100644 packages/evm-client/src/base/testing/IERC20.ts create mode 100644 packages/evm-client/src/base/testing/accounts.ts create mode 100644 packages/evm-client/src/base/types.ts create mode 100644 packages/evm-client/src/cache/factories/createLruSimpleCache.ts create mode 100644 packages/evm-client/src/cache/types/SimpleCache.ts create mode 100644 packages/evm-client/src/cache/utils/createSimpleCacheKey.ts create mode 100644 packages/evm-client/src/contract/factories/createCachedReadContract.test.ts create mode 100644 packages/evm-client/src/contract/factories/createCachedReadContract.ts create mode 100644 packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts create mode 100644 packages/evm-client/src/contract/stubs/ReadContractStub.test.ts create mode 100644 packages/evm-client/src/contract/stubs/ReadContractStub.ts create mode 100644 packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts create mode 100644 packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts create mode 100644 packages/evm-client/src/contract/types/AbiEntry.ts create mode 100644 packages/evm-client/src/contract/types/CachedContract.ts create mode 100644 packages/evm-client/src/contract/types/Contract.ts create mode 100644 packages/evm-client/src/contract/types/Event.ts create mode 100644 packages/evm-client/src/contract/types/Function.ts create mode 100644 packages/evm-client/src/contract/utils/arrayToFriendly.test.ts create mode 100644 packages/evm-client/src/contract/utils/arrayToFriendly.ts create mode 100644 packages/evm-client/src/contract/utils/arrayToObject.test.ts create mode 100644 packages/evm-client/src/contract/utils/arrayToObject.ts create mode 100644 packages/evm-client/src/contract/utils/getAbiEntry.ts create mode 100644 packages/evm-client/src/contract/utils/objectToArray.test.ts create mode 100644 packages/evm-client/src/contract/utils/objectToArray.ts create mode 100644 packages/evm-client/src/errors/AbiEntryNotFound.ts create mode 100644 packages/evm-client/src/exports/cache.ts create mode 100644 packages/evm-client/src/exports/contract.ts create mode 100644 packages/evm-client/src/exports/errors.ts create mode 100644 packages/evm-client/src/exports/index.ts create mode 100644 packages/evm-client/src/exports/network.ts create mode 100644 packages/evm-client/src/exports/stubs.ts create mode 100644 packages/evm-client/src/network/stubs/NetworkStub.test.ts create mode 100644 packages/evm-client/src/network/stubs/NetworkStub.ts create mode 100644 packages/evm-client/src/network/types/Block.ts create mode 100644 packages/evm-client/src/network/types/Network.ts create mode 100644 packages/evm-client/src/network/types/Transaction.ts create mode 100644 packages/evm-client/tsconfig.json create mode 100644 packages/evm-client/tsup.config.ts create mode 100644 packages/evm-client/vite.config.ts diff --git a/packages/drift-ethers/src/stubs.ts b/packages/drift-ethers/src/stubs.ts deleted file mode 100644 index 4c9072d4..00000000 --- a/packages/drift-ethers/src/stubs.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@delvtech/drift/stubs"; diff --git a/packages/drift-viem/src/stubs.ts b/packages/drift-viem/src/stubs.ts deleted file mode 100644 index 4c9072d4..00000000 --- a/packages/drift-viem/src/stubs.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "@delvtech/drift/stubs"; diff --git a/packages/drift-ethers/.gitignore b/packages/evm-client-ethers/.gitignore similarity index 100% rename from packages/drift-ethers/.gitignore rename to packages/evm-client-ethers/.gitignore diff --git a/packages/drift-ethers/CHANGELOG.md b/packages/evm-client-ethers/CHANGELOG.md similarity index 100% rename from packages/drift-ethers/CHANGELOG.md rename to packages/evm-client-ethers/CHANGELOG.md diff --git a/packages/drift-ethers/README.md b/packages/evm-client-ethers/README.md similarity index 100% rename from packages/drift-ethers/README.md rename to packages/evm-client-ethers/README.md diff --git a/packages/drift-ethers/integration-tests/artifacts/CoreVoting.ts b/packages/evm-client-ethers/integration-tests/artifacts/CoreVoting.ts similarity index 100% rename from packages/drift-ethers/integration-tests/artifacts/CoreVoting.ts rename to packages/evm-client-ethers/integration-tests/artifacts/CoreVoting.ts diff --git a/packages/drift-ethers/integration-tests/createReadContract.test.ts b/packages/evm-client-ethers/integration-tests/createReadContract.test.ts similarity index 100% rename from packages/drift-ethers/integration-tests/createReadContract.test.ts rename to packages/evm-client-ethers/integration-tests/createReadContract.test.ts diff --git a/packages/drift-ethers/package.json b/packages/evm-client-ethers/package.json similarity index 90% rename from packages/drift-ethers/package.json rename to packages/evm-client-ethers/package.json index b1633565..6da7e0dc 100644 --- a/packages/drift-ethers/package.json +++ b/packages/evm-client-ethers/package.json @@ -1,6 +1,6 @@ { - "name": "@delvtech/drift-ethers", - "version": "0.0.0", + "name": "@delvtech/evm-client-ethers", + "version": "0.5.1", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -33,7 +33,7 @@ "ethers": "^6" }, "dependencies": { - "@delvtech/drift": "0.0.0" + "@delvtech/evm-client": "0.5.1" }, "devDependencies": { "@repo/typescript-config": "*", diff --git a/packages/drift-ethers/src/contract/createCachedReadContract.ts b/packages/evm-client-ethers/src/contract/createCachedReadContract.ts similarity index 95% rename from packages/drift-ethers/src/contract/createCachedReadContract.ts rename to packages/evm-client-ethers/src/contract/createCachedReadContract.ts index 4abc18f4..5e4d1e5e 100644 --- a/packages/drift-ethers/src/contract/createCachedReadContract.ts +++ b/packages/evm-client-ethers/src/contract/createCachedReadContract.ts @@ -2,7 +2,7 @@ import { type CachedReadContract, type SimpleCache, createCachedReadContract as baseFactory, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import type { Abi } from "abitype"; import { type CreateReadContractOptions, diff --git a/packages/drift-ethers/src/contract/createCachedReadWriteContract.ts b/packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts similarity index 96% rename from packages/drift-ethers/src/contract/createCachedReadWriteContract.ts rename to packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts index 006f0e46..2f13720b 100644 --- a/packages/drift-ethers/src/contract/createCachedReadWriteContract.ts +++ b/packages/evm-client-ethers/src/contract/createCachedReadWriteContract.ts @@ -2,7 +2,7 @@ import { type CachedReadWriteContract, type SimpleCache, createCachedReadWriteContract as baseFactory, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import type { Abi } from "abitype"; import { type ReadWriteContractOptions, diff --git a/packages/drift-ethers/src/contract/createReadContract.ts b/packages/evm-client-ethers/src/contract/createReadContract.ts similarity index 97% rename from packages/drift-ethers/src/contract/createReadContract.ts rename to packages/evm-client-ethers/src/contract/createReadContract.ts index 279ee7ab..058919c8 100644 --- a/packages/drift-ethers/src/contract/createReadContract.ts +++ b/packages/evm-client-ethers/src/contract/createReadContract.ts @@ -7,15 +7,9 @@ import { arrayToFriendly, arrayToObject, objectToArray, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import type { Abi } from "abitype"; -import { - Contract, - type EventLog, - type InterfaceAbi, - type Provider, - type Signer, -} from "ethers"; +import { Contract, type EventLog, type InterfaceAbi, type Provider, type Signer } from "ethers"; import { createReadWriteContract } from "src/contract/createReadWriteContract"; export interface CreateReadContractOptions { diff --git a/packages/drift-ethers/src/contract/createReadWriteContract.ts b/packages/evm-client-ethers/src/contract/createReadWriteContract.ts similarity index 92% rename from packages/drift-ethers/src/contract/createReadWriteContract.ts rename to packages/evm-client-ethers/src/contract/createReadWriteContract.ts index 9e90f4eb..3ad329b0 100644 --- a/packages/drift-ethers/src/contract/createReadWriteContract.ts +++ b/packages/evm-client-ethers/src/contract/createReadWriteContract.ts @@ -1,15 +1,10 @@ import { + objectToArray, type ReadContract, type ReadWriteContract, - objectToArray, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import type { Abi } from "abitype"; -import { - Contract, - type InterfaceAbi, - type Provider, - type Signer, -} from "ethers"; +import { Contract, type InterfaceAbi, type Provider, type Signer } from "ethers"; import { createReadContract } from "src/contract/createReadContract"; export interface ReadWriteContractOptions { diff --git a/packages/drift-ethers/src/index.ts b/packages/evm-client-ethers/src/index.ts similarity index 85% rename from packages/drift-ethers/src/index.ts rename to packages/evm-client-ethers/src/index.ts index 8a1f1db5..7775a6cd 100644 --- a/packages/drift-ethers/src/index.ts +++ b/packages/evm-client-ethers/src/index.ts @@ -21,14 +21,14 @@ export { export { createNetwork } from "src/network/createNetwork"; // Re-exports -export * from "@delvtech/drift/cache"; +export * from "@delvtech/evm-client/cache"; export { arrayToFriendly, arrayToObject, getAbiEntry, objectToArray, -} from "@delvtech/drift/contract"; +} from "@delvtech/evm-client/contract"; export type { AbiArrayType, AbiEntry, @@ -57,7 +57,7 @@ export type { FunctionReturn, ReadContract, ReadWriteContract, -} from "@delvtech/drift/contract"; +} from "@delvtech/evm-client/contract"; -export * from "@delvtech/drift/errors"; -export * from "@delvtech/drift/network"; +export * from "@delvtech/evm-client/errors"; +export * from "@delvtech/evm-client/network"; diff --git a/packages/drift-ethers/src/network/createNetwork.ts b/packages/evm-client-ethers/src/network/createNetwork.ts similarity index 98% rename from packages/drift-ethers/src/network/createNetwork.ts rename to packages/evm-client-ethers/src/network/createNetwork.ts index 91e0ad0e..2c51c904 100644 --- a/packages/drift-ethers/src/network/createNetwork.ts +++ b/packages/evm-client-ethers/src/network/createNetwork.ts @@ -1,4 +1,4 @@ -import type { Network } from "@delvtech/drift"; +import type { Network } from "@delvtech/evm-client"; import type { Provider } from "ethers"; export function createNetwork(provider: Provider): Network { diff --git a/packages/evm-client-ethers/src/stubs.ts b/packages/evm-client-ethers/src/stubs.ts new file mode 100644 index 00000000..363f08fb --- /dev/null +++ b/packages/evm-client-ethers/src/stubs.ts @@ -0,0 +1 @@ +export * from "@delvtech/evm-client/stubs"; diff --git a/packages/drift-ethers/tsconfig.json b/packages/evm-client-ethers/tsconfig.json similarity index 100% rename from packages/drift-ethers/tsconfig.json rename to packages/evm-client-ethers/tsconfig.json diff --git a/packages/drift-ethers/tsup.config.ts b/packages/evm-client-ethers/tsup.config.ts similarity index 100% rename from packages/drift-ethers/tsup.config.ts rename to packages/evm-client-ethers/tsup.config.ts diff --git a/packages/drift-ethers/vite.config.ts b/packages/evm-client-ethers/vite.config.ts similarity index 100% rename from packages/drift-ethers/vite.config.ts rename to packages/evm-client-ethers/vite.config.ts diff --git a/packages/drift-viem/.gitignore b/packages/evm-client-viem/.gitignore similarity index 100% rename from packages/drift-viem/.gitignore rename to packages/evm-client-viem/.gitignore diff --git a/packages/drift-viem/CHANGELOG.md b/packages/evm-client-viem/CHANGELOG.md similarity index 100% rename from packages/drift-viem/CHANGELOG.md rename to packages/evm-client-viem/CHANGELOG.md diff --git a/packages/drift-viem/README.md b/packages/evm-client-viem/README.md similarity index 100% rename from packages/drift-viem/README.md rename to packages/evm-client-viem/README.md diff --git a/packages/drift-viem/integration-tests/artifacts/CoreVoting.ts b/packages/evm-client-viem/integration-tests/artifacts/CoreVoting.ts similarity index 100% rename from packages/drift-viem/integration-tests/artifacts/CoreVoting.ts rename to packages/evm-client-viem/integration-tests/artifacts/CoreVoting.ts diff --git a/packages/drift-viem/integration-tests/createReadContract.test.ts b/packages/evm-client-viem/integration-tests/createReadContract.test.ts similarity index 100% rename from packages/drift-viem/integration-tests/createReadContract.test.ts rename to packages/evm-client-viem/integration-tests/createReadContract.test.ts diff --git a/packages/drift-viem/package.json b/packages/evm-client-viem/package.json similarity index 92% rename from packages/drift-viem/package.json rename to packages/evm-client-viem/package.json index 938527ef..600ce549 100644 --- a/packages/drift-viem/package.json +++ b/packages/evm-client-viem/package.json @@ -1,6 +1,6 @@ { - "name": "@delvtech/drift-viem", - "version": "0.0.0", + "name": "@delvtech/evm-client-viem", + "version": "0.6.3", "license": "MIT", "type": "module", "main": "dist/index.js", @@ -39,7 +39,7 @@ } }, "dependencies": { - "@delvtech/drift": "0.0.0" + "@delvtech/evm-client": "0.5.1" }, "devDependencies": { "@repo/typescript-config": "*", diff --git a/packages/drift-viem/src/contract/createCachedReadContract.ts b/packages/evm-client-viem/src/contract/createCachedReadContract.ts similarity index 96% rename from packages/drift-viem/src/contract/createCachedReadContract.ts rename to packages/evm-client-viem/src/contract/createCachedReadContract.ts index 04b8b98b..2fba9c7e 100644 --- a/packages/drift-viem/src/contract/createCachedReadContract.ts +++ b/packages/evm-client-viem/src/contract/createCachedReadContract.ts @@ -2,7 +2,7 @@ import { type CachedReadContract, type SimpleCache, createCachedReadContract as baseFactory, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import { type CreateReadContractOptions, createReadContract, diff --git a/packages/drift-viem/src/contract/createCachedReadWriteContract.ts b/packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts similarity index 96% rename from packages/drift-viem/src/contract/createCachedReadWriteContract.ts rename to packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts index 7ada2f65..5da447d5 100644 --- a/packages/drift-viem/src/contract/createCachedReadWriteContract.ts +++ b/packages/evm-client-viem/src/contract/createCachedReadWriteContract.ts @@ -2,7 +2,7 @@ import { type CachedReadWriteContract, type SimpleCache, createCachedReadWriteContract as baseFactory, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import { type ReadWriteContractOptions, createReadWriteContract, diff --git a/packages/drift-viem/src/contract/createReadContract.ts b/packages/evm-client-viem/src/contract/createReadContract.ts similarity index 99% rename from packages/drift-viem/src/contract/createReadContract.ts rename to packages/evm-client-viem/src/contract/createReadContract.ts index d05237f9..6cd0b3e8 100644 --- a/packages/drift-viem/src/contract/createReadContract.ts +++ b/packages/evm-client-viem/src/contract/createReadContract.ts @@ -7,7 +7,7 @@ import { type ReadWriteContract, arrayToObject, objectToArray, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import { createSimulateContractParameters } from "src/contract/utils/createSimulateContractParameters"; import { type Abi, diff --git a/packages/drift-viem/src/contract/createReadWriteContract.ts b/packages/evm-client-viem/src/contract/createReadWriteContract.ts similarity index 98% rename from packages/drift-viem/src/contract/createReadWriteContract.ts rename to packages/evm-client-viem/src/contract/createReadWriteContract.ts index 31abe18d..03c69efc 100644 --- a/packages/drift-viem/src/contract/createReadWriteContract.ts +++ b/packages/evm-client-viem/src/contract/createReadWriteContract.ts @@ -1,11 +1,11 @@ import { + objectToArray, type ReadContract, type ReadWriteContract, - objectToArray, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import { - type CreateReadContractOptions, createReadContract, + type CreateReadContractOptions, } from "src/contract/createReadContract"; import { createSimulateContractParameters } from "src/contract/utils/createSimulateContractParameters"; import type { Abi, WalletClient } from "viem"; diff --git a/packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts b/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts similarity index 85% rename from packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts rename to packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts index d18b736b..3ae91c95 100644 --- a/packages/drift-viem/src/contract/utils/createSimulateContractParameters.ts +++ b/packages/evm-client-viem/src/contract/utils/createSimulateContractParameters.ts @@ -1,4 +1,4 @@ -import type { ContractWriteOptions } from "@delvtech/drift"; +import type { ContractWriteOptions } from "@delvtech/evm-client"; /** * Get parameters for `simulateContract` from `ContractWriteOptions` @@ -40,5 +40,6 @@ type SimulateContractParameters = { value?: bigint; } & ( | { gasPrice?: bigint } - | { maxFeePerGas?: bigint; maxPriorityFeePerGas?: bigint } + | { maxFeePerGas?: bigint } + | { maxPriorityFeePerGas?: bigint } ); diff --git a/packages/drift-viem/src/contract/utils/outputToFriendly.ts b/packages/evm-client-viem/src/contract/utils/outputToFriendly.ts similarity index 96% rename from packages/drift-viem/src/contract/utils/outputToFriendly.ts rename to packages/evm-client-viem/src/contract/utils/outputToFriendly.ts index 4a2d5a73..ea06b066 100644 --- a/packages/drift-viem/src/contract/utils/outputToFriendly.ts +++ b/packages/evm-client-viem/src/contract/utils/outputToFriendly.ts @@ -2,7 +2,7 @@ import { type FunctionReturn, arrayToFriendly, getAbiEntry, -} from "@delvtech/drift"; +} from "@delvtech/evm-client"; import type { Abi } from "viem"; export function outputToFriendly({ diff --git a/packages/drift-viem/src/index.ts b/packages/evm-client-viem/src/index.ts similarity index 85% rename from packages/drift-viem/src/index.ts rename to packages/evm-client-viem/src/index.ts index 3c898f90..27bb04bc 100644 --- a/packages/drift-viem/src/index.ts +++ b/packages/evm-client-viem/src/index.ts @@ -21,14 +21,14 @@ export { export { createNetwork } from "src/network/createNetwork"; // Re-exports -export * from "@delvtech/drift/cache"; +export * from "@delvtech/evm-client/cache"; export { arrayToFriendly, arrayToObject, getAbiEntry, objectToArray, -} from "@delvtech/drift/contract"; +} from "@delvtech/evm-client/contract"; export type { AbiArrayType, AbiEntry, @@ -57,7 +57,7 @@ export type { FunctionReturn, ReadContract, ReadWriteContract, -} from "@delvtech/drift/contract"; +} from "@delvtech/evm-client/contract"; -export * from "@delvtech/drift/errors"; -export * from "@delvtech/drift/network"; +export * from "@delvtech/evm-client/errors"; +export * from "@delvtech/evm-client/network"; diff --git a/packages/drift-viem/src/network/createNetwork.ts b/packages/evm-client-viem/src/network/createNetwork.ts similarity index 97% rename from packages/drift-viem/src/network/createNetwork.ts rename to packages/evm-client-viem/src/network/createNetwork.ts index 850f392a..672fc396 100644 --- a/packages/drift-viem/src/network/createNetwork.ts +++ b/packages/evm-client-viem/src/network/createNetwork.ts @@ -1,4 +1,4 @@ -import type { Network } from "@delvtech/drift"; +import type { Network } from "@delvtech/evm-client"; import { type GetBalanceParameters, type PublicClient, diff --git a/packages/evm-client-viem/src/stubs.ts b/packages/evm-client-viem/src/stubs.ts new file mode 100644 index 00000000..363f08fb --- /dev/null +++ b/packages/evm-client-viem/src/stubs.ts @@ -0,0 +1 @@ +export * from "@delvtech/evm-client/stubs"; diff --git a/packages/drift-viem/tsconfig.json b/packages/evm-client-viem/tsconfig.json similarity index 100% rename from packages/drift-viem/tsconfig.json rename to packages/evm-client-viem/tsconfig.json diff --git a/packages/drift-viem/tsup.config.ts b/packages/evm-client-viem/tsup.config.ts similarity index 100% rename from packages/drift-viem/tsup.config.ts rename to packages/evm-client-viem/tsup.config.ts diff --git a/packages/drift-viem/vite.config.ts b/packages/evm-client-viem/vite.config.ts similarity index 100% rename from packages/drift-viem/vite.config.ts rename to packages/evm-client-viem/vite.config.ts diff --git a/packages/evm-client/.gitignore b/packages/evm-client/.gitignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/packages/evm-client/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/packages/evm-client/CHANGELOG.md b/packages/evm-client/CHANGELOG.md new file mode 100644 index 00000000..709a38a1 --- /dev/null +++ b/packages/evm-client/CHANGELOG.md @@ -0,0 +1,157 @@ +# @delvtech/evm-client + +## 0.5.1 + +### Patch Changes + +- 66d9dc3: Added `ConstructorArgs` type + +## 0.5.0 + +### Minor Changes + +- 919f525: Renamed `deleteReadMatch` to `deleteReadsMatching` + +### Patch Changes + +- 93531e6: Added getChainId method to the Network interface. + +## 0.4.2 + +### Patch Changes + +- 52edea9: Add deleteReadMatch method to CachedReadContract + +## 0.4.1 + +### Patch Changes + +- 4609a60: Export TransactionReceipt type + +## 0.4.0 + +### Minor Changes + +- 51fd2e4: Add status field to Transaction + +## 0.3.1 + +### Patch Changes + +- 5c35487: Fix error with `NamedEventInput` type which was broken and causing broken downstream types such as `EventFilter` + +## 0.3.0 + +### Minor Changes + +- 91106f8: Add a getBalance method to the Network interface for fetching native currency balances (e.g. ETH) + +## 0.2.4 + +### Patch Changes + +- 322edf5: Remove onTransactionMined + +## 0.2.3 + +### Patch Changes + +- e3880c8: Add txHash to onTransactionMined + +## 0.2.2 + +### Patch Changes + +- 01ec0b1: Add onTransactionMined callback to ReadWriteContract +- 01ec0b1: Make onTransactionMined optional + +## 0.2.1 + +### Patch Changes + +- 5f6a374: Add `waitForTransaction` method to `Network` type. + +## 0.2.0 + +### Minor Changes + +- affd95f: Add `entries` property to the `SimpleCache` type. + +## 0.1.1 + +### Patch Changes + +- eb6575b: Added a `cache` property to the `CachedReadContract` type and ensured the factories preserve the prototypes of the contract's they're given. + +## 0.1.0 + +### Minor Changes + +- cc17b3c: Changed the type of all inputs to objects. This means that functions with a single argument (e.g., `balanceOf` will now expect ``{ owner: `0x${string}` }``, not `` `0x${string}` ``). Outputs remain the "Friendly" type which deconstructs to a single primitive type for single outputs values (e.g., `symbol` will return a `string`, not `{ "0": string }`) since many single output return values are unnamed + +## 0.0.11 + +### Patch Changes + +- 1098f69: Fix bug causing stub lookups to fail + +## 0.0.10 + +### Patch Changes + +- 5cf2921: Add ability to stub options to stubRead + +## 0.0.9 + +### Patch Changes + +- dd129be: Use stable stringify for stub keys + +## 0.0.8 + +### Patch Changes + +- 593a286: Add ability to stub events for dynamic filter args + +## 0.0.7 + +### Patch Changes + +- 9db4f0f: Fix argument handling for parameters that have empty strings as names + +## 0.0.6 + +### Patch Changes + +- a2edb5a: Fix handling of single params + +## 0.0.5 + +### Patch Changes + +- 6307e1b: Fix the last fix... + +## 0.0.4 + +### Patch Changes + +- cfdf0db: Fix param prep for contract calls with single params + +## 0.0.3 + +### Patch Changes + +- 76b1bc8: Fix type resolutions by adding a `typeVersions` field to the `package.json`s +- fdcc9ef: Modify exports + +## 0.0.2 + +### Patch Changes + +- 6d60418: Added NetworkGetBlockOptions type + +## 0.0.1 + +### Patch Changes + +- e2f697f: Initial release! πŸš€ diff --git a/packages/evm-client/README.md b/packages/evm-client/README.md new file mode 100644 index 00000000..c9002279 --- /dev/null +++ b/packages/evm-client/README.md @@ -0,0 +1,129 @@ +# @delvtech/evm-client + +Useful EVM client abstractions for TypeScript projects that want to remain web3 +library agnostic. + +```ts +import { + CachedReadWriteContract, + ContractReadOptions, +} from '@delvtech/evm-client'; +import erc20Abi from './abis/erc20Abi.json'; + +type CachedErc20Contract = CachedReadWriteContract; + +async function approve( + contract: CachedErc20Contract, + spender: `0x${string}`, + amount: bigint, +) { + const hash = await contract.write('approve', { spender, amount }); + + this.contract.deleteRead('allowance', { + owner: await contract.getSignerAddress(), + spender, + }); + + return hash; +} +``` + +This project contains types that can be used in TypeScript projects that need to +interact with contracts in a type-safe way based on ABIs. It allows your project +to focus on core contract logic and remain flexible in it's implementation. To +aid in implementation, this project provides: + +- Utility types +- Utility functions for transforming arguments +- Factories for wrapping contract instances with caching logic +- Stubs to facilitate testing + +## Primary Abstractions + +### Contracts + +The contract abstraction lets you write type-safe contract interactions that can +be implemented in multiple web3 libraries and even multiple persistence layers. +The API is meant to be easy to both read and write. + +#### Types + +- **[`ReadContract`](./src/contract/types/Contract.ts):** A basic contract that + can be used to fetch data, but can't submit transactions. +- **[`ReadWriteContract`](./src/contract/types/Contract.ts):** An extended + `ReadContract` that has a signer attached to it and can be used to submit + transactions. +- **[`CachedReadContract`](./src/contract/types/CachedContract.ts):** An + extended `ReadContract` that will cache reads and event queries based on + arguments with a few additional methods for interacting with the cache. +- **[`CachedReadWriteContract`](./src/contract/types/CachedContract.ts):** An + extended `CachedReadContract` that has a signer attached to it and can be used + to submit transactions. + +#### Utils + +- **[`objectToArray`](./src/contract/utils/friendlyToArray.ts):** A function + that takes an object of inputs (function and event arguments) and converts it + into an array, ensuring parameters are properly ordered and the correct number + of parameters are present. + +- **[`arrayToObject`](./src/contract/utils/arrayToFriendly.ts):** The opposite + of `objectToArray`. A function to transform contract input and output arrays + into objects. + +- **[`arrayToFriendly`](./src/contract/utils/arrayToFriendly.ts):** A function + to transform contract output arrays into "Friendly" types. The friendly type + of an output array depends on the number of output parameters: + + - Multiple parameters: An object with the argument names as keys (or their + index if no name is found in the ABI) and the primitive type of the + parameters as values. + - Single parameters: The primitive type of the single parameter. + - No parameters: `undefined` + +#### Factories + +- **[`createCachedReadContract`](./src/contract/factories/createCachedReadContract.ts):** + A factory that turns a `ReadContract` into a `CachedReadContract`. +- **[`createCachedReadWriteContract`](./src/contract/factories/createCachedReadWriteContract.ts):** + A factory that turns a `ReadWriteContract` into a `CachedReadWriteContract`. + +#### Stubs + +- **[`ReadContractStub`](./src/contract/stubs/ReadContractStub.ts):** A stub of + a `ReadContract` for use in tests. +- **[`ReadWriteContractStub`](./src/contract/stubs/ReadWriteContractStub.ts):** + A stub of a `ReadWriteContract` for use in tests. + +### Network + +The `Network` abstraction provides a small interface for fetching vital network +information like blocks and transactions. + +#### Types + +- **[`Network`](./src/network/types/Network.ts)** + +#### Stubs + +- **[`NetworkStub`](./src/network/stubs/NetworkStub.ts):** A stub of a `Network` + for use in tests. + +### SimpleCache + +A simple cache abstraction providing a minimal interface for facilitating +contract caching. + +#### Types + +- **[`SimpleCache`](./src/cache/types/SimpleCache.ts)** + +#### Utils + +- **[`createSimpleCacheKey`](./src/cache/utils/createSimpleCacheKey.ts):** + Creates a consistent serializable cache key from basic types. + +#### Factories + +- **[`createLruSimpleCache`](./src/cache/factories/createLruSimpleCache.ts):** + Creates a `SimpleCache` instance using an LRU cache. diff --git a/packages/evm-client/package.json b/packages/evm-client/package.json new file mode 100644 index 00000000..5ab65ef5 --- /dev/null +++ b/packages/evm-client/package.json @@ -0,0 +1,81 @@ +{ + "name": "@delvtech/evm-client", + "version": "0.5.1", + "license": "MIT", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./cache": { + "types": "./dist/cache.d.ts", + "default": "./dist/cache.js" + }, + "./contract": { + "types": "./dist/contract.d.ts", + "default": "./dist/contract.js" + }, + "./errors": { + "types": "./dist/errors.d.ts", + "default": "./dist/errors.js" + }, + "./network": { + "types": "./dist/network.d.ts", + "default": "./dist/network.js" + }, + "./stubs": { + "types": "./dist/stubs.d.ts", + "default": "./dist/stubs.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "cache": ["./dist/cache.d.ts"], + "contract": ["./dist/contract.d.ts"], + "errors": ["./dist/errors.d.ts"], + "network": ["./dist/network.d.ts"], + "stubs": ["./dist/stubs.d.ts"] + } + }, + "scripts": { + "build": "tsup", + "test:watch": "vitest --reporter=verbose", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "watch": "tsup --watch" + }, + "peerDependencies": { + "sinon": "^17.0.1" + }, + "peerDependenciesMeta": { + "sinon": { + "optional": true + } + }, + "dependencies": { + "@types/lodash.ismatch": "^4.4.9", + "fast-safe-stringify": "^2.1.1", + "lodash.ismatch": "^4.4.0", + "lru-cache": "^10.0.1" + }, + "devDependencies": { + "@repo/typescript-config": "*", + "@types/sinon": "^17.0.3", + "abitype": "^1.0.0", + "dotenv": "^16.4.2", + "fast-json-stable-stringify": "^2.1.0", + "sinon": "^17.0.1", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "tsup": "^8.0.2", + "typescript": "^5.4.5", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.2" + }, + "publishConfig": { + "access": "public" + }, + "files": ["dist"] +} diff --git a/packages/evm-client/src/base/testing/IERC20.ts b/packages/evm-client/src/base/testing/IERC20.ts new file mode 100644 index 00000000..ba428143 --- /dev/null +++ b/packages/evm-client/src/base/testing/IERC20.ts @@ -0,0 +1,224 @@ +export const IERC20 = { + abi: [ + { + constant: true, + inputs: [], + name: "name", + outputs: [ + { + name: "", + type: "string", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + ], + name: "approve", + outputs: [ + { + name: "", + type: "bool", + }, + ], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "totalSupply", + outputs: [ + { + name: "", + type: "uint256", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { + name: "from", + type: "address", + }, + { + name: "to", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + ], + name: "transferFrom", + outputs: [ + { + name: "", + type: "bool", + }, + ], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [], + name: "decimals", + outputs: [ + { + name: "", + type: "uint8", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [ + { + name: "owner", + type: "address", + }, + ], + name: "balanceOf", + outputs: [ + { + name: "balance", + type: "uint256", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: true, + inputs: [], + name: "symbol", + outputs: [ + { + name: "", + type: "string", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + constant: false, + inputs: [ + { + name: "to", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + ], + name: "transfer", + outputs: [ + { + name: "", + type: "bool", + }, + ], + payable: false, + stateMutability: "nonpayable", + type: "function", + }, + { + constant: true, + inputs: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + ], + name: "allowance", + outputs: [ + { + name: "", + type: "uint256", + }, + ], + payable: false, + stateMutability: "view", + type: "function", + }, + { + payable: true, + stateMutability: "payable", + type: "fallback", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: "owner", + type: "address", + }, + { + indexed: true, + name: "spender", + type: "address", + }, + { + indexed: false, + name: "value", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + name: "from", + type: "address", + }, + { + indexed: true, + name: "to", + type: "address", + }, + { + indexed: false, + name: "value", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + ], +} as const; diff --git a/packages/evm-client/src/base/testing/accounts.ts b/packages/evm-client/src/base/testing/accounts.ts new file mode 100644 index 00000000..f04008ee --- /dev/null +++ b/packages/evm-client/src/base/testing/accounts.ts @@ -0,0 +1,3 @@ +export const BOB = "0xBob"; +export const ALICE = "0xAlice"; +export const NANCY = "0xNancy"; diff --git a/packages/evm-client/src/base/types.ts b/packages/evm-client/src/base/types.ts new file mode 100644 index 00000000..31da0da4 --- /dev/null +++ b/packages/evm-client/src/base/types.ts @@ -0,0 +1,7 @@ +export type EmptyObject = Record; + +/** + * Combines members of an intersection into a readable type. + * @see https://twitter.com/mattpocockuk/status/1622730173446557697?s=20&t=NdpAcmEFXY01xkqU3KO0Mg + */ +export type Prettify = { [K in keyof T]: T[K] } & unknown; diff --git a/packages/evm-client/src/cache/factories/createLruSimpleCache.ts b/packages/evm-client/src/cache/factories/createLruSimpleCache.ts new file mode 100644 index 00000000..70db363d --- /dev/null +++ b/packages/evm-client/src/cache/factories/createLruSimpleCache.ts @@ -0,0 +1,57 @@ +import stringify from "fast-json-stable-stringify"; +import { LRUCache } from "lru-cache"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; + +/** + * An LRU (Least Recently Used) implementation of the `SimpleCache` interface. + * This class wraps around the + * [lru-cache](https://www.npmjs.com/package/lru-cache) library to provide LRU + * caching capabilities conforming to the `SimpleCache` interface. + * + * @template TValue - The type of value to be stored in the cache. + * @template TKey - The type of key used to access values in the cache. + * @hidden + */ +export function createLruSimpleCache< + TValue extends NonNullable = NonNullable, + TKey extends SimpleCacheKey = SimpleCacheKey, +>(options: LRUCache.Options): SimpleCache { + const cache = new LRUCache(options); + + function* entriesGenerator( + originalGenerator: Generator<[TKey, TValue]>, + ): Generator<[TKey, TValue]> { + for (const [key, value] of originalGenerator) { + // Modify the entry here before yielding it + const modifiedEntry = [JSON.parse(key as string), value]; + yield modifiedEntry as [TKey, TValue]; + } + } + + return { + get entries() { + // Keys need to be returned in the same format as they were given to the cache + return entriesGenerator(cache.entries() as Generator<[TKey, TValue]>); + }, + + get(key) { + return cache.get(stringify(key)); + }, + + set(key, value) { + cache.set(stringify(key), value); + }, + + delete(key) { + return cache.delete(stringify(key)); + }, + + clear() { + cache.clear(); + }, + + find(predicate) { + return cache.find((value, key) => predicate(value, JSON.parse(key))); + }, + }; +} diff --git a/packages/evm-client/src/cache/types/SimpleCache.ts b/packages/evm-client/src/cache/types/SimpleCache.ts new file mode 100644 index 00000000..001d6d04 --- /dev/null +++ b/packages/evm-client/src/cache/types/SimpleCache.ts @@ -0,0 +1,64 @@ +/** + * Represents a simple caching mechanism with basic operations such as + * get, set, delete, clear, and find. + * + * @template TValue - The type of value to be stored in the cache. + * @template TKey - The type of key used to access values in the cache. + * Must be a serializable value to ensure consistency and predictability. + */ +export interface SimpleCache< + TValue = any, + TKey extends SimpleCacheKey = SimpleCacheKey, +> { + /** + * Returns an iterable of key-value pairs for every entry in the cache. + */ + readonly entries: Iterable<[TKey, TValue]>; + + /** + * Retrieves the value associated with the specified key. + */ + get: (key: TKey) => TValue | undefined; + + /** + * Associates the specified value with the specified key in the cache. If the + * cache previously contained a mapping for the key, the old value is + * replaced. + */ + set: (key: TKey, value: TValue) => void; + + /** + * Removes the mapping for the specified key from this cache if present. + */ + delete: (key: TKey) => void; + + /** + * Removes all of the mappings from this cache. + */ + clear: () => void; + + /** + * Returns the the first value from the cache that the specified predicate + * matches, or undefined if no match is found. + * + * @param predicate - A function to test each key-value pair in the cache. + */ + find: ( + predicate: (value: TValue, key: TKey) => boolean, + ) => TValue | undefined; +} + +/** + * Represents possible serializable key types for the SimpleCache. Can be a + * primitive (string, number, boolean), an array of SimpleCache (with possible + * null/undefined values), or a record with string keys and SimpleCache values. + */ +export type SimpleCacheKey = + | KeyPrimitive + | (SimpleCacheKey | null | undefined)[] + | { + [key: string]: SimpleCacheKey; + }; + +/** Primitive types that can be used as part of a cache key. */ +type KeyPrimitive = string | number | boolean; diff --git a/packages/evm-client/src/cache/utils/createSimpleCacheKey.ts b/packages/evm-client/src/cache/utils/createSimpleCacheKey.ts new file mode 100644 index 00000000..76a57bec --- /dev/null +++ b/packages/evm-client/src/cache/utils/createSimpleCacheKey.ts @@ -0,0 +1,63 @@ +import type { SimpleCacheKey } from "src/cache/types/SimpleCache"; + +type DefinedValue = NonNullable< + Record | string | number | boolean | symbol +>; + +/** + * Converts a given raw key into a `SimpleCacheKey``. + * + * The method ensures that any given raw key, regardless of its structure, is + * converted into a format suitable for consistent cache key referencing. + * + * - For scalar (string, number, boolean), it returns them directly. + * - For arrays, it recursively processes each element. + * - For objects, it sorts the keys and then recursively processes each value, + * ensuring consistent key generation. + * - For other types, it attempts to convert the raw key to a string. + * + * @param rawKey - The raw input to be converted into a cache key. + * @returns A standardized cache key suitable for consistent referencing within + * the cache. + */ +export function createSimpleCacheKey(rawKey: DefinedValue): SimpleCacheKey { + switch (typeof rawKey) { + case "string": + case "number": + case "boolean": + return rawKey; + + case "object": { + if (Array.isArray(rawKey)) { + return rawKey.map((value) => + // undefined or null values are converted to null to follow the + // precedent set by JSON.stringify + value === undefined || value === null + ? null + : createSimpleCacheKey(value), + ); + } + + const processedObject: Record = {}; + + // sort keys to ensure consistent key generation + for (const key of Object.keys(rawKey).sort()) { + const value = rawKey[key]; + + // ignore properties with undefined or null values + if (value !== undefined && value !== null) { + processedObject[key] = createSimpleCacheKey(value); + } + } + + return processedObject; + } + + default: + try { + return rawKey.toString(); + } catch (err) { + throw new Error(`Unable to process cache key value: ${String(rawKey)}`); + } + } +} diff --git a/packages/evm-client/src/contract/factories/createCachedReadContract.test.ts b/packages/evm-client/src/contract/factories/createCachedReadContract.test.ts new file mode 100644 index 00000000..06e26808 --- /dev/null +++ b/packages/evm-client/src/contract/factories/createCachedReadContract.test.ts @@ -0,0 +1,174 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { ALICE, BOB } from "src/base/testing/accounts"; +import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +import type { Event } from "src/contract/types/Event"; +import { describe, expect, it } from "vitest"; +import { createCachedReadContract } from "./createCachedReadContract"; + +const ERC20ABI = IERC20.abi; + +describe("createCachedReadContract", () => { + it("caches the read function", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + const stubbedValue = "0x123abc"; + contract.stubRead({ + functionName: "name", + value: stubbedValue, + }); + + const value = await cachedContract.read("name"); + expect(value).toBe(stubbedValue); + + const value2 = await cachedContract.read("name"); + expect(value2).toBe(stubbedValue); + + const stub = contract.getReadStub("name"); + expect(stub?.callCount).toBe(1); + }); + + it("caches the getEvents function", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + const stubbedEvents: Event[] = [ + { + eventName: "Transfer", + args: { + from: ALICE, + to: BOB, + value: 100n, + }, + blockNumber: 1n, + data: "0x123abc", + transactionHash: "0x123abc", + }, + ]; + contract.stubEvents("Transfer", undefined, stubbedEvents); + + const events = await cachedContract.getEvents("Transfer"); + expect(events).toBe(stubbedEvents); + + const events2 = await cachedContract.getEvents("Transfer"); + expect(events2).toBe(stubbedEvents); + + const stub = contract.getEventsStub("Transfer"); + expect(stub?.callCount).toBe(1); + }); + + it("deletes cached reads", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + const stubbedValue = 100n; + contract.stubRead({ functionName: "balanceOf", value: stubbedValue }); + + const value = await cachedContract.read("balanceOf", { owner: "0x123abc" }); + expect(value).toBe(stubbedValue); + + cachedContract.deleteRead("balanceOf", { owner: "0x123abc" }); + + const value2 = await cachedContract.read("balanceOf", { + owner: "0x123abc", + }); + expect(value2).toBe(stubbedValue); + + const stub = contract.getReadStub("balanceOf"); + expect(stub?.callCount).toBe(2); + }); + + it("deletes cached reads from function name only", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + contract.stubRead({ + functionName: "balanceOf", + value: 100n, + args: { owner: ALICE }, + }); + contract.stubRead({ + functionName: "balanceOf", + value: 200n, + args: { owner: BOB }, + }); + + // Get both alice and bob's balance + const aliceValue = await cachedContract.read("balanceOf", { owner: ALICE }); + expect(aliceValue).toBe(100n); + + const bobValue = await cachedContract.read("balanceOf", { owner: BOB }); + expect(bobValue).toBe(200n); + + // Deleting anything that matches a balanceOf call + cachedContract.deleteReadsMatching("balanceOf"); + + // Request bob and alice's balance again + const aliceValue2 = await cachedContract.read("balanceOf", { + owner: ALICE, + }); + expect(aliceValue2).toBe(100n); + const bobValue2 = await cachedContract.read("balanceOf", { owner: BOB }); + expect(bobValue2).toBe(200n); + + const stub = contract.getReadStub("balanceOf"); + expect(stub?.callCount).toBe(4); + }); + + it("deletes cached reads with partial args", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + const aliceArgs = { owner: ALICE, spender: BOB } as const; + contract.stubRead({ + functionName: "allowance", + value: 100n, + args: aliceArgs, + }); + + const bobArgs = { owner: BOB, spender: ALICE } as const; + contract.stubRead({ + functionName: "allowance", + value: 200n, + args: bobArgs, + }); + + // Get both alice and bob's allowance + await cachedContract.read("allowance", aliceArgs); + await cachedContract.read("allowance", bobArgs); + + // Deleting any allowance calls where BOB is the spender + cachedContract.deleteReadsMatching("allowance", { spender: BOB }); + + // Request bob and alice's allowance again + await cachedContract.read("allowance", aliceArgs); + await cachedContract.read("allowance", bobArgs); + + const stub = contract.getReadStub("allowance"); + expect(stub?.callCount).toBe(3); + }); + + it("clears the cache", async () => { + const contract = new ReadContractStub(ERC20ABI); + const cachedContract = createCachedReadContract({ contract }); + + contract.stubRead({ functionName: "balanceOf", value: 100n }); + contract.stubRead({ + functionName: "name", + value: "Base Token", + }); + + await cachedContract.read("balanceOf", { owner: "0x123abc" }); + await cachedContract.read("name"); + + cachedContract.clearCache(); + + await cachedContract.read("balanceOf", { owner: "0x123abc" }); + await cachedContract.read("name"); + + const stubA = contract.getReadStub("balanceOf"); + const stubB = contract.getReadStub("name"); + expect(stubA?.callCount).toBe(2); + expect(stubB?.callCount).toBe(2); + }); +}); diff --git a/packages/evm-client/src/contract/factories/createCachedReadContract.ts b/packages/evm-client/src/contract/factories/createCachedReadContract.ts new file mode 100644 index 00000000..ae51e5e9 --- /dev/null +++ b/packages/evm-client/src/contract/factories/createCachedReadContract.ts @@ -0,0 +1,168 @@ +import type { Abi } from "abitype"; +import isMatch from "lodash.ismatch"; +import { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; +import type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; +import { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; +import type { CachedReadContract } from "src/contract/types/CachedContract"; +import type { ReadContract } from "src/contract/types/Contract"; + +// TODO: Figure out a good default cache size +const DEFAULT_CACHE_SIZE = 100; + +export interface CreateCachedReadContractOptions { + contract: ReadContract; + cache?: SimpleCache; + /** + * A namespace to distinguish this instance from others in the cache by + * prefixing all cache keys. + */ + namespace?: string; +} + +/** + * A wrapped Ethereum contract reader that provides caching capabilities. Useful + * for reducing the number of actual reads from a contract by caching and + * reusing previous read results. + * + * @example + * const cachedContract = new CachedReadContract({ contract: myContract }); + * const result1 = await cachedContract.read("functionName", args); + * const result2 = await cachedContract.read("functionName", args); // Fetched from cache + */ +export function createCachedReadContract({ + contract, + cache = createLruSimpleCache({ max: DEFAULT_CACHE_SIZE }), + namespace, +}: CreateCachedReadContractOptions): CachedReadContract { + // Because this is part of the public API, we won't know if the original + // contract is a plain object or a class instance, so we use Object.create to + // preserve the original contract's prototype chain when extending, ensuring + // the new contract includes all the original contract's methods and + // instanceof checks will still work. + const contractPrototype = Object.getPrototypeOf(contract); + const newContract = Object.create(contractPrototype); + + const overrides: Partial> = { + cache, + + /** + * Reads data from the contract. First checks the cache, and if not present, + * fetches from the contract and then caches the result. + */ + async read(functionName, args, options) { + return getOrSet({ + cache, + key: createSimpleCacheKey([ + namespace, + "read", + { + address: contract.address, + functionName, + args, + options, + }, + ]), + callback: () => contract.read(functionName, args, options), + }); + }, + + /** + * Deletes a specific read from the cache. + * + * @example + * const cachedContract = new CachedReadContract({ contract: myContract }); + * const result1 = await cachedContract.read("functionName", args); + * const result2 = await cachedContract.read("functionName", args); // Fetched from cache + * + * cachedContract.deleteRead("functionName", args); + * const result3 = await cachedContract.read("functionName", args); // Fetched from contract + */ + deleteRead(functionName, args, options) { + const key = createSimpleCacheKey([ + namespace, + "read", + { + address: contract.address, + functionName, + args, + options, + }, + ]); + + cache.delete(key); + }, + + deleteReadsMatching(...args) { + const [functionName, functionArgs, options] = args; + + const sourceKey = createSimpleCacheKey([ + namespace, + "read", + { + address: contract.address, + functionName, + args: functionArgs, + options, + }, + ]); + + for (const [key] of cache.entries) { + if ( + typeof key === "object" && + isMatch(key, sourceKey as SimpleCacheKey[]) + ) { + cache.delete(key); + } + } + }, + + /** + * Gets events from the contract. First checks the cache, and if not present, + * fetches from the contract and then caches the result. + */ + async getEvents(eventName, options) { + return getOrSet({ + cache, + key: createSimpleCacheKey([ + namespace, + "getEvents", + { + address: contract.address, + eventName, + options, + }, + ]), + callback: () => contract.getEvents(eventName, options), + }); + }, + + /** + * Clears the entire cache. + */ + clearCache() { + cache.clear(); + }, + }; + + return Object.assign(newContract, contract, overrides); +} + +async function getOrSet({ + cache, + key, + callback, +}: { + cache: SimpleCache; + key: SimpleCacheKey; + callback: () => Promise | TValue; +}): Promise { + let value = cache.get(key); + if (typeof value !== "undefined") { + return value; + } + + value = await callback(); + cache.set(key, value); + + return value; +} diff --git a/packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts b/packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts new file mode 100644 index 00000000..4f4a9368 --- /dev/null +++ b/packages/evm-client/src/contract/factories/createCachedReadWriteContract.ts @@ -0,0 +1,46 @@ +import type { Abi } from "abitype"; +import { + type CreateCachedReadContractOptions, + createCachedReadContract, +} from "src/contract/factories/createCachedReadContract"; +import type { CachedReadWriteContract } from "src/contract/types/CachedContract"; +import type { ReadWriteContract } from "src/contract/types/Contract"; + +export interface CreateCachedReadWriteContractOptions + extends CreateCachedReadContractOptions { + contract: ReadWriteContract; +} + +/** + * Provides a cached wrapper around an Ethereum writable contract. This class is + * useful for both reading (with caching) and writing to a contract. It extends + * the functionality provided by CachedReadContract by adding write + * capabilities. + */ +export function createCachedReadWriteContract({ + contract, + cache, + namespace, +}: CreateCachedReadWriteContractOptions): CachedReadWriteContract { + // Avoid double-caching if given a contract that already has a cache. + if (isCached(contract)) { + return contract; + } + // Because this is part of the public API, we won't know if the original + // contract is a plain object or a class instance, so we use Object.create to + // preserve the original contract's prototype chain when extending, ensuring + // the new contract includes all the original contract's methods and + // instanceof checks will still work. + const contractPrototype = Object.getPrototypeOf(contract); + const newContract = Object.create(contractPrototype); + return Object.assign( + newContract, + createCachedReadContract({ contract, cache, namespace }), + ); +} + +function isCached( + contract: ReadWriteContract, +): contract is CachedReadWriteContract { + return "clearCache" in contract; +} diff --git a/packages/evm-client/src/contract/stubs/ReadContractStub.test.ts b/packages/evm-client/src/contract/stubs/ReadContractStub.test.ts new file mode 100644 index 00000000..89f6ced9 --- /dev/null +++ b/packages/evm-client/src/contract/stubs/ReadContractStub.test.ts @@ -0,0 +1,180 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { ALICE, BOB, NANCY } from "src/base/testing/accounts"; +import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +import type { Event } from "src/contract/types/Event"; +import { describe, expect, it } from "vitest"; + +const ERC20ABI = IERC20.abi; + +describe("ReadContractStub", () => { + it("stubs the read function without args, but with options", async () => { + const contract = new ReadContractStub(IERC20.abi); + + // stub total supply + contract.stubRead({ + functionName: "totalSupply", + value: 30n, + // options can be specfied as well + options: { blockNumber: 12n }, + }); + contract.stubRead({ + functionName: "totalSupply", + value: 40n, + // options can be specfied as well + options: { blockNumber: 16n }, + }); + // Now try and read them based on their args + const totalSupplyAtBlock12 = await contract.read("totalSupply", undefined, { + blockNumber: 12n, + }); + expect(totalSupplyAtBlock12).toBe(30n); + + const totalSupplyAtBlock16 = await contract.read("totalSupply", undefined, { + blockNumber: 16n, + }); + expect(totalSupplyAtBlock16).toBe(40n); + }); + + it("stubs the read function", async () => { + const contract = new ReadContractStub(IERC20.abi); + + expect(contract.read("balanceOf", { owner: NANCY })).rejects.toThrowError(); + + // Stub bob and alice's balances first + const bobValue = 10n; + contract.stubRead({ + functionName: "balanceOf", + args: { owner: BOB }, + value: bobValue, + }); + + const aliceValue = 20n; + contract.stubRead({ + functionName: "balanceOf", + args: { owner: ALICE }, + value: aliceValue, + // options can be specfied as well + options: { blockNumber: 10n }, + }); + + // Now try and read them based on their args + const bobResult = await contract.read("balanceOf", { owner: BOB }); + const aliceResult = await contract.read( + "balanceOf", + { owner: ALICE }, + { blockNumber: 10n }, + ); + expect(bobResult).toBe(bobValue); + expect(aliceResult).toBe(aliceValue); + + // Now stub w/out any args and see if we get the default value back + const defaultValue = 30n; + contract.stubRead({ + functionName: "balanceOf", + value: defaultValue, + }); + const defaultResult = await contract.read("balanceOf", { owner: NANCY }); + expect(defaultResult).toBe(defaultValue); + + const stub = contract.getReadStub("balanceOf"); + expect(stub?.callCount).toBe(3); + }); + + it("stubs the simulateWrite function", async () => { + const contract = new ReadContractStub(ERC20ABI); + + expect( + contract.simulateWrite("transferFrom", { + from: ALICE, + to: BOB, + value: 100n, + }), + ).rejects.toThrowError(); + + const stubbedResult = true; + contract.stubSimulateWrite("transferFrom", stubbedResult); + + const result = await contract.simulateWrite("transferFrom", { + from: ALICE, + to: BOB, + value: 100n, + }); + + expect(result).toStrictEqual(stubbedResult); + + const stub = contract.getSimulateWriteStub("transferFrom"); + expect(stub?.callCount).toBe(1); + }); + + it("stubs the getEvents function", async () => { + const contract = new ReadContractStub(ERC20ABI); + + // throws an error if you forget to stub the event your requesting + expect(contract.getEvents("Transfer")).rejects.toThrowError(); + + // Stub out the events when calling `getEvents` without any filter args + const stubbedAllEvents: Event[] = [ + { + eventName: "Transfer", + args: { + to: ALICE, + from: BOB, + value: 100n, + }, + blockNumber: 1n, + data: "0x123abc", + transactionHash: "0x123abc", + }, + { + eventName: "Transfer", + args: { + from: ALICE, + to: BOB, + value: 100n, + }, + blockNumber: 1n, + data: "0x123abc", + transactionHash: "0x123abc", + }, + ]; + contract.stubEvents("Transfer", undefined, stubbedAllEvents); + + // Stub out the events when calling `getEvents` *with* filter args + const stubbedFilteredEvents: Event[] = [ + { + eventName: "Transfer", + args: { + to: ALICE, + from: BOB, + value: 100n, + }, + blockNumber: 1n, + data: "0x123abc", + transactionHash: "0x123abc", + }, + ]; + contract.stubEvents( + "Transfer", + { filter: { from: BOB } }, + stubbedFilteredEvents, + ); + + // getting events without any filter args should return the stub that was + // specified without any filter args + const events = await contract.getEvents("Transfer"); + expect(events).toBe(stubbedAllEvents); + const stub = contract.getEventsStub("Transfer"); + expect(stub?.callCount).toBe(1); + + // getting events with filter args should return the stub that was specified + // *with* filter args + const filteredEvents = await contract.getEvents("Transfer", { + filter: { from: BOB }, + }); + expect(filteredEvents).toBe(stubbedFilteredEvents); + const filteredStub = contract.getEventsStub("Transfer", { + filter: { from: BOB }, + }); + expect(filteredStub?.callCount).toBe(1); + }); +}); diff --git a/packages/evm-client/src/contract/stubs/ReadContractStub.ts b/packages/evm-client/src/contract/stubs/ReadContractStub.ts new file mode 100644 index 00000000..813428b6 --- /dev/null +++ b/packages/evm-client/src/contract/stubs/ReadContractStub.ts @@ -0,0 +1,289 @@ +import type { Abi } from "abitype"; +import stringify from "fast-safe-stringify"; +import { type SinonStub, stub } from "sinon"; +import type { + ContractDecodeFunctionDataArgs, + ContractEncodeFunctionDataArgs, + ContractGetEventsArgs, + ContractGetEventsOptions, + ContractReadArgs, + ContractReadOptions, + ContractWriteArgs, + ContractWriteOptions, + ReadContract, +} from "src/contract/types/Contract"; +import type { Event, EventName } from "src/contract/types/Event"; +import type { + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/contract/types/Function"; + +/** + * A mock implementation of a `ReadContract` designed to facilitate unit + * testing. The `ReadContractStub` provides a way to stub out specific + * contract read, write, and event-fetching behaviors, allowing tests to focus + * on the business logic of the SDK. + * + * @example + * const contract = new ReadContractStub(ERC20ABI); + * contract.stubRead("baseToken", "0x123abc"); + * + * const value = await contract.read("baseToken", []); // "0x123abc" + * + */ +export class ReadContractStub + implements ReadContract +{ + abi; + address = "0x0000000000000000000000000000000000000000" as const; + + // Maps to store stubs for different contract methods based on their name. + protected readStubMap = new Map< + FunctionName, + ReadStub> + >(); + protected eventsStubMap = new Map< + EventName, + EventsStub> + >(); + protected simulateWriteStubMap = new Map< + FunctionName, + SimulateWriteStub> + >(); + + constructor(abi: TAbi = [] as any) { + this.abi = abi; + } + + /** + * Simulates a contract read operation for a given function. If the function + * is not previously stubbed using `stubRead`, an error will be thrown. + */ + async read>( + ...[functionName, args, options]: ContractReadArgs + ): Promise> { + const stub = this.getReadStub(functionName); + if (!stub) { + throw new Error( + `Called read for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubRead("${functionName}", value)`, + ); + } + return stub(args, options); + } + + /** + * Simulates a contract write operation for a given function. If the function + * is not previously stubbed using `stubWrite`, an error will be thrown. + */ + async simulateWrite< + TFunctionName extends FunctionName, + >( + ...[functionName, args, options]: ContractWriteArgs + ): Promise> { + const stub = this.getSimulateWriteStub(functionName); + if (!stub) { + throw new Error( + `Called simulateWrite for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, + ); + } + return stub(args, options); + } + + /** + * Simulates fetching events for a given event name from the contract. If the + * event name is not previously stubbed using `stubEvents`, an error will be + * thrown. + */ + async getEvents>( + ...[eventName, options]: ContractGetEventsArgs + ): Promise[]> { + const stub = this.getEventsStub(eventName, options); + if (!stub) { + throw new Error( + `Called getEvents for ${eventName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubEvents("${eventName}", value)`, + ); + } + return stub(options); + } + + /** + * Stubs the return value for a given function when `read` is called with that + * function name. This method overrides any previously stubbed values for the + * same function. + */ + stubRead>({ + functionName, + args, + value, + options, + }: { + functionName: TFunctionName; + args?: FunctionArgs; + value: FunctionReturn; + options?: ContractReadOptions; + }): void { + let readStub = this.readStubMap.get(functionName); + if (!readStub) { + readStub = stub(); + this.readStubMap.set(functionName, readStub); + } + + // Account for dynamic args if provided + if (args || options) { + // The stub returned from the map doesn't have a strong FunctionName type + // so we have to cast to avoid contravariance errors with the args. + (readStub as ReadStub) + .withArgs(args, options) + .resolves(value); + return; + } + + readStub.resolves(value); + } + + /** + * Stubs the return value for a given function when `simulateWrite` is called + * with that function name. This method overrides any previously stubbed + * values for the same function. + * + * *Note: The stub doesn't account for dynamic values based on provided + * arguments/options.* + */ + stubSimulateWrite< + TFunctionName extends FunctionName, + >( + functionName: TFunctionName, + value: FunctionReturn, + ): void { + let simulateWriteStub = this.simulateWriteStubMap.get(functionName); + if (!simulateWriteStub) { + simulateWriteStub = stub(); + this.simulateWriteStubMap.set(functionName, simulateWriteStub); + } + simulateWriteStub.resolves(value); + } + + /** + * Stubs the return value for a given event name when `getEvents` is called + * with that event name. This method overrides any previously stubbed values + * for the same event. + */ + stubEvents>( + eventName: TEventName, + args: ContractGetEventsOptions | undefined, + value: Event[], + ): void { + const stubKey = stableStringify({ eventName, args }); + if (this.eventsStubMap.has(stubKey)) { + this.getEventsStub(eventName, args)!.resolves(value as any); + } else { + this.eventsStubMap.set(stubKey, stub().resolves(value) as any); + } + } + + /** + * Retrieves the stub associated with a read function name. + * Useful for assertions in testing, such as checking call counts. + */ + getReadStub>( + functionName: TFunctionName, + ): ReadStub | undefined { + return this.readStubMap.get(functionName) as + | ReadStub + | undefined; + } + + /** + * Retrieves the stub associated with a write function name. + * Useful for assertions in testing, such as checking call counts. + */ + getSimulateWriteStub< + TFunctionName extends FunctionName, + >( + functionName: TFunctionName, + ): SimulateWriteStub | undefined { + return this.simulateWriteStubMap.get(functionName) as + | SimulateWriteStub + | undefined; + } + + /** + * Retrieves the stub associated with an event name. + * Useful for assertions in testing, such as checking call counts. + */ + getEventsStub>( + eventName: TEventName, + args?: ContractGetEventsOptions | undefined, + ): EventsStub | undefined { + const stubKey = stableStringify({ eventName, args }); + return this.eventsStubMap.get(stubKey) as + | EventsStub + | undefined; + } + + // TODO: + decodeFunctionData< + TFunctionName extends FunctionName = FunctionName, + >( + ...args: ContractDecodeFunctionDataArgs + ): DecodedFunctionData { + throw new Error("Method not implemented."); + } + + // TODO: + encodeFunctionData< + TFunctionName extends FunctionName = FunctionName, + >( + ...args: ContractEncodeFunctionDataArgs + ): `0x${string}` { + throw new Error("Method not implemented."); + } +} + +/** + * Type representing a stub for the "read" function of a contract. + */ +type ReadStub< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = SinonStub< + [args?: FunctionArgs, options?: ContractReadOptions], + Promise> +>; + +/** + * Type representing a stub for the "getEvents" function of a contract. + */ +type EventsStub< + TAbi extends Abi, + TEventName extends EventName, +> = SinonStub< + [options?: ContractGetEventsOptions], + Promise[]> +>; + +/** + * Type representing a stub for the "write" and "simulateWrite" functions of a + * contract. + */ +type SimulateWriteStub< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = SinonStub< + [ + args?: FunctionArgs | undefined, + options?: ContractWriteOptions, + ], + Promise> +>; + +function stableStringify(obj: Record) { + // simple non-recursive stringify replacer for bigints + function replacer(_: any, v: any) { + return typeof v === "bigint" ? v.toString() : v; + } + + return stringify.stableStringify(obj, replacer); +} diff --git a/packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts b/packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts new file mode 100644 index 00000000..73589b2d --- /dev/null +++ b/packages/evm-client/src/contract/stubs/ReadWriteContractStub.test.ts @@ -0,0 +1,23 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; +import { describe, expect, it } from "vitest"; + +const ERC20ABI = IERC20.abi; + +describe("ReadWriteContractStub", () => { + it("stubs the write function", async () => { + const contract = new ReadWriteContractStub(ERC20ABI); + + const stubbedValue = "0x01234"; + contract.stubWrite("transfer", stubbedValue); + + const value = await contract.write("transfer", { + to: "0x123abc", + value: 100n, + }); + expect(value).toBe(stubbedValue); + + const stub = contract.getWriteStub("transfer"); + expect(stub?.callCount).toBe(1); + }); +}); diff --git a/packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts b/packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts new file mode 100644 index 00000000..a2cae13f --- /dev/null +++ b/packages/evm-client/src/contract/stubs/ReadWriteContractStub.ts @@ -0,0 +1,102 @@ +import type { Abi } from "abitype"; +import { type SinonStub, stub } from "sinon"; +import { BOB } from "src/base/testing/accounts"; +import { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +import type { + ContractWriteArgs, + ContractWriteOptions, + ReadWriteContract, +} from "src/contract/types/Contract"; +import type { FunctionArgs, FunctionName } from "src/contract/types/Function"; + +/** + * A mock implementation of a writable Ethereum contract designed for unit + * testing purposes. The `ReadWriteContractStub` extends the functionalities of + * `ReadContractStub` and provides capabilities to stub out specific + * contract write behaviors. This makes it a valuable tool when testing + * scenarios that involve contract writing operations, without actually + * interacting with a real Ethereum contract. + * + * @example + * const contract = new ReadWriteContractStub(ERC20ABI); + * contract.stubWrite("addLiquidity", 100n); + * + * const result = await contract.write("addLiquidity", []); // 100n + * @extends {ReadContractStub} + * @implements {ReadWriteContract} + */ +export class ReadWriteContractStub + extends ReadContractStub + implements ReadWriteContract +{ + protected writeStubMap = new Map< + FunctionName, + WriteStub> + >(); + + getSignerAddress = stub().resolves(BOB); + + /** + * Simulates a contract write operation for a given function. If the function + * is not previously stubbed using `stubWrite` from the parent class, an error + * will be thrown. + */ + async write< + TFunctionName extends FunctionName, + >( + ...[functionName, args, options]: ContractWriteArgs + ): Promise<`0x${string}`> { + const stub = this.getWriteStub(functionName); + if (!stub) { + throw new Error( + `Called write for ${functionName} on a stubbed contract without a return value. The function must be stubbed first:\n\tcontract.stubWrite("${functionName}", value)`, + ); + } + return stub(args, options); + } + + /** + * Stubs the return value for a given function when `simulateWrite` is called + * with that function name. This method overrides any previously stubbed + * values for the same function. + * + * *Note: The stub doesn't account for dynamic values based on provided + * arguments/options.* + */ + stubWrite>( + functionName: TFunctionName, + value: `0x${string}`, + ): void { + let writeStub = this.writeStubMap.get(functionName); + if (!writeStub) { + writeStub = stub(); + this.writeStubMap.set(functionName, writeStub); + } + writeStub.resolves(value); + } + + /** + * Retrieves the stub associated with a write function name. + * Useful for assertions in testing, such as checking call counts. + */ + getWriteStub< + TFunctionName extends FunctionName, + >(functionName: TFunctionName): WriteStub | undefined { + return this.writeStubMap.get(functionName) as WriteStub< + TAbi, + TFunctionName + >; + } +} + +/** + * Type representing a stub for the "write" and "simulateWrite" functions of a + * contract. + */ +type WriteStub< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = SinonStub< + [args?: FunctionArgs, options?: ContractWriteOptions], + `0x${string}` +>; diff --git a/packages/evm-client/src/contract/types/AbiEntry.ts b/packages/evm-client/src/contract/types/AbiEntry.ts new file mode 100644 index 00000000..f1698b1c --- /dev/null +++ b/packages/evm-client/src/contract/types/AbiEntry.ts @@ -0,0 +1,260 @@ +import type { + Abi, + AbiItemType, + AbiParameter, + AbiParameterKind, + AbiParametersToPrimitiveTypes, + AbiParameterToPrimitiveType, + AbiStateMutability, +} from "abitype"; +import type { EmptyObject, Prettify } from "src/base/types"; + +// https://docs.soliditylang.org/en/latest/abi-spec.html#json + +export type NamedAbiParameter = AbiParameter & { name: string }; + +/** + * Get a union of possible names for an abi item type. + * + * @example + * ```ts + * type Erc20EventNames = AbiEntryName; + * // -> "Approval" | "Transfer" + * ``` + */ +export type AbiEntryName< + TAbi extends Abi, + TItemType extends AbiItemType = AbiItemType, +> = Extract["name"]; + +/** + * Get the ABI entry for a specific type, name, and state mutability. + * + * @example + * ```ts + * type ApproveEntry = AbiEntry; + * // -> + * // { + * // type: "function"; + * // name: "approve"; + * // inputs: [{ name: "spender", type: "address" }, { name: "value", type: "uint256" }]; + * // outputs: [{ name: "", type: "bool" }]; + * // stateMutability: "nonpayable"; + * // } + * ``` + */ +export type AbiEntry< + TAbi extends Abi, + TItemType extends AbiItemType = AbiItemType, + TName extends AbiEntryName = AbiEntryName, + TStateMutability extends AbiStateMutability = AbiStateMutability, +> = Extract< + TAbi[number], + { type: TItemType; name?: TName; stateMutability?: TStateMutability } +>; + +/** + * Get the parameters for a specific ABI entry. + * + * @example + * ```ts + * type ApproveParameters = AbiParameters; + * // -> [{ name: "spender", type: "address" }, { name: "value", type: "uint256" }] + * ``` + */ +export type AbiParameters< + TAbi extends Abi = Abi, + TItemType extends AbiItemType = AbiItemType, + TName extends AbiEntryName = AbiEntryName, + TParameterKind extends AbiParameterKind = AbiParameterKind, +> = AbiEntry extends infer TAbiEntry + ? TParameterKind extends keyof TAbiEntry + ? TAbiEntry[TParameterKind] + : [] + : []; + +/** + * Add default names to any ABI parameters that are missing a name. The default + * name is the index of the parameter. + * + * @example + * ```ts + * type Parameters = WithDefaultNames<[{ name: "spender", type: "address" }, { type: "uint256" }]>; + * // -> [{ name: "spender", type: "address" }, { name: "1", type: "uint256" }] + * ``` + */ +type WithDefaultNames = { + [K in keyof TParameters]: TParameters[K] extends infer TParameter extends + AbiParameter + ? TParameter extends NamedAbiParameter + ? TParameter + : TParameter & { name: `${K}` } + : never; +}; + +/** + * Convert an array or tuple of named abi parameters to an object type with the + * parameter names as keys and their primitive types as values. If a parameter + * has an empty name, it's index is used as the key. + * + * @example + * ```ts + * type Parameters = NamedParametersToObject<[{ name: "spender", type: "address" }, { name: "", type: "uint256" }]>; + * // -> { spender: `${string}`, "1": bigint } + * ``` + */ +type NamedParametersToObject< + TParameters extends readonly NamedAbiParameter[], + TParameterKind extends AbiParameterKind = AbiParameterKind, +> = Prettify< + { + // For every parameter name, excluding empty names, add a key to the object + // for the parameter name + [TName in Exclude< + TParameters[number]["name"], + "" + >]: AbiParameterToPrimitiveType< + Extract, + TParameterKind + >; + // Check if the parameters are in a Tuple. Tuples have known indexes, so we + // can use the index as the key for the nameless parameters + } & (TParameters extends readonly [NamedAbiParameter, ...NamedAbiParameter[]] + ? { + // For every key on the parameters type, if it's value is a parameter + // and the parameter's name is empty (""), then add a key for the index + [K in keyof TParameters as TParameters[K] extends NamedAbiParameter + ? TParameters[K]["name"] extends "" + ? // Exclude `number` to ensure only the specific index keys are + // included and not `number` itself + Exclude + : never // <- Key for named parameters (already handled above) + : never /* <- Prototype key (e.g., `length`, `toString`) */]: TParameters[K] extends NamedAbiParameter + ? AbiParameterToPrimitiveType + : never; // <- Prototype value + } + : // If the parameters are not in a Tuple, then we can't use the index as a + // key, so we have to use `number` as the key for any parameters that have + // empty names ("") in arrays + Extract extends never + ? unknown // <- No parameters with empty names + : { + [index: number]: AbiParameterToPrimitiveType< + Extract, + "inputs" + >; + }) +>; + +/** + * Convert an array or tuple of abi parameters to an object type. + * + * @example + * ```ts + * type ApproveArgs = AbiParametersToObject<[ + * { name: "spender", type: "address" }, + * { name: "value", type: "uint256" } + * ]>; + * // -> { spender: `0x${string}`, value: bigint } + * ``` + */ +export type AbiParametersToObject< + TParameters extends readonly AbiParameter[], + TParameterKind extends AbiParameterKind = AbiParameterKind, +> = TParameters extends readonly [] + ? EmptyObject + : TParameters extends NamedAbiParameter[] + ? NamedParametersToObject + : NamedParametersToObject, TParameterKind>; + +/** + * Get an array of primitive types for any ABI parameters. + * + * @example + * ```ts + * type ApproveInput = AbiArrayType; + * // -> [`0x${string}`, bigint] + * + * type BalanceOutput = AbiArrayType; + * // -> [bigint] + * ``` + */ +export type AbiArrayType< + TAbi extends Abi, + TItemType extends AbiItemType = AbiItemType, + TName extends AbiEntryName = AbiEntryName, + TParameterKind extends AbiParameterKind = AbiParameterKind, +> = AbiParameters< + TAbi, + TItemType, + TName, + TParameterKind +> extends infer TParameters + ? TParameters extends readonly AbiParameter[] + ? AbiParametersToPrimitiveTypes + : [] + : []; + +/** + * Get an object of primitive types for any ABI parameters. + * + * @example + * ```ts + * type ApproveArgs = AbiObjectType; + * // -> { spender: `0x${string}`, value: bigint } + * + * type Balance = AbiObjectType; + * // -> { balance: bigint } + * ``` + */ +export type AbiObjectType< + TAbi extends Abi, + TItemType extends AbiItemType = AbiItemType, + TName extends AbiEntryName = AbiEntryName, + TParameterKind extends AbiParameterKind = AbiParameterKind, +> = AbiParametersToObject< + AbiParameters +>; + +/** + * Get a user-friendly primitive type for any ABI parameters, which is + * determined by the number of parameters: + * - __Single parameter:__ the primitive type of the parameter. + * - __Multiple parameters:__ an object with the parameter names as keys and the + * their primitive types as values. If a parameter has no name, it's index is + * used as the key. + * - __No parameters:__ `undefined`. + * + * @example + * ```ts + * type ApproveArgs = AbiFriendlyType; + * // -> { spender: `${string}`, value: bigint } + * + * type Balance = AbiFriendlyType; + * // -> bigint + * + * type DecimalArgs = AbiFriendlyType; + * // -> undefined + * ``` + */ +export type AbiFriendlyType< + TAbi extends Abi, + TItemType extends AbiItemType = AbiItemType, + TName extends AbiEntryName = AbiEntryName, + TParameterKind extends AbiParameterKind = AbiParameterKind, + TStateMutability extends AbiStateMutability = AbiStateMutability, +> = AbiEntry extends infer TAbiEntry + ? TParameterKind extends keyof TAbiEntry & AbiParameterKind // Check if the ABI entry includes the parameter kind (inputs/outputs) + ? TAbiEntry[TParameterKind] extends readonly [AbiParameter] // Check if it's a single parameter + ? AbiParameterToPrimitiveType< + TAbiEntry[TParameterKind][0], + TParameterKind + > // Single parameter type + : TAbiEntry[TParameterKind] extends readonly [ + AbiParameter, + ...AbiParameter[], + ] // Check if it's multiple parameters + ? AbiParametersToObject // Multiple parameters type + : undefined // Empty parameters + : undefined // ABI entry doesn't include the parameter kind (inputs/outputs) + : undefined; // ABI entry not found diff --git a/packages/evm-client/src/contract/types/CachedContract.ts b/packages/evm-client/src/contract/types/CachedContract.ts new file mode 100644 index 00000000..8f116935 --- /dev/null +++ b/packages/evm-client/src/contract/types/CachedContract.ts @@ -0,0 +1,32 @@ +import type { Abi } from "abitype"; +import type { + ContractReadArgs, + ReadContract, + ReadWriteContract, +} from "src/contract/types/Contract"; +import type { FunctionName } from "src/contract/types/Function"; +import type { SimpleCache } from "src/exports"; + +export interface CachedReadContract + extends ReadContract { + cache: SimpleCache; + namespace?: string; + clearCache(): void; + deleteRead>( + ...[functionName, args, options]: ContractReadArgs + ): void; + deleteReadsMatching>( + ...[functionName, args, options]: DeepPartial< + ContractReadArgs + > + ): void; +} + +export interface CachedReadWriteContract + extends CachedReadContract, + ReadWriteContract {} + +/** Recursively make all properties in T partial. */ +type DeepPartial = { + [K in keyof T]?: DeepPartial; +}; diff --git a/packages/evm-client/src/contract/types/Contract.ts b/packages/evm-client/src/contract/types/Contract.ts new file mode 100644 index 00000000..f485217f --- /dev/null +++ b/packages/evm-client/src/contract/types/Contract.ts @@ -0,0 +1,189 @@ +import type { Abi } from "abitype"; +import type { EmptyObject } from "src/base/types"; +import type { Event, EventFilter, EventName } from "src/contract/types/Event"; +import type { + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/contract/types/Function"; +import type { BlockTag } from "src/network/types/Block"; + +// https://ethereum.github.io/execution-apis/api-documentation/ + +/** + * Interface representing a readable contract with specified ABI. Provides type + * safe methods to read and simulate write operations on the contract. + */ +export interface ReadContract { + abi: TAbi; + address: `0x${string}`; + + /** + * Reads a specified function from the contract. + */ + read>( + ...args: ContractReadArgs + ): Promise>; + + /** + * Simulates a write operation on a specified function of the contract. + */ + simulateWrite< + TFunctionName extends FunctionName, + >( + ...args: ContractWriteArgs + ): Promise>; + + /** + * Retrieves specified events from the contract. + */ + getEvents>( + ...args: ContractGetEventsArgs + ): Promise[]>; + + /** + * Encodes a function call into calldata. + */ + encodeFunctionData>( + ...args: ContractEncodeFunctionDataArgs + ): `0x${string}`; + + /** + * Decodes a string of function calldata into it's arguments and function + * name. + */ + decodeFunctionData< + TFunctionName extends FunctionName = FunctionName, + >( + ...args: ContractDecodeFunctionDataArgs + ): DecodedFunctionData; +} + +/** + * Interface representing a writable contract with specified ABI. + * Extends IReadContract to also include write operations. + */ +export interface ReadWriteContract + extends ReadContract { + /** + * Get the address of the signer for this contract. + */ + getSignerAddress(): Promise<`0x${string}`>; + + /** + * Writes to a specified function on the contract. + * @returns The transaction hash of the submitted transaction. + */ + write>( + ...args: ContractWriteArgs + ): Promise<`0x${string}`>; +} + +// https://github.com/ethereum/execution-apis/blob/main/src/eth/execute.yaml#L1 +export type ContractReadOptions = + | { + blockNumber?: bigint; + blockTag?: never; + } + | { + blockNumber?: never; + /** + * @default 'latest' + */ + blockTag?: BlockTag; + }; + +export type ContractReadArgs< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = FunctionArgs extends EmptyObject + ? [ + functionName: TFunctionName, + args?: FunctionArgs, + options?: ContractReadOptions, + ] + : [ + functionName: TFunctionName, + args: FunctionArgs, + options?: ContractReadOptions, + ]; + +export interface ContractGetEventsOptions< + TAbi extends Abi, + TEventName extends EventName = EventName, +> { + filter?: EventFilter; + fromBlock?: bigint | BlockTag; + toBlock?: bigint | BlockTag; +} + +export type ContractGetEventsArgs< + TAbi extends Abi, + TEventName extends EventName, +> = [ + eventName: TEventName, + options?: ContractGetEventsOptions, +]; + +// https://github.com/ethereum/execution-apis/blob/main/src/schemas/transaction.yaml#L274 +export interface ContractWriteOptions { + type?: `0x${string}`; + nonce?: bigint; + to?: `0x${string}`; + from?: `0x${string}`; + /** + * Gas limit + */ + gas?: bigint; + value?: bigint; + input?: `0x${string}`; + /** + * The gas price willing to be paid by the sender in wei + */ + gasPrice?: bigint; + /** + * Maximum fee per gas the sender is willing to pay to miners in wei + */ + maxPriorityFeePerGas?: bigint; + /** + * The maximum total fee per gas the sender is willing to pay (includes the + * network / base fee and miner / priority fee) in wei + */ + maxFeePerGas?: bigint; + /** + * EIP-2930 access list + */ + accessList?: { + address: `0x${string}`; + storageKeys: `0x${string}`[]; + }[]; + /** + * Chain ID that this transaction is valid on. + */ + chainId?: bigint; +} + +export type ContractWriteArgs< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = FunctionArgs extends EmptyObject + ? [ + functionName: TFunctionName, + args?: FunctionArgs, + options?: ContractWriteOptions, + ] + : [ + functionName: TFunctionName, + args: FunctionArgs, + options?: ContractWriteOptions, + ]; + +export type ContractEncodeFunctionDataArgs< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = FunctionArgs extends EmptyObject + ? [functionName: TFunctionName, args?: FunctionArgs] + : [functionName: TFunctionName, args: FunctionArgs]; + +export type ContractDecodeFunctionDataArgs = [data: `0x${string}`]; diff --git a/packages/evm-client/src/contract/types/Event.ts b/packages/evm-client/src/contract/types/Event.ts new file mode 100644 index 00000000..a4070bed --- /dev/null +++ b/packages/evm-client/src/contract/types/Event.ts @@ -0,0 +1,64 @@ +import type { Abi } from "abitype"; +import type { + AbiEntry, + AbiObjectType, + AbiParameters, + AbiParametersToObject, + NamedAbiParameter, +} from "src/contract/types/AbiEntry"; + +/** + * Get a union of event names from an abi + */ +export type EventName = AbiEntry["name"]; + +/** + * Get a union of named input parameters for an event from an abi + */ +type NamedEventInput< + TAbi extends Abi, + TEventName extends EventName, +> = Extract< + AbiParameters[number], + NamedAbiParameter +>; + +/** + * Get an object type for an event's arguments from an abi. + */ +export type EventArgs< + TAbi extends Abi, + TEventName extends EventName, +> = AbiObjectType; + +/** + * Get a union of indexed input objects for an event from an abi + */ +type IndexedEventInput< + TAbi extends Abi, + TEventName extends EventName, +> = Extract, { indexed: true }>; + +/** + * Get an object type for an event's indexed fields from an abi + */ +export type EventFilter< + TAbi extends Abi, + TEventName extends EventName, +> = Partial< + AbiParametersToObject[], "inputs"> +>; + +/** + * A strongly typed event object based on an abi + */ +export interface Event< + TAbi extends Abi, + TEventName extends EventName = EventName, +> { + eventName: TEventName; + args: EventArgs; + data?: `0x${string}`; + blockNumber?: bigint; + transactionHash?: `0x${string}`; +} diff --git a/packages/evm-client/src/contract/types/Function.ts b/packages/evm-client/src/contract/types/Function.ts new file mode 100644 index 00000000..f0e5a0ce --- /dev/null +++ b/packages/evm-client/src/contract/types/Function.ts @@ -0,0 +1,57 @@ +import type { Abi, AbiStateMutability } from "abitype"; +import type { AbiFriendlyType, AbiObjectType } from "src/contract/types/AbiEntry"; + +/** + * Get a union of function names from an abi + */ +export type FunctionName< + TAbi extends Abi, + TAbiStateMutability extends AbiStateMutability = AbiStateMutability, +> = Extract< + TAbi[number], + { type: "function"; stateMutability: TAbiStateMutability } +>["name"]; + +/** + * Get an object type for an abi function's arguments. + */ +export type FunctionArgs< + TAbi extends Abi, + TFunctionName extends FunctionName = FunctionName, +> = AbiObjectType; + +/** + * Get an object type for an abi's constructor arguments. + */ +export type ConstructorArgs = AbiObjectType< + TAbi, + "constructor", + any, + "inputs" +>; + +/** + * Get a user-friendly return type for an abi function, which is determined by + * it's outputs: + * - __Single output:__ the type of the single output. + * - __Multiple outputs:__ an object with the output names as keys and the + * output types as values. + * - __No outputs:__ `undefined`. + */ +export type FunctionReturn< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = AbiFriendlyType; + +/** + * Get an object representing decoded function or constructor data from an ABI. + */ +export type DecodedFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, +> = { + [K in TFunctionName]: { + args: FunctionArgs; + functionName: K; + }; +}[TFunctionName]; diff --git a/packages/evm-client/src/contract/utils/arrayToFriendly.test.ts b/packages/evm-client/src/contract/utils/arrayToFriendly.test.ts new file mode 100644 index 00000000..1b00fbe3 --- /dev/null +++ b/packages/evm-client/src/contract/utils/arrayToFriendly.test.ts @@ -0,0 +1,67 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { arrayToFriendly } from "src/contract/utils/arrayToFriendly"; +import { describe, expect, it } from "vitest"; + +describe("arrayToFriendly", () => { + it("correctly converts arrays with multiple items into objects", async () => { + const transferArgsObject = arrayToFriendly({ + abi: IERC20.abi, + type: "function", + name: "transfer", + kind: "inputs", + values: ["0x123", 123n], + }); + expect(transferArgsObject).toEqual({ + to: "0x123", + value: 123n, + }); + + // empty parameter names (index keys) + const votesArgsObject = arrayToFriendly({ + abi: exampleAbi, + type: "function", + name: "votes", + kind: "inputs", + values: ["0x123", 0n], + }); + expect(votesArgsObject).toEqual({ + "0": "0x123", + "1": 0n, + }); + }); + + it("returns the item from arrays with a single item", async () => { + const balanceInput = arrayToFriendly({ + abi: IERC20.abi, + type: "function", + name: "balanceOf", + kind: "inputs", + values: ["0x123"], + }); + expect(balanceInput).toEqual("0x123"); + }); + + it("Converts an empty arrays into undefined", async () => { + const notDefined = arrayToFriendly({ + abi: IERC20.abi, + type: "function", + name: "symbol", + kind: "inputs", + values: [], + }); + expect(notDefined).toBeUndefined(); + }); +}); + +const exampleAbi = [ + { + inputs: [ + { name: "", type: "address" }, + { name: "", type: "uint256" }, + ], + name: "votes", + outputs: [], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/evm-client/src/contract/utils/arrayToFriendly.ts b/packages/evm-client/src/contract/utils/arrayToFriendly.ts new file mode 100644 index 00000000..a4c99b2a --- /dev/null +++ b/packages/evm-client/src/contract/utils/arrayToFriendly.ts @@ -0,0 +1,105 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiFriendlyType, +} from "src/contract/types/AbiEntry"; +import { getAbiEntry } from "src/contract/utils/getAbiEntry"; + +/** + * Converts an array of input or output values into an + * {@linkcode AbiFriendlyType}, ensuring the values are properly identified + * based on their index. + * + * @example + * const abi = [ + * { + * type: "function", + * name: "transfer", + * inputs: [ + * { name: "to", type: "address" }, + * { name: "value", type: "uint256" }, + * ], + * outputs: [{ name: "", type: "bool" }], + * stateMutability: "nonpayable", + * }, + * { + * type: "event", + * name: "Approval", + * inputs: [ + * { indexed: true, name: "owner", type: "address" }, + * { indexed: true, name: "spender", type: "address" }, + * { indexed: false, name: "value", type: "uint256" }, + * ], + * }, + * ] as const; + * + * const parsedArgs = arrayToFriendly({ + * abi, + * type: "function", + * name: "transfer", + * kind: "inputs", + * values: ["0x123", 123n], + * }); // -> { to: "0x123", value: 123n } + * + * const parsedReturn = arrayToFriendly({ + * abi, + * type: "function", + * name: "transfer", + * kind: "outputs", + * values: [true], + * }); // -> true + * + * const parsedFilter = arrayToFriendly({ + * abi, + * type: "event", + * name: "Approval", + * kind: "inputs", + * values: [undefined, "0x123", undefined], + * }); // -> { owner: undefined, spender: "0x123", value: undefined } + */ +export function arrayToFriendly< + TAbi extends Abi, + TItemType extends AbiItemType, + TName extends AbiEntryName, + TParameterKind extends AbiParameterKind, +>({ + abi, + type, + name, + kind, + values, +}: { + abi: TAbi; + name: TName; + values?: Abi extends TAbi + ? readonly unknown[] // <- fallback for unknown ABI type + : Partial>; + kind: TParameterKind; + type: TItemType; +}): AbiFriendlyType { + const abiEntry = getAbiEntry({ abi, type, name }); + + let parameters: AbiParameter[] = []; + if (kind in abiEntry) { + parameters = (abiEntry as any)[kind]; + } + + // Single or no parameters + if (parameters.length <= 1) { + return (values as any[])[0]; + } + + const valuesArray = values || []; + + const friendlyValue: Record = {}; + parameters.forEach(({ name }, i) => { + if (name) { + friendlyValue[name] = valuesArray[i]; + } else { + friendlyValue[i] = valuesArray[i]; + } + }); + + return friendlyValue as any; +} diff --git a/packages/evm-client/src/contract/utils/arrayToObject.test.ts b/packages/evm-client/src/contract/utils/arrayToObject.test.ts new file mode 100644 index 00000000..732a8110 --- /dev/null +++ b/packages/evm-client/src/contract/utils/arrayToObject.test.ts @@ -0,0 +1,54 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { arrayToObject } from "src/contract/utils/arrayToObject"; +import { describe, expect, it } from "vitest"; + +describe("arrayToObject", () => { + it("correctly converts arrays into objects", async () => { + const transferArgsObject = arrayToObject({ + abi: IERC20.abi, + type: "function", + name: "transfer", + kind: "inputs", + values: ["0x123", 123n], + }); + expect(transferArgsObject).toEqual({ + to: "0x123", + value: 123n, + }); + + // empty parameter names (index keys) + const votesArgsObject = arrayToObject({ + abi: exampleAbi, + type: "function", + name: "votes", + kind: "inputs", + values: ["0x123", 0n], + }); + expect(votesArgsObject).toEqual({ + "0": "0x123", + "1": 0n, + }); + + const balanceInput = arrayToObject({ + abi: IERC20.abi, + type: "function", + name: "balanceOf", + kind: "inputs", + values: ["0x123"], + }); + expect(balanceInput).toEqual({ owner: "0x123" }); + }); +}); + +const exampleAbi = [ + { + inputs: [ + { name: "", type: "address" }, + { name: "", type: "uint256" }, + ], + name: "votes", + outputs: [], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/evm-client/src/contract/utils/arrayToObject.ts b/packages/evm-client/src/contract/utils/arrayToObject.ts new file mode 100644 index 00000000..d9399b95 --- /dev/null +++ b/packages/evm-client/src/contract/utils/arrayToObject.ts @@ -0,0 +1,91 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiObjectType, +} from "src/contract/types/AbiEntry"; +import { getAbiEntry } from "src/contract/utils/getAbiEntry"; + +/** + * Converts an array of input or output values into an object typ, ensuring the + * values are properly identified based on their index. + * + * @example + * const abi = [ + * { + * type: "function", + * name: "transfer", + * inputs: [ + * { name: "to", type: "address" }, + * { name: "value", type: "uint256" }, + * ], + * outputs: [{ name: "", type: "bool" }], + * stateMutability: "nonpayable", + * }, + * { + * type: "event", + * name: "Approval", + * inputs: [ + * { indexed: true, name: "owner", type: "address" }, + * { indexed: true, name: "spender", type: "address" }, + * { indexed: false, name: "value", type: "uint256" }, + * ], + * }, + * ] as const; + * + * const parsedArgs = arrayToObject({ + * abi, + * type: "function", + * name: "transfer", + * kind: "inputs", + * values: ["0x123", 123n], + * }); // -> { to: "0x123", value: 123n } + * + * const parsedFilter = arrayToObject({ + * abi, + * type: "event", + * name: "Approval", + * kind: "inputs", + * values: [undefined, "0x123", undefined], + * }); // -> { owner: undefined, spender: "0x123", value: undefined } + */ +export function arrayToObject< + TAbi extends Abi, + TItemType extends AbiItemType, + TName extends AbiEntryName, + TParameterKind extends AbiParameterKind, +>({ + abi, + type, + name, + kind, + values, +}: { + abi: TAbi; + name: TName; + values?: Abi extends TAbi + ? readonly unknown[] // <- fallback for unknown ABI type + : Partial>; + kind: TParameterKind; + type: TItemType; +}): AbiObjectType { + const abiEntry = getAbiEntry({ abi, type, name }); + + let parameters: AbiParameter[] = []; + if (kind in abiEntry) { + parameters = (abiEntry as any)[kind]; + } + + const valuesArray = values || []; + + const valuesObject: Record = {}; + parameters.forEach(({ name }, i) => { + if (name) { + valuesObject[name] = valuesArray[i]; + } else { + valuesObject[i] = valuesArray[i]; + } + }); + + return valuesObject as any; +} diff --git a/packages/evm-client/src/contract/utils/getAbiEntry.ts b/packages/evm-client/src/contract/utils/getAbiEntry.ts new file mode 100644 index 00000000..d9ae2fc5 --- /dev/null +++ b/packages/evm-client/src/contract/utils/getAbiEntry.ts @@ -0,0 +1,33 @@ +import type { Abi, AbiItemType } from "abitype"; +import type { AbiEntry, AbiEntryName } from "src/contract/types/AbiEntry"; +import { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; + +/** + * Get an entry from an ABI by type and name. + * @throws If the entry is not found in the ABI. + */ +export function getAbiEntry< + TAbi extends Abi, + TItemType extends AbiItemType, + TName extends AbiEntryName, +>({ + abi, + type, + name, +}: { + abi: TAbi; + type: TItemType; + name?: TName; +}): AbiEntry { + const abiItem = abi.find( + (item) => + item.type === type && + (type === "constructor" || (item as any).name === name), + ) as AbiEntry | undefined; + + if (!abiItem) { + throw new AbiEntryNotFoundError({ type, name }); + } + + return abiItem; +} diff --git a/packages/evm-client/src/contract/utils/objectToArray.test.ts b/packages/evm-client/src/contract/utils/objectToArray.test.ts new file mode 100644 index 00000000..bcaaa8b1 --- /dev/null +++ b/packages/evm-client/src/contract/utils/objectToArray.test.ts @@ -0,0 +1,62 @@ +import { IERC20 } from "src/base/testing/IERC20"; +import { objectToArray } from "src/contract/utils/objectToArray"; +import { describe, expect, it } from "vitest"; + +describe("objectToArray", () => { + it("correctly converts objects into arrays", async () => { + const transferArgsArray = objectToArray({ + abi: IERC20.abi, + type: "function", + name: "transfer", + kind: "inputs", + value: { + to: "0x123", + value: 123n, + }, + }); + expect(transferArgsArray).toEqual(["0x123", 123n]); + + // empty parameter names (index keys) + const votesArgsArray = objectToArray({ + abi: exampleAbi, + type: "function", + name: "votes", + kind: "inputs", + value: { + "0": "0x123", + "1": 0n, + }, + }); + expect(votesArgsArray).toEqual(["0x123", 0n]); + }); + + const emptyArray = objectToArray({ + abi: IERC20.abi, + type: "function", + name: "symbol", + kind: "inputs", + value: {}, + }); + expect(emptyArray).toEqual([]); + + const emptyArrayFromUndefined = objectToArray({ + abi: IERC20.abi, + type: "function", + name: "symbol", + kind: "inputs", + }); + expect(emptyArrayFromUndefined).toEqual([]); +}); + +const exampleAbi = [ + { + inputs: [ + { name: "", type: "address" }, + { name: "", type: "uint256" }, + ], + name: "votes", + outputs: [], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/packages/evm-client/src/contract/utils/objectToArray.ts b/packages/evm-client/src/contract/utils/objectToArray.ts new file mode 100644 index 00000000..bce6edab --- /dev/null +++ b/packages/evm-client/src/contract/utils/objectToArray.ts @@ -0,0 +1,89 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiObjectType, +} from "src/contract/types/AbiEntry"; +import { getAbiEntry } from "src/contract/utils/getAbiEntry"; + +/** + * Converts an object into an array of input or output values, ensuring the the + * correct number and order of values are present. + * + * @example + * const abi = [ + * { + * type: "function", + * name: "transfer", + * inputs: [ + * { name: "to", type: "address" }, + * { name: "value", type: "uint256" }, + * ], + * outputs: [{ name: "", type: "bool" }], + * stateMutability: "nonpayable", + * }, + * { + * type: "event", + * name: "Approval", + * inputs: [ + * { indexed: true, name: "owner", type: "address" }, + * { indexed: true, name: "spender", type: "address" }, + * { indexed: false, name: "value", type: "uint256" }, + * ], + * }, + * ] as const; + * + * const preppedArgs = objectToArray({ + * abi, + * type: "function", + * name: "transfer", + * kind: "inputs", + * value: { value: 123n, to: "0x123" }, + * }); // -> ["0x123", 123n] + * + * const preppedFilter = objectToArray({ + * abi, + * type: "event", + * name: "Approval", + * kind: "inputs", + * value: { spender: "0x123" }, + * }); // -> [undefined, "0x123", undefined] + */ +export function objectToArray< + TAbi extends Abi, + TItemType extends AbiItemType, + TName extends AbiEntryName, + TParameterKind extends AbiParameterKind, + TValue extends AbiObjectType, +>({ + abi, + type, + name, + kind, + value, +}: { + abi: TAbi; + name: TName; + kind: TParameterKind; + type: TItemType; + value?: Abi extends TAbi ? Record : TValue; +}): AbiArrayType { + const abiEntry = getAbiEntry({ abi, type, name }); + + let parameters: AbiParameter[] = []; + if (kind in abiEntry) { + parameters = (abiEntry as any)[kind]; + } + + // No parameters + if (!parameters.length) { + return [] as AbiArrayType; + } + + const valueObject: Record = + value && typeof value === "object" ? value : {}; + + const array = parameters.map(({ name }, i) => valueObject[name || i]); + + return array as AbiArrayType; +} diff --git a/packages/evm-client/src/errors/AbiEntryNotFound.ts b/packages/evm-client/src/errors/AbiEntryNotFound.ts new file mode 100644 index 00000000..df9ead4c --- /dev/null +++ b/packages/evm-client/src/errors/AbiEntryNotFound.ts @@ -0,0 +1,7 @@ +import type { AbiItemType } from "abitype"; + +export class AbiEntryNotFoundError extends Error { + constructor({ type, name }: { type: AbiItemType; name?: string }) { + super(`No ${type}${name ? ` with name ${name}` : ""} found in ABI.`); + } +} diff --git a/packages/evm-client/src/exports/cache.ts b/packages/evm-client/src/exports/cache.ts new file mode 100644 index 00000000..e4ea412a --- /dev/null +++ b/packages/evm-client/src/exports/cache.ts @@ -0,0 +1,3 @@ +export { createLruSimpleCache } from "src/cache/factories/createLruSimpleCache"; +export type { SimpleCache, SimpleCacheKey } from "src/cache/types/SimpleCache"; +export { createSimpleCacheKey } from "src/cache/utils/createSimpleCacheKey"; diff --git a/packages/evm-client/src/exports/contract.ts b/packages/evm-client/src/exports/contract.ts new file mode 100644 index 00000000..69867527 --- /dev/null +++ b/packages/evm-client/src/exports/contract.ts @@ -0,0 +1,54 @@ +// Factories +export { + createCachedReadContract, + type CreateCachedReadContractOptions, +} from "src/contract/factories/createCachedReadContract"; +export { + createCachedReadWriteContract, + type CreateCachedReadWriteContractOptions, +} from "src/contract/factories/createCachedReadWriteContract"; + +// Types +export type { + AbiArrayType, + AbiEntry, + AbiEntryName, + AbiFriendlyType, + AbiObjectType, + AbiParameters, +} from "src/contract/types/AbiEntry"; +export type { + CachedReadContract, + CachedReadWriteContract, +} from "src/contract/types/CachedContract"; +export type { + ContractDecodeFunctionDataArgs, + ContractEncodeFunctionDataArgs, + ContractGetEventsArgs, + ContractGetEventsOptions, + ContractReadArgs, + ContractReadOptions, + ContractWriteArgs, + ContractWriteOptions, + ReadContract, + ReadWriteContract, +} from "src/contract/types/Contract"; +export type { + Event, + EventArgs, + EventFilter, + EventName, +} from "src/contract/types/Event"; +export type { + ConstructorArgs, + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/contract/types/Function"; + +// Utils +export { arrayToFriendly } from "src/contract/utils/arrayToFriendly"; +export { arrayToObject } from "src/contract/utils/arrayToObject"; +export { getAbiEntry } from "src/contract/utils/getAbiEntry"; +export { objectToArray } from "src/contract/utils/objectToArray"; diff --git a/packages/evm-client/src/exports/errors.ts b/packages/evm-client/src/exports/errors.ts new file mode 100644 index 00000000..4e681a8e --- /dev/null +++ b/packages/evm-client/src/exports/errors.ts @@ -0,0 +1 @@ +export { AbiEntryNotFoundError } from "src/errors/AbiEntryNotFound"; diff --git a/packages/evm-client/src/exports/index.ts b/packages/evm-client/src/exports/index.ts new file mode 100644 index 00000000..ef10a96d --- /dev/null +++ b/packages/evm-client/src/exports/index.ts @@ -0,0 +1,4 @@ +export * from "src/exports/cache"; +export * from "src/exports/contract"; +export * from "src/exports/errors"; +export * from "src/exports/network"; diff --git a/packages/evm-client/src/exports/network.ts b/packages/evm-client/src/exports/network.ts new file mode 100644 index 00000000..2c822415 --- /dev/null +++ b/packages/evm-client/src/exports/network.ts @@ -0,0 +1,15 @@ +export type { Block, BlockTag } from "src/network/types/Block"; +export type { + Network, + NetworkGetBalanceArgs, + NetworkGetBlockArgs, + NetworkGetBlockOptions, + NetworkGetTransactionArgs, + NetworkWaitForTransactionArgs, +} from "src/network/types/Network"; +export type { + MinedTransaction, + Transaction, + TransactionInfo, + TransactionReceipt, +} from "src/network/types/Transaction"; diff --git a/packages/evm-client/src/exports/stubs.ts b/packages/evm-client/src/exports/stubs.ts new file mode 100644 index 00000000..f31d75d4 --- /dev/null +++ b/packages/evm-client/src/exports/stubs.ts @@ -0,0 +1,6 @@ +// Contract +export { ReadContractStub } from "src/contract/stubs/ReadContractStub"; +export { ReadWriteContractStub } from "src/contract/stubs/ReadWriteContractStub"; + +// Network +export { NetworkStub } from "src/network/stubs/NetworkStub"; diff --git a/packages/evm-client/src/network/stubs/NetworkStub.test.ts b/packages/evm-client/src/network/stubs/NetworkStub.test.ts new file mode 100644 index 00000000..2613579d --- /dev/null +++ b/packages/evm-client/src/network/stubs/NetworkStub.test.ts @@ -0,0 +1,112 @@ +import { ALICE } from "src/base/testing/accounts"; +import { + NetworkStub, + transactionToReceipt, +} from "src/network/stubs/NetworkStub"; +import { describe, expect, it } from "vitest"; +import type { Transaction } from "../types/Transaction"; + +describe("NetworkStub", () => { + it("stubs getBalance", async () => { + const network = new NetworkStub(); + + network.stubGetBalance({ + args: [ALICE], + value: 100n, + }); + + const balance = await network.getBalance(ALICE); + + expect(balance).toEqual(100n); + }); + + it("stubs getBlock", async () => { + const network = new NetworkStub(); + + const block = { + blockNumber: 1n, + timestamp: 1000n, + }; + network.stubGetBlock({ + args: [{ blockNumber: 1n }], + value: block, + }); + + const blockResponse = await network.getBlock({ blockNumber: 1n }); + + expect(blockResponse).toEqual(block); + }); + + it("stubs getChainId", async () => { + const network = new NetworkStub(); + + network.stubGetChainId(42069); + + const chainId = await network.getChainId(); + + expect(chainId).toEqual(42069); + }); + + it("stubs getTransaction", async () => { + const network = new NetworkStub(); + + const txHash = "0x123abc"; + const tx: Transaction = { + gas: 100n, + gasPrice: 100n, + input: "0x456def", + nonce: 0, + type: "0x0", + value: 0n, + }; + + network.stubGetTransaction({ + args: [txHash], + value: tx, + }); + + const transaction = await network.getTransaction(txHash); + + expect(transaction).toEqual(tx); + }); + + it("waits for stubbed transactions", async () => { + const network = new NetworkStub(); + + const txHash = "0x123abc"; + const stubbedTx = { + gas: 100n, + gasPrice: 100n, + input: "0x456def", + nonce: 0, + type: "0x0", + value: 0n, + } as const; + + const waitPromise = network.waitForTransaction(txHash); + + await new Promise((resolve) => { + setTimeout(() => { + network.stubGetTransaction({ + args: [txHash], + value: stubbedTx, + }); + resolve(undefined); + }, 1000); + }); + + const tx = await waitPromise; + + expect(tx).toEqual(transactionToReceipt(stubbedTx)); + }); + + it("reaches timeout when waiting for transactions that are never stubbed", async () => { + const network = new NetworkStub(); + + const waitPromise = await network.waitForTransaction("0x123abc", { + timeout: 1000, + }); + + expect(waitPromise).toBe(undefined); + }); +}); diff --git a/packages/evm-client/src/network/stubs/NetworkStub.ts b/packages/evm-client/src/network/stubs/NetworkStub.ts new file mode 100644 index 00000000..0b091120 --- /dev/null +++ b/packages/evm-client/src/network/stubs/NetworkStub.ts @@ -0,0 +1,179 @@ +import { type SinonStub, stub } from "sinon"; +import type { Block } from "src/network/types/Block"; +import type { + Network, + NetworkGetBalanceArgs, + NetworkGetBlockArgs, + NetworkGetTransactionArgs, + NetworkWaitForTransactionArgs, +} from "src/network/types/Network"; +import type { Transaction, TransactionReceipt } from "src/network/types/Transaction"; + +/** + * A mock implementation of a `Network` designed to facilitate unit + * testing. + */ +export class NetworkStub implements Network { + protected getBalanceStub: + | SinonStub<[NetworkGetBalanceArgs?], Promise> + | undefined; + protected getBlockStub: + | SinonStub<[NetworkGetBlockArgs?], Promise> + | undefined; + protected getChainIdStub: SinonStub<[], Promise> | undefined; + protected getTransactionStub: + | SinonStub<[NetworkGetTransactionArgs?], Promise> + | undefined; + + stubGetBalance({ + args, + value, + }: { + args?: NetworkGetBalanceArgs | undefined; + value: bigint; + }): void { + if (!this.getBalanceStub) { + this.getBalanceStub = stub(); + } + + // Account for dynamic args if provided + if (args) { + this.getBalanceStub.withArgs(args).resolves(value); + return; + } + + this.getBalanceStub.resolves(value); + } + + stubGetBlock({ + args, + value, + }: { + args?: NetworkGetBlockArgs | undefined; + value: Block | undefined; + }): void { + if (!this.getBlockStub) { + this.getBlockStub = stub(); + } + + // Account for dynamic args if provided + if (args) { + this.getBlockStub.withArgs(args).resolves(value); + return; + } + + this.getBlockStub.resolves(value); + } + + stubGetChainId(id: number): void { + if (!this.getChainIdStub) { + this.getChainIdStub = stub(); + } + + this.getChainIdStub.resolves(id); + } + + stubGetTransaction({ + args, + value, + }: { + args?: NetworkGetTransactionArgs; + value: Transaction | undefined; + }): void { + if (!this.getTransactionStub) { + this.getTransactionStub = stub(); + } + + // Account for dynamic args if provided + if (args) { + this.getTransactionStub.withArgs(args).resolves(value); + return; + } + + this.getTransactionStub.resolves(value); + } + + getBalance(...args: NetworkGetBalanceArgs): Promise { + if (!this.getBalanceStub) { + throw new Error( + "The getBalance function must be stubbed first:\n\tcontract.stubGetBalance()", + ); + } + return this.getBalanceStub(args); + } + + getBlock(...args: NetworkGetBlockArgs): Promise { + if (!this.getBlockStub) { + throw new Error( + "The getBlock function must be stubbed first:\n\tcontract.stubGetBlock()", + ); + } + return this.getBlockStub(args); + } + + getChainId(): Promise { + if (!this.getChainIdStub) { + throw new Error( + "The getChainId function must be stubbed first:\n\tcontract.stubGetChainId()", + ); + } + return this.getChainIdStub(); + } + + getTransaction( + ...args: NetworkGetTransactionArgs + ): Promise { + if (!this.getTransactionStub) { + throw new Error( + "The getTransaction function must be stubbed first:\n\tcontract.stubGetTransaction()", + ); + } + return this.getTransactionStub(args); + } + + async waitForTransaction( + ...[hash, { timeout = 60_000 } = {}]: NetworkWaitForTransactionArgs + ): Promise { + // biome-ignore lint/suspicious/noAsyncPromiseExecutor: special case for testing + return new Promise(async (resolve) => { + let transaction: Transaction | undefined; + + transaction = await this.getTransactionStub?.([hash]).catch(); + + if (transaction) { + return resolve(transactionToReceipt(transaction)); + } + + // Poll for the transaction until it's found or the timeout is reached + let waitedTime = 0; + const interval = setInterval(async () => { + waitedTime += 1000; + transaction = await this.getTransactionStub?.([hash]).catch(); + if (transaction || waitedTime >= timeout) { + clearInterval(interval); + resolve(transactionToReceipt(transaction)); + } + }, 1000); + }); + } +} + +export function transactionToReceipt( + transaction: Transaction | undefined, +): TransactionReceipt | undefined { + return transaction + ? { + blockHash: transaction.blockHash!, + blockNumber: transaction.blockNumber!, + from: transaction.from!, + to: transaction.to!, + transactionIndex: transaction.transactionIndex!, + cumulativeGasUsed: 0n, + effectiveGasPrice: 0n, + transactionHash: transaction.hash!, + gasUsed: 0n, + logsBloom: "0x", + status: "success", + } + : undefined; +} diff --git a/packages/evm-client/src/network/types/Block.ts b/packages/evm-client/src/network/types/Block.ts new file mode 100644 index 00000000..da151e40 --- /dev/null +++ b/packages/evm-client/src/network/types/Block.ts @@ -0,0 +1,8 @@ +export interface Block { + /** `null` if pending */ + blockNumber: bigint | null; + timestamp: bigint; +} + +// https://github.com/ethereum/execution-apis/blob/3ae3d29fc9900e5c48924c238dff7643fdc3680e/src/schemas/block.yaml#L114 +export type BlockTag = "latest" | "earliest" | "pending" | "safe" | "finalized"; diff --git a/packages/evm-client/src/network/types/Network.ts b/packages/evm-client/src/network/types/Network.ts new file mode 100644 index 00000000..de200e58 --- /dev/null +++ b/packages/evm-client/src/network/types/Network.ts @@ -0,0 +1,76 @@ +import type { Block, BlockTag } from "src/network/types/Block"; +import type { Transaction, TransactionReceipt } from "src/network/types/Transaction"; + +// https://ethereum.github.io/execution-apis/api-documentation/ + +/** + * An interface representing data the SDK needs to get from the network. + */ +export interface Network { + /** + * Get the balance of native currency for an account. + */ + getBalance(...args: NetworkGetBalanceArgs): Promise; + + /** + * Get a block from a block tag, number, or hash. If no argument is provided, + * the latest block is returned. + */ + getBlock(...args: NetworkGetBlockArgs): Promise; + + /** + * Get the chain ID of the network. + */ + getChainId(): Promise; + + /** + * Get a transaction from a transaction hash. + */ + getTransaction( + ...args: NetworkGetTransactionArgs + ): Promise; + + /** + * Wait for a transaction to be mined. + */ + waitForTransaction( + ...args: NetworkWaitForTransactionArgs + ): Promise; +} + +export type NetworkGetBlockOptions = + | { + blockHash?: `0x${string}`; + blockNumber?: never; + blockTag?: never; + } + | { + blockHash?: never; + blockNumber?: bigint; + blockTag?: never; + } + | { + blockHash?: never; + blockNumber?: never; + blockTag?: BlockTag; + }; + +export type NetworkGetBalanceArgs = [ + address: `0x${string}`, + block?: NetworkGetBlockOptions, +]; + +export type NetworkGetBlockArgs = [options?: NetworkGetBlockOptions]; + +export type NetworkGetTransactionArgs = [hash: `0x${string}`]; + +export type NetworkWaitForTransactionArgs = [ + hash: `0x${string}`, + options?: { + /** + * The number of milliseconds to wait for the transaction until rejecting + * the promise. + */ + timeout?: number; + }, +]; diff --git a/packages/evm-client/src/network/types/Transaction.ts b/packages/evm-client/src/network/types/Transaction.ts new file mode 100644 index 00000000..c537e787 --- /dev/null +++ b/packages/evm-client/src/network/types/Transaction.ts @@ -0,0 +1,57 @@ +// https://github.com/ethereum/execution-apis/blob/e3d2745289bd2bb61dc8593069871be4be441952/src/schemas/transaction.yaml#L329 +export interface TransactionInfo { + blockHash?: `0x${string}`; + blockNumber?: bigint; + from?: `0x${string}`; + hash?: `0x${string}`; + transactionIndex?: number; +} + +/** Basic legacy compatible transaction */ +// https://github.com/ethereum/execution-apis/blob/e8727564bb74a1ebcd22a933b7b57eb7b71a11c3/src/schemas/transaction.yaml#L184 +export interface Transaction extends TransactionInfo { + type: `0x${string}`; + nonce: number; + gas: bigint; + value: bigint; + input: `0x${string}`; + gasPrice: bigint; + chainId?: number; + to?: `0x${string}` | null; +} + +export type MinedTransaction = Transaction & Required; + +// https://github.com/ethereum/execution-apis/blob/e3d2745289bd2bb61dc8593069871be4be441952/src/schemas/receipt.yaml#L37 +export interface TransactionReceipt { + blockHash: `0x${string}`; + blockNumber: bigint; + from: `0x${string}`; + /** + * Address of the receiver or `null` in a contract creation transaction. + */ + to: `0x${string}` | null; + /** + * The sum of gas used by this transaction and all preceding transactions in + * the same block. + */ + cumulativeGasUsed: bigint; + /** + * The amount of gas used for this specific transaction alone. + */ + gasUsed: bigint; + // TODO: + // logs: Log[]; + logsBloom: `0x${string}`; + transactionHash: `0x${string}`; + transactionIndex: number; + + status: "success" | "reverted"; + + /** + * The actual value per gas deducted from the sender's account. Before + * EIP-1559, this is equal to the transaction's gas price. After, it is equal + * to baseFeePerGas + min(maxFeePerGas - baseFeePerGas, maxPriorityFeePerGas). + */ + effectiveGasPrice: bigint; +} diff --git a/packages/evm-client/tsconfig.json b/packages/evm-client/tsconfig.json new file mode 100644 index 00000000..b0dd9ad7 --- /dev/null +++ b/packages/evm-client/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@repo/typescript-config/base.json", + "include": ["./*.ts", "src", "./.eslintrc.cjs"], + "exclude": ["node_modules"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": { + "src/*": ["src/*"] + } + } +} diff --git a/packages/evm-client/tsup.config.ts b/packages/evm-client/tsup.config.ts new file mode 100644 index 00000000..4d02f6ab --- /dev/null +++ b/packages/evm-client/tsup.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + // Splitting the entry points in foundational packages like this makes it + // easier for wrapper packages to selectively re-export `*` from some entry + // points and while augmenting or modifying others. + entry: [ + "src/exports/cache.ts", + "src/exports/contract.ts", + "src/exports/errors.ts", + "src/exports/index.ts", + "src/exports/network.ts", + "src/exports/stubs.ts", + ], + format: ["esm"], + sourcemap: true, + dts: true, + clean: true, + minify: true, + shims: true, + cjsInterop: true, +}); diff --git a/packages/evm-client/vite.config.ts b/packages/evm-client/vite.config.ts new file mode 100644 index 00000000..3b5ea6b9 --- /dev/null +++ b/packages/evm-client/vite.config.ts @@ -0,0 +1,6 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths() as any], +}); From 3e15ea3e49b8153d1eae1e81fb945f8d5ced7705 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 6 Oct 2024 12:02:51 -0500 Subject: [PATCH 34/49] Prep for build --- packages/drift/package.json | 29 ++++--------------- packages/drift/src/adapter/MockAdapter.ts | 5 ---- .../src/{exports.ts => exports/index.ts} | 0 packages/drift/src/exports/testing.ts | 6 ++++ packages/drift/tsup.config.ts | 12 +------- 5 files changed, 12 insertions(+), 40 deletions(-) rename packages/drift/src/{exports.ts => exports/index.ts} (100%) create mode 100644 packages/drift/src/exports/testing.ts diff --git a/packages/drift/package.json b/packages/drift/package.json index 246af59c..6cf2e34a 100644 --- a/packages/drift/package.json +++ b/packages/drift/package.json @@ -8,35 +8,16 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./cache": { - "types": "./dist/cache.d.ts", - "default": "./dist/cache.js" - }, - "./contract": { - "types": "./dist/contract.d.ts", - "default": "./dist/contract.js" - }, - "./errors": { - "types": "./dist/errors.d.ts", - "default": "./dist/errors.js" - }, - "./network": { - "types": "./dist/network.d.ts", - "default": "./dist/network.js" - }, - "./stubs": { - "types": "./dist/stubs.d.ts", - "default": "./dist/stubs.js" + "./testing": { + "types": "./dist/testing.d.ts", + "default": "./dist/testing.js" }, "./package.json": "./package.json" }, "typesVersions": { "*": { - "cache": ["./dist/cache.d.ts"], - "contract": ["./dist/contract.d.ts"], - "errors": ["./dist/errors.d.ts"], - "network": ["./dist/network.d.ts"], - "stubs": ["./dist/stubs.d.ts"] + ".": ["./dist/index.d.ts"], + "testing": ["./dist/testing.d.ts"] } }, "scripts": { diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index ac299a96..53883298 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -373,8 +373,3 @@ export class MockAdapter implements ReadWriteAdapter { })(); } } - -export type AdapterOnWriteParams< - TAbi extends Abi = Abi, - TFunctionName extends FunctionName = FunctionName, -> = OptionalKeys, "args" | "address">; diff --git a/packages/drift/src/exports.ts b/packages/drift/src/exports/index.ts similarity index 100% rename from packages/drift/src/exports.ts rename to packages/drift/src/exports/index.ts diff --git a/packages/drift/src/exports/testing.ts b/packages/drift/src/exports/testing.ts new file mode 100644 index 00000000..16519c27 --- /dev/null +++ b/packages/drift/src/exports/testing.ts @@ -0,0 +1,6 @@ +export { MockAdapter } from "src/adapter/MockAdapter"; +export { + type MockContractParams, + MockContract, +} from "src/client/Contract/MockContract"; +export { MockDrift } from "src/client/Drift/MockDrift"; diff --git a/packages/drift/tsup.config.ts b/packages/drift/tsup.config.ts index 4d02f6ab..891d6c4c 100644 --- a/packages/drift/tsup.config.ts +++ b/packages/drift/tsup.config.ts @@ -1,17 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - // Splitting the entry points in foundational packages like this makes it - // easier for wrapper packages to selectively re-export `*` from some entry - // points and while augmenting or modifying others. - entry: [ - "src/exports/cache.ts", - "src/exports/contract.ts", - "src/exports/errors.ts", - "src/exports/index.ts", - "src/exports/network.ts", - "src/exports/stubs.ts", - ], + entry: ["src/exports/index.ts", "src/exports/testing.ts"], format: ["esm"], sourcemap: true, dts: true, From 1bcd048fb1239dd8db4dcdcfe3eeb4c0fb45ea9a Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 6 Oct 2024 12:03:20 -0500 Subject: [PATCH 35/49] yarn --- yarn.lock | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/yarn.lock b/yarn.lock index 8962a931..5544c1c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -294,6 +294,13 @@ human-id "^1.0.2" prettier "^2.7.1" +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@esbuild/aix-ppc64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" @@ -437,6 +444,11 @@ "@jridgewell/sourcemap-codec" "^1.4.10" "@jridgewell/trace-mapping" "^0.3.9" +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + "@jridgewell/resolve-uri@^3.1.0": version "3.1.1" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" @@ -452,6 +464,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.9": version "0.3.22" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz#72a621e5de59f5f1ef792d0793a82ee20f645e4c" @@ -652,6 +672,26 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -755,6 +795,13 @@ abitype@1.0.0, abitype@^1.0.0: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + acorn-walk@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" @@ -765,6 +812,11 @@ acorn@^8.10.0, acorn@^8.11.3: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.11.0, acorn@^8.4.1: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + aes-js@4.0.0-beta.5: version "4.0.0-beta.5" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" @@ -822,6 +874,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1069,6 +1126,11 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -1175,6 +1237,11 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diff@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" @@ -2056,6 +2123,11 @@ magic-string@^0.30.5: dependencies: "@jridgewell/sourcemap-codec" "^1.4.15" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" @@ -2991,6 +3063,25 @@ ts-interface-checker@^0.1.9: resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfck@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.2.tgz#d8e279f7a049d55f207f528d13fa493e1d8e7ceb" @@ -3169,6 +3260,11 @@ universalify@^0.1.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -3432,6 +3528,11 @@ yargs@^17.7.1: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 7508668f28067fe1702073cdfcebfc90c19f54ff Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Sun, 6 Oct 2024 12:47:55 -0500 Subject: [PATCH 36/49] Rename type, remove changelog --- packages/drift/CHANGELOG.md | 157 ------------------------ packages/drift/src/adapter/types/Abi.ts | 6 +- packages/drift/src/exports/index.ts | 2 +- packages/drift/src/utils/types.ts | 6 +- 4 files changed, 7 insertions(+), 164 deletions(-) delete mode 100644 packages/drift/CHANGELOG.md diff --git a/packages/drift/CHANGELOG.md b/packages/drift/CHANGELOG.md deleted file mode 100644 index 709a38a1..00000000 --- a/packages/drift/CHANGELOG.md +++ /dev/null @@ -1,157 +0,0 @@ -# @delvtech/evm-client - -## 0.5.1 - -### Patch Changes - -- 66d9dc3: Added `ConstructorArgs` type - -## 0.5.0 - -### Minor Changes - -- 919f525: Renamed `deleteReadMatch` to `deleteReadsMatching` - -### Patch Changes - -- 93531e6: Added getChainId method to the Network interface. - -## 0.4.2 - -### Patch Changes - -- 52edea9: Add deleteReadMatch method to CachedReadContract - -## 0.4.1 - -### Patch Changes - -- 4609a60: Export TransactionReceipt type - -## 0.4.0 - -### Minor Changes - -- 51fd2e4: Add status field to Transaction - -## 0.3.1 - -### Patch Changes - -- 5c35487: Fix error with `NamedEventInput` type which was broken and causing broken downstream types such as `EventFilter` - -## 0.3.0 - -### Minor Changes - -- 91106f8: Add a getBalance method to the Network interface for fetching native currency balances (e.g. ETH) - -## 0.2.4 - -### Patch Changes - -- 322edf5: Remove onTransactionMined - -## 0.2.3 - -### Patch Changes - -- e3880c8: Add txHash to onTransactionMined - -## 0.2.2 - -### Patch Changes - -- 01ec0b1: Add onTransactionMined callback to ReadWriteContract -- 01ec0b1: Make onTransactionMined optional - -## 0.2.1 - -### Patch Changes - -- 5f6a374: Add `waitForTransaction` method to `Network` type. - -## 0.2.0 - -### Minor Changes - -- affd95f: Add `entries` property to the `SimpleCache` type. - -## 0.1.1 - -### Patch Changes - -- eb6575b: Added a `cache` property to the `CachedReadContract` type and ensured the factories preserve the prototypes of the contract's they're given. - -## 0.1.0 - -### Minor Changes - -- cc17b3c: Changed the type of all inputs to objects. This means that functions with a single argument (e.g., `balanceOf` will now expect ``{ owner: `0x${string}` }``, not `` `0x${string}` ``). Outputs remain the "Friendly" type which deconstructs to a single primitive type for single outputs values (e.g., `symbol` will return a `string`, not `{ "0": string }`) since many single output return values are unnamed - -## 0.0.11 - -### Patch Changes - -- 1098f69: Fix bug causing stub lookups to fail - -## 0.0.10 - -### Patch Changes - -- 5cf2921: Add ability to stub options to stubRead - -## 0.0.9 - -### Patch Changes - -- dd129be: Use stable stringify for stub keys - -## 0.0.8 - -### Patch Changes - -- 593a286: Add ability to stub events for dynamic filter args - -## 0.0.7 - -### Patch Changes - -- 9db4f0f: Fix argument handling for parameters that have empty strings as names - -## 0.0.6 - -### Patch Changes - -- a2edb5a: Fix handling of single params - -## 0.0.5 - -### Patch Changes - -- 6307e1b: Fix the last fix... - -## 0.0.4 - -### Patch Changes - -- cfdf0db: Fix param prep for contract calls with single params - -## 0.0.3 - -### Patch Changes - -- 76b1bc8: Fix type resolutions by adding a `typeVersions` field to the `package.json`s -- fdcc9ef: Modify exports - -## 0.0.2 - -### Patch Changes - -- 6d60418: Added NetworkGetBlockOptions type - -## 0.0.1 - -### Patch Changes - -- e2f697f: Initial release! πŸš€ diff --git a/packages/drift/src/adapter/types/Abi.ts b/packages/drift/src/adapter/types/Abi.ts index dbe74f7b..1fbaaf5b 100644 --- a/packages/drift/src/adapter/types/Abi.ts +++ b/packages/drift/src/adapter/types/Abi.ts @@ -11,7 +11,7 @@ import type { EmptyObject, MergeKeys, Pretty, - ReplaceKeys, + ReplaceProps, } from "src/utils/types"; // https://docs.soliditylang.org/en/latest/abi-spec.html#json @@ -19,7 +19,7 @@ import type { export type NamedAbiParameter = AbiParameter extends infer TAbiParameter ? TAbiParameter extends { name: string } ? TAbiParameter - : ReplaceKeys + : ReplaceProps : never; /** @@ -97,7 +97,7 @@ type WithDefaultNames = { AbiParameter ? TParameter extends NamedAbiParameter ? TParameter - : ReplaceKeys, { name: `${K}` | string }> + : ReplaceProps, { name: `${K}` | string }> : never; }; diff --git a/packages/drift/src/exports/index.ts b/packages/drift/src/exports/index.ts index 01162a8c..076f4905 100644 --- a/packages/drift/src/exports/index.ts +++ b/packages/drift/src/exports/index.ts @@ -127,7 +127,7 @@ export type { MergeKeys, OptionalKeys, Pretty, - ReplaceKeys, + ReplaceProps, RequiredKeys, UnionToIntersection, } from "src/utils/types"; diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index f52c50c5..3a17ee61 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -14,13 +14,13 @@ export type Pretty = { [K in keyof T]: T[K] } & {}; /** * Replace properties in `T` with properties in `U`. */ -export type ReplaceKeys = Pretty & U>; +export type ReplaceProps = Pretty & U>; /** * Make all properties in `T` whose keys are in the union `K` required and * non-nullable. */ -export type RequiredKeys = ReplaceKeys< +export type RequiredKeys = ReplaceProps< T, { [U in K]-?: NonNullable; @@ -30,7 +30,7 @@ export type RequiredKeys = ReplaceKeys< /** * Make all properties in `T` whose keys are in the union `K` optional. */ -export type OptionalKeys = ReplaceKeys< +export type OptionalKeys = ReplaceProps< T, { [U in K]?: T[U]; From 23f40a3e7794c532f80c69351869e344f879ec5a Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 00:45:36 -0500 Subject: [PATCH 37/49] Add drift-viem --- package.json | 1 + packages/drift-viem/.env.example | 5 + packages/drift-viem/.gitignore | 2 + packages/drift-viem/README.md | 22 ++ .../integration-tests/ViemReadAdapter.test.ts | 194 ++++++++++++ .../integration-tests/artifacts/erc20.ts | 141 +++++++++ .../integration-tests/viemAdapter.test.ts | 24 ++ packages/drift-viem/package.json | 58 ++++ packages/drift-viem/src/ReadAdapter.ts | 279 ++++++++++++++++++ packages/drift-viem/src/ReadWriteAdapter.ts | 71 +++++ .../utils/createSimulateContractParameters.ts | 65 ++++ .../drift-viem/src/utils/outputToFriendly.ts | 38 +++ packages/drift-viem/src/viemAdapter.ts | 27 ++ packages/drift-viem/tsconfig.json | 15 + packages/drift-viem/tsup.config.ts | 12 + packages/drift-viem/vite.config.ts | 6 + .../drift/src/adapter/MockAdapter.test.ts | 8 +- packages/drift/src/adapter/MockAdapter.ts | 42 ++- packages/drift/src/adapter/types/Adapter.ts | 4 +- packages/drift/src/adapter/types/Event.ts | 2 +- packages/drift/src/cache/ClientCache/types.ts | 4 +- .../drift/src/client/Contract/Contract.ts | 6 +- .../src/client/Contract/MockContract.test.ts | 20 +- .../drift/src/client/Contract/MockContract.ts | 92 ++++-- packages/drift/src/client/Drift/Drift.ts | 4 +- .../drift/src/client/Drift/MockDrift.test.ts | 8 +- packages/drift/src/client/Drift/MockDrift.ts | 6 +- packages/drift/src/exports/index.ts | 3 +- packages/drift/src/utils/types.ts | 17 ++ 29 files changed, 1117 insertions(+), 59 deletions(-) create mode 100644 packages/drift-viem/.env.example create mode 100644 packages/drift-viem/.gitignore create mode 100644 packages/drift-viem/README.md create mode 100644 packages/drift-viem/integration-tests/ViemReadAdapter.test.ts create mode 100644 packages/drift-viem/integration-tests/artifacts/erc20.ts create mode 100644 packages/drift-viem/integration-tests/viemAdapter.test.ts create mode 100644 packages/drift-viem/package.json create mode 100644 packages/drift-viem/src/ReadAdapter.ts create mode 100644 packages/drift-viem/src/ReadWriteAdapter.ts create mode 100644 packages/drift-viem/src/utils/createSimulateContractParameters.ts create mode 100644 packages/drift-viem/src/utils/outputToFriendly.ts create mode 100644 packages/drift-viem/src/viemAdapter.ts create mode 100644 packages/drift-viem/tsconfig.json create mode 100644 packages/drift-viem/tsup.config.ts create mode 100644 packages/drift-viem/vite.config.ts diff --git a/package.json b/package.json index aa56e6f0..a35c8fca 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "drift", "private": true, "scripts": { + "cli": "yarn workspace @delvtech/drift-cli dev", "build:packages": "turbo build --filter=./packages/*", "build": "turbo build", "dev": "turbo dev", diff --git a/packages/drift-viem/.env.example b/packages/drift-viem/.env.example new file mode 100644 index 00000000..c8bd068c --- /dev/null +++ b/packages/drift-viem/.env.example @@ -0,0 +1,5 @@ +# The RPC URL to use in integration tests. +VITE_RPC_URL= + +# The token address to use in integration tests. +VITE_TOKEN_ADDRESS=0x6B175474E89094C44Da98b954EedeAC495271d0F # Mainnet DAI diff --git a/packages/drift-viem/.gitignore b/packages/drift-viem/.gitignore new file mode 100644 index 00000000..db4c6d9b --- /dev/null +++ b/packages/drift-viem/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/packages/drift-viem/README.md b/packages/drift-viem/README.md new file mode 100644 index 00000000..80c76fea --- /dev/null +++ b/packages/drift-viem/README.md @@ -0,0 +1,22 @@ +# @delvtech/drift-viem + +Viem adapter for [@delvtech/drift](https://github.com/delvtech/evm-client/). + +```ts +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { createPublicClient, http } from "viem"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +// optionally, create a wallet client +const walletClient = createWalletClient({ + transport: http(), + // ...other options +}); + +const drift = new Drift(viemAdapter({ publicClient, walletClient })); +``` diff --git a/packages/drift-viem/integration-tests/ViemReadAdapter.test.ts b/packages/drift-viem/integration-tests/ViemReadAdapter.test.ts new file mode 100644 index 00000000..c24fbf40 --- /dev/null +++ b/packages/drift-viem/integration-tests/ViemReadAdapter.test.ts @@ -0,0 +1,194 @@ +import type { + Block, + ContractEvent, + DecodedFunctionData, + FunctionArgs, + Transaction, + TransactionReceipt, +} from "@delvtech/drift"; +import { erc20 } from "integration-tests/artifacts/erc20"; +import { ViemReadAdapter } from "src/ReadAdapter"; +import { http, type Address, createPublicClient } from "viem"; +import { describe, expect, it } from "vitest"; + +const { VITE_RPC_URL = "", VITE_TOKEN_ADDRESS = "" } = process.env; +const publicClient = createPublicClient({ + transport: http(VITE_RPC_URL), +}); + +describe("ViemReadAdapter", () => { + it("fetches the chain id", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const chainId = await adapter.getChainId(); + expect(chainId).toBeTypeOf("number"); + }); + + it("fetches the current block", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const block = await adapter.getBlock(); + expect(block).toEqual({ + blockNumber: expect.any(BigInt), + timestamp: expect.any(BigInt), + } as Block); + }); + + it("fetches account balances", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const balance = await adapter.getBalance({ + address: VITE_TOKEN_ADDRESS, + }); + expect(balance).toBeTypeOf("bigint"); + }); + + it("fetches transactions", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + let block = await publicClient.getBlock(); + while (block.transactions.length === 0) { + console.log( + `No transactions in block ${block.number}. Fetching parent block.`, + ); + block = await publicClient.getBlock({ blockHash: block.parentHash }); + } + const tx = await adapter.getTransaction({ + hash: block.transactions[0]!, + }); + expect(tx).toEqual( + expect.objectContaining({ + gas: expect.any(BigInt), + gasPrice: expect.any(BigInt), + input: expect.any(String), + nonce: expect.any(Number), + type: expect.any(String), + value: expect.any(BigInt), + blockHash: expect.any(String), + blockNumber: expect.any(BigInt), + from: expect.any(String), + hash: expect.any(String), + to: expect.any(String), + transactionIndex: expect.any(Number), + } as Transaction), + ); + }); + + it("returns receipts for waited transactions", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + let block = await publicClient.getBlock(); + while (block.transactions.length === 0) { + console.log( + `No transactions in block ${block.number}. Fetching parent block.`, + ); + block = await publicClient.getBlock({ blockHash: block.parentHash }); + } + const tx = await adapter.waitForTransaction({ + hash: block.transactions[0]!, + }); + expect(tx).toEqual( + expect.objectContaining({ + blockHash: expect.any(String), + blockNumber: expect.any(BigInt), + from: expect.any(String), + cumulativeGasUsed: expect.any(BigInt), + effectiveGasPrice: expect.any(BigInt), + gasUsed: expect.any(BigInt), + logsBloom: expect.any(String), + status: expect.stringMatching(/^(success|reverted)$/), + to: expect.any(String), + transactionHash: expect.any(String), + transactionIndex: expect.any(Number), + } as TransactionReceipt), + ); + }); + + it("fetches events", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const currentBlock = await publicClient.getBlockNumber(); + const events = await adapter.getEvents({ + abi: erc20.abi, + address: VITE_TOKEN_ADDRESS, + event: "Transfer", + fromBlock: currentBlock - 100n, + }); + expect(events).toBeInstanceOf(Array); + expect(events[0]).toEqual( + expect.objectContaining({ + args: expect.any(Object), + blockNumber: expect.any(BigInt), + data: expect.any(String), + transactionHash: expect.any(String), + } as ContractEvent), + ); + }); + + describe("read", () => { + it("reads from contracts", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const symbol = await adapter.read({ + abi: erc20.abi, + address: VITE_TOKEN_ADDRESS, + fn: "symbol", + }); + expect(symbol).toBeTypeOf("string"); + }); + + it("reads from contracts with args", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const balance = await adapter.read({ + abi: erc20.abi, + address: VITE_TOKEN_ADDRESS, + fn: "balanceOf", + args: { + account: VITE_TOKEN_ADDRESS as Address, + }, + }); + expect(balance).toBeTypeOf("bigint"); + }); + }); + + it("simulates writes to a contracts", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const balance = await adapter.simulateWrite({ + abi: erc20.abi, + address: VITE_TOKEN_ADDRESS, + fn: "transfer", + args: { + amount: 1n, + to: VITE_TOKEN_ADDRESS as Address, + }, + }); + expect(balance).toBeTypeOf("boolean"); + }); + + it("encodes function data", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const encoded = adapter.encodeFunctionData({ + abi: erc20.abi, + fn: "transfer", + args: { + amount: 123n, + to: VITE_TOKEN_ADDRESS as Address, + }, + }); + expect(encoded).toBeTypeOf("string"); + }); + + it("decodes function data", async () => { + const adapter = new ViemReadAdapter({ publicClient }); + const args: FunctionArgs = { + amount: 123n, + to: VITE_TOKEN_ADDRESS as Address, + }; + const decoded = adapter.decodeFunctionData({ + abi: erc20.abi, + fn: "transfer", + data: adapter.encodeFunctionData({ + abi: erc20.abi, + fn: "transfer", + args, + }), + }); + expect(decoded).toEqual({ + args, + functionName: "transfer", + } as DecodedFunctionData); + }); +}); diff --git a/packages/drift-viem/integration-tests/artifacts/erc20.ts b/packages/drift-viem/integration-tests/artifacts/erc20.ts new file mode 100644 index 00000000..30336081 --- /dev/null +++ b/packages/drift-viem/integration-tests/artifacts/erc20.ts @@ -0,0 +1,141 @@ +export const erc20 = { + abi: [ + { + type: "function", + name: "allowance", + inputs: [ + { name: "owner", type: "address", internalType: "address" }, + { name: "spender", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "approve", + inputs: [ + { name: "spender", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "balanceOf", + inputs: [{ name: "account", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "decimals", + inputs: [], + outputs: [{ name: "", type: "uint8", internalType: "uint8" }], + stateMutability: "view", + }, + { + type: "function", + name: "name", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "symbol", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "totalSupply", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "transfer", + inputs: [ + { name: "to", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "transferFrom", + inputs: [ + { name: "from", type: "address", internalType: "address" }, + { name: "to", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "event", + name: "Approval", + inputs: [ + { + name: "owner", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "spender", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "value", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Transfer", + inputs: [ + { + name: "from", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "to", + type: "address", + indexed: true, + internalType: "address", + }, + { + name: "value", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + ], + methodIdentifiers: { + "allowance(address,address)": "dd62ed3e", + "approve(address,uint256)": "095ea7b3", + "balanceOf(address)": "70a08231", + "decimals()": "313ce567", + "name()": "06fdde03", + "symbol()": "95d89b41", + "totalSupply()": "18160ddd", + "transfer(address,uint256)": "a9059cbb", + "transferFrom(address,address,uint256)": "23b872dd", + }, +} as const; diff --git a/packages/drift-viem/integration-tests/viemAdapter.test.ts b/packages/drift-viem/integration-tests/viemAdapter.test.ts new file mode 100644 index 00000000..3755d505 --- /dev/null +++ b/packages/drift-viem/integration-tests/viemAdapter.test.ts @@ -0,0 +1,24 @@ +import { ViemReadAdapter } from "src/ReadAdapter"; +import { ViemReadWriteAdapter } from "src/ReadWriteAdapter"; +import { viemAdapter } from "src/viemAdapter"; +import { http, createPublicClient, createWalletClient } from "viem"; +import { describe, expect, it } from "vitest"; + +const publicClient = createPublicClient({ + transport: http("https://localhost:8545"), +}); +const walletClient = createWalletClient({ + transport: http("https://localhost:8545"), +}); + +describe("viemAdapter", () => { + it("Creates a read adapter if no wallet client is provided", async () => { + const adapter = viemAdapter({ publicClient }); + expect(adapter).toBeInstanceOf(ViemReadAdapter); + }); + + it("Creates a read adapter if a wallet client is provided", async () => { + const adapter = viemAdapter({ publicClient, walletClient }); + expect(adapter).toBeInstanceOf(ViemReadWriteAdapter); + }); +}); diff --git a/packages/drift-viem/package.json b/packages/drift-viem/package.json new file mode 100644 index 00000000..d219b28d --- /dev/null +++ b/packages/drift-viem/package.json @@ -0,0 +1,58 @@ +{ + "name": "@delvtech/drift-viem", + "version": "0.0.0", + "license": "MIT", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./testing": { + "types": "./dist/testing.d.ts", + "default": "./dist/testing.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + ".": ["./dist/index.d.ts"], + "testing": ["./dist/testing.d.ts"] + } + }, + "scripts": { + "build": "tsup", + "test:watch": "vitest", + "test": "vitest run", + "test:integration": "vitest run integration", + "typecheck": "tsc --noEmit", + "watch": "tsup --watch" + }, + "peerDependencies": { + "@delvtech/drift": "0.0.0", + "sinon": "^17.0.1", + "viem": "^2" + }, + "peerDependenciesMeta": { + "sinon": { + "optional": true + } + }, + "devDependencies": { + "@delvtech/drift": "0.0.0", + "@repo/typescript-config": "*", + "sinon": "^17.0.1", + "tsconfig-paths": "^4.2.0", + "tsup": "^8.0.2", + "typescript": "^5.3.3", + "viem": "^2.7.3", + "vite-tsconfig-paths": "^4.3.1", + "vitest": "^1.2.2" + }, + "publishConfig": { + "access": "public" + }, + "files": ["dist"] +} diff --git a/packages/drift-viem/src/ReadAdapter.ts b/packages/drift-viem/src/ReadAdapter.ts new file mode 100644 index 00000000..3e05afd5 --- /dev/null +++ b/packages/drift-viem/src/ReadAdapter.ts @@ -0,0 +1,279 @@ +import { + type AbiObjectType, + type AdapterDecodeFunctionDataParams, + type AdapterEncodeFunctionDataParams, + type AdapterGetEventsParams, + type AdapterReadParams, + type AdapterWriteParams, + type DecodedFunctionData, + type EventName, + type FunctionName, + type FunctionReturn, + type NetworkGetBalanceParams, + type NetworkGetBlockParams, + type NetworkGetTransactionParams, + type NetworkWaitForTransactionParams, + type ReadAdapter, + arrayToObject, + objectToArray, +} from "@delvtech/drift"; +import { createSimulateContractParameters } from "src/utils/createSimulateContractParameters"; +import { outputToFriendly } from "src/utils/outputToFriendly"; +import { + type Abi, + type Address, + type GetBalanceParameters, + type GetBlockParameters, + type Hash, + type Hex, + type PublicClient, + decodeFunctionData, + encodeFunctionData, + rpcTransactionType, +} from "viem"; + +export interface ViemReadAdapterParams { + publicClient: PublicClient; +} + +export class ViemReadAdapter implements ReadAdapter { + publicClient: PublicClient; + + constructor({ publicClient }: ViemReadAdapterParams) { + this.publicClient = publicClient; + } + + getChainId = () => this.publicClient.getChainId(); + + getBlock = (params: NetworkGetBlockParams = {}) => { + return this.publicClient + .getBlock(params as GetBlockParameters) + .then((block) => { + if (!block) { + return undefined; + } + + return { + blockNumber: block.number, + timestamp: block.timestamp, + }; + }); + }; + + getBalance = ({ + address, + blockNumber, + blockTag, + blockHash, + }: NetworkGetBalanceParams) => { + const parameters: Partial = { + address: address as Address, + }; + + if (blockNumber) { + parameters.blockNumber = blockNumber; + } else if (blockTag) { + parameters.blockTag = blockHash; + } else if (blockHash) { + return this.getBlock({ blockHash }).then((block) => { + return this.publicClient.getBalance({ + address: address as Address, + blockNumber: block?.blockNumber ?? undefined, + }); + }); + } + + return this.publicClient.getBalance(parameters as GetBalanceParameters); + }; + + getTransaction = ({ hash }: NetworkGetTransactionParams) => { + return this.publicClient + .getTransaction({ + hash: hash as Hash, + }) + .then( + ({ + gas, + gasPrice, + input, + nonce, + type, + value, + blockHash, + blockNumber, + from, + chainId, + hash, + to, + transactionIndex, + }) => { + return { + gas, + gasPrice: gasPrice as bigint, + input, + nonce, + type: rpcTransactionType[type], + value, + blockHash: blockHash ?? undefined, + blockNumber: blockNumber ?? undefined, + from, + chainId, + hash, + to, + transactionIndex: transactionIndex ?? undefined, + }; + }, + ); + }; + + waitForTransaction = ({ hash, timeout }: NetworkWaitForTransactionParams) => { + return this.publicClient.waitForTransactionReceipt({ + hash: hash as Hash, + timeout, + }); + }; + + getEvents = >({ + abi, + address, + event, + filter, + fromBlock, + toBlock, + }: AdapterGetEventsParams) => { + return this.publicClient + .getContractEvents({ + address: address as Address, + abi: abi as Abi, + eventName: event as string, + fromBlock: fromBlock ?? "earliest", + toBlock: toBlock ?? "latest", + args: filter, + }) + .then((events) => { + return events.map(({ args, blockNumber, data, transactionHash }) => { + const objectArgs = Array.isArray(args) + ? arrayToObject({ + abi: abi as Abi, + type: "event", + name: event, + kind: "inputs", + values: args, + }) + : (args as AbiObjectType); + + return { + args: objectArgs, + blockNumber: blockNumber ?? undefined, + data, + eventName: event, + transactionHash: transactionHash ?? undefined, + }; + }); + }); + }; + + read = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + address, + fn, + args, + block, + }: AdapterReadParams) => { + const argsArray = objectToArray({ + abi: abi as Abi, + type: "function", + name: fn, + kind: "inputs", + value: args, + }); + + return this.publicClient + .readContract({ + abi: abi as Abi, + address: address as Address, + functionName: fn, + args: argsArray, + blockNumber: typeof block === "bigint" ? block : undefined, + blockTag: typeof block === "string" ? block : undefined, + }) + .then((output) => { + return outputToFriendly({ + abi, + functionName: fn, + output, + }) as FunctionReturn; + }); + }; + + simulateWrite = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: AdapterWriteParams, + ) => { + return this.publicClient + .simulateContract(createSimulateContractParameters(params)) + .then(({ result }) => { + return outputToFriendly({ + abi: params.abi, + functionName: params.fn, + output: result, + }) as FunctionReturn; + }); + }; + + encodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + fn, + args, + }: AdapterEncodeFunctionDataParams) => { + const arrayArgs = objectToArray({ + abi: abi as Abi, + type: "function", + name: fn, + kind: "inputs", + value: args, + }); + + return encodeFunctionData({ + abi: abi as Abi, + functionName: fn as string, + args: arrayArgs, + }); + }; + + decodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + data, + fn, + }: AdapterDecodeFunctionDataParams) => { + const { args, functionName } = decodeFunctionData({ + abi: abi, + data: data as Hex, + }); + + const arrayArgs = Array.isArray(args) ? args : [args]; + + return { + args: arrayToObject({ + // Cast to allow any array type for values + abi: abi as Abi, + type: "function", + name: functionName, + kind: "inputs", + values: arrayArgs, + }), + functionName, + } as DecodedFunctionData; + }; +} diff --git a/packages/drift-viem/src/ReadWriteAdapter.ts b/packages/drift-viem/src/ReadWriteAdapter.ts new file mode 100644 index 00000000..610b75d4 --- /dev/null +++ b/packages/drift-viem/src/ReadWriteAdapter.ts @@ -0,0 +1,71 @@ +import type { + AdapterWriteParams, + FunctionName, + FunctionReturn, + ReadWriteAdapter, +} from "@delvtech/drift"; +import { ViemReadAdapter, type ViemReadAdapterParams } from "src/ReadAdapter"; +import { createSimulateContractParameters } from "src/utils/createSimulateContractParameters"; +import { outputToFriendly } from "src/utils/outputToFriendly"; +import type { Abi, WalletClient, WriteContractParameters } from "viem"; + +export interface ViemReadWriteAdapterParams extends ViemReadAdapterParams { + walletClient: WalletClient; +} + +export class ViemReadWriteAdapter + extends ViemReadAdapter + implements ReadWriteAdapter +{ + walletClient: WalletClient; + + constructor({ walletClient, ...rest }: ViemReadWriteAdapterParams) { + super(rest); + this.walletClient = walletClient; + } + + getSignerAddress = () => { + return this.walletClient.getAddresses().then(([address]) => address!); + }; + + // override to get the account from the wallet client + simulateWrite = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: AdapterWriteParams, + ) => { + return this.getSignerAddress().then((from) => { + const viemParams = createSimulateContractParameters( + Object.assign({}, params, { from }), + ); + return this.publicClient + .simulateContract(viemParams) + .then(({ result }) => { + return outputToFriendly({ + abi: params.abi, + functionName: params.fn, + output: result, + }) as FunctionReturn; + }); + }); + }; + + write = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: AdapterWriteParams, + ) => { + return this.getSignerAddress().then((from) => { + const viemParams = createSimulateContractParameters( + Object.assign({}, params, { from }), + ); + return this.publicClient + .simulateContract(viemParams) + .then(({ request }) => + this.walletClient.writeContract(request as WriteContractParameters), + ); + }); + }; +} diff --git a/packages/drift-viem/src/utils/createSimulateContractParameters.ts b/packages/drift-viem/src/utils/createSimulateContractParameters.ts new file mode 100644 index 00000000..b204bb16 --- /dev/null +++ b/packages/drift-viem/src/utils/createSimulateContractParameters.ts @@ -0,0 +1,65 @@ +import { + type AdapterWriteParams, + type FunctionName, + objectToArray, +} from "@delvtech/drift"; +import type { Abi, SimulateContractParameters } from "viem"; + +/** + * Get parameters for `simulateContract` from `ContractWriteOptions` + */ +export function createSimulateContractParameters< + TAbi extends Abi, + TFunctionName extends FunctionName, +>({ + abi, + address, + fn, + args, + accessList, + from, + gas, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + nonce, + value, +}: AdapterWriteParams): SimulateContractParameters { + const argsArray = objectToArray({ + abi: abi as Abi, + type: "function", + name: fn, + kind: "inputs", + value: args, + }); + + const gasPriceOptions = + gasPrice !== undefined + ? { gasPrice } + : { maxFeePerGas, maxPriorityFeePerGas }; + + return { + abi, + address, + functionName: fn, + args: argsArray, + accessList, + account: from, + gas, + nonce: nonce !== undefined ? Number(nonce) : undefined, + value, + ...gasPriceOptions, + } as SimulateContractParameters; +} + +// type SimulateContractParameters = { +// accessList?: ContractWriteOptions["accessList"]; +// account?: `0x${string}`; +// gas?: bigint; +// nonce?: number; +// value?: bigint; +// } & ( +// | { gasPrice?: bigint } +// | { maxFeePerGas?: bigint } +// | { maxPriorityFeePerGas?: bigint } +// ); diff --git a/packages/drift-viem/src/utils/outputToFriendly.ts b/packages/drift-viem/src/utils/outputToFriendly.ts new file mode 100644 index 00000000..4a2d5a73 --- /dev/null +++ b/packages/drift-viem/src/utils/outputToFriendly.ts @@ -0,0 +1,38 @@ +import { + type FunctionReturn, + arrayToFriendly, + getAbiEntry, +} from "@delvtech/drift"; +import type { Abi } from "viem"; + +export function outputToFriendly({ + abi, + functionName, + output, +}: { + abi: TAbi; + functionName: string; + output: unknown; +}) { + // Viem automatically returns a single value if the function has only one + // output parameter so we don't need to convert it. It's important to + // check the ABI to determine the number of output parameters vs. checking + // if the output is an array because the outputs could be a single array + // (tuple) parameter. + const abiEntry = getAbiEntry({ + abi, + type: "function", + name: functionName, + }); + if (abiEntry.outputs.length === 1) { + return output as FunctionReturn; + } + + return arrayToFriendly({ + abi: abi as Abi, + type: "function", + name: functionName, + kind: "outputs", + values: output as unknown[], + }) as FunctionReturn; +} diff --git a/packages/drift-viem/src/viemAdapter.ts b/packages/drift-viem/src/viemAdapter.ts new file mode 100644 index 00000000..8f4ef61c --- /dev/null +++ b/packages/drift-viem/src/viemAdapter.ts @@ -0,0 +1,27 @@ +import type { Adapter, ReadAdapter, ReadWriteAdapter } from "@delvtech/drift"; +import { ViemReadAdapter } from "src/ReadAdapter"; +import { ViemReadWriteAdapter } from "src/ReadWriteAdapter"; +import type { PublicClient, WalletClient } from "viem"; + +export interface ViemAdapterParams< + TPublicClient extends PublicClient = PublicClient, + TWalletClient extends WalletClient | undefined = undefined, +> { + publicClient: TPublicClient; + walletClient?: TWalletClient; +} + +export function viemAdapter< + TPublicClient extends PublicClient, + TWalletClient extends WalletClient | undefined, +>({ + publicClient, + walletClient, +}: ViemAdapterParams< + TPublicClient, + TWalletClient +>): TWalletClient extends undefined ? ReadAdapter : ReadWriteAdapter { + return walletClient + ? new ViemReadWriteAdapter({ publicClient, walletClient }) + : (new ViemReadAdapter({ publicClient }) as Adapter as ReadWriteAdapter); +} diff --git a/packages/drift-viem/tsconfig.json b/packages/drift-viem/tsconfig.json new file mode 100644 index 00000000..d785e1bb --- /dev/null +++ b/packages/drift-viem/tsconfig.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "@repo/typescript-config/base.json", + "include": ["./*.ts", "src", "integration-tests"], + "exclude": ["node_modules"], + "compilerOptions": { + "rootDir": ".", + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "Bundler", + "paths": { + "src/*": ["src/*"] + } + } +} diff --git a/packages/drift-viem/tsup.config.ts b/packages/drift-viem/tsup.config.ts new file mode 100644 index 00000000..cfe2fee9 --- /dev/null +++ b/packages/drift-viem/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts", "src/stubs.ts"], + format: ["esm"], + sourcemap: true, + dts: true, + clean: true, + minify: true, + shims: true, + cjsInterop: true, +}); diff --git a/packages/drift-viem/vite.config.ts b/packages/drift-viem/vite.config.ts new file mode 100644 index 00000000..3b5ea6b9 --- /dev/null +++ b/packages/drift-viem/vite.config.ts @@ -0,0 +1,6 @@ +import tsconfigPaths from "vite-tsconfig-paths"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [tsconfigPaths() as any], +}); diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts index 1adc347e..953ed0f2 100644 --- a/packages/drift/src/adapter/MockAdapter.test.ts +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -7,7 +7,7 @@ import type { AdapterWriteParams, } from "src/adapter/types/Adapter"; import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent } from "src/adapter/types/Event"; +import type { ContractEvent } from "src/adapter/types/Event"; import type { DecodedFunctionData } from "src/adapter/types/Function"; import type { Transaction, @@ -331,7 +331,7 @@ describe("MockAdapter", () => { it("Can be stubbed", async () => { const adapter = new MockAdapter(); - const events: ContactEvent[] = [ + const events: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -369,7 +369,7 @@ describe("MockAdapter", () => { ...params1, filter: { from: "0x2" }, }; - const events1: ContactEvent[] = [ + const events1: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -379,7 +379,7 @@ describe("MockAdapter", () => { }, }, ]; - const events2: ContactEvent[] = [ + const events2: ContractEvent[] = [ { eventName: "Transfer", args: { diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 53883298..bd74e265 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -8,9 +8,10 @@ import type { ReadWriteAdapter, } from "src/adapter/types/Adapter"; import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, + FunctionArgs, FunctionName, FunctionReturn, } from "src/adapter/types/Function"; @@ -219,7 +220,7 @@ export class MockAdapter implements ReadWriteAdapter { return this.mocks .get< [AdapterGetEventsParams], - Promise[]> + Promise[]> >({ method: "getEvents", key: params.event, @@ -232,7 +233,7 @@ export class MockAdapter implements ReadWriteAdapter { ) { return this.mocks.get< [AdapterGetEventsParams], - Promise[]> + Promise[]> >({ method: "getEvents", key: params.event, @@ -244,34 +245,47 @@ export class MockAdapter implements ReadWriteAdapter { onRead< TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: OptionalKeys< - AdapterReadParams, - "args" | "address" - >, - ) { + >({ + abi, + address, + fn, + args, + block, + }: OptionalKeys, "args" | "address">) { return this.mocks .get< [AdapterReadParams], Promise> >({ method: "read", - key: params.fn, + key: fn, }) - .withArgs(params as Partial>); + .withArgs({ + abi, + address, + fn, + args, + block, + } as Partial>); } async read< TAbi extends Abi, TFunctionName extends FunctionName, - >(params: AdapterReadParams) { + >({ abi, address, fn, args, block }: AdapterReadParams) { return this.mocks.get< [AdapterReadParams], Promise> >({ method: "read", - key: params.fn, - })(params); + key: fn, + })({ + abi, + address: address, + fn: fn, + args: args as FunctionArgs, + block: block, + }); } // simulateWrite // diff --git a/packages/drift/src/adapter/types/Adapter.ts b/packages/drift/src/adapter/types/Adapter.ts index d29c4483..550b2cbf 100644 --- a/packages/drift/src/adapter/types/Adapter.ts +++ b/packages/drift/src/adapter/types/Adapter.ts @@ -4,7 +4,7 @@ import type { ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionArgs, @@ -21,7 +21,7 @@ export type Adapter = ReadAdapter | ReadWriteAdapter; export interface ReadAdapter extends Network { getEvents>( params: AdapterGetEventsParams, - ): Promise[]>; + ): Promise[]>; read< TAbi extends Abi, diff --git a/packages/drift/src/adapter/types/Event.ts b/packages/drift/src/adapter/types/Event.ts index be6a3172..e2566bf6 100644 --- a/packages/drift/src/adapter/types/Event.ts +++ b/packages/drift/src/adapter/types/Event.ts @@ -52,7 +52,7 @@ export type EventFilter< /** * A strongly typed event object based on an abi */ -export interface ContactEvent< +export interface ContractEvent< TAbi extends Abi, TEventName extends EventName = EventName, > { diff --git a/packages/drift/src/cache/ClientCache/types.ts b/packages/drift/src/cache/ClientCache/types.ts index c6a94d78..e944a3e3 100644 --- a/packages/drift/src/cache/ClientCache/types.ts +++ b/packages/drift/src/cache/ClientCache/types.ts @@ -4,7 +4,7 @@ import type { AdapterReadParams, } from "src/adapter/types/Adapter"; import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { FunctionName, FunctionReturn } from "src/adapter/types/Function"; import type { NetworkGetBlockParams } from "src/adapter/types/Network"; import type { Transaction } from "src/adapter/types/Transaction"; @@ -60,7 +60,7 @@ export type ClientCache = T & { preloadEvents>( params: EventsKeyParams & { - value: readonly ContactEvent[]; + value: readonly ContractEvent[]; }, ): MaybePromise; diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts index 346d06e6..f3f77e5c 100644 --- a/packages/drift/src/client/Contract/Contract.ts +++ b/packages/drift/src/client/Contract/Contract.ts @@ -10,7 +10,7 @@ import type { ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionArgs, @@ -89,7 +89,7 @@ export class ReadContract< */ getEvents = async >( ...[event, options]: ContractGetEventsArgs - ): Promise[]> => { + ): Promise[]> => { const key = this.eventsKey(event, options); if (this.cache.has(key)) { return this.cache.get(key); @@ -112,7 +112,7 @@ export class ReadContract< EventsKeyParams, keyof ReadContractParams > & { - value: readonly ContactEvent[]; + value: readonly ContractEvent[]; }, ): MaybePromise => { return this.cache.preloadEvents({ diff --git a/packages/drift/src/client/Contract/MockContract.test.ts b/packages/drift/src/client/Contract/MockContract.test.ts index 7423361e..435e739c 100644 --- a/packages/drift/src/client/Contract/MockContract.test.ts +++ b/packages/drift/src/client/Contract/MockContract.test.ts @@ -1,4 +1,4 @@ -import type { ContactEvent } from "src/adapter/types/Event"; +import type { ContractEvent } from "src/adapter/types/Event"; import type { DecodedFunctionData } from "src/adapter/types/Function"; import { MockContract } from "src/client/Contract/MockContract"; import { IERC20 } from "src/utils/testing/IERC20"; @@ -22,7 +22,7 @@ describe("MockContract", () => { it("Can be stubbed", async () => { const contract = new MockContract({ abi }); - const events: ContactEvent[] = [ + const events: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -38,7 +38,7 @@ describe("MockContract", () => { it("Can be stubbed with specific args", async () => { const contract = new MockContract({ abi }); - const events1: ContactEvent[] = [ + const events1: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -48,7 +48,7 @@ describe("MockContract", () => { }, }, ]; - const events2: ContactEvent[] = [ + const events2: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -112,6 +112,18 @@ describe("MockContract", () => { contract.onRead("balanceOf").resolves(123n); expect(await contract.read("balanceOf", { owner: "0x" })).toBe(123n); }); + + it("Inherits stubbed values from the adapter", async () => { + const contract = new MockContract({ abi }); + contract.adapter + .onRead({ + abi: contract.abi, + address: contract.address, + fn: "symbol", + }) + .resolves("ABC"); + expect(await contract.read("symbol")).toBe("ABC"); + }); }); describe("simulateWrite", () => { diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 0d26e2fd..433e36c9 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -1,11 +1,12 @@ import type { Abi } from "abitype"; +import type { SinonStub } from "sinon"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionArgs, @@ -22,6 +23,7 @@ import { ReadWriteContract, } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; +import type { AdapterReadParams } from "src/exports"; import type { Address, Bytes, TransactionHash } from "src/types"; import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; @@ -68,7 +70,7 @@ export class MockContract< event: TEventName, options?: ContractGetEventsOptions, ], - Promise[]> + Promise[]> >({ method: "getEvents", key: event, @@ -82,7 +84,7 @@ export class MockContract< ) => { return this.mocks.get< [event: TEventName, options?: ContractGetEventsOptions], - Promise[]> + Promise[]> >({ method: "getEvents", key: event, @@ -96,27 +98,79 @@ export class MockContract< args?: FunctionArgs, options?: ContractReadOptions, ) { - return this.mocks - .get< - ContractReadArgs, - Promise> - >({ - method: "read", - key: fn, - }) - .withArgs(fn, args as any, options); + // return this.mocks + // .get< + // ContractReadArgs, + // Promise> + // >({ + // method: "read", + // }) + // .withArgs(fn, args as any, options); + + // const mock = this.adapter.onRead({ + // abi: this.abi as Abi, + // address: this.address, + // fn, + // ...{ + // args, + // ...options, + // }, + // }) as SinonStub as SinonStub< + // ContractReadArgs, + // Promise> + // >; + + // return mock.withArgs(fn, args as any, options); + + return this.adapter.onRead({ + abi: this.abi as Abi, + address: this.address, + args, + fn, + ...options, + }) as SinonStub as SinonStub< + [Omit, "address" | "abi" | "fn">], + Promise> + >; } read = async >( ...[fn, args, options]: ContractReadArgs ) => { - return this.mocks.get< - ContractReadArgs, - Promise> - >({ - method: "read", - key: fn, - })(fn, args as any, options); + // return this.mocks.get< + // ContractReadArgs, + // Promise> + // >({ + // method: "read", + // key: fn, + // })(fn, args as any, options); + + // return this.onRead(fn, args as any, options)(fn, args as any, options); + + // return this.onRead( + // fn, + // args as any, + // options, + // )({ + // args, + // ...options, + // } as Omit< + // AdapterReadParams, + // "address" | "abi" | "fn" + // >); + return this.adapter.onRead({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + })({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as unknown as FunctionReturn; }; // simulateWrite // diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts index 179c69ea..7046875a 100644 --- a/packages/drift/src/client/Drift/Drift.ts +++ b/packages/drift/src/client/Drift/Drift.ts @@ -7,7 +7,7 @@ import type { ReadWriteAdapter, } from "src/adapter/types/Adapter"; import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionName, @@ -212,7 +212,7 @@ export class Drift< */ getEvents = async >( params: GetEventsParams, - ): Promise[]> => { + ): Promise[]> => { const key = this.cache.eventsKey(params); if (this.cache.has(key)) { return this.cache.get(key); diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts index 3cd86d3f..e0cc3b60 100644 --- a/packages/drift/src/client/Drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -1,5 +1,5 @@ import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent } from "src/adapter/types/Event"; +import type { ContractEvent } from "src/adapter/types/Event"; import type { DecodedFunctionData } from "src/adapter/types/Function"; import type { Transaction, @@ -343,7 +343,7 @@ describe("MockDrift", () => { it("Can be stubbed", async () => { const drift = new MockDrift(); - const events: ContactEvent[] = [ + const events: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -381,7 +381,7 @@ describe("MockDrift", () => { ...params1, filter: { from: "0x2" }, }; - const events1: ContactEvent[] = [ + const events1: ContractEvent[] = [ { eventName: "Transfer", args: { @@ -391,7 +391,7 @@ describe("MockDrift", () => { }, }, ]; - const events2: ContactEvent[] = [ + const events2: ContractEvent[] = [ { eventName: "Transfer", args: { diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index 437e4929..1b5eb61b 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -1,7 +1,7 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { Block } from "src/adapter/types/Block"; -import type { ContactEvent, EventName } from "src/adapter/types/Event"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; import type { DecodedFunctionData, FunctionName, @@ -238,7 +238,7 @@ export class MockDrift extends Drift { return this.mocks .get< [GetEventsParams], - Promise[]> + Promise[]> >({ method: "getEvents", key: params.event, @@ -251,7 +251,7 @@ export class MockDrift extends Drift { ) => { return this.mocks.get< [GetEventsParams], - Promise[]> + Promise[]> >({ method: "getEvents", key: params.event, diff --git a/packages/drift/src/exports/index.ts b/packages/drift/src/exports/index.ts index 076f4905..bef7a8b8 100644 --- a/packages/drift/src/exports/index.ts +++ b/packages/drift/src/exports/index.ts @@ -32,7 +32,7 @@ export type { ContractWriteOptions, } from "src/adapter/types/Contract"; export type { - ContactEvent, + ContractEvent, EventArgs, EventFilter, EventName, @@ -120,6 +120,7 @@ export { export type { AnyFunction, AnyObject, + Converted, DeepPartial, EmptyObject, FunctionKey, diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts index 3a17ee61..1fefae47 100644 --- a/packages/drift/src/utils/types.ts +++ b/packages/drift/src/utils/types.ts @@ -130,3 +130,20 @@ export type MergeKeys = UnionToIntersection extends infer I [K in keyof I]: K extends keyof T ? T[K] : I[K]; } : never; + +/** + * Convert all properties in `T` whose values are of type `U` to type `V`. + * + * @example + * ```ts + * type Converted = Converted<{ a: string, b: number }, string, number>; + * // { a: number, b: number } + * ``` + */ +export type Converted = T extends U + ? V + : T extends Array + ? Converted[] + : T extends object + ? { [K in keyof T]: Converted } + : T; From fd8a10b47a9ee18da8403dfa71f4e90dfd71f7e9 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 01:09:56 -0500 Subject: [PATCH 38/49] Add licenses --- LICENSE | 201 ++++++++++++++++++ package.json | 1 + packages/drift-viem/LICENSE | 201 ++++++++++++++++++ packages/drift-viem/package.json | 24 +-- .../ViemReadAdapter.test.ts | 2 +- packages/drift-viem/src/index.ts | 6 + .../artifacts => src/utils/testing}/erc20.ts | 0 .../viemAdapter.test.ts | 0 packages/drift-viem/src/viemAdapter.ts | 7 +- packages/drift-viem/tsconfig.json | 2 +- packages/drift/LICENSE | 201 ++++++++++++++++++ packages/drift/package.json | 7 +- 12 files changed, 631 insertions(+), 21 deletions(-) create mode 100644 LICENSE create mode 100644 packages/drift-viem/LICENSE rename packages/drift-viem/{integration-tests => src}/ViemReadAdapter.test.ts (99%) create mode 100644 packages/drift-viem/src/index.ts rename packages/drift-viem/{integration-tests/artifacts => src/utils/testing}/erc20.ts (100%) rename packages/drift-viem/{integration-tests => src}/viemAdapter.test.ts (100%) create mode 100644 packages/drift/LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4f4a9cf4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [DELV, Inc] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/package.json b/package.json index a35c8fca..0b125237 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "drift", + "license": "AGPL-3.0", "private": true, "scripts": { "cli": "yarn workspace @delvtech/drift-cli dev", diff --git a/packages/drift-viem/LICENSE b/packages/drift-viem/LICENSE new file mode 100644 index 00000000..4f4a9cf4 --- /dev/null +++ b/packages/drift-viem/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [DELV, Inc] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/drift-viem/package.json b/packages/drift-viem/package.json index d219b28d..fe1bed8e 100644 --- a/packages/drift-viem/package.json +++ b/packages/drift-viem/package.json @@ -1,7 +1,7 @@ { "name": "@delvtech/drift-viem", "version": "0.0.0", - "license": "MIT", + "license": "AGPL-3.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -24,15 +24,13 @@ }, "scripts": { "build": "tsup", - "test:watch": "vitest", + "test:watch": "vitest --reporter=verbose", "test": "vitest run", - "test:integration": "vitest run integration", "typecheck": "tsc --noEmit", "watch": "tsup --watch" }, "peerDependencies": { - "@delvtech/drift": "0.0.0", - "sinon": "^17.0.1", + "sinon": "^17.0.3", "viem": "^2" }, "peerDependenciesMeta": { @@ -40,16 +38,18 @@ "optional": true } }, + "dependencies": { + "@delvtech/drift": "0.0.0" + }, "devDependencies": { - "@delvtech/drift": "0.0.0", "@repo/typescript-config": "*", - "sinon": "^17.0.1", + "@types/sinon": "^17.0.3", "tsconfig-paths": "^4.2.0", - "tsup": "^8.0.2", - "typescript": "^5.3.3", - "viem": "^2.7.3", - "vite-tsconfig-paths": "^4.3.1", - "vitest": "^1.2.2" + "tsup": "^8.3.0", + "typescript": "^5.6.2", + "viem": "^2.21.19", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.2" }, "publishConfig": { "access": "public" diff --git a/packages/drift-viem/integration-tests/ViemReadAdapter.test.ts b/packages/drift-viem/src/ViemReadAdapter.test.ts similarity index 99% rename from packages/drift-viem/integration-tests/ViemReadAdapter.test.ts rename to packages/drift-viem/src/ViemReadAdapter.test.ts index c24fbf40..2fc88c5d 100644 --- a/packages/drift-viem/integration-tests/ViemReadAdapter.test.ts +++ b/packages/drift-viem/src/ViemReadAdapter.test.ts @@ -6,8 +6,8 @@ import type { Transaction, TransactionReceipt, } from "@delvtech/drift"; -import { erc20 } from "integration-tests/artifacts/erc20"; import { ViemReadAdapter } from "src/ReadAdapter"; +import { erc20 } from "src/utils/testing/erc20"; import { http, type Address, createPublicClient } from "viem"; import { describe, expect, it } from "vitest"; diff --git a/packages/drift-viem/src/index.ts b/packages/drift-viem/src/index.ts new file mode 100644 index 00000000..91fbb35e --- /dev/null +++ b/packages/drift-viem/src/index.ts @@ -0,0 +1,6 @@ +export { type ViemReadAdapterParams, ViemReadAdapter } from "./ReadAdapter"; +export { + type ViemReadWriteAdapterParams, + ViemReadWriteAdapter, +} from "./ReadWriteAdapter"; +export { viemAdapter } from "./viemAdapter"; diff --git a/packages/drift-viem/integration-tests/artifacts/erc20.ts b/packages/drift-viem/src/utils/testing/erc20.ts similarity index 100% rename from packages/drift-viem/integration-tests/artifacts/erc20.ts rename to packages/drift-viem/src/utils/testing/erc20.ts diff --git a/packages/drift-viem/integration-tests/viemAdapter.test.ts b/packages/drift-viem/src/viemAdapter.test.ts similarity index 100% rename from packages/drift-viem/integration-tests/viemAdapter.test.ts rename to packages/drift-viem/src/viemAdapter.test.ts diff --git a/packages/drift-viem/src/viemAdapter.ts b/packages/drift-viem/src/viemAdapter.ts index 8f4ef61c..cda20d22 100644 --- a/packages/drift-viem/src/viemAdapter.ts +++ b/packages/drift-viem/src/viemAdapter.ts @@ -1,4 +1,3 @@ -import type { Adapter, ReadAdapter, ReadWriteAdapter } from "@delvtech/drift"; import { ViemReadAdapter } from "src/ReadAdapter"; import { ViemReadWriteAdapter } from "src/ReadWriteAdapter"; import type { PublicClient, WalletClient } from "viem"; @@ -20,8 +19,10 @@ export function viemAdapter< }: ViemAdapterParams< TPublicClient, TWalletClient ->): TWalletClient extends undefined ? ReadAdapter : ReadWriteAdapter { +>): TWalletClient extends undefined ? ViemReadAdapter : ViemReadWriteAdapter { return walletClient ? new ViemReadWriteAdapter({ publicClient, walletClient }) - : (new ViemReadAdapter({ publicClient }) as Adapter as ReadWriteAdapter); + : (new ViemReadAdapter({ + publicClient, + }) as ViemReadWriteAdapter); } diff --git a/packages/drift-viem/tsconfig.json b/packages/drift-viem/tsconfig.json index d785e1bb..0ff67498 100644 --- a/packages/drift-viem/tsconfig.json +++ b/packages/drift-viem/tsconfig.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/tsconfig", "extends": "@repo/typescript-config/base.json", - "include": ["./*.ts", "src", "integration-tests"], + "include": ["./*.ts", "src", "src/utils/testing"], "exclude": ["node_modules"], "compilerOptions": { "rootDir": ".", diff --git a/packages/drift/LICENSE b/packages/drift/LICENSE new file mode 100644 index 00000000..4f4a9cf4 --- /dev/null +++ b/packages/drift/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [DELV, Inc] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/drift/package.json b/packages/drift/package.json index 6cf2e34a..ca29b5ae 100644 --- a/packages/drift/package.json +++ b/packages/drift/package.json @@ -1,7 +1,7 @@ { "name": "@delvtech/drift", "version": "0.0.0", - "license": "MIT", + "license": "AGPL-3.0", "type": "module", "exports": { ".": { @@ -28,7 +28,7 @@ "watch": "tsup --watch" }, "peerDependencies": { - "sinon": "^17.0.1" + "sinon": "^17.0.3" }, "peerDependenciesMeta": { "sinon": { @@ -45,11 +45,10 @@ "@repo/typescript-config": "*", "@types/sinon": "^17.0.3", "abitype": "^1.0.0", - "dotenv": "^16.4.2", "fast-json-stable-stringify": "^2.1.0", "sinon": "^17.0.1", "tsconfig-paths": "^4.2.0", - "tsup": "^8.0.2", + "tsup": "^8.3.0", "typescript": "^5.4.5", "vite-tsconfig-paths": "^4.3.1", "vitest": "^1.2.2" From 98f348727d1dce5fc6aa5e574f5299624862916f Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 01:10:51 -0500 Subject: [PATCH 39/49] Update deps --- packages/drift/package.json | 8 +- yarn.lock | 621 +++++++++++++++++++++++++++++++++++- 2 files changed, 619 insertions(+), 10 deletions(-) diff --git a/packages/drift/package.json b/packages/drift/package.json index ca29b5ae..7f8e5b37 100644 --- a/packages/drift/package.json +++ b/packages/drift/package.json @@ -44,14 +44,14 @@ "devDependencies": { "@repo/typescript-config": "*", "@types/sinon": "^17.0.3", - "abitype": "^1.0.0", + "abitype": "^1.0.6", "fast-json-stable-stringify": "^2.1.0", "sinon": "^17.0.1", "tsconfig-paths": "^4.2.0", "tsup": "^8.3.0", - "typescript": "^5.4.5", - "vite-tsconfig-paths": "^4.3.1", - "vitest": "^1.2.2" + "typescript": "^5.6.2", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.2" }, "publishConfig": { "access": "public" diff --git a/yarn.lock b/yarn.lock index 5544c1c4..d92eae75 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== +"@adraffy/ens-normalize@1.11.0": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@babel/code-frame@^7.0.0": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" @@ -306,116 +311,236 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== +"@esbuild/aix-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" + integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== + "@esbuild/android-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== +"@esbuild/android-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" + integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== + "@esbuild/android-arm@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== +"@esbuild/android-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" + integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== + "@esbuild/android-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== +"@esbuild/android-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" + integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== + "@esbuild/darwin-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== +"@esbuild/darwin-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16" + integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== + "@esbuild/darwin-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== +"@esbuild/darwin-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" + integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== + "@esbuild/freebsd-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== +"@esbuild/freebsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" + integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== + "@esbuild/freebsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== +"@esbuild/freebsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" + integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== + "@esbuild/linux-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== +"@esbuild/linux-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" + integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== + "@esbuild/linux-arm@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== +"@esbuild/linux-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" + integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== + "@esbuild/linux-ia32@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== +"@esbuild/linux-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" + integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== + "@esbuild/linux-loong64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== +"@esbuild/linux-loong64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" + integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== + "@esbuild/linux-mips64el@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== +"@esbuild/linux-mips64el@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" + integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== + "@esbuild/linux-ppc64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== +"@esbuild/linux-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" + integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== + "@esbuild/linux-riscv64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== +"@esbuild/linux-riscv64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" + integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== + "@esbuild/linux-s390x@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== +"@esbuild/linux-s390x@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" + integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== + "@esbuild/linux-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + "@esbuild/netbsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== +"@esbuild/netbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" + integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== + +"@esbuild/openbsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" + integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== + "@esbuild/openbsd-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== +"@esbuild/openbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" + integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== + "@esbuild/sunos-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== +"@esbuild/sunos-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" + integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== + "@esbuild/win32-arm64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== +"@esbuild/win32-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" + integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== + "@esbuild/win32-ia32@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== +"@esbuild/win32-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" + integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== + "@esbuild/win32-x64@0.19.12": version "0.19.12" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== +"@esbuild/win32-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" + integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -464,6 +589,11 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + "@jridgewell/trace-mapping@0.3.9": version "0.3.9" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" @@ -509,11 +639,23 @@ dependencies: "@noble/hashes" "1.3.2" +"@noble/curves@1.6.0", "@noble/curves@^1.4.0", "@noble/curves@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + "@noble/hashes@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@noble/hashes@1.5.0", "@noble/hashes@^1.4.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.2": version "1.3.3" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.3.tgz#39908da56a4adc270147bb07968bf3b16cfe1699" @@ -550,71 +692,156 @@ resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz#786eaf6372be2fc209cc957c14aa9d3ff8fefe6a" integrity sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A== +"@rollup/rollup-android-arm-eabi@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz#1661ff5ea9beb362795304cb916049aba7ac9c54" + integrity sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA== + "@rollup/rollup-android-arm64@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz#0114a042fd6396f4f3233e6171fd5b61a36ed539" integrity sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ== +"@rollup/rollup-android-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz#2ffaa91f1b55a0082b8a722525741aadcbd3971e" + integrity sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA== + "@rollup/rollup-darwin-arm64@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz#944d007c1dc71a8c9174d11671c0c34bd74a2c81" integrity sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg== +"@rollup/rollup-darwin-arm64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz#627007221b24b8cc3063703eee0b9177edf49c1f" + integrity sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA== + "@rollup/rollup-darwin-x64@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz#1d08cb4521a058d7736ab1c7fe988daf034a2598" integrity sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q== +"@rollup/rollup-darwin-x64@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz#0605506142b9e796c370d59c5984ae95b9758724" + integrity sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ== + "@rollup/rollup-linux-arm-gnueabihf@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz#4763eec1591bf0e99a54ad3d1ef39cb268ed7b19" integrity sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw== +"@rollup/rollup-linux-arm-gnueabihf@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz#62dfd196d4b10c0c2db833897164d2d319ee0cbb" + integrity sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA== + +"@rollup/rollup-linux-arm-musleabihf@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz#53ce72aeb982f1f34b58b380baafaf6a240fddb3" + integrity sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw== + "@rollup/rollup-linux-arm64-gnu@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz#e6dae70c53ace836973526c41803b877cffc6f7b" integrity sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q== +"@rollup/rollup-linux-arm64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz#1632990f62a75c74f43e4b14ab3597d7ed416496" + integrity sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA== + "@rollup/rollup-linux-arm64-musl@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz#5692e1a0feba0cc4a933864961afc3211177d242" integrity sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ== +"@rollup/rollup-linux-arm64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz#8c03a996efb41e257b414b2e0560b7a21f2d9065" + integrity sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz#5b98729628d5bcc8f7f37b58b04d6845f85c7b5d" + integrity sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw== + "@rollup/rollup-linux-riscv64-gnu@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz#fbe3d80f7a7ac54a8847f5bddd1bc6f7b9ccb65f" integrity sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA== +"@rollup/rollup-linux-riscv64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz#48e42e41f4cabf3573cfefcb448599c512e22983" + integrity sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg== + +"@rollup/rollup-linux-s390x-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz#e0b4f9a966872cb7d3e21b9e412a4b7efd7f0b58" + integrity sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g== + "@rollup/rollup-linux-x64-gnu@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz#3f06b55ccf173446d390d0306643dff62ec99807" integrity sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw== +"@rollup/rollup-linux-x64-gnu@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz#78144741993100f47bd3da72fce215e077ae036b" + integrity sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A== + "@rollup/rollup-linux-x64-musl@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz#e4ac9b27041c83d7faab6205f62763103eb317ba" integrity sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw== +"@rollup/rollup-linux-x64-musl@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz#d9fe32971883cd1bd858336bd33a1c3ca6146127" + integrity sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ== + "@rollup/rollup-win32-arm64-msvc@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz#6ad0d4fb0066f240778ee3f61eecf7aa0357f883" integrity sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ== +"@rollup/rollup-win32-arm64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz#71fa3ea369316db703a909c790743972e98afae5" + integrity sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ== + "@rollup/rollup-win32-ia32-msvc@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz#29d50292381311cc8d3623e73b427b7e2e40a653" integrity sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg== +"@rollup/rollup-win32-ia32-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz#653f5989a60658e17d7576a3996deb3902e342e2" + integrity sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ== + "@rollup/rollup-win32-x64-msvc@4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz#4eedd01af3a82c1acb0fe6d837ebf339c4cbf839" integrity sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ== +"@rollup/rollup-win32-x64-msvc@4.24.0": + version "4.24.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz#0574d7e87b44ee8511d08cc7f914bcb802b70818" + integrity sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw== + "@scure/base@~1.1.0", "@scure/base@~1.1.2": version "1.1.5" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.5.tgz#1d85d17269fe97694b9c592552dd9e5e33552157" integrity sha512-Brj9FiG2W1MRQSTB212YVPRrcbjkv48FoZi/u4l/zds/ieRrqsh7aUf6CLwkAq61oKXr/ZlTzlY66gLIj3TFTQ== +"@scure/base@~1.1.7", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + "@scure/bip32@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" @@ -624,6 +851,15 @@ "@noble/hashes" "~1.3.2" "@scure/base" "~1.1.2" +"@scure/bip32@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -632,6 +868,14 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + "@sinclair/typebox@^0.27.8": version "0.27.8" resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" @@ -697,6 +941,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== +"@types/estree@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" + integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== + "@types/lodash.ismatch@^4.4.9": version "4.4.9" resolved "https://registry.yarnpkg.com/@types/lodash.ismatch/-/lodash.ismatch-4.4.9.tgz#97b4317f7dc3975bb51660a0f9a055ac7b67b134" @@ -755,6 +1004,32 @@ "@vitest/utils" "1.2.2" chai "^4.3.10" +"@vitest/expect@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.2.tgz#e92fa284b8472548f72cacfe896020c64af6bf78" + integrity sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg== + dependencies: + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" + chai "^5.1.1" + tinyrainbow "^1.2.0" + +"@vitest/mocker@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.2.tgz#08853a9d8d12afba284aebdf9b5ea26ddae5f20a" + integrity sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA== + dependencies: + "@vitest/spy" "^2.1.0-beta.1" + estree-walker "^3.0.3" + magic-string "^0.30.11" + +"@vitest/pretty-format@2.1.2", "@vitest/pretty-format@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.2.tgz#42882ea18c4cd40428e34f74bbac706a82465193" + integrity sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA== + dependencies: + tinyrainbow "^1.2.0" + "@vitest/runner@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.2.2.tgz#8b060a56ecf8b3d607b044d79f5f50d3cd9fee2f" @@ -764,6 +1039,14 @@ p-limit "^5.0.0" pathe "^1.1.1" +"@vitest/runner@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.2.tgz#14da1f5eac43fbd9a37d7cd72de102e8f785d727" + integrity sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw== + dependencies: + "@vitest/utils" "2.1.2" + pathe "^1.1.2" + "@vitest/snapshot@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.2.2.tgz#f56fd575569774968f3eeba9382a166c26201042" @@ -773,6 +1056,15 @@ pathe "^1.1.1" pretty-format "^29.7.0" +"@vitest/snapshot@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.2.tgz#e20bd794b33fdcd4bfe69138baac7bb890c4d51f" + integrity sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA== + dependencies: + "@vitest/pretty-format" "2.1.2" + magic-string "^0.30.11" + pathe "^1.1.2" + "@vitest/spy@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.2.2.tgz#8fc2aeccb96cecbbdd192c643729bd5f97a01c86" @@ -780,6 +1072,13 @@ dependencies: tinyspy "^2.2.0" +"@vitest/spy@2.1.2", "@vitest/spy@^2.1.0-beta.1": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.2.tgz#bccdeca597c8fc3777302889e8c98cec9264df44" + integrity sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A== + dependencies: + tinyspy "^3.0.0" + "@vitest/utils@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.2.2.tgz#94b5a1bd8745ac28cf220a99a8719efea1bcfc83" @@ -790,11 +1089,25 @@ loupe "^2.3.7" pretty-format "^29.7.0" +"@vitest/utils@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.2.tgz#222ac35ba02493173e40581256eb7a62520fcdba" + integrity sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ== + dependencies: + "@vitest/pretty-format" "2.1.2" + loupe "^3.1.1" + tinyrainbow "^1.2.0" + abitype@1.0.0, abitype@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.0.tgz#237176dace81d90d018bebf3a45cb42f2a2d9e97" integrity sha512-NMeMah//6bJ56H5XRj8QCV4AwuW6hB6zqz2LnhhLdcWVQOsXki6/Pn3APeqxCma62nXIcmZWdu1DlHWS74umVQ== +abitype@1.0.6, abitype@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" + integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== + acorn-walk@^8.1.1: version "8.3.4" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" @@ -933,6 +1246,11 @@ assertion-error@^1.1.0: resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + available-typed-arrays@^1.0.5, available-typed-arrays@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz#ac812d8ce5a6b976d738e1c45f08d0b00bc7d725" @@ -983,6 +1301,13 @@ bundle-require@^4.0.0: dependencies: load-tsconfig "^0.2.3" +bundle-require@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/bundle-require/-/bundle-require-5.0.0.tgz#071521bdea6534495cf23e92a83f889f91729e93" + integrity sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w== + dependencies: + load-tsconfig "^0.2.3" + cac@^6.7.12, cac@^6.7.14: version "6.7.14" resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" @@ -1025,6 +1350,17 @@ chai@^4.3.10: pathval "^1.1.1" type-detect "^4.0.8" +chai@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.1.tgz#f035d9792a22b481ead1c65908d14bb62ec1c82c" + integrity sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk@^2.1.0, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1054,7 +1390,12 @@ check-error@^1.0.3: dependencies: get-func-name "^2.0.2" -chokidar@^3.5.1: +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + +chokidar@^3.5.1, chokidar@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -1126,6 +1467,11 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +consola@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-3.2.3.tgz#0741857aa88cfa0d6fd53f1cff0375136e98502f" + integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + create-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" @@ -1181,6 +1527,13 @@ debug@^4.1.1, debug@^4.3.1, debug@^4.3.4: dependencies: ms "2.1.2" +debug@^4.3.5, debug@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -1201,6 +1554,11 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + defaults@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" @@ -1393,6 +1751,36 @@ esbuild@^0.19.2, esbuild@^0.19.3: "@esbuild/win32-ia32" "0.19.12" "@esbuild/win32-x64" "0.19.12" +esbuild@^0.23.0: + version "0.23.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.1.tgz#40fdc3f9265ec0beae6f59824ade1bd3d3d2dab8" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" @@ -1428,7 +1816,7 @@ ethers@^6.11.0: tslib "2.4.0" ws "8.5.0" -execa@^5.0.0: +execa@^5.0.0, execa@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -1500,6 +1888,11 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" +fdir@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.0.tgz#8e80ab4b18a2ac24beebf9d20d71e1bc2627dbae" + integrity sha512-3oB133prH1o4j/L5lLW7uOCF1PlD+/It2L0eL/iAqWMB91RBbqTewABqxhj0ibBd90EEmWZq7ntIWzVaWcXTGQ== + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1958,6 +2351,11 @@ isows@1.0.3: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.3.tgz#93c1cf0575daf56e7120bab5c8c448b0809d0d74" integrity sha512-2cKei4vlmg2cxEjm3wVSqn8pcoRF/LX/wpifuuNquFO4SQmPwarClT+SUCA2lt+l581tTeZIPIZuIDo2jWN1fg== +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + jackspeak@^2.3.5: version "2.3.6" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" @@ -1967,7 +2365,7 @@ jackspeak@^2.3.5: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -joycon@^3.0.1: +joycon@^3.0.1, joycon@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/joycon/-/joycon-3.1.1.tgz#bce8596d6ae808f8b68168f5fc69280996894f03" integrity sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw== @@ -2027,6 +2425,11 @@ lilconfig@^3.0.0: resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.0.0.tgz#f8067feb033b5b74dab4602a5f5029420be749bc" integrity sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g== +lilconfig@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -2096,6 +2499,13 @@ loupe@^2.3.6, loupe@^2.3.7: dependencies: get-func-name "^2.0.1" +loupe@^3.1.0, loupe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.1.tgz#71d038d59007d890e3247c5db97c1ec5a92edc54" + integrity sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw== + dependencies: + get-func-name "^2.0.1" + lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": version "10.2.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" @@ -2116,6 +2526,13 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +magic-string@^0.30.11: + version "0.30.11" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.11.tgz#301a6f93b3e8c2cb13ac1a7a673492c0dfd12954" + integrity sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + magic-string@^0.30.5: version "0.30.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" @@ -2234,6 +2651,11 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + mz@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -2447,16 +2869,31 @@ pathval@^1.1.1: resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== +picocolors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== + picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -2491,6 +2928,13 @@ postcss-load-config@^4.0.1: lilconfig "^3.0.0" yaml "^2.3.4" +postcss-load-config@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz#6fd7dcd8ae89badcf1b2d644489cbabf83aa8096" + integrity sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g== + dependencies: + lilconfig "^3.1.1" + postcss@^8.4.35: version "8.4.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" @@ -2658,6 +3102,31 @@ rollup@^4.0.2, rollup@^4.2.0: "@rollup/rollup-win32-x64-msvc" "4.10.0" fsevents "~2.3.2" +rollup@^4.19.0: + version "4.24.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.24.0.tgz#c14a3576f20622ea6a5c9cad7caca5e6e9555d05" + integrity sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg== + dependencies: + "@types/estree" "1.0.6" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.24.0" + "@rollup/rollup-android-arm64" "4.24.0" + "@rollup/rollup-darwin-arm64" "4.24.0" + "@rollup/rollup-darwin-x64" "4.24.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.24.0" + "@rollup/rollup-linux-arm-musleabihf" "4.24.0" + "@rollup/rollup-linux-arm64-gnu" "4.24.0" + "@rollup/rollup-linux-arm64-musl" "4.24.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.24.0" + "@rollup/rollup-linux-riscv64-gnu" "4.24.0" + "@rollup/rollup-linux-s390x-gnu" "4.24.0" + "@rollup/rollup-linux-x64-gnu" "4.24.0" + "@rollup/rollup-linux-x64-musl" "4.24.0" + "@rollup/rollup-win32-arm64-msvc" "4.24.0" + "@rollup/rollup-win32-ia32-msvc" "4.24.0" + "@rollup/rollup-win32-x64-msvc" "4.24.0" + fsevents "~2.3.2" + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -2861,7 +3330,7 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -std-env@^3.5.0: +std-env@^3.5.0, std-env@^3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== @@ -2961,7 +3430,7 @@ strip-literal@^1.3.0: dependencies: acorn "^8.10.0" -sucrase@^3.20.3: +sucrase@^3.20.3, sucrase@^3.35.0: version "3.35.0" resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== @@ -3017,16 +3486,49 @@ tinybench@^2.5.1: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.6.0.tgz#1423284ee22de07c91b3752c048d2764714b341b" integrity sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.0.tgz#ed60cfce19c17799d4a241e06b31b0ec2bee69e6" + integrity sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg== + +tinyglobby@^0.2.1: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.9.tgz#6baddd1b0fe416403efb0dd40442c7d7c03c1c66" + integrity sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw== + dependencies: + fdir "^6.4.0" + picomatch "^4.0.2" + tinypool@^0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.2.tgz#84013b03dc69dacb322563a475d4c0a9be00f82a" integrity sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ== +tinypool@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.1.tgz#c64233c4fac4304e109a64340178760116dbe1fe" + integrity sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + tinyspy@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.1.tgz#117b2342f1f38a0dbdcc73a50a454883adf861d1" integrity sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A== +tinyspy@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -3087,6 +3589,11 @@ tsconfck@^3.0.1: resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.0.2.tgz#d8e279f7a049d55f207f528d13fa493e1d8e7ceb" integrity sha512-6lWtFjwuhS3XI4HsX4Zg0izOI3FU/AI9EGVlPEUMDIhvLPMD4wkiof0WCoDgW7qY+Dy198g4d9miAqUHWHFH6Q== +tsconfck@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.3.tgz#a8202f51dab684c426314796cdb0bbd0fe0cdf80" + integrity sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ== + tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" @@ -3121,6 +3628,28 @@ tsup@^8.0.2: sucrase "^3.20.3" tree-kill "^1.2.2" +tsup@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/tsup/-/tsup-8.3.0.tgz#c7dae40b13d11d81fb144c0f90077a99102a572a" + integrity sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag== + dependencies: + bundle-require "^5.0.0" + cac "^6.7.14" + chokidar "^3.6.0" + consola "^3.2.3" + debug "^4.3.5" + esbuild "^0.23.0" + execa "^5.1.1" + joycon "^3.1.1" + picocolors "^1.0.1" + postcss-load-config "^6.0.1" + resolve-from "^5.0.0" + rollup "^4.19.0" + source-map "0.8.0-beta.0" + sucrase "^3.35.0" + tinyglobby "^0.2.1" + tree-kill "^1.2.2" + tty-table@^4.1.5: version "4.2.3" resolved "https://registry.yarnpkg.com/tty-table/-/tty-table-4.2.3.tgz#e33eb4007a0a9c976c97c37fa13ba66329a5c515" @@ -3235,7 +3764,7 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^5.3.3, typescript@^5.4.5: +typescript@^5.3.3, typescript@^5.4.5, typescript@^5.6.2: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== @@ -3273,6 +3802,21 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +viem@^2.21.19: + version "2.21.19" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.19.tgz#5e1a7efc45903d83306416ffa2e3a11ed23cd924" + integrity sha512-FdlkN+UI1IU5sYOmzvygkxsUNjDRD5YHht3gZFu2X9xFv6Z3h9pXq9ycrYQ3F17lNfb41O2Ot4/aqbUkwOv9dA== + dependencies: + "@adraffy/ens-normalize" "1.11.0" + "@noble/curves" "1.6.0" + "@noble/hashes" "1.5.0" + "@scure/bip32" "1.5.0" + "@scure/bip39" "1.4.0" + abitype "1.0.6" + isows "1.0.6" + webauthn-p256 "0.0.10" + ws "8.18.0" + viem@^2.7.3: version "2.7.8" resolved "https://registry.yarnpkg.com/viem/-/viem-2.7.8.tgz#ca60552190cdc501cf4e1d1140d8da7625b1b1f4" @@ -3298,6 +3842,16 @@ vite-node@1.2.2: picocolors "^1.0.0" vite "^5.0.0" +vite-node@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.2.tgz#f5491a2b399959c9e2f3c4b70cb0cbaecf9be6d2" + integrity sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ== + dependencies: + cac "^6.7.14" + debug "^4.3.6" + pathe "^1.1.2" + vite "^5.0.0" + vite-tsconfig-paths@^4.3.1: version "4.3.1" resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.1.tgz#28762938151e7c80aec9d70c57e65ddce43a576f" @@ -3307,6 +3861,15 @@ vite-tsconfig-paths@^4.3.1: globrex "^0.1.2" tsconfck "^3.0.1" +vite-tsconfig-paths@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz#c9387a29c32fd586e4c7f4e2b2da1f0b5c9a7403" + integrity sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + vite@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.1.tgz#294e39b199d669981efc7e0261b14f78ec80819e" @@ -3345,6 +3908,31 @@ vitest@^1.2.2: vite-node "1.2.2" why-is-node-running "^2.2.2" +vitest@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.2.tgz#f285fdde876749fddc0cb4d9748ae224443c1694" + integrity sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A== + dependencies: + "@vitest/expect" "2.1.2" + "@vitest/mocker" "2.1.2" + "@vitest/pretty-format" "^2.1.2" + "@vitest/runner" "2.1.2" + "@vitest/snapshot" "2.1.2" + "@vitest/spy" "2.1.2" + "@vitest/utils" "2.1.2" + chai "^5.1.1" + debug "^4.3.6" + magic-string "^0.30.11" + pathe "^1.1.2" + std-env "^3.7.0" + tinybench "^2.9.0" + tinyexec "^0.3.0" + tinypool "^1.0.0" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.2" + why-is-node-running "^2.3.0" + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -3352,6 +3940,14 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webauthn-p256@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" + integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -3423,6 +4019,14 @@ why-is-node-running@^2.2.2: siginfo "^2.0.0" stackback "0.0.2" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + "wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" @@ -3455,6 +4059,11 @@ ws@8.13.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + ws@8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.5.0.tgz#bfb4be96600757fe5382de12c670dab984a1ed4f" From 1da363cd916b6f38a2e00a8d715dc6d0375d1d68 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 01:36:04 -0500 Subject: [PATCH 40/49] Change Contract cache param to SimpleCache --- .../cache/ClientCache/createClientCache.ts | 31 ++++++++++++++++--- .../drift/src/client/Contract/Contract.ts | 17 +++++----- 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/packages/drift/src/cache/ClientCache/createClientCache.ts b/packages/drift/src/cache/ClientCache/createClientCache.ts index ec408a50..39cd99d5 100644 --- a/packages/drift/src/cache/ClientCache/createClientCache.ts +++ b/packages/drift/src/cache/ClientCache/createClientCache.ts @@ -1,8 +1,5 @@ import isMatch from "lodash.ismatch"; -import type { - ClientCache, - ReadKeyParams -} from "src/cache/ClientCache/types"; +import type { ClientCache, ReadKeyParams } from "src/cache/ClientCache/types"; import { createLruSimpleCache } from "src/cache/SimpleCache/createLruSimpleCache"; import type { SimpleCache } from "src/cache/SimpleCache/types"; import { createSerializableKey } from "src/utils/createSerializableKey"; @@ -18,6 +15,9 @@ import { extendInstance } from "src/utils/extendInstance"; export function createClientCache( cache: T = createLruSimpleCache({ max: 500 }) as T, ): ClientCache { + if (isClientCache(cache)) { + return cache; + } const clientCache: ClientCache = extendInstance< T, Omit @@ -136,3 +136,26 @@ export function createClientCache( return clientCache; } + +function isClientCache( + cache: T, +): cache is ClientCache { + return [ + "preloadChainId", + "chainIdKey", + "preloadBlock", + "blockKey", + "preloadBalance", + "invalidateBalance", + "balanceKey", + "preloadTransaction", + "transactionKey", + "preloadEvents", + "eventsKey", + "preloadRead", + "invalidateRead", + "invalidateReadsMatching", + "readKey", + "partialReadKey", + ].every((key) => key in cache); +} diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts index f3f77e5c..8935e5c3 100644 --- a/packages/drift/src/client/Contract/Contract.ts +++ b/packages/drift/src/client/Contract/Contract.ts @@ -24,6 +24,7 @@ import type { NameSpaceParam, ReadKeyParams, } from "src/cache/ClientCache/types"; +import { type SimpleCache, createLruSimpleCache } from "src/exports"; import type { Address, Bytes, TransactionHash } from "src/types"; import type { SerializableKey } from "src/utils/createSerializableKey"; import type { AnyObject, EmptyObject, MaybePromise } from "src/utils/types"; @@ -31,7 +32,7 @@ import type { AnyObject, EmptyObject, MaybePromise } from "src/utils/types"; export interface ContractParams< TAbi extends Abi = Abi, TAdapter extends Adapter = Adapter, - TCache extends ClientCache = ClientCache, + TCache extends SimpleCache = SimpleCache, > extends NameSpaceParam { abi: TAbi; adapter: TAdapter; @@ -42,7 +43,7 @@ export interface ContractParams< export type Contract< TAbi extends Abi = Abi, TAdapter extends Adapter = Adapter, - TCache extends ClientCache = ClientCache, + TCache extends SimpleCache = SimpleCache, > = TAdapter extends ReadWriteAdapter ? ReadWriteContract : ReadContract; @@ -50,7 +51,7 @@ export type Contract< export interface ReadContractParams< TAbi extends Abi = Abi, TAdapter extends ReadAdapter = ReadAdapter, - TCache extends ClientCache = ClientCache, + TCache extends SimpleCache = SimpleCache, > extends ContractParams {} /** @@ -60,25 +61,25 @@ export interface ReadContractParams< export class ReadContract< TAbi extends Abi = Abi, TAdapter extends ReadAdapter = ReadAdapter, - TCache extends ClientCache = ClientCache, + TCache extends SimpleCache = SimpleCache, > { abi: TAbi; adapter: TAdapter; address: Address; - cache: TCache; + cache: ClientCache; cacheNamespace?: PropertyKey; constructor({ abi, adapter, address, - cache = createClientCache() as TCache, + cache = createLruSimpleCache({ max: 500 }) as TCache, cacheNamespace, }: ReadContractParams) { this.abi = abi; this.adapter = adapter; this.address = address; - this.cache = cache; + this.cache = createClientCache(cache); this.cacheNamespace = cacheNamespace; } @@ -322,7 +323,7 @@ export type ReadWriteContractParams< export class ReadWriteContract< TAbi extends Abi = Abi, TAdapter extends ReadWriteAdapter = ReadWriteAdapter, - TCache extends ClientCache = ClientCache, + TCache extends SimpleCache = SimpleCache, > extends ReadContract { /** * Get the address of the signer for this contract. From 110bc1e8ed30835959114b4a3a3322a08b7d6eb5 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 03:18:46 -0500 Subject: [PATCH 41/49] Simplify MockDrift by forwarding methods --- packages/drift/src/adapter/MockAdapter.ts | 2 + packages/drift/src/client/Drift/Drift.ts | 8 +- packages/drift/src/client/Drift/MockDrift.ts | 356 ++----------------- 3 files changed, 42 insertions(+), 324 deletions(-) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index bd74e265..3c886f39 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -242,6 +242,8 @@ export class MockAdapter implements ReadWriteAdapter { // read // + // FIXME: Partial args in `on` methods is not working as expected. Currently, + // you must stub the method with all expected args. onRead< TAbi extends Abi, TFunctionName extends FunctionName, diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts index 7046875a..703259b9 100644 --- a/packages/drift/src/client/Drift/Drift.ts +++ b/packages/drift/src/client/Drift/Drift.ts @@ -112,7 +112,11 @@ export class Drift< address, cache = this.cache, cacheNamespace = this.cacheNamespace, - }: ContractParams): Contract => { + }: Omit, "adapter">): Contract< + TAbi, + TAdapter, + TCache + > => { return ( this.isReadWrite() ? new ReadWriteContract({ @@ -331,5 +335,5 @@ export type DecodeFunctionDataParams< > = AdapterDecodeFunctionDataParams; function isReadWriteAdapter(adapter: Adapter): adapter is ReadWriteAdapter { - return "readWriteContract" in adapter; + return "write" in adapter; } diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index 1b5eb61b..f7cef745 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -1,16 +1,7 @@ import type { Abi } from "abitype"; import { MockAdapter } from "src/adapter/MockAdapter"; -import type { Block } from "src/adapter/types/Block"; -import type { ContractEvent, EventName } from "src/adapter/types/Event"; -import type { - DecodedFunctionData, - FunctionName, - FunctionReturn, -} from "src/adapter/types/Function"; -import type { - Transaction, - TransactionReceipt, -} from "src/adapter/types/Transaction"; +import type { EventName } from "src/adapter/types/Event"; +import type { FunctionName } from "src/adapter/types/Function"; import type { ClientCache } from "src/cache/ClientCache/types"; import { MockContract, @@ -22,14 +13,12 @@ import { type EncodeFunctionDataParams, type GetBalanceParams, type GetBlockParams, - type GetChainIdParams, type GetEventsParams, type GetTransactionParams, type ReadParams, type WaitForTransactionParams, type WriteParams, } from "src/client/Drift/Drift"; -import type { Address, Bytes, TransactionHash } from "src/types"; import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; @@ -46,342 +35,65 @@ export class MockDrift extends Drift { params: MockContractParams, ): MockContract => new MockContract(params); - // getChainId // + onGetChainId = () => this.adapter.onGetChainId(); - // FIXME: Partial args in `on` methods is not working as expected. Currently, - // you must stub the method with all expected args. - onGetChainId(params?: Partial) { - let mock = this.mocks.get<[GetChainIdParams?], Promise>({ - method: "getChainId", - create: (mock) => mock.resolves(96024), - }); - if (params) { - mock = mock.withArgs(params); - } - return mock; - } - - getChainId = async (params?: GetChainIdParams) => { - return this.mocks.get<[GetChainIdParams?], Promise>({ - method: "getChainId", - create: (mock) => mock.resolves(96024), - })(params); - }; - - // getBlock // - - onGetBlock(params?: Partial) { - return this.mocks - .get<[GetBlockParams?], Promise>({ - method: "getBlock", - create: (mock) => - mock.resolves({ - blockNumber: 0n, - timestamp: 0n, - }), - }) - .withArgs(params); - } - - getBlock = async (params?: GetBlockParams) => { - return this.mocks.get<[GetBlockParams?], Promise>({ - method: "getBlock", - create: (mock) => - mock.resolves({ - blockNumber: 0n, - timestamp: 0n, - }), - })(params); - }; - - // getBalance // - - onGetBalance(params?: Partial) { - let mock = this.mocks.get<[GetBalanceParams], Promise>({ - method: "getBalance", - create: (mock) => mock.resolves(0n), - }); - if (params) { - mock = mock.withArgs(params); - } - return mock; - } - - getBalance = async (params: GetBalanceParams) => { - return this.mocks.get<[GetBalanceParams], Promise>({ - method: "getBalance", - create: (mock) => mock.resolves(0n), - })(params); - }; - - // getTransaction // - - onGetTransaction(params?: Partial) { - let mock = this.mocks.get< - [GetTransactionParams], - Promise - >({ - method: "getTransaction", - create: (mock) => mock.resolves(undefined), - }); - if (params) { - mock = mock.withArgs(params); - } - return mock; - } - - getTransaction = async (params: GetTransactionParams) => { - return this.mocks.get< - [GetTransactionParams], - Promise - >({ - method: "getTransaction", - create: (mock) => mock.resolves(undefined), - })(params); - }; + onGetBlock = (params?: Partial) => + this.adapter.onGetBlock(params); - // waitForTransaction // - - onWaitForTransaction(params?: Partial) { - let mock = this.mocks.get< - [WaitForTransactionParams], - Promise - >({ - method: "waitForTransaction", - create: (mock) => mock.resolves(undefined), - }); - if (params) { - mock = mock.withArgs(params); - } - return mock; - } + onGetBalance = (params?: Partial) => + this.adapter.onGetBalance(params); - waitForTransaction = async (params: WaitForTransactionParams) => { - return this.mocks.get< - [WaitForTransactionParams], - Promise - >({ - method: "waitForTransaction", - create: (mock) => mock.resolves(undefined), - })(params); - }; + onGetTransaction = (params?: Partial) => + this.adapter.onGetTransaction(params); - // encodeFunction // + onWaitForTransaction = (params?: Partial) => + this.adapter.onWaitForTransaction(params); - onEncodeFunctionData< + onEncodeFunctionData = < TAbi extends Abi, TFunctionName extends FunctionName, - >( - params: OptionalKeys, "args">, - ) { - return this.mocks - .get<[EncodeFunctionDataParams], Bytes>({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - }) - .withArgs(params); - } - - encodeFunctionData = < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: EncodeFunctionDataParams, - ) => { - return this.mocks.get<[EncodeFunctionDataParams], Bytes>({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - })(params); - }; - - // decodeFunction // + >({ + abi, + fn, + args, + }: OptionalKeys, "args">) => + this.adapter.onEncodeFunctionData({ + abi, + fn, + args, + }); - onDecodeFunctionData< + onDecodeFunctionData = < TAbi extends Abi, TFunctionName extends FunctionName, >( params: OptionalKeys, "data">, - ) { - return this.mocks - .get< - [DecodeFunctionDataParams], - DecodedFunctionData - >({ - method: "decodeFunctionData", - key: params.fn, - }) - .withArgs(params); - } + ) => this.adapter.onDecodeFunctionData(params); - decodeFunctionData = < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: DecodeFunctionDataParams, - ) => { - return this.mocks.get< - [DecodeFunctionDataParams], - DecodedFunctionData - >({ - method: "decodeFunctionData", - // TODO: This should be specific to the abi to ensure the correct return - // type. - key: params.fn, - })(params); - }; - - // getEvents // - - onGetEvents>( + onGetEvents = >( params: OptionalKeys, "address">, - ) { - return this.mocks - .get< - [GetEventsParams], - Promise[]> - >({ - method: "getEvents", - key: params.event, - }) - .withArgs(params); - } - - getEvents = async >( - params: GetEventsParams, - ) => { - return this.mocks.get< - [GetEventsParams], - Promise[]> - >({ - method: "getEvents", - key: params.event, - })(params); - }; + ) => this.adapter.onGetEvents(params); - // read // - - onRead< - TAbi extends Abi, - TFunctionName extends FunctionName, - >(params: OptionalKeys, "args" | "address">) { - return this.mocks - .get< - [ReadParams], - Promise> - >({ - method: "read", - key: params.fn, - }) - .withArgs(params as Partial>); - } - - read = async < + onRead = < TAbi extends Abi, TFunctionName extends FunctionName, >( - params: ReadParams, - ) => { - return this.mocks.get< - [ReadParams], - Promise> - >({ - method: "read", - key: params.fn, - })(params); - }; - - // simulateWrite // + params: OptionalKeys, "args" | "address">, + ) => this.adapter.onRead(params); - onSimulateWrite< + onSimulateWrite = < TAbi extends Abi, TFunctionName extends FunctionName, >( params: OptionalKeys, "args" | "address">, - ) { - return this.mocks - .get< - [WriteParams], - Promise> - >({ - method: "simulateWrite", - key: params.fn, - }) - .withArgs(params as Partial>); - } + ) => this.adapter.onSimulateWrite(params); - simulateWrite = async < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: WriteParams, - ) => { - return this.mocks.get< - [WriteParams], - Promise> - >({ - method: "simulateWrite", - key: params.fn, - })(params); - }; - - // write // - - onWrite< + onWrite = < TAbi extends Abi, TFunctionName extends FunctionName, >( params: OptionalKeys, "args" | "address">, - ) { - return this.mocks - .get<[WriteParams], Promise>({ - method: "write", - key: params.fn, - create: (mock) => mock.resolves("0x0"), - }) - .withArgs(params as Partial>); - } - - write = async < - TAbi extends Abi, - TFunctionName extends FunctionName, - >( - params: WriteParams, - ) => { - const writePromise = Promise.resolve( - this.mocks.get< - [WriteParams], - Promise - >({ - method: "write", - key: params.fn, - create: (mock) => mock.resolves("0x0"), - })(params), - ); - - // TODO: unit test - if (params.onMined) { - writePromise.then((hash) => { - this.waitForTransaction({ hash }).then(params.onMined); - return hash; - }); - } - - return writePromise; - }; - - // getSignerAddress // - - onGetSignerAddress() { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), - }); - } + ) => this.adapter.onWrite(params); - getSignerAddress = async () => { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), - })(); - }; + onGetSignerAddress = () => this.adapter.onGetSignerAddress(); } From a6a94406cf1a2d2e1faed2eaa7e740f5eca2ab94 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 03:30:25 -0500 Subject: [PATCH 42/49] Simplify MockContract --- .../drift/src/client/Contract/MockContract.ts | 287 +++--------------- packages/drift/src/client/Drift/MockDrift.ts | 5 +- 2 files changed, 47 insertions(+), 245 deletions(-) diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 433e36c9..c3576c7a 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -1,38 +1,21 @@ import type { Abi } from "abitype"; -import type { SinonStub } from "sinon"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { - ContractGetEventsOptions, ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; -import type { ContractEvent, EventName } from "src/adapter/types/Event"; -import type { - DecodedFunctionData, - FunctionArgs, - FunctionName, - FunctionReturn, -} from "src/adapter/types/Function"; +import type { EventName } from "src/adapter/types/Event"; +import type { FunctionArgs, FunctionName } from "src/adapter/types/Function"; import type { ClientCache } from "src/cache/ClientCache/types"; import { - type ContractEncodeFunctionDataArgs, type ContractGetEventsArgs, type ContractParams, - type ContractReadArgs, - type ContractWriteArgs, ReadWriteContract, } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; -import type { AdapterReadParams } from "src/exports"; -import type { Address, Bytes, TransactionHash } from "src/types"; -import { MockStore } from "src/utils/testing/MockStore"; +import type { Bytes } from "src/types"; import type { OptionalKeys } from "src/utils/types"; -// TODO: DRY up the mock clients and integrate them better so that modifying a -// mock fn in one client will modify the same mock in another client, even if -// the signatures are different. This might mean replacing the `on` methods with -// specific mock methods to control their behavior. - export type MockContractParams = Omit< OptionalKeys, "address">, "adapter" | "cache" @@ -42,8 +25,6 @@ export class MockContract< TAbi extends Abi = Abi, TCache extends ClientCache = ClientCache, > extends ReadWriteContract { - mocks = new MockStore>(); - constructor({ abi, address = ZERO_ADDRESS, @@ -59,250 +40,74 @@ export class MockContract< reset = () => this.adapter.reset(); - // getEvents // - - onGetEvents>( + onGetEvents = >( ...[event, options]: ContractGetEventsArgs - ) { - return this.mocks - .get< - [ - event: TEventName, - options?: ContractGetEventsOptions, - ], - Promise[]> - >({ - method: "getEvents", - key: event, - }) - .withArgs(event, options); - } - - getEvents = async >( - event: TEventName, - options?: ContractGetEventsOptions, - ) => { - return this.mocks.get< - [event: TEventName, options?: ContractGetEventsOptions], - Promise[]> - >({ - method: "getEvents", - key: event, - })(event, options); - }; - - // read // + ) => + this.adapter.onGetEvents({ + abi: this.abi, + address: this.address, + event, + ...options, + }); - onRead>( + onRead = >( fn: TFunctionName, args?: FunctionArgs, options?: ContractReadOptions, - ) { - // return this.mocks - // .get< - // ContractReadArgs, - // Promise> - // >({ - // method: "read", - // }) - // .withArgs(fn, args as any, options); - - // const mock = this.adapter.onRead({ - // abi: this.abi as Abi, - // address: this.address, - // fn, - // ...{ - // args, - // ...options, - // }, - // }) as SinonStub as SinonStub< - // ContractReadArgs, - // Promise> - // >; - - // return mock.withArgs(fn, args as any, options); - - return this.adapter.onRead({ - abi: this.abi as Abi, - address: this.address, - args, - fn, - ...options, - }) as SinonStub as SinonStub< - [Omit, "address" | "abi" | "fn">], - Promise> - >; - } - - read = async >( - ...[fn, args, options]: ContractReadArgs - ) => { - // return this.mocks.get< - // ContractReadArgs, - // Promise> - // >({ - // method: "read", - // key: fn, - // })(fn, args as any, options); - - // return this.onRead(fn, args as any, options)(fn, args as any, options); - - // return this.onRead( - // fn, - // args as any, - // options, - // )({ - // args, - // ...options, - // } as Omit< - // AdapterReadParams, - // "address" | "abi" | "fn" - // >); - return this.adapter.onRead({ - abi: this.abi as Abi, - address: this.address, - fn, - args, - ...options, - })({ + ) => + this.adapter.onRead({ abi: this.abi as Abi, address: this.address, fn, args, ...options, - }) as unknown as FunctionReturn; - }; - - // simulateWrite // + }); - onSimulateWrite< + onSimulateWrite = < TFunctionName extends FunctionName, >( fn: TFunctionName, args?: FunctionArgs, options?: ContractWriteOptions, - ) { - return this.mocks - .get< - ContractWriteArgs, - Promise> - >({ - method: "simulateWrite", - key: fn, - }) - .withArgs(fn, args as any, options); - } - - simulateWrite = async < - TFunctionName extends FunctionName, - >( - ...[fn, args, options]: ContractWriteArgs - ) => { - return this.mocks.get< - ContractWriteArgs, - Promise> - >({ - method: "simulateWrite", - key: fn, - })(fn, args as any, options); - }; - - // encodeFunction // + ) => + this.adapter.onSimulateWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); - onEncodeFunctionData>( + onEncodeFunctionData = >( fn?: TFunctionName, args?: FunctionArgs, - ) { - let mock = this.mocks.get< - ContractEncodeFunctionDataArgs, - Bytes - >({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), + ) => + this.adapter.onEncodeFunctionData({ + abi: this.abi, + fn, + args, }); - if (fn && args) { - mock = mock.withArgs(fn, args); - } - return mock; - } - - encodeFunctionData = >( - ...[fn, args]: ContractEncodeFunctionDataArgs - ) => { - return this.mocks.get< - ContractEncodeFunctionDataArgs, - Bytes - >({ - method: "encodeFunctionData", - create: (mock) => mock.returns("0x0"), - })(fn, args as FunctionArgs); - }; - - // decodeFunction // - - onDecodeFunctionData>(data?: Bytes) { - return this.mocks - .get<[data: Bytes], DecodedFunctionData>({ - method: "decodeFunctionData", - }) - .withArgs(data); - } - - decodeFunctionData = >( - data: Bytes, - ) => { - return this.mocks.get< - [data: Bytes], - DecodedFunctionData - >({ - method: "decodeFunctionData", - })(data); - }; - // getSignerAddress // - - onGetSignerAddress() { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), + onDecodeFunctionData = (data?: Bytes) => + this.adapter.onDecodeFunctionData({ + abi: this.abi, + data, }); - } - getSignerAddress = async () => { - return this.mocks.get<[], Address>({ - method: "getSignerAddress", - create: (mock) => mock.resolves("0xMockSigner"), - })(); - }; + onGetSignerAddress = () => this.adapter.onGetSignerAddress(); - // write // - - onWrite>( + onWrite = < + TFunctionName extends FunctionName, + >( fn: TFunctionName, args?: FunctionArgs, options?: ContractWriteOptions, - ) { - return this.mocks - .get, Promise>({ - method: "write", - key: fn, - create: (mock) => mock.resolves("0x0"), - }) - .withArgs(fn, args as any, options); - } - - write = async < - TFunctionName extends FunctionName, - >( - ...[fn, args, options]: ContractWriteArgs - ) => { - return this.mocks.get< - ContractWriteArgs, - Promise - >({ - method: "write", - key: fn, - create: (mock) => mock.resolves("0x0"), - })(fn, args as any, options); - }; + ) => + this.adapter.onWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); } diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index f7cef745..7d1fedb5 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -19,17 +19,14 @@ import { type WaitForTransactionParams, type WriteParams, } from "src/client/Drift/Drift"; -import { MockStore } from "src/utils/testing/MockStore"; import type { OptionalKeys } from "src/utils/types"; export class MockDrift extends Drift { - mocks = new MockStore>(); - constructor() { super(new MockAdapter()); } - reset = () => this.mocks.reset(); + reset = () => this.adapter.reset(); contract = ( params: MockContractParams, From 9b3b4781cde81d0a3580568b4d27c459c59f912d Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 03:53:09 -0500 Subject: [PATCH 43/49] Fix types --- .../drift/src/client/Contract/MockContract.ts | 38 ++++++++++++++++--- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index c3576c7a..14c9d693 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -1,11 +1,16 @@ import type { Abi } from "abitype"; +import type { SinonStub } from "sinon"; import { MockAdapter } from "src/adapter/MockAdapter"; import type { ContractReadOptions, ContractWriteOptions, } from "src/adapter/types/Contract"; -import type { EventName } from "src/adapter/types/Event"; -import type { FunctionArgs, FunctionName } from "src/adapter/types/Function"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; +import type { + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; import type { ClientCache } from "src/cache/ClientCache/types"; import { type ContractGetEventsArgs, @@ -13,7 +18,13 @@ import { ReadWriteContract, } from "src/client/Contract/Contract"; import { ZERO_ADDRESS } from "src/constants"; +import type { + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, +} from "src/exports"; import type { Bytes } from "src/types"; +import { IERC20 } from "src/utils/testing/IERC20"; import type { OptionalKeys } from "src/utils/types"; export type MockContractParams = Omit< @@ -48,7 +59,10 @@ export class MockContract< address: this.address, event, ...options, - }); + }) as SinonStub< + [AdapterGetEventsParams], + Promise[]> + >; onRead = >( fn: TFunctionName, @@ -61,7 +75,10 @@ export class MockContract< fn, args, ...options, - }); + }) as SinonStub as SinonStub< + [AdapterReadParams], + Promise> + >; onSimulateWrite = < TFunctionName extends FunctionName, @@ -76,7 +93,10 @@ export class MockContract< fn, args, ...options, - }); + }) as SinonStub as SinonStub< + [AdapterWriteParams, "abi" | "address"], + Promise> + >; onEncodeFunctionData = >( fn?: TFunctionName, @@ -111,3 +131,11 @@ export class MockContract< ...options, }); } + +const foo = new MockContract({ abi: IERC20.abi }); +foo + .onWrite("transfer", { + to: "0x123", + value: 100n, + }) + .resolves(); From 18038a862c0804c0f7049526d09187ccfd7f420d Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 04:00:23 -0500 Subject: [PATCH 44/49] Add workflow env variables for tests --- .github/workflows/pull-request.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1f329270..bae079df 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -22,10 +22,13 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20 - cache: 'yarn' + cache: "yarn" - name: Install dependencies run: yarn --frozen-lockfile - name: Run ${{ matrix.task }} run: yarn ${{ matrix.task }} + env: + VITE_RPC_URL: ${{ secrets.MAINNET_RPC_URL }} + VITE_TOKEN_ADDRESS: "0x6B175474E89094C44Da98b954EedeAC495271d0F" From 3776977683e1babd2613c12724481cd7562caba8 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 04:16:22 -0500 Subject: [PATCH 45/49] Couple more tests, fixme --- packages/drift/src/adapter/MockAdapter.ts | 1 + .../src/client/Contract/MockContract.test.ts | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts index 3c886f39..c7235996 100644 --- a/packages/drift/src/adapter/MockAdapter.ts +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -156,6 +156,7 @@ export class MockAdapter implements ReadWriteAdapter { onEncodeFunctionData< TAbi extends Abi, TFunctionName extends FunctionName, + // FIXME: The `fn` prop is getting typed as a generic `string`. >(params: Partial>) { return this.mocks .get<[AdapterEncodeFunctionDataParams], Bytes>({ diff --git a/packages/drift/src/client/Contract/MockContract.test.ts b/packages/drift/src/client/Contract/MockContract.test.ts index 435e739c..f97ff312 100644 --- a/packages/drift/src/client/Contract/MockContract.test.ts +++ b/packages/drift/src/client/Contract/MockContract.test.ts @@ -71,6 +71,28 @@ describe("MockContract", () => { await contract.getEvents("Transfer", { filter: { from: "0x2" } }), ).toBe(events2); }); + + it("Inherits stubbed values from the adapter", async () => { + const contract = new MockContract({ abi }); + const events: ContractEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x", + to: "0x", + value: 123n, + }, + }, + ]; + contract.adapter + .onGetEvents({ + abi: contract.abi, + address: contract.address, + event: "Transfer", + }) + .resolves(events); + expect(await contract.getEvents("Transfer")).toBe(events); + }); }); describe("read", () => { @@ -163,6 +185,21 @@ describe("MockContract", () => { await contract.simulateWrite("transfer", { to: "0x2", value: 123n }), ).toBe(false); }); + + it("Inherits stubbed values from the adapter", async () => { + const contract = new MockContract({ abi }); + contract.adapter + .onSimulateWrite({ + abi: contract.abi, + address: contract.address, + fn: "transfer", + args: { to: "0x", value: 123n }, + }) + .resolves(true); + expect( + await contract.simulateWrite("transfer", { to: "0x", value: 123n }), + ).toBe(true); + }); }); describe("encodeFunctionData", () => { From e9eb59d760b9fe5f5b675735496e50ffd67b1298 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 04:35:52 -0500 Subject: [PATCH 46/49] Make MockDrift and MockContract share adapters --- .../drift/src/client/Contract/MockContract.ts | 17 +++-- .../drift/src/client/Drift/MockDrift.test.ts | 71 +++++++++++++------ packages/drift/src/client/Drift/MockDrift.ts | 6 +- 3 files changed, 67 insertions(+), 27 deletions(-) diff --git a/packages/drift/src/client/Contract/MockContract.ts b/packages/drift/src/client/Contract/MockContract.ts index 14c9d693..59a5d649 100644 --- a/packages/drift/src/client/Contract/MockContract.ts +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -27,23 +27,28 @@ import type { Bytes } from "src/types"; import { IERC20 } from "src/utils/testing/IERC20"; import type { OptionalKeys } from "src/utils/types"; -export type MockContractParams = Omit< - OptionalKeys, "address">, - "adapter" | "cache" +export type MockContractParams< + TAbi extends Abi = Abi, + TAdapter extends MockAdapter = MockAdapter, +> = Omit< + OptionalKeys, "address" | "adapter">, + "cache" >; export class MockContract< TAbi extends Abi = Abi, TCache extends ClientCache = ClientCache, -> extends ReadWriteContract { + TAdapter extends MockAdapter = MockAdapter, +> extends ReadWriteContract { constructor({ abi, + adapter = new MockAdapter() as TAdapter, address = ZERO_ADDRESS, cacheNamespace, - }: MockContractParams) { + }: MockContractParams) { super({ abi, - adapter: new MockAdapter(), + adapter, address, cacheNamespace, }); diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts index e0cc3b60..5a37117f 100644 --- a/packages/drift/src/client/Drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -17,26 +17,57 @@ import { IERC20 } from "src/utils/testing/IERC20"; import { describe, expect, it } from "vitest"; describe("MockDrift", () => { - it("Creates mock read-write contracts", async () => { - const mockDrift = new MockDrift(); - const mockContract = mockDrift.contract({ - abi: IERC20.abi, - address: "0xVaultAddress", - }); - - mockContract - .onWrite("approve", { - spender: "0x1", - value: 100n, - }) - .resolves("0xHash"); - - expect( - await mockContract.write("approve", { - spender: "0x1", - value: 100n, - }), - ).toBe("0xHash"); + describe("contract", () => { + it("Creates mock read-write contracts", async () => { + const mockDrift = new MockDrift(); + const mockContract = mockDrift.contract({ + abi: IERC20.abi, + address: "0xVaultAddress", + }); + + mockContract + .onWrite("approve", { + spender: "0x1", + value: 100n, + }) + .resolves("0xHash"); + + expect( + await mockContract.write("approve", { + spender: "0x1", + value: 100n, + }), + ).toBe("0xHash"); + }); + + // biome-ignore lint/suspicious/noFocusedTests: + it.only("Creates contracts that share mock values", async () => { + const mockDrift = new MockDrift(); + const contract = mockDrift.contract({ + abi: IERC20.abi, + address: "0xVaultAddress", + }); + + mockDrift + .onRead({ + abi: IERC20.abi, + address: "0xVaultAddress", + fn: "symbol", + }) + .resolves("VAULT"); + + expect(await contract.read("symbol")).toBe("VAULT"); + + contract.onRead("name").resolves("Vault Token"); + + expect( + await mockDrift.read({ + abi: IERC20.abi, + address: "0xVaultAddress", + fn: "name", + }), + ).toBe("Vault Token"); + }); }); describe("getChainId", () => { diff --git a/packages/drift/src/client/Drift/MockDrift.ts b/packages/drift/src/client/Drift/MockDrift.ts index 7d1fedb5..8f91675e 100644 --- a/packages/drift/src/client/Drift/MockDrift.ts +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -30,7 +30,11 @@ export class MockDrift extends Drift { contract = ( params: MockContractParams, - ): MockContract => new MockContract(params); + ): MockContract => + new MockContract({ + ...params, + adapter: this.adapter, + }); onGetChainId = () => this.adapter.onGetChainId(); From c63d5e8d279c1807caed9e58dce50a752f085ccb Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 10:27:16 -0500 Subject: [PATCH 47/49] Remove only --- packages/drift/src/client/Drift/MockDrift.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts index 5a37117f..91ef5c3a 100644 --- a/packages/drift/src/client/Drift/MockDrift.test.ts +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -40,8 +40,7 @@ describe("MockDrift", () => { ).toBe("0xHash"); }); - // biome-ignore lint/suspicious/noFocusedTests: - it.only("Creates contracts that share mock values", async () => { + it("Creates contracts that share mock values", async () => { const mockDrift = new MockDrift(); const contract = mockDrift.contract({ abi: IERC20.abi, From 54e58888715077632e2febef222c96d775a2574d Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 10:33:35 -0500 Subject: [PATCH 48/49] Add READMEs --- README.md | 2 +- packages/drift/README.md | 522 ++++++++++++++++++++++++++++++++------- 2 files changed, 430 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index fbe6d592..a95e2b4e 100644 --- a/README.md +++ b/README.md @@ -462,4 +462,4 @@ Guide](./.github/CONTRIBUTING.md) to get started. ## License -Drift is open-source software licensed under the [TODO License](LICENSE). +Drift is open-source software licensed under the [Apache 2.0](./LICENSE). diff --git a/packages/drift/README.md b/packages/drift/README.md index c9002279..a95e2b4e 100644 --- a/packages/drift/README.md +++ b/packages/drift/README.md @@ -1,129 +1,465 @@ -# @delvtech/evm-client +# Drift + +**Effortless Ethereum Development Across Web3 Libraries** + +Write Ethereum smart contract interactions once with Drift and run them +anywhere. Seamlessly support multiple web3 libraries like ethers.js, viem, and +moreβ€”without getting locked into a single provider or rewriting code. + +With built-in caching, type-safe contract APIs, and easy-to-use testing mocks, +Drift lets you build efficient and reliable applications without worrying about +call optimizations or juggling countless hooks. Focus on what matters: creating +great features and user experiences. + +## Why Drift? + +Building on Ethereum often means dealing with: + +- **Hard Dependency on a Specific Web3 Library:** There are several competing + options, like ethers.js, viem, or web3.js. Tying your business logic to a + specific one creates vendor lock-in and makes it harder to switch down the + road. +- **Managing Multiple Hooks:** Each contract call often needs its own hook and + query key to prevent redundant network requests. +- **Optimizing Network Calls:** Manually caching calls and optimizing queries to + minimize RPC requests slows down development. +- **Complex Testing:** Setting up mocks for contract interactions can be + cumbersome and error-prone. + +## Drift Solves These Problems + +- 🌐 **Multi-Library Support:** Drift provides a unified interface compatible + with multiple web3 libraries. Write your contract logic once and use it across + different providers. +- ⚑ **Optimized Performance:** Automatically reduces redundant RPC calls with + built-in caching. No need to manage hooks or query keys for each call. +- πŸ”’ **Type Safety:** Drift's type-checked APIs help catch errors at compile + time. +- πŸ§ͺ **Testing Made Easy:** Built-in mocks simplify testing your contract + interactions. Drift's testing mocks are also type-safe, ensuring your tests + are always in sync with your contracts. +- πŸ”„ **Extensibility:** Designed to grow with your project's needs, Drift allows + you to easily extend support to new web3 libraries by creating small adapter + packages. + +## Installation + +Install Drift and the adapter for your preferred web3 library: + +```bash +npm install @delvtech/drift +# For ethers.js +npm install @delvtech/drift-ethers +# For viem +npm install @delvtech/drift-viem +``` + +## Start Drifting + +### 1. Initialize Drift with your chosen adapter + +```typescript +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { createPublicClient, http } from "viem"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +// optionally, create a wallet client +const walletClient = createWalletClient({ + transport: http(), + // ...other options +}); + +const drift = new Drift(viemAdapter({ publicClient, walletClient })); +``` + +### 2. Interact with your Contracts + +#### Read Operations with Caching + +```typescript +import { VaultAbi } from "./abis/VaultAbi"; + +// No need to wrap in separate hooks; Drift handles caching internally +const balance = await drift.read({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "balanceOf", + args: { + account: "0xUserAddress", + }, +}); +``` + +#### Write Operations + +If Drift was initialized with a wallet client, you can perform write operations: + +```typescript +const txHash = await drift.write({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "deposit", + args: { + amount: BigInt(100e18), + receiver: "0xReceiverAddress", + }, + + // Optionally wait for the transaction to be mined and invalidate cache + onMined: () => { + drift.cache.invalidateRead({ + abi: VaultAbi, + address: "0xYourVaultAddress", + fn: "balanceOf", + args: { + account: "0xReceiverAddress", + }, + }); + }, +}); +``` + +#### Contract Instances + +Create contract instances to write your options once and get a streamlined, +type-safe API to re-use across your application. + +```typescript +const vault = drift.contract({ + abi: VaultAbi, + address: "0xYourVaultAddress", + // ...other options +}); + +const balance = await vault.read("balanceOf", { account }); + +const txHash = await vault.write( + "deposit", + { + amount: BigInt(100e18), + receiver: "0xReceiverAddress", + }, + { + onMined: () => { + vault.invalidateRead("balanceOf", { account: "0xReceiverAddress" }); + }, + }, +); +``` -Useful EVM client abstractions for TypeScript projects that want to remain web3 -library agnostic. +## Example: Building Vault Clients -```ts +Let's build a simple library agnostic SDK with `ReadVault` and `ReadWriteVault` +clients using Drift. + +### 1. Define core vault clients + +In your core SDK package, define the `ReadVault` and `ReadWriteVault` clients +using Drift's `ReadContract` and `ReadWriteContract` abstractions. + +```typescript +// sdk-core/src/VaultClient.ts import { - CachedReadWriteContract, - ContractReadOptions, -} from '@delvtech/evm-client'; -import erc20Abi from './abis/erc20Abi.json'; - -type CachedErc20Contract = CachedReadWriteContract; - -async function approve( - contract: CachedErc20Contract, - spender: `0x${string}`, - amount: bigint, -) { - const hash = await contract.write('approve', { spender, amount }); - - this.contract.deleteRead('allowance', { - owner: await contract.getSignerAddress(), - spender, - }); + ContractEvent, + Drift, + ReadContract, + ReadWriteAdapter, + ReadWriteContract, +} from "@delvtech/drift"; +import { vaultAbi } from "./abis/VaultAbi"; + +type VaultAbi = typeof vaultAbi; + +export class ReadVault { + contract: ReadContract; + + constructor(address: string, drift: Drift) { + this.contract = drift.contract({ + abi: vaultAbi, + address, + }); + } + + // Read balance with internal caching + async getBalance(account: string): Promise { + return this.contract.read("balanceOf", { account }); + } + + // Get all deposit events for an account with internal caching + async getDeposits( + account?: string, + ): Promise[]> { + return this.contract.getEvents("Deposit", { + filter: { + depositor: account, + recipient: account, + }, + }); + } +} + +export class ReadWriteVault extends ReadVault { + declare contract: ReadWriteContract; + + constructor(address: string, drift: Drift) { + super(wallet, drift); + } + + // Make a deposit + async deposit(amount: bigint, recipient: string): Promise { + const txHash = await this.contract.write( + "deposit", + { amount, recipient }, + { + // Optionally wait for the transaction to be mined and invalidate cache + onMined: () => { + this.contract.invalidateRead("balanceOf", { recipient }); + }, + }, + ); + + return txHash; + } +} +``` + +### 2. Use the clients in your application + +Using an adapter, you can integrate Drift with your chosen web3 library. Here's +an example using `viem`: + +```typescript +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { createPublicClient, http } from "viem"; +import { ReadVault } from "sdk-core"; + +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); + +const drift = new Drift(viemAdapter({ publicClient })); + +// Instantiate the ReadVault client +const readVault = new ReadVault("0xYourVaultAddress", drift); + +// Fetch user balance +const userBalance = await readVault.getBalance("0xUserAddress"); + +// Get deposit history +const deposits = await readVault.getDeposits("0xUserAddress"); +``` + +#### Benefits of This Architecture + +- **Modularity:** Your core logic remains untouched when switching web3 + libraries. +- **Reusability:** Write your business logic once and reuse it across different + environments. +- **Flexibility:** Easily extend support to new web3 libraries by creating small + adapter packages. +- **Simplicity:** Your application code stays clean and focused on business + logic rather than on handling different web3 providers. + +### 3. Extend core clients for library-specific clients + +To provide library specific client packages, e.g., `sdk-viem`, extend the core +clients and overwrite their constructors to accept `viem` clients. - return hash; +```typescript +// sdk-viem/src/VaultClient.ts +import { + ReadVault as CoreReadVault, + ReadWriteVault as CoreReadWriteVault, +} from "sdk-core"; +import { Drift } from "@delvtech/drift"; +import { viemAdapter } from "@delvtech/drift-viem"; +import { PublicClient, WalletClient } from "viem"; + +export class ReadVault extends CoreReadVault { + constructor(address: string, publicClient: PublicClient) { + const drift = new Drift(viemAdapter({ publicClient })); + super(address, drift); + } +} + +export class ReadWriteVault extends CoreReadWriteVault { + constructor( + address: string, + publicClient: PublicClient, + walletClient: WalletClient, + ) { + const drift = new Drift(viemAdapter({ publicClient, walletClient })); + super(address, drift); + } } ``` -This project contains types that can be used in TypeScript projects that need to -interact with contracts in a type-safe way based on ABIs. It allows your project -to focus on core contract logic and remain flexible in it's implementation. To -aid in implementation, this project provides: +Then, in your app: -- Utility types -- Utility functions for transforming arguments -- Factories for wrapping contract instances with caching logic -- Stubs to facilitate testing +```typescript +import { ReadVault } from "sdk-viem"; +import { createPublicClient, http } from "viem"; -## Primary Abstractions +const publicClient = createPublicClient({ + transport: http(), + // ...other options +}); -### Contracts +// Instantiate the ReadVault client with viem directly +const readVault = new ReadVault("0xYourVaultAddress", publicClient); +``` -The contract abstraction lets you write type-safe contract interactions that can -be implemented in multiple web3 libraries and even multiple persistence layers. -The API is meant to be easy to both read and write. +### 4. Test Your Clients with Drift's Built-in Mocks -#### Types +Testing smart contract interactions can be complex and time-consuming. Drift +simplifies this process by providing built-in mocks that allow you to stub +responses and focus on testing your application logic. -- **[`ReadContract`](./src/contract/types/Contract.ts):** A basic contract that - can be used to fetch data, but can't submit transactions. -- **[`ReadWriteContract`](./src/contract/types/Contract.ts):** An extended - `ReadContract` that has a signer attached to it and can be used to submit - transactions. -- **[`CachedReadContract`](./src/contract/types/CachedContract.ts):** An - extended `ReadContract` that will cache reads and event queries based on - arguments with a few additional methods for interacting with the cache. -- **[`CachedReadWriteContract`](./src/contract/types/CachedContract.ts):** An - extended `CachedReadContract` that has a signer attached to it and can be used - to submit transactions. +#### Example: Testing Client Methods with Multiple RPC Calls -#### Utils +Suppose you have a method `getAccountValue` in your `ReadVault` client that +get's the total asset value for an account by fetching their vault balance and +converting it to assets. Under the hood, this method makes multiple RPC +requests. -- **[`objectToArray`](./src/contract/utils/friendlyToArray.ts):** A function - that takes an object of inputs (function and event arguments) and converts it - into an array, ensuring parameters are properly ordered and the correct number - of parameters are present. +Here's how you can use Drift's mocks to stub contract calls and test your +method: -- **[`arrayToObject`](./src/contract/utils/arrayToFriendly.ts):** The opposite - of `objectToArray`. A function to transform contract input and output arrays - into objects. +```typescript +// sdk-core/src/ReadVault.test.ts +import { MockDrift } from "@delvtech/drift/testing"; +import { vaultAbi } from "./abis/VaultAbi"; +import { ReadVault } from "./VaultClient"; -- **[`arrayToFriendly`](./src/contract/utils/arrayToFriendly.ts):** A function - to transform contract output arrays into "Friendly" types. The friendly type - of an output array depends on the number of output parameters: +test("getUserAssetValue should return the total asset value for a user", async () => { + // Set up mocks + const mockDrift = new MockDrift(); + const mockContract = mockDrift.contract({ + abi: vaultAbi, + address: "0xVaultAddress", + }); + + // Stub the vault's return values + mockContract.onRead("balanceOf", { account: "0xUserAddress" }).returns( + BigInt(100e18), // User has 100 vault shares + ); + mockContract.onRead("convertToAssets", { shares: BigInt(100e18) }).returns( + BigInt(150e18), // 100 vault shares are worth 150 in assets + ); + + // Instantiate your client with the mocked Drift instance + const readVault = new ReadVault("0xVaultAddress", mockDrift); + + // Call the method you want to test + const accountAssetValue = await readVault.getAccountValue("0xUserAddress"); - - Multiple parameters: An object with the argument names as keys (or their - index if no name is found in the ABI) and the primitive type of the - parameters as values. - - Single parameters: The primitive type of the single parameter. - - No parameters: `undefined` + // Assert the expected result + expect(accountAssetValue).toEqual(BigInt(150e18)); +}); +``` + +#### Benefits -#### Factories +- **No Network Calls:** Tests run faster and more reliably without actual + network interactions. +- **Focus on Logic:** Concentrate on testing your application's business logic. +- **Easy Setup:** Minimal configuration required to get started with testing. -- **[`createCachedReadContract`](./src/contract/factories/createCachedReadContract.ts):** - A factory that turns a `ReadContract` into a `CachedReadContract`. -- **[`createCachedReadWriteContract`](./src/contract/factories/createCachedReadWriteContract.ts):** - A factory that turns a `ReadWriteContract` into a `CachedReadWriteContract`. +## Simplifying React Hook Management -#### Stubs +### The Problem Without Drift -- **[`ReadContractStub`](./src/contract/stubs/ReadContractStub.ts):** A stub of - a `ReadContract` for use in tests. -- **[`ReadWriteContractStub`](./src/contract/stubs/ReadWriteContractStub.ts):** - A stub of a `ReadWriteContract` for use in tests. +In traditional setups, you might rely on data-fetching libraries like React +Query. However, to prevent redundant network requests, each contract call would +need: -### Network +- Its own hook (e.g., `useBalanceOf`, `useTokenSymbol`). +- Unique query keys for caching. -The `Network` abstraction provides a small interface for fetching vital network -information like blocks and transactions. +Composing multiple calls becomes cumbersome, as you have to manage each hook's +result separately. -#### Types +### How Drift Helps -- **[`Network`](./src/network/types/Network.ts)** +Drift's internal caching means you don't need to wrap every contract call in a +separate hook. You can perform multiple contract interactions within a single +function or hook without worrying about redundant requests. -#### Stubs +#### Example Using React Query -- **[`NetworkStub`](./src/network/stubs/NetworkStub.ts):** A stub of a `Network` - for use in tests. +```typescript +import { useQuery } from "@tanstack/react-query"; +import { ReadVault } from "sdk-core"; -### SimpleCache +function useVaultData(readVault: ReadVault, userAddress: string) { + return useQuery(["vaultData", userAddress], async () => { + // Perform multiple reads without separate hooks or query keys + const [balance, symbol, deposits] = await Promise.all([ + readVault.getBalance(userAddress), + readVault.contract.read("symbol"), + readVault.getDeposits(userAddress), + ]); -A simple cache abstraction providing a minimal interface for facilitating -contract caching. + return { balance, symbol, deposits }; + }); +} +``` + +No need to manage multiple hooks or query keys β€” Drift handles caching +internally, simplifying your code and development process. + +## Caching in Action + +Drift's caching mechanism ensures that repeated calls with the same parameters +don't result in unnecessary network requests, even when composed within the same +function. + +```typescript +// Both calls use the cache; only one network request is made +const balance1 = await contract.read("balanceOf", { account }); +const balance2 = await contract.read("balanceOf", { account }); +``` + +You can also manually invalidate the cache if needed: + +```typescript +// Invalidate the cache for a specific read +contract.invalidateRead("balanceOf", { account }); + +// Invalidate all reads matching partial arguments +contract.invalidateReadsMatching("balanceOf"); +``` + +## Advanced Usage + +### Custom Cache Implementation + +If you have specific caching needs, you can provide your own cache +implementation: + +```typescript +import { LRUCache } from "lru-cache"; + +const customCache = new LRUCache({ max: 500 }); +const drift = new Drift(viemAdapter({ publicClient }), { cache: customCache }); +``` -#### Types +### Extending Drift for Your Needs -- **[`SimpleCache`](./src/cache/types/SimpleCache.ts)** +Drift is designed to be extensible. You can build additional abstractions or +utilities on top of it to suit your project's requirements. -#### Utils +## Contributing -- **[`createSimpleCacheKey`](./src/cache/utils/createSimpleCacheKey.ts):** - Creates a consistent serializable cache key from basic types. +Got ideas or found a bug? Check the [Contributing +Guide](./.github/CONTRIBUTING.md) to get started. -#### Factories +## License -- **[`createLruSimpleCache`](./src/cache/factories/createLruSimpleCache.ts):** - Creates a `SimpleCache` instance using an LRU cache. +Drift is open-source software licensed under the [Apache 2.0](./LICENSE). From 560a9dbcd1581a5aa45fa873482da97b2207f308 Mon Sep 17 00:00:00 2001 From: Ryan Goree Date: Mon, 7 Oct 2024 10:34:45 -0500 Subject: [PATCH 49/49] Add changeset --- .changeset/flat-rocks-worry.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/flat-rocks-worry.md diff --git a/.changeset/flat-rocks-worry.md b/.changeset/flat-rocks-worry.md new file mode 100644 index 00000000..d1b4bf34 --- /dev/null +++ b/.changeset/flat-rocks-worry.md @@ -0,0 +1,6 @@ +--- +"@delvtech/drift-viem": patch +"@delvtech/drift": patch +--- + +drift