July 31, 2023

Injective Typescript Building dApps "Смарт Контракт"

Смарт-контракт
В этой небольшой серии статей мы покажем, как легко создать dApp на базе Injective. Существует открытый ресурс, на который каждый может ссылаться и использовать для создания приложений поверх Injective. Есть примеры для Next, Nuxt и Vanilla Js. Для тех, кто хочет начать с нуля, это подходящее место для старта.
В этом примере мы реализуем подключение и взаимодействие с примером смарт-контракта, развернутого на Injective Chain, с помощью модуля injective-ts.
Эта серия включает в себя:
Настройка API-клиентов и среды,
Подключение к цепочке и API индексатора,
Подключение к кошельку пользователя и получение его адреса,
Запрос смарт-контракта (в данном случае получение текущего счета смарт-контракта),
Изменение состояния контракта (в данном случае увеличение счета на 1 или установка его на заданное значение),

Настройка
Сначала настройте желаемый фреймворк пользовательского интерфейса. Более подробно о настройке можно прочитать здесь.
Для начала работы с dex нам необходимо настроить API-клиенты и окружение. Для построения нашего DEX мы будем запрашивать данные как из Injective Chain, так и из Indexer API. В данном примере мы будем использовать существующую среду testnet.
Сначала настроим некоторые классы, необходимые для запроса данных.
Для взаимодействия со смарт-контрактом мы будем использовать ChainGrpcWasmApi из @injectivelabs/sdk-ts. Также нам понадобятся конечные точки сети, которые мы будем использовать (Mainnet или Testnet), которые можно найти в @injectivelabs/networks
Пример:

//filename: services.ts import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts"; import { Network, getNetworkEndpoints } from "@injectivelabs/networks";

export const NETWORK = Network.TestnetK8s; export const ENDPOINTS = getNetworkEndpoints(NETWORK);

export const chainGrpcWasmApi = new ChainGrpcWasmApi(ENDPOINTS.grpc);

Затем нам также необходимо настроить соединение с кошельком, чтобы пользователь мог подключиться к нашему DEX и начать подписывать транзакции. Для этого мы используем наш пакет @injectivelabs/wallet-ts, который позволяет пользователям подключаться к различным провайдерам кошельков и использовать их для подписания транзакций в Injective.
Основная цель @injectivelabs/wallet-ts - предоставить разработчикам возможность реализовать на Injective различные кошельки. Все реализации этих кошельков открывают один и тот же интерфейс ConcreteStrategy, что означает, что пользователи могут просто использовать эти методы без необходимости знать базовые реализации конкретных кошельков, поскольку они абстрагированы от них.
Для начала необходимо создать экземпляр класса WalletStrategy, что дает возможность использовать различные кошельки. Переключить текущий используемый кошелек можно с помощью метода setWallet экземпляра walletStrategy. По умолчанию используется кошелек Metamask.

// filename: wallet.ts import { WalletStrategy } from "@injectivelabs/wallet-ts"; import { Web3Exception } from "@injectivelabs/exceptions";

// These imports are from .env import { CHAIN_ID, ETHEREUM_CHAIN_ID, IS_TESTNET, alchemyRpcEndpoint, alchemyWsRpcEndpoint, } from "/constants";

export const walletStrategy = new WalletStrategy({ chainId: CHAIN_ID, ethereumOptions: { ethereumChainId: ETHEREUM_CHAIN_ID, wsRpcUrl: alchemyWsRpcEndpoint, rpcUrl: alchemyRpcEndpoint, }, });

Если мы не хотим использовать родные кошельки Ethereum, достаточно опустить параметр ethereumOptions в конструкторе WalletStrategy.
Наконец, для выполнения всего потока транзакций (prepare + sign + broadcast) на Injective мы будем использовать класс MsgBroadcaster.

import { Network } from "@injectivelabs/networks"; export const NETWORK = Network.TestnetK8s;

export const msgBroadcastClient = new MsgBroadcaster({ walletStrategy, network: NETWORK, });

Подключение к кошельку пользователя
Поскольку для связи с кошельком пользователя мы используем стратегию WalletStrategy, мы можем использовать ее методы для решения некоторых задач, таких как получение адресов пользователей, подписание/трансляция транзакции и т.д. Для получения более подробной информации о стратегии кошелька можно изучить интерфейс документации и методы, предлагаемые WalletStrategy.
Примечание: Мы можем переключать "активный" кошелек внутри WalletStrategy с помощью метода setWallet.

// filename: WalletConnection.ts import { WalletException, UnspecifiedErrorCode, ErrorType, } from "@injectivelabs/exceptions"; import { Wallet } from "@injectivelabs/wallet-ts"; import { walletStrategy } from "./Wallet.ts";

export const getAddresses = async (wallet: Wallet): Promise<string[]> => { walletStrategy.setWallet(wallet);

const addresses = await walletStrategy.getAddresses();

if (addresses.length === 0) { throw new WalletException( new Error("There are no addresses linked in this wallet."), { code: UnspecifiedErrorCode, type: ErrorType.WalletError, } ); }

if (!addresses.every((address) => !!address)) { throw new WalletException( new Error("There are no addresses linked in this wallet."), { code: UnspecifiedErrorCode, type: ErrorType.WalletError, } ); }

// If we are using Ethereum native wallets the 'addresses' are the hex addresses // If we are using Cosmos native wallets the 'addresses' are bech32 injective addresses, return addresses; };

Запрос
После того как начальная настройка выполнена, давайте посмотрим, как сделать запрос к смарт-контракту для получения текущего количества, используя созданный нами ранее сервис chainGrpcWasmApi и вызов get_count на смарт-контракте.

function getCount() { const response = (await chainGrpcWasmApi.fetchSmartContractState( COUNTER_CONTRACT_ADDRESS, // The address of the contract toBase64({ get_count: {} }) // We need to convert our query to Base64 )) as { data: string };

const { count } = fromBase64(response.data) as { count: number }; // we need to convert the response from Base64

return count; // return the current counter value. }

Получив эти функции (getCount или другие, созданные нами), мы можем вызывать их в любом месте нашего приложения (обычно это централизованные сервисы управления состоянием, такие как Pinia в Nuxt, или Context providers в React и т.д.).

Модификация состояния
Далее мы изменим состояние счетчика. Для этого мы можем отправить сообщения в цепочку с помощью созданного нами ранее Broadcast Client и MsgExecuteContractCompat из @injectivelabs/sdk-ts.
Смарт-контракт, который мы используем для этого примера, имеет 2 метода изменения состояния:
increment
сброс
increment увеличивает счетчик на 1, а reset устанавливает счетчик в заданное значение. Обратите внимание, что функция reset может быть вызвана только в том случае, если вы являетесь создателем смарт-контракта.
Когда мы вызываем эти функции, наш кошелек открывается для подписания сообщения/транзакции и транслирует его.
Сначала рассмотрим, как увеличить счетчик.

// Preparing the message

const msg = MsgExecuteContractCompat.fromJSON({ contractAddress: COUNTER_CONTRACT_ADDRESS, sender: injectiveAddress, msg: { increment: {}, // we pass an empty object if the method doesnt have parameters }, });

// Signing and broadcasting the message

const response = await msgBroadcastClient.broadcast({ msgs: msg, // we can pass multiple messages here using an array. ex: [msg1,msg2] injectiveAddress: injectiveAddress, });

console.log(response);

Теперь рассмотрим пример установки счетчика на определенное значение. Обратите внимание, что в данном смарт-контракте счетчик может быть установлен на определенное значение только создателем смарт-контракта.

// Preparing the message

const msg = MsgExecuteContractCompat.fromJSON({ contractAddress: COUNTER_CONTRACT_ADDRESS, sender: injectiveAddress, msg: { reset: { count: parseInt(number, 10), // we are parseing the number variable here because usualy it comes from an input which always gives a string, and we need to pass a number instead. }, }, });

// Signing and broadcasting the message

const response = await msgBroadcastClient.broadcast({ msgs: msg, injectiveAddress: injectiveAddress, });

console.log(response);

Полный пример
Теперь давайте посмотрим полный пример этого на Vanilla JS (примеры для конкретных фреймворков, таких как Nuxt и Next, можно найти ЗДЕСЬ)

import { ChainGrpcWasmApi, getInjectiveAddress } from "@injectivelabs/sdk-ts"; import { Network, getNetworkEndpoints } from "@injectivelabs/networks"; import { WalletStrategy } from "@injectivelabs/wallet-ts"; import { Web3Exception } from "@injectivelabs/exceptions";

// These imports are from .env import { CHAIN_ID, ETHEREUM_CHAIN_ID, IS_TESTNET, alchemyRpcEndpoint, alchemyWsRpcEndpoint, } from "/constants";

const NETWORK = Network.TestnetK8s; const ENDPOINTS = getNetworkEndpoints(NETWORK);

const chainGrpcWasmApi = new ChainGrpcWasmApi(ENDPOINTS.grpc);

const walletStrategy = new WalletStrategy({ chainId: CHAIN_ID, ethereumOptions: { ethereumChainId: ETHEREUM_CHAIN_ID, wsRpcUrl: alchemyWsRpcEndpoint, rpcUrl: alchemyRpcEndpoint, }, });

export const getAddresses = async (): Promise<string[]> => { const addresses = await walletStrategy.getAddresses();

if (addresses.length === 0) { throw new Web3Exception( new Error("There are no addresses linked in this wallet.") ); }

return addresses; };

const msgBroadcastClient = new MsgBroadcaster({ walletStrategy, network: NETWORK, });

const [address] = await getAddresses(); const injectiveAddress = getInjectiveAddress(getInjectiveAddress);

async function fetchCount() { const response = (await chainGrpcWasmApi.fetchSmartContractState( COUNTER_CONTRACT_ADDRESS, // The address of the contract toBase64({ get_count: {} }) // We need to convert our query to Base64 )) as { data: string };

const { count } = fromBase64(response.data) as { count: number }; // we need to convert the response from Base64

console.log(count) }

async function increment(){ const msg = MsgExecuteContractCompat.fromJSON({ contractAddress: COUNTER_CONTRACT_ADDRESS, sender: injectiveAddress, msg: { increment: {}, }, });

// Signing and broadcasting the message

await msgBroadcastClient.broadcast({ msgs: msg, injectiveAddress: injectiveAddress, }); }

async function main() { await fetchCount() // this will log: {count: 5} await increment() // this opens up your wallet to sign the transaction and broadcast it await fetchCount() // the count now is 6. log: {count: 6} }

main()

Заключительные мысли
Осталось только построить красивый пользовательский интерфейс вокруг бизнес-логики, описанной выше :)