Receitas testadas
Cada receita é um contrato completo da pasta examples/ com um cenário que confere o comportamento, inclusive as falhas. Todos os cenários rodam na integração contínua (cargo test e tccl test examples), então o código desta página funciona com esta versão.
Rode você mesmo:
git clone https://github.com/LucasBolla94/tccl && cd tccl/examples
tccl test counter.scenario
Ou abra o contrato no playground: o cenário aparece na aba Cenário.
Contador
Estado, actions, views e eventos — o menor contrato útil. Versão 1 da linguagem, publicável na The Coin hoje.
counter.tccl# The smallest useful contract: a counter anyone can increase.
contract Counter
state count: int
state last_caller: address
event Increased(by: address, amount: int, total: int)
action increment(amount: int):
require amount > 0, "amount must be positive"
require amount <= 100, "at most 100 per call"
count += amount
last_caller = caller
emit Increased(caller, amount, count)
view get() -> int:
return count
view last() -> address:
return last_caller
counter.scenario# Recipe: a counter anyone can increase (state, actions, views, events).
deploy counter.tccl as counter --from alice
call counter increment 5 --from bob
expect ok
expect event Increased
view counter get
expect result 5
view counter last
expect result @bob
call counter increment 500
expect fail "at most 100"
call counter increment 1 --value 1tcn
expect fail "not payable"
view counter get
expect result 5
Pote de gorjetas
Uma action payable, saque só do dono e uma view que devolve vários números. Versão 1.
tip_jar.tccl# A tip jar: anyone can send TCN with a message, only the owner withdraws.
contract TipJar
state owner: address
state total_received: int
state tips: int
event Tip(from: address, amount: int, message: text)
event Withdrawn(to: address, amount: int)
init():
owner = caller
action tip(message: text) payable:
require value >= TCN / 100, "minimum tip is 0.01 TCN"
require len(message) <= 140, "message too long"
total_received += value
tips += 1
emit Tip(caller, value, message)
action withdraw(amount: int):
require caller == owner, "only the owner can withdraw"
require amount > 0 and amount <= balance, "invalid amount"
send(owner, amount)
emit Withdrawn(owner, amount)
view stats() -> list[int]:
return [total_received, tips, balance]
tip_jar.scenario# Recipe: a tip jar (payable action, owner-only withdrawal).
deploy tip_jar.tccl as jar --from owner
call jar tip "great work" --from fan --value 3tcn
expect ok
expect balance jar 3tcn
call jar withdraw 1tcn --from fan
expect fail
call jar withdraw 2tcn --from owner
expect ok
expect balance @owner 1000002tcn
expect balance jar 1tcn
Token com papel de emissor
O módulo de token padrão mais um papel. transfer, approve, transfer_from e as views vêm do std.token; o contrato só decide quem pode emitir.
cloud_coin.tccl# A fungible token in a few lines: the standard token module plus a minter role.
contract CloudCoin
use std.token
role minter
init(supply: int):
token.setup("Cloud Coin", "CLD", 8)
grant minter to caller
token.mint(caller, supply)
action mint(to: address, amount: int) only minter:
token.mint(to, amount)
action add_minter(who: address) only minter:
grant minter to who
action burn(amount: int):
token.burn(caller, amount)
cloud_coin.scenario# Recipe: a token with the standard module and a minter role.
deploy cloud_coin.tccl as coin 1000 --from issuer
view coin balance_of @issuer
expect result 1000
call coin mint @mallory 5 --from mallory
expect fail "only minter"
call coin add_minter @bob --from issuer
expect event RoleGranted
call coin mint @carol 7 --from bob
expect ok
call coin transfer @dave 3 --from carol
expect event Transfer
call coin transfer @dave 5 --from carol
expect fail "insufficient token balance"
call coin approve @erin 2 --from dave
call coin transfer_from @dave @erin 2 --from erin
expect ok
view coin balance_of @erin
expect result 2
view coin total_supply_of
expect result 1007
Pedidos com records, transições e papéis
Um record Order tipado, um enum Status cujas transições permitidas impedem mudanças impossíveis (um pedido enviado não pode ser cancelado) e dois papéis.
orders.tccl# A small store: typed orders, a status with allowed transitions and staff roles.
contract Orders
enum Status:
Placed -> Paid, Cancelled
Paid -> Shipped, Refunded
Shipped -> Delivered
Delivered
Cancelled
Refunded
record Order:
buyer: address
item: text
price: int
status: Status
role manager
role shipper
state orders: map[int, Order]
state next_id: int
state prices: map[text, int]
event StatusChanged(id: int, status: text)
init():
grant manager to caller
action set_price(item: text, price: int) only manager:
require len(item) >= 1 and len(item) <= 64, "item names have 1 to 64 bytes"
require price >= 0, "price cannot be negative"
prices[item] = price
action hire_shipper(who: address) only manager:
grant shipper to who
action fire_shipper(who: address) only manager:
revoke shipper from who
action place(item: text) -> int:
require prices.has(item), "unknown item"
next_id += 1
orders[next_id] = Order(buyer: caller, item: item, price: prices[item], status: Status.Placed)
changed(next_id)
return next_id
action pay(id: int) payable:
require orders.has(id), "unknown order"
require caller == orders[id].buyer, "only the buyer pays"
require value == orders[id].price, "wrong amount"
orders[id].status = Status.Paid
changed(id)
action ship(id: int) only shipper:
orders[id].status = Status.Shipped
changed(id)
action confirm(id: int):
require caller == orders[id].buyer, "only the buyer confirms"
orders[id].status = Status.Delivered
changed(id)
action cancel(id: int):
require caller == orders[id].buyer, "only the buyer cancels"
orders[id].status = Status.Cancelled
changed(id)
action refund(id: int) only manager:
let o: Order = orders[id]
orders[id].status = Status.Refunded
send(o.buyer, o.price)
changed(id)
fn changed(id: int):
emit StatusChanged(id, to_text(orders[id].status))
view order(id: int) -> Order:
return orders[id]
orders.scenario# Recipe: typed records, a status with allowed transitions, staff roles.
deploy orders.tccl as shop --from boss
call shop set_price "lamp" 5tcn --from boss
call shop hire_shipper @sam --from boss
call shop place "lamp" --from ann
expect result 1
call shop confirm 1 --from ann
expect fail "Placed to Delivered"
call shop pay 1 --from ann --value 5tcn
expect ok
call shop ship 1 --from ann
expect fail "only shipper"
call shop ship 1 --from sam
expect event StatusChanged
call shop cancel 1 --from ann
expect fail "Shipped to Cancelled"
call shop confirm 1 --from ann
expect ok
view shop order 1
expect result {buyer: @ann, item: "lamp", price: 5tcn, status: Delivered}
Pool de troca
Uma corretora de produto constante (DEX) para dois contratos de token, com taxa de 0,30 %. Mostra interfaces, caller dentro de outro contrato (os negociantes aprovam o pool), mul_div e isqrt, proteção contra variação de preço com min_out e atomicidade: um swap sem autorização suficiente falha no token e reverte a atualização das reservas do pool.
pool.tccl# Constant-product exchange pool (x × y = k) for two token contracts, 0.30 % fee.
#
# Calls other contracts through an interface. Inside the token, `caller` is this
# pool, so traders first `approve` the pool on each token. Every call is atomic:
# if a transfer fails, the whole swap is reverted.
contract Pool
interface Token:
action transfer(to: address, amount: int) -> bool
action transfer_from(from: address, to: address, amount: int) -> bool
view balance_of(who: address) -> int
const FEE_BP: int = 30
const BP: int = 10_000
state token_a: address
state token_b: address
state reserve_a: int
state reserve_b: int
state total_shares: int
state shares: map[address, int]
event Added(provider: address, amount_a: int, amount_b: int, minted: int)
event Removed(provider: address, amount_a: int, amount_b: int, burned: int)
event Swapped(trader: address, sell_a: bool, amount_in: int, amount_out: int)
init(a: address, b: address):
require a != b, "the two tokens must differ"
require is_contract(a) and is_contract(b), "both tokens must be contracts"
token_a = a
token_b = b
action add_liquidity(amount_a: int, amount_b: int) -> int:
require amount_a > 0 and amount_b > 0, "amounts must be positive"
let minted: int = 0
if total_shares == 0:
minted = isqrt(amount_a) * isqrt(amount_b)
else:
minted = min(mul_div(amount_a, total_shares, reserve_a), mul_div(amount_b, total_shares, reserve_b))
require minted > 0, "liquidity too small"
# Effects first, then calls to other contracts.
reserve_a += amount_a
reserve_b += amount_b
total_shares += minted
shares[caller] += minted
require Token(token_a).transfer_from(caller, self, amount_a), "token A transfer failed"
require Token(token_b).transfer_from(caller, self, amount_b), "token B transfer failed"
emit Added(caller, amount_a, amount_b, minted)
return minted
action remove_liquidity(burn: int):
require burn > 0 and burn <= shares[caller], "invalid share amount"
let out_a: int = mul_div(burn, reserve_a, total_shares)
let out_b: int = mul_div(burn, reserve_b, total_shares)
shares[caller] -= burn
total_shares -= burn
reserve_a -= out_a
reserve_b -= out_b
require Token(token_a).transfer(caller, out_a), "token A transfer failed"
require Token(token_b).transfer(caller, out_b), "token B transfer failed"
emit Removed(caller, out_a, out_b, burn)
view quote(sell_a: bool, amount_in: int) -> int:
return output_for(sell_a, amount_in)
fn output_for(sell_a: bool, amount_in: int) -> int:
require amount_in > 0, "amount must be positive"
require reserve_a > 0 and reserve_b > 0, "the pool is empty"
let with_fee: int = amount_in * (BP - FEE_BP)
if sell_a:
return mul_div(with_fee, reserve_b, reserve_a * BP + with_fee)
return mul_div(with_fee, reserve_a, reserve_b * BP + with_fee)
action swap(sell_a: bool, amount_in: int, min_out: int) -> int:
let out: int = output_for(sell_a, amount_in)
require out > 0, "output too small"
require out >= min_out, "price moved: output below min_out"
if sell_a:
reserve_a += amount_in
reserve_b -= out
require Token(token_a).transfer_from(caller, self, amount_in), "payment failed"
require Token(token_b).transfer(caller, out), "payout failed"
else:
reserve_b += amount_in
reserve_a -= out
require Token(token_b).transfer_from(caller, self, amount_in), "payment failed"
require Token(token_a).transfer(caller, out), "payout failed"
emit Swapped(caller, sell_a, amount_in, out)
return out
view reserves() -> list[int]:
return [reserve_a, reserve_b, total_shares]
pool.scenario# Recipe: a constant-product exchange (DEX) calling two token contracts.
deploy cloud_coin.tccl as alpha 1000000 --from lp
deploy cloud_coin.tccl as beta 1000000 --from lp
deploy pool.tccl as pool $alpha $beta --from lp
call alpha approve $pool 100000 --from lp
call beta approve $pool 400000 --from lp
call pool add_liquidity 100000 400000 --from lp
expect ok
expect event Added
call alpha transfer @trader 10000 --from lp
call alpha approve $pool 10000 --from trader
view pool quote true 10000
expect result 36264
call pool swap true 10000 40000 --from trader
expect fail "min_out"
call pool swap true 10000 36000 --from trader
expect result 36264
expect event Swapped
view beta balance_of @trader
expect result 36264
call pool swap true 1000 1 --from trader
expect fail "allowance too small"
view pool reserves
expect result [110000, 363736, 199712]
Corretoras reais também precisam de proteção contra manipulação de preço dentro de um bloco quando outros contratos usam o preço do pool. Não use
quotecomo oráculo de preço.
Jogo de cara ou coroa
Um jogo para dois jogadores sem aleatoriedade falsa: o anfitrião se compromete com uma escolha escondida, o convidado tenta adivinhar e o anfitrião revela. Um anfitrião que se recusa a revelar perde depois de 20 blocos. Veja Aleatoriedade.
coin_flip.tccl# Two-player coin flip with commit–reveal.
#
# Nothing on a blockchain is random: block data can be predicted or influenced.
# The host commits to a hidden choice (sha256 of a 32-byte secret and the choice),
# the guest guesses in public, then the host reveals. A host who refuses to reveal
# loses after REVEAL_BLOCKS.
contract CoinFlip
const REVEAL_BLOCKS: int = 20
enum Stage:
Open -> Joined, Cancelled
Joined -> Settled, Forfeited
Settled
Cancelled
Forfeited
record Game:
host: address
guest: address
stake: int
commitment: bytes
guess: bool
deadline: int
stage: Stage
state games: map[int, Game]
state count: int
event Created(id: int, host: address, stake: int)
event Joined(id: int, guest: address, guess: bool)
event Won(id: int, winner: address, prize: int, how: text)
action create(commitment: bytes) payable -> int:
require value > 0, "attach the stake"
require len(commitment) == 32, "commitment must be a sha256 hash"
count += 1
games[count] = Game(host: caller, guest: zero_address(), stake: value, commitment: commitment, guess: false, deadline: 0, stage: Stage.Open)
emit Created(count, caller, value)
return count
action join(id: int, guess: bool) payable:
require games.has(id), "unknown game"
let g: Game = games[id]
require g.stage == Stage.Open, "game is not open"
require caller != g.host, "the host cannot join"
require value == g.stake, "stake must match"
games[id].guest = caller
games[id].guess = guess
games[id].deadline = height + REVEAL_BLOCKS
games[id].stage = Stage.Joined
emit Joined(id, caller, guess)
action reveal(id: int, secret: bytes, choice: bool):
let g: Game = games[id]
require g.stage == Stage.Joined, "nothing to reveal"
require caller == g.host, "only the host reveals"
require len(secret) == 32, "secret must have 32 bytes"
require sha256(secret + to_bytes(choice)) == g.commitment, "secret does not match the commitment"
let winner: address = g.host
if g.guess == choice:
winner = g.guest
games[id].stage = Stage.Settled
send(winner, g.stake * 2)
emit Won(id, winner, g.stake * 2, "reveal")
action claim_timeout(id: int):
let g: Game = games[id]
require g.stage == Stage.Joined, "game is not waiting for a reveal"
require height > g.deadline, "the host can still reveal"
games[id].stage = Stage.Forfeited
send(g.guest, g.stake * 2)
emit Won(id, g.guest, g.stake * 2, "timeout")
action cancel(id: int):
let g: Game = games[id]
require caller == g.host, "only the host can cancel"
games[id].stage = Stage.Cancelled
send(g.host, g.stake)
view stage_of(id: int) -> text:
return to_text(games[id].stage)
coin_flip.scenario# Recipe: a two-player game with commit-reveal (no fake randomness).
# The host picked `true` and a secret of 32 bytes 0x07:
# commitment = sha256(0x0707…07 ++ 0x01)
deploy coin_flip.tccl as game --from house
call game create 0x842ef3c3b4e4a5b477257cff946cdaf69fbe8c739395f2b2e092039b830eb690 --from host --value 2tcn
expect result 1
call game join 1 true --from guest --value 1tcn
expect fail "stake must match"
call game join 1 true --from guest --value 2tcn
expect event Joined
call game reveal 1 0x0707070707070707070707070707070707070707070707070707070707070707 false --from host
expect fail "does not match the commitment"
call game reveal 1 0x0707070707070707070707070707070707070707070707070707070707070707 true --from host
expect event Won
expect balance @guest 1000002tcn
view game stage_of 1
expect result "Settled"
# A host who never reveals loses after 20 blocks.
call game create 0x842ef3c3b4e4a5b477257cff946cdaf69fbe8c739395f2b2e092039b830eb690 --from host --value 1tcn
call game join 2 false --from guest --value 1tcn
call game claim_timeout 2 --from guest
expect fail "can still reveal"
advance 21
call game claim_timeout 2 --from guest
expect ok
view game stage_of 2
expect result "Forfeited"
Ingressos como itens únicos
Ingressos emitidos por um organizador com std.items, capacidade limitada, transferências e check-in (queimando o ingresso).
tickets.tccl# Event tickets as unique items (std.items). The organizer issues them; holders
# can transfer or approve someone else (e.g. a resale contract).
contract Tickets
use std.items
state organizer: address
state capacity: int
init(seats: int):
require seats > 0, "capacity must be positive"
organizer = caller
capacity = seats
action issue(to: address, seat: text) -> int only organizer:
require items.count < capacity, "sold out"
return items.create(to, "ticket", seat)
action check_in(id: int) only organizer:
items.burn_item(id)
tickets.scenario# Recipe: tickets as unique items (std.items).
deploy tickets.tccl as tickets 2 --from org
call tickets issue @ann "A1" --from org
expect result 1
call tickets issue @ben "A2" --from org
call tickets issue @cat "A3" --from org
expect fail "sold out"
call tickets issue @cat "A3" --from ann
expect fail "only organizer"
call tickets transfer_item 1 @cat --from ann
expect event ItemTransferred
view tickets owner_of 1
expect result @cat
call tickets check_in 1 --from org
expect event ItemBurned
Pagamentos condicionais
Todas as actions vêm do std.payments: um pagamento com trava de hash liberado por quem revelar o segredo, e um pagamento que o comprador pode recuperar depois de um prazo.
deals.tccl# Conditional payments (escrow with an arbiter, time locks and hash locks) using
# the standard payments module. All actions come from std.payments.
contract Deals
use std.payments
view about() -> text:
return "conditional payments: release, claim after a height, reveal a secret, refund"
deals.scenario# Recipe: conditional payments — a hash lock, an arbiter and a refund deadline.
deploy deals.tccl as deals --from anyone
# hashlock = sha256("open sesame"); whoever reveals the secret pays the payee.
call deals create_payment @seller @judge 0 0 0x41ef4bb0b23661e66301aac36066912dac037827b4ae63a7b1165a5aa93ed4eb --from buyer --value 5tcn
expect result 1
call deals reveal_payment 1 0x6775657373 --from courier
expect fail "wrong secret"
call deals reveal_payment 1 0x6f70656e20736573616d65 --from courier
expect event PaymentReleased
expect balance @seller 1000005tcn
call deals refund_payment 1 --from buyer
expect fail "no longer pending"
# No hash lock, refundable by the buyer after height 50.
call deals create_payment @seller @judge 0 50 0x --from buyer --value 3tcn
expect result 2
call deals refund_payment 2 --from buyer
expect fail "not allowed yet"
height 60
call deals refund_payment 2 --from buyer
expect event PaymentRefunded
view deals payment_status 2
expect result "Refunded"
view deals locked_total
expect result 0
Atualizar um contrato
O contador acima, atualizado para uma segunda versão pela sua autoridade de upgrade. A nova variável de estado é inicializada por upgrade(), os valores antigos são mantidos, estranhos não conseguem atualizar e um contrato final nunca mais muda.
counter_v2.tccl# Second version of counter.tccl, installed with an upgrade by the upgrade authority.
# Existing state (count, last_caller) is kept; a new variable is added at the end.
contract Counter
state count: int
state last_caller: address
state step: int
event Increased(by: address, amount: int, total: int)
upgrade():
step = 10
action increment(amount: int):
require amount > 0, "amount must be positive"
require amount <= 100, "at most 100 per call"
count += amount * step
last_caller = caller
emit Increased(caller, amount * step, count)
view get() -> int:
return count
view last() -> address:
return last_caller
upgrade.scenario# Recipe: upgrading a contract with its upgrade authority (the deployer).
deploy counter.tccl as counter --from dev
call counter increment 5 --from bob
upgrade counter counter_v2.tccl --from bob
expect fail
upgrade counter counter_v2.tccl --from dev
expect ok
call counter increment 2 --from bob
view counter get
expect result 25
expect state counter step 10
authority counter none --from dev
upgrade counter counter_v2.tccl --from dev
expect fail
Escrow com árbitro
Papéis expressos com require, prazos, uma disputa e destroy. Versão 1.
escrow.tccl# Escrow with an arbiter.
#
# The buyer deploys the contract with the payment attached. The buyer releases
# the money when the goods arrive; if buyer and seller disagree, either can
# open a dispute and the arbiter decides. If nobody acts before the deadline,
# the buyer can take the money back.
contract Escrow
const MIN_DURATION: int = 60 # about 1 hour (1 block = 1 minute)
const MAX_DURATION: int = 525_600 # about 1 year
state buyer: address
state seller: address
state arbiter: address
state amount: int
state deadline: int
state disputed: bool
state settled: bool
event Funded(buyer: address, seller: address, amount: int, deadline: int)
event Disputed(by: address)
event Settled(to: address, amount: int)
init(seller_address: address, arbiter_address: address, duration_blocks: int) payable:
require value > 0, "attach the payment with --value"
require seller_address != caller, "buyer and seller must be different"
require arbiter_address != caller, "the arbiter must be a third party"
require arbiter_address != seller_address, "the arbiter must be a third party"
require duration_blocks >= MIN_DURATION, "duration too short (min 60 blocks)"
require duration_blocks <= MAX_DURATION, "duration too long (max 525600 blocks)"
buyer = caller
seller = seller_address
arbiter = arbiter_address
amount = value
deadline = height + duration_blocks
emit Funded(caller, seller_address, value, deadline)
# The buyer is happy: pay the seller.
action release():
require caller == buyer, "only the buyer can release"
pay(seller)
# The seller cannot deliver: give the money back.
action cancel():
require caller == seller, "only the seller can cancel"
pay(buyer)
action dispute():
require caller == buyer or caller == seller, "only the buyer or the seller"
require not settled, "already settled"
require not disputed, "already disputed"
disputed = true
emit Disputed(caller)
action resolve(pay_seller: bool):
require caller == arbiter, "only the arbiter"
require disputed, "there is no dispute"
if pay_seller:
pay(seller)
else:
pay(buyer)
action reclaim():
require caller == buyer, "only the buyer"
require height > deadline, "the deadline has not passed"
require not disputed, "a dispute is open: the arbiter decides"
pay(buyer)
# After settlement the buyer removes the contract and recovers its storage deposit.
action close():
require caller == buyer, "only the buyer"
require settled, "settle the escrow first"
destroy(buyer)
view status() -> text:
if settled:
return "settled"
elif disputed:
return "disputed"
elif height > deadline:
return "expired"
return "open"
view locked() -> int:
if settled:
return 0
return amount
fn pay(to: address):
require not settled, "already settled"
settled = true
send(to, amount)
emit Settled(to, amount)
escrow.scenario# Recipe (language 1 contract): escrow with an arbiter.
deploy escrow.tccl as deal @seller @arbiter 100 --from buyer --value 10tcn
call deal release --from seller
expect fail "only the buyer"
call deal dispute --from seller
expect event Disputed
call deal resolve true --from arbiter
expect event Settled
expect balance @seller 1000010tcn
Mais receitas
Estes contratos da versão 1 são testados em crates/tccl/tests/examples.rs:
| Contrato | Mostra |
|---|---|
shop.tccl | Todo tipo de declaração, payable, auxiliares |
token.tccl | Um token escrito à mão: maps, autorizações, chaves compostas |
crowdfund.tccl | Prazos e reembolsos |
poll.tccl | Argumentos lista, listas de estado, laços limitados |
savings.tccl | Travas de tempo e reembolso de armazenamento |
treasury.tccl | Aprovações M de N |
names.tccl | Chaves de texto, validação, expiração |
private_pool.tccl | Assinaturas em anel para pagamentos privados (veja Privacidade) |