以太坊Dapp項目-網頁錢包開發手冊
修訂日期 | 姓名 | 郵箱 |
---|---|---|
2018-10-10 | brucefeng | [email protected] |
前言
在之前的一篇文章以太坊智能合約項目-Token合約開發與部署中,我們提到了錢包了錢包的概念以及主流的幾種錢包,如Mist,MyEtherWallet,MetaMask等,之前我們主要將錢包作爲一個開發工具使用,用於智能合約的開發與調試工作,使用較多的是瀏覽器插件錢包MetaMask。
在本文中,我們主要介紹MyEtherWallet以及如何實現一個簡易版的MyEtherWallet網頁版應用,在本文中,我們能夠學習到如下內容
- node.js開發基礎
- web3.js API使用
- 以太幣轉賬實現
- Token轉賬實現
目前主流的錢包平臺已經有太多了,而且有很多已經做得比較完善了,所以我們本文的開發工作只是爲了學習以太坊開發技術,並非去設計一個新的錢包軟件,重複造輪子幾乎沒有任何價值。
一. MyEtherWallet介紹
MyEtherWallet 是一個輕錢包,無需下載,所有操作在直接在網頁上就可以完成
主要功能如下
- Net Wallet : 新建錢包
- Send Ether && Tokens :以太幣或者Token轉賬
- Contract: 部署智能合約
- ENS:以太坊域名平臺
- Check TX Status: 查看交易狀態
- View Wallet Info: 查看錢包信息
由於操作比較簡單,這裏不做詳細講解,在下文中我們對其主要功能,如新建錢包,以太幣或者Token轉賬,查看交易狀態進行參照開發。
二.node.js與web.js
1.Node.js
Node.js是一個JS運行時環境,可以解析,執行JavaScript代碼,在這個執行環境中,爲JS提供了一些服務器級別的操作API,如文件讀寫,網絡通信,Http服務器等,其使用事件驅動,非阻塞IO模型(異步),輕量高效,大多數與JS相關的包都放在npm上,通過命令就可以下載不同的庫跟框架,無需在去各個模塊的官網上面單獨下載,如安裝koa直接通過npm install koa
即可完成。
2. Web3.js
web3.js是一個庫集合,允許使用HTTP或者RPC連接與本地或者遠程以太坊節點進行交互,包含以太坊生態系統的特定功能,開發者利用web3模塊主要連接以太坊的RPC層,從而與區塊鏈進行交互
- web3-eth : 與以太坊區塊鏈和智能合約之間的交互
- web3-ssh: 用於進行通信的p2p和廣播
- web-bzz: 用於羣協議,分散的文件存儲
- web3-utils: 主要用於Dapp開發的輔助函數
- 創建錢包賬戶
web3.eth.accounts.create();
- 生成錢包配置
web3.eth.accounts.encrypt(privateKey, password);
- 通過私鑰生成賬戶對象
web3.eth.accounts.privateKeyToAccount(privateKey);
- 查詢餘額
web3.eth.getBalance(address [, defaultBlock] [, callback])
- 通過私鑰跟密碼生成配置文件
web3.eth.accounts.encrypt(privateKey, password);
- 通過配置文件和密碼解鎖賬戶
web3.eth.accounts.decrypt(keystoreJsonV3, password);
- 發送簽名交易
web3.eth.sendSignedTransaction(signedTransactionData [, callback])
Example
var Tx = require('ethereumjs-tx');
var privateKey = new Buffer('e331b6d69882b4cb4ea581d88e0b604039a3de5967688d3dcffdd2270c0fd109', 'hex')
var rawTx = {
nonce: '0x00',
gasPrice: '0x09184e72a000',
gasLimit: '0x2710',
to: '0x0000000000000000000000000000000000000000',
value: '0x00',
data: '0x7f7465737432000000000000000000000000000000000000000000000000000000600057'
}
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
// console.log(serializedTx.toString('hex'));
// 0xf889808609184e72a00082271094000000000000000000000000000000000000000080a47f74657374320000000000000000000000000000000000000000000000000000006000571ca08a8bbf888cfa37bbf0bb965423625641fc956967b81d12e23709cead01446075a01ce999b56a8a88504be365442ea61239198e23d1fce7d00fcfc5cd3b44b7215f
web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'))
.on('receipt', console.log);
> // see eth.getTransactionReceipt() for details
(1) nonce
web3.eth.getTransactionCount(address [, defaultBlock] [, callback])
(2) gasPrice
web3.eth.getGasPrice([callback])
(3) gasLimit
- 預估gas值
3. Koa中間件
Koa號稱是基於Node.js平臺的下一代web開發框架,Koa 是一個新的 web 框架,由 Express 幕後的原班人馬打造, 致力於成爲 web 應用和 API 開發領域中的一個更小、更富有表現力、更健壯的基石。 通過利用 async 函數,Koa 幫你丟棄回調函數,並有力地增強錯誤處理。 Koa 並沒有捆綁任何中間件, 而是提供了一套優雅的方法,幫助您快速而愉快地編寫服務端應用程序。
其使用方法可以參考https://koa.bootcss.com/
Koa的最大特色就是中間件,Koa中間件是簡單的函數,調用app.user()傳入,MiddlewareFunction函數帶有兩個參數(ctx,next),中間件作爲HTTP Request和HTTP Reponse的橋樑,用來實現連接功能,如
app.use(async (ctx, next) =>{
console.log(`Process ${ctx.request.method} ${ctx.request.url} ...`);
await next();
});
ctx是一個請求的上下文,封裝了傳入的http消息,並對該消息進行響應,koa提供了一個Request/Response對象提供了Context的request/response屬性與用於處理Http請求的方法。
next是被調用來執行上下游中間件的函數,必須手動調用next()運行下游中間件,否則下游中間件不會被正常執行。可以採用兩種不同的方法來實現中間件
- async function
- common function
(1) 安裝啓動Koa服務
- 安裝koa
$ mkdir koaTest ; cd koaTest
$ npm init -y
$ npm install koa
- 創建啓動文件
$ vim index.js
var Koa = require("koa") //導入Koa庫
var app = new Koa(); //創建Koa應用對象,帶有node http服務的koa接口
app.listen(3003); //啓動服務的快捷方法
- 啓動服務
$ node index.js
此時會監聽3003端口,通過瀏覽器訪問
(2) Context
context是一個請求的上下文,該對象封裝了一個傳入的http消息,context有request和response屬性,我們可以通過涉案值兩個屬性來處理和相應不同的請求。
$ vim index.js
var Koa = require("koa");
var app = new Koa();
//
app.use(function(ctx,next){
console.log(ctx.request.path);
//通過設置ctx的response的body值可以返回數據到客戶端
ctx.response.body = "my koa app";
console.log(ctx.response.type);
});
app.listen("3003");
console.log("koa server is listening on port 3003");
- app.use(function):將給定的function作爲中間件加載到應用中。
- ctx: 每一個請求都會創建一段上下文,在控制業務邏輯的中間件中,上下文被寄存在ctx對象中,許多上線文屬性和方法都被存在ctx.request和ctx.response中,比如訪問ctx.type和ctx.length都被代理到reponse對象,ctx.path和ctx.method都被代理到request對象中。
(3) 網頁模板
在實際開發中,返回給用戶的網頁都是通過模板文件進行返回的,可以讓Koa先讀取模板文件,然後將這個模板返回給用戶,需要指定response的type爲text/html
類型
var Koa = require("koa");
var app = new Koa();
var fs = require("fs");
app.use(ctx=>{
console.log(ctx.path);
//必須指定type
ctx.type = "text/html";
ctx.body = fs.createReadStream("./views/teest.html");
console.log(ctx.type);
});
app.listen(3003);
console.log("koa server is listening on port 3003");
(4) 中間件
Koa所有的功能都是通過中間件實現的,中間件處於HTTP Request和HTTP Response之間。
Koa的中間件之間按照編碼書序在棧內以此執行,允許執行操作並向下傳遞請求,之後過濾並必須返回響應,響應的測試代碼與步驟如下
var Koa = require("koa");
var app = new Koa();
//es6新語法:
//函數名 =>(參數) =>{}
var one = (ctx,next) =>{
console.log("1.1");
next();
console.log("1.2");
};
var two = (ctx,next) =>{
console.log("2.1");
next();
console.log("2.2");
};
var three = (ctx, next) =>{
console.log("3.1");
next();
console.log("3.2");
};
app.use(one);
app.use(two);
app.use(three);
app.listen(3003);
console.log("服務啓動完畢");
返回結果
2.1
3.1
3.2
2.2
1.2
(5) 異步中間件
由async標記的函數被稱爲異步函數,在異步函數中,可以通過await調用另外一個異步函數,使用await時,其所在的方法必須使用關鍵字async
var Koa = require("koa");
var app = new Koa();
app.use(async(ctx,next) =>{
var start = Date.now();
await next();
console.log(`${ctx.url} ${Date.now - start}`);
});
app.use(async (ctx,next)=>{
ctx.response.body = "async test"
});
app.listen(3003);
console.log("服務啓動完畢");
(6) 原生路由
var Koa = require("koa");
var app = new Koa();
//es6新語法:
//函數名 =>(參數) =>{}
app.use((ctx,next)=> {
console.log("%s %s", ctx.method, ctx.url);
next();
});
app.use((ctx,next) =>{
if (ctx.request.path == '/'){
ctx.response.body = 'index page';
}else {
next();
}
});
app.use((ctx,next) =>{
if (ctx.request.path == '/error'){
ctx.response.body = 'error page';
}
});
app.listen(3003);
console.log("服務啓動完畢");
(7) koa-router路由
由於原生路由使用比較繁瑣,所以可以通過封裝好的koa-router模塊,使用router.routers()綁定到中間件
安裝koa-router
$ npm install koa-router
var Koa = require("koa");
var app = new Koa();
//導入koa-router,注意要加上()才能生效
var router = require("koa-router")()
router.get("/hello",function(ctx,next){
ctx.response.body = "hello,brucefeng";
});
router.get("/bye",function (ctx,next){
ctx.response.body = "good bye brucefeng";
});
//將router路由註冊到中間件
app.use(router.routes());
app.listen(3003);
console.log("服務啓動完畢");
(8) 請求重定向
一般在如下情況下需要使用到重定向
- 後臺系統升級,對之前的頁面不在支持,此時需要使用重定向到新的API上滿足用戶的訪問準確性
- 完成某個操作後自動跳轉至其他頁面,如註冊成功,登錄成功等等
var Koa = require("koa");
var app = new Koa();
//導入koa-router,注意要加上()才能生效
var router = require("koa-router")()
router.get("/hello",function(ctx,next){
ctx.response.body = "hello,brucefeng";
});
router.get("/hi",function (ctx,next){
ctx.response.redirect("/hello")
});
//將router路由註冊到中間件
app.use(router.routes());
app.listen(3003);
console.log("服務啓動完畢");
通過 ctx.response.redirect("/hello")將"/hi"請求重定向到/hello對應的頁面
在node.js中訪問的url中有中文時,需要通過全局encodeURIComponent(string)進行編碼
(9) 獲取get請求參數
客戶端在請求獲取服務的數據時,獲取的URL中通常會攜帶各種參數,服務端如何獲取到get請求的參數呢?
- 格式1:
http://127.0.0.1:3003/hello/brucefeng
獲取方式: ctx.params.name
- 格式2:
http://127.0.0.1:3003/bye?name=brucefeng
獲取方式: ctx.query.name
調用params獲取參數的時候,params不是request的屬性,需要通過ctx直接調用獲取。
(10) 獲取post請求參數
Get請求的參數附帶在了url上,Post請求的參數在請求體body裏面,所以要獲取body的數據,需要使用到插件koa-body
,通過ctx.request.body.name
獲取參數.
$ npm install koa-router
var Koa = require("koa");
var app = new Koa();
//導入koa-router,注意要加上()才能生效
var router = require("koa-router")()
//引入koa-body
var koaBody = require("koa-body")
router.post("/hello",function(ctx,next){
var body = ctx.request.body;
ctx.response.body = "hello,bruce";
console.log(body);
console.log(body.username);
});
//設置multipart : true,支持多個參數
app.use(koaBody({
multipart:true
}))
//將router路由註冊到中間件
app.use(router.routes());
app.listen(3003);
console.log("服務啓動完畢");
//通過命令使用curl插件模擬調用一個Post請求
//curl -H "Content-Type:application/json" -X POST --data '{"username":"brucefeng"}' http://localhost:3003/hello
brucefengdeMBP:ETHWalletDemo brucefeng$ node index.js
服務啓動完畢
{ username: 'brucefeng' }
brucefeng
(11) 加載靜態資源
加載靜態資源,如圖片,字體,樣式表,腳本等,編碼指定靜態資源的路徑是相對於./static的路徑。
$ npm install koa-static
var Koa = require("koa");
var app = new Koa();
//導入koa-router,注意要加上()才能生效
var router = require("koa-router")();
var static = require("koa-static");
var path = require("path")
router.get("/hello",function (ctx,next){
ctx.response.body = "<html> <a href='/0.png'>看我</html>"
})
//靜態資源的路徑是相對於./static的路徑
app.use(static(path.join(__dirname,"./static")))
//將router路由註冊到中間件
app.use(router.routes());
app.listen(3003);
console.log("服務啓動完畢");
啓動服務,通過瀏覽器訪問測試
(12) 模板引擎
模板引擎ejs需要配上模板渲染中間件koa-views使用,如果需要支持其他後綴的文件,需要將文件擴展名映射到引擎中。
$ npm install ejs koa-views
index.js
var Koa = require("koa");
var app = new Koa();
//導入koa-router,注意要加上()才能生效
var router = require("koa-router")();
var static = require("koa-static");
var path = require("path")
var views = require("koa-views")
router.get("/hello",async (ctx,next) =>{
//將json裏面的值替換爲文件裏面的變量
var name = "brucefeng";
await ctx.render("test.ejs",{
name,
"sex":"帥哥"
})
})
router.get("/bye",async (ctx,next)=>{
await ctx.render("home.html",{
"name": "fengyingcong"
})
})
app.use(views(
//默認是views下面獲取ejs後綴的文件,如果是其他類型的文件需要指定文件類型
path.join(__dirname,"./static/views"),
{extension:"ejs", map:{html: "ejs"}}
))
//靜態資源的路徑是相對於./static的路徑
app.use(static(path.join(__dirname,"./static")))
//將router路由註冊到中間件
app.use(router.routes());
app.listen(3003);
console.log("服務啓動完畢");
static/views/test.ejs
<!DOCTYPE <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="main.js"></script>
</head>
<body>
<div>姓名: <%= name %> 性別: <%= sex %></div>
</body>
</html>
static/views/home.html
<!DOCTYPE <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Page Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" media="screen" href="main.css" />
<script src="main.js"></script>
</head>
<body>
<div>姓名: <%= name %> </div>
</body>
</html>
三.初始化項目環境
1.導入項目依賴
$ mkdir ETHWalletDemo ;cd ETHWalletDemo
$ npm init -y
$ npm i web3 koa koa-body koa-static koa-views koa-router ejs
2.創建路由文件
$ mkdir router ; cd router
$ vim router.js
var router = require("koa-router")()
//定義路由newaccount
router.get("/newaccount",(ctx,next)=>{
ctx.response.body = "創建錢包"
})
module.exports = router
3.創建入口文件
ETHWalletDemo/index.js
//導入./router/router包
var router = require("./router/router.js")
//引入Koa庫
var Koa = require("koa")
//通過koa創建一個應用程序
var app = new Koa()
app.use(router.routes())
app.listen(3003)
console.log("錢包啓動成功,請訪問http://127.0.0.1:3003/newaccount進行測試")
4.創建MVC框架
ETHWalletDemo
下創建
$ mkdir models views controllers #創建MVC框架目錄
$ mkdir utils #創建幫助文件目錄
$ mkdir -p static/{css,js,html} #創建靜態文件目錄
5.完善項目框架
ETHWalletDemo/index.js
//引入Koa庫
var Koa = require("koa")
//通過koa創建一個應用程序
var app = new Koa()
//導入./router/router包
var router = require("./router/router.js")
var static = require("koa-static")
var path = require("path")
var views = require("koa-views")
var koaBody = require("koa-body")
//攔截獲取網絡請求,自定義的function需要使用next()
app.use(async (ctx,next)=>{
console.log(`${ctx.url} ${ctx.method}...`)
await next();
})
//註冊中間件
//註冊靜態文件的庫到中間件
app.use(static(path.join(__dirname,"static")))
//註冊模板引擎的庫到中間件
app.use(views(path.join(__dirname,"views"),{extension:"ejs", map:{html: "ejs"}}))
//針對於文件上傳時,可以解析多個字段
app.use(koaBody({multipart:true}))
app.use(router.routes())
app.listen(3003)
console.log("錢包啓動成功,請訪問http://127.0.0.1:3003/...進行測試")
四.創建錢包賬戶
1.創建錢包賬戶
(1) 封裝web3庫調用
utils/myUtils.js
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
getWeb3: ()=>{
var Web3 = require("web3");
var web3 = new Web3(Web3.givenProvider || 'http://127.0.0.1:8545');
return web3;
}
}
(2) 創建控制器
controllers/newAccount.js
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
//獲取創建賬號的頁面
newAccountHtml: async (ctx) =>{
await ctx.render("newaccount.html")
},
//表單提交被觸發的方法
newAccount: (ctx) =>{
console.log("newAccount");
var password = ctx.request.body.password;
//通過密碼創建錢包賬戶
var account = web3.eth.accounts.create(password);
console.log(account.address);
ctx.response.body = "錢包賬戶: "+account.address +" 創建成功";
}
}
(3) 創建前端頁面
views/newaccount.html
<html>
<head>
<title>創建錢包</title>
</head>
<body>
<div id="main">
<h1>創建一個新的錢包</h1>
<form method="POST" action="/newaccount">
<input type="text" placeholder="請輸入密碼" name="password">
<button type="submit">創建錢包</button>
</form>
</div>
</body>
</html>
(4) 配置路由
router/router.js
var router = require("koa-router")()
var newAccount = require("../controllers/newAccount")
//創建賬號的頁面
router.get("/newaccount",newAccount.newAccountHtml)
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount)
module.exports = router
2.下載配置文件
(1) 創建目錄保存配置文件
$ mkdir static/keystore
(2) 實現配置文件的保存
controllers/newAccount.js
var web3 = require("../utils/myUtils").getWeb3()
var fs = require("fs")
var path = require("path")
module.exports = {
//獲取創建賬號的頁面
newAccountHtml: async (ctx) =>{
await ctx.render("newaccount.html")
},
//表單提交被觸發的方法
newAccount: async (ctx) =>{
console.log("newAccount");
var password = ctx.request.body.password;
//通過密碼創建錢包賬戶
var account = web3.eth.accounts.create(password);
console.log(account.address);
//根據賬號私鑰跟密碼生成keystore文件
var keystore = web3.eth.accounts.encrypt(account.privateKey, password);
//keystore文件保存到文件中,
var keystoreString = JSON.stringify(keystore);
//格式如下:UTC--Date--Adress
//UTC--2018-09-26T05-07-57.260Z--937d091780693ab7f51331bb52797a9267bb9ed2
var fileTime = new Date().toDateString()
var fileName = 'UTC--' + fileTime + '--' + account.address.slice(2) ;
var filePath = path.join(__dirname,"../static/keystore",fileName)
fs.writeFileSync(filePath,keystoreString)
await ctx.render("downloadkeystore.html",{
"downloadurl":path.join("keystore",fileName),
"privatekey":account.privateKey
})
}
}
此時生成的Keystore文件將會被保存至static/keystore目錄中
(3) 創建前端頁面
view/downloadkeystore.html
<html>
<head>
<title>保存KeyStore文件</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<div id="save-keystore">
<h1>保存KeyStore文件</h1>
<a href="<%= downloadurl %>">保存KeyStore文件</a>
<br>
<button onclick="saveKeystoreNext()">Next Step</button>
</div>
<div id="save-privatekey" style="display:none">
<h1>保存錢包私鑰</h1>
<div>
<%= privatekey %>
<span>請務必妥善保管!</span>
</div>
</div>
</div>
</body>
</html>
由於涉及到了onclick,所以,我們現在需要創建js代碼實現相關方法
實現
saveKeystoreNext
方法
- 導入jquery文件
$ mkdir js/lib
將jquery.url.js
與jquery-3.3.1.min.js
拷貝進lib目錄中
- 實現方法
static/js/wallet.js
function saveKeystoreNext(){
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
3.導航頁設計
此處的導航頁前端用的是藍鯨智雲的MagicBox組件,作爲藍鯨智雲的忠實粉絲,推薦大家使用
(1) 創建導航頁
static/html/nav.html
<link href="https://magicbox.bk.tencent.com/static_api/v3/assets/bootstrap-3.3.4/css/bootstrap.min.css" rel="stylesheet">
<link href="https://magicbox.bk.tencent.com/static_api/v3/bk/css/bk.css" rel="stylesheet">
<div class="king-horizontal-nav4">
<div class="logo_wrap">
<a class="logo" title="" href="javascript:;">
<img src="https://brucefeng-1251273438.cos.ap-shanghai.myqcloud.com/8.%E4%BB%A5%E5%A4%AA%E5%9D%8A%E7%BD%91%E9%A1%B5%E9%92%B1%E5%8C%85%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C/logo.png"
alt="" class="logo mt10 ml20">
</a>
</div>
<div class="nav_wrap tmp">
<ul>
<li>
<a href="/"><span>首頁</span></a>
</li>
<li>
<a href="/newaccount.html"><span>創建錢包</span></a>
</li>
<li>
<a href="/transaction.html"><span>轉賬</span></a>
</li>
<li>
<a href="/queryTransaction.html"><span>查詢交易</span></a>
</li>
<li>
<a href="https://github.com/DiaboFong/ETHWalletDemo" target="_blank"><span>項目代碼</span></a>
</li>
<li>
<a href="http://blog.51cto.com/clovemfong" target="_blank"><span>我的博客</span></a>
</li>
</ul>
</div>
</div>
(2) 集成導航頁
將nav.html集成到所有相關網頁中即可,如newaccount.html
<html>
<head>
<title>創建錢包</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h1>創建一個新的錢包</h1>
<form method="POST" action="/newaccount">
<input type="text" placeholder="請輸入密碼" name="password">
<button type="submit">創建錢包</button>
</form>
</div>
</body>
</html>
五.解鎖錢包賬戶
在通過錢包轉賬之前,我們都需要先對錢包賬戶進行解鎖後才能進行正常交易,目前主要的解鎖方式爲
- 私鑰解鎖
- 配置文件+密碼解鎖
- 助記詞解鎖
本文我們主要講解如何開發通過私鑰解鎖跟配置文件解鎖來進行賬戶解鎖。
我們模仿MyEtherWallet的方式,將解鎖錢包的功能放在轉賬交易模塊下面,便於整合。
1. 搭建代碼框架
(1) 創建控制器
controllers/transaction.js
module.exports = {
transactionHtml: async (ctx) =>{
await ctx.render("transaction.html")
},
}
(2) 創建前端頁面
views/transaction.html
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
</div>
<div id="unlockAccount1" style="display:none">
</div>
</div>
</body>
</html>
(3) 配置路由
router/router.js
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
module.exports = router
2. 通過私鑰解鎖賬戶
(1) 修改前端頁面
views/transaction.html
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
</div>
<div id="unlockAccount1" style="display:none">
<h3>請輸入錢包私鑰[請認準官網,防止釣魚]</h3>
<textarea id="inputAccountType1" cols="" rows="3"></textarea>
<button onclick="unlockAccountWithPK()">解鎖</button>
</div>
</div>
</body>
</html>
修改
js/wallet.js
代碼
function saveKeystoreNext(){
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function unlockAccountWithPK(){
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$post("/unlockWithPK",`privatekey=${privateKey}`,function(res,status){
console.log(status + JSON.stringify(res))
})
}
// 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function (){
$("input[name=unlockAccountType]").change(function(){
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
}else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
})
(2) 創建控制器
controllers/account.js
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
unlockWithPK: (ctx) => {
var privateKey = ctx.request.body.privatekey
console.log(privateKey)
var account = web3.eth.accounts.privateKeyToAccount(privateKey);
console.log(account.address);
ctx.response.body = "解鎖成功";
}
}
(3) 修改路由
router/router.js
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
var accountController = require("../controllers/account")
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
router.post("/unlockWithPK",accountController.unlockWithPK);
module.exports = router
(4) 階段性測試
- 啓動服務
$ cd ETHWalletDemo
$ $ node index.js
錢包啓動成功,請訪問http://127.0.0.1:3003/...進行測試
- 瀏覽器訪問
http://127.0.0.1:3003/
[1] 首頁
[2] 創建錢包
輸入密碼,此處先不隱藏,後期設計即可
)
點擊保存Keystore文件,並點擊Next Step
注意:此處的賬號私鑰不做隱藏,僅僅是爲了測試需要,大家自己實際使用的私鑰請務必保存妥當!!!
[3] 解鎖賬戶
後端返回
!
前端返回
前後端調試成功
3. 顯示賬戶餘額
賬戶剛剛創建完畢,餘額爲0,我們可以通過私鏈轉賬的方式給新賬戶轉入10以太幣,具體操作可以參考博客:以太坊聯盟鏈-多節點私鏈搭建手冊中轉賬交易章節。
(1) 修改控制器
controllers/account.js
var web3 = require("../utils/myUtils").getWeb3()
async function getAccountBalance(address) {
var balance = await web3.eth.getBalance(address);
var balanceEther = web3.utils.fromWei(balance, 'ether')
return balanceEther;
}
module.exports = {
unlockWithPK: async (ctx) => {
//1.獲取私鑰
var privateKey = ctx.request.body.privatekey
console.log(privateKey)
//2.通過私鑰解鎖賬戶
var account = web3.eth.accounts.privateKeyToAccount(privateKey);
console.log(account)
//3.獲取賬戶餘額
var balance = await getAccountBalance(account.address)
console.log(balance)
responseData = {
code: 0,
status: "success",
data: {
balance: balance,
address: account.address
}
}
ctx.response.body = responseData
}
}
(2) 修改前端頁面
views/transaction.html
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<div id="transaction0">
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
</div>
<div id="unlockAccount1" style="display:none">
<h3>請輸入錢包私鑰[請認準官網,防止釣魚]</h3>
<textarea id="inputAccountType1" cols="" rows="3"></textarea>
<button onclick="unlockAccountWithPK()">解鎖</button>
</div>
</div>
<div id="transaction1" style="display: none">
<div id="sendTransaction">
</div>
<div id="accountInfo"></div>
<div>
<span>賬戶地址:</span> <span id="accountAddress"></span>
</div>
<div>
<span>賬戶餘額:</span> <span id="accountBalance"></span>
</div>
</div>
</div>
</body>
</html>
- 將解鎖界面跟賬戶顯示頁面分離
- 根據響應是否成功顯示不同頁面
修改wallet.js文件
function saveKeystoreNext(){
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function unlockAccountWithPK(){
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$.post("/unlockWithPK",`privatekey=${privateKey}`,function(res,status){
console.log(status + JSON.stringify(res))
//將服務端返回的賬戶信息顯示到頁面上
if (res.code == 0){
console.log("success yes")
$("#accountAddress").text(res.data.address)
$("#accountBalance").text(res.data.balance + " ETH")
// 隱藏
$("#transaction0").hide()
$("#transaction1").show()
}
})
}
// 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function (){
$("input[name=unlockAccountType]").change(function(){
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
}else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
})
3. 通過配置文件解鎖賬戶
(1) 封裝響應消息
utils/myUtils.js
module.exports = {
getWeb3: () => {
var Web3 = require("web3");
var web3 = new Web3(Web3.givenProvider || 'http://127.0.0.1:8545');
return web3;
},
success: (data) => {
responseData = {
code: 0,
status: "success",
data: data
}
return responseData
},
fail :(msg) => {
responseData = {
code: 1,
status: "fail",
msg: msg
}
return responseData
}
}
(2) 修改控制器
controller/account.js
var web3 = require("../utils/myUtils").getWeb3()
var {success,fail} = require("../utils/myUtils")
var fs = require("fs")
async function getAccountBalance(address) {
var balance = await web3.eth.getBalance(address);
var balanceEther = web3.utils.fromWei(balance, 'ether')
return balanceEther;
}
module.exports = {
unlockWithPK: async (ctx) => {
//1.獲取私鑰
var privateKey = ctx.request.body.privatekey
console.log(privateKey)
//2.通過私鑰解鎖賬戶
var account = web3.eth.accounts.privateKeyToAccount(privateKey);
console.log(account)
//3.獲取賬戶餘額
var balance = await getAccountBalance(account.address)
console.log(balance)
ctx.response.body = success({
balance:balance,
address:account.address
})
},
unlockWithKS: async (ctx) => {
//獲取前端傳遞的數據,password跟keystore
var password = ctx.request.body.password
console.log(password)
var keystore = ctx.request.files.file
console.log(keystore)
//讀取緩存文件中keystore的數據
var keystoreData = fs.readFileSync(keystore.path, "utf8")
console.log(keystoreData)
// 通過keystore和密碼解鎖賬戶
var account = web3.eth.accounts.decrypt(JSON.parse(keystoreData), password)
console.log(account)
//獲取賬戶餘額
var balance = await getAccountBalance(account.address)
console.log(balance)
ctx.response.body = success({
balance:balance,
address:account.address
})
}
}
(2) 修改前端頁面
views/transaction.html
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<div id="transaction0">
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
<h3>請選擇KeyStore文件</h3>
<input type="file">
<input type="password" id="inputAccountType0">
<button onclick="unlockAccountWithKS()">解鎖</button>
</div>
<div id="unlockAccount1" style="display:none">
<h3>請輸入錢包私鑰[請認準官網,防止釣魚]</h3>
<textarea id="inputAccountType1" cols="" rows="3"></textarea>
<button onclick="unlockAccountWithPK()">解鎖</button>
</div>
</div>
<div id="transaction1" style="display: none">
<div id="sendTransaction">
</div>
<div id="accountInfo"></div>
<div>
<span>賬戶地址:</span> <span id="accountAddress"></span>
</div>
<div>
<span>賬戶餘額:</span> <span id="accountBalance"></span>
</div>
</div>
</div>
</body>
</html>
修改wallet.js文件
function saveKeystoreNext(){
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function configAccountInfo(data) {
$("#accountAddress").text(data.address)
$("#accountBalance").text(data.balance + " ETH")
// 隱藏
$("#transaction0").hide()
$("#transaction1").show()
}
function unlockAccountWithPK(){
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$.post("/unlockWithPK",`privatekey=${privateKey}`,function(res,status){
console.log(status + JSON.stringify(res))
//將服務端返回的賬戶信息顯示到頁面上
if (res.code == 0){
configAccountInfo(res.data)
}
})
}
function unlockAccountWithKS(){
var filedata = $("#inputAccountType0").val()
if (filedata.length <=0 ){
alert("未選擇文件,請選擇文件上傳!")
return
}
//文件上傳通過Formdata去存儲文件的數據
var data = new FormData()
data.append("file", $("#inputAccountType0")[0].files[0])
data.append("password",$("#inputAccountTypePassword").val())
//提交到後端的路徑
var urlStr = "/unlockWithKS"
$.ajax({
url: urlStr,
type: "post",
dataType: "json",
contentType : false,
data :data,
processData: false,
success : function(res, status) {
alert("解鎖成功,可以使用該賬戶進行轉賬操作")
if (res.code == 0) {
configAccountInfo(res.data)
}
},
error: function(res, status){
alert("KeyStore文件與密碼不匹配")
}
})
}
// 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function (){
$("input[name=unlockAccountType]").change(function(){
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
}else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
})
(3) 配置路由
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
var accountController = require("../controllers/account")
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
//通過私鑰解鎖賬戶
router.post("/unlockWithPK",accountController.unlockWithPK);
//通過配置文件解鎖賬戶
router.post("/unlockWithKS",accountController.unlockWithKS);
module.exports = router
(4) 階段性測試
!
選擇配置文件,輸入密碼解鎖
驗證成功
顯示賬戶信息
至此,我們現在完成了私鑰解鎖賬戶以及配置文件解鎖賬戶的功能了。
六.實現交易轉賬
1. 以太幣轉賬
$ npm install ethereumjs-tx
(1) 修改控制器
controllers/transaction.js
var {success, fail } = require("../utils/myUtils")
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
transactionHtml: async (ctx) => {
await ctx.render("transaction.html")
},
sendTransaction: async (ctx) => {
var { fromAddress, toAddress, amount, privateKey } = ctx.request.body
var Tx = require('ethereumjs-tx');
var privateKey = new Buffer(privateKey.slice(2), 'hex')
var nonce = await web3.eth.getTransactionCount(fromAddress)
var gasPrice = await web3.eth.getGasPrice()
var amountToWei = web3.utils.toWei(amount,'ether')
var rawTx = {
nonce: nonce ,
gasPrice: gasPrice,
gasLimit: '0x2710',
to: toAddress,
value: amountToWei,
data: '0x00'
}
//對交易的數據進行gas計算,然後將gas值設置到參數中
var gas = await web3.eth.estimateGas(rawTx)
rawTx.gas = gas
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
var responseData
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'),function(err,data){
console.log(err)
console.log(data)
if (err) {
responseData = fail(err)
}
}).then(function (data){
console.log(data)
if (data) {
responseData = success({
"blockHash": data.blockHash,
"transactionHash": data.transactionHash
})
}else {
responseData = fail("交易失敗")
}
})
}
}
(2) 修改前端頁面
views/transaction.html
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<div id="transaction0">
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
<h3>請選擇KeyStore文件</h3>
<input type="file" id="inputAccountType0">
<span>輸入密碼:</span>
<input type="password" id="inputAccountTypePassword">
<br>
<button onclick="unlockAccountWithKS()">解鎖</button>
</div>
<div id="unlockAccount1" style="display:none">
<h3>請輸入錢包私鑰[請認準官網,防止釣魚]</h3>
<textarea id="inputAccountType1" cols="" rows="3"></textarea>
<button onclick="unlockAccountWithPK()">解鎖</button>
</div>
</div>
<div id="transaction1" style="display: none">
<div id="sendTransaction">
<form id="sendTransactionForm">
<div>
<span>對方地址</span>
<input type="text" name="toAddress">
</div>
<div>
<span>發送金額</span>
<input type="text" name="amount">
</div>
<input name="fromAddress" hidden="hidden">
<input name="privateKey" hidden="hidden">
<button type="submit">發送交易</button>
</form>
</div>
<div id="accountInfo"></div>
<div>
<span>賬戶地址:</span> <span id="accountAddress"></span>
</div>
<div>
<span>賬戶餘額:</span> <span id="accountBalance"></span>
</div>
<div id="transactionComplete" style="display:none">
<div>
<span>交易Hash:</span>
<span id="transactionCompleteHash0"></span>
</div>
<div>
<span>Block Hash:</span>
<span id="transactionCompleteHash1"></span>
</div>
</div>
</div>
</div>
</body>
</html>
修改wallet.js文件
function saveKeystoreNext(){
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function configAccountInfo(data) {
$("#accountAddress").text(data.address)
$("#accountBalance").text(data.balance + " ETH")
// 隱藏
$("#transaction0").hide()
$("#transaction1").show()
$("input[name=fromAddress]").val(data.address)
$("input[name=privateKey]").val(data.privatekey)
}
function unlockAccountWithPK(){
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$.post("/unlockWithPK",`privatekey=${privateKey}`,function(res,status){
console.log(status + JSON.stringify(res))
//將服務端返回的賬戶信息顯示到頁面上
if (res.code == 0){
configAccountInfo(res.data)
}
})
}
function unlockAccountWithKS(){
var filedata = $("#inputAccountType0").val()
if (filedata.length <=0 ){
alert("未選擇文件,請選擇文件上傳!")
return
}
//文件上傳通過Formdata去存儲文件的數據
var data = new FormData()
data.append("file", $("#inputAccountType0")[0].files[0])
data.append("password",$("#inputAccountTypePassword").val())
//提交到後端的路徑
var urlStr = "/unlockWithKS"
$.ajax({
url: urlStr,
type: "post",
dataType: "json",
contentType : false,
data :data,
processData: false,
success : function(res, status) {
alert("解鎖成功,可以使用該賬戶進行轉賬操作")
if (res.code == 0) {
configAccountInfo(res.data)
}
},
error: function(res, status){
alert("KeyStore文件與密碼不匹配")
}
})
}
//轉賬 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function (){
$("input[name=unlockAccountType]").change(function(){
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
}else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
$("#sendTransactionForm").validate({
rules: {
toAddress:{
required:true,
},
amount:{
required:true,
},
},
messages: {
toAddress:{
required:"請輸入對方錢包地址",
},
amount:{
required:"請輸入轉賬金額",
},
},
submitHandler: function(form)
{
var urlStr = "/sendtransaction"
alert("urlStr:" +urlStr)
$(form).ajaxSubmit({
url:urlStr,
type:"post",
dataType:"json",
success:function(res,status){
console.log(status + JSON.stringify(res))
if (res.code == 0){
$("#transactionCompleteHash0").text(res.data.transactionHash)
$("#transactionCompleteHash1").text(res.data.blockHash)
$("#transactionComplete").show()
}
},
error:function(res,status){
console.log(status + JSON.stringify(res))
}
})
}
})
})
(3) 配置路由
router/router.js
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
var accountController = require("../controllers/account")
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
router.post("/sendtransaction",transactionController.sendTransaction)
//通過私鑰解鎖賬戶
router.post("/unlockWithPK",accountController.unlockWithPK);
//通過配置文件解鎖賬戶
router.post("/unlockWithKS",accountController.unlockWithKS);
module.exports = router
2. 交易信息查詢
(1) 修改控制器
controller/transaction.js
var { success, fail } = require("../utils/myUtils")
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
transactionHtml: async (ctx) => {
await ctx.render("transaction.html")
},
sendTransaction: async (ctx) => {
var { fromAddress, toAddress, amount, privateKey } = ctx.request.body
var Tx = require('ethereumjs-tx');
var privateKey = new Buffer(privateKey.slice(2), 'hex')
var nonce = await web3.eth.getTransactionCount(fromAddress)
var gasPrice = await web3.eth.getGasPrice()
var amountToWei = web3.utils.toWei(amount, 'ether')
var rawTx = {
nonce: nonce,
gasPrice: gasPrice,
gasLimit: '0x2710',
to: toAddress,
value: amountToWei,
data: '0x00'
}
//對交易的數據進行gas計算,然後將gas值設置到參數中
var gas = await web3.eth.estimateGas(rawTx)
rawTx.gas = gas
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
var responseData;
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'), function (err, data) {
console.log(err)
console.log(data)
if (err) {
responseData = fail(err)
}
}).then(function (data) {
console.log(data)
if (data) {
responseData = success({
"blockHash": data.blockHash,
"transactionHash": data.transactionHash
})
} else {
responseData = fail("交易失敗")
}
})
ctx.response.body = responseData
},
queryTransactionHtml: async (ctx) => {
await ctx.render("queryTransaction.html")
},
queryTransaction: async (ctx) => {
var txHash = ctx.request.body.txHash
await web3.eth.getTransaction(txHash, function (err, res) {
if (err) {
responseData = fail(err)
}
}).then(function(res){
if (res) {
responseData = success(res)
}else {
responseData = fail("查詢失敗")
}
})
ctx.response.body = responseData
}
}
(2) 創建前端頁面
views/queryTransaction.html
<html>
<head>
<title>查詢交易詳情</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h1>查詢交易詳情</h1>
<input type="text" id="txHash">
<button onclick="queryTransaction()">查詢</button>
<!-- 用於顯示代碼塊 -->
<pre id="transactionInfo"></pre>
</div>
</body>
</html>
修改wallet.js文件
function saveKeystoreNext() {
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function configAccountInfo(data) {
$("#accountAddress").text(data.address)
$("#accountBalance").text(data.balance + " ETH")
// 隱藏
$("#transaction0").hide()
$("#transaction1").show()
$("input[name=fromAddress]").val(data.address)
$("input[name=privateKey]").val(data.privatekey)
}
function unlockAccountWithPK() {
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$.post("/unlockWithPK", `privatekey=${privateKey}`, function (res, status) {
console.log(status + JSON.stringify(res))
//將服務端返回的賬戶信息顯示到頁面上
if (res.code == 0) {
configAccountInfo(res.data)
}
})
}
function unlockAccountWithKS() {
var filedata = $("#inputAccountType0").val()
if (filedata.length <= 0) {
alert("未選擇文件,請選擇文件上傳!")
return
}
//文件上傳通過Formdata去存儲文件的數據
var data = new FormData()
data.append("file", $("#inputAccountType0")[0].files[0])
data.append("password", $("#inputAccountTypePassword").val())
//提交到後端的路徑
var urlStr = "/unlockWithKS"
$.ajax({
url: urlStr,
type: "post",
dataType: "json",
contentType: false,
data: data,
processData: false,
success: function (res, status) {
alert("解鎖成功,可以使用該賬戶進行轉賬操作")
if (res.code == 0) {
configAccountInfo(res.data)
}
},
error: function (res, status) {
alert("KeyStore文件與密碼不匹配")
}
})
}
//轉賬 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function () {
$("input[name=unlockAccountType]").change(function () {
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
} else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
$("#sendTransactionForm").validate({
rules: {
toAddress: {
required: true,
},
amount: {
required: true,
},
},
messages: {
toAddress: {
required: "請輸入對方錢包地址",
},
amount: {
required: "請輸入轉賬金額",
},
},
submitHandler: function (form) {
var urlStr = "/sendtransaction"
alert("urlStr:" + urlStr)
$(form).ajaxSubmit({
url: urlStr,
type: "post",
dataType: "json",
success: function (res, status) {
console.log(status + JSON.stringify(res))
if (res.code == 0) {
$("#transactionCompleteHash0").text(res.data.transactionHash)
$("#transactionCompleteHash1").text(res.data.blockHash)
$("#transactionComplete").show()
}
},
error: function (res, status) {
console.log(status + JSON.stringify(res))
}
})
}
})
})
//查詢交易詳情
function queryTransaction() {
var txHash = $("#txHash").val()
$.post("/queryTransaction", "txHash=" + txHash, function (res, status) {
console.log(status + JSON.stringify(res))
if (res.code == 0) {
alert("查詢成功")
$("#transactionInfo").text(JSON.stringify(res.data, null, 4))
} else {
alert("查詢失敗")
}
})
}
(3) 配置路由
router/router.js
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
var accountController = require("../controllers/account")
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
//發送交易
router.post("/sendtransaction",transactionController.sendTransaction)
//查詢交易詳情
router.get("/queryTransaction.html",transactionController.queryTransactionHtml)
router.post("/queryTransaction",transactionController.queryTransaction)
//通過私鑰解鎖賬戶
router.post("/unlockWithPK",accountController.unlockWithPK);
//通過配置文件解鎖賬戶
router.post("/unlockWithKS",accountController.unlockWithKS);
module.exports = router
(4) 階段性測試
執行賬戶解鎖
執行轉賬交易
以太坊私有鏈終端產生交易,執行挖礦
顯示交易Hash以及區塊Hash
通過交易Hash返回交易詳情
3. 實現Token轉賬
以上內容,我們實現了對以太幣的轉賬與查詢功能,而現實情況中,我們很多區塊鏈公司都會開發適用於以太坊與自己Token的錢包,本文直接沿用以太坊智能合約項目-Token合約開發與部署中編寫的合約代碼。
(1) 獲取合約信息
- 獲取Token的ABI
- 獲取Token合約地址
通過Remix進行部署後獲取地址:0xa77a5c71b9cf71e89215ceec9767c536e79ced68
(2) 創建控制器
controller/token.js
var { success, fail } = require("../utils/myUtils")
var web3 = require("../utils/myUtils").getWeb3()
var myContract = require("../models/contract").getContract()
module.exports = {
sendTokenTransaction: async (ctx) => {
var { fromAddress, toAddress, amount, privateKey } = ctx.request.body
var Tx = require('ethereumjs-tx');
var privateKey = new Buffer(privateKey.slice(2), 'hex')
var nonce = await web3.eth.getTransactionCount(fromAddress)
var gasPrice = await web3.eth.getGasPrice()
//獲取Token合約的decimals
var decimals = await myContract.methods.decimals().call()
var amountToWei = amount * Math.pow(10, decimals)
var myBalance = await myContract.methods.balanceOf(fromAddress).call()
if (myBalance < amountToWei) {
ctx.response.body = fail("餘額不足")
return
}
var tokenData = await myContract.methods.transfer(toAddress, amountToWei).encodeABI()
var rawTx = {
nonce: nonce,
gasPrice: gasPrice,
gasLimit: '0x2710',
to: myContract.options.address, //如果是發送token ,此處應該填寫合約地址,此處需要注意
value: amountToWei,
// data: tokenData
data: "0x00"
}
//對交易的數據進行gas計算,然後將gas值設置到參數中
var gas = await web3.eth.estimateGas(rawTx)
rawTx.gas = gas
var tx = new Tx(rawTx);
tx.sign(privateKey);
var serializedTx = tx.serialize();
var responseData;
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex'), function (err, data) {
console.log(err)
console.log(data)
if (err) {
responseData = fail(err)
}
}).then(function (data) {
console.log(data)
if (data) {
responseData = success({
"blockHash": data.blockHash,
"transactionHash": data.transactionHash
})
} else {
responseData = fail("交易失敗")
}
})
ctx.response.body = responseData
}
}
(3) 創建模型
models/contract.js
var web3 = require("../utils/myUtils").getWeb3()
module.exports = {
getContract :(ctx)=> {
var ABI = [
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "success",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "remaining",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"name": "_name",
"type": "string"
},
{
"name": "_symbol",
"type": "string"
},
{
"name": "_decimals",
"type": "uint8"
},
{
"name": "_totalSupply",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_from",
"type": "address"
},
{
"indexed": true,
"name": "_to",
"type": "address"
},
{
"indexed": false,
"name": "_value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "_owner",
"type": "address"
},
{
"indexed": true,
"name": "_spender",
"type": "address"
},
{
"indexed": false,
"name": "_value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
}
]
var contractAddress = "0xa77a5c71b9cf71e89215ceec9767c536e79ced68"
var myContract = new web3.eth.Contract(ABI,contractAddress)
return myContract
}
}
(4) 修改控制器
controller/account.js
var web3 = require("../utils/myUtils").getWeb3()
var { success, fail } = require("../utils/myUtils")
var fs = require("fs")
var myContract = require("..//models/contract").getContract()
async function getAccountBalance(address) {
var balance = await web3.eth.getBalance(address);
var balanceEther = web3.utils.fromWei(balance, 'ether')
return balanceEther;
}
async function setResponseData(account) {
//配置返回給前端的數據:以太幣跟Token的數據
var balance = await getAccountBalance(account.address)
console.log(balance)
//獲取Token數據
var tokenBalance = await myContract.methods.balanceOf(account.address).call()
var tokenSymbol = await myContract.methods.symbol().call()
return success({
balance: balance,
address: account.address,
privatekey: account.privateKey,
tokenBalance: tokenBalance,
tokenSymbol: tokenSymbol
})
}
module.exports = {
unlockWithPK: async (ctx) => {
//1.獲取私鑰
var privateKey = ctx.request.body.privatekey
console.log(privateKey)
//2.通過私鑰解鎖賬戶
var account = web3.eth.accounts.privateKeyToAccount(privateKey);
console.log(account)
//3.獲取賬戶餘額
var balance = await getAccountBalance(account.address)
//將賬戶信息返回給前端
ctx.response.body = await setResponseData(account)
},
unlockWithKS: async (ctx) => {
//獲取前端傳遞的數據,password跟keystore
var password = ctx.request.body.password
console.log(password)
var keystore = ctx.request.files.file
console.log(keystore)
//讀取緩存文件中keystore的數據
var keystoreData = fs.readFileSync(keystore.path, "utf8")
console.log(keystoreData)
// 通過keystore和密碼解鎖賬戶
var account = web3.eth.accounts.decrypt(JSON.parse(keystoreData), password)
console.log(account)
//將賬戶信息返回給前端
ctx.response.body = await setResponseData(account)
}
}
修改前端頁面
<html>
<head>
<title>轉賬</title>
<script src="js/lib/jquery-3.3.1.min.js"></script>
<script src="js/lib/jquery.url.js"></script>
<script src="js/wallet.js"></script>
<link rel="stylesheet" href="css/wallet.css">
</head>
<body>
<div id="nav">
<script>
$("#nav").load("html/nav.html")
</script>
</div>
<div id="main">
<h3>發送以太幣/Token</h3>
<div id="transaction0">
<input type="radio" id="unlockAccountType0" name="unlockAccountType" value="0">
<label for="unlockAccountType0">Keystore File</label>
<br>
<input type="radio" id="unlockAccountType1" name="unlockAccountType" vaule="1">
<label for="unlockAccountType1">Private Key</label>
<!-- unlockAccount0 表示通過Keystore解鎖賬戶 unlockAccount1 表示通過私鑰解鎖賬戶-->
<div id="unlockAccount0" style="display:none">
<h3>請選擇KeyStore文件</h3>
<input type="file" id="inputAccountType0">
<span>輸入密碼:</span>
<input type="password" id="inputAccountTypePassword">
<br>
<button onclick="unlockAccountWithKS()">解鎖</button>
</div>
<div id="unlockAccount1" style="display:none">
<h3>請輸入錢包私鑰[請認準官網,防止釣魚]</h3>
<textarea id="inputAccountType1" cols="" rows="3"></textarea>
<button onclick="unlockAccountWithPK()">解鎖</button>
</div>
</div>
<div id="transaction1" style="display: none">
<div id="sendTransaction">
<form id="sendTransactionForm">
<div>
<span>對方地址</span>
<input type="text" name="toAddress">
</div>
<div>
<span>發送金額</span>
<input type="text" name="amount">
</div>
<input name="fromAddress" hidden="hidden">
<input name="privateKey" hidden="hidden">
<button type="submit">發送交易</button>
</form>
</div>
<div id="accountInfo"></div>
<div>
<span>賬戶地址:</span> <span id="accountAddress"></span>
</div>
<div>
<span>賬戶餘額:</span> <span id="accountBalance"></span>
<br>
<span id="accountTokenInfo"></span>
</div>
<div id="transactionComplete" style="display:none">
<div>
<span>交易Hash:</span>
<span id="transactionCompleteHash0"></span>
</div>
<div>
<span>Block Hash:</span>
<span id="transactionCompleteHash1"></span>
</div>
</div>
</div>
</div>
</body>
</html>
修改wallet.js文件
function saveKeystoreNext() {
//隱藏保存keystore頁面
$("#save-keystore").hide()
//顯示保存private頁面
$("#save-privatekey").show()
}
function configAccountInfo(data) {
$("#accountAddress").text(data.address)
$("#accountBalance").text(data.balance + " ETH")
// 隱藏
$("#transaction0").hide()
$("#transaction1").show()
$("input[name=fromAddress]").val(data.address)
$("input[name=privateKey]").val(data.privatekey)
$("#accountTokenInfo").text(data.tokenBalance + " " + data.tokenSymbol)
$("#TokenSymbol").text(data.tokenSymbol)
}
function unlockAccountWithPK() {
var privateKey = $("#inputAccountType1").val()
console.log(privateKey)
//將私鑰傳至服務端
$.post("/unlockWithPK", `privatekey=${privateKey}`, function (res, status) {
console.log(status + JSON.stringify(res))
//將服務端返回的賬戶信息顯示到頁面上
if (res.code == 0) {
configAccountInfo(res.data)
}
})
}
function unlockAccountWithKS() {
var filedata = $("#inputAccountType0").val()
if (filedata.length <= 0) {
alert("未選擇文件,請選擇文件上傳!")
return
}
//文件上傳通過Formdata去存儲文件的數據
var data = new FormData()
data.append("file", $("#inputAccountType0")[0].files[0])
data.append("password", $("#inputAccountTypePassword").val())
//提交到後端的路徑
var urlStr = "/unlockWithKS"
$.ajax({
url: urlStr,
type: "post",
dataType: "json",
contentType: false,
data: data,
processData: false,
success: function (res, status) {
alert("解鎖成功,可以使用該賬戶進行轉賬操作")
if (res.code == 0) {
configAccountInfo(res.data)
}
},
error: function (res, status) {
alert("KeyStore文件與密碼不匹配")
}
})
}
//轉賬 對元素的操作需要等文檔加載完畢後才能調用成功
$(document).ready(function () {
$("input[name=unlockAccountType]").change(function () {
if (this.value == 0) {
//如果點擊keystore,則顯示keystore操作
$("#unlockAccount0").show()
$("#unlockAccount1").hide()
} else {
$("#unlockAccount0").hide()
$("#unlockAccount1").show()
}
})
$("#sendTransactionForm").validate({
rules: {
toAddress: {
required: true,
},
amount: {
required: true,
},
},
messages: {
toAddress: {
required: "請輸入對方錢包地址",
},
amount: {
required: "請輸入轉賬金額",
},
},
submitHandler: function (form) {
var urlStr
var tokenType = $("#TokenType").val()
if (tokenType == 0) {
urlStr = "/sendtransaction"
}else {
urlStr = "/sendToken"
}
alert("urlStr:" + urlStr)
$(form).ajaxSubmit({
url: urlStr,
type: "post",
dataType: "json",
success: function (res, status) {
console.log(status + JSON.stringify(res))
if (res.code == 0) {
$("#transactionCompleteHash0").text(res.data.transactionHash)
$("#transactionCompleteHash1").text(res.data.blockHash)
$("#transactionComplete").show()
}
},
error: function (res, status) {
console.log(status + JSON.stringify(res))
}
})
}
})
})
//查詢交易詳情
function queryTransaction() {
var txHash = $("#txHash").val()
$.post("/queryTransaction", "txHash=" + txHash, function (res, status) {
console.log(status + JSON.stringify(res))
if (res.code == 0) {
alert("查詢成功")
$("#transactionInfo").text(JSON.stringify(res.data, null, 4))
} else {
alert("查詢失敗")
}
})
}
(5) 配置路由
var router = require("koa-router")();
var newAccount = require("../controllers/newAccount");
var transactionController = require("../controllers/transaction");
var accountController = require("../controllers/account")
var tokenController = require("../controllers/token")
router.get("/",newAccount.homeHtml);
//創建賬號的頁面
router.get("/newaccount.html",newAccount.newAccountHtml);
//提交創建賬號表單
router.post("/newaccount",newAccount.newAccount);
//獲取轉賬頁面
router.get("/transaction.html",transactionController.transactionHtml);
//發送交易
router.post("/sendtransaction",transactionController.sendTransaction)
//查詢交易詳情
router.get("/queryTransaction.html",transactionController.queryTransactionHtml)
router.post("/queryTransaction",transactionController.queryTransaction)
//通過私鑰解鎖賬戶
router.post("/unlockWithPK",accountController.unlockWithPK);
//通過配置文件解鎖賬戶
router.post("/unlockWithKS",accountController.unlockWithKS);
//token轉賬
router.post("/sendToken", tokenController.sendTokenTransaction)
module.exports = router