摘要:本文通過宏觀和微觀兩個層面窺探以太坊底層執(zhí)行邏輯。開發(fā)等前端還是好,和就免了不太好用全局安裝初始化一個基于的項目項目里安裝依賴是的庫,通過方式與以太坊節(jié)點交互。
本文通過宏觀和微觀兩個層面窺探以太坊底層執(zhí)行邏輯。
宏觀層面描述創(chuàng)建并運(yùn)行一個小型帶錢包的發(fā)幣APP的過程,微觀層面是順藤摸瓜從http api深入go-ethereum源碼執(zhí)行過程。
分析思路:自上而下,從APP深入EVM。
從應(yīng)用入手,如果一頭扎進(jìn)ethereum,收獲的可能是純理論的東西,要想有所理解還得結(jié)合以后的實踐才能恍然大悟。所以我始終堅持從應(yīng)用入手、自上而下是一種正確、事半功倍的方法論。
我在講解以太坊基礎(chǔ)概念的那篇專題文章里,用的是從整體到局部的方法論,因為研究目標(biāo)就是一個抽象的理論的東西,我對一個全然未知的東西的了解總是堅持從整體到局部的思路。
項目創(chuàng)建、部署合約到私鏈之前用truffle框架做項目開發(fā),這個框架封裝了合約的創(chuàng)建、編譯、部署過程,為了研究清楚自上至下的架構(gòu),這里就不用truffle構(gòu)建項目了。
項目前端基于vue,后端是geth節(jié)點,通過web3 http api通信。
開發(fā)vue、solidity等前端IDE還是webstorm好,Atom和goland就免了不太好用!
1、全局安裝vue-cli
npm i -g vue-cli
2、初始化一個基于webpack的vue項目
vue init webpack XXXProject
3、項目里安裝web3依賴
web3.js是ethereum的javascript api庫,通過rpc方式與以太坊節(jié)點交互。
npm install --save [email protected]
盡量用npm安裝,不要用cnpm,有時候是個坑玩意,會生成“_”開頭的很多垃圾還要求各種install。也可以寫好了package.json,刪除node_modules文件夾,再執(zhí)行npm i。web3版本用1.0以上,和1.0以下語法有很大不同。
4、項目里創(chuàng)建全局web3對象
用vuex有點啰嗦,這里就寫個vue插件,提供全局的web3對象。
import Web3 from "web3" export default { install: function (Vue, options) { var web3 = window.web3 if (typeof web3 !== "undefined") { web3 = new Web3(web3.currentProvider) } else { web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")) } Vue.prototype.$web3 = web3 } }
在main.js里啟用該插件,以后就可以這樣使用this.$web3這個全局對象了。
Vue.use(插件名)
5、寫一個ERC20合約
代碼省略
項目全部代碼地址:
https://github.com/m3shine/To...
6、編譯&部署合約
有必要說明一下編譯和部署方式的選擇,它嚴(yán)重關(guān)系到你實際項目的開發(fā):
1)使用Remix,把自己寫好的合約拷貝到Remix里進(jìn)行編譯和部署。這種方式最方便。
2)使用truffle這類的框架,這種方式是需要項目基于框架開發(fā)了,編譯和部署也是在truffle控制臺進(jìn)行。
3)基于web3和solc依賴,寫編譯(solc)和部署(web3)程序,這些代碼就獨(dú)立(vue是前端,nodejs是后端,運(yùn)行環(huán)境不同)于項目了,用node多帶帶運(yùn)行。
4)本地安裝solidity進(jìn)行編譯,部署的話也需要自己想辦法完成。
5)使用geth錢包、mist等編譯部署。
……
從geth1.6.0開始,剝離了solidity編譯函數(shù),所以web3也不能調(diào)用編譯方法了。可以本地安裝solidity帶編譯器,也可以在項目里依賴solc進(jìn)行編譯。
編譯部署的方式眼花繚亂,這里選擇方式3。
編譯部署參考代碼(web3的1.0及以上版本):token_deploy.js
const Web3 = require("web3") const fs = require("fs") const solc = require("solc") const web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545")); const input = fs.readFileSync("../contracts/Token.sol"); const output = solc.compile(input.toString(), 1); fs.writeFile("../abi/Token.abi", output.contracts[":Token"].interface, err => { if (err) { console.log(err) } }) const bytecode = output.contracts[":Token"].bytecode; const abi = JSON.parse(output.contracts[":Token"].interface); const tokenContract = new web3.eth.Contract(abi); let log = { time: new Date(Date.now()), transactionHash: "", contractAddress: "" } // 部署合約 tokenContract.deploy({ data: "0x" + bytecode, arguments: ["200000000", "魔法幣", "MFC"] // Token.sol構(gòu)造參數(shù) }) .send({ from: "0x2d2afb7d0ef71f85dfbdc89d288cb3ce8e049e10", //寫你自己的礦工(發(fā)幣)地址 gas: 5000000, // 這個值很微妙 }, (err, transactionHash) => { if (err) { console.log(err); return; } log.transactionHash = transactionHash }) .on("error", error => { console.log(error); }) // 不是總能成功獲取newContractInstance, 包括監(jiān)聽receipt也可能發(fā)生異常,原因是receipt獲取時機(jī)可能發(fā)生在交易未完成前。 .then(function (newContractInstance) { if(newContractInstance){ log.contractAddress = newContractInstance.options.address } fs.appendFile("Token_deploy.log",JSON.stringify(log) + " ", err => { if (err) { console.log(err) } }) }); ;
7、在執(zhí)行部署腳本前,需要有一個賬戶并解鎖,在geth控制臺執(zhí)行以下命令:
personal.newAccount("密碼") personal.unlockAccount(eth.coinbase,"密碼","20000")
8、發(fā)布合約是需要eth幣的,所以先挖礦弄點以太幣:
miner.start()
9、現(xiàn)在可以執(zhí)行編譯部署腳本了:
node token_deploy.js
如果前面miner.stop()過,那么在執(zhí)行部署的時候,要確保miner.start(),有礦工打包才能出塊。
這里還要知道,因為就是本礦工賬戶創(chuàng)建合約,所以交易費(fèi)又回到了本賬戶,因此余額看起來總是沒有減少。
至此,我們已經(jīng)在私鏈上部署了一個合約,產(chǎn)生了一筆交易(即創(chuàng)建合約本身這個交易)、一個礦工賬戶、一個合約賬戶。
常見錯誤Error: insufficient funds for gas * price + value
意思是賬戶里沒有足夠的eth幣,給創(chuàng)建合約的賬戶里弄些比特幣。
Error: intrinsic gas too low
調(diào)高以下發(fā)布合約時的gas值。
Error: Invalid number of parameters for "undefined". Got 0 expected 3! (類似這樣的)
沒有傳入合約構(gòu)造函數(shù)參數(shù)
調(diào)用鏈上合約合約部署成功,就有了合約地址,根據(jù)合約地址構(gòu)建合約實例。
let tokenContract = new this.$web3.eth.Contract(JSON.parse(abi),"合約地址")
tokenContract.methods.myMethod.
call()調(diào)用的都是abi里的constant方法,即合約里定義的狀態(tài)屬性,EVM里不會發(fā)送交易,不會改變合約狀態(tài)。
send()調(diào)用的是合約里定義的函數(shù),是要發(fā)送交易到合約并執(zhí)行合約方法的,會改變合約狀態(tài)。
以上就簡單說一下,不寫太多了??垂倏梢宰孕邢螺d本項目源碼(上面第5步有g(shù)ithub鏈接),自己運(yùn)行起來看看界面和發(fā)幣/轉(zhuǎn)賬操作。
源碼交易過程分析當(dāng)我們在項目中創(chuàng)建一個合約的時候,發(fā)生了什么?
geth節(jié)點默認(rèn)開放了8545 RPC端口,web3通過連接這個rpc端口,以http的方式調(diào)用geth開放的rpc方法。從這一web3與以太坊節(jié)點交互基本原理入手,先分析web3源碼是怎樣調(diào)用rpc接口,對應(yīng)的geth接口是否同名,再看geth源碼該接口又是怎么執(zhí)行的。
new web3.eth.Contract(jsonInterface[, address][, options])這個函數(shù),jsonInterface就是abi,不管傳不傳options,options.data屬性總是abi的編碼。
這個web3接口源碼中調(diào)用eth.sendTransaction,options.data編碼前面加了簽名,options.to賦值一個地址,最后返回這筆交易的hash。
再返回上面第6步看一下部署腳本,代碼截止到deploy都是在構(gòu)造web3里的對象,首次與本地geth節(jié)點通信的方法是send,它是web3的一個接口方法。
deploy返回的是個web3定義的泛型TransactionObject
Contract對send接口方法的實現(xiàn)如下:
var sendTransaction = (new Method({ name: "sendTransaction", call: "eth_sendTransaction", params: 1, inputFormatter: [formatters.inputTransactionFormatter], requestManager: _this._parent._requestManager, accounts: Contract._ethAccounts, // is eth.accounts (necessary for wallet signing) defaultAccount: _this._parent.defaultAccount, defaultBlock: _this._parent.defaultBlock, extraFormatters: extraFormatters })).createFunction(); return sendTransaction(args.options, args.callback);
這個send最終由XMLHttpRequest2的request.send(JSON.stringify(payload))與節(jié)點通信。
var sendSignedTx = function(sign){ payload.method = "eth_sendRawTransaction"; payload.params = [sign.rawTransaction]; method.requestManager.send(payload, sendTxCallback); };
所以send方法對應(yīng)的節(jié)點api是eth_sendRawTransaction。
go-ethereum/ethclient/ethclient.go
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error { data, err := rlp.EncodeToBytes(tx) if err != nil { return err } return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data)) }
找到該api執(zhí)行入口
go-ethereum/internal/ethapi.SendTransaction
func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) { // Look up the wallet containing the requested signer account := accounts.Account{Address: args.From} wallet, err := s.b.AccountManager().Find(account) if err != nil { return common.Hash{}, err } …… return submitTransaction(ctx, s.b, signed) }
我們在這個函數(shù)處打一個斷點!然后執(zhí)行部署腳本(可以多次執(zhí)行),運(yùn)行到斷點處:
要調(diào)試geth需要對其重新編譯,去掉它原來編譯的優(yōu)化,參見下面“調(diào)試源碼”一節(jié)。
(dlv) p args github.com/ethereum/go-ethereum/internal/ethapi.SendTxArgs { From: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], To: *github.com/ethereum/go-ethereum/common.Address nil, Gas: *5000000, GasPrice: *github.com/ethereum/go-ethereum/common/hexutil.Big { neg: false, abs: math/big.nat len: 1, cap: 1, [18000000000],}, Value: *github.com/ethereum/go-ethereum/common/hexutil.Big nil, Nonce: *github.com/ethereum/go-ethereum/common/hexutil.Uint64 nil, Data: *github.com/ethereum/go-ethereum/common/hexutil.Bytes len: 2397, cap: 2397, [96,96,96,64,82,96,2,128,84,96,255,25,22,96,18,23,144,85,52,21,97,0,28,87,96,0,128,253,91,96,64,81,97,8,125,56,3,128,97,8,125,131,57,129,1,96,64,82,128,128,81,145,144,96,32,1,128,81,130,1,145,144,96,32,...+2333 more], Input: *github.com/ethereum/go-ethereum/common/hexutil.Bytes nil,}
(dlv) p wallet github.com/ethereum/go-ethereum/accounts.Wallet(*github.com/ethereum/go-ethereum/accounts/keystore.keystoreWallet) *{ account: github.com/ethereum/go-ethereum/accounts.Account { Address: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], URL: (*github.com/ethereum/go-ethereum/accounts.URL)(0xc4200d9f18),}, keystore: *github.com/ethereum/go-ethereum/accounts/keystore.KeyStore { storage: github.com/ethereum/go-ethereum/accounts/keystore.keyStore(*github.com/ethereum/go-ethereum/accounts/keystore.keyStorePassphrase) ..., cache: *(*github.com/ethereum/go-ethereum/accounts/keystore.accountCache)(0xc4202fe360), changes: chan struct {} { qcount: 0, dataqsiz: 1, buf: *[1]struct struct {} [ {}, ], elemsize: 0, closed: 0, elemtype: *runtime._type { size: 0, ptrdata: 0, hash: 670477339, tflag: 2, align: 1, fieldalign: 1, kind: 153, alg: *(*runtime.typeAlg)(0x59e69d0), gcdata: *1, str: 67481, ptrToThis: 601472,}, sendx: 0, recvx: 0, recvq: waitq{ first: *(*sudog )(0xc42006ed20), last: *(*sudog )(0xc42006ed20),}, sendq: waitq { first: *sudog nil, last: *sudog nil,}, lock: runtime.mutex {key: 0},}, unlocked: map[github.com/ethereum/go-ethereum/common.Address]*github.com/ethereum/go-ethereum/accounts/keystore.unlocked [...], wallets: []github.com/ethereum/go-ethereum/accounts.Wallet len: 2, cap: 2, [ ..., ..., ], updateFeed: (*github.com/ethereum/go-ethereum/event.Feed)(0xc4202c4040), updateScope: (*github.com/ethereum/go-ethereum/event.SubscriptionScope)(0xc4202c40b0), updating: true, mu: (*sync.RWMutex)(0xc4202c40cc),},}
(dlv) p s.b github.com/ethereum/go-ethereum/internal/ethapi.Backend(*github.com/ethereum/go-ethereum/eth.EthApiBackend) *{ eth: *github.com/ethereum/go-ethereum/eth.Ethereum { config: *(*github.com/ethereum/go-ethereum/eth.Config)(0xc420153000), chainConfig: *(*github.com/ethereum/go-ethereum/params.ChainConfig)(0xc4201da540), shutdownChan: chan bool { qcount: 0, dataqsiz: 0, buf: *[0]bool [], elemsize: 1, closed: 0, elemtype: *runtime._type { size: 1, ptrdata: 0, hash: 335480517, tflag: 7, align: 1, fieldalign: 1, kind: 129, alg: *(*runtime.typeAlg)(0x59e69e0), gcdata: *1, str: 21072, ptrToThis: 452544,}, sendx: 0, recvx: 0, recvq: waitq{ first: *(*sudog )(0xc420230ba0), last: *(*sudog )(0xc420231440),}, sendq: waitq { first: *sudog nil, last: *sudog nil,}, lock: runtime.mutex {key: 0},}, stopDbUpgrade: nil, txPool: *(*github.com/ethereum/go-ethereum/core.TxPool)(0xc420012380), blockchain: *(*github.com/ethereum/go-ethereum/core.BlockChain)(0xc42029c000), protocolManager: *(*github.com/ethereum/go-ethereum/eth.ProtocolManager)(0xc420320270), lesServer: github.com/ethereum/go-ethereum/eth.LesServer nil, chainDb: github.com/ethereum/go-ethereum/ethdb.Database(*github.com/ethereum/go-ethereum/ethdb.LDBDatabase) ..., eventMux: *(*github.com/ethereum/go-ethereum/event.TypeMux)(0xc4201986c0), engine: github.com/ethereum/go-ethereum/consensus.Engine(*github.com/ethereum/go-ethereum/consensus/ethash.Ethash) ..., accountManager: *(*github.com/ethereum/go-ethereum/accounts.Manager)(0xc420089860), bloomRequests: chan chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval { qcount: 0, dataqsiz: 0, buf: *[0]chan *github.com/ethereum/go-ethereum/core/bloombits.Retrieval [], elemsize: 8, closed: 0, elemtype: *runtime._type { size: 8, ptrdata: 8, hash: 991379238, tflag: 2, align: 8, fieldalign: 8, kind: 50, alg: *(*runtime.typeAlg)(0x59e6a10), gcdata: *1, str: 283111, ptrToThis: 0,}, sendx: 0, recvx: 0, recvq: waitq { first: *(*sudog )(0xc420230c00), last: *(*sudog )(0xc4202314a0),}, sendq: waitq { first: *sudog nil, last: *sudog nil,}, lock: runtime.mutex {key: 0},}, bloomIndexer: unsafe.Pointer(0xc4201b23c0), ApiBackend: *(*github.com/ethereum/go-ethereum/eth.EthApiBackend)(0xc4202b8910), miner: *(*github.com/ethereum/go-ethereum/miner.Miner)(0xc420379540), gasPrice: *(*math/big.Int)(0xc420233c40), etherbase: github.com/ethereum/go-ethereum/common.Address [45,42,251,125,14,247,31,133,223,189,200,157,40,140,179,206,142,4,158,16], networkId: 13, netRPCService: *(*github.com/ethereum/go-ethereum/internal/ethapi.PublicNetAPI)(0xc42007feb0), lock: (*sync.RWMutex)(0xc4202ea528),}, gpo: *github.com/ethereum/go-ethereum/eth/gasprice.Oracle { backend: github.com/ethereum/go-ethereum/internal/ethapi.Backend(*github.com/ethereum/go-ethereum/eth.EthApiBackend) ..., lastHead: github.com/ethereum/go-ethereum/common.Hash [139,147,220,247,224,227,136,250,220,62,217,102,160,96,23,182,90,90,108,254,82,158,234,95,150,120,163,5,61,248,168,168], lastPrice: *(*math/big.Int)(0xc420233c40), cacheLock: (*sync.RWMutex)(0xc420010938), fetchLock: (*sync.Mutex)(0xc420010950), checkBlocks: 20, maxEmpty: 10, maxBlocks: 100, percentile: 60,},}
// submitTransaction is a helper function that submits tx to txPool and logs a message. func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } if tx.To() == nil { signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) from, err := types.Sender(signer, tx) if err != nil { return common.Hash{}, err } addr := crypto.CreateAddress(from, tx.Nonce()) log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex()) } else { log.Info("Submitted transaction", "fullhash", tx.Hash().Hex(), "recipient", tx.To()) } return tx.Hash(), nil }
(dlv) p tx *github.com/ethereum/go-ethereum/core/types.Transaction { data: github.com/ethereum/go-ethereum/core/types.txdata { AccountNonce: 27, Price: *(*math/big.Int)(0xc4217f5640), GasLimit: 5000000, Recipient: *github.com/ethereum/go-ethereum/common.Address nil, Amount: *(*math/big.Int)(0xc4217f5620), Payload: []uint8 len: 2397, cap: 2397, [96,96,96,64,82,96,2,128,84,96,255,25,22,96,18,23,144,85,52,21,97,0,28,87,96,0,128,253,91,96,64,81,97,8,125,56,3,128,97,8,125,131,57,129,1,96,64,82,128,128,81,145,144,96,32,1,128,81,130,1,145,144,96,32,...+2333 more], V: *(*math/big.Int)(0xc4217e0a20), R: *(*math/big.Int)(0xc4217e09c0), S: *(*math/big.Int)(0xc4217e09e0), Hash: *github.com/ethereum/go-ethereum/common.Hash nil,}, hash: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,}, size: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,}, from: sync/atomic.Value { noCopy: sync/atomic.noCopy {}, v: interface {} nil,},}
(dlv) bt 0 0x00000000048d9248 in github.com/ethereum/go-ethereum/internal/ethapi.submitTransaction at ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1130 1 0x00000000048d9bd1 in github.com/ethereum/go-ethereum/internal/ethapi.(*PublicTransactionPoolAPI).SendTransaction at ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1176
(dlv) frame 0 l > github.com/ethereum/go-ethereum/internal/ethapi.submitTransaction() ./go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1130 (PC: 0x48d9248) Warning: debugging optimized function 1125: if err := b.SendTx(ctx, tx); err != nil { 1126: return common.Hash{}, err 1127: } 1128: if tx.To() == nil { 1129: signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) =>1130: from, err := types.Sender(signer, tx) 1131: if err != nil { 1132: return common.Hash{}, err 1133: } 1134: addr := crypto.CreateAddress(from, tx.Nonce()) 1135: log.Info("Submitted contract creation", "fullhash", tx.Hash().Hex(), "contract", addr.Hex()) (dlv) frame 1 l Goroutine 3593 frame 1 at /Users/jiang/go/src/github.com/ethereum/go-ethereum/internal/ethapi/api.go:1176 (PC: 0x48d9bd1) 1171: } 1172: signed, err := wallet.SignTx(account, tx, chainID) 1173: if err != nil { 1174: return common.Hash{}, err 1175: } =>1176: return submitTransaction(ctx, s.b, signed) 1177: } 1178: 1179: // SendRawTransaction will add the signed transaction to the transaction pool. 1180: // The sender is responsible for signing the transaction and using the correct nonce. 1181: func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encodedTx hexutil.Bytes) (common.Hash, error) {
先把調(diào)試結(jié)果展示出來,通過對一個交易的內(nèi)部分析,可以了解EVM執(zhí)行的大部分細(xì)節(jié),此處需要另開篇幅詳述。請關(guān)注后續(xù)專題。
源碼調(diào)試1、重新強(qiáng)制編譯geth,去掉編譯內(nèi)聯(lián)優(yōu)化,方便跟蹤調(diào)試。
cd path/go-ethereum sudo go install -a -gcflags "-N -l" -v ./cmd/geth
編譯后的geth執(zhí)行文件就在$gopath/bin下。
2、在datadir下啟動這個重新編譯后的geth
geth --datadir "./" --rpc --rpccorsdomain="*" --networkid 13 console 2>>00.glog
3、調(diào)試這個進(jìn)程
dlv attach
4、給交易api入口函數(shù)設(shè)置斷點
b ethapi.(*PublicTransactionPoolAPI).SendTransaction
下面是一個區(qū)塊鏈小程序,供大家參考。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/24036.html
摘要:使用和以太坊客戶端的容器鏡像,可以快速啟動解決方案,實現(xiàn)區(qū)塊鏈技術(shù)的本地開發(fā)。以太坊,主要是針對工程師使用進(jìn)行區(qū)塊鏈以太坊開發(fā)的詳解。以太坊,主要講解如何使用開發(fā)基于的以太坊應(yīng)用,包括賬戶管理狀態(tài)與交易智能合約開發(fā)與交互過濾器和事件等。 區(qū)塊鏈最近IT世界的流行語之一。這項有關(guān)數(shù)字加密貨幣的技術(shù),并與比特幣一起構(gòu)成了這個熱門的流行趨勢。它是去中心化的,不可變的分塊數(shù)據(jù)結(jié)構(gòu),這是可以安全...
摘要:使用和以太坊客戶端的容器鏡像,可以快速啟動解決方案,實現(xiàn)區(qū)塊鏈技術(shù)的本地開發(fā)。以太坊,主要是針對工程師使用進(jìn)行區(qū)塊鏈以太坊開發(fā)的詳解。以太坊,主要講解如何使用開發(fā)基于的以太坊應(yīng)用,包括賬戶管理狀態(tài)與交易智能合約開發(fā)與交互過濾器和事件等。 區(qū)塊鏈最近IT世界的流行語之一。這項有關(guān)數(shù)字加密貨幣的技術(shù),并與比特幣一起構(gòu)成了這個熱門的流行趨勢。它是去中心化的,不可變的分塊數(shù)據(jù)結(jié)構(gòu),這是可以安全...
本文首發(fā)于深入淺出區(qū)塊鏈社區(qū)原文鏈接:[使用 ethers.js 開發(fā)以太坊 Web 錢包 3 - 展示錢包信息及發(fā)起簽名交易)](https://learnblockchain.cn/20...,請讀者前往原文閱讀 以太坊去中心化網(wǎng)頁錢包開發(fā)系列,將從零開始開發(fā)出一個可以實際使用的錢包,本系列文章是理論與實戰(zhàn)相結(jié)合,一共有四篇:創(chuàng)建錢包賬號、賬號Keystore文件導(dǎo)入導(dǎo)出、展示錢包信息及發(fā)起簽...
本文首發(fā)于深入淺出區(qū)塊鏈社區(qū)原文鏈接:[使用 ethers.js 開發(fā)以太坊 Web 錢包 3 - 展示錢包信息及發(fā)起簽名交易)](https://learnblockchain.cn/20...,請讀者前往原文閱讀 以太坊去中心化網(wǎng)頁錢包開發(fā)系列,將從零開始開發(fā)出一個可以實際使用的錢包,本系列文章是理論與實戰(zhàn)相結(jié)合,一共有四篇:創(chuàng)建錢包賬號、賬號Keystore文件導(dǎo)入導(dǎo)出、展示錢包信息及發(fā)起簽...
閱讀 2613·2021-11-15 11:38
閱讀 2631·2021-11-04 16:13
閱讀 18072·2021-09-22 15:07
閱讀 1028·2019-08-30 15:55
閱讀 3273·2019-08-30 14:15
閱讀 1674·2019-08-29 13:59
閱讀 3230·2019-08-28 18:28
閱讀 1585·2019-08-23 18:29