May 16, 2022

Руководство по Connext Network

В данной статье рассмотрим пошаговую инструкцию подключения пользовательского интерфейса или какого-либо серверного сервиса, который подключается к сети Connext, при помощи SDK Quickstart.



Содержание

1.Контракты Quickstart

1.1 Целевой контракт

1.2 Исходный контракт

1.3 Разрешенные

1.4 Дальнейшие действия с целевым контрактом

2. SDK Quickstart

2.1 Кросс-цепная передача

2.2 Установочный проект

2.3 Установка зависимостей

2.4 Импорт

2.5 Создание подписи

2.6 Постройте NxtpSdkConfig

2.7 Создайте SDK

2.8 Постройте xCallArgs

2.9 Одобрение передачи активов

2.10 Отправка

2.11 Кросс-цепь

2.12 Закодируйте calldata

2.13 Постройте xCallArgs

3.Отслеживание xcall

3.1 Запрос подграфов



Контракты Quickstart

В этом быстром запуске будет рассказано о том, как писать смарт-контракты в Solidity, которые взаимодействуют с развернутыми контрактами Connext.

Эти примеры (и другие) можно найти в нашем xApp Starter Kit в разделе src/contract-to-contract-interactions.

An xcallможет быть инициирован из смарт-контракта, который позволяет произвольное выполнение в разных доменах. Это позволяет использовать Connext в качестве базового кросс-цепного слоя, который может быть интегрирован в dApps, превращая их в xApps.

Например, вот интересные варианты использования протокола:

  • Проведите голосование по управлению в одной цепочке и выполните его результат в другой (плюс другие операции DAO).
  • Блокировка и монетный двор или burn-and-mint token bridging
  • Выполните своп и передайте полученные токены по цепочкам
  • Подключение ликвидности DEX по цепочкам в одной бесшовной транзакции
  • Crossch vault zaps и управление стратегией vault
  • Критические операции протокола, такие как репликация/ синхронизация глобальных констант (например, PCV) по цепочкам
  • Включение UniV3 TWAPs в каждую цепочку без введения оракулов
  • Цепно-агностическое управление Ветокеном
  • Взаимодействие между метавселенными


Целевой контракт

Рассмотрим, что у нас есть целевой контракт в целевом домене следующим образом.

Target.sol

import {ERC20} from "@solmate/tokens/ERC20.sol";

contract Target {
  mapping(address => mapping(address => uint256)) public balances;

  function deposit(
    address asset,
    uint256 amount,
    address onBehalfOf
  ) public payable returns (uint256) {
    ERC20 token = ERC20(asset);
    balances[asset][onBehalfOf] += amount;
    token.transferFrom(msg.sender, address(this), amount);

    return balances[asset][onBehalfOf];
  }
}


Надо хотим вызвать depositфункцию через смарт-контракт в другом исходном домене. Любой желающий может вызвать depositфункцию пополнения счета от имени любого адреса. Вот тут-то и вступает в игру термин "неиспользованный".



Исходный контракт

Исходный контракт инициирует кросс-цепное взаимодействие с Connext.

Во-первых, вы импортируете IConnextHandlerинтерфейс и используем ERC20реализацию solmate для обработки токена, который вы намерены deposit.

Source.sol

import {IConnextHandler} from "nxtp/interfaces/IConnextHandler.sol";
import {ERC20} from "@solmate/tokens/ERC20.sol";

Контракт будет принимать адрес ConnextHandler.solв качестве аргумента конструктора.

contract Source {
  IConnextHandler public immutable connext;

  constructor(IConnextHandler _connext) {
    connext = _connext;
  }

Затем мы определяем функцию этого контракта на стороне источникаdeposit, которая требует набора аргументов, необходимых для xcallпоследующего.

  function deposit(
    address to, // the address of the destination contract (UnpermissionedTarget.sol)
    address asset, // the address of the token to bridge and deposit
    uint32 originDomain, // from Kovan (2111)
    uint32 destinationDomain, // to Rinkeby (1111)
    uint256 amount // amount of tokens to deposit
  ) external payable {

В теле depositмы сначала убеждаемся, что пользователь одобрил сумму для отправки по этому контракту. Токены передаются , а затем этот контракт сам должен одобрить передачу ConnextHandler.sol.

    ERC20 token = ERC20(asset);
    require(
      token.allowance(msg.sender, address(this)) >= amount,
      "User must approve amount"
    );

    token.transferFrom(msg.sender, address(this), amount);
    token.approve(address(connext), amount);

Мы создаем calldataего, кодируя функцию целевого контракта depositправильными аргументами. Напомним, что сигнатура целевой функции deposit(address asset, uint256 amount, address onBehalfOf)

Здесь мы указываем, что хотим отправить depositнекоторые amountтокены на тот же адрес кошелька, который инициирует этот вызов, но в целевом домене.

    bytes4 selector = bytes4(keccak256("deposit(address,uint256,address)"));

    bytes memory callData = abi.encodeWithSelector(
      selector,
      asset,
      amount,
      msg.sender
    );

Наконец, мы строим XCallArgsи вызываем xcallконтракт Connext.

    IConnextHandler.CallParams memory callParams = IConnextHandler.CallParams({
      to: to,
      callData: callData,
      originDomain: originDomain,
      destinationDomain: destinationDomain,
      forceSlow: false,
      receiveLocal: false
    });

    IConnextHandler.XCallArgs memory xcallArgs = IConnextHandler.XCallArgs({
      params: callParams,
      transactingAssetId: asset,
      amount: amount,
      relayerFee: 0
    });

    connext.xcall(xcallArgs);
  }
}

Разрешенные

Наиболее интересные кросс-цепочечные варианты использования могут быть реализованы только с помощью разрешенных вызовов в целевом домене. С требованиями к разрешениям разработчик xapp должен тщательно выполнять проверки разрешений. Мы увидим, как это делается в следующем примере.

Допустим, наш целевой контракт содержит функцию, которую должен вызывать только наш исходный контракт. Для простоты мы нацелимся на надуманную функцию:

  function updateValue(uint256 newValue) external onlyExecutor {
    value = newValue;
  }

Вы заметите, что у нас есть пользовательский модификатор onlyExecutorдля этой функции. Это сигнализирует о каком-то требовании разрешения - мы немного углубимся в это. Для разрешенного потока на самом деле проще сначала рассмотреть исходный контракт.

Существует небольшая разница между этим исходным контрактом и контрактом из примера unpermissioned . Единственное важное замечание заключается в том, что мы не отправляем средства с xcallтаким танцем утверждения не требуется.

Внутри updateфункции мы просто создаемcalldata, чтобы соответствовать сигнатуре целевой функции, строим XCallArgsи вызываем xcallее.

Source.sol

import {IConnextHandler} from "nxtp/interfaces/IConnextHandler.sol";

contract Source {
  IConnextHandler public immutable connext;

  constructor(IConnextHandler _connext) {
    connext = _connext;
  }

  function update(
    address to,
    address asset,
    uint32 originDomain,
    uint32 destinationDomain,
    uint256 newValue
  ) external payable {

    bytes4 selector = bytes4(keccak256("updateValue(uint256)"));
    bytes memory callData = abi.encodeWithSelector(selector, newValue);

    IConnextHandler.CallParams memory callParams = IConnextHandler.CallParams({
      to: to,
      callData: callData,
      originDomain: originDomain,
      destinationDomain: destinationDomain,
      forceSlow: false,
      receiveLocal: false
    });

    IConnextHandler.XCallArgs memory xcallArgs = IConnextHandler.XCallArgs({
      params: callParams,
      transactingAssetId: asset,
      amount: 0,
      relayerFee: 0
    });

    connext.xcall(xcallArgs);
  }
}


Дальнейшие действия с целевым контрактом

Теперь целевой контракт должен быть тщательно написан с учетом наших требований к разрешению. Помните, что только наш исходный контракт должен иметь возможность вызывать целевую функцию.

Имея это в виду, давайте погрузимся.

Во-первых, мы импортируем то же IConnextHandlerсамое, что и ERC20раньше. Теперь нам также нужно импортировать IExecutor интерфейс.

Target.sol

import {IExecutor} from "nxtp/interfaces/IExecutor.sol";
import {IConnextHandler} from "nxtp/interfaces/IConnextHandler.sol";
import {ERC20} from "@solmate/tokens/ERC20.sol";

В конструкторе мы передаем адрес исходного контракта и идентификатор домена исходного домена. Мы также передаем адрес контракта Connext Executor, который должен быть единственным разрешенным вызывающим абонентом целевой функции. Это важнейшая информация, которую мы будем проверять, чтобы выполнить наше требование о допуске.

contract Target {
  uint256 public value;
  address public originContract; // The address of Source.sol
  uint32 public originDomain; // The origin Domain ID
  address public executor; // The address of Executor.sol
  
  constructor(
    address _originContract, 
    uint32 _originDomain, 
    address payable _connext
  ) {
    originContract = _originContract;
    originDomain = _originDomain;
    executor = ConnextHandler(_connext).getExecutor(); 
  }

Вот onlyExecutorмодификатор, который мы видели ранее. В нем мы используем IExecutorслужебные функции originSender()и origin()проверяем, что исходящий вызов пришел из ожидаемого исходного контракта и домена. Как уже упоминалось выше, нам также необходимо проверить, что msg.senderэто контракт исполнителя Connext - в противном случае любой вызывающий контракт может просто вернуть контракт и домен, которые мы ожидаем.

  modifier onlyExecutor() {
    require(
      IExecutor(msg.sender).originSender() == originContract && 
      IExecutor(msg.sender).origin() == originDomain && 
      msg.sender == executor,
      "Expected origin contract on origin domain called by Executor"
    );
    _;
  } 

С onlyExecutorмодификатором на месте наша разрешенная функция защищена.

  function updateValue(uint256 newValue) external onlyExecutor {
    value = newValue;
  }
}


SDK Quickstart

Connext SDK позволяет разработчикам взаимодействовать с протоколом Connext в стандарте Node.js или веб-среды. В этом quickstart будет рассказано о том, как построить поверх Connext с помощью TypeScript SDK.


Кросс-цепная передача

В этом быстром запуске мы продемонстрируем, как выполнить xcallперевод средств с кошелька на Kovan на адрес назначения на Rinkeby.


Установочный проект

Если у вас есть существующий проект, вы можете пропустить установку зависимостей.

Создайте папку проекта и инициализируйте пакет.

mkdir node-examples && cd node-examples
yarn init

Мы будем использовать TypeScript / Node.js в этом примере.

yarn add @types/node typescript 
yarn add -D @types/chain
yarn tsc --init

Мы хотим использовать await верхнего уровня, поэтому мы установим параметры компилятора соответствующим образом.

tsconfig.json

{
  "compilerOptions": {
    "outDir": "./dist",
    "target": "es2017",
    "module": "esnext",
    "moduleResolution": "node"
  },
  "exclude": ["node_modules"]
}

И добавьте следующее вpackage.json:

package.json

"type": "module",
"scripts": {
  "build": "tsc",
  "xtransfer": "node dist/xtransfer.js"
}

Создать xtransfer.tsв каталоге проекта, куда мы напишем весь код в этом примере.

mkdir src && touch src/xtransfer.ts


Установка зависимостей

Установите SDK.

yarn add @connext/nxtp-sdk

Кроме того, установитеethers.

yarn add ethers


Импорт

src/xtransfer.ts

import { create, NxtpSdkConfig } from "@connext/nxtp-sdk";
import { ethers } from "ethers";


Создание подписи

Используйте кошелек (т.е. MetaMask) в качестве подписанта.

const privateKey = "<wallet_private_key>";
let signer = new ethers.Wallet(privateKey);

И подключите его к поставщику в цепочке отправки (Infura, Alchemyи т. Д.).

const provider = new ethers.providers.JsonRpcProvider("<kovan_rpc_url>");
signer = signer.connect(provider);
const signerAddress = await signer.getAddress();


Постройте NxtpSdkConfig

Заполните заполнители соответствующими URL-адресами поставщиков.

const nxtpConfig: NxtpSdkConfig = {
  logLevel: "info",
  signerAddress: signerAddress,
  chains: {
    "1111": {
      providers: ["<rinkeby_rpc_url>"],
      assets: [
        {
          name: "TEST",
          address: "0xB7b1d3cC52E658922b2aF00c5729001ceA98142C",
        },
      ],
    },
    "2221": {
      providers: ["<kovan_rpc_url>"],
      assets: [
        {
          name: "TEST",
          address: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F",
        },
      ],
    },
  },
};
Не знаете, откуда взялись эти удостоверения личности? Они относятся к идентификаторам доменов Nomad, которые представляют собой пользовательское сопоставление ID с конкретной средой выполнения (не всегда эквивалентно "цепочке", поэтому у нас есть идентификаторы доменов).


Создайте SDK

Просто позвоните create()с конфигурацией сверху.

const {nxtpSdkBase} = await create(nxtpConfig);


Постройте xCallArgs

Теперь мы построим аргументы, которые будут переданы в xcall.

const callParams = {
  to: "<destination_address>", // the address that should receive the funds
  callData: "0x", // empty calldata for a simple transfer
  originDomain: "2221", // send from Kovan
  destinationDomain: "1111", // to Rinkeby
};

const xCallArgs = {
  params: callParams,
  transactingAssetId: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F", // the Kovan Test Token
  amount: "1000000000000000000", // amount to send (1 TEST)
  relayerFee: "0", // relayers on testnet don't take a fee
};


Одобрение передачи активов

Это необходимо, потому что средства сначала будут отправлены в контракт ConnextHandler перед соединением.

approveIfNeeded() это вспомогательная функция, которая находит правильный адрес контракта и выполняет стандартный поток "увеличение резерва" для актива.

const approveTxReq = await nxtpSdkBase.approveIfNeeded(
  xCallArgs.params.originDomain,
  xCallArgs.transactingAssetId,
  xCallArgs.amount
)
const approveTxReceipt = await signer.sendTransaction(approveTxReq);
const approveResult = await approveTxReceipt.wait();


Отправка

Присылайте xcall свои:

const xcallTxReq = await nxtpSdkBase.xcall(xCallArgs);
xcallTxReq.gasLimit = ethers.BigNumber.from("30000000"); 
const xcallTxReceipt = await signer.sendTransaction(xcallTxReq);
console.log(xcallTxReceipt); // so we can see the transaction hash
const xcallResult = await xcallTxReceipt.wait();

Наконец, выполните следующее, чтобы запустить кросс-цепную передачу!

yarn build
yarn xtransfer


Кросс-цепь

Команда также может отправить произвольный calldataфайл вместе с темxcall, который будет выполнен в домене назначения.

В этом примере мы создадим некоторую calldataцелевую функцию существующего контракта, чтобы избежать развертывания нового контракта. Мы будем стремиться к mintфункции тестового токена ERC20 (TEST), чтобы продемонстрировать это.

Чеканка обычно требует разрешения, но тестовый токен имеет публичную mintфункцию (вызываемую кем угодно!) это мы можем использовать для этого примера. Следовательно, это "unpermissioned" xcallс calldata - ничего лишнего не нужно делать на стороне назначения.


Закодируйте calldata

После создания SDK (шаги 1-6 выше) мы должны создать и закодировать calldata.

Для этого мы просто возьмем ABI контракта тестового токена (здесь мы заботимся только о mintфункции) и закодируем calldataего правильными аргументами.

const contractABI = [
  "function mint(address account, uint256 amount)"
];
const iface = new ethers.utils.Interface(contractABI);

const calldata = iface.encodeFunctionData(
  "mint", 
  [
    "0x6d2A06543D23Cc6523AE5046adD8bb60817E0a94", // address to mint tokens for
    ethers.BigNumber.from("100000000000000000000") // amount to mint (100 TEST)
  ]
)


Постройте xCallArgs

Теперьcalldata, когда все готово, мы поставляем его xCallArgsсвоим.

const callParams = {
  to: "0xB7b1d3cC52E658922b2aF00c5729001ceA98142C", // Rinkeby Test Token - this is the contract we are targeting
  callData: calldata, 
  originDomain: "2221", // send from Kovan
  destinationDomain: "1111", // to Rinkeby
};

const xCallArgs = {
  params: callParams,
  transactingAssetId: "0xB5AabB55385bfBe31D627E2A717a7B189ddA4F8F", // the Kovan Test Token
  amount: "0", // not sending any funds
  relayerFee: "0", // relayers on testnet don't take a fee
};

Обратите внимание, что вы указали amount: "0"выше, поэтому команда не отправляем никаких средств с этимxcall. Поэтому команда может пропустить утверждение и просто отправить транзакцию.

* тот же код*

const xcallTxReq = await nxtpSdkBase.xcall(xCallArgs);
xcallTxReq.gasLimit = ethers.BigNumber.from("30000000"); 
const xcallTxReceipt = await signer.sendTransaction(xcallTxReq);
console.log(xcallTxReceipt); // so we can see the transaction hash
const xcallResult = await xcallTxReceipt.wait();

Добавить новый скрипт вpackage.json:

package.json

"scripts": {
  "xmint": "node dist/xmint.js"
}

Наконец, запустите следующее, чтобы запустить кросс-цепной монетный двор!

yarn build
yarn xmint


Отслеживание xcall

Примечание: Будут создаваться дополнительные утилиты, чтобы упростить отслеживание полного потока xcalls в ближайшем будущем.

Запрос подграфов

На данный момент рекомендуется запрашивать размещенные подграфы в каждой цепочке, чтобы проверить статус транзакции.

Обратите внимание на хэш транзакции, который взаимодействовал с контрактом Connext

Перейдите к размещенному подграфу для цепочки отправки (Kovan)Запрос по хэшу транзакции{
  originTransfers(
    where: {
      transactionHash: "<your_transaction_hash>"
    }
  ) {
    transferId
    caller
    to
    originDomain
    destinationDomain
    transactingAsset
    transactingAmount
    bridgedAsset
    bridgedAmount
    # other fields if desired
  }
}
Перейдите к размещенному подграфу для получения цепочки (Rinkeby).Запрос по идентификатору передачи, полученному из подграфа отправляющей цепочки{
  destinationTransfers(
    where: {
      transferId: "<your_transfer_id>"
    }
  ) {
    to
    originDomain
    destinationDomain
    transactingAsset
    transactingAmount
    localAsset
    localAmount
    executedCaller
    reconciledCaller
    # other fields if desired
  }
}