1. 程式人生 > >第十八課 【ERC875】Hiblock黑客馬拉松門票從定製到編碼實現

第十八課 【ERC875】Hiblock黑客馬拉松門票從定製到編碼實現

#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

1. 提交合約部署

2)“Submmit”按鈕 “Gas Price”設定為30,點選“Submmit”按鈕。該賬號要有一定的ETH測試幣,否則點選"Buy"找平臺免費買點。

2. 確認交易

3)購買成功確認 購買成功的會有彈出提示。點選“確定”按鈕後,拉到下方的按鈕可以檢視智慧合約部署連結和ABI合約資訊。

3.合約部署成功

4. 檢視ABI資訊和合約記錄

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設計目標 AlphaWallet團隊核心成員(左二:CEO張中南;右二:創始人兼CTO張韡武) 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就會變一次。 AlphaWallet錢包兌換俄羅斯世界盃門票(test)流程體驗 據張中南介紹,這次合作,「盛開那邊做了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