Skip to content

Test without a live network

Outcome: a clean project that tests code built on the exact packed SDK, through its public exports only, with no RPC, subgraph, Pyth, or other live service involved.

You need: Node 24 and the SDK package you were granted (Get access), installed in a clean project with its public peers: viem, react, react-dom, and @tanstack/react-query. Use the exact package you intend to accept.

Pass a generated profile and inject viem clients built on custom transports. The transport has no URL and rejects every JSON-RPC method the test didn’t define, and replacing fetch with a failing function catches any stray HTTP path. The example reads an ERC-20 allowance with a fixed result, builds a revoke transaction without sending it, and signs a Collateral Vault deposit approval that it checks against typed data written out by hand.

import assert from 'node:assert/strict';
import {
AnvilSDK,
buildERC20RevokeTransaction,
buildVaultDepositAuthorization,
getERC20Allowance,
signAuthorization,
testnetProfile,
} from '@anvil/sdk/core';
import {
createPublicClient,
createWalletClient,
custom,
encodeFunctionData,
parseAbi,
recoverTypedDataAddress,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { sepolia } from 'viem/chains';
const account = privateKeyToAccount(
'0x0000000000000000000000000000000000000000000000000000000000000001'
);
const token = '0x1111111111111111111111111111111111111111';
const spender = '0x2222222222222222222222222222222222222222';
const expectedAllowance = 123456789n;
const requests: string[] = [];
globalThis.fetch = async input => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.href
: input.url;
throw new Error(`live network access denied: ${url}`);
};
const transport = custom({
async request({ method }) {
requests.push(method);
if (method === 'eth_chainId') return '0xaa36a7';
if (method === 'eth_call') {
return `0x${expectedAllowance.toString(16).padStart(64, '0')}`;
}
throw new Error(`unexpected JSON-RPC method: ${method}`);
},
});
const publicClient = createPublicClient({ chain: sepolia, transport });
const walletClient = createWalletClient({
account,
chain: sepolia,
transport,
});
const sdk = new AnvilSDK({
accountAddress: account.address,
profile: testnetProfile,
publicClient,
walletClient,
});
const allowance = await getERC20Allowance(sdk, {
erc20Address: token,
owner: account.address,
spender,
});
assert.equal(allowance, expectedAllowance, 'allowance contract mismatch');
const transaction = await buildERC20RevokeTransaction(sdk, {
erc20Address: token,
spender,
});
const expectedData = encodeFunctionData({
abi: parseAbi([
'function approve(address spender, uint256 amount) returns (bool)',
]),
args: [spender, 0n],
functionName: 'approve',
});
const calldataMismatch = 'transaction calldata mismatch';
assert.equal(transaction.to?.toLowerCase(), token.toLowerCase());
assert.equal(
transaction.from?.toLowerCase(),
account.address.toLowerCase(),
'transaction signer mismatch'
);
assert.equal(transaction.data, expectedData, calldataMismatch);
assert.equal(transaction.chainId, BigInt(sepolia.id));
assert.ok(requests.includes('eth_call'));
assert.ok(requests.includes('eth_chainId'));
// Sign a vault deposit approval for a collateralizable contract, then
// check it against typed data written out by hand: the domain and struct
// CollateralVault verifies in `depositFromAccount`.
const collateralizable = '0x3333333333333333333333333333333333333333';
const depositAmount = 5_000_000n;
const authorization = await buildVaultDepositAuthorization(sdk, {
collateralizableAddress: collateralizable,
tokenAddress: token,
depositAmount,
nonce: 0n, // explicit, so no `nonces` read is needed offline
});
const signature = await signAuthorization(sdk, authorization);
const depositApprovalSigner = await recoverTypedDataAddress({
domain: {
name: 'CollateralVault',
version: '1',
chainId: sepolia.id,
verifyingContract: testnetProfile.contracts.collateralVault.address,
},
types: {
CollateralizableDepositApproval: [
{ name: 'collateralizableAddress', type: 'address' },
{ name: 'tokenAddress', type: 'address' },
{ name: 'depositAmount', type: 'uint256' },
{ name: 'approverNonce', type: 'uint256' },
],
},
primaryType: 'CollateralizableDepositApproval',
message: {
collateralizableAddress: collateralizable,
tokenAddress: token,
depositAmount,
approverNonce: 0n,
},
signature,
});
assert.equal(
depositApprovalSigner,
account.address,
'deposit approval signer mismatch'
);
console.log(
JSON.stringify({
account: account.address,
allowance: allowance.toString(),
chainId: transaction.chainId.toString(),
data: transaction.data,
depositApprovalSigner,
from: transaction.from,
requests,
to: transaction.to,
})
);

Save the example as offline-sdk-test.mts (the extension makes it unambiguously ESM) in the clean project. To block fetch-based network access before Node evaluates the SDK’s static imports, also create deny-live-network.mjs:

globalThis.fetch = async input => {
throw new Error(`live network access denied: ${String(input)}`);
};

Preload that guard, then run the typed example with Node’s built-in type stripping:

node --import ./deny-live-network.mjs offline-sdk-test.mts

Success exits with status 0 and prints one JSON object containing "allowance":"123456789", "chainId":"11155111", and both expected JSON-RPC method names in requests. Any unexpected network path or changed public result throws and exits non-zero.

The account comes from a fixed test-only key, testnetProfile supplies the public generated environment, and the custom transport decides the chain and contract-read results. The assertions check the read result, signer, target, chain id, and calldata. Keep real keys and live URLs out of this fixture.

What this test covers, and what it leaves to you

  • This test proves the package’s public contract. It shows that the installed package’s reads and transaction building behave as documented, with every network path under your control.
  • Your acceptance runs cover the live pieces. A deployment, the subgraph, the oracle, a wallet connector, and transaction submission each need their own acceptance run against the same package artifact.