Injective Typescript Building dApps "DEX"
DEX
В этой небольшой серии статей мы покажем, как легко построить DEX на базе Injective. Существует открытый исходный код, на который каждый может ссылаться и использовать для построения поверх Injective. Для тех, кто хочет начать с нуля, это подходящее место для старта.
В серию занятий войдут:
Настройка API-клиентов и среды,
Подключение к Chain и Indexer API,
Подключение к кошельку пользователя и получение его адреса,
Получение данных о спотовых и производных рынках и их ордерах,
Размещение рыночных ордеров на спотовом и срочном рынках,
Просмотр всех позиций для инъекционного адреса.
Настройка
Сначала настройте желаемый фреймворк пользовательского интерфейса. Более подробную информацию о настройке можно найти здесь.
Для начала работы с dex нам необходимо настроить API-клиенты и окружение. Для построения нашего DEX мы будем запрашивать данные как из Injective Chain, так и из Indexer API. В данном примере мы будем использовать существующую среду testnet.
Сначала настроим некоторые классы, необходимые для запроса данных.
// filename: Services.ts import { ChainGrpcBankApi, IndexerGrpcSpotApi, IndexerGrpcDerivativesApi, } from '@injectivelabs/sdk-ts' import { getNetworkEndpoints, Network } from '@injectivelabs/networks'
// Getting the pre-defined endpoints for the Testnet environment // (using TestnetK8s here because we want to use the Kubernetes infra) export const NETWORK = Network.TestnetK8s export const ENDPOINTS = getNetworkEndpoints(NETWORK)
export const chainBankApi = new ChainGrpcBankApi(ENDPOINTS.grpc) export const indexerSpotApi = new IndexerGrpcSpotApi(ENDPOINTS.indexer) export const indexerDerivativesApi = new IndexerGrpcDerivativesApi(ENDPOINTS.indexer)
export const indexerSpotStream = new IndexerGrpcDerivativeStream(ENDPOINTS.indexer) export const indexerDerivativeStream = new IndexerGrpcDerivativeStream(ENDPOINTS.indexer)
Затем нам также необходимо настроить соединение с кошельком, чтобы пользователь мог подключиться к нашему DEX и начать подписывать транзакции. Для этого мы используем наш пакет @injectivelabs/wallet-ts, который позволяет пользователям подключаться к различным провайдерам кошельков и использовать их для подписания транзакций на Injective.
// filename: Wallet.ts import { WalletStrategy, Wallet } from '@injectivelabs/wallet-ts' import { ChainId, EthereumChainId } from '@injectivelabs/ts-types'
const chainId = ChainId.Testnet // The Injective Chain chainId const ethereumChainId = EthereumChainId.Goerli // The Ethereum Chain ID
export const alchemyRpcEndpoint = `https://eth-goerli.alchemyapi.io/v2/${process.env.APP_ALCHEMY_GOERLI_KEY}`
export const walletStrategy = new WalletStrategy({ chainId: CHAIN_ID, ethereumOptions: { rpcUrl: alchemyRpcEndpoint, ethereumChainId: ETHEREUM_CHAIN_ID, }, })
Если мы не хотим использовать родные кошельки Ethereum, достаточно опустить параметр ethereumOptions в конструкторе WalletStrategy.
Наконец, для выполнения всего потока транзакций (prepare + sign + broadcast) на Injective мы будем использовать класс MsgBroadcaster.
// filename: MsgBroadcaster.ts import { MsgBroadcaster } from '@injectivelabs/wallet-ts' import { walletStrategy } from './Wallet.ts' import { NETWORK } from './Services.ts'
export const msgBroadcaster = 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 }
Запрос
После того как начальная настройка выполнена, рассмотрим, как запрашивать (и транслировать) рынки из IndexerAPI, а также балансы пользователей из цепочки напрямую.
// filename: Query.ts import { getDefaultSubaccountId, OrderbookWithSequence } from '@injectivelabs/sdk-ts' import { chainBankApi, indexerSpotApi, indexerSpotStream, indexerDerivativesApi indexerDerivativesStream, } from './Services.ts'
export const fetchDerivativeMarkets = async () => { return await indexerDerivativesApi.fetchMarkets() }
export const fetchPositions = async (injectiveAddress: string) => { const subaccountId = getDefaultSubaccountId(injectiveAddress)
return await indexerDerivativesApi.fetchPositions({ subaccountId }) }
export const fetchSpotMarkets = async () => { return await indexerSpotsApi.fetchMarkets() }
export const fetchBankBalances = async (injectiveAddress: string) => { return await chainBankApi.fetchBalances(injectiveAddress) }
export const streamDerivativeMarketOrderbook = async ( marketId: string, ) => { const streamOrderbookUpdates = indexerDerivativesStream.streamDerivativeOrderbookUpdate.bind(indexerDerivativesStream) const callback = (orderbookUpdate) => { console.log(orderbookUpdate) }
streamOrderbookUpdates({ marketIds, callback }) }
export const streamSpotMarketOrderbook = async ( marketId: string, ) => { const streamOrderbookUpdates = indexerSpotsStream.streamSpotOrderbookUpdate.bind(indexerSpotsStream) const callback = (orderbookUpdate) => { console.log(orderbookUpdate) }
streamOrderbookUpdates({ marketIds, callback }) }
Получив эти функции, мы можем вызывать их в любом месте нашего приложения (обычно это централизованные сервисы управления состоянием, такие как Pinia в Nuxt, или Context providers в React и т.д.).
Транзакции
Наконец, давайте сделаем несколько транзакций. В данном примере мы собираемся:
Отправить активы с одного адреса на другой,
Создать спотовый лимитный ордер,
выставить рыночный ордер по деривативу.
// filename: Transactions.ts import { BigNumberInWei } from '@injectivelabs/utils' import { MsgSend, MsgCreateSpotLimitOrder, spotPriceToChainPriceToFixed, MsgCreateDerivativeMarketOrder, spotQuantityToChainQuantityToFixed } from '@injectivelabs/sdk-ts'
// used to send assets from one address to another export const makeMsgSend = ({ sender: string, recipient: string, amount: string, // human readable amount denom: string }) => { const amount = { denom, amount: new BigNumberInBase(amount).toWei(/** denom's decimals */) }
return MsgSend.fromJSON({ amount, srcInjectiveAddress: sender, dstInjectiveAddress: recipient, }) }
// used to create a spot limit order export const makeMsgCreateSpotLimitOrder = ({ price, // human readable number quantity, // human readable number orderType, // OrderType enum injectiveAddress, }) => { const subaccountId = getDefaultSubaccountId(injectiveAddress) const market = { marketId: '0x...', baseDecimals: 18, quoteDecimals: 6, minPriceTickSize: '', /* fetched from the chain */ minQuantityTickSize: '', /* fetched from the chain */ priceTensMultiplier: '', /** can be fetched from getDerivativeMarketTensMultiplier */ quantityTensMultiplier: '', /** can be fetched from getDerivativeMarketTensMultiplier */ }
return MsgCreateSpotLimitOrder.fromJSON({ subaccountId, injectiveAddress, orderType: orderType, price: spotPriceToChainPriceToFixed({ value: price, tensMultiplier: market.priceTensMultiplier, baseDecimals: market.baseDecimals, quoteDecimals: market.quoteDecimals }), quantity: spotQuantityToChainQuantityToFixed({ value: quantity, tensMultiplier: market.quantityTensMultiplier, baseDecimals: market.baseDecimals }), marketId: market.marketId, feeRecipient: injectiveAddress, }) }
// used to create a derivative market order export const makeMsgCreateDerivativeMarketOrder = ({ price, // human readable number margin, // human readable number quantity, // human readable number orderType, // OrderType enum injectiveAddress, }) => { const subaccountId = getDefaultSubaccountId(injectiveAddress) const market = { marketId: '0x...', baseDecimals: 18, quoteDecimals: 6, minPriceTickSize: '', /* fetched from the chain */ minQuantityTickSize: '', /* fetched from the chain */ priceTensMultiplier: '', /** can be fetched from getDerivativeMarketTensMultiplier */ quantityTensMultiplier: '', /** can be fetched from getDerivativeMarketTensMultiplier */ }
return MsgCreateDerivativeMarketOrder.fromJSON( orderType: orderPrice, triggerPrice: '0', injectiveAddress, price: derivativePriceToChainPriceToFixed({ value: order.price, tensMultiplier: market.priceTensMultiplier, quoteDecimals: market.quoteDecimals }), quantity: derivativeQuantityToChainQuantityToFixed({ value: order.quantity, tensMultiplier: market.quantityTensMultiplier, }), margin: derivativeMarginToChainMarginToFixed({ value: order.margin, quoteDecimals: market.quoteDecimals, tensMultiplier: priceTensMultiplier, }), marketId: market.marketId, feeRecipient: feeRecipient, subaccountId: subaccountI })
После того как мы получили сообщения, можно использовать клиент msgBroadcaster для трансляции этих транзакций:
const response = await msgBroadcaster({ msgs: /** the message here */, injectiveAddress: signersInjectiveAddress, })
Заключительные мысли
Осталось только построить красивый пользовательский интерфейс вокруг бизнес-логики, описанной выше :)