В предыдущей статье мы обсудили расширенные возможности применения подобной установки. Мы также завершили часть проекта, посвященную Sanity, и с этого момента речь пойдет только о Chainlink и смарт-контрактах. Если вам показалось, что в предыдущей статье не хватало кода, надеюсь, вы не будете жаловаться, что эта статья слишком насыщена им XD. В этой статье мы:
— напишем наш внешний адаптер для подключения к Sanity
— Запустим наш локальный узел Chainlink
— Добавим наш внешний адаптер к нашему узлу
В нашем случае мы определим задание Chainlink Job для разбора нескольких аргументов вместо разбора одного (задания с многопеременным выводом). В этой статье мы не будем вызывать его из смарт-контракта. Я оставил эту часть для статьи 3 — полной статьи о смарт-контракте. Начнем…
Написание адаптера
Возможно, это немного разочарует, но внешние адаптеры Chainlink по своей сути являются просто серверами NodeJs/Express с некоторой пользовательской логикой. Если у вас есть опыт разработки бэкенда, эта часть будет для вас легкой. Если нет, то не волнуйтесь, так как мы будем действовать проще.
Хотя мы можем написать наш внешний адаптер полностью с нуля, я предпочитаю использовать шаблон, который Chainlink предоставляет уже некоторое время. Это не тот Mono Repo, о котором я упоминал в предыдущей статье. Перейдите по этой ссылке.
По этой ссылке находится загруженный Express Server, который сэкономит нам много времени. Даже если вы будете создавать его с нуля, основная идея будет заключаться в размещении где-нибудь сервера NodeJS, а затем использовании его через раскрытие REST API и помещение его в узел Chainlink.
Здесь мы будем использовать вышеупомянутое репо для экономии времени.
Клонируйте репозиторий на вашей рабочей станции, а затем перейдите в каталог клонированного проекта и откройте его в вашем любимом редакторе кода. Запустите npm install
для установки всех зависимостей. Откройте файл index.js
. По умолчанию он должен указывать на API цены криптовалюты. Это то, что обсуждается в документации по Chainlink. Вам НЕ нужно удалять весь файл. Мы изменим только те части, которые нам нужны.
Запустите npm i ethers @sanity/client dotenv
из терминала внутри проекта. Это установит Ethers.Js, Dotenv и Sanity Client. Последний будет необходим нам для запроса БД, которую мы создали в предыдущей статье. На данном этапе я предполагаю, что у вас уже есть API-ключ типа view-only для БД Sanity.
Я рекомендую использовать .env
для передачи его через переменные окружения, и здесь на помощь приходит Dotenv. Если вы не знаете как, просто создайте файл .env
в корне вашего проекта, а затем вставьте туда ID проекта и API ключ из Sanity. Это должно помочь.
Вернитесь к файлу index.js
. Здесь должна быть переменная customParams
. Мы всегда будем передавать все необходимые нам параметры через тело запроса, используя команду POST запрос. Даже узел Chainlink делает POST-запрос, потому что в большинстве случаев есть некоторые поля, которые вы хотите извлечь и выборочно получить в смарт-контракте. Эта переменная используется для этой цели.
Кроме endpoint: false
удалите все остальное и добавьте wallet: ["wallet", "walletAddr", "addr"],
перед endpoint: false
. По сути, это означает, что мы будем искать аргумент под названием «wallet» в запросе, отправленном адаптеру. Однако вместо «wallet» аргумент может быть отправлен как «walletAddr» или «addr». После этого добавления customParams
должен выглядеть примерно так:
const customParams = {
walletAddr: ["wallet", "address", "walletAddress"],
endpoint: false
}
Метод createRequest()
— это место, где запрос к БД Sanity будет сделан через Sanity Client, обработан и затем отправлен обратно смарт-контракту через узел Chainlink. Наш createRequest
будет выглядеть примерно так:
const createRequest = (input, callback) => {
// The Validator helps you validate the Chainlink request data
const validator = new Validator(callback, input, customParams)
const jobRunID = validator.validated.id;
let walletAddr = validator.validated.data.walletAddr;
walletAddr = utils.getAddress(walletAddr);
const client = sanityClient({
projectId: process.env.PROJECT_ID ,
dataset: 'production',
apiVersion: '2021-04-27',
token: process.env.API_TOKEN,
useCdn: false,
});
const query = `*[_type == "user" && walletAddress == $walletAddr] {isVerified, signupDate, walletAddress}`
const params = {walletAddr};
//id of the document to fetch
client.fetch(query, params)
.then((user) => {
const {isVerified, signupDate, walletAddress} = user[0];
const joined = Date.parse(signupDate+"T00:00:00")/1000;
const qualified = Date.now()/1000 - joined > 20 * 24 * 60 * 60;
const response = { data: { isVerified, qualified, walletAddress } };
callback(200, Requester.success(jobRunID, response))
})
.catch(error => {
callback(500, Requester.errored(jobRunID, error))
})
}
После извлечения jobRunID
мы извлекаем параметр wallet
, содержащий адрес кошелька инвокера.
Обратите внимание, что внутри смарт-контракта адрес кошелька инвокера будет получен по msg.sender
. Перед отправкой нам нужно будет преобразовать его в строку. Как это сделать, будет показано в контракте в следующей статье. Однако, при таком преобразовании мы потеряем достоверность контрольной суммы адреса кошелька.. Здесь на помощь приходит метод utils.getAddress()
из ethers
, который дает нам правильный форматированный адрес. Мы будем использовать его для запроса к Sanity DB.
Далее мы инициализируем наш Sanity Client. Мы передаем ID нашего проекта (через переменную окружения), набор данных (который в вашем случае будет production, если вы не настроили его), версию API (пусть это будет текущая дата), ключ API (через ключ окружения). useCdn
— необязательный флаг на случай, если вы захотите создать бэкенд и фронтенд для вашей Sanity DB.
query
и params
— это то, что мы будем передавать клиенту sanity при выполнении запроса. Первый — это запрос на основе GraphQL (называется GROQ). Действительно удобный и интуитивно понятный в использовании. Второй — это параметры, которые мы используем внутри него. Поскольку walletAddress
для поиска будет меняться в каждом запросе, мы помещаем его в константу params
в качестве свойства, а затем ссылаемся на соответствующий ключ в запросе GROQ. Внутри фигурных скобок мы передаем поля, которые хотим получить обратно.
Затем мы передаем этот запрос вместе с параметрами, чтобы получить документы, соответствующие заданным критериям. Это асинхронный процесс, и мы используем структуру Promise для получения ответа и его форматирования. Я решил не возвращать дату в исходном формате и вместо этого сделал из нее булевский флаг qualified
, который оценивается как true
, если с момента регистрации прошло 20 дней.
Обратите внимание, что константа response
, которая будет отправлена обратно, имеет определенный формат — все поля, которые мы хотим отправить обратно, находятся внутри поля data
. Это не выбор. Это обязательно. Мы увидим, как задание, которое мы определим в нашем узле цепочки, будет ссылаться на это поле data
.
Наконец, внутри функции callback()
мы передаем HTTP код успеха 200 и функцию Requester.success()
, которая содержит jobRunId
и response
. Мы передаем код ошибки HTTP сервера вместе с jobRunId
и ошибкой в Requester.errorred()
, если Promise будет отклонен или мы встретим какую-либо ошибку в нашем коде.
На этом работа с внешним адаптером завершена. Если вы посмотрите дальше в файл index.js
, то обнаружите, что там есть много обработчиков утилит, определенных для таких сценариев, как размещение адаптера на GCP Cloud Function или AWS Lambda. Я никогда не пробовал размещать его на Heroku, но, полагаю, это может стать темой любой будущей статьи. Как я уже говорил, этот внешний адаптер представляет собой Express Server, который будет работать как Webhook, если вы разместите его на любой из облачных платформ. Мы просто запустим его локально. Запуск npm start
или npm run start
запускает сервер на порту по умолчанию 8080
.
Запуск нашего локального узла Chainlink
Хотя я с удовольствием пройдусь по шагам, как запустить узел Chainlink локально, это сделает эту статью очень длинной. Поэтому я хотел бы обратиться к тем, кто обладает гораздо большим опытом, чем я. Посмотрите видео ниже, транслируемое во время Chainlink Hackathon 2022, которое дает полное представление о том, как запустить узел Chainlink вместе с экземпляром Postgres SQL в контейнерах Docker. Если вы не знали, Chainlink использует Postgres SQL под капотом.
В приведенном выше видео Code along развертывается сначала докер-контейнер Postgres SQL с использованием учетных данных, указанных в файлах окружения, а затем поверх него — узел Chainlink в докер-контейнере. Доступ к узлу можно получить по адресу localhost:6688
. У вас уже должны быть полномочия для входа в систему, если вы смотрели видео выше.
Добавление внешнего адаптера к узлу Chainlink
Chainlink выполняет задания через «JOBS». Задания выполняются на узле всякий раз, когда смарт-контракт размещает запрос через Oracle (вы уже должны были развернуть Oracle-контракт, как показано в видео). На самом деле вы можете следовать всему, что показано в видео выше, и у вас будет задание, состоящее из основных адаптеров, предоставляемых Chainlink. На самом деле, я бы рекомендовал вам следовать за кодом, потому что это даст вам опыт в определении Job.
Внешние адаптеры необходимы, когда вам нужно выполнить пользовательскую логику. Чаще всего это происходит, когда вам нужен аутентифицированный доступ или нужный вам материал находится за брандмауэром. Здесь мы создаем аутентифицированный доступ к БД Sanity. Основной поток заключается в создании моста, который будет ссылаться на URL webhook/webserver внешнего сервера адаптера (обратите внимание, что он уже должен быть запущен или размещен). Затем создается задание для передачи данных на внешний адаптер через этот мост.
Если вы знакомы с арифметикой указателей в C, считайте, что сервер внешнего адаптера — это данные, хранящиеся в Heap, мост — указатель на эти данные, а задание — метод или процедура, которая обращается к этим данным через указатель.
Создание моста
Чтобы создать мост, войдите в свой узел Chainlink по адресу localhost:6688
и введите учетные данные, которые вы определили при настройке среды узла Chainlink. Затем перейдите на вкладку Мосты, как показано ниже.
Здесь я называю мост sanity-bridge
. Ссылка на мост будет ссылкой на размещенный сервер. Если вы размещаете свой сервер на GCP Cloud Functions или AWS Lambda, вставьте полученную ссылку webhook. Если вы запускаете его локально, как я, то http://localhost:8080
сделает свою работу.
На рисунке выше я определил его, используя частный IP, потому что localhost разрешается в 127.0.0.1 узлом Chainlink, который не является моим IP-адресом localhost. Это привело к отказу в подключении в моем случае. Если вы столкнулись с такой проблемой, когда при доступе задания Chainlink к localhost возникает ошибка отказа в подключении, вы можете заменить его на свой IP-адрес localhost, как я сделал здесь.
Нажатие на кнопку Create Bridge завершает этот шаг. Далее мы создадим задание, которое будет ссылаться на этот мост.
Создание задания из моста
Нажмите на Jobs и затем нажмите на New Job. Вы попадете на страницу, где будет показан редактор, в котором вам нужно определить задание через TOML. Ранее это был JSON (и вы все еще найдете его примеры в market.link). Ниже приведена спецификация TOML для нашего задания.
type = "directrequest"
schemaVersion = 1
name = "(Get > x2 Bool)"
maxTaskDuration = "0s"
contractAddress = "0xf8b64a4273F13C2521ACC715d3022b8Bd31e1bE8"
minContractPaymentLinkJuels = 0
minIncomingConfirmations = 0
observationSource = """
decode_log [type="ethabidecodelog"
abi="OracleRequest(bytes32 indexed specId, address requester, bytes32 requestId, uint256 payment, address callbackAddr, bytes4 callbackFunctionId, uint256 cancelExpiration, uint256 dataVersion, bytes data)"
data="$(jobRun.logData)"
topics="$(jobRun.logTopics)"]
decode_cbor [type="cborparse" data="$(decode_log.data)"]
fetch [type="bridge" name="sanity-bridge" requestData="{\"id\": $(jobSpec.externalJobID), \"data\": { \"wallet\": $(decode_cbor.wallet)}}"]
parseVerified [type="jsonparse" path="data,isVerified" data="$(fetch)"]
parseQualified [type="jsonparse" path="data,qualified" data="$(fetch)"]
parseWallet [type="jsonparse" path="data,walletAddress" data="$(fetch)"]
encode_data [type="ethabiencode" abi="(bytes32 requestId, address walletAddress, bool isVerified, bool qualified)" data="{ \"requestId\": $(decode_log.requestId), \"isVerified\": $(parseVerified), \"qualified\": $(parseQualified), \"walletAddress\": $(parseWallet)}"]
encode_tx [type="ethabiencode"
abi="fulfillOracleRequest(bytes32 requestId, uint256 payment, address callbackAddress, bytes4 callbackFunctionId, uint256 expiration, bytes calldata data)"
data="{\"requestId\": $(decode_log.requestId), \"payment\": $(decode_log.payment), \"callbackAddress\": $(decode_log.callbackAddr), \"callbackFunctionId\": $(decode_log.callbackFunctionId), \"expiration\": $(decode_log.cancelExpiration), \"data\": $(encode_data)}"
]
submit_tx [type="ethtx" to="0xf8b64a4273F13C2521ACC715d3022b8Bd31e1bE8" data="$(encode_tx)"]
decode_log -> decode_cbor -> fetch -> parseVerified -> parseQualified -> parseWallet -> encode_data -> encode_tx -> submit_tx
"""
На первый взгляд спецификация TOML может показаться непосильной, но как только вы начнете ее читать. Ниже приведено описание:
-
type
определяет типы запросов для обработки. Это может бытьwebhook
или в нашем случаеdirectrequest
. Это означает, что смарт-контракт будет напрямую запрашивать задание на выполнение через внешний идентификатор задания. - Пока вы можете пропустить
schemaVersion
, полеname
достаточно интуитивно понятно.maxTaskDuration
— время, отведенное на выполнение задачи. -
contractAddress
— это НЕ адрес контракта, по которому размещается запрос. Это адрес контракта Oracle. Помните, что в приведенном выше видео он развернут, поэтому у вас он уже должен быть. -
minContractPaymentLinkJuels
определяет количество LINK-токенов, необходимых для выполнения Задания. Вообще, если вы собираетесь развернуть его в mainnet, я бы рекомендовал вам не держать его равным 0, как мы сделали здесь. Это предотвратит спам задания, поскольку смарт-контракт и, следовательно, пользователь должны будут заплатить реальными токенами LINK. -
minIncomingConfirmations
— количество подтверждений, необходимых для транзакции этого задания. -
observationSource
— суть задания. Здесь мы определяем задачи, которые должно выполнить задание. Сначала мы определяем задачи, а затем, в самом конце, определяем порядок, в котором эти задачи должны выполняться. Здесь мы имеем:a. Заданиеdecode_log
декодирует данные, отправленные смарт-контрактом на узел Chainlink. Она делает это из события, которое здесь названоOracleRequest
. В качестве данных, передаваемых заданию, берутся данные журнала события.b.decode_cbor
— CBOR — это формат представления данных, подобный формату JSON. Обратите внимание, что в этом заданииdecode_log.data
ссылается на предыдущее задание вместе с параметром задания. По сути, она будет разбирать данные для дальнейшего использования.c.fetch
— Здесьfetch
является задачей типаbridge
. Эта задача относится к мосту, который мы определили ранее. Задачи типаbridge
должны передавать имя моста в параметреname
вместе с данными, которые нужно отправить этому мосту в параметреrequestData
, как мы сделали здесь. d.parseVerified
,parseQualified
иparseWallet
анализируют ответ JSON, который задание получает от внешнего адаптера через промежуточный мост. Затем они извлекают определенные свойства из этого JSON, как указано в параметреpath
. Данные JSON в эту задачу передаются из задачиfetch
ранее. e. Задачаencode_data
— это место, где начинается часть возврата данных обратно в смарт-контракт. Здесь обратите внимание на параметрabi
. Значение этого параметра должно совпадать со списком параметров функции выполнения (функция, выбранная которой передается в запрос Chainlink из контракта и выполняется после завершения задания). Параметрdata
содержит данные, которые получит смарт-контракт. Обратите внимание, что поля имеют то же имя, что и значение параметраabi
, и ссылаются на результат предыдущих заданийparseVerified
,parseQualified
иparseWallet
по имени соответствующих заданий. Таким образом, наше задание цепочки является заданием с несколькими переменными выводами. Это означает, что у этого задания будет более одного выхода, и они будут меняться в зависимости от запроса.f.encode_tx
вызывает функцию на Oracle Contract, содержащую то же имя и параметр функции, что и значение параметра заданияabi
.g. Наконец,submit_tx
отправляет транзакцию по адресу, указанному в параметре «to». Это должен быть адрес oracle contract.h. Ниже вы должны заметить, что указана последовательность, разделенная->
. Вы угадали правильно! Это последовательность, в которой мы определяем, в какой последовательности будут выполняться задания в Job.
Если вы обратитесь к изображению выше, то заметите, что справа есть панель, показывающая ту же последовательность сверху вниз, которую мы указали в последнем разделе спецификации Job TOML. Это обеспечивает визуальную проверку того, что задания будут выполняться так, как вы задумали. Наведя курсор на любое задание на этой панели, вы увидите его детали.
Нажатие на кнопку Create Job создает задание. Это даст вам внешний идентификатор задания, который мы будем использовать в нашем смарт-контракте в следующей статье. А теперь несколько советов, прежде чем вы перейдете к следующей статье.
— Локальный узел Chainlink должен быть профинансирован токенами ETH и LINK. Это можно сделать, нажав на иконку Chainlink Operator. Это покажет вам что-то вроде экрана ниже. Отправьте немного ETH и LINK (оба токена в тестовой сети, я не буду нести ответственность, если вы отправите на адрес реальные мейннет ETH и LINK, а затем потеряете их.). Вы можете получить некоторое количество ETH и LINK Testnet из крана Chainlink.
— Вам нужно выполнить setFulfillmentPermission()
на вашем контракте oracle. Передайте адрес локального узла Chainlink в поле _node
и поле _allowed
как true
. Как только эта транзакция будет подтверждена, Oracle Contract разрешит нашему узлу Chainlink Node посылать на него запросы. Это защита от рассылки спама.
— Если после этой статьи вы сделаете перерыв, запустив docker compose down
и закрыв всю установку, вам нужно будет снова определить Job и Bridge. Вы получите новый адрес узла Chainlink, и вам нужно будет финансировать этот адрес и установить разрешение от вашего контракта oracle для этого контракта заново.
Вот и все на этом, друзья!
После этого остается только разработать смарт-контракт, чем мы и займемся в следующей статье цикла. Честно говоря, мне кажется, что для одной статьи было достаточно, а для новичка это очень много информации.
Прежде чем завершить эту статью, я бы рекомендовал вам присоединиться к Chainlink Discord. Там много хороших людей и ресурсов. Кроме того, вы будете получать новости обо всех хакатонах, в которых участвует Chainlink.
Я также хотел бы поблагодарить Мэтта из Block-Farms.io, который помог мне в этом деле. Block Farms предлагает операторов узлов, где вы можете разместить свои рабочие места Chainlink, как эти, по привлекательной цене, так что вы можете проверить их, если вы хотите разместить свои рабочие места для вас контрактов.
На этом спасибо, что прочитали эту статью. Надеюсь, она показалась вам интересной и вы почерпнули из нее что-то новое. До встречи в следующей части этой серии!