经过一段时间的准备,各种文档查看和代码验证,我觉得可以完成这个项目。所以在这里记录展示出来,目的是有很多开发的感悟, cardano太特殊了,lisp函数式样的技术路线。与以太坊的solidity语言开发智能合约是天差地别,很多人可能会犯和我一样的错误,走一些弯路,我觉得可能我的分享或许可以帮到一些人。
我计划几天发帖一部分开发或是感悟的内容,希望能在工作日每天更新,看情况吧。
废话不多说,我的开发技术工具清单说下
- vs code工具,很顺手,各种支持
- 智能合约语言: opshin
- 智能合约交互脚本包:pycardano
- web框架:react
- 钱包交互框架: Lucid
- python: 3.11
标题: cardano没有智能合约发布按钮,了解utxo(更新002)
开发过以太坊智能合约或是普通程序员在想要发布一个智能合约的时候肯定有一个按钮,就像是 remix里的“发布”。但是cardano没有,而是在代码里调用一个函数传入,而且智能合约脚本代码放在utxo中,如果需要执行这个智能合约 就标注下 指向这个智能合约地址;是不是很惊奇, 合约的发布和调用完全出乎普通开发的意料, 为啥是这样?不要急着找到原因,有些基础知识先要了解,否则会晕掉。
先来看一个图
cardano的网络其实就是utxo拼装组合的一个网络,满满的utxo, 所有的东西都塞在里面。UTXO全称: Unspent Transaction Output , 未花费的交易输出,是不是有点懵,没关系,满满看下去。
举例说,看上面的图,若干个input对应若干个output, 当然可以若干个。 假设A用户有10ADA 发送给B 3ada,发送给C 7ada,那么可以拼装成上面的图的那种utxo,一个input,两个output。注意只能 10 ada input, 如果output 加起来不是10ada, 11ada或是9ada都会失败,必须10ada。看上去也没啥高深的知识么,是的,很简单。这个是原则,合约编写一定基于这个基础的。
除了这个,utxo里还有啥,看下图
utxo里有智能合约,是那种可以执行的脚本, CBOR脚本, 全称:Concise Binary Object Representation,【 简明二进制对象】 ,像我们平时学的合约语言最终都会编译成cbor上链,不管是 plutus/ aiken或是opshin。
还有一个就是datum,这个你就理解为存在链上的常量,一直保存在那边的,不能修改。
今天就先说这么些吧。
标题: 如何查看链上的智能合约脚本 (更新003)
每次提交脚本后,我们一般在cardano scan上查看,提交合约后有返回一个tx,就是交易编号,获取编号在https://preprod.cardanoscan.io 查询。这里查看一个我的例子
https://preprod.cardanoscan.io/transaction/2af9b342e92a88be47da9e41eba470da23c7a860b9c420ec4e5a1c4b1cdf1e21?tab=utxo
这个就是datum,上次说的保存在链上的常量。
这里的script就是智能合约的脚本,链上执行的合约就是这个。
下次整理一个全流程最小的最简单的智能合约和可以发布的脚本的github项目。
标题:创建合约上链需要的第一个对象 Context (更新004)
context = .......
builder = TransactionBuilder(context)
上面的没有完成的代码里context 有几种途径。
第一种,使用blockfrost.com提供,第二种使用ogmios, 第三种使用kupo。
那context是什么? 就和以太坊里 Alchemy作用类似。blockfrost.com可以网上注册就可以。ogmios和kupo在Demeter - The turnkey solution for Cardano dApp infrastructure
blockfrost获取context代码
blockfrost_key = os.getenv("BET_BLOCKFROST_PROJECT_ID", None)
network = Network.TESTNET
def get_blockfrost_chain_context():
return BlockFrostChainContext(
project_id=blockfrost_key,
network=network,
)
ogmios获得context
from pycardano import Network
from pycardano import (
BlockFrostChainContext,
KupoOgmiosV6ChainContext,
OgmiosV6ChainContext,
)
ogmios_host = os.getenv("OGMIOS_API_HOST", "localhost")
ogmios_port = os.getenv("OGMIOS_API_PORT", "443")
ogmios_protocol = os.getenv("OGMIOS_API_PROTOCOL", "wss")
ogmios_url = f"{ogmios_protocol}://{ogmios_host}:{ogmios_port}"
def get_OGMIOS_chain_context():
return OgmiosV6ChainContext(
host=ogmios_host,
port=int(ogmios_port),
secure=True,
network=network,
)
注意:这里ws协议端口是1337,wss端口是443.
ogmios的context对象函数更多一点。
最后再补充下,如果你想获取更多,更灵活的cardano链上数据,可以使用Cardano DB-Sync
。
三种input , 消费utxo的地方 (更新005)
突然发现上面的context没有说仔细,context就是一个真正通往cardano主链的通道接口。
好进入正题,先看下代码
builder = TransactionBuilder(context)
builder.add_input_address(source_address)
builder.add_script_input(utxo_to_spend, gift_script, datum, redeemer)
builder.reference_inputs.add(sc_utxo)
所有的合约都要消费utxo的,如果没有utxo就没有智能合约的“出生证”,而这里要说的三种input就是出生证。
- 第一个是 add_input_address(address) ,这里是普通的utxo,在这个address上由算法选择那个或是哪些utxo作为可以消费的utxo
- 第二个是add_script_input , 这个是如果需要调用智能合约的脚本,就需要用这个入口,这里utxo_to_spend一定是有datum的,否则会报错。utxo_to_spend是具体的utxo
- 第三个是reference_inputs,这个是唯一不能消费的utxo,但是它非常重要,一般的预言机oracle就是用这个,用这个的datum。当然他也是一个script address的utxo。
今天东西不多,但是都是非常有用的知识点,cardano的智能合约开发其实真正代码的知识点不多,主要是围绕合约的代码, 如果代码是2,那么周边就是8。
铺垫了一些基础知识,但是如果没有代码把玩估计会很无趣,后面我出一个简短的全流程合约github 项目,一个基于官方改的。不过好像我还要说一期redeemer
一个完整的全流程的cardano智能合约可运行的github repository(更新006)
不多废话,直接访问 GitHub - malakaw/publish_gifts
里面是两个简单的智能合约执行方式的不同实现例子。
花了我半天时间,是可以运行的,完整的,不过准备的东西比较多,还要装cardano-cli。
具体的内容不再这里说,具体看github.
下注网站的预言机 C3目前啥情况 (更新007)
说了这么些技术的知识点,好像对于下注网具体业务的还没有提及 ,今天就来一段这块的整理。说到下注那必须要有预言机来触发或是cardano 的智能合约必须要用的预言机信息。cardano上目前可行的oracle就只有 C3了。预言机主要分两类,一类是Push Oracle Node****,另一类是Pull Oracle Node (ODV),第一类其实就是主要指各种币token的价格, 第二类类似我们需求的下注类信息,单个的信息验证类。目前的情况看c3的文档Charli3 Docs - Technical Guide of Oracle Integration 第一种已经上生成,第二种我们要的pull模式还是Alpha状态,相关的Charli3 ODV nodes搭建的文档都是没有的, 后来我不死心,通过电报上的 cardnao founder lab群协助联系到c3官方,好像也是没啥消息,就问我为啥要Charli3 ODV nodes,然后没有正式恢复。好吧,死心了。那就用我自己手动oracle了。
如果要做一个有竞争力的bet Dapp那么就必须要用Hydra (更新008)
了解cardano 的一段时间的应该是都知道Hydra-九头蛇,据说是可以大幅度提高性能。是的今天我来说说Hydra。
在说Hydra之前,先提一下polymarket, 我用过,战绩惨淡,目前下注的2025年cardano是否EFT通过估计要黄了。目前polygan的作为以太坊的lay2速度和交易费用肯定是各种便宜和高速的。那在cardano上如果做一个bet网站需要长时间等待交易完成肯定是没啥意思的,何必呢,慢又贵,相比polymarket。好吧,我是这么想,我也想做bet网站dapp,但是实话实说,我没想过做另外一个polymarket, 我只是想给我的cardano stake pool : cmore pool 的用户提供各种趣味性,或则吸引潜在的用户通过bet dapp来质押到我的cmore pool。但是不能因为是mini 版本的下注网站,就考虑产品设计的合理性,对不。所以,这就是我考虑hydra 的原因。但是估计第一个版本不会使用hydra.
好的,现在开始讲hydra,我刚开始的时候寻找各种平台有没有免费的测试环境的hydra, 后来发现是没有,好吧,自己安装,Getting started | Hydra Head protocol documentation 参考这个,用docker安装,比我想象中简单,很容易。不过,后来没做太多测试。
总结下我的学习知识点或是内容吧, hydra可以让我有免费的uxto来消费,其实hydra就是lay2, 如果你不提交的主网,那你就不需要付费,在提交主网前,你的各种操作和在操作主网的一摸一样。在类似polymarket的用户只有在sell和claim 的时候真正最后提交到主网,之前都可以在hydra上操作,各种费用都是0. 还有在Hydra Head 初始化(开头)的时候也要付费。这个是费用的理解。 那速度呢? 不用上主网,不需要各个cardano node 来确认,那么不就是毫秒级别的速度么,超级快。那这样安全么?想象一下吧, polygan不就是这么干的么,lay2; 当然一定是一定规模hydra才是安全的,这个肯定;但是cardano目前是可以做到这些。
为啥用Opshin在合约里不能验证签名cardano-cli生成的钱包签名(更新009)
记录下,最近测试给合约传redeemer里有签名信息,但是在用opshin的verify_ed25519_signature验证签名的时候死活不行。后来问下chatGPT,了解了这里记录下。
总的来说就是cardnao-cli生成的是“助记词”钱包,一种Ed25519-extended 钱包,不纯的ed25519。不过后来也查到可以cardnao-cli生成纯 ed25519钱包,就是命令再带上一个参数“ --normal-key”。不管怎么说,所以后来我用pycardano生成纯的ed25519钱包就可以verify_ed25519_signature验证签名信息了。
下面是pycardano钱包生成代码。
from pycardano import (
PaymentSigningKey,
PaymentVerificationKey,
Address,
Network
)
# 生成 Ed25519 私钥
sk = PaymentSigningKey.generate()
vk = PaymentVerificationKey.from_signing_key(sk)
# 将 keys 保存为文件
with open("oracle.sk", "wb") as f:
f.write(sk.to_cbor())
with open("oracle.vk", "wb") as f:
f.write(vk.to_cbor())
# 生成地址
address = Address(payment_part=vk.hash(), network=Network.TESTNET)
with open("oracle.addr", "w") as f:
f.write(str(address))
print("Private key (hex):", sk.to_cbor().hex())
print("Public key (hex):", vk.to_cbor().hex())
print("Address:", address)
放弃Lucid,拥抱meshjs(更新010)
还是那个原因,文档和使用例子较多。下面奉上reference 执行合约的例子,官方目前也没有提供的。我这个折腾老长时间。
提交ada和合约script到链上
const script: PlutusScript = {
code: cbor
.encode(Buffer.from(plutusScript.validators[0].compiledCode, "hex"))
.toString("hex"),
version: "V3",
};
const scriptAddress = resolvePlutusScriptAddress(script, 0);
async function sendLMSR(){
console.log("connected:",connected)
if(connected){
console.log("scriptAddress:",scriptAddress)
const address = (await wallet.getUsedAddresses())[0];
const addressPubKey = deserializeAddress(address);
const changeAddress = await wallet.getChangeAddress();
const utxos = await wallet.getUtxos();
const unsignedTx = await txBuilder
.spendingPlutusScriptV3()
.setNetwork("preprod")
.txOut(scriptAddress, [{ unit: "lovelace", quantity: "15300000" }])
.txOutInlineDatumValue(mConStr0([addressPubKey.pubKeyHash]))
.txOutReferenceScript(script.code,"V3")
.changeAddress(changeAddress)
.selectUtxosFrom(utxos)
.complete();
const signedTx = await wallet.signTx(unsignedTx);
const txHash = await wallet.submitTx(signedTx);
console.log("txHash", txHash);
}
}
reference方式经济执行合约
const unsignedTx = await txBuilder
.spendingPlutusScriptV3()
.txIn(assetUtxo!.input.txHash, assetUtxo!.input.outputIndex)
.txInInlineDatumPresent()
.txInRedeemerValue(mConStr0([]))
.txOut(test_receive_address, [{ unit: "lovelace", quantity: "1700000" }])
.spendingTxInReference(reference_script_tx,0,undefined,scriptHash_)
.changeAddress(changeAddress)
.txInCollateral(
collateral[0]?.input.txHash,
collateral[0]?.input.outputIndex,
collateral[0]?.output.amount,
collateral[0]?.output.address,
)
.requiredSignerHash(addressPubKey.pubKeyHash)
.complete();
const signedTx = await wallet.signTx(unsignedTx, true);
const txHash = await wallet.submitTx(signedTx);