Ksana
EN

开发

示例合约

仓库附带五个示例合约,从代币到出售 KSA 换取比特币的市场,另有一个测试合约。每一个都经过测试,都是可供阅读、构建和部署的范本。

本页目录

这些合约位于仓库的 contracts/ 目录,自成一个 Cargo workspace。每个 crate 的 src/lib.rs 开头的注释,记载了它的每个方法、事件和规则;本页对这些内容做个概述。

合约 展示什么 目录
token 存储、事件、授权额度 contracts/examples/token
amm 调用其他合约、附带资产 contracts/examples/amm
amm-factory 由合约创建合约 contracts/examples/amm-factory
btc-payment-trigger 比特币区块头与付款证明 contracts/examples/btc-payment-trigger
ksa-exchange native_tx 与路径 C contracts/examples/ksa-exchange
probe 每一个宿主函数,供测试使用 contracts/testing/probe

构建与测试

本机测试在 SDK 的模拟宿主上运行每个合约,不需要节点。在这个 workspace 里运行它们:

Shell
git clone https://github.com/ksana-in-bitcoin/Ksana
cd Ksana/contracts
cargo test --workspace

在 contracts/ 目录里构建模块,并且只产出 cdylib,这样 contracts/.cargo/config.toml 才会生效,模块也能保持小巧:

Shell
cargo rustc --release --locked --target wasm32-unknown-unknown \
  --package ksa-exchange --crate-type cdylib

模块会生成在 contracts/target/wasm32-unknown-unknown/release/ 里,以包名命名,连字符换成下划线:ksa_exchange.wasm、amm_factory.wasm。它的字节取决于你检出目录的绝对路径,所以要部署的文件只构建一次,并用这个文件算出 ID。

部署一个合约

用 ksana-cli deploy 部署,它接受模块,以及 init 参数的 Borsh 字节(写成十六进制)。输出里没有新合约的 ID:要用你的账户、部署操作的 nonce 和模块的 SHA-256 自己算出来,可以用代币与资金池教程里的辅助函数 contract_id,并像那里一样把你的账户放在 ACCOUNT 中。ksa-exchange 的 init 不接受参数:

Shell
export KSANA_RPC=https://signet.ksana.network/rpc
W=target/wasm32-unknown-unknown/release
ksana-cli deploy --key alice.key --code $W/ksa_exchange.wasm --fee-token 1 --wait > exchange.json
EXCHANGE=$(contract_id $ACCOUNT $(jq -r .nonce exchange.json) $W/ksa_exchange.wasm)

那篇教程在公开 signet 上部署了 token、amm 和 amm-factory,并附有真实的输出。主网尚未上线,所以请在 signet 上部署。

token

一种同质化代币,余额记在合约的存储里。它不是 KSA 那样的内置资产:节点的余额和资产方法看不到它,要转移它,就得调用合约。init 把全部总量都交给部署者。

要让资金池之类的合约转移你的代币,先调用 approve(spender, amount),之后由被授权方调用 transfer_from。approve 把授权额度设为 amount,而不是在原有额度上累加。余额或授权额度不足的转账,会以 insufficient balance 或 insufficient allowance revert。

方法 参数 返回
init (name: String, symbol: String, decimals: u8, supply: u128) ()
transfer (to: [u8; 32], amount: u128) ()
approve (spender: [u8; 32], amount: u128) ()
transfer_from (from: [u8; 32], to: [u8; 32], amount: u128) ()
balance_of account: [u8; 32] u128
allowance (owner: [u8; 32], spender: [u8; 32]) u128
total_supply 无 u128
metadata 无 (name, symbol, decimals)

事件:("transfer", from, to, amount),对 init 创建的总量,from 为全零;以及 ("approve", owner, spender, amount)。

amm

一个由两侧组成的恒定乘积资金池。每一侧是一个代币合约,或是 KSA 这样的内置资产,编码为一个字节(0 为代币,1 为资产)加上 32 字节的 ID。资金池通过代币合约的 transfer_from 拉入代币一侧的资金,所以要先授权给资金池。资产一侧的资金附带在调用里送来,数额必须恰好等于参数所写的数额。

第一次存入得到的份额为 isqrt(amount_a × amount_b)。之后的存入,按两侧数额与储备之比中较小的那个得到份额,资金池也只收取这个比例所需的数额,资产一侧多出的部分会退还。兑换时资金池留下 0.3% 的手续费:out = in × 997 × R_out / (R_in × 1000 + in × 997),向下取整;out 低于 min_out 时 revert。资金池把储备记在存储里,所以直接转给它的资产不会影响价格。

方法 参数 返回
init (side_a: Side, side_b: Side) ()
add_liquidity (amount_a: u128, amount_b: u128) u128,即份额
remove_liquidity shares: u128 (u128, u128),即 A 和 B 的数额
swap (id_in: [u8; 32], amount_in: u128, min_out: u128) u128,即换出的数额
reserves 无 (u128, u128)
shares_of account: [u8; 32] u128
total_shares 无 u128
tokens 无 (Side, Side)

事件:("add_liquidity", provider, amount_a, amount_b, shares)、("remove_liquidity", provider, shares, amount_a, amount_b) 和 ("swap", trader, id_in, amount_in, amount_out)。

amm-factory

用 create_contract,以链上已有的 amm 代码创建 amm 资金池:必须已经有人部署过一次 amm。init 接受这份代码的哈希,即模块的 SHA-256。

create_pair 把两侧先按 ID、再按种类排序,并以它们的 Borsh 编码的 SHA-256 作为盐值。所以每个交易对只对应一个资金池,它的 ID 可以事先用工厂合约的 ID、盐值和代码哈希算出来。资金池的 init 按这个顺序收到两侧:在 KSA 与某个代币组成的资金池里,KSA 是 A 侧,因为它的 ID 全为零。

方法 参数 返回
init amm_code_hash: [u8; 32] ()
create_pair (side_a: Side, side_b: Side) [u8; 32],即资金池的 ID
get_pair (side_a: Side, side_b: Side),顺序不限 Option<[u8; 32]>
pair_count 无 u64
pair_at index: u64 Option<[u8; 32]>
amm_code_hash 无 [u8; 32]

事件:("pair_created", side_a, side_b, pair)。

btc-payment-trigger

谁证明了一笔付给被监视脚本的比特币付款,就向谁发行本合约自己的资产。init 接受这个脚本、资产的 sub_id、资产的元数据 (name: String, decimals: u8) 以及 units_per_sat。脚本为空、比率为 0,或者元数据超过 1,024 字节时,它会 revert。

claim 对照比特币区块头检查证明,并检查输出 vout 付款给被监视的脚本。这笔交易还必须有一个 OP_RETURN <beneficiary> 输出(6a20 加上 32 字节的账户),这样别人就无法抢先领取这笔付款。每个 (txid, vout) 只能领取一次,换得 value × units_per_sat 的资产,发行给受益账户。

方法 参数 返回
init (watch_script: Vec<u8>, sub_id: [u8; 32], meta, units_per_sat: u128) ()
claim 先是证明,然后是 vout: u32 和 beneficiary: [u8; 32] u128,即发行的数额
asset_id 无 [u8; 32]
is_claimed (txid: [u8; 32], vout: u32) bool

证明依次是 height: u32、raw_tx: Vec<u8>、tx_index: u32、branch: Vec<[u8; 32]>、coinbase_tx: Vec<u8> 和 coinbase_branch: Vec<[u8; 32]>。合约中的比特币解释了每一部分,并从一个区块构造出 claim 的参数。事件是 ("claim", txid, vout, beneficiary, asset_id, amount);资产的 ID 是合约 ID 与 sub_id 的 SHA-256。

ksa-exchange

出售 KSA,换取比特币。充值用的就是它:手上有聪、没有 KSA 的买方,用一笔比特币交易,在比特币上付款给卖方,在 Ksana 上收到 KSA。

  1. 卖方存入 KSA(用 deposit,或者附带在 lock 上),然后调用 lock。这个锁定为买方的账户预留 ksa 个最小单位,价格为每聪 price 个最小单位,付款要付给一个其他锁定从未用过的比特币脚本,有效期到比特币高度 expiry 为止。
  2. 买方发出一笔比特币交易,它向锁定的脚本付款,并携带一个调用 claim(lock) 的路径 C 操作。操作运行时,claim 读取 native_tx(),加总这笔交易付给该脚本的金额,把买到的 KSA 付给买方的账户,以仍处于锁定的 KSA 为限。
  3. 只付了一部分时,剩下的部分继续为同一个买方锁定。锚定高度达到 expiry + 6 之后,卖方可以对一个开放的锁定调用 release,其中仍锁定的 KSA 回到卖方的可用余额;withdraw 从这个余额中取出 KSA。

受益账户在锁定里就已写定,所以无论谁提交领取,KSA 都归买方。操作的签名覆盖交易的第一个输入,所以复制来的领取会失败。付款超出锁定里 KSA 的价值,也买不到更多;向已不再开放的锁定付款,比特币就白白损失了:付款之前,先读取锁定。

方法 参数 返回
init 无,且不附带资产 ()
deposit 无,附带 KSA u128,即可用余额
withdraw amount: u128 u128,即可用余额
lock (ksa: u128, price: u128, script: Vec<u8>, beneficiary: [u8; 32], expiry: u32) u64,即锁定编号
claim lock: u64,只能来自路径 C 操作 (paid_sats: u64, ksa_paid: u128)
release lock: u64 u128,即取回的 KSA
get_lock lock: u64 Lock
lock_of_script script: Vec<u8> Option<u64>
balance seller: [u8; 32] u128,即可用余额
lock_count seller: [u8; 32] u64
locks_of (seller: [u8; 32], start: u64, limit: u32),limit 为 1 到 256 Vec<u64>

Lock 是 (seller, beneficiary, script, price, ksa, expiry, state),其中 ksa 是仍处于锁定的数额。state 为 0(开放)、附带总计 paid_sats: u64 和 ksa_paid: u128 的 1(已领取),或 2(已释放)。锁定的脚本最长 64 字节,且不能以 OP_RETURN 开头。ksana-cli 为卖方提供 exchange 命令,为买方提供 top-up 命令;TypeScript SDK 能为浏览器钱包构造充值。

probe

这是一个测试合约,不是示例:它把宿主函数公开为方法,让节点的执行测试和确定性测试能逐个驱动它们。它的方法包括 send、issue、burn、forward、try_forward、delegate、create、set、get、env、recurse、spin、trap 和 revert。

读它,可以看到单独使用某个宿主函数是什么样子;不要把它部署来实际使用。它的方法表在 contracts/testing/probe/src/lib.rs 的开头。