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
quotecomo 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:
| Contrato | Muestra |
|---|---|
shop.tccl | Todo tipo de declaración, payable, auxiliares |
token.tccl | Un token escrito a mano: maps, autorizaciones, claves compuestas |
crowdfund.tccl | Plazos y reembolsos |
poll.tccl | Argumentos lista, listas de estado, bucles acotados |
savings.tccl | Bloqueos por tiempo y reembolsos de almacenamiento |
treasury.tccl | Aprobaciones M de N |
names.tccl | Claves de texto, validación, vencimiento |
private_pool.tccl | Firmas en anillo para pagos privados (vea Privacidad) |