
Security News
pnpm 11.5 Adds Support for Recognizing npm Staged Publishes
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.
@delvtech/drift
Advanced tools
Effortless Ethereum Development Across Web3 Libraries
Write cached Ethereum smart contract interactions once with Drift and run them anywhere. Seamlessly support multiple web3 libraries like viem, web3.js, and ethers—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.
Building on Ethereum often means dealing with:
Install Drift:
npm install @delvtech/drift
Optional: To use Drift with a specific web3 library, install the corresponding adapter:
# Install one (optional)
npm install @delvtech/drift-viem
npm install @delvtech/drift-web3
npm install @delvtech/drift-ethers
npm install @delvtech/drift-ethers-v5
[!TIP]
Drift can be used without an adapter, however adapters reuse clients from their corresponding web3 library, which can improve performance depending on their configuration. For example, the
publicClientfrom Viem can automatically batch requests via MultiCall.
import { createDrift } from "@delvtech/drift";
const drift = createDrift({
rpcUrl: "[YOUR_RPC_URL]",
});
Viem adapter example:
import { createDrift } from "@delvtech/drift";
import { viemAdapter } from "@delvtech/drift-viem";
import { createPublicClient, createWalletClient, http } from "viem";
const publicClient = createPublicClient({
transport: http(),
});
// optionally create a wallet client
const walletClient = createWalletClient({
transport: http(),
});
const drift = createDrift({
adapter: viemAdapter({ publicClient, walletClient }),
});
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",
},
});
If Drift was initialized with a signer, you can perform write operations:
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",
},
});
},
});
const txHash = await drift.deploy({
abi: ERC20.abi,
bytecode: ERC20.bytecode,
args: {
decimals_: 18,
initialSupply: 100_000_000n * 10n ** 18n, // 100M
},
});
// Wait for the receipt to get the contract address
const receipt = await drift.waitForTransaction({ hash: txHash });
if (receipt?.status === "success" && receipt?.contractAddress) {
const totalSupply = await drift.read({
abi: ERC20.abi,
address: receipt.contractAddress,
fn: "totalSupply",
});
// => 100000000000000000000000000n
}
Create contract instances to write your options once and get a streamlined, type-safe API to re-use across your application.
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.cache.invalidateRead("balanceOf", { account: "0xReceiverAddress" });
},
},
);
Let's build a simple library agnostic SDK with ReadVault and ReadWriteVault
clients using Drift.
In your core SDK package, define the ReadVault and ReadWriteVault clients
using Drift's ReadContract and ReadWriteContract abstractions.
// sdk-core/src/VaultClient.ts
import {
type Address,
type Drift,
type EventLog,
type Hash,
type ReadContract,
type ReadWriteAdapter,
type ReadWriteContract,
createDrift,
} from "@delvtech/drift";
import { erc4626Abi } from "./abis/Erc4626";
type VaultAbi = typeof erc4626Abi;
/** A Read-Only Vault client */
export class ReadVault {
contract: ReadContract<VaultAbi>;
constructor(address: Address, drift: Drift = createDrift()) {
this.contract = drift.contract({
abi: erc4626Abi,
address,
});
}
// Make read calls with internal caching
getBalance(account: Address): Promise<bigint> {
return this.contract.read("balanceOf", { account });
}
getDecimals(): Promise<number> {
return this.contract.read("decimals");
}
// Fetch events with internal caching
getDeposits(account?: Address): Promise<EventLog<VaultAbi, "Deposit">[]> {
return this.contract.getEvents("Deposit", {
filter: {
sender: account,
},
});
}
}
export class ReadWriteVault extends ReadVault {
declare contract: ReadWriteContract<VaultAbi>;
constructor(
address: Address,
drift: Drift<ReadWriteAdapter> = createDrift(),
) {
super(address, drift);
}
// Make a deposit
deposit(amount: bigint, account: Address): Promise<Hash> {
return this.contract.write(
"deposit",
{
assets: amount,
receiver: account,
},
{
// Optionally wait for the transaction to be mined and invalidate cache
onMined: (receipt) => {
if (receipt?.status === "success") {
this.contract.cache.invalidateRead("balanceOf", { account });
}
},
},
);
}
}
Using an adapter, you can integrate Drift with your chosen web3 library. Here's
an example using viem:
import { createDrift } 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 = createDrift({
adapter: 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");
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.
[!IMPORTANT]
Drift's testing mocks have a peer dependency on Sinon.JS. Make sure to install it before using the mocks.
npm install --save-dev sinon
Suppose you have a method, getAccountValue, in your ReadVault client that
gets 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:
// sdk-core/src/ReadVault.test.ts
import { createMockDrift } 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 = createMockDrift();
const mockContract = mockDrift.contract({
abi: vaultAbi,
address: "0xVaultAddress",
});
// Stub the vault's return values using `on*` methods
mockContract.onRead("balanceOf", { account: "0xUserAddress" }).resolves(
BigInt(100e18), // User has 100 vault shares
);
mockContract.onRead("convertToAssets", { shares: BigInt(100e18) }).resolves(
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));
});
In traditional setups, you might rely on data-fetching libraries like React Query. However, to prevent redundant network requests, each contract call would need:
useBalanceOf, useTokenSymbol).Composing multiple calls becomes cumbersome, as you have to manage each hook's result separately.
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.
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.
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.
// 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 });
Delete cached data to ensure it's re-fetched using invalidate* and clear*
methods.
// Invalidate the cache for a specific read
contract.cache.invalidateRead("balanceOf", { account });
// Invalidate all reads matching partial arguments
contract.cache.invalidateReadsMatching("balanceOf");
// Invalidate all reads associated with the contract
contract.cache.clearReads();
// Let it all go...
contract.cache.clear();
Add static data such as immutables from token lists using preload* methods to
avoid network requests without changing how the data is accessed.
const drift = createDrift(/* ... */);
const contract = drift.contract({
abi: erc20Abi,
// ...
});
// Preloading read data
contract.cache.preloadRead({ fn: "symbol", value: "DAI" });
// Preloading event data
contract.cache.preloadEvents({
event: "Transfer",
value: [],
// ...
});
Drift clients will automatically check the cache before fetching new data, but
direct access to the cached data is available via get* methods.
// Get a cached read return
const cachedBalance = await contract.cache.getRead("balanceOf", { account });
// Get a cached transaction receipt
const cachedReceipt = await drift.cache.getTransactionReceipt({ hash })
The invalidate*, clear*, preload*, and get* methods are available on
both the Drift.cache and Contract.cache instances.
[!IMPORTANT]
Manipulating cache data affects all clients that share the same cache. Since Drift passes its own cache to the contracts it creates via
Drift.contract(), they'll already be preloaded with theDriftinstance's cache and any cache operations performed on the contract cache will also affect theDriftcache.
Drift is designed to be extensible. You can build additional abstractions or utilities on top of it to suit your project's requirements.
Extend support to new web3 libraries or custom providers by implementing the
Adapter interface. Drift Clients, including
Drift, extend the prototype of the Adapter they're provided,
inheriting all of it's properties and methods, extending some, and adding some
of their own. See the DefaultAdapter for an example
implementation.
import { DefaultAdapter, createDrift } from "@delvtech/drift";
class CustomAdapter extends DefaultAdapter {
override async getChainId(): Promise<number> {
// Custom implementation...
}
}
const drift = await createDrift({
adapter: new CustomAdapter({
rpcUrl: process.env.RPC_URL,
}),
});
Implement a custom Store to manage
caching in a way that suits your application. The default store is an in-memory
LRU cache, but you can create a custom store that uses TTL, localStorage,
IndexedDB,
QueryClient, or
any other storage mechanism, sync or async.
import { createDrift } from "@delvtech/drift";
const drift = createDrift({
store: new Map<string, unknown>(),
});
Add custom logic by intercepting and modifying client methods with hooks. Each
client method (including custom adapter methods) has type-safe before:<method>
and after:<method> hooks that can be used to inspect and modify the arguments
or results.
// Simulate writes before sending transactions
drift.hooks.on("before:write", async ({ args: [params] }) => {
await drift.simulateWrite(params);
});
// Run middleware on reads
drift.hooks.on("before:read", async ({ args: [params], resolve }) => {
const cachedValue = await drift.cache.getRead(params);
resolve(readMiddleware({ drift, params, cachedValue }));
});
// Run middleware to transform the result of calls
drift.hooks.on("after:call", ({ args: [params], result, setResult }) => {
const transformedResult = callResultMiddleware({ drift, params, result });
setResult(transformedResult);
});
Got ideas or found a bug? Check the Contributing Guide to get started.
Drift is open-source software licensed under the Apache 2.0.
FAQs
Effortless ethereum development across Web3 libraries
The npm package @delvtech/drift receives a total of 1,282 weekly downloads. As such, @delvtech/drift popularity was classified as popular.
We found that @delvtech/drift demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
pnpm 11.5 now recognizes npm staged publish approvals in release metadata, preventing those releases from being mistaken for lower-trust package publishes.

Security News
Federal audit finds NIST lacked a plan to clear the NVD backlog, wasted funds on duplicate work, and delayed use of CISA data.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.