Recetas probadas

Cada receta es un contrato completo de la carpeta examples/ con un escenario que comprueba su comportamiento, incluidos los fallos. Todos los escenarios se ejecutan en integración continua (cargo test y tccl test examples), así que el código de esta página funciona con esta versión.

Ejecute uno usted mismo:

git clone https://github.com/LucasBolla94/tccl && cd tccl/examples
tccl test counter.scenario

O abra el contrato en el playground: el escenario se carga en la pestaña Escenario.

Contador

Estado, actions, views y eventos — el contrato útil más pequeño. Versión 1 del lenguaje, desplegable hoy en The Coin.

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

Bote de propinas

Una action payable, un retiro solo para el dueño y una view que devuelve varios números. Versión 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 con rol de emisor

El módulo de token estándar más un rol. transfer, approve, transfer_from y las views vienen de std.token; el contrato solo decide quién puede 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 con records, transiciones y roles

Un record Order tipado, un enum Status cuyas transiciones permitidas impiden cambios imposibles (un pedido enviado no se puede cancelar) y dos roles.

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 intercambio

Un exchange de producto constante (DEX) para dos contratos de token con una comisión del 0,30 %. Muestra interfaces, caller dentro de otro contrato (los operadores aprueban al pool), mul_div e isqrt, protección contra deslizamiento con min_out y atomicidad: un swap sin autorización suficiente falla en el token y revierte la actualización de reservas del 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]

Los exchanges reales también necesitan protección contra la manipulación del precio dentro de un bloque cuando otros contratos usan el precio del pool. No use quote como oráculo de precios.

Juego de cara o cruz

Un juego para dos jugadores sin aleatoriedad falsa: el anfitrión se compromete con una elección oculta, el invitado adivina y el anfitrión revela. Un anfitrión que se niega a revelar pierde después de 20 bloques. Vea Aleatoriedad.

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"

Entradas como objetos únicos

Entradas emitidas por un organizador con std.items, capacidad limitada, transferencias y control de acceso (quemando la entrada).

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

Pagos condicionales

Todas las actions vienen de std.payments: un pago bloqueado por hash que se libera cuando alguien revela el secreto, y un pago que el comprador puede recuperar después de un plazo.

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

Actualizar un contrato

El contador de arriba, actualizado a una segunda versión por su autoridad de actualización. La nueva variable de estado se inicializa en upgrade(), los valores antiguos se conservan, los extraños no pueden actualizar y un contrato final nunca vuelve a cambiar.

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

Depósito en garantía con árbitro

Roles expresados con require, plazos, una disputa y destroy. Versión 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

Más recetas

Estos contratos de la versión 1 se prueban en crates/tccl/tests/examples.rs:

ContratoMuestra
shop.tcclTodo tipo de declaración, payable, auxiliares
token.tcclUn token escrito a mano: maps, autorizaciones, claves compuestas
crowdfund.tcclPlazos y reembolsos
poll.tcclArgumentos lista, listas de estado, bucles acotados
savings.tcclBloqueos por tiempo y reembolsos de almacenamiento
treasury.tcclAprobaciones M de N
names.tcclClaves de texto, validación, vencimiento
private_pool.tcclFirmas en anillo para pagos privados (vea Privacidad)

Mejore esta página en GitHub