GSN и dApps: Роман без газовых препятствий

Представьте себе: вы только что создали невероятное децентрализованное приложение, и оно настолько крутое, что даже ваша бабушка захотела его попробовать. Но стоит только пользователям столкнуться с необходимостью оплаты комиссии и весь UX (User Experience) стремительно скатывается вниз как мячик с горки. Блокчейн обещает светлое будущее, в котором децентрализация, прозрачность и безопасность — наши лучшие друзья, а сам заставляет платить за ежедневные операции. Представьте, если бы вам приходилось платить каждый раз, когда вы ставите лайк в соцсетях или отправляете сообщение в мессенджере. Ужас, правда? А ведь пользователи dApps сталкиваются с чем-то подобным ежедневно.

Но вот, словно принц на белом коне, появляется GSN (Gas Station Network). С его помощью разработчики могут сделать свои приложения gas-less, а пользователи наконец-то смогут забыть о комиссиях, как о страшном сне.

В этой статье мы разберем, что такое GSN, как он работает, и как внедрить его в свои проекты, чтобы порадовать пользователей.

5b09453d9a450eb7cd9b778e86b03648.png

Введение в GSN

Gas Station Network (GSN) — это инфраструктура, позволяющая пользователям взаимодействовать с децентрализованными приложениями без необходимости платить за газ (или позволяя делать это каким‑то другим способом, например, платить токенами ERC-20).

На данный момент есть три основных сценария внедрения оплаты с помощью GSN:

1. Пул оплаты, выделяемый разработчиком. Разработчик может создать пул средств, который будет использоваться для оплаты газ‑комиссий за пользователей. Это позволяет пользователям взаимодействовать с приложением без каких‑либо затрат с их стороны.

2. Оплата с помощью ERC20. Пользователи могут оплачивать газ‑комиссии, используя токены ERC20. Один из контрактов, Paymaster (о котором расскажу далее), обрабатывает эти платежи и покрывает соответствующие газ‑комиссии.

3. Подписки. Разработчики могут внедрить модель подписки, где пользователи платят фиксированную сумму за доступ к dApp на определенный период. В этом случае газ‑комиссии покрываются за счет подписки.

GSN — это не просто магия, а хорошо отлаженная система, состоящая из нескольких ключевых компонентов:

Сервер релея (Relayer)

Представьте себе, что сервер релея — это своего рода почтальон, который принимает вашу мета‑транзакцию и проверяет ее с помощью других компонентов системы. Если все в порядке, этот умный почтальон подписывает транзакцию и отправляет ее в Ethereum, а потом возвращает вам подписанную транзакцию для проверки. Все, что вам остается — это сидеть и наслаждаться процессом, передав транзакцию в надежные цифровые «руки».

Paymaster

Paymaster — это что‑то вроде финансового менеджера, который следит за газовыми расходами. Он обеспечивает логику возврата газовых комиссий. Например, он может принимать только транзакции от пользователей из белого списка или обрабатывать возврат в токенах. Paymaster всегда держит запас ETH в RelayHub, чтобы вовремя покрывать расходы.

Forwarder

Forwarder — это маленький, но очень важный контракт, который можно сравнить с охранником на входе. Он проверяет подлинность мета‑транзакций, удостоверяясь, что подпись и nonce оригинального отправителя верны. Без его разрешения ни одна транзакция не пройдет.

Контракт dApp

Ваш контракт dApp должен унаследоваться от ERC2771Recipient (в старых версиях он назывался BaseRelayRecipient). При этом, для поиска оригинального отправителя, вместо `msg.sender` используется `_msgSender ()`. Таким образом, контракт всегда знает, кто именно отправил запрос.

RelayHub

RelayHub — это настоящий генеральный директор этой корпорации. Именно он является главным координатором всей этой суеты. Он соединяет клиентов, серверы релеев и Paymaster’ов, обеспечивая доверенную среду для всех участников.

Благодаря слаженной работе всех этих компонентов пользователи довольны, разработчики довольны, и всем хорошо. Чтобы использовать GSN, вам не нужно разворачивать все контракты самостоятельно. Например, некоторые из этих смарт‑контрактов уже развернут в основных сетях. Со списком этих контрактов и их адресами в сетях можно ознакомиться на официальном сайте. Минимально нужно развернуть только собственный смарт‑контракт, наследованный от ERC2771Recipient/BaseRelayRecipient, и Paymaster с собственной логикой. Благодаря этому интеграции с GSN проще и быстрее, чем кажется на первый взгляд.

Протестируем локально?

Шаг 1. Запуск локальной сети с помощью Hardhat

Для начала установим hardhat и запустим локальную сеть с помощью npx hardhat node

Шаг 2. Развертка GSN

Для работы с GSN установим пакет @opengsn/cli и выполним соответствующую команду:

yarn add --dev @opengsn/cli
npx gsn start [--workdir ] [-n ]

На выходе получим список адресов в локальной сети, а также url развернутого релея:

...
  RelayHub: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
  RelayRegistrar: 0x5FbDB2315678afecb367f032d93F642f64180aa3
  StakeManager: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
  Penalizer: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
  Forwarder: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
  TestToken (test only): 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
  Paymaster : 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
info:    Relay is active, URL = http://127.0.0.1:57599/ . Press Ctrl-C to abort

В тестовом случае мы не будем самостоятельно использовать контракт Paymaster, а воспользуемся готовым из библиотеки. Данный контракт не рекомендуется использовать в основной сети, т. к. он компенсирует все транзакции без какой‑либо проверки.

Шаг 3. Создание простого смарт‑контракта

Рассмотрим создание на примере самого простого смарт‑контракта:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@opengsn/contracts/src/BaseRelayRecipient.sol";

contract SimpleContract is BaseRelayRecipient {
    constructor(address forwarder ) {
        _setTrustedForwarder(forwarder);
    }

    address public lastCaller;

    function versionRecipient() external override virtual view returns (string memory){
        return "1.0.0";
    }

    function updateCaller() external {
        lastCaller = _msgSender();
    }
}

Для простоты деплоя контракта можно использовать https://remix.ethereum.org, подключив её к локальной сети.

Шаг 4. Взаимодействие с помощью ethers.js

Установим необходимые зависимости:

npm install @opengsn/provider ethers

Для начала заполним конфигурацию в соответствии с адресами, полученными на предыдущих шагах:

const config = {
  relayHubAddress: "0xrelayHubAddress",
  ownerAddress: "0xownerAddress",
  payMaster: "0xpayMaster",
  trustForwarder: "0xtrustForwarder",
  stakeManagerAddress: "0xstakeManagerAddress",
  relayersURL: ['http://127.0.0.1:YOUR-PORT/'],
  gasPriceFactor: 1,
  maxFeePerGas: 1000000000000,
  ethereumNodeUrl: "http://127.0.0.1:8545", //Возьмите из настроек hardhat
  chainId: 1337,
  simpleContractAddress: "0xSimpleContractAddress",
};

// Адреса и приватные ключи, можно взять при запуске локальной сети в hardhat 
const sender = {
  address: 'YOUR-ADDRESS',
  privateKey: 'YOUR-PRIVATE-KEY'
};

Теперь нам необходимо пополнить Paymaster, передав в RelayHub некоторое количество средств. Реализуем это следующим образом:

async function sendEtherToPaymaster() {
  const relayHubAddress = config.relayHubAddress;
  const paymasterAddress = config.payMaster;
  const depositAmount = ethers.utils.parseEther("1.0"); // 1 ETH, например
  
  const relayHubContract = new ethers.Contract(relayHubAddress, RelayHubABI, senderWallet);
  
  const tx = await relayHubContract.depositFor(paymasterAddress, { value: depositAmount });
  await tx.wait();
  const balance = await relayHubContract.balanceOf(paymasterAddress)
  console.log(`Funds deposited to RelayHub for Paymaster: ${paymasterAddress} with balance ${balance}`);
  console.log("______________________________________________________________")  
}

А теперь создадим саму gas‑less транзакцию. Для этого потребуется инициализировать нового провайдера в соответствии с нашим конфигом:

async function gaslessTx() {
  console.log("________________________SERVICE MESSAGES______________________")
  const gsnProvider = await RelayProvider.newProvider({
    provider: provider,
    config: {
      auditorsCount: 0,
      relayHubAddress: config.relayHubAddress,
      stakeManagerAddress: config.managerStakeTokenAddress,
      gasPriceFactor: config.gasPriceFactor,
      maxFeePerGas: config.maxFeePerGas,
      paymasterAddress: config.payMaster,
      forwarderAddress: config.trustForwarder, 
      chainId: config.chainId,
      performDryRunViewRelayCall: false,
      preferredRelays: config.relayersURL
    }
  }).init();

  console.log("______________________________________________________________")
  const ethersGsnProvider = new ethers.providers.Web3Provider(gsnProvider);
  const gsnSigner = ethersGsnProvider.getSigner(sender.address);

  const simpleContract = new ethers.Contract(config.simpleContractAddress, simpleContractABI, gsnSigner);


  try {
    const txResponse = await simpleContract.connect(gsnSigner).updateCaller({
        gasLimit: 1000000 // Пример лимита газа, значение зависит от транзакции
      });
    console.log(`Transaction hash: ${txResponse.hash}`);
    await txResponse.wait();
    console.log('Transaction confirmed');
  } catch (error) {
    console.error('Transaction failed:', error);
  }
}

Таким образом, первая транзакция пополнения (sendEtherToPaymaster) будет вызвана обычным способом с оплатой комиссии, а вторая (gaslessTx) — уже будет использовать gasless метод, при котором сам пользователь не будет оплачивать комиссию.

Полный код:

const { ethers } = require('ethers');
const { RelayProvider } = require('@opengsn/provider');

const RelayHubABI = [
  // Будем использовать минимально необходимые функции
  "function depositFor(address target) public payable",
  "function balanceOf(address target) view returns (uint256)"
];


const simpleContractABI = [
  "function updateCaller() external",
  "function lastCaller() view returns (address)"
];

// Замените адреса!
const config = {
  relayHubAddress: "0xrelayHubAddress",
  ownerAddress: "0xownerAddress",
  payMaster: "0xpayMaster",
  trustForwarder: "0xtrustForwarder",
  stakeManagerAddress: "0xstakeManagerAddress",
  relayersURL: ['http://127.0.0.1:YOUR-PORT/'],
  gasPriceFactor: 1,
  maxFeePerGas: 1000000000000,
  ethereumNodeUrl: "http://127.0.0.1:8545", //Возьмите из настроек hardhat
  chainId: 1337,
  simpleContractAddress: "0xSimpleContractAddress",
};
// Адреса и приватные ключи 
const sender = {
  address: 'YOUR-ADDRESS',
  privateKey: 'YOUR-PRIVATE-KEY'
};

const provider = new ethers.providers.JsonRpcProvider(config.ethereumNodeUrl);
const senderWallet = new ethers.Wallet(sender.privateKey, provider);


//Пополнить Paymaster для оплаты комиссии
async function sendEtherToPaymaster() {
  const relayHubAddress = config.relayHubAddress;
  const paymasterAddress = config.payMaster;
  const depositAmount = ethers.utils.parseEther("1.0"); // 1 ETH, например
  
  const relayHubContract = new ethers.Contract(relayHubAddress, RelayHubABI, senderWallet);

  const tx = await relayHubContract.depositFor(paymasterAddress, { value: depositAmount });
  await tx.wait();
  const balance = await relayHubContract.balanceOf(paymasterAddress)
  console.log(`Funds deposited to RelayHub for Paymaster: ${paymasterAddress} with balance ${balance}`);
  console.log("______________________________________________________________")  
}

async function getBalance(address) {
  console.log("______________________BALANCES_______________________________")
  const etherBalance = await provider.getBalance(address);
  console.log(`BALANCE OF ${address} IS ${ethers.utils.formatEther(etherBalance)} ETH`);
  console.log("______________________________________________________________")
}


async function gaslessTx() {
  console.log("________________________SERVICE MESSAGES______________________")
  const gsnProvider = await RelayProvider.newProvider({
    provider: provider,
    config: {
      auditorsCount: 0,
      relayHubAddress: config.relayHubAddress,
      stakeManagerAddress: config.managerStakeTokenAddress,
      gasPriceFactor: config.gasPriceFactor,
      maxFeePerGas: config.maxFeePerGas,
      paymasterAddress: config.payMaster,
      forwarderAddress: config.trustForwarder, 
      chainId: config.chainId,
      performDryRunViewRelayCall: false,
      preferredRelays: config.relayersURL
    }
  }).init();

  console.log("______________________________________________________________")
  const ethersGsnProvider = new ethers.providers.Web3Provider(gsnProvider);
  const gsnSigner = ethersGsnProvider.getSigner(sender.address);

  const simpleContract = new ethers.Contract(config.simpleContractAddress, simpleContractABI, gsnSigner);


  try {
    const txResponse = await simpleContract.connect(gsnSigner).updateCaller({
        gasLimit: 1000000 // Пример лимита газа, значение зависит от транзакции
      });
    console.log(`Transaction hash: ${txResponse.hash}`);
    await txResponse.wait();
    console.log('Transaction confirmed');
  } catch (error) {
    console.error('Transaction failed:', error);
  }
}

async function main() {
  await getBalance(sender.address);
  await sendEtherToPaymaster()
  await getBalance(sender.address);
  await gaslessTx();
  await getBalance(sender.address);
}

main().catch(console.error);

Использование GSN — это как иметь личного супергероя, который всегда готов заплатить за вас. Так что не бойтесь внедрять эти технологии в свои проекты и делать жизнь пользователей проще и приятнее. Пусть ваши dApps станут популярными и любимыми, а газовые комиссии останутся в прошлом, и надеюсь, ваша бабушка, наконец, попробует ваше крутое приложение и скажет: «А это ничего, мне нравится!»

Всем удачи в разработке, и пусть ваш код всегда компилируется с первого раза!

© Habrahabr.ru