Injective 是一条专注于金融应用的区块链。相比于其他 EVM 系区块链,Injective 提供了模块等功能,我们可以直接通过模块实现代币发行和代币交易等功能,所以在构建应用时,开发者并不需要编写大量的链上合约,而只需要调用多个模块就可以完成一个金融应用的构建。
本文将使用 Injective 提供的模块实现以下几个功能:
代币发行,使用 Tokenfactory
模块发行原生代币
代币交易,使用 helix 进行市价代币交易和使用原生的 Exchange
模块完成订单簿交易
在介绍具体的功能开发前,我们会首先介绍一些本文所需要的基础内容。由于笔者不擅长前端开发,所以本文编写的 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 测试网内部。我们会在后文多次使用此格式的代码进行发起交易操作。
在编写脚本前,我们需要了解 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().address; console.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 有以下两个水龙头:
Google Web3 Faucet
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 = 1; const suffix ="0".repeat(23) + subaccountIndex; export const subaccountId = ethereumAddress + suffix;
假如读者希望知道账户模型下的更多信息,读者可以阅读 Account 模块 的文档。总而言之,Bank 模块和 Account 模块是最常用的关于账户数据的底层接口,使用 indexerGrpcAccountApi 可以获得一些更加复杂的聚合数据,建议读者阅读相关文档或者直接阅读 SDK 源代码中的注释。
而 denom 等内容代表的含义,我们会在下文调用 TokenFactory 模块部署代币时再次介绍。
在介绍完基础的账户模型后,我们介绍功能模块 TokenFactory 模块,该模块可以用于代币发行,且发行后的代币可以在 Bank 模块内查找。如上文所述,在发行代币时,我们只需要找到与 TokenFactory 代币发行直接相关 msg 即可。
调用模块发行代币的代码如下:
import { injectiveAddress, privateKey } from "./config"; import { MsgCreateDenom, MsgBroadcasterWithPk } 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, decimals: 6, }); const txHash = await newMsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: createMsg, });console.log(txHash);
此处的 denom 其实是代币的全称,所有使用 TokenFactory 模块发行的代币其名称都是由 factory/${injectiveAddress}/${subdenom} 格式构成。MsgCreateDenom 可以设置如下参数:
export declare namespace MsgCreateDenom { interface Params { sender: string; subdenom: string; decimals?: number;name?: string; symbol?: string; allowAdminBurn?: boolean; } type Proto =InjectiveTokenFactoryV1Beta1Tx.MsgCreateDenom; }
此处的 allowAdminBurn 指是否授予代币发行者销毁代币的权力,在上文代码内,我们没有设置此参数,所以事实上作为代币部署者,我们无法销毁其他代币持有者的代币。但是需要注意的是,代币发行者默认持有代币的铸造权限。
上述代码会输出交易的执行结果,其中输出中最为重要的是 txHash: "9A5E5ECFBDF91FB806AF66AD1F773E44D68D8FC73CFC7DEDDAEE6D775C805BFF", 的结论。我们可以打开 Injective 浏览器看到这笔交易。
当我们完成代币的初始发行后,我们可以设置一些更加复杂的代币元数据,比如代币 logo 和代币的单位等。代币单位似乎是一个较少被提及的事情,一个明显的例子是以太坊代币的单位:
我们可以看到以太坊内存在 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, exponent: 0, aliases: ["microtst"], }, { denom: "mtst", exponent: 3, aliases: ["millitst"], }, { denom: "tst",exponent: 6, aliases: [], }, ], base: denom, display: "mtst", decimals: 6, 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 的区块浏览器就可以正常显示我们的发行的代币,如下图:
最后,我们介绍如何铸造代币。正如上文所述,铸造代币也只是向 Injective 发送铸造代币对应的 Msg 即可。
import { injectiveAddress, privateKey } from "./config"; import { MsgMint, MsgBroadcasterWithPk } 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, amount: newBigNumberInBase(0.02).toWei(18).toString(), }, }); const txHash = await new MsgBroadcasterWithPk({ privateKey,network: Network.Testnet, }).broadcast({ msgs: mintMsg, }); console.log(txHash);
上述代码展示了如何配置某一个精度下代币子铸造,比如我们需要铸造 1000 TST 代币,但根据上文的配置 TST 代币具有 6 位精度,所以此处使用了 BigNumberInBase(1000).toWei(6).toString() 获得 1000 * 10^6 的数值。
至此,我们就完成了 TokenFactory 内的大部分操作。关于代币销毁 (MsgBurn) 和代币权限转移 (MsgChangeAdmin) 等内容,读者可以参考 Tokenfactory 文档
代币分发也是十分简单的。在上文内,我们已经介绍过 Bank 模块用于代币的存储,而代币的转移也是与 Bank 模块直接相关的。我们可以使用 MsgSend 将代币进行转移。具体代码如下:
import { injectiveAddress, privateKey } from "./config"; import { MsgSend, MsgBroadcasterWithPk } 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, amount: newBigNumberInBase(10).toWei(6).toString(), }, }); const txHash = await new MsgBroadcasterWithPk({ privateKey,network: Network.Testnet, }).broadcast({ msgs: sendMsg, }); console.log(txHash);
除了简单的单次代币转移外,Bank 模块也支持多代币发送操作,使用的信息类型是 MsgMultiSend。在多代币发送中,我们需要指定输入代币的数量和输出代币的数量。进行多代币发送也是较为简单的,建议读者直接阅读相关文档。
此处我们先跳过代币上线 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 实际上代表:
我们一般使用
当我们交易前,我们需要了解当前 Exachange 模块内的订单簿挂单情况,通过订单簿挂单,我们可以知道当前交易的输出结果。我们可以调用 IndexerGrpcSpotApi 接口获得订单簿内容。读者对比不使用 formatOrderLevel 函数和使用 formatOrderLevel 的输出区别。当我们不使用 formatOrderLevel 函数时,我们发现 fetchOrderbookV2 获得的结果都是人类不可读的 ( 即包含较多位小数 ),我们需要使用 formatOrderLevel 将其转换为人类可读的。SDK 内提供了 spotPriceFromChainPrice和 spotQuantityFromChainQuantity 帮助我们进行转换。
import { IndexerGrpcSpotApi, type 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 { price: spotPriceFromChainPrice({ value: order.price,baseDecimals: market.baseToken?.decimals, quoteDecimals: market.quoteToken?.decimals, }), quantity:spotQuantityFromChainQuantity({ value: order.quantity, baseDecimals: market.baseToken?.decimals, }), }; };console.log("BUY: "); console.table(orderbook.buys.slice(0, 3).map(formatOrderLevel)); console.log("SELL: ");console.table(orderbook.sells.slice(0, 3).map(formatOrderLevel));
上述结果输出为:
我们可以看到目前 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", amount: newBigNumberInBase(0.1).toWei(18).toString(), }, msg: { swap_min_output: { min_output_quantity: newBigNumberInBase(10.4).toWei(6).toString(), target_denom: usdtAddress, }, }, }); const txHash = await newMsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: swapMsg, });console.log(txHash);
上述交易执行后,我们再次查询当前 Exchange 模块内的订单簿情况,我们可以获得如下输出:
我们可以看到挂在 price = 104.562 的订单原有 0.11 INJ 挂单,但我们使用 Helix 进行交易后,挂单数量变成了 0.01 INJ。
笔者并没有找到 Mito 在测试网内的合约地址,假如读者有使用 Mito 进行主网交易的需求,可以自行阅读 文档。在此处简单说明 Mito 相比于 Helix 的不同。Helix 只是一个通过合约调用简化用户发起交易难度的应用,而且 Helix 提供了包括 K 线图在内的前端应用以方便用户交易。而 Mito 除了进行交易外,Mito 还提供了 CPMM 自动做市服务。
介绍完具体的使用应用的交易后,我们继续介绍直接与 Exchange 交互发起的交易。首先,我们需要阐明一些概念:
LimitOrder 限价单,该订单会进行撮合成交,但未完成撮合的部分会留在订单簿中作为挂单存在
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 = 0; const suffix = "0".repeat(23) +subaccountIndex; const subaccountId = ethereumAddress + suffix; const order = { price: 104.562, quantity: 0.02, }; constmsg = MsgCreateSpotMarketOrder.fromJSON({ subaccountId, injectiveAddress, orderType: 2, price:spotPriceToChainPriceToFixed({ 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, }); const txHash = await newMsgBroadcasterWithPk({ privateKey, network: Network.Testnet, }).broadcast({ msgs: msg, }); console.log(txHash);
在此处,我们再次看到了 subaccountId 变量。如上文所述,Bank 模块允许用户创建子账户进行交易。当然,我们使用的主账户也可以被归为一个子账户,其 subaccountIndex = 0。读者也可以尝试使用其他子账户进行交易,但需要在交易前进行充值操作,将主账户内的资金划入子账户,以下代码将 10 USDT 划转给了 subaccountIndex = 1 的子账户,读者可以通过此链接找到这笔交易。
继续回到市价订单相关代码,当我们执行 MsgCreateSpotMarketOrder 后,我们可以在区块链浏览器上看到这笔交易。
当交易结束后,我们再次查询订单簿情况,会发现订单簿应该变成了以下情况:
我们通过卖出交易成功与 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 = 0; const 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 内的所有订单都被成交后,剩余代币会返还给用户。限价单则与市价单不同,限价单在吃单结束后,会将剩余资产挂到订单簿上。
我们首先展示未挂单前的市场订单簿情况,如下:
我们希望在 price = 165 位置挂上 0.1 INJ 的买单。此时我们可以使用如下:
const order = { price: 167, quantity: 0.01, }; const msg = MsgCreateSpotLimitOrder.fromJSON({ subaccountId,injectiveAddress, orderType: 1, price: spotPriceToChainPriceToFixed({ 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, });
为了简化代码,我们省略了部分与市价单挂单相同的代码。当我们完成挂单后,我们再次检索订单簿可以看到如下结果:
我们可以使用 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 状态并没有被成交,此处也可以检索到之前已经成交的市价单。
本文介绍了 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 最前沿!
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。