Разработка SDK для взаимодействия со смарт-контрактами
Смарт-контракт написан, задеплоен, верифицирован. Теперь фронтенд-разработчик пытается с ним работать: копирует ABI из etherscan, вручную кодирует параметры через ethers.utils.defaultAbiCoder.encode, ловит unknown error без stacktrace потому что контракт вернул revert без причины. Хороший SDK убирает весь этот friction и делает контракт пригодным к интеграции за часы, а не дни.
Что отличает хороший SDK от обёртки над ethers.js
Типичный «SDK», который пишут быстро — это файл с функциями-обёртками:
// плохо
export async function transfer(to: string, amount: string) {
const contract = new ethers.Contract(ADDRESS, ABI, signer);
return contract.transfer(to, amount);
}
Проблемы: нет типизации параметров, amount — строка или BigInt?, нет обработки ошибок, нет события о завершении, нет поддержки нескольких чейнов.
Нормальный SDK — это слой с чёткими контрактами:
import { type Address, parseUnits, formatUnits } from "viem";
export interface TransferParams {
to: Address;
amount: bigint; // всегда wei, не строка
chainId: SupportedChain;
}
export interface TransferResult {
hash: `0x${string}`;
waitForConfirmation: () => Promise<TransactionReceipt>;
}
export async function transfer(params: TransferParams): Promise<TransferResult>
amount — всегда bigint в wei. Никаких строк. Никакой двусмысленности. TypeScript не даст передать неправильный тип.
Архитектура SDK
Строим на viem для новых проектов. viem заменил ethers.js v5 в большинстве наших проектов: tree-shakeable, строгая типизация, нативные BigInt, значительно меньший bundle size.
sdk/
├── src/
│ ├── contracts/
│ │ ├── abi/ # типизированные ABI (wagmi/viem generate)
│ │ └── addresses.ts # адреса по chainId
│ ├── actions/ # функции-действия (transfer, mint, stake)
│ ├── queries/ # read-only запросы (balanceOf, getAllowance)
│ ├── types/ # общие типы и interfaces
│ ├── errors/ # кастомные ошибки с человеческими сообщениями
│ └── index.ts # public API
├── tests/
└── package.json
Типизированные ABI через codegen. Вместо const ABI = [...] без типов — генерируем через @wagmi/cli:
npx wagmi generate
Это даёт const ABI = [...] as const с полной типизацией. viem использует эти типы для автодополнения аргументов функций и типов возвращаемых значений на уровне TypeScript.
Обработка ошибок — самое важное
Контракт reverts — пользователь видит execution reverted. Это бесполезно. Нужно:
- Декодировать custom error из revert data
- Перевести в человеческое сообщение
- Добавить контекст (какая операция, с какими параметрами)
import { decodeErrorResult, BaseError, ContractFunctionRevertedError } from "viem";
export function parseContractError(error: unknown): SdkError {
if (error instanceof BaseError) {
const revertError = error.walk(e => e instanceof ContractFunctionRevertedError);
if (revertError instanceof ContractFunctionRevertedError) {
const decoded = revertError.data;
switch (decoded?.errorName) {
case "InsufficientBalance":
return new SdkError("INSUFFICIENT_BALANCE",
`Недостаточно средств: требуется ${formatUnits(decoded.args[0], 18)} токенов`);
case "Unauthorized":
return new SdkError("UNAUTHORIZED", "Нет прав для этой операции");
default:
return new SdkError("CONTRACT_ERROR", decoded?.errorName ?? "Неизвестная ошибка контракта");
}
}
}
return new SdkError("UNKNOWN", "Непредвиденная ошибка");
}
Это важнее любой другой части SDK. Разработчики, интегрирующие контракт, тратят 60% времени на отладку ошибок — хороший error handling сокращает это кратно.
Мультичейн поддержка
Контракт на Ethereum и Polygon — не два разных SDK, а один с конфигурацией:
const ADDRESSES: Record<SupportedChain, Address> = {
[mainnet.id]: "0x...",
[polygon.id]: "0x...",
[arbitrum.id]: "0x...",
};
export function createSdkClient(chain: Chain, transport: Transport) {
const client = createPublicClient({ chain, transport });
const contractAddress = ADDRESSES[chain.id];
if (!contractAddress) {
throw new Error(`Chain ${chain.name} not supported`);
}
return {
transfer: (params: TransferParams) => transfer({ ...params, client, contractAddress }),
balanceOf: (address: Address) => balanceOf({ address, client, contractAddress }),
};
}
Тестирование SDK
Unit-тесты через viem testClient + anvil (локальный fork mainnet):
import { createTestClient, http } from "viem";
import { foundry } from "viem/chains";
const testClient = createTestClient({
chain: foundry,
transport: http("http://127.0.0.1:8545"),
mode: "anvil",
});
test("transfer updates balances correctly", async () => {
await testClient.impersonateAccount({ address: WHALE_ADDRESS });
const result = await sdk.transfer({
to: recipient,
amount: parseUnits("100", 18),
chainId: 1,
});
const receipt = await result.waitForConfirmation();
expect(receipt.status).toBe("success");
const balance = await sdk.balanceOf(recipient);
expect(balance).toBe(parseUnits("100", 18));
});
Anvil форкает mainnet со всем state — тестируем против реальных контрактов, не моков.
Документация и публикация
Генерируем документацию через TypeDoc из JSDoc комментариев. Публикуем на npm (приватный реестр для закрытых проектов, публичный для open source).
Версионирование — semver: patch для bagfixes, minor для новых функций без breaking changes, major для изменений API.
Срок разработки базового SDK для одного контракта — 3-4 дня. SDK с мультичейн поддержкой, полным покрытием ошибок, тестами и документацией — 5-7 дней.







