开发
教程:代币与资金池
在公开 signet 上部署示例代币,为它和 KSA 开一个资金池,添加流动性,再双向兑换,全程只用 ksana-cli。
本页目录
在本教程中,你会部署示例合约中的三个,并在命令行里使用它们:
token:一种同质化代币,余额记在合约自己的存储里;amm:一个恒定乘积资金池,一侧是你的代币,另一侧是 KSA;amm-factory:一个创建资金池的合约,资金池的 ID 可以事先算出。
本页的每条命令和输出,都来自 2026 年 10 月 14 日在公开 signet 上的同一次运行。你得到的 ID、哈希和数额都会不同。signet 上的币没有价值,主网也尚未上线:请在 signet 上运行。
开始之前
你需要:
ksana-cli,按快速上手的做法从仓库构建,并放在PATH中(ksana-cli 一页讲解了每一条命令);- 仓库所用的 Rust 工具链,以及
wasm32-unknown-unknown编译目标:在克隆目录里运行rustup toolchain install,就会按rust-toolchain.toml把两者都装好; bash、python3和jq,下面的辅助函数要用到它们;- signet 上约 51 KSA。水龙头给的 100 KSA 就够了,你移除的流动性会回到你手里。
所有命令都在你克隆的仓库根目录下、在同一个 bash 会话里运行:后面的步骤要用到前面步骤设置的 shell 变量。这里每笔操作的手续费是 1 个最小单位。
构建合约
这些合约是 contracts/ 里一个独立的 Cargo workspace。在这个目录里逐个构建,只产出 cdylib,这样 contracts/.cargo/config.toml 才会生效,模块也能保持小巧:
cd contracts
for p in token amm amm-factory; do
cargo rustc --release --locked --target wasm32-unknown-unknown --package $p --crate-type cdylib
done
cd ..
W=contracts/target/wasm32-unknown-unknown/release
wc -c $W/token.wasm $W/amm.wasm $W/amm_factory.wasm11572 contracts/target/wasm32-unknown-unknown/release/token.wasm
20768 contracts/target/wasm32-unknown-unknown/release/amm.wasm
9598 contracts/target/wasm32-unknown-unknown/release/amm_factory.wasm
41938 total模块的字节取决于你检出目录的绝对路径,所以你的代码哈希会和下面的不同,合约 ID 也随之不同。
载入辅助函数
ksana-cli 接受的合约参数是其 Borsh 编码的十六进制,它不会替你做任何编码。把下面这些函数存为 helpers.sh:
# 给 --args 和 --init-args 用的 Borsh 值、合约 ID,以及精简的回执(python3、jq)
enc_u128() { python3 -c 'import sys; print(int(sys.argv[1]).to_bytes(16, "little").hex())' "$1"; }
enc_string() {
python3 -c 'import sys; b = sys.argv[1].encode()
print(len(b).to_bytes(4, "little").hex() + b.hex())' "$1"
}
dec_u128() { # 一个或多个依次相连的 u128 值
python3 -c 'import sys; b = bytes.fromhex(sys.argv[1])
print(*(int.from_bytes(b[i:i + 16], "little") for i in range(0, len(b), 16)))' "$1"
}
code_hash() {
python3 -c 'import hashlib, sys
print(hashlib.sha256(open(sys.argv[1], "rb").read()).hexdigest())' "$1"
}
contract_id() { # sha256(账户 + 写成小端 u64 的 nonce + sha256(代码))
python3 -c 'import hashlib, sys
code = hashlib.sha256(open(sys.argv[3], "rb").read()).digest()
seed = bytes.fromhex(sys.argv[1]) + int(sys.argv[2]).to_bytes(8, "little") + code
print(hashlib.sha256(seed).hexdigest())' "$@"
}
brief() { # --wait 结果中本教程要读的部分
jq '{nonce, op_id, status: .status.status, block: .status.block.height,
gas_used: .status.receipt.gas_used, error: .status.receipt.error,
output: .status.receipt.output}' "$@"
}载入这些函数,让 CLI 指向 signet 节点,再把 KSA 的 ID(32 个零字节)放在手边:
source ./helpers.sh
export KSANA_RPC=https://signet.ksana.network/rpc
KSA=0000000000000000000000000000000000000000000000000000000000000000步骤之后的参数如何编码一节,解释了这些辅助函数生成的字节。
生成密钥并领取 KSA
创建一个密钥文件,把你的账户存进 ACCOUNT,再把你的地址发给水龙头:
ksana-cli --chain signet key alice.key
ACCOUNT=$(ksana-cli --chain signet key alice.key | jq -r .account)
curl -sS https://signet.ksana.network/faucet -H 'content-type: application/json' \
--data '{"to": "tb1p6z9g9hz82q62tuvtkkymaq6rguhx0xttsvrkfzyme0k39cj9zzlsk7pwe9"}' | jq{
"account": "d08a82dc475034a5f18bb589be8343472e67996b830764889bcbed12e24510bf",
"address": "tb1p6z9g9hz82q62tuvtkkymaq6rguhx0xttsvrkfzyme0k39cj9zzlsk7pwe9",
"internal_key": "14e52f83357e9f7ebc1e5939036475473208acd8561431b64d7a56d512ca9560",
"script": "5120d08a82dc475034a5f18bb589be8343472e67996b830764889bcbed12e24510bf"
}
{
"op_id": "cdfbd418ad44c47c8fe5a31ba885e9bf1de6c13b5a467e79f4b2b735fc54be97",
"to": "d08a82dc475034a5f18bb589be8343472e67996b830764889bcbed12e24510bf",
"amount": "10000000000",
"nonce": "5"
}向水龙头申请时,请填上你自己的 address。水龙头每天给每个账户发一次 100 KSA(10,000,000,000 个最小单位);几秒钟后,ksana-cli account @alice.key 就会显示这笔余额。如果你手上已有 signet KSA,就跳过水龙头。
部署代币
代币的 init 接受 (name: String, symbol: String, decimals: u8, supply: u128),并把总量全部发给部署者。这次运行创建 1,000,000 TUT,有 8 位小数:
INIT=$(enc_string "Tutorial Token")$(enc_string TUT)08$(enc_u128 100000000000000)
echo $INIT
ksana-cli deploy --key alice.key --code $W/token.wasm --init-args $INIT \
--fee-token 1 --wait | tee token.json | brief0e0000005475746f7269616c20546f6b656e030000005455540800407a10f35a00000000000000000000
{
"nonce": "0",
"op_id": "6daa9848433a349d8f915102679c3cd8632f351c3ee4a17207a58839158dda99",
"status": "fast_confirmed",
"block": "264",
"gas_used": "383868",
"error": null,
"output": ""
}--fee-token 1 让操作走路径 B,手续费为 1 个最小单位,这是节点默认的打包门槛,signet 的种子节点也沿用这个值。运行 ksana-cli estimate-fee,可以看到你的节点建议多少。--wait 会一直轮询,直到有区块收录这笔操作:fast_confirmed 是它的三道印记中的第一道。完整的结果连同回执里的事件,都保存在 token.json 中。
算出代币的 ID
没有哪个地方会报告新合约的 ID:要你自己算。它是你的账户、部署操作的 nonce(写成小端 u64)和代码的 SHA-256 三者的 SHA-256:
TOKEN=$(contract_id $ACCOUNT $(jq -r .nonce token.json) $W/token.wasm)
echo $TOKEN
jq -r '.status.receipt.events[0].contract' token.jsonf622bdcea40708c8776b36dbe387e7285b57f8c3734d370a31b63c5c10756740
f622bdcea40708c8776b36dbe387e7285b57f8c3734d370a31b63c5c10756740第二行用来交叉核对:代币的 init 会发出一个 transfer 事件,而每个事件都写明了发出它的合约。现在把这个代币读回来。query 在节点上运行一个方法,不提交任何东西,所以不花手续费:
ksana-cli query --contract $TOKEN --method metadata
ksana-cli query --contract $TOKEN --method balance_of --args $ACCOUNT | jq -r .output{
"error": null,
"events": [],
"gas_used": "37780",
"op_id": null,
"output": "0e0000005475746f7269616c20546f6b656e0300000054555408",
"success": true
}
00407a10f35a00000000000000000000第一段输出是 Borsh 编码的 (name, symbol, decimals)。余额是一个 u128:dec_u128 00407a10f35a00000000000000000000 打印出 100000000000000,正是全部总量。
部署资金池
资金池的 init 接受它的两侧。每一侧先是表示种类的一个字节(00 表示代币合约,01 表示 KSA 这样的内置资产),后接 32 字节的 ID。A 侧是你的代币,B 侧是 KSA:
ksana-cli deploy --key alice.key --code $W/amm.wasm --init-args 00${TOKEN}01${KSA} \
--fee-token 1 --wait | tee pool.json | brief
POOL=$(contract_id $ACCOUNT $(jq -r .nonce pool.json) $W/amm.wasm)
echo $POOL{
"nonce": "1",
"op_id": "fe894004a8b65f507112a395ef01b9648c399b0471f9af98b621d85d5d625ce2",
"status": "fast_confirmed",
"block": "266",
"gas_used": "574872",
"error": null,
"output": ""
}
2d6e5edc46352b2931657479fce65fdbf01aafc5acc3de7c27660ce19dda8171资金池的方法按两侧的顺序接受数额:先 TUT,后 KSA。
授权资金池
资金池用代币的 transfer_from 从你这里拉取 TUT,所以先要允许它动用一部分。approve 接受 (spender: [u8; 32], amount: u128),把授权额度设为 amount,而不是在原有额度上累加:
ksana-cli call --key alice.key --contract $TOKEN --method approve \
--args $POOL$(enc_u128 10000000000000) --fee-token 1 --wait | tee approve.json | brief{
"nonce": "2",
"op_id": "74d05565eccaca72a833327ce51d1046f7f485aa7d4f804428c1e47ea26dfdca",
"status": "fast_confirmed",
"block": "267",
"gas_used": "48026",
"error": null,
"output": ""
}添加流动性
add_liquidity 接受 (amount_a: u128, amount_b: u128)。KSA 是内置资产,所以要用 --fund 把它附带在调用上,附带的数额必须等于 amount_b。这次存入把 100,000 TUT 和 50 KSA 放进资金池:
ksana-cli call --key alice.key --contract $POOL --method add_liquidity \
--args $(enc_u128 10000000000000)$(enc_u128 5000000000) --fund 5000000000 \
--fee-token 1 --wait | tee add.json | brief
dec_u128 $(jq -r .status.receipt.output add.json){
"nonce": "3",
"op_id": "ff4586be24579b8929a77f5c0c83b5464bf29759a36018e72702462ff2a3de6d",
"status": "fast_confirmed",
"block": "268",
"gas_used": "182212",
"error": null,
"output": "b5f50010340000000000000000000000"
}
223606797749输出是你得到的资金池份额数。第一笔存入得到的份额,是两个数额乘积的整数平方根。之后的存入按储备的比例得到份额;资金池只收下当前比例所需的数额,附带资产多出的部分会退回。
查询兑换报价
swap 接受 (id_in: [u8; 32], amount_in: u128, min_out: u128),并为资金池留下 0.3% 的手续费。要看一次兑换会返回多少,就用 query 运行它,附带同样的资产,并以你的账户作为调用者,这样模拟的转账才有 KSA 可转:
QUOTE=$(ksana-cli query --contract $POOL --method swap \
--args $KSA$(enc_u128 100000000)$(enc_u128 1) --fund 100000000 --caller $ACCOUNT \
| jq -r .output)
dec_u128 $QUOTE
MIN_OUT=$(( $(dec_u128 $QUOTE) * 99 / 100 ))
echo $MIN_OUT195501696178
193546679216按目前的储备,1 KSA 大约能买到 1,955.02 TUT。MIN_OUT 容许最多少换 1%,以防另一笔交易抢在你之前推动了价格。
用 KSA 换 TUT
把同样的兑换作为操作发出,以 MIN_OUT 作为下限:
ksana-cli call --key alice.key --contract $POOL --method swap \
--args $KSA$(enc_u128 100000000)$(enc_u128 $MIN_OUT) --fund 100000000 \
--fee-token 1 --wait | tee swap1.json | brief{
"nonce": "4",
"op_id": "95536d3e8c77f64e16126ae83c5aa628a393a269d9e4f6210e6f4815018bfe82",
"status": "fast_confirmed",
"block": "269",
"gas_used": "144458",
"error": null,
"output": "b224cf842d0000000000000000000000"
}输出是你换到的 TUT,195,501,696,178 个最小单位:这期间没有别的交易,所以报价完全准确。假如价格的变动越过了 MIN_OUT,这笔操作就会失败:失败的操作照样付手续费、用掉 nonce,除此之外什么也不改变。
用 TUT 换 KSA
卖出 TUT 的做法相同,只是资金池通过你的授权额度拉取 TUT,而不是收取附带的资产。前面的存入已经用完了第一次的授权额度,所以先授权 1,000 TUT,再询价:
ksana-cli call --key alice.key --contract $TOKEN --method approve \
--args $POOL$(enc_u128 100000000000) --fee-token 1 --wait | tee approve2.json | brief
QUOTE=$(ksana-cli query --contract $POOL --method swap \
--args $TOKEN$(enc_u128 100000000000)$(enc_u128 1) --caller $ACCOUNT | jq -r .output)
dec_u128 $QUOTE
MIN_OUT=$(( $(dec_u128 $QUOTE) * 99 / 100 ))
echo $MIN_OUT{
"nonce": "5",
"op_id": "9ce4f9f63517986d438a0f0623f1b8ac4ee215ea968508e8527ac96c4ce2ede1",
"status": "fast_confirmed",
"block": "270",
"gas_used": "48026",
"error": null,
"output": ""
}
51338834
50825445然后兑换:
ksana-cli call --key alice.key --contract $POOL --method swap \
--args $TOKEN$(enc_u128 100000000000)$(enc_u128 $MIN_OUT) \
--fee-token 1 --wait | tee swap2.json | brief{
"nonce": "6",
"op_id": "e326283aa0609470c215ad2f79c9867d0815012f32c616e69c88bcd5e6df1413",
"status": "fast_confirmed",
"block": "271",
"gas_used": "152265",
"error": null,
"output": "525e0f03000000000000000000000000"
}1,000 TUT 换到了 51,338,834 个最小单位的 KSA,约 0.513 KSA。
移除流动性
remove_liquidity 接受一个份额数,按这些份额所占的比例付出两侧的储备,向下取整。读出你的份额,然后移除其中一半:
dec_u128 $(ksana-cli query --contract $POOL --method shares_of --args $ACCOUNT | jq -r .output)
ksana-cli call --key alice.key --contract $POOL --method remove_liquidity \
--args $(enc_u128 111803398874) --fee-token 1 --wait | tee remove.json | brief
dec_u128 $(jq -r .status.receipt.output remove.json)223606797749
{
"nonce": "7",
"op_id": "1d9adba6e303c417f287022e7c5f1fb9c3cd48b92794f55d780e7c91aa01e70b",
"status": "fast_confirmed",
"block": "272",
"gas_used": "170509",
"error": null,
"output": "90310d09810400000000000000000000563a7696000000000000000000000000"
}
4952249151888 2524330582输出是 (u128, u128):49,522.49151888 TUT 和 25.24330582 KSA 回到了你的账户。
用工厂合约创建资金池
amm-factory 用 create_contract 创建资金池,后者用链上已经存有的代码实例化新合约:你在上面部署的资金池,已经把 amm 的代码存到了链上。工厂合约的 init 接受这份代码的哈希:
ksana-cli deploy --key alice.key --code $W/amm_factory.wasm \
--init-args $(code_hash $W/amm.wasm) --fee-token 1 --wait | tee factory.json | brief
FACTORY=$(contract_id $ACCOUNT $(jq -r .nonce factory.json) $W/amm_factory.wasm)
echo $FACTORY{
"nonce": "8",
"op_id": "7e3c309278bbad5cc0f0c0dfcb55bbd3689cdced9d4410c704e2f09c89330026",
"status": "fast_confirmed",
"block": "273",
"gas_used": "326178",
"error": null,
"output": ""
}
5ba8f13c2f75c64be4852d39eea42ec84840c811d1f2199f71a7e2a7c407e4bdcreate_pair 接受两侧,创建资金池,并返回它的 ID。get_pair 能再找到它,两侧按哪种顺序给都行;它返回一个 Borsh Option:01 加上 ID,没有这样的资金池时为 00:
ksana-cli call --key alice.key --contract $FACTORY --method create_pair \
--args 00${TOKEN}01${KSA} --fee-token 1 --wait | tee pair.json | brief
PAIR=$(jq -r .status.receipt.output pair.json)
ksana-cli query --contract $FACTORY --method get_pair --args 01${KSA}00${TOKEN} | jq -r .output{
"nonce": "9",
"op_id": "2f9117f181b5ea9b3269d91503bb564e77bd4eae54d8a6f48f4d6c77f1e1cc3d",
"status": "fast_confirmed",
"block": "274",
"gas_used": "238389",
"error": null,
"output": "94e081b6f8de861947733beb835c70ba16049c89926be3f81e5f8587f15bac90"
}
0194e081b6f8de861947733beb835c70ba16049c89926be3f81e5f8587f15bac90工厂合约把两侧排成同一种顺序,所以同样的两侧只对应一个资金池。KSA 的 ID 全是零,所以在这个资金池里,A 侧是 KSA,B 侧是 TUT:调用 add_liquidity 时要先给 KSA 的数额。新资金池是空的,与你自己部署的那个互不相干;像前面那样,用 approve 和 add_liquidity 为它注资。
跟踪一笔操作到最终
你的操作都已快轨确认。一旦携带某笔操作所在区块的结算在比特币上得到确认,这笔操作就已写入比特币;这笔结算有了 6 个确认,操作就是最终的。查询部署操作,以及它所在区块 264 的结算:
ksana-cli op $(jq -r .op_id token.json) | jq '{status, block: .block.height}'
ksana-cli settlement 264 | jq '{status, txid_rpc, confirmed, settled, btc_final}'{
"status": "btc_final",
"block": "264"
}
{
"status": "confirmed",
"txid_rpc": "e84abcb90855f3b4bf43989d21814c5d5bd70592d88d25e18f5c1c7cb0e8caa7",
"confirmed": {
"block": "c48a4f4f1f7ce1e696becf01bbc5f15fdbf8ba7145b9badff31c7e7711000000",
"confirmations": 6,
"height": 326322,
"txid": "a7cae8b07c1c5c8fe1258dd89205d75b5d4c81219d9843bfb4f35508b9bc4ae8"
},
"settled": true,
"btc_final": true
}在它的区块产出约一小时后,携带该区块结算的比特币交易有了 6 个确认,这笔操作随之变为 btc_final。txid_rpc 是这笔交易在区块浏览器里显示的 ID;confirmed.txid 和 confirmed.block 则按比特币的内部字节序书写,所以区块哈希以零结尾。随着各自区块的结算达到 6 个确认,你的其他操作也会相继变为最终;失败的操作永远不会显示 btc_final:即使它的区块已经最终,它的状态也一直是 failed。
参数如何编码
方法的参数是一个 Borsh 元组:各个参数的编码依次紧挨着排列。只有一个参数的方法,接受的就是这个参数的编码;没有参数的方法,接受空的字节。返回值遵循同样的规则。
| 类型 | Borsh 字节 | 辅助函数或示例 |
|---|---|---|
u8 |
1 字节 | 08 |
u128 |
16 字节,小端 | enc_u128 5000000000、dec_u128 |
String |
u32 长度(小端),然后是 UTF-8 | enc_string TUT 得到 03000000545554 |
[u8; 32] |
这 32 个字节本身,不带长度 | 账户、合约 ID、$KSA |
Option<T> |
00,或 01 后接 T |
get_pair 的输出 |
amm 的 Side |
00(代币)或 01(资产),后接 ID |
00${TOKEN}、01${KSA} |
编写合约从合约的角度讲解这些约定。
出了问题时
- 状态是
failed。error会说明原因,比如资金池的授权额度太小时,它是revert: insufficient allowance;多授权一些,再试一次。失败的操作仍会用掉它的 nonce,并支付手续费。 --wait超时。等满 120 秒(--timeout)后,CLI 只打印一条错误,不带操作 ID,尽管操作已经发出。运行ksana-cli account @alice.key:如果nonce增加了,操作就已在区块里,JSON-RPC API 中的ksana_getAccountOps会列出它。- 节点拒收操作。手续费低于节点的门槛时,节点会拒收。CLI 从账户的状态读取 nonce,所以在第一笔操作进入区块之前发出的第二笔,会拿到同一个 nonce:节点要么拒收它,要么在它的手续费更高时,丢弃第一笔、换成它。像本页这样,用
--wait一次只发一笔操作。
下一步
- 示例合约 代币、资金池和工厂合约的每一个方法,以及其他示例。
- 编写合约 开始写你自己的合约,测试它,再部署它。
- TypeScript SDK 在网页应用或脚本里调用同样的合约。