元宇宙应用开发实例——以太坊里的智能合约和Decentraland里的3D前端交互组件
从核心技术上来看,其实`元宇宙`、`Web3.0`和这个`全真互联`都是一回事儿,都是前端和后端两方面技术发展的产物:- 随着前端交互技术(既包括`软件渲染技术`,也包括`硬件交互设备`)的发展,互联网从只能在PC上看PGC的Web1.0,发展到还可以在手机上看UGC的Web2.0。到了今天,发展出了又能在各种智能穿戴设备上,用不同的人类感官,去交互三维内容的Web3.0,也就是元宇宙。- 内容的
目录
1. 元宇宙核心技术
腾讯最近发布了一个全真互联白皮书,虽然他们强调全真互联
跟元宇宙
不同,但怎么看都像是无奈之下的牵强附会。从核心技术上来看,其实元宇宙
、Web3.0
和这个全真互联
都是一回事儿,都是前端和后端两方面技术发展的产物:
- 随着前端交互技术(既包括
软件渲染技术
,也包括硬件交互设备
)的发展,互联网从只能在PC上看PGC的Web1.0,发展到还可以在手机上看UGC的Web2.0。到了今天,发展出了又能在各种智能穿戴设备上,用不同的人类感官,去交互三维内容的Web3.0,也就是元宇宙。 - 内容的种类和规模的增加,必然要求后端计算、存储、网络的处理能力的增强。
云计算全栈软硬件技术
的不断演进支持了Web1.0、2.0到3.0的变化,只是到了3.0,也就是元宇宙,后端又增加了另一项配合社会形态转变的智能合约
技术(比如NFT-非同质数字化资产、数字货币等),智能合约当然不见得要跟区块链连在一起,但区块链确实是实现它的一种有效方式。
腾讯的全真互联确实刻意的回避了智能合约,这也不奇怪,乖巧如腾讯者深谙个中道理,不可能碰这种危险的东西,这也是为啥国内元宇宙初创公司大多数都集中在前端技术的原因,而且腾讯自己是Web2.0的既得利益者,商业上,显然也不愿意去掉包括自己在内的这些中心互联网平台。
为了从技术上实操完整的元宇宙应用开发,本文选择Decentraland作为做实验的平台。
2. 元宇宙实例及应用实例
关于本实例完整的业务模式描述,请参见前面的文章。本文是从技术角度记录下验证的实际开发过程。
Decentraland是一个基于以太坊区块链实现的分布式虚拟现实平台。它是一个高度符合元宇宙定义的虚拟世界,这个世界的成员可以在它的土地
上创建内容
和应用
,体验
他人的内容和应用,用自己的内容和应用赚钱
。Decentraland中的土地是用以太坊智能合约维护的一种NFT,里面的空间是3D的,用户可以在里面游逛,土地总数有限。土地被分为Parcel
,Parcel用坐标来标识,这些Parcel被这个世界的成员们永久持有,可以用MANA(马那币)交易,MANA是Decentraland的官方加密数字货币。用户可以全权控制他们的土地,在上面用现成或自定义的3D前端组件,创建静态3D场景或可交互的游戏和应用。
本文实现的就是一个自定义的3D可交互前端组件(在Decentraland叫做Smart Item
),组件里包含由一个以太坊智能合约维护的、可交互的广告内容。这个合约是个4方合约,参与方有组件运营者
、土地所有者
、广告发布者
、消费者
。整个合约发生作用的过程如下:
- 组件运营者把组件发布到
Decentraland Builder
里 - 广告发布者将广告发布到组件上
- 土地所有者在Decentraland Builder里把组件装配在自己土地上的建筑上
- 消费者来这个建筑里,点击广告,广告发布者按约定的金额,支付MANA给土地所有者和组件运营者
- 消费者打开广告后,发生了购买行为,广告发布者按约定的金额,支付MANA给消费者
3. 以太坊里的智能合约开发
3.1. World Wide Web的访问能力
首先,使用代理或其他方式,获得World Wide网络访问能力。
3.2. 初始化以太坊钱包
- 在
chrome应用商店
中安装chrome插件形式
的加密货币钱包MetaMask,并依提示创建默认账户。 - 在chrome右上角的
MetaMask插件界面
中,将默认账户的名字改为“组件运营者”。 - 继续创建三个账户,分别命名为“土地所有者”、“广告发布者”、“消费者”。
- 将当前所在链从
以太坊Ethereum主网络
,切换至Ropsten测试网络
。注意:这个测试网络只能用到22年Q4末,随着以太坊完成了“Merge”,此测试网络将下线,测试需要切换至新网络,建议使用Sepolia test network
,不过,现在Decentraland还不支持这个新测试网络。 - 打开Ropsten测试网络的代币免费获取页面,将账户依次切换至“组件运营者”、“广告发布者”、“消费者”,为这三个账户分别充值1个
以太币
,转账需要在MetaMask面板上点击确定。这些以太币是用于支付在以太坊上执行交易所需支付的成本。如果使用“Sepolia test network”,则需要使用Sepolia的代币免费获取页面,不过它的代币到账时间需要大概2个小时。 - 打开Decentraland在Ropster上的MANA测试代币免费获取页面,为“广告发布者”账户充值MANA代币,转账需要在MetaMask面板上点击确定。这些MANA是4方合约在执行交易时使用的货币。
3.3. 开发4方合约
以太坊智能合约
开发使用的语言是Solidity,使用Solidity让人想起用c++写系统软件,对内存精细规划,对代码极致雕琢。这是因为合约要被存储到链上并在链上执行,代码体积和执行内存越大,消耗的资源越多,要付的钱就越多。它必须很干净,能删除掉的都要删掉,注释不要有,甚至变量名都应该越短越好。本文中代码注释是为了说明逻辑,实际生产运行时一定要删掉注释,缩短变量名。
合约的开发工具为Remix。Remix可进行本地部署,但验证过程就直接使用SaaS版本了。打开Remix,越过欢迎界面,进入开发界面,在左侧选择File explorer
,然后在contracts
文件夹下,创建一个新的Solidity程序文件,命名为dmall.sol
(此实例中的合约名字定为DMall
),并将以下代码复制进文件中:
//SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.7.0 <0.9.0;
// 声明MANAToken的接口,用于DMall合约中调用
interface MANAToken {
// 广告所有者必须在发布广告之前,调用MANAToken的这个接口,允许DMall从他的MANA账户里划转代币到其他账户,其中_sender参数为DMall部署后的地址,_value参数为允许划转的MANA代币数量的上限
function approve(address _sender, uint256 _value) external returns (bool);
// DMall合约会调用这个接口,将MANA代币从广告所有者转账到其他三个账户,_from是广告所有者,_to是其他三个账户,_value是转账金额
function transferFrom(address _from, address _to, uint256 _value) external returns (bool);
}
contract DMall {
// 广告的状态枚举,广告所有者首次调用uptAd接口创建接口后为Created,组件运营者调用apprAd接口同意后变为Approved,广告所有者再次调用uptAd接口后为Updated,此时还需要组件运营者调用apprAd接口。只有Approved状态的广告可以被看到和购买。Undefined是初始状态,无意义,Invalid是广告因为其他原因(如广告发布者没给MANA)失效。
enum AdState { Undefined, Created, Approved, Updated, Invalid }
// 广告点击记录的状态,Undefined是初始状态,无意义,Clicked表示消费者点击过了(即调用clickAd),Bought表示消费者买过了(即调用buyAd)。
enum ClickState { Undefined, Clicked, Bought }
// 在链上存储的一条广告的数据结构
struct Ad {
// 广告点击时,广告发布者给土地所有者的MANA数量
uint m2L;
// 广告点击时,广告发布者给组件运营者的MANA数量
uint m2O;
// 广告被购买时,广告发布者给消费者的MANA数量
uint m2C;
// 广告的状态
AdState state;
// 广告的点击记录数据集合,第一个地址是土地所有者的地址,第二个地址是消费者的地址,最终里面存的数据是一条点击数据的状态
mapping(address => mapping(address => ClickState)) clicks;
}
// MANAToken合约的地址
MANAToken manaToken;
// 广告数据集合,第一个地址是广告发布者的地址,第二个整数是广告ID,数据是上面的Ad结构体
mapping(address => mapping(uint => Ad)) public ads;
// 合约运营者,就是组件运营者的地址
address public op;
// 合约构造函数,用调用者的地址初始化合约运营者地址,用输入的MANAToken合约地址初始化对应数据,由所在的链是生产链还是测试链决定
constructor(address _manaToken) {
op = msg.sender;
manaToken = MANAToken(_manaToken);
}
// 因为链上的操作都是异步的,需要存在链上才算成功,以下事件是对应接口调用成功后的回调,可在前端应用程序中实现
// 广告创建成功后的事件(uptAd接口首次调用后的事件)
event AdCrted(address merchant, uint adId, uint m2L, uint m2O, uint m2C);
// 广告更新成功后的事件(uptAd接口再次调用后的事件)
event AdUpted(address merchant, uint adId, uint oM2L, uint oM2O, uint oM2C, uint m2L, uint m2O, uint m2C);
// 广告批准成功后的事件(apprAd接口调用后的事件)
event AdAppred(address merchant, uint adId);
// 广告点击后的事件(clickAd接口调用后的事件)
event AdClicked(address merchant, uint adId, address landowner, address consumer);
// 广告失效事件(clickAd接口或buyAd接口中发生失败后的事件)
event AdInvalid(address merchant, uint adId);
// 广告被购买事件(buyAd接口调用后的事件)
event AdBought(address merchant, uint adId, address landowner, address consumer);
// 创建或更新广告,由广告发布者调用,_adId是广告的ID,_m2L是广告发布者愿意给土地所有者的MANA数量,_m2O是广告发布者愿意给组件运营者的MANA数量,_m2C是广告发布者愿意给消费者的MANA数量
function uptAd(uint _adId, uint _m2L, uint _m2O, uint _m2C) public {
// 初始化一个Ad对象
Ad storage ad = ads[msg.sender][_adId];
if (ad.state == AdState.Undefined) {
// 如果这是个新广告,则设置参数,并触发AdCrted
ad.state = AdState.Created;
ad.m2L = _m2L;
ad.m2O = _m2O;
ad.m2C = _m2C;
emit AdCrted(msg.sender, _adId, _m2L, _m2O, _m2C);
}
else {
// 如果不是新广告,则记录当前参数,设置新参数,并触发AdUpted
ad.state = AdState.Updated;
uint oM2L = ad.m2L;
uint oM2O = ad.m2O;
uint oM2C = ad.m2C;
ad.m2L = _m2L;
ad.m2O = _m2O;
ad.m2C = _m2C;
emit AdUpted(msg.sender, _adId, oM2L, oM2O, oM2C, _m2L, _m2O, _m2C);
}
}
// 批准广告,由组件运营者调用, _merchant是广告发布者地址,_adId是广告的ID
function apprAd(address _merchant, uint _adId) public {
// 要求只能是组件所有者调用
require(msg.sender == op, "Only DMall operator can approve the deployment of Ad");
// 要求广告不是初始状态和批准过的状态
require(ads[_merchant][_adId].state != AdState.Undefined && ads[_merchant][_adId].state != AdState.Approved , "The ad of merchant with the ID does not exist or has been approved");
// 设置广告状态
ads[_merchant][_adId].state = AdState.Approved;
// 触发AdAppred事件
emit AdAppred(_merchant, _adId);
}
// 点击广告,由消费者调用,_merchant是广告发布者地址,_adId是广告的ID,_landowner是广告所在土地的土地所有者
function clickAd(address _merchant, uint _adId, address _landowner) public {
// 需要广告是Approved的
require(ads[_merchant][_adId].state == AdState.Approved, "The ad of merchant with the ID has not been approved");
// 需要广告不是已被此消费者点击状态
require(ads[_merchant][_adId].clicks[_landowner][msg.sender] != ClickState.Clicked, "The clicking had been paid but does not lead to buying yet");
// 广告发布者按约定给土地所有者和组件运营者转账
if (manaToken.transferFrom(_merchant, _landowner, ads[_merchant][_adId].m2L) && manaToken.transferFrom(_merchant, op, ads[_merchant][_adId].m2O) ) {
// 如果转账成功,则标记点击状态,触发AdClicked
ads[_merchant][_adId].clicks[_landowner][msg.sender] = ClickState.Clicked;
emit AdClicked(_merchant, _adId, _landowner, msg.sender);
}
else {
// 转账失败,则标记广告失效,触发AdInvalid
ads[_merchant][_adId].state = AdState.Invalid;
emit AdInvalid(_merchant, _adId);
}
}
// 购买广告商品,由消费者调用,_merchant是广告发布者地址,_adId是广告的ID,_landowner是广告所在土地的土地所有者
function buyAd(address _merchant, uint _adId, address _landowner) public {
// 需要消费者点击过此广告
require(ads[_merchant][_adId].clicks[_landowner][msg.sender] == ClickState.Clicked, "The consumer does not click the ad or has bought");
// 广告发布者转账给消费者
if (manaToken.transferFrom(_merchant, msg.sender, ads[_merchant][_adId].m2C)) {
// 转账成功,则将广告点击记录标记为Bought,并触发AdBought
ads[_merchant][_adId].clicks[_landowner][msg.sender] = ClickState.Bought;
emit AdBought(_merchant, _adId, _landowner, msg.sender);
}
else {
// 转账失败,则标记广告失效,触发AdInvalid
ads[_merchant][_adId].state = AdState.Invalid;
emit AdInvalid(_merchant, _adId);
}
}
}
3.4. 手动运行DMall智能合约
- 在chrome右上角MetaMask插件的面板里,确保切换到“Ropsten测试网络”链和“组件运营者”账号
- 在Remix左侧,选择
Solidity Compiler
,点击Compile dmall.sol
按钮 ENEIRONMENT
选择Injected Provide - Metamask
,CONTRACT
选择DMall
- 在Decentraland中的智能合约地址页面,找到Ropsten网络的MANAToken地址,即
0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb
- 将上述地址填写进
Deploy
按钮后的输入框,然后点击Deploy按钮,等待MetaMask面板上弹出确认
并点击,完成DMall合约部署 - 重选选择CONTRACT为
MANAToken
,将前面的MANAToken地址填入At Address
按钮后的输入框,然后点击At Address按钮,等待MetaMask面板上弹出确认并点击,完成MANAToken合约在Remix上的显示,这个不是我们部署的,只是为了能调用接口 - 在MetaMask面板上,切换至广告发布者账号
- 在
Deployed Contracts
下,找到并复制DMALL
合约的地址,然后打开MANATOKEN
合约,打开approve
接口,输入_sender
为上面复制的DMALL地址,_value
输入个大数,如100000。点击transact
按钮,等待MetaMask面板上弹出确认并点击,完成广告发布者对DMALL合约的授权 - 打开DMALL合约的
uptAd
接口,输入_adId
=1,_m2L
=11,_m2O
=12,_m2C
=13,点击transact
按钮,等待MetaMask面板上弹出确认并点击,完成广告创建 - 在MetaMask面板上,复制广告发布者的地址,然后切换至组件运营者账号
- 打开DMALL合约的
apprAd
接口,输入_merchant
为刚才复制的地址,_adId
为1,点击transact
按钮,等待MetaMask面板上弹出确认并点击,完成广告批准 - 在MetaMask面板上,切换至消费者账号
- 打开DMALL合约的
clickAd
接口,输入_merchant
为广告发布者地址,_adId
为1,_landowner
为土地所有者的地址,点击transact
按钮,等待MetaMask面板上弹出确认并点击,完成广告点击 - 此时,可以通过MetaMask面板上的账户详情页面,查看以太币和MANA币的各账户变化情况
- 打开DMALL合约的
buyAd
接口,输入_merchant
为广告发布者地址,_adId
为1,_landowner
为土地所有者的地址,点击transact
按钮,等待MetaMask面板上弹出确认并点击,完成广告购买 - 此时,可以再通过MetaMask面板上的账户详情页面,查看以太币和MANA币的各账户变化情况
3.5. Python调用DMall智能合约
- 创建可供代码调用的以太坊API接口地址
- 在infura页面上,注册用户并登录,选择右上角的
Dashboard
进入 - 选择右上角的
CREATE NEW KEY
按钮 - 网络选择
Web3 API
- 在
Ethereum
下,从MAINNET
切换到Ropsten
,不过现在Ropsten,已经消失了,大家可以按照以下格式,将key替换进去即可,或者直接用也行:https://ropsten.infura.io/v3/dd4cc999659f448d905400a4e8fb4e9d
- 在infura页面上,注册用户并登录,选择右上角的
- 安装PyCharm
- 创建合约接口文件
- 在PyCharm中创建一个工作区,在里面增加一个
contract_abi.py
文件 - 在Remix左侧,选择
Solidity Compiler
,CONTRACT选择DMALL,点击下方的ABI复制按钮
- 在contract_abi.py中,输入
dmall_abi = """"""
,在六个分号的正中间粘贴上面的复制内容 - 在MANAToken的代码页面,点击
Contract ABI
部分右上角的Copy ABI to clipboard
- 在contract_abi.py中,换行输入
manatoken_abi = """"""
,在六个分号的正中间粘贴上面的复制内容
- 在PyCharm中创建一个工作区,在里面增加一个
- 在MetaMask面板中,导出广告发布者的Private Key
- 在PyCharm中,为项目安装Web3包(pip install web3),创建dmalltest.py,粘贴以下代码,按注释调试验证合约的部分接口
import time
from web3 import Web3, HTTPProvider
from web3.logs import IGNORE
import contract_abi
# DMall合约的地址,从Remix中获得,输入单引号之间
dmall_address = Web3.toChecksumAddress('')
# MANAToken合约的地址
manatoken_address = Web3.toChecksumAddress('0x2a8fd99c19271f4f04b1b7b9c4f7cf264b626edb')
# 广告发布者的Private Key,将前文导出的值输入单引号之间
merchant_private_key = ''
# 广告发布者的账号地址,从MetaMask面板中获得,输入单引号之间
merchant_address = ''
# 土地所有者的账号地址,从MetaMask面板中获得,输入单引号之间
landowner_address = ''
# 消费者的账号地址,从MetaMask面板中获得,输入单引号之间
consumer_address = ''
# 以太坊API地址,将前文infura获取的https地址输入单引号之间
w3 = Web3(HTTPProvider(''))
# 初始化DMall和MANAToken合约的接口和事件
dmall_contract = w3.eth.contract(address=dmall_address, abi=contract_abi.dmall_abi)
manatoken_contract = w3.eth.contract(address=manatoken_address, abi=contract_abi.manatoken_abi)
# 广告所有者在MANAToken合约上批准DMall合约对其账户的MANA进行操作
def m_appr_dmall(dmall, amount):
nonce = w3.eth.getTransactionCount(merchant_address)
# 创建MANAToken上的一个交易,内容就是执行approve接口
txn_dict = manatoken_contract.functions.approve(dmall, amount).buildTransaction({
'chainId': 3,
'gas': 140000,
'gasPrice': w3.toWei('40', 'gwei'),
'nonce': nonce,
})
# 用广告所有者的Private Key给上述交易签名
signed_txn = w3.eth.account.signTransaction(txn_dict, private_key=merchant_private_key)
# 执行交易
result = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
# 轮询等待交易成功
tx_receipt = None
count = 0
while tx_receipt is None and (count < 30):
time.sleep(10)
try:
tx_receipt = w3.eth.get_transaction_receipt(result)
print(tx_receipt)
except Exception as e:
print('error: ', e)
if tx_receipt is None:
return {'status': 'failed', 'error': 'timeout'}
# 处理异步返回的事件
processed_receipt = manatoken_contract.events.Approval().processReceipt(tx_receipt, errors=IGNORE)
print(processed_receipt)
return {'status': 'added', 'processed_receipt': processed_receipt}
# 广告所有者发布广告
def m_pub_ad(ad_id, m_2_l, m_2_o, m_2_c):
nonce = w3.eth.getTransactionCount(merchant_address)
# 创建DMall上的一个交易,内容就是执行创建广告的接口
txn_dict = dmall_contract.functions.uptAd(ad_id, m_2_l, m_2_o, m_2_c).buildTransaction({
'chainId': 3,
'gas': 140000,
'gasPrice': w3.toWei('40', 'gwei'),
'nonce': nonce,
})
# 用广告所有者的Private Key给上述交易签名
signed_txn = w3.eth.account.signTransaction(txn_dict, private_key=merchant_private_key)
# 执行交易
result = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
# 轮询等待交易成功
tx_receipt = None
count = 0
while tx_receipt is None and (count < 30):
time.sleep(10)
try:
tx_receipt = w3.eth.get_transaction_receipt(result)
print(tx_receipt)
except Exception as e:
print('error: ', e)
if tx_receipt is None:
return {'status': 'failed', 'error': 'timeout'}
# 处理异步返回的事件
processed_receipt = dmall_contract.events.AdCrted().processReceipt(tx_receipt)
print(processed_receipt)
return {'status': 'added', 'processed_receipt': processed_receipt}
if __name__ == "__main__":
# 可分别打开以下两行注释符号,执行对应函数,验证合约执行情况
m_appr_dmall(dmall_address, 100)
#m_pub_ad(2, 21, 22, 23)
4. Decentraland里的Smart Item开发
Decentraland使用Builder(需使用以太坊Ethereum主网络登录)搭建Scene
,Scene被部署到实际的Land
中,就成为建筑物。搭建Scene的组件有普通和Smart Item两种,Smart Item就是可以响应用户操作,执行代码的组件。可从github上获取现有Smart Item代码,修改代码,验证合约执行:
git clone git@github.com:decentraland/smart-items.git
可以选择一个现有的Smart Item,在其item.ts
源代码的spawn
函数中,增加如下代码,调用智能合约
ent.addComponent(
new OnPointerDown(
async function () {
const provider = await getProvider()
const requestManager = new RequestManager(provider)
const factory = new ContractFactory(requestManager, abi)
// 需将从Remix中获取的DMall合约地址,输入双引号之间
const contract = (await factory.at(
""
)) as any
const address = await getUserAccount()
log(address)
const res = await contract.clickAd(
// 需将从MetaMask中获取的广告发布者账号地址,输入双引号之间
"",
0,
// 需将从MetaMask中获取的土地所有者账号地址,输入双引号之间
"",
{
from: address,
}
)
log(res)
// 打开外部的商品购买链接
openExternalURL("https://item.jd.com/10045659650093.html")
},
{
button: ActionButton.PRIMARY,
hoverText: locationString,
}
)
)
接下来可按如下步骤上传Smart Item到Builder进行验证使用:
- 安装Node.js
- 安装Decentraland
npm install -g decentraland
- 到对应的Smart Item源代码文件夹下,在本地启动进行交互验证
dcl install
dcl start
- 也可以打包上传到Builder使用
dcl pack
更多推荐
所有评论(0)