1,摘要
【本文目标】
通过本文,可以从一个HiBlock黑客马拉松活动门票定制,转让,出售和签到为例,说明ERC875的设计初心,ERC875的标准接口分析,也给出了官网的ERC875的代码和本地测试,便于更多项目使用ERC875解决区块链业务中遇到的实际问题。
【前置条件】
(1)体验门票受让的用户不需要有任何技术门槛;
(2)做门票定制和开发的需要本地已安装好MetaMASK,在Reposton Test Net获取了几个测试ETH(免费)的,要懂Solidity语言。
不熟悉的建议参考文档《第六课 技术小白如何开发一个DAPP区块链应用(以宠物商店为例)》的“5. 安装 MetaMask和配置区块链网络”章节。
2,Hiblock黑客马拉松区块链门票全体验
2.1 门票定制创建 - [辉哥]
ALPHA WALLET团队已经封装好了ERC785协议实现,可以通过浏览器完成票务类ERC875的智能合约创建。对应的TOKEN工厂网址为https://alpha-wallet.github.io/ERC875-token-factory/index.html
测试使用,MetaMASK选择的测试网络为"Ropsten Test Net"。
1) “Deploy Contract”
定义名称和标识,对应的地址是以太坊钱包地址。Owner Address必须为MetaMast的当前账号地址,然后点击“Deploy Contract”按钮。[名称和标识命名跟一般使用的搞反了,将就用吧]
Contract Name: HHT
Ticket Symbol: Hiblock Hackathon Ticket
Owner Address:0xB51Fa936B744CFEbAeD8DbB79d2060903e689F89
Recipient Address:0xB51Fa936B744CFEbAeD8DbB79d2060903e689F89
2)“Submmit”按钮
“Gas Price”设置为30,点击“Submmit”按钮。该账号要有一定的ETH测试币,否则点击"Buy"找平台免费买点。
3)购买成功确认
购买成功的会有弹出提示。点击“确定”按钮后,拉到下方的按钮可以查看智能合约部署链接和ABI合约信息。
4)查看部署合约成功地址
点击可知其部署成功:https://ropsten.etherscan.io/address/0x07fc44d796d30b317013cb907fadb6d738f5779e
2.2 安装APP,导入钱包,导入门票 - [辉哥]
1) 安装APP
辉哥在官网(https://awallet.io/)下载APP完成安装。
2) 导入钱包
点击配置页面,更换网络为"Ropsten(Test)"网络,导入创建门票的钱包私钥。
3)添加代币
输入之前的智能合约地址,符号和名称会自动联想出来的。
导入成功后,钱包页面可以看到对应的通证信息。如果是没有这个资产的钱包导入这个通证,钱包页面是看不到这个通证门票的。
2.3 转让门票 - [辉哥-欧阳哥哥]
通过报名渠道,辉哥知道欧阳哥哥已报名参加HiBlock黑客马拉松,所以把区块链门票转给他。
1) 辉哥点击“转让”按钮
选择HHT后,点击右下角的“转让”按钮进行票务转让。
2)点击“转让”按钮
选择“现在直接转让门票”,
获取欧阳哥哥的钱包地址,输入:
3)确认转让
2.4 出售门票 - [欧阳哥哥-小辉]
1)导入通证
欧阳哥哥在AlphaWallet钱包中输入HHT的合约地址(0x07fc44d796d30b317013cb907fadb6d738f5779e)即可查看到辉哥转账过来的门票通证。
2) 出售门票
小辉同学知道了黑客马拉松的事情,也很想参加。欧阳哥哥刚好弄了2张票,就同意把一张票低价转让给小辉。双方协商好价格是0.2个ETH。
欧阳哥哥点击出售按钮,设置好价格,最后链接通过微信发给小辉。
3) 导入支付
小辉安装好APP。复制链接打开APP时,会提示导入门票。点击购买,支付了0.2个ETH后即可完成支付。
4) 导入代币地址完成呈现
小辉在钱包导入HHT智能合约的地址(0x07fc44d796d30b317013cb907fadb6d738f5779e)后,即可在APP上呈现购买的HHT门票一张。
2.5 兑现门票
欧阳哥哥和小辉到达HiBlock黑客马拉松现场,点击门票的“兑换”按钮,主办方Bob根据他们展示的二维码扫描完成。该门票的状态会变更为已兑换。
【后记】他们组队参加黑客马拉松,依靠其过硬的技术实力,获得了一个二等奖!
3,ERC875设计目标
ERC875协议是由AlphaWallet团队提出的,他们希望基于ERC875协议族,能够实现人、事、物、权token化。
在创始人张中南看来,人、事、物、权全部token化,即可以用token来替代物理世界里面的任何商品。在此其中,token替代的是一个权益,可以指代各种各样的权益。比如,「人」的token化,「跟吴亦凡今天晚上6点钟到8点钟一起吃饭的权益,可以做成一个token」,「事」的token化,「用信用卡在商店买了一瓶水,也可以做成一个token」,而「物」、「权」的token化,就更好理解了。
将人、事、物、权token化,可以有不同层级的愿景和意义。张中南介绍:
第一层级,简单的来说,就是把 人、事、物、权做成token,放到区块链上面流通,或者说放到钱包里,做成APP,能够使用token做流转。
再往上一个级别,是这些token和token之间的交互。比如,可能有一件事,可以同时调用7、8个token,不再是简单的转让或流通。
再往上一个级别,「我们能够看到最远的地方就是这些token用来指代人、事、物、权之后,它们本身可以变成一个集成点,可以在用户端集成各种各样的服务和应用。比如,租车服务、保险、信用卡公司等,当需要调用他们的服务时,不再通过微信来使用,而是直接在用户端就能集成。
现阶段,为了实现初级目标,AlphaWallet选择从一款可编程钱包切入。今年5月23日,该公司正式发布了这款筹备已久的钱包产品——AlphaWallet 1.0版。
公开资料显示,这是一款直接支持不可替代性token的钱包,可作为连接虚拟世界和真实世界的网关。基于该钱包之上,真实世界内的生活服务可利用区块链技术而具备强有力的基础技术平台,从而拥有无限想象的可能性。
通常来说,大量token广泛使用的是ERC20协议。遵循ERC20的token可以跟踪任何人在任何时候拥有多少token。在一些开源组织的推动下,目前第三方基于ERC20接口5分钟即能发行一个ERC20的token。不过,相对来说,ERC20还存在两个问题:
第一,ERC20无法代表现实世界中无法拆分、独一无二的资产;
第二,现有的打包、转账流程复杂,ERC20缺乏可扩展性,无法实现更复杂的功能。
基于此,AlphaWallet自主开发了ERC875协议族。该协议不仅会让数字资产变得具有收藏价值,同时也能帮助现实世界中不可拆分替代、具有物权唯一性的资产上链,这就能为线下服务的链上操作提供了可能性。
虽然另一种协议ERC721也能实现token的不可置换性,但其存在需要交易双方支付gas费用、无法简单实现原子化交易等一些不易于用户使用的问题。
张中南向雷锋网AI金融评论介绍称,ERC875内置了两个密码学协议, 一方面能够简单实现原子化交易(atomic swap)——直接搭建去中心化市场、降低普通用户使用门槛,卖家无需持有以太币,买家支付一次gas即能完成;另外一方面可以简单打包处理大量交易。
拿基于ERC721的加密猫来说,换用ERC875协议的话,能够实现。用户在商家网站法币购猫,通过MagicLink免费把猫导入用户的钱包,之后用户还可以在不需要持有以太币的情况下,通过MagicLink把猫售出或者免费转让,全部过程都是无中心的原子化交易。另外商家可以一次批发100只猫给分销商。
首个落地应用:体育票务
或许与张中南在票务业务的经历有关,AlphaWallet选择从ERC875和钱包切入的第一个use case就是俄罗斯世界杯门票。
相较人、事而言,「票务」由于具备物理和权益属性,利用区块链技术来实现不可置换的token的流转,更具操作性和可行性。
目前 AlphaWallet 已与盛开体育达成合作。今年的俄罗斯世界杯,二者联合引入区块链技术以测试新的票务解决方案,将盛开体育世界杯票库内的部分门票转化为以太坊上的ERC875的token。由于这些token具有不可置换性,用户通过AlphaWallet钱包的动态二维码,以及线下的现场扫描,即可获得世界杯门票。考虑到进一步安全的问题,AlphaWallet钱包显示的动态二维码,每隔10s就会变一次。
据张中南介绍,这次合作,「盛开那边做了10张票,AlphaWallet则拿了10张开幕式的VIP门票,所以一共只有20张门票」。经过雷锋网AI金融评论现场测试体验,通过AlphaWallet钱包流转一张世界杯门票,所花时间在4-7s以内。而买方从卖方手里通过支付以太坊的方式买入一张门票,所需时间则在10s左右。
「这应该是目前世界上首个不可替代通证与现实物权交互的落地案例。」团队向雷锋网AI金融评论表示。
除票务外,AlphaWallet近期还会继续考虑在「物」上面开发use case,主要专注在物理商品这一块,如 奢侈手表和限量球鞋等等。
不过,也有业内人士指出,通过不可置换协议,从token到实物的映射,可能还是难以避免实物造假的情况,这点又该如何防范?在张中南看来,给物理商品配备数字身份证,是通过经济学原理来实现防伪的。这点与溯源、防伪等又不一样。
4,ERC875标准
function name() constant returns (string name)
返回智能合约的名字,例如CarLotContract。
function symbol() constant returns (string symbol)
返回智能合约通证的标识符。
function balanceOf(address _owner) public view returns (uint256[] balance)
返回一组账户余额的数组。
function transfer(address _to, uint256[] _tokens) public;
通过包含通证索引的数组参数,把一组独一无二的通证转移给一个账户地址。相比ERC721一次只能转账一个通证,ERC875更显友好,它可以一次批量转账一组通证。这样既便利又能节约大量的GAS消耗。
function transferFrom(address _from, address _to, uint256[] _tokens) public;
从一个账户给另一个账户转账批量通证。这个可由一个获得特定KEY例如合同创建者的授权的账号来完成。
【以下为可选函数】
function totalSupply() constant returns (uint256 totalSupply);
返回给定合同的通证总数。这个通证总数可能是可变的。
function ownerOf(uint256 _tokenId) public view returns (address _owner);
返回特定通证的拥有者。这个函数是可选的,因为并不是每一个通证合约都需要跟踪每一个独一无二通知的拥有者,并且每次查询需要消耗GAS用于遍历和匹配token id于拥有者的关系。
function trade(uint256 expiryTimeStamp, uint256[] tokenIndices, uint8 v, bytes32 r, bytes32 s) public payable
该函数允许用户出售一组非同质通证而不需要支付GAS费,只需要购买者支付。这是通过签署包含要销售的代币数量,合同地址,到期时间戳,价格和包含ERC规范名称和链ID的前缀的证明来实现的。然后,买方可以通过附加适当的以太币(ether)来满足交易,从而在一次交易中支付交易。
这种设计也更有效,因为它允许订单在离线前完成,而不是在智能合约中创建订单并更新订单。到期时间戳保护卖方免受使用旧订单的人的影响。
这为点对点(p2p)原子交换(atomic swap)打开了大门,但对于这个标准应该是可选的,因为有些可能没有用它。
需要在消息中添加一些保护,例如编码链ID,合同地址和ERC规范名称,以防止重放和欺骗人们签署允许交易的消息。
5,ERC875样例代码
官方给出的ERC875代码样例如下,函数含义参考第4章。
contract ERC
{
event Transfer(address indexed _from, address indexed _to, uint256[] tokenIndices);
function name() constant public returns (string name);
function symbol() constant public returns (string symbol);
function balanceOf(address _owner) public view returns (uint256[] _balances);
//function ownerOf(uint256 _tokenId) public view returns (address _owner);
function transfer(address _to, uint256[] _tokens) public;
function transferFrom(address _from, address _to, uint256[] _tokens) public;
//optional
//function totalSupply() public constant returns (uint256 totalSupply);
function trade(uint256 expiryTimeStamp, uint256[] tokenIndices, uint8 v, bytes32 r, bytes32 s) public payable;
}
pragma solidity ^0.4.17;
contract Token is ERC
{
uint totalTickets;
mapping(address => uint256[]) inventory;
uint16 ticketIndex = 0; //to track mapping in tickets
uint expiryTimeStamp;
address owner; // the address that calls selfdestruct() and takes fees
address admin;
uint transferFee;
uint numOfTransfers = 0;
string public name;
string public symbol;
uint8 public constant decimals = 0; //no decimals as tickets cannot be split
event Transfer(address indexed _from, address indexed _to, uint256[] tokenIndices);
event TransferFrom(address indexed _from, address indexed _to, uint _value);
modifier adminOnly()
{
if(msg.sender != admin) revert();
else _;
}
function() public { revert(); } //should not send any ether directly
// example: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], "MJ comeback", 1603152000, "MJC", "0x007bEe82BDd9e866b2bd114780a47f2261C684E3"
function Token(
uint256[] numberOfTokens,
string evName,
uint expiry,
string eventSymbol,
address adminAddr) public
{
totalTickets = numberOfTokens.length;
//assign some tickets to event admin
expiryTimeStamp = expiry;
owner = msg.sender;
admin = adminAddr;
inventory[admin] = numberOfTokens;
symbol = eventSymbol;
name = evName;
}
function getDecimals() public pure returns(uint)
{
return decimals;
}
// price is 1 in the example and the contract address is 0xfFAB5Ce7C012bc942F5CA0cd42c3C2e1AE5F0005
// example: 0, [3, 4], 27, "0x2C011885E2D8FF02F813A4CB83EC51E1BFD5A7848B3B3400AE746FB08ADCFBFB", "0x21E80BAD65535DA1D692B4CEE3E740CD3282CCDC0174D4CF1E2F70483A6F4EB2"
// price is encoded in the server and the msg.value is added to the message digest,
// if the message digest is thus invalid then either the price or something else in the message is invalid
function trade(uint256 expiry,
uint256[] tokenIndices,
uint8 v,
bytes32 r,
bytes32 s) public payable
{
//checks expiry timestamp,
//if fake timestamp is added then message verification will fail
require(expiry > block.timestamp || expiry == 0);
//id 1 for mainnet
bytes12 prefix = "ERC800-CNID1";
bytes32 message = encodeMessage(prefix, msg.value, expiry, tokenIndices);
address seller = ecrecover(message, v, r, s);
for(uint i = 0; i < tokenIndices.length; i++)
{ // transfer each individual tickets in the ask order
uint index = uint(tokenIndices[i]);
require((inventory[seller][index] > 0)); // 0 means ticket sold.
inventory[msg.sender].push(inventory[seller][index]);
inventory[seller][index] = 0; // 0 means ticket sold.
}
seller.transfer(msg.value);
}
//must also sign in the contractAddress
//prefix must contain ERC and chain id
function encodeMessage(bytes12 prefix, uint value,
uint expiry, uint256[] tokenIndices)
internal view returns (bytes32)
{
bytes memory message = new bytes(96 + tokenIndices.length * 2);
address contractAddress = getContractAddress();
for (uint i = 0; i < 32; i++)
{ // convert bytes32 to bytes[32]
// this adds the price to the message
message[i] = byte(bytes32(value << (8 * i)));
}
for (i = 0; i < 32; i++)
{
message[i + 32] = byte(bytes32(expiry << (8 * i)));
}
for(i = 0; i < 12; i++)
{
message[i + 64] = byte(prefix << (8 * i));
}
for(i = 0; i < 20; i++)
{
message[76 + i] = byte(bytes20(bytes20(contractAddress) << (8 * i)));
}
for (i = 0; i < tokenIndices.length; i++)
{
// convert int[] to bytes
message[96 + i * 2 ] = byte(tokenIndices[i] >> 8);
message[96 + i * 2 + 1] = byte(tokenIndices[i]);
}
return keccak256(message);
}
function name() public view returns(string)
{
return name;
}
function symbol() public view returns(string)
{
return symbol;
}
function getAmountTransferred() public view returns (uint)
{
return numOfTransfers;
}
function isContractExpired() public view returns (bool)
{
if(block.timestamp > expiryTimeStamp)
{
return true;
}
else return false;
}
function balanceOf(address _owner) public view returns (uint256[])
{
return inventory[_owner];
}
function myBalance() public view returns(uint256[])
{
return inventory[msg.sender];
}
function transfer(address _to, uint256[] tokenIndices) public
{
for(uint i = 0; i < tokenIndices.length; i++)
{
require(inventory[msg.sender][i] != 0);
//pushes each element with ordering
uint index = uint(tokenIndices[i]);
inventory[_to].push(inventory[msg.sender][index]);
inventory[msg.sender][index] = 0;
}
}
function transferFrom(address _from, address _to, uint256[] tokenIndices)
adminOnly public
{
bool isadmin = msg.sender == admin;
for(uint i = 0; i < tokenIndices.length; i++)
{
require(inventory[_from][i] != 0 || isadmin);
//pushes each element with ordering
uint index = uint(tokenIndices[i]);
inventory[_to].push(inventory[msg.sender][index]);
inventory[_from][index] = 0;
}
}
function endContract() public
{
if(msg.sender == owner)
{
selfdestruct(owner);
}
else revert();
}
function getContractAddress() public view returns(address)
{
return this;
}
}
【函数说明】
1,trade函数是发起批量转让的智能合约函数
trade(uint256 expiry,/超时时间,以s计算/
uint256[] tokenIndices, /通证索引/
uint8 v, /*v,r,s是卖家签名的3个部分,产生的方法参考文件 */
bytes32 r,
bytes32 s )
6,ERC875测试(REMIX+MetaMASK环境)
6.1 创建合约
[1] 管理员(0xca35b7d915458ef540ade6068dfe2f44e8fa733c)构建函数CREATE
[101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115], "DJ Family", 1603152000, "DJ", "0xca35b7d915458ef540ade6068dfe2f44e8fa733c"
【结果】
智能合约创建成功,得到智能合约地址:0x692a70d2e424a56d2c6c27aa97d1a86395877b3a
6.2 门票转让
管理员(0xca35b7d915458ef540ade6068dfe2f44e8fa733c)转移2张座位号为101,102的门票给李四(0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db)
transfer("0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db", [0,1])
*【结果】:门票已转让给李四,李四并没有消耗GAS,是管理员消耗了GAS。
6.3 trade门票
管理员(0xca35b7d915458ef540ade6068dfe2f44e8fa733c)把门票trade给赵六(0xdd870fa1b7c4700f2bd7f44238821c26f7392148)
当智能合约地址为0xfFAB5Ce7C012bc942F5CA0cd42c3C2e1AE5F0005,price is 1时,
trade(0, [3, 4], 27, "0x2C011885E2D8FF02F813A4CB83EC51E1BFD5A7848B3B3400AE746FB08ADCFBFB", "0x21E80BAD65535DA1D692B4CEE3E740CD3282CCDC0174D4CF1E2F70483A6F4EB2")
【结果】操作失败了,也无法触发购买。
【官方答复】
那个Trade function的功能是,在卖家发了签名信息给买家,然后买家联合卖家的签名信息和自己的签名信息一起call trade fundction来完成交易。你在现在的模式,是创建不出来卖家签名信息的, 你需要参考AlphaWallet的代码。
源码参考地址:
https://github.com/alpha-wallet/AlphaWallet-Mobile-Apps
【详细说明】
(1) START TO TRANSFER:
transferTicketDetailVeiwModel.java - CreateTicketTransfer
(2) HOW TO BUY A TICKET
ImportTokenViewModel.java - PerformImport
7 参考
1) 2018世界杯门票的一笔交易记录
2) 深入浅出以太坊ERC875标准(不可替代性通证标准)
3) AlphaWallet野心有点大:基于ERC875协议族,实现人、事、物、权token化
4)ERC875 for non fungible tokens and simple atomic swaps
5) AlphaWallet代码
6) ERC875智能合约案例 (TradeImplementationExample.java 和ERCTokenImplementation.sol)
7) AlphaWallet钱包下载-支持测试网络代币