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 quote como 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:

ContratoMostra
shop.tcclTodo tipo de declaração, payable, auxiliares
token.tcclUm token escrito à mão: maps, autorizações, chaves compostas
crowdfund.tcclPrazos e reembolsos
poll.tcclArgumentos lista, listas de estado, laços limitados
savings.tcclTravas de tempo e reembolso de armazenamento
treasury.tcclAprovações M de N
names.tcclChaves de texto, validação, expiração
private_pool.tcclAssinaturas em anel para pagamentos privados (veja Privacidade)

Melhore esta página no GitHub