详解 Injective 使用指南: 发行与交易
2025-06-03 18:51
TinTinLand
2025-06-03 18:51
订阅此专栏
收藏此文章
原文作者:Shew,仙壤  编译来源:https://hackmd.io/@wongssh/r1qubHCA1e
1.png

概述

Injective 是一条专注于金融应用的区块链。相比于其他 EVM 系区块链,Injective 提供了模块等功能,我们可以直接通过模块实现代币发行和代币交易等功能,所以在构建应用时,开发者并不需要编写大量的链上合约,而只需要调用多个模块就可以完成一个金融应用的构建。

本文将使用 Injective 提供的模块实现以下几个功能:

  1. 代币发行,使用 Tokenfactory 模块发行原生代币

  2. 代币交易,使用 helix 进行市价代币交易和使用原生的 Exchange 模块完成订单簿交易


2.png

基础知识

在介绍具体的功能开发前,我们会首先介绍一些本文所需要的基础内容。由于笔者不擅长前端开发,所以本文编写的 Typescript 只是脚本。同时,笔者更喜欢 bun 作为 Typescript 的运行时,本文内所有的项目都是使用 bun 作为运行环境开发。

我们可以使用 mkdir injective-example 创建一个新的文件夹然后调用 bun init 将文件夹初始化为一个 bun 项目,最后使用如下命令按照 injective sdk 即可:

bun i @injectivelabs/sdk-ts

至此,我们就完成了基础的 Injective 项目的构建,后文我们所有的代码都在 injective-example 文件夹内进行编写。由于 Injective 不同于 EVM 系区块链,我们将介绍 Injective 的基础交易内容。

在 Injective 内部,交易的基本内容如下 ( 为了文章的简洁性,我们忽略了一些内容 ):

{ "id": "", "blockNumber": 69976290, "blockTimestamp": "2025-04-02 14:11:09.359 +0000 UTC", "hash":"0x9dabb0f73b545bb31b017e197b10dc90f5281325cb9afaa49ff9781570c7a89b", "txType": "injective", "data":"EnMKNi9pbmplY3RpdmUudG9rZW5mYWN0b3J5LnYxYmV0YTEuTXNnQ3JlYXRlRGVub21SZXNwb25zZRI5CjdmYWN0b3J5L2luajF4bHN3dGc3OGozdDBoYzB2ODZmbXN4M2syMmo3amVkbjU3NzNlYy90ZXN0","events": [], "messages": [ { "type": "/injective.tokenfactory.v1beta1.MsgCreateDenom", "message": { "sender":"inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec", "subdenom": "test", "name": "Test Token", "symbol": "TST","decimals": 0, "allow_admin_burn": false } } ], "errorLog": "", "claimIds": [] }

这笔交易的完整内容可以在此链接找到。我们所吸引关注的就是messages 内部的内容,这部分的内容其实就是我们交易的具体内容,此处的 type 代表交易调用的模块,我们可以在 文档 内找到一系列 msg 的定义,下图展示了 MsgCreateDenom 在文档中的定义。

{ "id": "", "blockNumber": 69976290, "blockTimestamp": "2025-04-02 14:11:09.359 +0000 UTC", "hash":"0x9dabb0f73b545bb31b017e197b10dc90f5281325cb9afaa49ff9781570c7a89b", "txType": "injective", "data":"EnMKNi9pbmplY3RpdmUudG9rZW5mYWN0b3J5LnYxYmV0YTEuTXNnQ3JlYXRlRGVub21SZXNwb25zZRI5CjdmYWN0b3J5L2luajF4bHN3dGc3OGozdDBoYzB2ODZmbXN4M2syMmo3amVkbjU3NzNlYy90ZXN0","events": [], "messages": [ { "type": "/injective.tokenfactory.v1beta1.MsgCreateDenom", "message": { "sender":"inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec", "subdenom": "test", "name": "Test Token", "symbol": "TST","decimals": 0, "allow_admin_burn": false } } ], "errorLog": "", "claimIds": [] }

在 Injective 内,当我们需要某些功能时,我们第一时间是检查 Injective 内是否存在模块提供相关功能,然后查找相关的格式定义。在具体编程中,我们往往会看到如下格式的代码:

const subdenom "test"const createMsg = MsgCreateDenom.fromJSON({ subdenom, symbol"TST", name"Test Token", sender: injectiveAddress, }); const txHash await new MsgBroadcasterWithPk({ privateKey, network:Network.Testnet, }).broadcast({ msgs: createMsg, });

上述代码中第一部分其实就是完成了 MsgCreateDenom 的构建,然后使用自己的私钥对 Msg 进行签名然后将其广播到 Injective 测试网内部。我们会在后文多次使用此格式的代码进行发起交易操作。


3.png

账户模型

在编写脚本前,我们需要了解 Injective 的账户模型。Injective 的账户使用了和以太坊一致的 secp256k1 曲线。这也是为什么 Injective 内部很多应用都可以使用 MetaMask 等 EVM 系钱包发起交易的原因。但是不同于以太坊直接使用公钥哈希作为地址,Injective 是用来 Bech32 作为地址编码方案。在 Bech32系统内,产生的地址可以包含一个固定的前缀,目前 injective 使用了 inj 作为地址前缀,所以我们可以看到如下所示的地址:

inj1spp27p48vpkyvclwu3g62ley0ulgvd3l44hq5y

我们可以使用以下代码进行私钥生成,并打印出私钥及其对应的地址:

import { PrivateKey } from "@injectivelabs/sdk-ts"const privateKey = PrivateKey.generate(); console.log(`Private Key: ${privateKey.privateKey.toPrivateKeyHex()}`); const publicKey = privateKey.privateKey.toPublicKey(); constinjectiveAddress = publicKey.toAddress().addressconsole.log(`INJ Address: ${injectiveAddress}`);

行上述代码后,读者可以在控制台获得如下输出:

Private Key: 0xf2716ac69b3f766c8a632afd23474c7e71cfbed7ad5f3c8be455d50f54777ad0 INJ Address: inj16emen7s7j09q50fk5zdx28pwn68lwzdc9zhe0j

此处使用的 generate 会随机生成私钥,所以假如读者输出结果与我不一致也是正常的。接下来,为了保证输出的一致性,我们会将私钥直接写入文件中,代码如下:

import { PublicKey } from "@injectivelabs/sdk-ts"export const privateKey ="0xf2716ac69b3f766c8a632afd23474c7e71cfbed7ad5f3c8be455d50f54777ad0"export const publicKey =PublicKey.fromPrivateKeyHex(privateKey); export const injectiveAddress = publicKey.toAddress().address;console.log(`INJ Address: ${injectiveAddress}`);

我们将上述文件放置在 config.ts 内部,在后续代码编程中,我们会直接引入 config.ts 内部的 privateKey 来进行编程。当我们获得地址后,进行后续开发的第一步就是前往 Injective 的水龙头获取资金,Injective 有以下两个水龙头:

  1. Google Web3 Faucet

  2. Injective Faucet

其中,Google 提供的水龙头会一次性发送 10 INJ 代币,且速度较快。而官方水龙头会提供 INJ 和 USDT 测试代币,但速度较慢,建议读者首先在 Google 水龙头内获取 INJ 代币进行后续开发,而官方水龙头代币也可以获取,但需要等待一段时间才可以到账。

接下来,我们进行第一项工作,获取账户余额。不同于其他 EVM 系链中,我们需要调用合约获取账户余额。在 Injective 内存在 Bank 模块存储有用户的代币,包括用户自己使用 TokenFactory 发行的代币。我们可以使用:

import { ChainGrpcBankApi } from "@injectivelabs/sdk-ts"import { getNetworkEndpoints, Network } from"@injectivelabs/networks"const endpoints = getNetworkEndpoints(Network.Testnet); const chainGrpcBankApi = newChainGrpcBankApi(endpoints.grpc); const injectiveAddress = "inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec"; constbankBalances = await chainGrpcBankApi.fetchBalances(injectiveAddress); console.log(bankBalances);

此处使用的 ChainGrpcBankApi 是用来调用 Bank 模块的 RPC 接口。我们可以看到如下输出:

{ balances: [ { denom: "factory/inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec/test", amount: "1000000002000000000",}, { denom: "inj", amount: "19122720000000000000", }, { denom:"peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", amount: "9983999999", } ], pagination: { total: 3, next:"", }, }

此处的 peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5 是 USDT 代币,而 factory/inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec/test 是之前发行的测试代币。假如读者希望进一步了解 Bank 模块的内容,可以阅读此文档。

除了这些通过与模块使用 RPC 交互获取信息的方法,injective 还提供了 indexer 接口,该模块往往可以提供更加复杂的数据。以获取用户资产为例,indexer 接口下存在 IndexerGrpcAccountPortfolioApi 模块,调用该模块,我们可以获得用户更加详细的资产配置。代码如下:

import { IndexerGrpcAccountPortfolioApi } from "@injectivelabs/sdk-ts"import { getNetworkEndpoints, Network }from "@injectivelabs/networks"const endpoints = getNetworkEndpoints(Network.Testnet); constindexerGrpcAccountApi = new IndexerGrpcAccountPortfolioApi( endpoints.indexer, ); const injectiveAddress ="inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec"const portfolio = awaitindexerGrpcAccountApi.fetchAccountPortfolioBalances(injectiveAddress); console.log(JSON.stringify(portfolio, null,2));

上述内容的输出结果如下:

{ "accountAddress": "inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec", "bankBalancesList": [ { "denom":"factory/inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec/test", "amount": "1000000002000000000" }, { "denom": "inj","amount": "19122720000000000000" }, { "denom": "peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5","amount": "9983999999" } ], "subaccountsList": [ { "subaccountId":"0x37e0e5a3c79456fbe1ec3e93b81a3652a5e965b3000000000000000000000001", "denom":"peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", "deposit": { "totalBalance": "11000001","availableBalance": "11000001" } } ] }

相比于 Bank 模块只检索了主账户的资产,Portfolio 模块对用户的子账户 (subaccounts) 也进行了检索。子账户也是 injective 内部的一个特殊设计,子账户内的资金可以用于与 exchange 模块交互进行交易。子账户的 subaccountId 是子账户的唯一标识,subaccountId 由两部分构成,其中第一部分是主账户的以太坊地址。我们在上文已经介绍过 injective 账户也是是用 secp256k1 公钥派生获得的,然后进行 bech32 编码获得的,所以我们可以通过 injective 地址计算对应的以太坊地址。在具体开发过程中,我们一般直接调用 getEthereumAddress(injectiveAddress) 方法。

subaccountId 第二部分是一个 24 个 16 进制数字构成的 ID 部分。我们可以使用以下代码计算 injectiveAddress 地址下 id = 1 的 subaccountId

const ethereumAddress = getEthereumAddress(injectiveAddress); const subaccountIndex = 1const suffix ="0".repeat(23) + subaccountIndex; export const subaccountId = ethereumAddress + suffix;

假如读者希望知道账户模型下的更多信息,读者可以阅读 Account 模块 的文档。总而言之,Bank 模块和 Account 模块是最常用的关于账户数据的底层接口,使用 indexerGrpcAccountApi 可以获得一些更加复杂的聚合数据,建议读者阅读相关文档或者直接阅读 SDK 源代码中的注释。


而 denom 等内容代表的含义,我们会在下文调用 TokenFactory 模块部署代币时再次介绍。


3.png

Token Factory

在介绍完基础的账户模型后,我们介绍功能模块 TokenFactory 模块,该模块可以用于代币发行,且发行后的代币可以在 Bank 模块内查找。如上文所述,在发行代币时,我们只需要找到与 TokenFactory 代币发行直接相关 msg 即可。

调用模块发行代币的代码如下:

import { injectiveAddress, privateKey } from "./config"import { MsgCreateDenomMsgBroadcasterWithPk } from"@injectivelabs/sdk-ts"import { Network } from "@injectivelabs/networks"const subdenom = "test"const denom =`factory/${injectiveAddress}/${subdenom}`const createMsg = MsgCreateDenom.fromJSON({ subdenom, symbol:"TST"name"Test Token"sender: injectiveAddress, decimals6, }); const txHash = await newMsgBroadcasterWithPk({ privateKey, networkNetwork.Testnet, }).broadcast({ msgs: createMsg, });console.log(txHash);

此处的 denom 其实是代币的全称,所有使用 TokenFactory 模块发行的代币其名称都是由 factory/${injectiveAddress}/${subdenom} 格式构成。MsgCreateDenom 可以设置如下参数:

export declare namespace MsgCreateDenom { interface Params { senderstringsubdenomstring; decimals?: number;name?: stringsymbol?: string; allowAdminBurn?: boolean; } type Proto =InjectiveTokenFactoryV1Beta1Tx.MsgCreateDenom; }

此处的 allowAdminBurn 指是否授予代币发行者销毁代币的权力,在上文代码内,我们没有设置此参数,所以事实上作为代币部署者,我们无法销毁其他代币持有者的代币。但是需要注意的是,代币发行者默认持有代币的铸造权限。

上述代码会输出交易的执行结果,其中输出中最为重要的是 txHash: "9A5E5ECFBDF91FB806AF66AD1F773E44D68D8FC73CFC7DEDDAEE6D775C805BFF", 的结论。我们可以打开 Injective 浏览器看到这笔交易。

当我们完成代币的初始发行后,我们可以设置一些更加复杂的代币元数据,比如代币 logo 和代币的单位等。代币单位似乎是一个较少被提及的事情,一个明显的例子是以太坊代币的单位:

43.png

我们可以看到以太坊内存在 Wei 等单位表示不同价值的 ETH。在 Injective 内,我们是可以对任何代币 (denom) 设置单位的,本文会简单介绍一些代币的 metadata 数据,假如读者希望进一步了解,可以阅读 ADR 024: Coin Metadata 标准。

我们可以编写以下代码实现代币的 metadata 配置:

import { injectiveAddress, privateKey } from "./config"; import { Network } from "@injectivelabs/networks"; import {MsgSetDenomMetadata, MsgBroadcasterWithPk, } from "@injectivelabs/sdk-ts"const subdenom "test"const denom = `factory/${injectiveAddress}/${subdenom}`; const metadataMsg = MsgSetDenomMetadata.fromJSON({ sender:injectiveAddress, metadata: { name"Test Token", symbol"TST", description"This is a test token", denomUnits: [ {denom: denom, exponent0, aliases: ["microtst"], }, { denom"mtst", exponent3, aliases: ["millitst"], }, { denom"tst",exponent6, aliases: [], }, ], base: denom, display"mtst", decimals6, uri"", uriHash"", }, }); const txHash awaitnew MsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: metadataMsg, });console.log(txHash);

上述代码的核心就是 metadata 部分,这部分的类型属于 CosmosBankV1Beta1Bank.Metadata 类型,读者可以通过翻找该类型的定义和相关注释获得更加详细的信息,其中 denomUnits 是最特殊的用于配置单位的列表。需要注意,最小单位必须为 denom 的全称,此处指 factory/${injectiveAddress}/${subdenom};的名称,而其他单位的 denom 可以自行设置,而 aliases 则是指当前名称的别名,比如我们可以使用 millitst 代替 mtst 使用。假如读者不将最小单位的 denom 设置为 factory/${injectiveAddress}/${subdenom} ,那么 Injective 将会返回如下报错:

metadata's first denomination unit must be the one with base denom 'factory/inj16emen7s7j09q50fk5zdx28pwn68lwzdc9zhe0j/test'

而上述代码中的 exponent 字段代表当前 unit 对应的代币数量,比如我们将 tst 的 exponent 设置为 6 则代表 1 tst = 10^6 microtst。当我们正确配置好代币的 metadata 后,等待一段时间,injective 的区块浏览器就可以正常显示我们的发行的代币,如下图:

InjTST.png

最后,我们介绍如何铸造代币。正如上文所述,铸造代币也只是向 Injective 发送铸造代币对应的 Msg 即可。

import { injectiveAddress, privateKey } from "./config"import { MsgMintMsgBroadcasterWithPk } from"@injectivelabs/sdk-ts"import { Network } from "@injectivelabs/networks"import { BigNumberInBase } from"@injectivelabs/utils"const subdenom = "test"const denom = `factory/${injectiveAddress}/${subdenom}`; constmintMsg = MsgMint.fromJSON({ sender: injectiveAddress, amount: { denom, amountnewBigNumberInBase(0.02).toWei(18).toString(), }, }); const txHash = await new MsgBroadcasterWithPk({ privateKey,networkNetwork.Testnet, }).broadcast({ msgs: mintMsg, }); console.log(txHash);

上述代码展示了如何配置某一个精度下代币子铸造,比如我们需要铸造 1000 TST 代币,但根据上文的配置 TST 代币具有 6 位精度,所以此处使用了 BigNumberInBase(1000).toWei(6).toString() 获得 1000 * 10^6 的数值。

至此,我们就完成了 TokenFactory 内的大部分操作。关于代币销毁 (MsgBurn) 和代币权限转移 (MsgChangeAdmin) 等内容,读者可以参考 Tokenfactory 文档


4.png

代币分发

代币分发也是十分简单的。在上文内,我们已经介绍过 Bank 模块用于代币的存储,而代币的转移也是与 Bank 模块直接相关的。我们可以使用 MsgSend 将代币进行转移。具体代码如下:

import { injectiveAddress, privateKey } from "./config"import { MsgSendMsgBroadcasterWithPk } from"@injectivelabs/sdk-ts"import { Network } from "@injectivelabs/networks"import { BigNumberInBase } from"@injectivelabs/utils"const subdenom = "test"const denom = `factory/${injectiveAddress}/${subdenom}`; constsendMsg = MsgSend.fromJSON({ srcInjectiveAddress: injectiveAddress, dstInjectiveAddress:"inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec"amount: { denom, amountnewBigNumberInBase(10).toWei(6).toString(), }, }); const txHash = await new MsgBroadcasterWithPk({ privateKey,networkNetwork.Testnet, }).broadcast({ msgs: sendMsg, }); console.log(txHash);

除了简单的单次代币转移外,Bank 模块也支持多代币发送操作,使用的信息类型是 MsgMultiSend。在多代币发送中,我们需要指定输入代币的数量和输出代币的数量。进行多代币发送也是较为简单的,建议读者直接阅读相关文档

5.png

代币交易

此处我们先跳过代币上线 Injective exchange 模块的教学,首先了解如何进行代币交易。注意在进行代币交易前,请确定您的账户内已经在 Injective Faucet 内领取了 USDT 测试代币。Injective 自带 exchange 模块用于现货和永续合约的交易,我们可以在区块链浏览器 和 Helix 应用内观察目前的代币交易情况。

此处我们第一次提及 Helix 应用,该应用是 Injective 推出的智能合约,该系统大幅度简化了交易的难度。假如我们使用原生的 Exchange 模块进行交易,我们需要填写一系列复杂参数。下文显示了一笔真实的使用 Exchange进行市价单交易,该订单要求卖出 0.1 INJ 代币:

{ "type": "/injective.exchange.v1beta1.MsgCreateSpotMarketOrder", "message": { "sender":"inj1spp27p48vpkyvclwu3g62ley0ulgvd3l44hq5y", "order": { "market_id":"0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", "order_info": { "subaccount_id":"0x8042af06a7606c4663eee451a57f247f3e86363f000000000000000000000000", "fee_recipient":"inj1zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3t5qxqh", "price": "0.000000000039991000", "quantity":"100000000000000000.000000000000000000", "cid": "" }, "order_type": "SELL", "trigger_price":"0.000000000000000000" } } }

市价订单内也包含 price 参数,该参数用于锁定最终交易价格,避免市价单在过于不利于交易者的价格成交,与 AMM 内设置交易滑点原理相同。


而 Helix 应用提供了市价交易的方法,并且只需要填写少量参数即可实现市价交易。下文显示了一笔使用 Helix 的交易将 5 USDT 兑换为 0.121INJ 的交易:

{ "type": "/injective.wasmx.v1.MsgExecuteContractCompat", "message": { "sender":"inj1spp27p48vpkyvclwu3g62ley0ulgvd3l44hq5y", "contract": "inj14d7h5j6ddq6pqppl65z24w7xrtmpcrqjxj8d43","msg": { "swap_min_output": { "min_output_quantity": "96000000000000000", "target_denom": "inj" } }, "funds":"5000000peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5" } }

但是需要注意的是,Injective 存在手续费返利计划,40% 的交易手续费会返还给 fee_recipient。假如我们手动使用 Exchange 模块发起交易,那么我们可以手动指定 fee_recipient 将手续费返还给自己。但假如我们使用 Helix 等应用,相关手续费会直接返还给应用。所以假如开发者是手续费敏感用户,请使用 Exchange 模块提供的原生订单方法。

在介绍具体的交易之前,我们首先需要大概知道市场的配置信息,我们可以使用以下代码读取 INJ/USDT 市场的基本信息:

import { IndexerGrpcSpotApi } from "@injectivelabs/sdk-ts"import { getNetworkEndpoints, Network } from"@injectivelabs/networks"const endpoints = getNetworkEndpoints(Network.Testnet); const indexerGrpcSpotApi = newIndexerGrpcSpotApi(endpoints.indexer); const markets = await indexerGrpcSpotApi.fetchMarkets({ baseDenom"inj",quoteDenom"peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", }); console.log(markets);

我们需要注意在 injective 内使用的 USDT 代币其实来自以太坊主网跨链,该代币归属于 peggy 模块管理,所以此处使用了 peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5 代表 USDT 代币。上述代码返回值如下:

[ { marketId: "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", marketStatus: "active",ticker: "INJ/USDT", baseDenom: "inj", quoteDenom: "peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5",quoteToken: { name: "Tether", address: "0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5", symbol: "USDT", logo:"https://imagedelivery.net/lPzngbR8EltRfBOi_WYaXw/a0bd252b-1005-47ef-d209-7c1c4a3cbf00/public", decimals: 6,updatedAt: "1744733213991", coinGeckoId: "", tokenType: "unknown", }, baseToken: { name: "Injective", address: "inj",symbol: "INJ", logo: "https://imagedelivery.net/lPzngbR8EltRfBOi_WYaXw/efaa2c96-5463-4707-0d2b-19e5b63df000/public", decimals: 18, updatedAt: "1744733213991", coinGeckoId: "", tokenType: "unknown", },makerFeeRate: "-0.0001", takerFeeRate: "0.001", serviceProviderFee: "0.4", minPriceTickSize: 1e-15,minQuantityTickSize: 1000000000000000, minNotional: 0, } ]

需要注意的是此处约定了 minPriceTickSize 代表报价的最小间隔和 minQuantityTickSize 代表单个订单最小数量。我们需要特别注意上述计算都是包含代币的精度的。以 INJ/USDT 为例,此处的 INJ 精度 (decimals) 为 18 位,而 USDT 的精度为 6 位。此时 1e-15 实际上代表:

INJ×1018USDT×106=INJUSDT×1012
我们一般使用 

INJUSDT 作为人类可读报价,所以配置中的 minPriceTickSize = 1e-15 其实代表人类可读报价中的 1e-3。换言之,INJ/USDT 交易对报价间隔为 0.001 USDT。在开发中,我们可以直接调用函数获得转换后的人类可读数据。


当我们交易前,我们需要了解当前 Exachange 模块内的订单簿挂单情况,通过订单簿挂单,我们可以知道当前交易的输出结果。我们可以调用 IndexerGrpcSpotApi 接口获得订单簿内容。读者对比不使用 formatOrderLevel 函数和使用 formatOrderLevel 的输出区别。当我们不使用 formatOrderLevel 函数时,我们发现 fetchOrderbookV2 获得的结果都是人类不可读的 ( 即包含较多位小数 ),我们需要使用 formatOrderLevel 将其转换为人类可读的。SDK 内提供了 spotPriceFromChainPrice和 spotQuantityFromChainQuantity 帮助我们进行转换。

import { IndexerGrpcSpotApitype PriceLevel } from "@injectivelabs/sdk-ts"import { getNetworkEndpoints, Network } from "@injectivelabs/networks"import { getSpotMarketTensMultiplier } from "@injectivelabs/sdk-ts"import {spotPriceToChainPriceToFixed } from "@injectivelabs/sdk-ts"import { spotPriceFromChainPrice } from"@injectivelabs/sdk-ts"import { spotQuantityFromChainQuantity } from "@injectivelabs/sdk-ts"const endpoints =getNetworkEndpoints(Network.Testnet); const indexerGrpcSpotApi = new IndexerGrpcSpotApi(endpoints.indexer);const marketId = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe"const market = awaitindexerGrpcSpotApi.fetchMarket(marketId); const orderbook = await indexerGrpcSpotApi.fetchOrderbookV2(marketId);const formatOrderLevel = (order: PriceLevel) => { return { pricespotPriceFromChainPrice({ value: order.price,baseDecimals: market.baseToken?.decimalsquoteDecimals: market.quoteToken?.decimals, }), quantity:spotQuantityFromChainQuantity({ value: order.quantitybaseDecimals: market.baseToken?.decimals, }), }; };console.log("BUY: "); console.table(orderbook.buys.slice(03).map(formatOrderLevel)); console.log("SELL: ");console.table(orderbook.sells.slice(03).map(formatOrderLevel));

上述结果输出为:

截屏 2025-06-03 11.38.17.png

我们可以看到目前 Injective 测试网订单簿内因为缺乏做市商,导致订单簿内买一和卖一价差极大,而且流动性并不是特别好。但 Injective 主网具有充足流动性。在获得了订单簿的基本信息后,我们可以尝试使用 Helix 进行一笔快速的代币交易。我们尝试卖出 0.1 INJ。根据上文输出的订单簿订单情况,我们可以挂在 price = 104.562 的 0.11 INJ 成交。所以我们设置最小的 USDT 输出为 10.4。假如我们设置了大于订单簿可接受的输出,我们可以在执行脚本时看到以下报错:

originalMessage"dispatch: submessages: reply: Min expected swap amount (11000000) not reached: execute wasm contract failed",

以下代码使用了 0.1 INJ 作为输入要求 Helix 输出不低于 10.4 USDT。根据订单博的情况,我们刚好与订单簿内的买一价格成交。读者可以在此链接找到这笔交易。

import { MsgExecuteContract } from "@injectivelabs/sdk-ts"; import { injectiveAddress, privateKey } from "../config";import { BigNumber, BigNumberInBase } from "@injectivelabs/utils"; import { MsgBroadcasterWithPk } from"@injectivelabs/sdk-ts"; import { Network } from "@injectivelabs/networks"const contractAddress ="inj14d7h5j6ddq6pqppl65z24w7xrtmpcrqjxj8d43"const usdtAddress ="peggy0x87aB3B4C8661e07D6372361211B96ed4Dc36B1B5"const swapMsg = MsgExecuteContract.fromJSON({contractAddress, sender: injectiveAddress, funds: { denom"inj", amountnewBigNumberInBase(0.1).toWei(18).toString(), }, msg: { swap_min_output: { min_output_quantitynewBigNumberInBase(10.4).toWei(6).toString(), target_denom: usdtAddress, }, }, }); const txHash = await newMsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: swapMsg, });console.log(txHash);

上述交易执行后,我们再次查询当前 Exchange 模块内的订单簿情况,我们可以获得如下输出:

截屏 2025-06-03 12.29.12.png

我们可以看到挂在 price = 104.562 的订单原有 0.11 INJ 挂单,但我们使用 Helix 进行交易后,挂单数量变成了 0.01 INJ。

笔者并没有找到 Mito 在测试网内的合约地址,假如读者有使用 Mito 进行主网交易的需求,可以自行阅读 文档。在此处简单说明 Mito 相比于 Helix 的不同。Helix 只是一个通过合约调用简化用户发起交易难度的应用,而且 Helix 提供了包括 K 线图在内的前端应用以方便用户交易。而 Mito 除了进行交易外,Mito 还提供了 CPMM 自动做市服务。

介绍完具体的使用应用的交易后,我们继续介绍直接与 Exchange 交互发起的交易。首先,我们需要阐明一些概念:

  1. LimitOrder 限价单,该订单会进行撮合成交,但未完成撮合的部分会留在订单簿中作为挂单存在

  2. MarketOrder 市价单,该订单也会撮合成交,但未完成撮合的部分会直接返会给用户

在更加细分的订单类型上,Injective 支持较多的订单类型,比如 BUY_PO 订单等,具体可以参考 Order Types 文档。我们首先介绍市价单。我们的目标还是与订单簿内的 price = 104.562 的 0.01 INJ 订单成交。此处我们会使用 0.02 INJ进行交易观察是否存在 INJ 退回的情况。我们使用的发送市价订单代码如下:

import { MsgCreateSpotMarketOrder, MsgBroadcasterWithPk, getEthereumAddress, getSpotMarketTensMultiplier,spotPriceToChainPriceToFixed, } from "@injectivelabs/sdk-ts"; import { Network } from "@injectivelabs/networks";import { privateKey, injectiveAddress } from "../config"; import { market } from "./marketInfo"; import {spotQuantityToChainQuantityToFixed } from "@injectivelabs/sdk-ts"const feeRecipient = injectiveAddress; constethereumAddress = getEthereumAddress(injectiveAddress); const subaccountIndex 0const suffix "0".repeat(23) +subaccountIndex; const subaccountId = ethereumAddress + suffix; const order = { price: 104.562, quantity: 0.02, }; constmsg = MsgCreateSpotMarketOrder.fromJSON({ subaccountId, injectiveAddress, orderType2, price:spotPriceToChainPriceToFixed({ value: order.price, baseDecimals: market.baseToken!.decimals, quoteDecimals:market.quoteToken!.decimals, }), quantityspotQuantityToChainQuantityToFixed({ value: order.quantity, baseDecimals:market.baseToken!.decimals, }), marketId: market.marketId, feeRecipient: feeRecipient, }); const txHash await newMsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: msg, }); console.log(txHash);

在此处,我们再次看到了 subaccountId 变量。如上文所述,Bank 模块允许用户创建子账户进行交易。当然,我们使用的主账户也可以被归为一个子账户,其 subaccountIndex = 0。读者也可以尝试使用其他子账户进行交易,但需要在交易前进行充值操作,将主账户内的资金划入子账户,以下代码将 10 USDT 划转给了 subaccountIndex = 1 的子账户,读者可以通过此链接找到这笔交易。

继续回到市价订单相关代码,当我们执行 MsgCreateSpotMarketOrder 后,我们可以在区块链浏览器上看到这笔交易。

截屏 2025-06-03 12.34.26.png

当交易结束后,我们再次查询订单簿情况,会发现订单簿应该变成了以下情况:

截屏 2025-06-03 12.35.03.png

我们通过卖出交易成功与 price = 104.562 的挂单成交。为了更进一步了解交易情况,读者可以使用以下代码抓取子账户的成交情况:

import { IndexerGrpcSpotApi, getEthereumAddress } from "@injectivelabs/sdk-ts"import { getNetworkEndpoints,Network } from "@injectivelabs/networks"import { injectiveAddress } from "./config"const endpoints =getNetworkEndpoints(Network.Testnet); const indexerGrpcSpotApi = new IndexerGrpcSpotApi(endpoints.indexer);const marketId = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe"; constethereumAddress = getEthereumAddress(injectiveAddress); const subaccountIndex = 0const suffix = "0".repeat(23) +subaccountIndex; const subaccountId = ethereumAddress + suffix; const subaccountOrders = awaitindexerGrpcSpotApi.fetchSubaccountTradesList({ marketId, subaccountId, }); console.log(subaccountOrders);

上述代码输出结果如下:

[ { orderHash: "0x8a62fb11e65ca09530598ac18435aa6374bccebbef97bf6c5cff8b510c0cffaa", subaccountId:"0x37e0e5a3c79456fbe1ec3e93b81a3652a5e965b3000000000000000000000000", marketId:"0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", tradeId: "71775274_0_0", cid: "",executedAt: 1744776190406, feeRecipient: "inj1xlswtg78j3t0hc0v86fmsx3k22j7jedn5773ec", tradeExecutionType:"market", executionSide: "taker", tradeDirection: "sell", fee: "313.686", price: "0.000000000104562", quantity:"10000000000000000", timestamp: 1744776190406, } ]

我们可以看到输出显示成交数量为 10000000000000000,转化为人类可读的数量是 0.01。这说明市价单的性质,市价单会一直吃单直到我们约定的 price,市价单尝试对 price 价格对订单成交。当 price 内的所有订单都被成交后,剩余代币会返还给用户。限价单则与市价单不同,限价单在吃单结束后,会将剩余资产挂到订单簿上。

我们首先展示未挂单前的市场订单簿情况,如下:

截屏 2025-06-03 12.36.30.png

我们希望在 price = 165 位置挂上 0.1 INJ 的买单。此时我们可以使用如下:

const order = { price: 167, quantity: 0.01, }; const msg = MsgCreateSpotLimitOrder.fromJSON({ subaccountId,injectiveAddress, orderType1, pricespotPriceToChainPriceToFixed({ value: order.price, baseDecimals:market.baseToken!.decimals, quoteDecimals: market.quoteToken!.decimals, }), quantity:spotQuantityToChainQuantityToFixed({ value: order.quantity, baseDecimals: market.baseToken!.decimals, }), marketId:market.marketId, feeRecipient: feeRecipient, });

为了简化代码,我们省略了部分与市价单挂单相同的代码。当我们完成挂单后,我们再次检索订单簿可以看到如下结果:

截屏 2025-06-03 12.37.06.png

我们可以使用 indexerGrpcSpotApi 内的 fetchOrderHistory 观察我们的订单情况,具体的调用代码如下:

const subaccountOrders = await indexerGrpcSpotApi.fetchOrderHistory({ marketId, subaccountId, });

调用以上 API 后输出结果如下:

{ orderHistory: [ { orderHash: "0x43346d810b2d34f11521246bc122c0105a59db46eac56cf6ad7cd8512e22d024",marketId: "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", cid: "", active: true,subaccountId: "0x37e0e5a3c79456fbe1ec3e93b81a3652a5e965b3000000000000000000000000", executionType: "limit",orderType: "buy", price: "0.000000000167", triggerPrice: "0", quantity: "10000000000000000", filledQuantity: "0", state:"booked", createdAt: 1744875419465, updatedAt: 1744875419465, direction: "buy", }, { orderHash:"0x8a62fb11e65ca09530598ac18435aa6374bccebbef97bf6c5cff8b510c0cffaa", marketId:"0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe", cid: "", active: false, subaccountId:"0x37e0e5a3c79456fbe1ec3e93b81a3652a5e965b3000000000000000000000000", executionType: "market", orderType:"sell", price: "0.000000000104562", triggerPrice: "0", quantity: "10000000000000000", filledQuantity:"10000000000000000", state: "filled", createdAt: 1744776190406, updatedAt: 1744776190406, direction: "sell", } ] }

我们可以看到我们挂单的订单还处于 booked 状态并没有被成交,此处也可以检索到之前已经成交的市价单。

6.png

总结

本文介绍了 Injective 与交易相关的基础模块,包括代币发行使用的 TokenFactory 模块和代币交易使用的 Exchange 模块。本文大部分内容都参考了 Injective Typescript SDK 文档,但笔者建议读者也可以尝试参考更加详细但不包含代码示例的 injective 开发者文档。在阅读完本文后,读者应该可以直接看懂开发者文档中的信息格式,并且使用 SDK 构建 Msg 并广播。在下一篇文章中的治理上币流程中,我们需要 50 INJ 作为提案费用,假如读者希望亲手尝试,可以准备一下。



往期精彩
图片
图片
图片
图片


关于我们

ABOUT US



TinTinLand 是赋能下一代开发者的技术社区,通过聚集、培育、输送开发者到各开放网络,共同定义并构建未来。


Discord: https://discord.gg/65N69bdsKw

Twitter: https://twitter.com/OurTinTinLand

Bilibili: https://space.bilibili.com/1152852334

Medium: https://medium.com/tintinland

YouTube: https://www.youtube.com/@tintinland3610


点击“阅读原文”进入 TinTinLand 社区空间 Notion 资源库。

关注同名小红书账号(3955930765)与小 T 同学一起探索 Web3 最前沿!

【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。

TinTinLand
数据请求中
查看更多

推荐专栏

数据请求中
在 App 打开