Xây dựng Hợp đồng Escrow hoàn chỉnh
Bài học này hướng dẫn thiết kế và triển khai một hợp đồng Escrow hoàn chỉnh - ứng dụng thực tế của smart contract trên Cardano.
Mục tiêu học tập
- Hiểu khái niệm và use cases của Escrow
- Thiết kế Datum và Redeemer cho các scenarios
- Xử lý disputes và refunds
- Viết tests toàn diện
- Best practices cho production contracts
Escrow là gì?
┌─────────────────────────────────────────────────────────────┐
│ ESCROW CONCEPT │
├─────────────────────────────────────────────────────────────┤
│ │
│ Escrow = Bên thứ 3 giữ tiền cho đến khi điều kiện │
│ được đáp ứng │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ BUYER ──────▶ ESCROW ──────▶ SELLER │ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Gửi tiền Giữ │ Giao hàng │ │
│ │ tiền│ │ │
│ │ │ │ │
│ │ ┌──────┴──────┐ │ │
│ │ ▼ ▼ │ │
│ │ Confirm Dispute │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ → Seller Arbitration │ │
│ │ gets $ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Use cases: │
│ • P2P Trading │
│ • Freelance payments │
│ • Real estate deposits │
│ • Online marketplace │
│ │
└─────────────────────────────────────────────────────────────┘
Thiết kế Contract
Flow diagram
┌─────────────────────────────────────────────────────────────┐
│ ESCROW FLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CREATE │ │
│ │ Seller ──▶ Lock ADA with Datum │ │
│ │ • Buyer PKH │ │
│ │ • Price │ │
│ │ • Deadline │ │
│ └────────────────────────┬────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ACTIVE STATE │ │
│ │ │ │
│ │ 4 possible outcomes: │ │
│ │ │ │
│ │ 1. COMPLETE (Buyer confirms) │ │
│ │ → Seller gets payment │ │
│ │ │ │
│ │ 2. CANCEL (Seller cancels before deadline) │ │
│ │ → Seller gets refund │ │
│ │ │ │
│ │ 3. REFUND (Buyer claims after deadline) │ │
│ │ → Buyer gets refund │ │
│ │ │ │
│ │ 4. DISPUTE (Optional - needs arbitrator) │ │
│ │ → Arbitrator decides │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
Types Definition
lib/escrow/types.ak
//// Types cho Escrow Contract
/// Trạng thái của Escrow
pub type EscrowState {
/// Đang chờ xác nhận từ buyer
AwaitingDelivery
/// Buyer đã xác nhận nhận hàng
DeliveryConfirmed
/// Có dispute, cần arbitrator
Disputed
/// Đã hoàn thành
Completed
}
/// Datum lưu trong UTxO
pub type EscrowDatum {
/// Public key hash của seller
seller: ByteArray,
/// Public key hash của buyer
buyer: ByteArray,
/// Số tiền escrow (lovelace)
amount: Int,
/// Deadline để buyer confirm (POSIX timestamp)
deadline: Int,
/// Arbitrator (optional) - xử lý disputes
arbitrator: Option<ByteArray>,
/// Trạng thái hiện tại
state: EscrowState,
}
/// Redeemer - các actions có thể thực hiện
pub type EscrowRedeemer {
/// Buyer xác nhận đã nhận hàng/dịch vụ
Complete
/// Seller hủy trước deadline
Cancel
/// Buyer claim refund sau deadline
Refund
/// Buyer hoặc Seller raise dispute
RaiseDispute
/// Arbitrator quyết định cho bên nào
ResolveDispute { winner: DisputeWinner }
}
/// Kết quả dispute
pub type DisputeWinner {
/// Seller thắng - nhận tiền
SellerWins
/// Buyer thắng - được refund
BuyerWins
}
Validator Implementation
validators/escrow.ak
//// Escrow Validator
//// Hợp đồng ký quỹ đơn giản
use aiken/collection/list
use aiken/interval
use cardano/assets.{lovelace_of}
use cardano/transaction.{OutputReference, Transaction}
// ============================================
// TYPES
// ============================================
pub type EscrowDatum {
seller: ByteArray,
buyer: ByteArray,
amount: Int,
deadline: Int,
}
pub type EscrowRedeemer {
Complete
Cancel
Refund
}
// ============================================
// HELPER FUNCTIONS
// ============================================
/// Kiểm tra signature
fn signed_by(tx: Transaction, pkh: ByteArray) -> Bool {
list.has(tx.extra_signatories, pkh)
}
/// Kiểm tra thời gian trước deadline sử dụng interval module
fn before_deadline(tx: Transaction, deadline: Int) -> Bool {
interval.is_entirely_before(tx.validity_range, deadline)
}
/// Kiểm tra thời gian sau deadline sử dụng interval module
fn after_deadline(tx: Transaction, deadline: Int) -> Bool {
interval.is_entirely_after(tx.validity_range, deadline)
}
/// Kiểm tra có output với đủ tiền
fn has_sufficient_output(tx: Transaction, min_amount: Int) -> Bool {
list.any(tx.outputs, fn(output) { lovelace_of(output.value) >= min_amount })
}
// ============================================
// MAIN VALIDATOR
// ============================================
validator escrow {
spend(
datum: Option<EscrowDatum>,
redeemer: EscrowRedeemer,
_input: OutputReference,
tx: Transaction,
) {
expect Some(d) = datum
when redeemer is {
// Buyer confirms receipt -> seller gets paid
Complete ->
signed_by(tx, d.buyer) && has_sufficient_output(tx, d.amount)
// Seller cancels before deadline
Cancel ->
signed_by(tx, d.seller) && before_deadline(tx, d.deadline)
// Buyer claims refund after deadline
Refund ->
signed_by(tx, d.buyer) && after_deadline(tx, d.deadline) && has_sufficient_output(
tx,
d.amount,
)
}
}
else(_) {
fail
}
}
Phiên bản đơn giản (không có dispute)
Phiên bản chính của escrow contract ở trên đã là phiên bản đơn giản và đầy đủ, sử dụng module aiken/interval cho xử lý thời gian.
Comprehensive Tests
lib/escrow/escrow_test.ak
//// Tests cho Escrow Contract
// ============================================
// MOCK TYPES
// ============================================
type MockDatum {
seller: ByteArray,
buyer: ByteArray,
amount: Int,
deadline: Int,
}
type MockRedeemer {
Complete
Cancel
Refund
}
// Hex constants for testing (phải dùng hex hợp lệ)
const seller_pkh: ByteArray = #"aabbccdd11223344"
const buyer_pkh: ByteArray = #"eeff00112233445566"
const random_pkh: ByteArray = #"99887766554433221100"
// ============================================
// VALIDATION LOGIC
// ============================================
fn validate_complete(datum: MockDatum, signer: ByteArray) -> Bool {
datum.buyer == signer
}
fn validate_cancel(
datum: MockDatum,
signer: ByteArray,
current_time: Int,
) -> Bool {
datum.seller == signer && current_time < datum.deadline
}
fn validate_refund(
datum: MockDatum,
signer: ByteArray,
current_time: Int,
) -> Bool {
datum.buyer == signer && current_time > datum.deadline
}
fn validate_escrow(
datum: MockDatum,
redeemer: MockRedeemer,
signer: ByteArray,
current_time: Int,
) -> Bool {
when redeemer is {
Complete -> validate_complete(datum, signer)
Cancel -> validate_cancel(datum, signer, current_time)
Refund -> validate_refund(datum, signer, current_time)
}
}
// ============================================
// TESTS: COMPLETE
// ============================================
test test_complete_by_buyer() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
validate_escrow(datum, Complete, buyer_pkh, 500) == True
}
test test_complete_by_seller_fails() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
// Seller không thể complete
validate_escrow(datum, Complete, seller_pkh, 500) == False
}
// ============================================
// TESTS: CANCEL
// ============================================
test test_cancel_by_seller_before_deadline() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
validate_escrow(datum, Cancel, seller_pkh, 500) == True
}
test test_cancel_by_seller_after_deadline_fails() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
// Không thể cancel sau deadline
validate_escrow(datum, Cancel, seller_pkh, 1500) == False
}
// ============================================
// TESTS: REFUND
// ============================================
test test_refund_by_buyer_after_deadline() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
validate_escrow(datum, Refund, buyer_pkh, 1500) == True
}
test test_refund_by_buyer_before_deadline_fails() {
let datum =
MockDatum {
seller: seller_pkh,
buyer: buyer_pkh,
amount: 10_000_000,
deadline: 1000,
}
// Không thể refund trước deadline
validate_escrow(datum, Refund, buyer_pkh, 500) == False
}
Off-chain Integration
Lucid (TypeScript)
import { Lucid, Data, fromText } from "lucid-cardano";
// Types matching Aiken
const EscrowDatum = Data.Object({
seller: Data.Bytes(),
buyer: Data.Bytes(),
amount: Data.Integer(),
deadline: Data.Integer(),
});
type EscrowDatum = Data.Static<typeof EscrowDatum>;
const EscrowRedeemer = Data.Enum([
Data.Literal("Complete"),
Data.Literal("Cancel"),
Data.Literal("Refund"),
]);
type EscrowRedeemer = Data.Static<typeof EscrowRedeemer>;
// Create Escrow
async function createEscrow(
lucid: Lucid,
validator: any,
buyerPkh: string,
amount: bigint,
deadlineMinutes: number
) {
const sellerPkh = lucid.utils.getAddressDetails(
await lucid.wallet.address()
).paymentCredential!.hash;
const deadline = BigInt(Date.now() + deadlineMinutes * 60 * 1000);
const datum: EscrowDatum = {
seller: sellerPkh,
buyer: buyerPkh,
amount,
deadline,
};
const validatorAddress = lucid.utils.validatorToAddress(validator);
const tx = await lucid
.newTx()
.payToContract(
validatorAddress,
{ inline: Data.to(datum, EscrowDatum) },
{ lovelace: amount }
)
.complete();
const signedTx = await tx.sign().complete();
return await signedTx.submit();
}
// Complete Escrow (Buyer confirms)
async function completeEscrow(
lucid: Lucid,
validator: any,
utxo: any
) {
const redeemer = Data.to("Complete", EscrowRedeemer);
const datum = Data.from(utxo.datum!, EscrowDatum);
// Get seller address
const sellerAddress = lucid.utils.credentialToAddress({
type: "Key",
hash: datum.seller,
});
const tx = await lucid
.newTx()
.collectFrom([utxo], redeemer)
.attachSpendingValidator(validator)
.payToAddress(sellerAddress, { lovelace: datum.amount })
.addSignerKey(datum.buyer)
.complete();
const signedTx = await tx.sign().complete();
return await signedTx.submit();
}
// Cancel Escrow (Seller cancels)
async function cancelEscrow(
lucid: Lucid,
validator: any,
utxo: any
) {
const redeemer = Data.to("Cancel", EscrowRedeemer);
const datum = Data.from(utxo.datum!, EscrowDatum);
const tx = await lucid
.newTx()
.collectFrom([utxo], redeemer)
.attachSpendingValidator(validator)
.addSignerKey(datum.seller)
.validTo(Number(datum.deadline) - 60000) // Before deadline
.complete();
const signedTx = await tx.sign().complete();
return await signedTx.submit();
}
// Refund Escrow (Buyer claims after deadline)
async function refundEscrow(
lucid: Lucid,
validator: any,
utxo: any
) {
const redeemer = Data.to("Refund", EscrowRedeemer);
const datum = Data.from(utxo.datum!, EscrowDatum);
// Get buyer address
const buyerAddress = lucid.utils.credentialToAddress({
type: "Key",
hash: datum.buyer,
});
const tx = await lucid
.newTx()
.collectFrom([utxo], redeemer)
.attachSpendingValidator(validator)
.payToAddress(buyerAddress, { lovelace: datum.amount })
.addSignerKey(datum.buyer)
.validFrom(Number(datum.deadline) + 60000) // After deadline
.complete();
const signedTx = await tx.sign().complete();
return await signedTx.submit();
}
Hoàn thành Khóa học
Chúc mừng! Bạn đã hoàn thành toàn bộ khóa học Aiken Smart Contract Development!
Những gì bạn đã học:
Part 1: The Aiken Foundation
- Cài đặt và sử dụng Aiken CLI
- Cấu trúc dự án và modules
- Types, functions, và control flow
- Testing và troubleshooting
Part 2: Cardano Architecture
- Kiến trúc Cardano và Ouroboros
- Mô hình UTxO và eUTxO
- Datum và Redeemer
Part 3: Your First Validator
- Spending validators
- Gift contract pattern
Part 4: Minting Tokens & NFTs
- Fungible và Non-Fungible tokens
- Minting policies
- One-shot pattern
Part 5: Escrow Contract
- Real-world smart contract design
Code mẫu
Xem code mẫu đầy đủ trong thư mục examples/:
- validators/escrow.ak - Escrow Contract hoàn chỉnh với Complete/Cancel/Refund
- lib/escrow_test.ak - 14 test cases bao gồm edge cases và scenarios
# Chạy tests
cd examples
aiken check -m "escrow_test"