Build
Example contracts
The repository ships five example contracts, from a token to a market that sells KSA for bitcoin, and one test contract. Each is a tested model to read, build and deploy.
The contracts live in contracts/ of the repository, a Cargo workspace of their own. The comment at the top of each crate’s src/lib.rs documents every method, event and rule; this page summarizes them.
| Contract | What it shows | Directory |
|---|---|---|
token |
storage, events, allowances | contracts/examples/token |
amm |
calls to other contracts, attached funds | contracts/examples/amm |
amm-factory |
contracts that create contracts | contracts/examples/amm-factory |
btc-payment-trigger |
Bitcoin headers and payment proofs | contracts/examples/btc-payment-trigger |
ksa-exchange |
native_tx and path C |
contracts/examples/ksa-exchange |
probe |
every host function, for tests | contracts/testing/probe |
Build and test them
The native tests run each contract against the SDK’s mock host, with no node. Run them from the workspace:
git clone https://github.com/ksana-in-bitcoin/Ksana
cd Ksana/contracts
cargo test --workspaceBuild a module from inside contracts/, as a cdylib alone, so that contracts/.cargo/config.toml applies and the module stays small:
cargo rustc --release --locked --target wasm32-unknown-unknown \
--package ksa-exchange --crate-type cdylibThe module lands in contracts/target/wasm32-unknown-unknown/release/, named after the package with underscores: ksa_exchange.wasm, amm_factory.wasm. Its bytes depend on the absolute path of your checkout, so build the file you deploy once and compute IDs from that file.
Deploy one
Deploy with ksana-cli deploy, which takes the module and the Borsh bytes of the init arguments in hex. The output does not give the new contract’s ID: compute it from your account, the deploy op’s nonce and the SHA-256 of the module, with the contract_id helper of the token and pool tutorial, and your account in ACCOUNT as there. ksa-exchange takes no init arguments:
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)The tutorial deploys token, amm and amm-factory on the public signet, with real output. Mainnet has not launched, so deploy on the signet.
token
A fungible token whose balances live in the contract’s storage. It is not a built-in asset like KSA: the node’s balance and asset methods do not see it, and you move it by calling the contract. init gives the whole supply to the deployer.
To let a contract such as a pool move your tokens, call approve(spender, amount); the spender then calls transfer_from. approve sets the allowance to amount; it does not add to it. A transfer without the balance or the allowance reverts with insufficient balance or insufficient allowance.
| Method | Arguments | Returns |
|---|---|---|
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 |
none | u128 |
metadata |
none | (name, symbol, decimals) |
Events: ("transfer", from, to, amount), with from all zeros for the supply that init creates, and ("approve", owner, spender, amount).
amm
A constant-product pool of two sides. A side is a token contract or a built-in asset such as KSA, encoded as one byte (0 token, 1 asset) and the 32-byte ID. The pool pulls a token side in with the token’s transfer_from, so approve the pool first. An asset side arrives attached to the call, in exactly the amount the arguments name.
The first deposit gets isqrt(amount_a × amount_b) shares. Later deposits get shares at the smaller of the two ratios of amount to reserve, and the pool takes only the amounts that ratio needs, refunding the excess of an asset side. A swap keeps a 0.3% fee: out = in × 997 × R_out / (R_in × 1000 + in × 997), rounded down, and it reverts when out is below min_out. The pool keeps its reserves in storage, so assets sent to it directly do not move the price.
| Method | Arguments | Returns |
|---|---|---|
init |
(side_a: Side, side_b: Side) |
() |
add_liquidity |
(amount_a: u128, amount_b: u128) |
u128, the shares |
remove_liquidity |
shares: u128 |
(u128, u128), the amounts of A and B |
swap |
(id_in: [u8; 32], amount_in: u128, min_out: u128) |
u128, the amount out |
reserves |
none | (u128, u128) |
shares_of |
account: [u8; 32] |
u128 |
total_shares |
none | u128 |
tokens |
none | (Side, Side) |
Events: ("add_liquidity", provider, amount_a, amount_b, shares), ("remove_liquidity", provider, shares, amount_a, amount_b) and ("swap", trader, id_in, amount_in, amount_out).
amm-factory
Creates amm pools with create_contract, from the amm code already on chain: someone must have deployed amm once. init takes the code’s hash, the SHA-256 of the module.
create_pair sorts the two sides by ID, then by kind, and uses the SHA-256 of their Borsh encoding as the salt. So each pair of sides gets one pool, at an ID you can compute in advance from the factory’s ID, the salt and the code hash. The pool’s init receives the sides in that order: in a pool of KSA and a token, KSA is side A, because its ID is all zeros.
| Method | Arguments | Returns |
|---|---|---|
init |
amm_code_hash: [u8; 32] |
() |
create_pair |
(side_a: Side, side_b: Side) |
[u8; 32], the pool’s ID |
get_pair |
(side_a: Side, side_b: Side), in either order |
Option<[u8; 32]> |
pair_count |
none | u64 |
pair_at |
index: u64 |
Option<[u8; 32]> |
amm_code_hash |
none | [u8; 32] |
Event: ("pair_created", side_a, side_b, pair).
btc-payment-trigger
Issues the contract’s own asset to whoever proves a Bitcoin payment to a watched script. init takes the script, the asset’s sub_id, its metadata (name: String, decimals: u8) and units_per_sat. It reverts on an empty script, a rate of 0 or metadata over 1,024 bytes.
claim checks the proof against a Bitcoin header and checks that output vout pays the watched script. The transaction must also have an output OP_RETURN <beneficiary> (6a20 and the 32-byte account), so nobody else can claim the payment first. Each (txid, vout) is claimed once, for value × units_per_sat of the asset, issued to the beneficiary.
| Method | Arguments | Returns |
|---|---|---|
init |
(watch_script: Vec<u8>, sub_id: [u8; 32], meta, units_per_sat: u128) |
() |
claim |
the proof, then vout: u32 and beneficiary: [u8; 32] |
u128, the amount issued |
asset_id |
none | [u8; 32] |
is_claimed |
(txid: [u8; 32], vout: u32) |
bool |
The proof is height: u32, raw_tx: Vec<u8>, tx_index: u32, branch: Vec<[u8; 32]>, coinbase_tx: Vec<u8> and coinbase_branch: Vec<[u8; 32]>, in that order. Bitcoin in contracts explains each part and builds the arguments of claim from a block. The event is ("claim", txid, vout, beneficiary, asset_id, amount), and the asset’s ID is the SHA-256 of the contract’s ID and sub_id.
ksa-exchange
Sells KSA for bitcoin. Top-ups use it: a buyer with sats and no KSA pays a seller on Bitcoin and receives KSA on Ksana, in one Bitcoin transaction.
- The seller deposits KSA, with
depositor attached tolock, and callslock. The lock reservesksabase units for the buyer’s account atpricebase units per sat, payable to a Bitcoin script no other lock has used, until Bitcoin heightexpiry. - The buyer sends one Bitcoin transaction that pays the lock’s script and carries a path C op calling
claim(lock). When the op runs,claimreadsnative_tx(), adds up what the transaction pays to the script, and pays the KSA it buys to the buyer’s account, at most the KSA still locked. - A partial payment leaves the rest locked for the same buyer. Once the anchor reaches
expiry+ 6, the seller canreleasean open lock, and the KSA still in it returns to the seller’s free balance;withdrawtakes KSA out of that balance.
The beneficiary is fixed in the lock, so whoever submits a claim, the KSA goes to the buyer. The op’s signature covers the transaction’s first input, so a copied claim fails. Paying more than the lock holds buys nothing more, and paying a lock that is no longer open loses the bitcoin: read the lock before you pay.
| Method | Arguments | Returns |
|---|---|---|
init |
none, with no funds | () |
deposit |
none, with KSA attached | u128, the free balance |
withdraw |
amount: u128 |
u128, the free balance |
lock |
(ksa: u128, price: u128, script: Vec<u8>, beneficiary: [u8; 32], expiry: u32) |
u64, the lock’s number |
claim |
lock: u64, from a path C op only |
(paid_sats: u64, ksa_paid: u128) |
release |
lock: u64 |
u128, the KSA taken back |
get_lock |
lock: u64 |
Lock |
lock_of_script |
script: Vec<u8> |
Option<u64> |
balance |
seller: [u8; 32] |
u128, the free balance |
lock_count |
seller: [u8; 32] |
u64 |
locks_of |
(seller: [u8; 32], start: u64, limit: u32), limit 1 to 256 |
Vec<u64> |
A Lock is (seller, beneficiary, script, price, ksa, expiry, state), where ksa is what is still locked. state is 0 (open), 1 with the totals paid_sats: u64 and ksa_paid: u128 (claimed), or 2 (released). A lock’s script is at most 64 bytes and cannot start with OP_RETURN. ksana-cli has exchange commands for sellers and a top-up command for buyers, and the TypeScript SDK builds a top-up for a browser wallet.
probe
A test contract, not an example: it exposes the host functions as methods, so that the node’s execution and determinism tests can drive each one. Its methods include send, issue, burn, forward, try_forward, delegate, create, set, get, env, recurse, spin, trap and revert.
Read it to see a host function used in isolation, and do not deploy it for real use. Its method table is at the top of contracts/testing/probe/src/lib.rs.