Cardano 测试网 NFT 铸造入门教程
Cardano 是一个第三代区块链平台,以其学术严谨性和可持续性著称。与以太坊不同,Cardano 使用原生代币标准,这意味着 NFT 在协议层面得到支持,而不是通过智能合约实现。这种设计带来了更低的费用、更高的安全性和更好的性能。
核心概念解释
策略ID(Policy ID):每个 NFT 系列都有唯一的策略ID,相当于 NFT 的"身份证"。策略ID 由策略脚本的哈希值生成,确保了 NFT 的唯一性和不可伪造性。
策略脚本(Policy Script):定义了 NFT 铸造规则的脚本,包括谁可以铸造、什么时候可以铸造等条件。一旦部署,这些规则就不可更改。
时间锁定(Time Lock):在策略脚本中设置的时间限制,过期后该策略就永远无法再用于铸造新的代币,保证了 NFT 的稀缺性。
资产名称(Asset Name):在同一策略下,每个代币的唯一标识符。完整的代币标识为"策略ID.资产名称"。
环境准备:搭建 Cardano 节点
为什么需要运行节点?
要与 Cardano 网络交互,我们需要一个全节点来:
- 同步区块链数据
- 验证交易
- 提交交易到网络
- 查询链上状态
我们使用测试网(Preprod)进行学习,因为:
- 测试币免费获取
- 可以安全地进行实验
- 与主网功能完全相同
- 不会产生真实费用
1. Docker Compose 配置详解
创建 docker-compose.yml 文件:
version: '3.8'
services:
# Cardano 节点服务
cardano-node:
image: ghcr.io/blinklabs-io/cardano-node:latest
container_name: cardano-node
restart: unless-stopped
ports:
- "3001:3001" # 节点 P2P 通信端口
environment:
- NETWORK=preprod # 指定测试网络
volumes:
- node-data:/data/db # 区块链数据存储
- node-ipc:/ipc # 进程间通信套接字
init: true # 启用 init 进程管理
# Cardano CLI 工具容器
cardano-cli:
image: ghcr.io/blinklabs-io/cardano-node:latest
container_name: cardano-cli
restart: unless-stopped
environment:
- NETWORK=preprod
volumes:
- node-ipc:/ipc # 共享通信套接字
depends_on:
- cardano-node # 依赖节点服务
command: ["sleep", "infinity"] # 保持容器运行
volumes:
node-data: # 持久化区块链数据
node-ipc: # 节点间通信配置说明:
init: true:启用轻量级 init 进程,正确处理信号和僵尸进程ports: 3001:3001:映射节点 P2P 通信端口NETWORK=preprod:指定使用 Preprod 测试网- 数据卷:
node-data存储区块链数据,node-ipc用于 CLI 与节点通信 depends_on:确保 CLI 容器在节点容器之后启动
2. 启动和验证服务
# 启动所有服务
docker-compose up -d
# 查看服务状态
docker-compose ps
# 查看节点日志(观察同步进度)
docker-compose logs -f cardano-node
# 查看最近100行日志
docker-compose logs --tail=100 cardano-node日志解读:
- 看到 "Chain extended" 表示正在同步区块
- "TraceNodeIsLeader" 表示节点正常参与网络
- 同步过程可能需要几小时,取决于网络速度
3. 进入 CLI 工作环境
# 进入 CLI 容器
docker exec -ti cardano-cli bash
# 或者使用 Dockge 的 Web 终端(如果使用 Dockge)第一步:环境配置和验证
设置关键环境变量
# 设置节点套接字路径(必须)
export CARDANO_NODE_SOCKET_PATH=/ipc/node.socket
# 验证套接字文件存在
ls -la /ipc/node.socket
# 创建工作目录
mkdir -p /keys
cd /keys
# 验证 CLI 工具版本
cardano-cli --version验证节点同步状态
# 查询节点状态
cardano-cli query tip --testnet-magic 1期望输出示例:
{
"block": 3992119,
"epoch": 245,
"era": "Conway",
"hash": "25ffab7df708fedb8d14b6bc2998907afa1735445ba132f03cb1f1f6716d57b2",
"slot": 104340690,
"slotInEpoch": 142290,
"slotsToEpochEnd": 289710,
"syncProgress": "100.00"
}重要指标解释:
syncProgress: "100.00":必须达到100%才能进行交易era: "Conway":当前网络时代,支持最新功能slot:当前时隙,用于时间锁定计算epoch:当前纪元,每个纪元约5天
⚠️ 警告:只有当 syncProgress 显示 "100.00" 时才能继续后续操作!
第二步:钱包生成和管理
理解 Cardano 地址体系
Cardano 使用 Bech32 编码的地址格式:
- 测试网地址:以
addr_test1开头 - 主网地址:以
addr1开头 - 地址类型:支付地址、质押地址、脚本地址等
生成密钥对
# 生成支付密钥对
cardano-cli address key-gen \\
--verification-key-file /keys/payment.vkey \\
--signing-key-file /keys/payment.skey
# 查看生成的文件
ls -la /keys/payment.*
# 查看公钥内容(可以安全分享)
cat /keys/payment.vkey文件说明:
payment.vkey:验证密钥(公钥),用于生成地址,可以公开payment.skey:签名密钥(私钥),用于签名交易,必须保密
生成钱包地址
# 生成基础支付地址
cardano-cli address build \\
--payment-verification-key-file /keys/payment.vkey \\
--out-file /keys/payment.addr \\
--testnet-magic 1
# 保存地址到变量
ADDRESS=$(cat /keys/payment.addr)
# 显示地址信息
echo "🏦 你的测试网钱包地址:"
echo "$ADDRESS"
echo ""
echo "📝 地址长度: $(echo -n $ADDRESS | wc -c) 字符"
echo "🌐 网络类型: Preprod 测试网"地址示例:
addr_test1vr35z3scn27mertcrm7qmtdec9373924jtt8f24pe9544dcjk0wsx可选:生成质押地址(高级功能)
# 生成质押密钥对
cardano-cli stake-address key-gen \\
--verification-key-file /keys/stake.vkey \\
--signing-key-file /keys/stake.skey
# 生成包含质押功能的地址
cardano-cli address build \\
--payment-verification-key-file /keys/payment.vkey \\
--stake-verification-key-file /keys/stake.vkey \\
--out-file /keys/payment_with_stake.addr \\
--testnet-magic 1
echo "💎 带质押功能的地址:"
cat /keys/payment_with_stake.addr第三步:获取测试币
理解 Cardano 代币单位
- ADA:Cardano 的主要代币
- Lovelace:ADA 的最小单位(1 ADA = 1,000,000 Lovelace)
- 测试网 ADA:没有真实价值,仅用于测试
使用官方水龙头
- 复制钱包地址:
# 显示地址以便复制
echo "请复制以下地址:"
echo "$ADDRESS"- 访问水龙头网站:
- 申请流程:
- 粘贴你的地址
- 选择 "Preprod" 网络
- 点击申请按钮
- 等待确认信息
验证资金到账
# 检查余额(可能需要等待几分钟)
cardano-cli query utxo --address $ADDRESS --testnet-magic 1
# 如果没有余额,等待一会儿再查询
sleep 30
cardano-cli query utxo --address $ADDRESS --testnet-magic 1成功输出示例:
{
"95504c30d402ae2244407a7d7edd1711d88000a20b2101b34c0e80fbf8e5ff4e#0": {
"address": "addr_test1vr35z3scn27mertcrm7qmtdec9373924jtt8f24pe9544dcjk0wsx",
"datum": null,
"datumhash": null,
"inlineDatum": null,
"inlineDatumRaw": null,
"referenceScript": null,
"value": {
"lovelace": 10000000000
}
}
}输出解释:
- UTXO 标识:
TxHash#TxIx格式,唯一标识每个未花费输出 lovelace: 10000000000:等于 10,000 ADAdatum和datumhash:智能合约相关数据,此处为空
备用获取方式
如果官方水龙头不可用:
- Discord 水龙头:
- 加入 Cardano 官方 Discord
- 在
#testnet-faucet频道申请
- Telegram 机器人:
- 联系
@CardanoTestnetFaucetBot
- 联系
第四步:策略脚本创建
理解策略脚本的作用
策略脚本定义了代币铸造的规则:
- 签名验证:只有特定密钥持有者可以铸造
- 时间限制:设置铸造的有效时间窗口
- 数量限制:可以设置最大铸造数量
- 多重签名:可以要求多个签名
生成策略密钥
# 生成策略专用密钥对
cardano-cli address key-gen \\
--verification-key-file /keys/policy.vkey \\
--signing-key-file /keys/policy.skey
echo "✅ 策略密钥生成完成"
# 验证密钥文件
ls -la /keys/policy.*计算密钥哈希
# 计算公钥哈希(用于策略脚本)
cardano-cli address key-hash \\
--payment-verification-key-file /keys/policy.vkey > /keys/policy.keyid
# 显示密钥哈希
POLICY_KEY_HASH=$(cat /keys/policy.keyid)
echo "🔑 策略密钥哈希: $POLICY_KEY_HASH"设置时间锁定
# 获取当前区块链状态
CURRENT_SLOT=$(cardano-cli query tip --testnet-magic 1 | jq .slot)
echo "⏰ 当前 Slot: $CURRENT_SLOT"
# 计算过期时间(24小时后)
# 1 slot = 1 秒,24小时 = 86400 秒
EXPIRE_SLOT=$((CURRENT_SLOT + 86400))
echo "⏰ 策略过期 Slot: $EXPIRE_SLOT"
# 计算剩余时间
REMAINING_SECONDS=$((EXPIRE_SLOT - CURRENT_SLOT))
REMAINING_HOURS=$((REMAINING_SECONDS / 3600))
echo "⏰ 策略有效期: $REMAINING_HOURS 小时"时间选择建议:
- NFT 收藏品:1-7天(保证稀缺性)
- 游戏道具:30-365天(根据游戏需求)
- 测试用途:1-24小时(便于快速实验)
创建策略脚本
# 创建策略脚本文件
cat > /keys/policy.script << EOF
{
"type": "all",
"scripts": [
{
"type": "sig",
"keyHash": "$(cat /keys/policy.keyid)"
},
{
"type": "before",
"slot": ${EXPIRE_SLOT}
}
]
}
EOF
echo "✅ 策略脚本创建完成"
# 显示策略脚本内容
echo "📜 策略脚本内容:"
cat /keys/policy.script | jq '.'策略脚本解释:
"type": "all":所有条件都必须满足"type": "sig":需要指定密钥的签名"type": "before":必须在指定 slot 之前执行keyHash:授权签名的密钥哈希slot:过期的绝对时间点
验证策略脚本
# 验证脚本格式
if jq empty /keys/policy.script 2>/dev/null; then
echo "✅ 策略脚本格式正确"
else
echo "❌ 策略脚本格式错误"
fi
# 检查文件权限
ls -la /keys/policy.script第五步:策略ID生成
理解策略ID的重要性
策略ID是策略脚本的加密哈希值,具有以下特性:
- 唯一性:每个不同的策略脚本都有唯一的策略ID
- 不可伪造:无法创建具有相同策略ID的不同脚本
- 永久性:策略ID一旦生成就永远不变
生成策略ID
# 从策略脚本生成策略ID
cardano-cli conway transaction policyid \\
--script-file /keys/policy.script > /keys/policy.id
# 保存到变量
POLICY_ID=$(cat /keys/policy.id)
echo "🆔 你的策略ID:"
echo "$POLICY_ID"
echo ""
echo "📏 策略ID长度: $(echo -n $POLICY_ID | wc -c) 字符"
echo "🔒 策略ID类型: 56位十六进制哈希值"策略ID示例:
bfc0c4cef7bfdb90550769f9f10a2f14709312097a5b6560b001c8b8验证策略ID
# 验证策略ID格式(应该是56位十六进制)
if [[ $POLICY_ID =~ ^[a-f0-9]{56}$ ]]; then
echo "✅ 策略ID格式正确"
else
echo "❌ 策略ID格式错误"
fi
# 重新生成验证(结果应该相同)
POLICY_ID_CHECK=$(cardano-cli conway transaction policyid --script-file /keys/policy.script)
if [ "$POLICY_ID" = "$POLICY_ID_CHECK" ]; then
echo "✅ 策略ID验证通过"
else
echo "❌ 策略ID验证失败"
fi第六步:NFT元数据创建
理解 CIP-25 元数据标准
Cardano 使用 CIP-25 标准定义 NFT 元数据:
- 标准化格式:确保不同平台间的兼容性
- 链上存储:元数据直接存储在交易中
- 丰富信息:支持名称、描述、图片、属性等
基础元数据结构
# 设置NFT资产名称
NFT_NAME="MyNFT"
echo "🎨 NFT名称: $NFT_NAME"
# 将名称转换为十六进制(避免特殊字符问题)
NFT_NAME_HEX="4d794e4654" # "MyNFT" 的十六进制编码
echo "🔢 NFT名称(十六进制): $NFT_NAME_HEX"
# 验证十六进制编码
echo -n "$NFT_NAME" | xxd -p # 如果xxd可用创建标准元数据
# 创建符合CIP-25标准的元数据
cat > /keys/metadata.json << EOF
{
"721": {
"${POLICY_ID}": {
"${NFT_NAME}": {
"name": "My First Cardano NFT",
"description": "This is my first NFT created on Cardano testnet for learning purposes.",
"image": "ipfs://QmYourImageHashHere",
"mediaType": "image/png",
"creator": "Cardano NFT Student",
"website": "https://cardano.org",
"attributes": {
"type": "Digital Art",
"rarity": "Common",
"color": "Blue",
"edition": "1/1",
"created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
},
"files": [
{
"name": "My First NFT",
"mediaType": "image/png",
"src": "ipfs://QmYourImageHashHere"
}
]
}
}
}
}
EOF
echo "✅ NFT元数据创建完成"添加版税信息(可选)
# 创建包含版税的增强元数据
cat > /keys/metadata_with_royalty.json << EOF
{
"721": {
"${POLICY_ID}": {
"${NFT_NAME}": {
"name": "My First Cardano NFT",
"description": "This is my first NFT created on Cardano testnet with royalty settings.",
"image": "ipfs://QmYourImageHashHere",
"mediaType": "image/png",
"creator": "Cardano NFT Student",
"website": "https://cardano.org",
"attributes": {
"type": "Digital Art",
"rarity": "Common",
"color": "Blue",
"edition": "1/1",
"created": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
},
"royalty": {
"rate": "0.05",
"addr": "$ADDRESS"
}
}
}
},
"777": {
"royalty": {
"${POLICY_ID}": {
"rate": "0.05",
"addr": "$ADDRESS"
}
}
}
}
EOF
# 选择使用哪个元数据文件
cp /keys/metadata_with_royalty.json /keys/metadata.json
echo "✅ 已启用5%版税设置"版税设置说明:
rate: "0.05":5%版税率addr:版税接收地址777标签:CIP-27版税标准- 市场支持:需要NFT市场主动支持版税执行
验证元数据格式
# 验证JSON格式
if jq empty /keys/metadata.json 2>/dev/null; then
echo "✅ 元数据JSON格式正确"
# 显示元数据摘要
echo "📊 元数据摘要:"
echo " - NFT名称: $(jq -r ".\\"721\\".\\"$POLICY_ID\\".\\"$NFT_NAME\\".name" /keys/metadata.json)"
echo " - 描述: $(jq -r ".\\"721\\".\\"$POLICY_ID\\".\\"$NFT_NAME\\".description" /keys/metadata.json)"
echo " - 创建者: $(jq -r ".\\"721\\".\\"$POLICY_ID\\".\\"$NFT_NAME\\".creator" /keys/metadata.json)"
# 检查版税设置
if jq -e ".\\"777\\"" /keys/metadata.json > /dev/null 2>&1; then
echo " - 版税: $(jq -r ".\\"777\\".royalty.\\"$POLICY_ID\\".rate" /keys/metadata.json | awk '{print $1*100}')%"
else
echo " - 版税: 未设置"
fi
else
echo "❌ 元数据JSON格式错误"
jq . /keys/metadata.json # 显示错误详情
fi第七步:构建铸造交易
理解UTXO模型
Cardano使用UTXO(未花费交易输出)模型:
- UTXO:类似现金,要么完全花费,要么不花费
- 找零机制:花费UTXO时,多余的金额作为找零返回
- 原子性:交易要么完全成功,要么完全失败
查询可用UTXO
# 查询当前所有可用UTXO
echo "🔍 查询可用UTXO..."
cardano-cli query utxo --address $ADDRESS --testnet-magic 1
# 保存查询结果用于分析
cardano-cli query utxo --address $ADDRESS --testnet-magic 1 > /keys/utxos.json
# 分析UTXO
echo ""
echo "📊 UTXO分析:"
UTXO_COUNT=$(jq 'length' /keys/utxos.json)
TOTAL_LOVELACE=$(jq '[.[].value.lovelace] | add' /keys/utxos.json)
TOTAL_ADA=$((TOTAL_LOVELACE / 1000000))
echo " - UTXO数量: $UTXO_COUNT"
echo " - 总余额: $TOTAL_LOVELACE Lovelace ($TOTAL_ADA ADA)"选择合适的UTXO
# 从查询结果中选择第一个UTXO
UTXO=$(jq -r 'keys[0]' /keys/utxos.json)
echo "🎯 选择的UTXO: $UTXO"
# 验证UTXO格式
if [[ $UTXO =~ ^[a-f0-9]{64}#[0-9]+$ ]]; then
echo "✅ UTXO格式正确"
else
echo "❌ UTXO格式错误,请手动设置"
echo "请从上面的查询结果中复制一个UTXO:"
read -p "输入UTXO (格式: txhash#txix): " UTXO
fi
echo "🔗 使用UTXO: $UTXO"设置交易参数
# 获取当前时间信息
CURRENT_SLOT=$(cardano-cli query tip --testnet-magic 1 | jq .slot)
echo "⏰ 当前Slot: $CURRENT_SLOT"
# 设置交易有效期(1小时后)
VALID_UNTIL=$((CURRENT_SLOT + 3600))
echo "⏰ 交易有效到Slot: $VALID_UNTIL"
# 计算剩余时间
REMAINING_TIME=$((VALID_UNTIL - CURRENT_SLOT))
echo "⏰ 交易有效期: $((REMAINING_TIME / 60))分钟"
# 验证策略是否仍然有效
POLICY_EXPIRE=$(jq -r '.scripts[1].slot' /keys/policy.script)
if [ $CURRENT_SLOT -lt $POLICY_EXPIRE ]; then
echo "✅ 策略仍然有效 (剩余 $(((POLICY_EXPIRE - CURRENT_SLOT) / 3600))小时)"
else
echo "❌ 策略已过期,需要重新创建"
exit 1
fi构建铸造交易
echo "🔨 构建铸造交易..."
# 构建交易(使用新版cardano-cli语法)
cardano-cli conway transaction build \\
--testnet-magic 1 \\
--tx-in $UTXO \\
--mint "1 ${POLICY_ID}.${NFT_NAME_HEX}" \\
--mint-script-file /keys/policy.script \\
--metadata-json-file /keys/metadata.json \\
--change-address $ADDRESS \\
--invalid-hereafter $VALID_UNTIL \\
--out-file /keys/tx.unsigned
# 检查构建结果
if [ -f "/keys/tx.unsigned" ]; then
echo "✅ 交易构建成功"
# 显示交易信息
echo ""
echo "📋 交易摘要:"
echo " - 输入UTXO: $UTXO"
echo " - 铸造资产: 1 ${POLICY_ID}.${NFT_NAME_HEX}"
echo " - 找零地址: $ADDRESS"
echo " - 有效期至: Slot $VALID_UNTIL"
# 显示文件大小
TX_SIZE=$(wc -c < /keys/tx.unsigned)
echo \" - 交易大小: $TX_SIZE 字节\"
else
echo \"❌ 交易构建失败\"
exit 1
fi构建参数说明:
--tx-in:指定输入UTXO--mint:指定要铸造的资产数量和名称--mint-script-file:提供策略脚本文件--metadata-json-file:附加元数据--change-address:指定找零地址--invalid-hereafter:设置交易过期时间
交易费用估算
# 从构建输出中提取费用信息
if grep -q \"Estimated transaction fee:\" /dev/stdout; then
echo \"💰 交易费用已在构建过程中显示\"
else
echo \"💰 交易费用: 约184,000 Lovelace (0.184 ADA)\"
fi
echo \"\"
echo \"💡 费用说明:\"
echo \" - 基础费用: ~155,000 Lovelace\"
echo \" - 元数据费用: ~20,000 Lovelace\"
echo \" - 脚本执行费: ~10,000 Lovelace\"
echo \" - 总计: ~185,000 Lovelace\"第八步:签名和提交交易
理解数字签名
在Cardano中,交易签名确保:
- 身份验证:证明交易由密钥持有者发起
- 完整性:确保交易内容未被篡改
- 不可否认性:签名者无法否认已签名的交易
签名交易
echo \"✍️ 签名交易...\"
# 使用两个密钥签名:支付密钥和策略密钥
cardano-cli conway transaction sign \\
--signing-key-file /keys/payment.skey \\
--signing-key-file /keys/policy.skey \\
--testnet-magic 1 \\
--tx-body-file /keys/tx.unsigned \\
--out-file /keys/tx.signed
# 验证签名结果
if [ -f \"/keys/tx.signed\" ]; then
echo \"✅ 交易签名成功\"
# 比较文件大小
UNSIGNED_SIZE=$(wc -c < /keys/tx.unsigned)
SIGNED_SIZE=$(wc -c < /keys/tx.signed)
echo " - 未签名交易: $UNSIGNED_SIZE 字节"
echo " - 已签名交易: $SIGNED_SIZE 字节"
echo " - 签名增加: $((SIGNED_SIZE - UNSIGNED_SIZE)) 字节"
else
echo "❌ 交易签名失败"
exit 1
fi签名过程说明:
- 支付密钥签名:证明有权花费UTXO
- 策略密钥签名:证明有权铸造NFT
- 网络标识:
--testnet-magic 1确保签名只在测试网有效
最终验证
echo "🔍 最终验证..."
# 检查所有必要文件
echo "📁 文件检查:"
for file in payment.addr payment.skey policy.skey policy.script metadata.json tx.signed; do
if [ -f "/keys/$file" ]; then
echo " ✅ $file"
else
echo " ❌ $file (缺失)"
fi
done
# 检查环境变量
echo ""
echo "🔧 环境检查:"
echo " - 节点套接字: $CARDANO_NODE_SOCKET_PATH"
echo " - 钱包地址: $ADDRESS"
echo " - 策略ID: $POLICY_ID"
echo " - 使用UTXO: $UTXO"
# 最后的节点连接测试
echo ""
echo "🌐 节点连接测试:"
if cardano-cli query tip --testnet-magic 1 > /dev/null 2>&1; then
echo " ✅ 节点连接正常"
else
echo " ❌ 节点连接失败"
exit 1
fi提交交易
echo ""
echo "🚀 提交交易到Cardano网络..."
echo "⏳ 请等待..."
# 提交已签名的交易
cardano-cli conway transaction submit \\
--testnet-magic 1 \\
--tx-file /keys/tx.signed
# 检查提交结果
if [ $? -eq 0 ]; then
echo ""
echo "🎉🎉🎉 交易提交成功!🎉🎉🎉"
echo ""
echo "🎊 恭喜!你已经成功铸造了你的第一个Cardano NFT!"
else
echo ""
echo "❌ 交易提交失败"
echo "请检查错误信息并重试"
exit 1
fi获取交易哈希
# 计算交易哈希
TX_HASH=$(cardano-cli hash tx --tx-file /keys/tx.signed)
echo "🔗 交易哈希: $TX_HASH"
# 保存交易哈希
echo "$TX_HASH" > /keys/tx.hash
echo "💾 交易哈希已保存到 /keys/tx.hash"
# 生成交易摘要
echo ""
echo "📋 交易摘要报告:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🆔 策略ID: $POLICY_ID"
echo "🎨 NFT名称: $NFT_NAME ($NFT_NAME_HEX)"
echo "🔗 完整资产ID: ${POLICY_ID}.${NFT_NAME_HEX}"
echo "📧 钱包地址: $ADDRESS"
echo "🔗 交易哈希: $TX_HASH"
echo "⏰ 提交时间: $(date)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"第九步:验证和确认结果
等待交易确认
echo ""
echo "⏳ 等待交易确认..."
echo "💡 Cardano出块时间约20秒,确认通常需要1-3分钟"
# 等待一段时间让交易被包含到区块中
for i in {1..6}; do
echo "⏳ 等待中... ($i/6)"
sleep 30
# 每次等待后检查一次
echo "🔍 检查交易状态..."
cardano-cli query utxo --address $ADDRESS --testnet-magic 1 > /tmp/current_utxos.json
# 检查是否包含新的NFT
if jq -e ".[].value | has(\\"$POLICY_ID\\")" /tmp/current_utxos.json > /dev/null 2>&1; then
echo "✅ 检测到NFT!交易已确认"
break
fi
if [ $i -eq 6 ]; then
echo "⏰ 等待时间较长,继续检查..."
fi
done验证NFT铸造结果
echo ""
echo "🔍 验证NFT铸造结果..."
# 查询当前钱包状态
cardano-cli query utxo --address $ADDRESS --testnet-magic 1 > /keys/final_utxos.json
echo "💼 当前钱包状态:"
cat /keys/final_utxos.json | jq '.'
# 分析结果
echo ""
echo "📊 详细分析:"
# 检查NFT是否存在
if jq -e ".[].value | has(\\"$POLICY_ID\\")" /keys/final_utxos.json > /dev/null; then
echo "✅ NFT铸造成功!"
# 提取NFT信息
NFT_AMOUNT=$(jq -r ".[].value.\\"$POLICY_ID\\".\\"$NFT_NAME_HEX\\"" /keys/final_utxos.json | grep -v null | head -1)
echo "🎨 NFT数量: $NFT_AMOUNT"
# 计算剩余ADA
REMAINING_LOVELACE=$(jq '[.[].value.lovelace] | add' /keys/final_utxos.json)
REMAINING_ADA=$((REMAINING_LOVELACE / 1000000))
echo "💰 剩余ADA: $REMAINING_ADA ADA ($REMAINING_LOVELACE Lovelace)"
# 计算费用
ORIGINAL_ADA=10000 # 水龙头给的数量
SPENT_ADA=$((ORIGINAL_ADA - REMAINING_ADA))
echo "💸 交易费用: ~$SPENT_ADA ADA"
else
echo "❌ 未检测到NFT,请检查交易状态"
echo "🔍 你可以通过交易哈希在区块链浏览器中查看详情:"
echo " https://preprod.cardanoscan.io/transaction/$TX_HASH"
fi生成成功报告
# 创建成功报告
cat > /keys/nft_success_report.txt << EOF
🎉 Cardano NFT 铸造成功报告
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📅 铸造时间: $(date)
🌐 网络: Cardano Preprod 测试网
🎨 NFT 详情:
名称: $NFT_NAME
完整资产ID: ${POLICY_ID}.${NFT_NAME_HEX}
策略ID: $POLICY_ID
资产名称(十六进制): $NFT_NAME_HEX
数量: 1 (独一无二)
🔗 技术信息:
交易哈希: $TX_HASH
钱包地址: $ADDRESS
使用UTXO: $UTXO
💰 费用信息:
估算费用: ~0.18 ADA
剩余余额: ~$REMAINING_ADA ADA
🔍 验证链接:
Cardano Scan: https://preprod.cardanoscan.io/transaction/$TX_HASH
🎊 恭喜!你已经成功创建了你的第一个Cardano NFT!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
echo \"\"
echo \"📄 成功报告已生成: /keys/nft_success_report.txt\"
cat /keys/nft_success_report.txt保存重要文件
echo \"\"
echo \"💾 保存重要文件...\"
# 创建备份目录
mkdir -p /keys/backup
# 备份关键文件
cp /keys/payment.skey /keys/backup/
cp /keys/payment.vkey /keys/backup/
cp /keys/payment.addr /keys/backup/
cp /keys/policy.skey /keys/backup/
cp /keys/policy.vkey /keys/backup/
cp /keys/policy.script /keys/backup/
cp /keys/policy.id /keys/backup/
cp /keys/metadata.json /keys/backup/
cp /keys/tx.signed /keys/backup/
cp /keys/tx.hash /keys/backup/
cp /keys/nft_success_report.txt /keys/backup/
echo \"✅ 重要文件已备份到 /keys/backup/\"
# 显示文件清单
echo \"\"
echo \"📁 文件清单:\"
ls -la /keys/backup/
echo \"\"
echo \"⚠️ 重要提醒:\"
echo \" - 请妥善保管私钥文件 (.skey)\"
echo \" - 私钥丢失将无法控制钱包和NFT\"
echo \" - 建议将备份文件下载到安全位置\"进阶操作指南
1. 转移NFT到其他地址
# 准备转移NFT的函数
transfer_nft() {
local RECIPIENT_ADDRESS=\"$1\"
if [ -z \"$RECIPIENT_ADDRESS\" ]; then
echo \"❌ 请提供接收地址\"
echo \"用法: transfer_nft "
return 1
fi
echo "📤 准备转移NFT到: $RECIPIENT_ADDRESS"
# 查询当前UTXO
cardano-cli query utxo --address $ADDRESS --testnet-magic 1 > /tmp/transfer_utxos.json
# 选择包含NFT的UTXO
NFT_UTXO=$(jq -r "to_entries | map(select(.value.value.\\"$POLICY_ID\\")) | .[0].key" /tmp/transfer_utxos.json)
if [ "$NFT_UTXO" = "null" ]; then
echo "❌ 未找到包含NFT的UTXO"
return 1
fi
echo "🎯 使用UTXO: $NFT_UTXO"
# 构建转移交易
cardano-cli conway transaction build \\
--testnet-magic 1 \\
--tx-in $NFT_UTXO \\
--tx-out "${RECIPIENT_ADDRESS}+2000000+1 ${POLICY_ID}.${NFT_NAME_HEX}" \\
--change-address $ADDRESS \\
--out-file /keys/transfer.unsigned
# 签名交易
cardano-cli conway transaction sign \\
--signing-key-file /keys/payment.skey \\
--testnet-magic 1 \\
--tx-body-file /keys/transfer.unsigned \\
--out-file /keys/transfer.signed
# 提交交易
cardano-cli conway transaction submit \\
--testnet-magic 1 \\
--tx-file /keys/transfer.signed
echo "✅ NFT转移交易已提交"
}
echo ""
echo "💡 NFT转移功能已准备就绪"
echo "用法: transfer_nft "2. 查询策略下的所有NFT
# 查询策略下所有代币的函数
query_policy_tokens() {
echo "🔍 查询策略 $POLICY_ID 下的所有代币..."
# 在整个UTXO集中搜索该策略的代币
cardano-cli query utxo --whole-utxo --testnet-magic 1 | \\
jq --arg policy "$POLICY_ID" '
| to_entries |
| --- |
| map(select(.value.value[$policy])) |
map({
utxo: .key,
address: .value.address,
tokens: .value.value[$policy]
})
'
}
echo ""
echo "💡 策略代币查询功能已准备就绪"
echo "用法: query_policy_tokens"3. 验证NFT真实性
# NFT真实性验证函数
verify_nft() {
local ASSET_ID="$1"
if [ -z "$ASSET_ID" ]; then
ASSET_ID="${POLICY_ID}.${NFT_NAME_HEX}"
fi
echo "🔍 验证NFT真实性: $ASSET_ID"
# 分离策略ID和资产名称
local VERIFY_POLICY_ID="${ASSET_ID%.*}"
local VERIFY_ASSET_NAME="${ASSET_ID##*.}"
echo " 策略ID: $VERIFY_POLICY_ID"
echo " 资产名称: $VERIFY_ASSET_NAME"
# 检查策略脚本是否存在
if [ -f "/keys/policy.script" ] && [ "$VERIFY_POLICY_ID" = "$POLICY_ID" ]; then
echo "✅ 策略验证通过 - 这是你创建的NFT"
# 验证策略ID是否匹配脚本
COMPUTED_POLICY=$(cardano-cli conway transaction policyid --script-file /keys/policy.script)
if [ "$COMPUTED_POLICY" = "$VERIFY_POLICY_ID" ]; then
echo "✅ 策略脚本验证通过"
else
echo "❌ 策略脚本验证失败"
fi
else
echo "⚠️ 这不是你创建的NFT,或策略脚本不可用"
fi
# 查询该NFT的当前持有者
echo ""
echo "🔍 查询当前持有者..."
cardano-cli query utxo --whole-utxo --testnet-magic 1 | \\
jq --arg policy "$VERIFY_POLICY_ID" --arg asset "$VERIFY_ASSET_NAME" '
| to_entries |
| --- |
| map(select(.value.value[$policy][$asset])) |
map({
holder: .value.address,
amount: .value.value[$policy][$asset]
})
'
}
echo ""
echo "💡 NFT验证功能已准备就绪"
echo "用法: verify_nft [资产ID]"故障排除指南
常见错误及解决方案
1. "ScriptWitnessNotValidatingUTXOW" 错误
原因分析:
- 策略时间锁定已过期
- UTXO已被其他交易消费
- 策略脚本签名问题
解决步骤:
# 检查策略是否过期
CURRENT_SLOT=$(cardano-cli query tip --testnet-magic 1 | jq .slot)
POLICY_EXPIRE=$(jq -r '.scripts[1].slot' /keys/policy.script)
if [ $CURRENT_SLOT -gt $POLICY_EXPIRE ]; then
echo "❌ 策略已过期,需要重新创建"
# 重新创建策略
EXPIRE_SLOT=$((CURRENT_SLOT + 86400))
# 更新策略脚本
jq --arg slot "$EXPIRE_SLOT" '.scripts[1].slot = ($slot | tonumber)' /keys/policy.script > /keys/policy_new.script
mv /keys/policy_new.script /keys/policy.script
# 重新生成策略ID
cardano-cli conway transaction policyid --script-file /keys/policy.script > /keys/policy.id
POLICY_ID=$(cat /keys/policy.id)
echo "✅ 策略已更新,新策略ID: $POLICY_ID"
else
echo "✅ 策略仍然有效"
fi
# 检查UTXO是否仍然可用
cardano-cli query utxo --address $ADDRESS --testnet-magic 12. "unexpected '.'" 语法错误
原因:资产名称包含特殊字符
解决方案:
# 使用十六进制编码
echo -n "YourNFTName" | od -A n -t x1 | tr -d ' \\n'
# 或使用预定义的十六进制值
NFT_NAME_HEX="4d794e4654" # "MyNFT"3. 节点同步问题
症状:查询命令返回错误或过时数据
解决方案:
# 检查节点状态
cardano-cli query tip --testnet-magic 1
# 如果同步进度不是100%,等待同步完成
while true; do
SYNC_PROGRESS=$(cardano-cli query tip --testnet-magic 1 | jq -r '.syncProgress')
echo "同步进度: $SYNC_PROGRESS"
if [ "$SYNC_PROGRESS" = "100.00" ]; then
echo "✅ 节点同步完成"
break
fi
sleep 30
done4. 内存或磁盘空间不足
检查资源使用:
# 检查磁盘空间
df -h
# 检查容器资源
docker stats cardano-node cardano-cli
# 清理不必要的文件
docker system prune -f调试工具和技巧
1. 交易详细分析
# 分析未签名交易
analyze_transaction() {
local TX_FILE="$1"
echo "🔍 分析交易文件: $TX_FILE"
# 使用cardano-cli解析交易
cardano-cli conway transaction view --tx-file "$TX_FILE"
echo ""
echo "📊 交易摘要:"
# 提取关键信息
local TX_JSON=$(mktemp)
cardano-cli conway transaction view --tx-file "$TX_FILE" --output-json > "$TX_JSON"
echo " 输入数量: $(jq '.body.inputs | length' "$TX_JSON")"
echo " 输出数量: $(jq '.body.outputs | length' "$TX_JSON")"
echo " 费用: $(jq '.body.fee' "$TX_JSON") Lovelace"
if jq -e '.body.mint' "$TX_JSON" > /dev/null; then
echo " 铸造资产: $(jq '.body.mint' "$TX_JSON")"
fi
rm "$TX_JSON"
}
# 使用示例
# analyze_transaction /keys/tx.unsigned2. 网络连接诊断
# 网络诊断函数
diagnose_network() {
echo "🌐 Cardano网络连接诊断"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 检查套接字连接
if [ -S "$CARDANO_NODE_SOCKET_PATH" ]; then
echo "✅ 节点套接字存在"
else
echo "❌ 节点套接字不存在: $CARDANO_NODE_SOCKET_PATH"
fi
# 检查节点响应
if cardano-cli query tip --testnet-magic 1 > /dev/null 2>&1; then
echo "✅ 节点响应正常"
# 获取网络信息
local TIP=$(cardano-cli query tip --testnet-magic 1)
echo " 当前区块: $(echo "$TIP" | jq -r '.block')"
echo " 当前纪元: $(echo "$TIP" | jq -r '.epoch')"
echo " 同步进度: $(echo "$TIP" | jq -r '.syncProgress')%"
else
echo "❌ 节点响应失败"
fi
# 检查协议参数
if cardano-cli query protocol-parameters --testnet-magic 1 > /dev/null 2>&1; then
echo "✅ 协议参数查询正常"
else
echo "❌ 协议参数查询失败"
fi
}3. 环境重置脚本
# 完整环境重置函数
reset_environment() {
echo "🔄 重置Cardano NFT铸造环境"
read -p "⚠️ 这将删除所有密钥和数据,确定继续吗?(y/N): " confirm
if [ "$confirm" != "y" ]; then
echo "❌ 操作已取消"
return 1
fi
# 备份现有数据
if [ -d "/keys" ]; then
local BACKUP_DIR="/keys_backup_$(date +%Y%m%d_%H%M%S)"
mv /keys "$BACKUP_DIR"
echo "📦 旧数据已备份到: $BACKUP_DIR"
fi
# 重新创建工作目录
mkdir -p /keys
cd /keys
# 重新设置环境变量
export CARDANO_NODE_SOCKET_PATH=/ipc/node.socket
echo "✅ 环境重置完成"
echo "💡 现在可以重新开始NFT铸造流程"
}最佳实践和安全建议
1. 密钥管理
# 安全的密钥备份脚本
backup_keys_securely() {
echo "🔐 安全备份密钥"
# 创建加密备份
local BACKUP_FILE="/keys/secure_backup_$(date +%Y%m%d_%H%M%S).tar.gz.gpg"
# 压缩关键文件
tar -czf - payment.skey payment.vkey policy.skey policy.vkey payment.addr policy.script policy.id metadata.json | \\
gpg --symmetric --cipher-algo AES256 --output "$BACKUP_FILE"
echo "✅ 加密备份已创建: $BACKUP_FILE"
echo "⚠️ 请记住加密密码!"
}
# 验证备份完整性
verify_backup() {
local BACKUP_FILE="$1"
echo "🔍 验证备份完整性: $BACKUP_FILE"
# 测试解密(不实际解压)
if gpg --decrypt "$BACKUP_FILE" | tar -tzf - > /dev/null 2>&1; then
echo "✅ 备份文件完整且可解密"
else
echo "❌ 备份文件损坏或密码错误"
fi
}2. 交易安全检查
# 交易提交前的安全检查
security_check() {
echo "🛡️ 交易安全检查"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# 检查网络环境
local NETWORK_MAGIC=$(cardano-cli query tip --testnet-magic 1 | jq -r '.networkMagic // empty')
if [ "$NETWORK_MAGIC" = "1" ]; then
echo "✅ 确认在测试网环境"
else
echo "⚠️ 网络环境异常"
fi
# 检查交易目标
echo "🎯 交易目标检查:"
echo " - 铸造策略: $POLICY_ID"
echo " - 目标地址: $ADDRESS"
echo " - NFT名称: $NFT_NAME ($NFT_NAME_HEX)"
# 检查余额充足性
local BALANCE=$(cardano-cli query utxo --address $ADDRESS --testnet-magic 1 | jq '[.[].value.lovelace] | add')
if [ "$BALANCE" -gt 1000000 ]; then
echo "✅ 余额充足: $((BALANCE / 1000000)) ADA"
else
echo "⚠️ 余额不足: $((BALANCE / 1000000)) ADA"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
}3. 性能优化建议
# 性能监控函数
monitor_performance() {
echo "📊 性能监控"
# 节点性能
echo "🖥️ 节点性能:"
docker stats cardano-node --no-stream --format "table {{.CPUPerc}}\\t{{.MemUsage}}\\t{{.NetIO}}\\t{{.BlockIO}}"
# 磁盘使用
echo ""
echo "💾 磁盘使用:"
docker exec cardano-node df -h /data/db
# 同步状态
echo ""
echo "🔄 同步状态:"
local TIP=$(cardano-cli query tip --testnet-magic 1)
echo " 同步进度: $(echo "$TIP" | jq -r '.syncProgress')%"
echo " 当前纪元: $(echo "$TIP" | jq -r '.epoch')"
}总结与后续学习
你已经掌握的技能
通过完成这个教程,你已经学会了:
- 🏗️ 基础设施搭建
- Docker环境配置
- Cardano节点部署
- 测试网络连接
- 🔐 密码学基础
- 公私钥对生成
- 数字签名原理
- 地址生成机制
- 📜 智能合约基础
- 策略脚本编写
- 时间锁定机制
- 原生代币铸造
- 💼 交易处理
- UTXO模型理解
- 交易构建和签名
- 网络提交流程
- 🎨 NFT标准
- CIP-25元数据格式
- 版税设置(CIP-27)
- 资产命名规范
以上这份详细的指南涵盖了从环境搭建到NFT铸造的完整流程,包含了故障排除、最佳实践和进阶学习路径。通过实际操作,你已经掌握了Cardano NFT开发的核心技能。