Provider против Signer: читать или писать

Главное различие ethers.js, которое путает новичков: Provider только читает, Signer ещё и подписывает.

Provider — соединение для чтения сети. Signer — провайдер плюс способность подписывать: он привязан к конкретному аккаунту и может отправлять транзакции.

Это самое практичное различие во всём ethers.js. Перепутаете — получите ошибку «cannot estimate gas» или «unknown account» в самый ответственный момент. Разберёмся раз и навсегда.

Provider: глаза, но не руки

Provider умеет всё, что не требует ключа: номер блока, баланс, чтение view-функций, история событий, оценка газа. Он анонимен — не знает «от чьего имени» действовать, потому что для чтения это и не нужно.

Signer: руки с ключом

Signer представляет конкретный аккаунт (адрес) и умеет подписывать: отправлять транзакции, подписывать сообщения. В браузерном dApp Signer получают из кошелька — это аккаунт пользователя. Кошелёк не отдаёт ключ, но даёт объект Signer, который при подписи покажет окно подтверждения.

import { BrowserProvider } from "ethers";

const provider = new BrowserProvider(window.ethereum);
// getSigner() запросит у кошелька активный аккаунт
const signer = await provider.getSigner();

const me = await signer.getAddress();   // адрес пользователя
console.log("я:", me);
// signer теперь можно передать в контракт для записи

Правило выбора

ЗадачаЧто нужно
Прочитать баланс / view-функциюProvider
Подписаться на событияProvider (лучше WebSocket)
Отправить транзакциюSigner
Подписать сообщение (вход)Signer

Как работает под капотом

Контракт в ethers.js создаётся с «runner» — это либо Provider, либо Signer. От него зависит, что контракт умеет. Если контракт подключён к Provider, у него доступны только view/pure-методы (чтение). Если к Signer — доступны и методы записи, потому что есть кому подписать. Поэтому частый паттерн: один экземпляр контракта на чтение (через Provider) и второй на запись (через Signer), либо переподключение методом contract.connect(signer).

Частые ошибки

  • Звать функцию записи на контракте с Provider. Будет ошибка — некому подписать. Нужен Signer.
  • Создавать Signer до подключения кошелька. getSigner() запрашивает аккаунт; без подключения вернёт ошибку.
  • Думать, что Signer «знает приватный ключ». В браузере Signer — это прокси к кошельку; ключ остаётся в MetaMask.

Итоги

  • Provider читает, Signer читает и подписывает (привязан к аккаунту).
  • Для записи и подписи сообщений нужен Signer из кошелька.
  • Контракт с Provider умеет только читать; с Signer — ещё и писать.
Проверьте себя
1. Что умеет Signer, чего не умеет Provider?
AЧитать баланс
BПодписывать транзакции от имени аккаунта
CУзнавать номер блока
DСлушать события
2. Что произойдёт, если вызвать функцию записи на контракте, подключённом к Provider?
AТранзакция уйдёт анонимно
BОшибка: некому подписать
CКонтракт выберет аккаунт сам
DЗапись станет бесплатной
3. Откуда в браузерном dApp обычно берут Signer?
AГенерируют из приватного ключа в коде
BЧерез provider.getSigner() из кошелька пользователя
CИз публичного RPC
DИз localStorage