直播记录整个设计开发过程,一个基于cardano智能合约的下注网站

主网环境合约中测试时间戳的宽度底线(更新022)

大致概括下,这次测试mainnet的内容,就是上面说过cardano的合约当前时间没有“点”的感念,只有当前时间“范围”的概念。那么这个“范围”的最低处,就是时间宽度范围的最小值,这个是我这次测试重点。
先看合约代码

from opshin.prelude import *
from opshin.std.builtins import *
from opshin.ledger.interval import *

@dataclass
class Sell(PlutusData):
    CONSTR_ID = 1

@dataclass()
class TimeRedeemer(PlutusData):
    CONSTR_ID = 0  # 显式指定构造器 ID
    reference_time: int


ChannelAction = Union[Sell,TimeRedeemer]

def validator(context: ScriptContext) -> None:
    cha: ChannelAction = context.redeemer
    assert isinstance(cha, TimeRedeemer), "Redeemer 错误"
    ref_timestamp = cha.reference_time

    # # 获取链上时间
    tx_info = context.transaction
    # # V3 的 valid_range 访问方式    
    lower_bound = tx_info.validity_range.lower_bound.limit
     

    if isinstance(lower_bound, FinitePOSIXTime):
        ledger_time = lower_bound.time
        unlock_range = make_from(ref_timestamp)
        assert ledger_time >= ref_timestamp, f"链上时间落后ledger_time:{ledger_time}"
    else:
        assert False, f"没有设置有效期下限ref_timestamp:{ref_timestamp} , lower_bound:{lower_bound}"

这里的 ledger_time是 合约中获取的执行当时的时间戳范围的最低点,ref_timestamp 是 我的输入时间点。
再看调用合约代码

import os
import time
from pycardano import *

# 1. 初始化环境
network = Network.MAINNET
blockfrost_key = "输入你的blockfrost_key "
# 建议显式指定 base_url
ctx = BlockFrostChainContext(blockfrost_key, network=network)

# 2. 关键:同步链上“真实”时间
# 直接获取最新块,确保我们拿到的 Slot 和 Time 是匹配的“真值”
latest_block = ctx.api.block_latest()
current_slot = ctx.last_block_slot

# 这是节点认定的 POSIX 秒,转为毫秒
print(latest_block)
ledger_truth_ms = latest_block.time * 1000 

print(f"--- 链上同步 ---")
print(f"当前 Slot: {current_slot}")
print(f"对应时间: {ledger_truth_ms} ms")

# 3. 准备地址和密钥 (请替换为你自己的)

psk = PaymentSigningKey.load("  。。。。你的钱包私钥目录/timstamp1.skey")
pvk = PaymentVerificationKey.load(" 。。。。你的钱包公钥目录/timstamp1.vkey")
my_addr = Address(pvk.hash(), network=network)
 


# 加载并计算合约地址
with open("。。。。你的合约文件目录/script.cbor", "r") as f:
    script_hex = f.read().strip()
v3_script = PlutusV3Script(bytes.fromhex(script_hex))
script_hash = plutus_script_hash(v3_script)
# 修改点3
#network = Network.TESTNET
network = Network.MAINNET
script_addr = Address(script_hash, network=network)
 
from dataclasses import dataclass

@dataclass
class TimeRedeemer(PlutusData):
    CONSTR_ID = 0
    reference_time: int

redeemer_data = TimeRedeemer(reference_time=ledger_truth_ms)

print(f"你的地址: {my_addr}")
print(f"合约地址: {script_addr}")

# --- 2. 第一阶段:锁定资金 (Locking) ---
def lock_ada(amount_ada: int):
    print(f"\n[阶段 1] 正在向合约转入 {amount_ada} ADA...")
    builder = TransactionBuilder(ctx)
    builder.add_input_address(my_addr)
    builder.add_output(TransactionOutput(script_addr, amount_ada * 1_000_000))
    
    signed_tx = builder.build_and_sign([psk], change_address=my_addr)
    tx_id = ctx.submit_tx(signed_tx)
    print(f"锁定交易已提交! TxID: {tx_id}")
    return tx_id

# --- 3. 第二阶段:赎回资金 (Spending/Calling Contract) ---
def unlock_ada():
    print(f"\n[阶段 2] 正在准备调用合约赎回资金...")
    latest_block = ctx.api.block_latest()
    current_slot = ctx.last_block_slot
    ledger_truth_ms = latest_block.time  * 1000


    # 3.2 查找合约地址上的 UTXO
    script_utxos = ctx.utxos(str(script_addr))
    if not script_utxos:
        print("错误:合约地址上没有发现 UTXO。")
        return

    # 选择一个 UTXO 进行花费 (这里假设选第一个)
    target_utxo = script_utxos[0]
    print(target_utxo)
    
    # 3.3 构建赎回交易
    builder = TransactionBuilder(ctx)

    # mainnet 成功记录
    # 【21分钟】成功,reference_time_for_contract = ledger_truth_ms - 1300_000
    # 【16分钟】成功,reference_time_for_contract = ledger_truth_ms - 1000_000


    # mainnet【10分钟】失败
    #reference_time_for_contract = ledger_truth_ms - 600_000
    # mainnet【13分钟】失败
    #reference_time_for_contract = ledger_truth_ms - 800_000 

    # 成功 17分钟
    reference_time_for_contract = ledger_truth_ms - 1000_000
    # 失败 13分钟
    # reference_time_for_contract = ledger_truth_ms - 800_000

    # 主网 800 , 80 , 8(测试3个档次)


    current_time  = int(time.time()) 
    from datetime import datetime
    dt0 = datetime.fromtimestamp(reference_time_for_contract / 1000)
    dt1 = datetime.fromtimestamp(ledger_truth_ms / 1000)
    dt2 = datetime.fromtimestamp(current_time )
    print(f" build  reference_time_for_contract time : {dt0}")
    print(f" build  ledger time : {dt1}")
    print(f" build  current_time  : {dt2}")


    redeemer = Redeemer(TimeRedeemer(reference_time_for_contract))
    
    # 必须指定脚本、Redeemer 以及这是 PlutusV3 逻辑
    builder.add_script_input(
        target_utxo,
        script=v3_script,
        redeemer=redeemer
    )
    
    # 设置时间有效期 (必须对齐)
    builder.invalid_before = current_slot
    builder.invalid_hereafter = current_slot + 600
    
    # 将钱领回到自己的地址,留出约 1 ADA 做手续费
    builder.add_input_address(my_addr)
    
    try:
        # build 会自动处理平衡,如果时间没对齐,这里会抛出异常
        signed_tx = builder.build_and_sign([psk], change_address=my_addr)

        # 正式提交
        tx_id = ctx.submit_tx(signed_tx.to_cbor_hex())
        print(f"合约调用成功! 资金已赎回。TxID: {tx_id}")
    except Exception as e:
        print(f"合约执行失败: {e}")


if __name__ == "__main__":
    # 执行阶段 1
    lock_tx = lock_ada(2) # 锁定 3 ADA
    print("等待交易上链确认 (约 60-90 秒)...")
    while True:
        # 轮询检查合约地址是否有刚才那笔交易的 UTXO
        utxos = ctx.utxos(str(script_addr))
        if any(u.input.transaction_id.payload.hex() == lock_tx for u in utxos):
            print("资金已到账,开始执行解锁合约。")
            break
        time.sleep(10)
    
    # 执行阶段 2
    unlock_ada()

测试效果是,如果输入时间没有把当前时间点减够,那么就会报错Namespace(EvaluationFailure=Namespace(ScriptFailures=Namespace()))。如果减够时间了,例如20分钟,那么久合约执行成功。有测试减13分钟也会失败。
所以,在cardano智能合约里判断时间戳一定要预留好足够的时间。哪怕是mainnet

面对utxo污染 (更新023)

挖了个坑给自己,合约脚本可以存在普通address中, 当初是为了不去面对合约的限制,在转移资产的时候就不需要执行合约,更方便管理。
但是问题来了,你在消费这个钱包的utxo的时候要注意这类保存合约脚本的 utxo,不能被消费掉的。 好吧,我是后来才反应过来,只能这么设计了,这个钱包必须不参与业务代码了,只管合约了。

Meshjs的与智能合约交互类MeshTxBuilder起步价不便宜(更新024)

最近测试很多内容,所以业务开发基本停止。但是也是有收获的,先来看两个tx,和合约的交互

Mem: 1,679,646
Steps: 490,934,646
Fee: ₳ 0.85

Mem: 7,000,000
Steps: 3,000,000,000
Fee: ₳ 1.64

同一个合约,基本相同的utxo消耗,但是费用相差2倍。然后为啥 Mem和step相差那么多; 再备注下,第一个是pycardnao提交的,第二个是meshjs提交的。
后来才知道,MeshJS 默认有时会为了保证交易 100% 成功,给出一个非常高的 budget( 看到的 30 亿 Steps 和 7.0 MB Mem).那怎么改呢,看代码

await txBuilder
  .spendingPlutusScriptV3()
  .txIn(txHash, txIndex)
  .txInScript(scriptCbor)
  .txInRedeemerValue(
    meshRedeemer, 
    "Mesh", // 或者是 "CBOR" 根据你的输入类型
    {
      mem: 1679646,     // 对应 PyCardano 的实测内存
      steps: 490934646  // 对应 PyCardano 的实测步数
    }
  )
  .changeAddress(address)
  .complete();

utxo的管理查询(更新025)
各种utxo的管理查询,原先太零散了,要统一集中方便查看


1 Like

Meshjs执行txBuilder顺序(更新026)

MeshSDK 的链式调用有时会产生莫名的“污染”。标准的推荐构建顺序是:

  1. txIn (普通钱包输入)
  2. spendingPlutusScript (脚本输入系列)
  3. readOnlyTxInReference / spendingTxInReference
  4. txOut
  5. txInCollateral

vercel docusaurus 知识信息网站 (更新027)

vercel + docusaurus技术 做doc集合网站,可以国际化,对国内访问相对比较友好。支持MD。

业务类型 钱包介绍,钱包知识普及,我知道这个很简单,但是好像还是要有个地方去说,例如抵押品(Collateral)是怎么一回事,钱包签名安全与否,这些都要解释,

特别是以太坊的钱包授权把大家都吓成惊弓之鸟后的大环境下。

本人cmore pool的 cardano质押池后期的网站主页也考虑迁移到这个框架下。

Lace (version 1.33.3)钱包不支持 Plutus V3 (更新028)

yoroi 钱包又开始摆烂了,不同步tx信息了,别的钱包发过来的tx是不刷新的,没有的,preprod 环境.

Lace 钱包貌似不支持 Plutus V3,环境 preprod,有严苛的 requiresForeignSignatures 检查。反正合约这块v3的就挂了。浪费我一天时间。

Typhon/eternl 一切ok.

Meshjs自动找零实效了-我咋没早点知道(更新029)

明明设置了.changeAddress(userAddress); 为啥没有起效呢?百思不得其解,后来从txBodyJson中发现

 "redeemer": {
                        "data": "d87b9f001058404e405297a9509fbfbf311d56bfd5a206470375349659ff",
                        "exUnits": {
                           "mem": 7000000,
                           "steps": 3000000000
                        }
                    }

exUnits 得到一个非常巨大的值,都不够用了,肯定没法找零了。减少这个就可以了。