Руководство по Connext Network
В данной статье рассмотрим пошаговую инструкцию подключения пользовательского интерфейса или какого-либо серверного сервиса, который подключается к сети Connext, при помощи SDK Quickstart.
Содержание
1.1 Целевой контракт
1.3 Разрешенные
1.4 Дальнейшие действия с целевым контрактом
2.4 Импорт
2.5 Создание подписи
2.7 Создайте SDK
2.9 Одобрение передачи активов
2.10 Отправка
2.11 Кросс-цепь
2.12 Закодируйте calldata
2.13 Постройте xCallArgs
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 в каждую цепочку без введения оракулов
- Цепно-агностическое управление Ветокеном
- Взаимодействие между метавселенными
Целевой контракт
Рассмотрим, что у нас есть целевой контракт в целевом домене следующим образом.
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
.
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
ее.
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
интерфейс.
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 верхнего уровня, поэтому мы установим параметры компилятора соответствующим образом.
{ "compilerOptions": { "outDir": "./dist", "target": "es2017", "module": "esnext", "moduleResolution": "node" }, "exclude": ["node_modules"] }
И добавьте следующее вpackage.json
:
"type": "module", "scripts": { "build": "tsc", "xtransfer": "node dist/xtransfer.js" }
Создать xtransfer.ts
в каталоге проекта, куда мы напишем весь код в этом примере.
mkdir src && touch src/xtransfer.ts
Установка зависимостей
yarn add @connext/nxtp-sdk
yarn add ethers
Импорт
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();
Отправка
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
:
"scripts": { "xmint": "node dist/xmint.js" }
Наконец, запустите следующее, чтобы запустить кросс-цепной монетный двор!
yarn build yarn xmint
Отслеживание xcall
Примечание: Будут создаваться дополнительные утилиты, чтобы упростить отслеживание полного потока xcall
s в ближайшем будущем.
Запрос подграфов
На данный момент рекомендуется запрашивать размещенные подграфы в каждой цепочке, чтобы проверить статус транзакции.
Обратите внимание на хэш транзакции, который взаимодействовал с контрактом 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 } }