知乎專欄 | 多維度架構 | | | 微信號 netkiller-ebook | | | QQ群:128659835 請註明“讀者” |
首先我們使用代幣是為了取代傳統的積分機制。因為代幣的“幣”特性能夠實現流通,交易等等,實現一個閉環生態系統。而傳統的積分只能內部使用,無法流通,外接不承認加分的價值,積分無法交易,流通。
其次我們並非是為了ICO炒作,然後割韭菜,代幣取代積分是剛性需求。
這裡假設您已經看我我之前寫的文章,知道什麼是代幣,並且能用錢包部署代幣合約。例如現在合約已經部署到主網,已經可以使用錢包轉賬代幣,甚至代幣已經上了交易所,接下來我們要做什麼呢?
接下來的工作是將代幣和網站或者手機App打通,只有將代幣整合到現有業務場景中代幣才有意義。
我認為我們需要下面幾種角色的賬號
代幣發行賬號,負責發行代幣,管理代幣
收款賬號,用戶代幣流通中的收款,這個賬號應該由財務人員負責,相當於企業對公賬號。
交易所賬號,用來對接交易所
用戶賬號,普用戶的賬號,依賴接受代幣,消費代幣,交易代幣。
如果着手一個遊戲項目上鏈,我們需要怎麼做呢?
上鏈步驟
收集需求,收集公司的內部上鏈需求,聽取所有部門的建議和訴求。
收集內容例如,代幣發行量多少?代幣小數位數,代幣名稱,是否會增發,是否需要凍結,代幣怎樣流通,怎樣回收
Dapp 的 UI 設計,各種功能流程
分析需求,因為需求來自各種部門,各種崗位等等,他們不一定從事需求分析工作,所以我們需求對他們的需求過濾,分析,然後給出初步的PRD文檔(產品需求文檔)
根據收集的需求設計合約和Dapp
根據需求設計Dapp
系統架構設計,軟件架構設計,技術選型;需要考慮擴展性,靈活性,並發設計,數據安全,部署,後期監控,各種埋點(統計、監控)
準備環境,我們需要三個環境,開發,測試,生產(運營)。
項目啟動
運維部門準備環境,開始建設監控系統
開發部門開發合約和Dapp
測試部門準備測試用例,測試環境
測試
Alpha 階段,將合約部署到測試環境,測試合約的每個函數的工作邏輯,確保無誤。因為合約一旦部署將不能更改,只能廢棄使用新的合約,所以這個測試步驟必須重視。
Beta 階段,將測試合約部署到測試網,例如 Ropsten ,可以邀請公司內部測試
部署生產環境
部署合約,將合約部署到主網
Dapp 部署到生產環境。
驗收測試,在生產環境做最後驗收測試
代幣上交易所
合約部署這里加就不介紹了,可以參考《Netkiller Blockchain 手札》
合約地址:https://raw.githubusercontent.com/ibook/TokenERC20/master/contracts/TokenERC20.sol
這個合約提供增發,凍結,所有權轉移等等功能。
pragma solidity ^0.4.21; interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; } contract TokenERC20 { address public owner; // Public variables of the token string public name; string public symbol; uint8 public decimals = 18; // 18 decimals is the strongly suggested default, avoid changing it uint256 public totalSupply; // This creates an array with all balances mapping (address => uint256) public balanceOf; mapping (address => mapping (address => uint256)) public allowance; // This generates a public event on the blockchain that will notify clients event Transfer(address indexed from, address indexed to, uint256 value); // This notifies clients about the amount burnt event Burn(address indexed from, uint256 value); mapping (address => bool) public frozenAccount; event FrozenFunds(address target, bool frozen); /** * Constrctor function * * Initializes contract with initial supply tokens to the creator of the contract */ function TokenERC20( uint256 initialSupply, string tokenName, string tokenSymbol ) public { owner = msg.sender; totalSupply = initialSupply * 10 ** uint256(decimals); // Update total supply with the decimal amount balanceOf[msg.sender] = totalSupply; // Give the creator all initial tokens name = tokenName; // Set the name for display purposes symbol = tokenSymbol; // Set the symbol for display purposes } modifier onlyOwner { require(msg.sender == owner); _; } /** * Internal transfer, only can be called by this contract */ function _transfer(address _from, address _to, uint _value) internal { // Prevent transfer to 0x0 address. Use burn() instead require(_to != 0x0); // Check if the sender has enough require(balanceOf[_from] >= _value); // Check for overflows require(balanceOf[_to] + _value > balanceOf[_to]); // Save this for an assertion in the future uint previousBalances = balanceOf[_from] + balanceOf[_to]; // Subtract from the sender balanceOf[_from] -= _value; // Add the same to the recipient balanceOf[_to] += _value; Transfer(_from, _to, _value); // Asserts are used to use static analysis to find bugs in your code. They should never fail assert(balanceOf[_from] + balanceOf[_to] == previousBalances); } function transfer(address _to, uint256 _value) public { require(!frozenAccount[msg.sender]); _transfer(msg.sender, _to, _value); } function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) { require(!frozenAccount[msg.sender]); require(_value <= allowance[_from][msg.sender]); // Check allowance allowance[_from][msg.sender] -= _value; _transfer(_from, _to, _value); return true; } function approve(address _spender, uint256 _value) public returns (bool success) { allowance[msg.sender][_spender] = _value; return true; } function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) { tokenRecipient spender = tokenRecipient(_spender); if (approve(_spender, _value)) { spender.receiveApproval(msg.sender, _value, this, _extraData); return true; } } function burn(uint256 _value) onlyOwner public returns (bool success) { require(balanceOf[msg.sender] >= _value); // Check if the sender has enough balanceOf[msg.sender] -= _value; // Subtract from the sender totalSupply -= _value; // Updates totalSupply Burn(msg.sender, _value); return true; } function burnFrom(address _from, uint256 _value) onlyOwner public returns (bool success) { require(balanceOf[_from] >= _value); // Check if the targeted balance is enough require(_value <= allowance[_from][msg.sender]); // Check allowance balanceOf[_from] -= _value; // Subtract from the targeted balance allowance[_from][msg.sender] -= _value; // Subtract from the sender's allowance totalSupply -= _value; // Update totalSupply Burn(_from, _value); return true; } function transfer(address _to, uint256 _value, bytes _data) public returns (bool) { require(_to != address(this)); transfer(_to, _value); require(_to.call(_data)); return true; } function transferFrom(address _from, address _to, uint256 _value, bytes _data) public returns (bool) { require(_to != address(this)); transferFrom(_from, _to, _value); require(_to.call(_data)); return true; } function approve(address _spender, uint256 _value, bytes _data) public returns (bool) { require(_spender != address(this)); approve(_spender, _value); require(_spender.call(_data)); return true; } function transferOwnership(address _owner) onlyOwner public { owner = _owner; } function mintToken(address target, uint256 mintedAmount) public onlyOwner { balanceOf[target] += mintedAmount; totalSupply += mintedAmount; Transfer(0, owner, mintedAmount); Transfer(owner, target, mintedAmount); } function freezeAccount(address target, bool freeze) public onlyOwner { frozenAccount[target] = freeze; FrozenFunds(target, freeze); } }
這裡我們要實現新用戶在網站上開戶為其創建一個錢包賬號。
對於現有的註冊流程界面部分無需修改,只需在創建賬號邏輯的環節增加一段代碼,去以太坊創建賬號。
web3.eth.personal.newAccount('!@superpassword').then(console.log);
創建賬號後
註冊成功 ------------------------- 用戶名:netkiller 性別:男 ... ... ... 錢包賬號:0x627306090abab3a6e1400e9345bc60c78a8bef57 [ 下載備份 ] * (提醒)請下載備份您的錢包檔案,並請牢記你的密碼,一旦丟失無法找回。
點擊下載按鈕後將對應賬號檔案提供給用戶,文章通常在 keystore 類似這種格式 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7
有了這個賬號檔案,用戶可以導入到Ethereum Wallet 或者 MetaMask 中。
新用戶我們可以在用戶註冊的時候為其創建一個錢包賬號。那麼老用戶呢?
老用戶我們提供“綁定”功能,如果用戶已有以太坊賬號,就可以將以太坊賬號與網站賬號做綁定。
通常我們需要一個頁面
當前用戶名:netkiller
以太坊錢包:___________________________
手機驗證碼:______ [ 獲取驗證碼]
[ 綁定 ] [取消]
填寫錢包賬號,然後點擊獲取驗證碼,然後輸入驗證碼,點擊 “提交” 完成賬號的綁定。
如果你想在平台上提供轉賬等高級操作,你還需要讓用戶上傳 UTC--2018-02-10T09-37-49.558088000Z--5c18a33df2cc41a1beddc91133b8422e89f041b7 檔案,如果採用上傳方案,就不需要綁定了,因為在檔案中(json格式)address 就是賬號。
當前用戶名:netkiller 以太坊錢包:___________________________ [瀏覽] 手機驗證碼:______ [ 獲取驗證碼] [ 上傳 ] [取消]
對於新開戶,或者老用戶綁定了錢包。我們通常要意思一下,就是送點幣。
送幣有兩種方式,第一種是轉賬給用戶,缺點是需要花店gas(氣),第二種是採用空投方式,就是在用戶查詢餘額的時候送幣。
先來說說第一種,轉賬方式。
fs = require('fs'); const Web3 = require('web3'); const web3 = new Web3('http://localhost:8545'); web3.version const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8'); const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4"; const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C"; var coinbase; web3.eth.getCoinbase().then(function (address){ coinbase = address; console.log(address); }); const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: coinbase , gas: 100000}); contract.methods.balanceOf('0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7').call().then(console.log).catch(console.error); contract.methods.balanceOf('0x2C687bfF93677D69bd20808a36E4BC2999B4767C').call().then(console.log).catch(console.error); web3.eth.personal.unlockAccount(coinbase, "netkiller").then(console.log); contract.methods.transfer('0x2C687bfF93677D69bd20808a36E4BC2999B4767C', 100).send().then(console.log).catch(console.error); contract.methods.balanceOf('0x2C687bfF93677D69bd20808a36E4BC2999B4767C').call().then(console.log).catch(console.error);
第二種是空投幣
uint totalSupply = 100000000 ether; // 總發行量 uint currentTotalAirdrop = 0; // 已經空投數量 uint airdrop = 1 ether; // 單個賬戶空投數量 // 存儲是否空投過 mapping(address => bool) touched; // 修改後的balanceOf方法 function balanceOf(address _owner) public view returns (uint256 balance) { if (!touched[_owner] && currentTotalAirdrop < totalSupply) { touched[_owner] = true; currentTotalAirdrop += airdrop; balances[_owner] += airdrop; } return balances[_owner]; }
空投代幣省了 Gas,但是帶來一個問題,就是實際代幣發行量成了 totalSupply * 2 ,因為創建合約的時候代幣全部發給了 msg.sender ,空投只能使用增發幣,無法去 msg.sender 扣除的空投數量。
空投幣不好管理髮行量。有一種做法,就是發行的時候分為2分,一份是 coinbase(msg.sender) 的 另一份是空投的。
這裡我們舉例幾個場景
發放代幣的方法
電商平台可以通過活動,訂單量等等條件將代幣發放給用戶
智能穿戴,例如鞋子,手環,可以根據用戶的運動值這算成代幣,發放給用戶
遊戲平台,用戶在綫時間,電子競技贏得分數都可以這算成代幣,發放給用戶
第一個界面一定是,請輸入用戶名和密碼,然後提交登錄。
登錄後進入用戶信息頁面
用戶登錄成功 ------------------------------ 當前用戶名:netkiller ... ... 錢包賬號:0x627306090abab3a6e1400e9345bc60c78a8bef57 ------------------------------ 當前餘額: 1000 NEO
NEO是代幣符號,假設叫NEO,這是我的英文名。實際上NEO已經被其他代幣使用:(
獲取賬號餘額代碼
fs = require('fs'); const Web3 = require('web3'); const web3 = new Web3('http://localhost:8545'); const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8'); const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4"; const fromAddress = "0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7"; //用戶賬號 const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C"; //收款賬號 const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: fromAddress , gas: 100000}); contract.methods.balanceOf(toAddress).call().then(console.log).catch(console.error);
這裡是消費代幣的地方,可以使用代幣對兌換禮品,購買物品等等。
用戶花出去代幣去嚮應該是,用戶收款的財務賬號。
fs = require('fs'); const Web3 = require('web3'); const web3 = new Web3('http://localhost:8545'); const abi = fs.readFileSync('output/TokenERC20.abi', 'utf-8'); const contractAddress = "0x05A97632C197a0496bc939C4e666c2E03Cb95DD4"; const fromAddress = "0x5c18a33DF2cc41a1bedDC91133b8422e89f041B7"; //用戶賬號 const toAddress = "0x2C687bfF93677D69bd20808a36E4BC2999B4767C"; //收款賬號 const contract = new web3.eth.Contract(JSON.parse(abi), contractAddress, { from: fromAddress , gas: 100000}); web3.eth.personal.unlockAccount(fromAddress, "netkiller").then(console.log); contract.methods.transfer(toAddress, 10).send().then(console.log).catch(console.error); //花費代幣 10 contract.methods.balanceOf(toAddress).call().then(console.log).catch(console.error);
報表是用來展示網站數據變化的表徵圖,這裡只列出與代幣有關的報表。