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 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/.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" 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/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/README.md b/README.md index 380f438f..a95e2b4e 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,465 @@ -# @delvtech/evm-client +# Drift -Useful EVM client abstractions for TypeScript projects that want to remain web3 -library agnostic. +**Effortless Ethereum Development Across Web3 Libraries** -## Packages +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. -- **[@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/). +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. -## Creating a release +## Why Drift? -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. +Building on Ethereum often means dealing with: -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. +- **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. -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`. +## Drift Solves These Problems -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!** +- 🌐 **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" }); + }, + }, +); +``` + +## 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); + } +} +``` + +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 Client Methods with Multiple 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 +// sdk-core/src/ReadVault.test.ts +import { MockDrift } from "@delvtech/drift/testing"; +import { vaultAbi } from "./abis/VaultAbi"; +import { ReadVault } from "./VaultClient"; + +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"); + + // Assert the expected result + expect(accountAssetValue).toEqual(BigInt(150e18)); +}); +``` + +#### 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 [Apache 2.0](./LICENSE). 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/package.json b/package.json index a697d788..0b125237 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { - "name": "evm-client", + "name": "drift", + "license": "AGPL-3.0", "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/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/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/package.json b/packages/drift-viem/package.json new file mode 100644 index 00000000..fe1bed8e --- /dev/null +++ b/packages/drift-viem/package.json @@ -0,0 +1,58 @@ +{ + "name": "@delvtech/drift-viem", + "version": "0.0.0", + "license": "AGPL-3.0", + "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 --reporter=verbose", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "watch": "tsup --watch" + }, + "peerDependencies": { + "sinon": "^17.0.3", + "viem": "^2" + }, + "peerDependenciesMeta": { + "sinon": { + "optional": true + } + }, + "dependencies": { + "@delvtech/drift": "0.0.0" + }, + "devDependencies": { + "@repo/typescript-config": "*", + "@types/sinon": "^17.0.3", + "tsconfig-paths": "^4.2.0", + "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" + }, + "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/ViemReadAdapter.test.ts b/packages/drift-viem/src/ViemReadAdapter.test.ts new file mode 100644 index 00000000..2fc88c5d --- /dev/null +++ b/packages/drift-viem/src/ViemReadAdapter.test.ts @@ -0,0 +1,194 @@ +import type { + Block, + ContractEvent, + DecodedFunctionData, + FunctionArgs, + Transaction, + TransactionReceipt, +} from "@delvtech/drift"; +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"; + +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/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/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/utils/testing/erc20.ts b/packages/drift-viem/src/utils/testing/erc20.ts new file mode 100644 index 00000000..30336081 --- /dev/null +++ b/packages/drift-viem/src/utils/testing/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/src/viemAdapter.test.ts b/packages/drift-viem/src/viemAdapter.test.ts new file mode 100644 index 00000000..3755d505 --- /dev/null +++ b/packages/drift-viem/src/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/src/viemAdapter.ts b/packages/drift-viem/src/viemAdapter.ts new file mode 100644 index 00000000..cda20d22 --- /dev/null +++ b/packages/drift-viem/src/viemAdapter.ts @@ -0,0 +1,28 @@ +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 ? ViemReadAdapter : ViemReadWriteAdapter { + return walletClient + ? new ViemReadWriteAdapter({ publicClient, walletClient }) + : (new ViemReadAdapter({ + publicClient, + }) as ViemReadWriteAdapter); +} diff --git a/packages/drift-viem/tsconfig.json b/packages/drift-viem/tsconfig.json new file mode 100644 index 00000000..0ff67498 --- /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", "src/utils/testing"], + "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/.gitignore b/packages/drift/.gitignore new file mode 100644 index 00000000..de4d1f00 --- /dev/null +++ b/packages/drift/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules 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/README.md b/packages/drift/README.md new file mode 100644 index 00000000..a95e2b4e --- /dev/null +++ b/packages/drift/README.md @@ -0,0 +1,465 @@ +# 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" }); + }, + }, +); +``` + +## 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); + } +} +``` + +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 Client Methods with Multiple 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 +// sdk-core/src/ReadVault.test.ts +import { MockDrift } from "@delvtech/drift/testing"; +import { vaultAbi } from "./abis/VaultAbi"; +import { ReadVault } from "./VaultClient"; + +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"); + + // Assert the expected result + expect(accountAssetValue).toEqual(BigInt(150e18)); +}); +``` + +#### 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 [Apache 2.0](./LICENSE). diff --git a/packages/drift/package.json b/packages/drift/package.json new file mode 100644 index 00000000..7f8e5b37 --- /dev/null +++ b/packages/drift/package.json @@ -0,0 +1,60 @@ +{ + "name": "@delvtech/drift", + "version": "0.0.0", + "license": "AGPL-3.0", + "type": "module", + "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 --reporter=verbose", + "test": "vitest run", + "typecheck": "tsc --noEmit", + "watch": "tsup --watch" + }, + "peerDependencies": { + "sinon": "^17.0.3" + }, + "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.6", + "fast-json-stable-stringify": "^2.1.0", + "sinon": "^17.0.1", + "tsconfig-paths": "^4.2.0", + "tsup": "^8.3.0", + "typescript": "^5.6.2", + "vite-tsconfig-paths": "^5.0.1", + "vitest": "^2.1.2" + }, + "publishConfig": { + "access": "public" + }, + "files": ["dist"] +} diff --git a/packages/drift/src/adapter/MockAdapter.test.ts b/packages/drift/src/adapter/MockAdapter.test.ts new file mode 100644 index 00000000..953ed0f2 --- /dev/null +++ b/packages/drift/src/adapter/MockAdapter.test.ts @@ -0,0 +1,591 @@ +import { MockAdapter } from "src/adapter/MockAdapter"; +import type { + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, +} from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; +import type { ContractEvent } from "src/adapter/types/Event"; +import type { DecodedFunctionData } from "src/adapter/types/Function"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/types/Transaction"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { describe, expect, it } from "vitest"; + +describe("MockAdapter", () => { + 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("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 = 0n } = (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("getBalance", () => { + it("Resolves to a default value", async () => { + const adapter = new MockAdapter(); + expect(await adapter.getBalance({ address: "0x" })).toBeTypeOf("bigint"); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + 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({ hash: "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({ hash: "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({ 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({ hash: "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({ hash: "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({ 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); + }); + }); + + 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: 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"); + expect(adapter.encodeFunctionData(params2)).toBe("0x2"); + }); + }); + + describe("decodeFunctionData", () => { + it("Throws an error by default", async () => { + const adapter = new MockAdapter(); + 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 () => { + const adapter = new MockAdapter(); + const decoded: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; + adapter + .onDecodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }) + .returns(decoded); + expect( + adapter.decodeFunctionData({ + abi: IERC20.abi, + fn: "balanceOf", + data: "0x", + }), + ).toBe(decoded); + }); + + it("Can be stubbed with specific args", async () => { + const adapter = new MockAdapter(); + const params1: AdapterDecodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + abi: IERC20.abi, + fn: "balanceOf", + data: "0x1", + }; + const return1: DecodedFunctionData = { + functionName: "balanceOf", + args: { owner: "0x1" }, + }; + const params2: AdapterDecodeFunctionDataParams< + typeof IERC20.abi, + "balanceOf" + > = { + ...params1, + data: "0x2", + }; + 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); + }); + }); + + describe("getEvents", () => { + it("Rejects with an error by default", async () => { + const adapter = new MockAdapter(); + let error: unknown; + try { + await adapter.getEvents({ + abi: IERC20.abi, + address: "0x", + event: "Transfer", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + it("Can be stubbed", async () => { + const adapter = new MockAdapter(); + const events: ContractEvent[] = [ + { + 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: AdapterGetEventsParams = { + abi: IERC20.abi, + address: "0x1", + event: "Transfer", + filter: { from: "0x1" }, + }; + const params2: AdapterGetEventsParams = { + ...params1, + filter: { from: "0x2" }, + }; + const events1: ContractEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: ContractEvent[] = [ + { + 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(); + let error: unknown; + try { + await adapter.read({ + abi: IERC20.abi, + address: "0x", + fn: "symbol", + }); + } catch (e) { + error = e; + } + expect(error).toBeInstanceOf(Error); + }); + + 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: AdapterReadParams = { + abi: IERC20.abi, + address: "0x1", + fn: "allowance", + args: { owner: "0x1", spender: "0x1" }, + }; + const params2: AdapterReadParams = { + ...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(); + let error: unknown; + try { + await adapter.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 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: AdapterWriteParams = { + abi: IERC20.abi, + address: "0x1", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: AdapterWriteParams = { + ...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 }, + }) + .resolves("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: AdapterWriteParams = { + abi: IERC20.abi, + address: "0x", + fn: "transfer", + args: { to: "0x1", value: 123n }, + }; + const params2: AdapterWriteParams = { + ...params1, + args: { to: "0x2", value: 123n }, + }; + adapter.onWrite(params1).resolves("0x1"); + adapter.onWrite(params2).resolves("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"); + }); + }); +}); diff --git a/packages/drift/src/adapter/MockAdapter.ts b/packages/drift/src/adapter/MockAdapter.ts new file mode 100644 index 00000000..c7235996 --- /dev/null +++ b/packages/drift/src/adapter/MockAdapter.ts @@ -0,0 +1,392 @@ +import type { Abi } from "abitype"; +import type { + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterGetEventsParams, + AdapterReadParams, + AdapterWriteParams, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; +import type { ContractEvent, EventName } from "src/adapter/types/Event"; +import type { + DecodedFunctionData, + FunctionArgs, + FunctionName, + FunctionReturn, +} from "src/adapter/types/Function"; +import type { + NetworkGetBalanceParams, + NetworkGetBlockParams, + NetworkGetTransactionParams, + NetworkWaitForTransactionParams, +} from "src/adapter/types/Network"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/types/Transaction"; +import type { Address, Bytes, TransactionHash } from "src/types"; +import { MockStore } from "src/utils/testing/MockStore"; +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(); + + reset = () => this.mocks.reset(); + + // getChainId // + + onGetChainId() { + return this.mocks.get<[], number>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + }); + } + + async getChainId() { + return this.mocks.get<[], number>({ + method: "getChainId", + create: (mock) => mock.resolves(96024), + })(); + } + + // getBlock // + + onGetBlock(params?: Partial) { + return this.mocks + .get<[NetworkGetBlockParams?], Promise>({ + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }) + .withArgs(params); + } + + async getBlock(params?: NetworkGetBlockParams) { + return this.mocks.get<[NetworkGetBlockParams?], Promise>( + { + method: "getBlock", + create: (mock) => + mock.resolves({ + blockNumber: 0n, + timestamp: 0n, + }), + }, + )(params); + } + + // getBalance // + + 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 getBalance(params: NetworkGetBalanceParams) { + return this.mocks.get<[NetworkGetBalanceParams], Promise>({ + method: "getBalance", + create: (mock) => mock.resolves(0n), + })(params); + } + + // getTransaction // + + 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(params: NetworkGetTransactionParams) { + return this.mocks.get< + [NetworkGetTransactionParams], + Promise + >({ + method: "getTransaction", + create: (mock) => mock.resolves(undefined), + })(params); + } + + // waitForTransaction // + + 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(params: NetworkWaitForTransactionParams) { + return this.mocks.get< + [NetworkWaitForTransactionParams], + Promise + >({ + method: "waitForTransaction", + create: (mock) => mock.resolves(undefined), + })(params); + } + + // encodeFunction // + + 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>({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + }) + .withArgs(params); + } + + encodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: AdapterEncodeFunctionDataParams) { + return this.mocks.get<[AdapterEncodeFunctionDataParams], Bytes>({ + method: "encodeFunctionData", + create: (mock) => mock.returns("0x0"), + })(params); + } + + // decodeFunction // + + onDecodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys< + AdapterDecodeFunctionDataParams, + "data" + >, + ) { + return this.mocks + .get< + [AdapterDecodeFunctionDataParams], + DecodedFunctionData + >({ + method: "decodeFunctionData", + key: params.fn, + }) + .withArgs(params); + } + + decodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: Partial>) { + return this.mocks.get< + [AdapterDecodeFunctionDataParams], + DecodedFunctionData + >({ + method: "decodeFunctionData", + // TODO: This should be specific to the abi to ensure the correct return + // type. + key: params.fn, + })(params as AdapterDecodeFunctionDataParams); + } + + // getEvents // + + onGetEvents>( + params: OptionalKeys, "address">, + ) { + return this.mocks + .get< + [AdapterGetEventsParams], + Promise[]> + >({ + method: "getEvents", + key: params.event, + }) + .withArgs(params); + } + + async getEvents>( + params: AdapterGetEventsParams, + ) { + return this.mocks.get< + [AdapterGetEventsParams], + Promise[]> + >({ + method: "getEvents", + key: params.event, + })(params); + } + + // 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, + >({ + abi, + address, + fn, + args, + block, + }: OptionalKeys, "args" | "address">) { + return this.mocks + .get< + [AdapterReadParams], + Promise> + >({ + method: "read", + key: fn, + }) + .withArgs({ + abi, + address, + fn, + args, + block, + } as Partial>); + } + + async read< + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ abi, address, fn, args, block }: AdapterReadParams) { + return this.mocks.get< + [AdapterReadParams], + Promise> + >({ + method: "read", + key: fn, + })({ + abi, + address: address, + fn: fn, + args: args as FunctionArgs, + block: block, + }); + } + + // simulateWrite // + + onSimulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + 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: AdapterWriteParams) { + return this.mocks.get< + [AdapterWriteParams], + Promise> + >({ + method: "simulateWrite", + key: params.fn, + })(params); + } + + // write // + + onWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + 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: 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.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + }); + } + + async getSignerAddress() { + return this.mocks.get<[], Address>({ + method: "getSignerAddress", + create: (mock) => mock.resolves("0xMockSigner"), + })(); + } +} diff --git a/packages/drift/src/adapter/types/Abi.ts b/packages/drift/src/adapter/types/Abi.ts new file mode 100644 index 00000000..1fbaaf5b --- /dev/null +++ b/packages/drift/src/adapter/types/Abi.ts @@ -0,0 +1,282 @@ +import type { + Abi, + AbiItemType, + AbiParameter, + AbiParameterKind, + AbiParameterToPrimitiveType, + AbiParametersToPrimitiveTypes, + AbiStateMutability, +} from "abitype"; +import type { + EmptyObject, + MergeKeys, + Pretty, + ReplaceProps, +} from "src/utils/types"; + +// https://docs.soliditylang.org/en/latest/abi-spec.html#json + +export type NamedAbiParameter = AbiParameter extends infer TAbiParameter + ? TAbiParameter extends { name: string } + ? TAbiParameter + : ReplaceProps + : never; + +/** + * 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 + : ReplaceProps, { 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 + * 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, +> = NamedAbiParameter[] extends TParameters + ? Record + : Pretty< + { + // 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 + ? unknown extends TPrimitive + ? 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 + >; + }) + >; + +/** + * 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/drift/src/adapter/types/Adapter.ts b/packages/drift/src/adapter/types/Adapter.ts new file mode 100644 index 00000000..550b2cbf --- /dev/null +++ b/packages/drift/src/adapter/types/Adapter.ts @@ -0,0 +1,135 @@ +import type { Abi } from "abitype"; +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 { Network } from "src/adapter/types/Network"; +import type { TransactionReceipt } from "src/adapter/types/Transaction"; +import type { Address, Bytes, TransactionHash } from "src/types"; +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, + >( + params: AdapterReadParams, + ): Promise>; + + simulateWrite< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: AdapterWriteParams, + ): Promise>; + + encodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: AdapterEncodeFunctionDataParams): Bytes; + + decodeFunctionData< + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: AdapterDecodeFunctionDataParams, + ): DecodedFunctionData; +} + +export interface ReadWriteAdapter extends ReadAdapter { + getSignerAddress(): Promise
; + + write< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: AdapterWriteParams): Promise; +} + +export type AdapterArgsParam< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = Abi extends TAbi + ? { + args?: AnyObject; + } + : EmptyObject extends FunctionArgs + ? { + args?: EmptyObject; + } + : { + args: FunctionArgs; + }; + +export type AdapterReadParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName< + TAbi, + "pure" | "view" + >, +> = { + abi: TAbi; + address: Address; + fn: TFunctionName; +} & AdapterArgsParam & + ContractReadOptions; + +export interface AdapterGetEventsParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> extends ContractGetEventsOptions { + abi: TAbi; + address: Address; + event: TEventName; +} + +export interface OnMinedParam { + onMined?: (receipt?: TransactionReceipt) => void; +} + +export type AdapterWriteParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName< + TAbi, + "nonpayable" | "payable" + > = FunctionName, +> = { + abi: TAbi; + address: Address; + fn: TFunctionName; +} & AdapterArgsParam & + ContractWriteOptions & + OnMinedParam; + +export type AdapterEncodeFunctionDataParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = { + abi: TAbi; + fn: TFunctionName; +} & AdapterArgsParam; + +export interface AdapterDecodeFunctionDataParams< + 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/adapter/types/Block.ts b/packages/drift/src/adapter/types/Block.ts new file mode 100644 index 00000000..da151e40 --- /dev/null +++ b/packages/drift/src/adapter/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/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/types/Event.ts b/packages/drift/src/adapter/types/Event.ts new file mode 100644 index 00000000..e2566bf6 --- /dev/null +++ b/packages/drift/src/adapter/types/Event.ts @@ -0,0 +1,64 @@ +import type { Abi } from "abitype"; +import type { + AbiEntry, + AbiObjectType, + AbiParameters, + AbiParametersToObject, + NamedAbiParameter, +} from "src/adapter/types/Abi"; + +/** + * 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 ContractEvent< + TAbi extends Abi, + TEventName extends EventName = EventName, +> { + eventName: TEventName; + args: EventArgs; + data?: `0x${string}`; + blockNumber?: bigint; + transactionHash?: `0x${string}`; +} diff --git a/packages/drift/src/adapter/types/Function.ts b/packages/drift/src/adapter/types/Function.ts new file mode 100644 index 00000000..db2c4e3b --- /dev/null +++ b/packages/drift/src/adapter/types/Function.ts @@ -0,0 +1,59 @@ +import type { Abi, AbiStateMutability } from "abitype"; +import type { AbiFriendlyType, AbiObjectType } from "src/adapter/types/Abi"; + +/** + * Get a union of function names from an abi + */ +export type FunctionName< + TAbi extends Abi, + TAbiStateMutability extends AbiStateMutability = AbiStateMutability, +> = Abi extends TAbi + ? string + : 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 = FunctionName, +> = AbiFriendlyType; + +/** + * Get an object representing decoded function or constructor data from an ABI. + */ +export type DecodedFunctionData< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = { + [K in TFunctionName]: { + args: FunctionArgs; + functionName: K; + }; +}[TFunctionName]; diff --git a/packages/drift/src/adapter/types/Network.ts b/packages/drift/src/adapter/types/Network.ts new file mode 100644 index 00000000..cafdf40b --- /dev/null +++ b/packages/drift/src/adapter/types/Network.ts @@ -0,0 +1,79 @@ +import type { Block, BlockTag } from "src/adapter/types/Block"; +import type { + Transaction, + TransactionReceipt, +} from "src/adapter/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 Network { + /** + * Get the chain ID of the network. + */ + getChainId(): Promise; + + /** + * Get a block from a block tag, number, or hash. If no argument is provided, + * the latest block is returned. + */ + getBlock(params?: NetworkGetBlockParams): Promise; + + /** + * Get the balance of native currency for an account. + */ + getBalance(params: NetworkGetBalanceParams): Promise; + + /** + * Get a transaction from a transaction hash. + */ + getTransaction( + params: NetworkGetTransactionParams, + ): Promise; + + /** + * Wait for a transaction to be mined. + */ + waitForTransaction( + params: NetworkWaitForTransactionParams, + ): Promise; +} + +export type NetworkGetBlockOptions = + | { + blockHash?: HexString; + blockNumber?: never; + blockTag?: never; + } + | { + blockHash?: never; + blockNumber?: bigint; + blockTag?: never; + } + | { + blockHash?: never; + blockNumber?: never; + blockTag?: BlockTag; + }; + +export type NetworkGetBalanceParams = { + address: Address; +} & NetworkGetBlockOptions; + +export type NetworkGetBlockParams = NetworkGetBlockOptions; + +export interface NetworkGetTransactionParams { + hash: TransactionHash; +} + +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/adapter/types/Transaction.ts b/packages/drift/src/adapter/types/Transaction.ts new file mode 100644 index 00000000..c537e787 --- /dev/null +++ b/packages/drift/src/adapter/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/drift/src/adapter/utils/arrayToFriendly.test.ts b/packages/drift/src/adapter/utils/arrayToFriendly.test.ts new file mode 100644 index 00000000..47d5822b --- /dev/null +++ b/packages/drift/src/adapter/utils/arrayToFriendly.test.ts @@ -0,0 +1,67 @@ +import { arrayToFriendly } from "src/adapter/utils/arrayToFriendly"; +import { IERC20 } from "src/utils/testing/IERC20"; +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/drift/src/adapter/utils/arrayToFriendly.ts b/packages/drift/src/adapter/utils/arrayToFriendly.ts new file mode 100644 index 00000000..bc5bdfa4 --- /dev/null +++ b/packages/drift/src/adapter/utils/arrayToFriendly.ts @@ -0,0 +1,105 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiFriendlyType, +} from "src/adapter/types/Abi"; +import { getAbiEntry } from "src/adapter/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/drift/src/adapter/utils/arrayToObject.test.ts b/packages/drift/src/adapter/utils/arrayToObject.test.ts new file mode 100644 index 00000000..ab1738f9 --- /dev/null +++ b/packages/drift/src/adapter/utils/arrayToObject.test.ts @@ -0,0 +1,54 @@ +import { arrayToObject } from "src/adapter/utils/arrayToObject"; +import { IERC20 } from "src/utils/testing/IERC20"; +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/drift/src/adapter/utils/arrayToObject.ts b/packages/drift/src/adapter/utils/arrayToObject.ts new file mode 100644 index 00000000..b4903a54 --- /dev/null +++ b/packages/drift/src/adapter/utils/arrayToObject.ts @@ -0,0 +1,91 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiObjectType, +} 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 + * 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/drift/src/adapter/utils/getAbiEntry.ts b/packages/drift/src/adapter/utils/getAbiEntry.ts new file mode 100644 index 00000000..b34e4c7f --- /dev/null +++ b/packages/drift/src/adapter/utils/getAbiEntry.ts @@ -0,0 +1,40 @@ +import type { Abi, AbiItemType } from "abitype"; +import type { AbiEntry, AbiEntryName } from "src/adapter/types/Abi"; +import { DriftError } from "src/error"; + +/** + * 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; +} + +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/adapter/utils/objectToArray.test.ts b/packages/drift/src/adapter/utils/objectToArray.test.ts new file mode 100644 index 00000000..7f4fa600 --- /dev/null +++ b/packages/drift/src/adapter/utils/objectToArray.test.ts @@ -0,0 +1,62 @@ +import { objectToArray } from "src/adapter/utils/objectToArray"; +import { IERC20 } from "src/utils/testing/IERC20"; +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/drift/src/adapter/utils/objectToArray.ts b/packages/drift/src/adapter/utils/objectToArray.ts new file mode 100644 index 00000000..e129bd6b --- /dev/null +++ b/packages/drift/src/adapter/utils/objectToArray.ts @@ -0,0 +1,89 @@ +import type { Abi, AbiItemType, AbiParameter, AbiParameterKind } from "abitype"; +import type { + AbiArrayType, + AbiEntryName, + AbiObjectType, +} 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 + * 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/drift/src/cache/ClientCache/createClientCache.test.ts b/packages/drift/src/cache/ClientCache/createClientCache.test.ts new file mode 100644 index 00000000..93b8808e --- /dev/null +++ b/packages/drift/src/cache/ClientCache/createClientCache.test.ts @@ -0,0 +1,88 @@ +import { createClientCache } from "src/cache/ClientCache/createClientCache"; +import { IERC20 } from "src/utils/testing/IERC20"; +import { describe, expect, it } from "vitest"; + +describe("createClientCache", () => { + it("Invalidates reads by their read key", () => { + const cache = createClientCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + spender: "0xSpender", + }, + } as const; + const key = cache.readKey(params); + const value = 100n; + + cache.set(key, value); + expect(cache.get(key)).toEqual(value); + + cache.invalidateRead(params); + expect(cache.get(key)).toBeUndefined(); + }); + + it("Invalidates reads matching a partial read key", () => { + const cache = createClientCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + 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(); + }); + + it("Preloads reads by their key", async () => { + const cache = createClientCache(); + const params = { + abi: IERC20.abi, + address: "0xContract", + fn: "allowance", + args: { + owner: "0xOwner", + spender: "0xSpender", + }, + } as const; + const key = cache.readKey(params); + const value = 100n; + + cache.preloadRead({ value, ...params }); + expect(cache.get(key)).toEqual(value); + }); + + it("Preloads events by their key", async () => { + const cache = createClientCache(); + 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; + + cache.preloadEvents({ value, ...params }); + expect(cache.get(key)).toEqual(value); + }); +}); diff --git a/packages/drift/src/cache/ClientCache/createClientCache.ts b/packages/drift/src/cache/ClientCache/createClientCache.ts new file mode 100644 index 00000000..39cd99d5 --- /dev/null +++ b/packages/drift/src/cache/ClientCache/createClientCache.ts @@ -0,0 +1,161 @@ +import isMatch from "lodash.ismatch"; +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 { 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 { + if (isClientCache(cache)) { + return cache; + } + const clientCache: ClientCache = extendInstance< + T, + Omit + >(cache, { + // Chain ID // + + preloadChainId({ value, ...params }) { + return cache.set(clientCache.chainIdKey(params), value); + }, + + chainIdKey({ cacheNamespace } = {}) { + return createSerializableKey([cacheNamespace, "chainId"]); + }, + + // Block // + + preloadBlock({ value, ...params }) { + return cache.set(clientCache.blockKey(params), value); + }, + + blockKey({ cacheNamespace, blockHash, blockNumber, blockTag } = {}) { + return createSerializableKey([ + cacheNamespace, + "block", + { blockHash, blockNumber, blockTag }, + ]); + }, + + // Balance // + + preloadBalance({ value, ...params }) { + return cache.set(clientCache.balanceKey(params), value); + }, + + invalidateBalance(params) { + return cache.delete(clientCache.balanceKey(params)); + }, + + balanceKey({ cacheNamespace, address, blockHash, blockNumber, blockTag }) { + return createSerializableKey([ + cacheNamespace, + "balance", + { address, blockHash, blockNumber, blockTag }, + ]); + }, + + // Transaction // + + preloadTransaction({ value, ...params }) { + return cache.set(clientCache.transactionKey(params), value); + }, + + transactionKey({ hash, cacheNamespace }) { + return createSerializableKey([cacheNamespace, "transaction", { hash }]); + }, + + // Events // + + preloadEvents({ value, ...params }) { + return cache.set(clientCache.eventsKey(params), value); + }, + + eventsKey({ cacheNamespace, address, event, filter, fromBlock, toBlock }) { + return createSerializableKey([ + cacheNamespace, + "events", + { address, event, filter, fromBlock, toBlock }, + ]); + }, + + // Read // + + preloadRead({ value, ...params }) { + return cache.set(clientCache.readKey(params as ReadKeyParams), value); + }, + + invalidateRead(params) { + return cache.delete(clientCache.readKey(params)); + }, + + invalidateReadsMatching(params) { + const matchKey = clientCache.partialReadKey(params); + + for (const [key] of cache.entries) { + if (key === matchKey) { + clientCache.delete(key); + continue; + } + if ( + typeof key === "object" && + typeof matchKey === "object" && + isMatch(key, matchKey) + ) { + clientCache.delete(key); + } + } + }, + + readKey(params) { + return clientCache.partialReadKey(params); + }, + + partialReadKey({ cacheNamespace, address, args, block, fn }) { + return createSerializableKey([ + cacheNamespace, + "read", + { + address, + args, + block, + fn, + }, + ]); + }, + }); + + 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/cache/ClientCache/types.ts b/packages/drift/src/cache/ClientCache/types.ts new file mode 100644 index 00000000..e944a3e3 --- /dev/null +++ b/packages/drift/src/cache/ClientCache/types.ts @@ -0,0 +1,125 @@ +import type { Abi } from "abitype"; +import type { + AdapterGetEventsParams, + AdapterReadParams, +} from "src/adapter/types/Adapter"; +import type { Block } from "src/adapter/types/Block"; +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"; +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: EventsKeyParams & { + value: readonly ContractEvent[]; + }, + ): MaybePromise; + + eventsKey>( + params: EventsKeyParams, + ): SerializableKey; + + // Read // + + preloadRead>( + params: ReadKeyParams & { + value: FunctionReturn; + }, + ): MaybePromise; + + invalidateRead>( + params: ReadKeyParams, + ): MaybePromise; + + invalidateReadsMatching< + TAbi extends Abi, + TFunctionName extends FunctionName, + >(params: Partial>): MaybePromise; + + readKey>( + params: ReadKeyParams, + ): SerializableKey; + + partialReadKey>( + params: Partial>, + ): SerializableKey; +}; + +export interface NameSpaceParam { + /** + * A namespace to distinguish this instance from others in the cache. + */ + // TODO: This needs unit tests + 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 ReadKeyParams< + TAbi extends Abi = Abi, + TFunctionName extends FunctionName = FunctionName, +> = NameSpaceParam & AdapterReadParams; + +export type EventsKeyParams< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = NameSpaceParam & AdapterGetEventsParams; diff --git a/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts new file mode 100644 index 00000000..9f8fb607 --- /dev/null +++ b/packages/drift/src/cache/SimpleCache/createLruSimpleCache.ts @@ -0,0 +1,62 @@ +import stringify from "fast-json-stable-stringify"; +import { LRUCache } from "lru-cache"; +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. + * 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 SerializableKey = SerializableKey, +>(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 { + 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]>); + }, + + 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/drift/src/cache/SimpleCache/types.ts b/packages/drift/src/cache/SimpleCache/types.ts new file mode 100644 index 00000000..1231bad8 --- /dev/null +++ b/packages/drift/src/cache/SimpleCache/types.ts @@ -0,0 +1,56 @@ +import type { SerializableKey } from "src/utils/createSerializableKey"; + +/** + * 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 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. + */ + 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; +} diff --git a/packages/drift/src/client/Contract/Contract.ts b/packages/drift/src/client/Contract/Contract.ts new file mode 100644 index 00000000..8935e5c3 --- /dev/null +++ b/packages/drift/src/client/Contract/Contract.ts @@ -0,0 +1,411 @@ +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 { ContractEvent, 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, + EventsKeyParams, + 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"; + +export interface ContractParams< + TAbi extends Abi = Abi, + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> extends NameSpaceParam { + abi: TAbi; + adapter: TAdapter; + address: Address; + cache?: TCache; +} + +export type Contract< + TAbi extends Abi = Abi, + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> = TAdapter extends ReadWriteAdapter + ? ReadWriteContract + : ReadContract; + +export interface ReadContractParams< + TAbi extends Abi = Abi, + TAdapter extends ReadAdapter = ReadAdapter, + TCache extends SimpleCache = SimpleCache, +> 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 SimpleCache = SimpleCache, +> { + abi: TAbi; + adapter: TAdapter; + address: Address; + cache: ClientCache; + cacheNamespace?: PropertyKey; + + constructor({ + abi, + adapter, + address, + cache = createLruSimpleCache({ max: 500 }) as TCache, + cacheNamespace, + }: ReadContractParams) { + this.abi = abi; + this.adapter = adapter; + this.address = address; + this.cache = createClientCache(cache); + this.cacheNamespace = cacheNamespace; + } + + // Events // + + /** + * Retrieves specified events from the contract. + */ + getEvents = async >( + ...[event, options]: ContractGetEventsArgs + ): 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< + EventsKeyParams, + keyof ReadContractParams + > & { + value: readonly ContractEvent[]; + }, + ): MaybePromise => { + return this.cache.preloadEvents({ + cacheNamespace: this.cacheNamespace, + abi: this.abi, + address: this.address, + ...params, + }); + }; + + eventsKey = >( + ...[event, options]: ContractGetEventsArgs + ): SerializableKey => { + return this.cache.eventsKey({ + cacheNamespace: this.cacheNamespace, + abi: this.abi, + address: this.address, + event, + ...options, + }); + }; + + // read // + + /** + * Reads a specified function from the contract. + */ + read = >( + ...[fn, args, options]: ContractReadArgs + ): Promise> => { + const key = this.readKey( + fn, + args as FunctionArgs, + 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< + ReadKeyParams, + keyof ReadContractParams + > & { + value: FunctionReturn; + }, + ): MaybePromise => { + this.cache.preloadRead({ + cacheNamespace: this.cacheNamespace, + // 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({ + cacheNamespace: this.cacheNamespace, + // 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({ + cacheNamespace: this.cacheNamespace, + 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({ + cacheNamespace: this.cacheNamespace, + // 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({ + cacheNamespace: this.cacheNamespace, + 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 SimpleCache = SimpleCache, +> 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 ContractGetEventsArgs< + TAbi extends Abi = Abi, + TEventName extends EventName = EventName, +> = [event: TEventName, options?: ContractGetEventsOptions]; + +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..f97ff312 --- /dev/null +++ b/packages/drift/src/client/Contract/MockContract.test.ts @@ -0,0 +1,320 @@ +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"; +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: ContractEvent[] = [ + { + 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: ContractEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: ContractEvent[] = [ + { + 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); + }); + + 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", () => { + 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); + }); + + 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", () => { + 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); + }); + + 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", () => { + 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..59a5d649 --- /dev/null +++ b/packages/drift/src/client/Contract/MockContract.ts @@ -0,0 +1,146 @@ +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 { 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, + type ContractParams, + 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< + TAbi extends Abi = Abi, + TAdapter extends MockAdapter = MockAdapter, +> = Omit< + OptionalKeys, "address" | "adapter">, + "cache" +>; + +export class MockContract< + TAbi extends Abi = Abi, + TCache extends ClientCache = ClientCache, + TAdapter extends MockAdapter = MockAdapter, +> extends ReadWriteContract { + constructor({ + abi, + adapter = new MockAdapter() as TAdapter, + address = ZERO_ADDRESS, + cacheNamespace, + }: MockContractParams) { + super({ + abi, + adapter, + address, + cacheNamespace, + }); + } + + reset = () => this.adapter.reset(); + + onGetEvents = >( + ...[event, options]: ContractGetEventsArgs + ) => + this.adapter.onGetEvents({ + abi: this.abi, + address: this.address, + event, + ...options, + }) as SinonStub< + [AdapterGetEventsParams], + Promise[]> + >; + + onRead = >( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractReadOptions, + ) => + this.adapter.onRead({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as SinonStub as SinonStub< + [AdapterReadParams], + Promise> + >; + + onSimulateWrite = < + TFunctionName extends FunctionName, + >( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractWriteOptions, + ) => + this.adapter.onSimulateWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }) as SinonStub as SinonStub< + [AdapterWriteParams, "abi" | "address"], + Promise> + >; + + onEncodeFunctionData = >( + fn?: TFunctionName, + args?: FunctionArgs, + ) => + this.adapter.onEncodeFunctionData({ + abi: this.abi, + fn, + args, + }); + + onDecodeFunctionData = (data?: Bytes) => + this.adapter.onDecodeFunctionData({ + abi: this.abi, + data, + }); + + onGetSignerAddress = () => this.adapter.onGetSignerAddress(); + + onWrite = < + TFunctionName extends FunctionName, + >( + fn: TFunctionName, + args?: FunctionArgs, + options?: ContractWriteOptions, + ) => + this.adapter.onWrite({ + abi: this.abi as Abi, + address: this.address, + fn, + args, + ...options, + }); +} + +const foo = new MockContract({ abi: IERC20.abi }); +foo + .onWrite("transfer", { + to: "0x123", + value: 100n, + }) + .resolves(); diff --git a/packages/drift/src/client/Drift/Drift.ts b/packages/drift/src/client/Drift/Drift.ts new file mode 100644 index 00000000..703259b9 --- /dev/null +++ b/packages/drift/src/client/Drift/Drift.ts @@ -0,0 +1,339 @@ +import type { Abi } from "abitype"; +import type { + Adapter, + AdapterDecodeFunctionDataParams, + AdapterEncodeFunctionDataParams, + AdapterWriteParams, + ReadWriteAdapter, +} from "src/adapter/types/Adapter"; +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 { 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 { + type Contract, + type ContractParams, + ReadContract, + ReadWriteContract, +} from "src/client/Contract/Contract"; +import type { Bytes, TransactionHash } from "src/types"; + +export interface DriftOptions + extends NameSpaceParam { + cache?: TCache; +} + +export class Drift< + TAdapter extends Adapter = Adapter, + TCache extends SimpleCache = SimpleCache, +> { + adapter: TAdapter; + cache: ClientCache; + cacheNamespace?: 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, cacheNamespace }: DriftOptions = {}, + ) { + this.adapter = adapter; + this.cache = createClientCache(cache); + this.cacheNamespace = cacheNamespace; + + // 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((hash) => { + this.adapter.waitForTransaction({ hash }).then(params.onMined); + return hash; + }); + } + + 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, + cacheNamespace = this.cacheNamespace, + }: Omit, "adapter">): Contract< + TAbi, + TAdapter, + TCache + > => { + return ( + this.isReadWrite() + ? new ReadWriteContract({ + abi, + adapter: this.adapter, + address, + cache, + cacheNamespace, + }) + : new ReadContract({ + abi, + adapter: this.adapter, + address, + cache, + cacheNamespace, + }) + ) 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. + */ + 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); + }; + + /** + * 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 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, +> = ReadKeyParams; + +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, +> = EventsKeyParams; + +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 "write" in adapter; +} diff --git a/packages/drift/src/client/Drift/MockDrift.test.ts b/packages/drift/src/client/Drift/MockDrift.test.ts new file mode 100644 index 00000000..91ef5c3a --- /dev/null +++ b/packages/drift/src/client/Drift/MockDrift.test.ts @@ -0,0 +1,633 @@ +import type { Block } from "src/adapter/types/Block"; +import type { ContractEvent } 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", () => { + 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"); + }); + + it("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", () => { + 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: ContractEvent[] = [ + { + 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: ContractEvent[] = [ + { + eventName: "Transfer", + args: { + from: "0x1", + to: "0x1", + value: 123n, + }, + }, + ]; + const events2: ContractEvent[] = [ + { + 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 new file mode 100644 index 00000000..8f91675e --- /dev/null +++ b/packages/drift/src/client/Drift/MockDrift.ts @@ -0,0 +1,100 @@ +import type { Abi } from "abitype"; +import { MockAdapter } from "src/adapter/MockAdapter"; +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, + type MockContractParams, +} from "src/client/Contract/MockContract"; +import { + type DecodeFunctionDataParams, + Drift, + type EncodeFunctionDataParams, + type GetBalanceParams, + type GetBlockParams, + type GetEventsParams, + type GetTransactionParams, + type ReadParams, + type WaitForTransactionParams, + type WriteParams, +} from "src/client/Drift/Drift"; +import type { OptionalKeys } from "src/utils/types"; + +export class MockDrift extends Drift { + constructor() { + super(new MockAdapter()); + } + + reset = () => this.adapter.reset(); + + contract = ( + params: MockContractParams, + ): MockContract => + new MockContract({ + ...params, + adapter: this.adapter, + }); + + onGetChainId = () => this.adapter.onGetChainId(); + + onGetBlock = (params?: Partial) => + this.adapter.onGetBlock(params); + + onGetBalance = (params?: Partial) => + this.adapter.onGetBalance(params); + + onGetTransaction = (params?: Partial) => + this.adapter.onGetTransaction(params); + + onWaitForTransaction = (params?: Partial) => + this.adapter.onWaitForTransaction(params); + + onEncodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >({ + abi, + fn, + args, + }: OptionalKeys, "args">) => + this.adapter.onEncodeFunctionData({ + abi, + fn, + args, + }); + + onDecodeFunctionData = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "data">, + ) => this.adapter.onDecodeFunctionData(params); + + onGetEvents = >( + params: OptionalKeys, "address">, + ) => this.adapter.onGetEvents(params); + + onRead = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args" | "address">, + ) => this.adapter.onRead(params); + + onSimulateWrite = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args" | "address">, + ) => this.adapter.onSimulateWrite(params); + + onWrite = < + TAbi extends Abi, + TFunctionName extends FunctionName, + >( + params: OptionalKeys, "args" | "address">, + ) => this.adapter.onWrite(params); + + onGetSignerAddress = () => this.adapter.onGetSignerAddress(); +} 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/error.ts b/packages/drift/src/error.ts new file mode 100644 index 00000000..ec2bcc0d --- /dev/null +++ b/packages/drift/src/error.ts @@ -0,0 +1,26 @@ +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 = name.startsWith(DriftError.prefix) + ? name + : `${DriftError.prefix}${name}`; + } +} diff --git a/packages/drift/src/exports/index.ts b/packages/drift/src/exports/index.ts new file mode 100644 index 00000000..bef7a8b8 --- /dev/null +++ b/packages/drift/src/exports/index.ts @@ -0,0 +1,145 @@ +// 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 { + ContractEvent, + 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, + Converted, + DeepPartial, + EmptyObject, + FunctionKey, + MaybePromise, + MergeKeys, + OptionalKeys, + Pretty, + ReplaceProps, + 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"; 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/src/types.ts b/packages/drift/src/types.ts new file mode 100644 index 00000000..3435f0dc --- /dev/null +++ b/packages/drift/src/types.ts @@ -0,0 +1,4 @@ +export type HexString = string; +export type Address = HexString; +export type Bytes = HexString; +export type TransactionHash = HexString; diff --git a/packages/drift/src/utils/createSerializableKey.ts b/packages/drift/src/utils/createSerializableKey.ts new file mode 100644 index 00000000..7b8208c4 --- /dev/null +++ b/packages/drift/src/utils/createSerializableKey.ts @@ -0,0 +1,74 @@ +import type { AnyObject } from "src/utils/types"; + +/** + * 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 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 key suitable for consistent referencing within a + * cache. + */ +export function createSerializableKey( + rawKey: string | number | boolean | symbol | AnyObject, +): SerializableKey { + 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 + : createSerializableKey(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] = createSerializableKey(value); + } + } + + return processedObject; + } + + default: + try { + return rawKey.toString(); + } catch (err) { + throw new Error(`Unable to process cache key value: ${String(rawKey)}`); + } + } +} + +/** + * 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/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/utils/testing/IERC20.ts b/packages/drift/src/utils/testing/IERC20.ts new file mode 100644 index 00000000..ba428143 --- /dev/null +++ b/packages/drift/src/utils/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/drift/src/utils/testing/MockStore.ts b/packages/drift/src/utils/testing/MockStore.ts new file mode 100644 index 00000000..dbb59f60 --- /dev/null +++ b/packages/drift/src/utils/testing/MockStore.ts @@ -0,0 +1,52 @@ +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"; + +export class MockStore { + protected mocks = new Map(); + + get({ + method, + key, + create, + }: { + method: FunctionKey; + key?: SerializableKey; + create?: (mock: SinonStub) => SinonStub; + }): SinonStub { + let mockKey: string = String(method); + if (key) { + mockKey += `:${stringify(key)}`; + } + if (this.mocks.has(mockKey)) { + return this.mocks.get(mockKey) as any; + } + let 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 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: + mock.on${method.replace(/^./, (c) => c.toUpperCase())}(...args).resolves(value)`, + ); + this.name = "NotImplementedError"; + } +} diff --git a/packages/drift/src/utils/testing/accounts.ts b/packages/drift/src/utils/testing/accounts.ts new file mode 100644 index 00000000..f04008ee --- /dev/null +++ b/packages/drift/src/utils/testing/accounts.ts @@ -0,0 +1,3 @@ +export const BOB = "0xBob"; +export const ALICE = "0xAlice"; +export const NANCY = "0xNancy"; diff --git a/packages/drift/src/utils/types.ts b/packages/drift/src/utils/types.ts new file mode 100644 index 00000000..1fefae47 --- /dev/null +++ b/packages/drift/src/utils/types.ts @@ -0,0 +1,149 @@ +export type EmptyObject = Record; +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 Pretty = { [K in keyof T]: T[K] } & {}; + +/** + * Replace properties in `T` with properties in `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 = ReplaceProps< + T, + { + [U in K]-?: NonNullable; + } +>; + +/** + * Make all properties in `T` whose keys are in the union `K` optional. + */ +export type OptionalKeys = ReplaceProps< + 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; + +/** + * 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; diff --git a/packages/drift/tsconfig.json b/packages/drift/tsconfig.json new file mode 100644 index 00000000..b0dd9ad7 --- /dev/null +++ b/packages/drift/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/drift/tsup.config.ts b/packages/drift/tsup.config.ts new file mode 100644 index 00000000..891d6c4c --- /dev/null +++ b/packages/drift/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/exports/index.ts", "src/exports/testing.ts"], + format: ["esm"], + sourcemap: true, + dts: true, + clean: true, + minify: true, + shims: true, + cjsInterop: true, +}); diff --git a/packages/drift/vite.config.ts b/packages/drift/vite.config.ts new file mode 100644 index 00000000..3b5ea6b9 --- /dev/null +++ b/packages/drift/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/yarn.lock b/yarn.lock index 0dc27c75..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" @@ -444,7 +569,12 @@ "@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.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" integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== @@ -459,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" @@ -504,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" @@ -545,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" @@ -619,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" @@ -627,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" @@ -668,9 +917,9 @@ 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== + 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" @@ -692,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" @@ -750,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" @@ -759,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" @@ -768,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" @@ -775,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" @@ -785,21 +1089,47 @@ 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== -acorn-walk@^8.1.1, acorn-walk@^8.3.2: +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" + 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" 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== +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" @@ -916,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" @@ -966,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" @@ -1008,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" @@ -1037,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== @@ -1109,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" @@ -1164,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" @@ -1184,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" @@ -1376,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" @@ -1411,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== @@ -1483,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" @@ -1941,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" @@ -1950,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== @@ -2010,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" @@ -2079,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" @@ -2099,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" @@ -2217,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" @@ -2430,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" @@ -2474,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" @@ -2641,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" @@ -2844,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== @@ -2944,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== @@ -3000,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" @@ -3070,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" @@ -3104,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" @@ -3218,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== @@ -3256,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" @@ -3281,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" @@ -3290,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" @@ -3328,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" @@ -3335,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" @@ -3406,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" @@ -3438,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"