Ksana
中文

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.

On this page

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:

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

Build a module from inside contracts/, as a cdylib alone, so that contracts/.cargo/config.toml applies and the module stays small:

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

The 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:

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)

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.

  1. The seller deposits KSA, with deposit or attached to lock, and calls lock. The lock reserves ksa base units for the buyer’s account at price base units per sat, payable to a Bitcoin script no other lock has used, until Bitcoin height expiry.
  2. 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, claim reads native_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.
  3. A partial payment leaves the rest locked for the same buyer. Once the anchor reaches expiry + 6, the seller can release an open lock, and the KSA still in it returns to the seller’s free balance; withdraw takes 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.