摘要:是,是比特幣采用的一個(gè)概念在比原鏈中對它進(jìn)行了擴(kuò)展,支持多種資產(chǎn)。在比特幣中沒有我們通常熟悉的銀行帳戶那樣有專門的地方記錄余額,而是通過計(jì)算屬于自己的所有未花費(fèi)掉的輸出來算出余額。
作者:freewind
比原項(xiàng)目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
在前幾篇里,我們研究了比原是如何通過web api接口來創(chuàng)建密鑰、帳戶和地址的,今天我們繼續(xù)看一下,比原是如何顯示帳戶余額的。
在Dashboard中,左側(cè)有一欄名為"Balances"(余額),點(diǎn)擊后,我們可以看到每個(gè)帳戶當(dāng)前有多少余額,如下圖:
這又是怎么實(shí)現(xiàn)的呢?我們還是和以前一樣,把它分成兩個(gè)部分:
前端是如何向后端發(fā)送請求的
后端接收到請求數(shù)據(jù)后,是如何去查詢出帳戶余額的
前端是如何向后端發(fā)送請求的對應(yīng)這個(gè)功能的前端代碼遠(yuǎn)比想像中復(fù)雜,我花了很多功夫才把邏輯理清楚,主要原因是它是一種通用的展示方式:以表格的形式來展示一個(gè)數(shù)組中多個(gè)元素的內(nèi)容。不過在上圖所展示的例子中,這個(gè)數(shù)組只有一個(gè)元素而已。
首先需要提醒的是,這里涉及到Redux和Redux-router的很多知識,如果不熟悉的話,最好能先去找點(diǎn)文檔和例子看看,把里面的一些基本概念弄清楚。比如,
在Redux中,通常會有一個(gè)叫store的數(shù)據(jù)結(jié)構(gòu),像一個(gè)巨大的JSON對象,持有整個(gè)應(yīng)用所有需要的數(shù)據(jù);
我們需要寫很多reducer,它們就是store的轉(zhuǎn)換器,根據(jù)當(dāng)前傳入的store返回一個(gè)新的內(nèi)容不同的store,store在不同時(shí)刻的內(nèi)容可以看作不同的state
action是用來向reducer傳遞數(shù)據(jù)的,reducer將根據(jù)action的類型和參數(shù)來做不同的轉(zhuǎn)換
dispatch是Redux提供的,我們一般不能直接調(diào)用reducer,而是調(diào)用dispatch,把a(bǔ)ction傳給它,它會幫我們拿到當(dāng)前的store,并且把它(或者一部分)和action一起傳給reducer去做轉(zhuǎn)換
redux-router會提供一個(gè)reduxConnect函數(shù),幫我們把store跟react的組件連接起來,使得我們在React組件中,可以方便的去dispatch
另外,在Chrome中,有兩個(gè)插件可以方便我們?nèi)フ{(diào)試React+Redux:
React DevTools: https://chrome.google.com/web...
Redux DevTools: https://chrome.google.com/web...
下面將結(jié)合前端源代碼來分析一下過程,因?yàn)樵谶壿嬌峡梢钥醋鞔嬖趲讞l線,所以我們將分開追蹤。
reducers首先我們發(fā)現(xiàn)在啟動(dòng)的地方,初始化了store:
src/app.js#L17-L18
// Start app export const store = configureStore()
并且在里面創(chuàng)建store的時(shí)候,還創(chuàng)建了reducer:
src/configureStore.js#L13-L37
export default function() { const store = createStore( makeRootReducer(), ... return store }
進(jìn)入makeRootReducer:
src/reducers.js#L18-L62
// ... import { reducers as balance } from "features/balances" // ... const makeRootReducer = () => (state, action) => { // ... return combineReducers({ // ... balance, // ... })(state, action) }
這個(gè)函數(shù)的最后實(shí)際上會把多個(gè)組件需要的reducer合并在一起,但是我把其它的都省略了,只留下了今天要研究的balance。
而這個(gè)balance是來自于"features/balances"暴露出來的reducers:
src/features/balances/index.js#L5-L9
import reducers from "./reducers" export { actions, reducers, routes }
可以看到除了reducers,它還暴露了別的,那些我們一會兒再研究。先看reducers,它對應(yīng)于reducers.js:
src/features/balances/reducers.js#L30-L33
export default combineReducers({ items: itemsReducer, queries: queriesReducer })
可以看到,它是把兩種作用的reducer合并起來了,一個(gè)是跟操作元素相關(guān)的,另一個(gè)是用來記錄查詢狀態(tài)的(是否查詢過)。
我們先看元素相關(guān)的itemsReducer:
src/features/balances/reducers.js#L3-L17
const itemsReducer = (state = {}, action) => { if (action.type == "APPEND_BALANCE_PAGE") { const newState = {} action.param.data.forEach((item, index) => { const id = `balance-${index}` newState[id] = { id: `balance-${index}`, ...item } }) return newState } return state }
可以看到,當(dāng)傳過來的參數(shù)action的type是APPEND_BALANCE_PAGE時(shí),就會把action.param.data中包含的元素放到一個(gè)新創(chuàng)建的state中,并且以索引順序給它們起了id,且在id前面加了balance-方便追蹤。比如我們在Chrome的Redux DevTools插件中就可以看到:
經(jīng)過這個(gè)reducer處理后產(chǎn)生的新store中就包含了與balance相關(guān)的數(shù)據(jù),它們可以用于在別處拿出來顯示在React組件中。這點(diǎn)我們在后面會看到。
再看另一個(gè)與查詢相關(guān)的queriesReducer:
src/features/balances/reducers.js#L19-L27
const queriesReducer = (state = {}, action) => { if (action.type == "APPEND_BALANCE_PAGE") { return { loadedOnce: true } } return state }
這個(gè)比較簡單,它關(guān)心的action.type跟前面一樣,也是APPEND_BALANCE_PAGE。返回的loadedOnce的作用是告訴前端有沒有向后臺查詢過,這樣可以用于控制比如提示信息的顯示等。
與balance相關(guān)的reducer就只有這些了,看起來還是比較簡單的。
actions在前面,我們看到在balance中除了reducer,還定義了actions:
src/features/balances/index.js#L5-L9
import actions from "./actions" // ... export { actions, reducers, routes }
其中的actions對應(yīng)的是actions.js:
src/features/balances/actions.js#L1-L2
import { baseListActions } from "features/shared/actions" export default baseListActions("balance")
可以看到,它實(shí)際上是利用了一個(gè)項(xiàng)目內(nèi)共享的action來產(chǎn)生自己的action,讓我們找到baseListActions:
src/features/shared/actions/index.js#L1-L9
// ... import baseListActions from "./list" export { // ... baseListActions, }
繼續(xù),先讓我們省略掉一些代碼,看看骨架:
src/features/shared/actions/list.js#L4-L147
// 1. export default function(type, options = {}) { // 2. const listPath = options.listPath || `/${type}s` // 3. const clientApi = () => options.clientApi ? options.clientApi() : chainClient()[`${type}s`] // 4. const fetchItems = (params) => { // ... } const fetchPage = (query, pageNumber = 1, options = {}) => { // ... } const fetchAll = () => { // ... } const _load = function(query = {}, list = {}, requestOptions) { // ... } const deleteItem = (id, confirmMessage, deleteMessage) => { // ... } const pushList = (query = {}, pageNumber, options = {}) => { // ... } // 5. return { fetchItems, fetchPage, fetchAll, deleteItem, pushList, didLoadAutocomplete: { type: `DID_LOAD_${type.toUpperCase()}_AUTOCOMPLETE` }, } }
這個(gè)函數(shù)比較大,它是一個(gè)通用的用來分頁分元素來展示數(shù)據(jù)的。為了方便理解,我們先把一些細(xì)節(jié)代碼注釋掉了,只留下了骨架,并且標(biāo)注了6塊內(nèi)容:
第1處需要關(guān)注的是,這是一個(gè)函數(shù),可以被外界調(diào)用,所以前面才可以baseListActions("balance"),傳進(jìn)來的第一個(gè)參數(shù)是用來表示這是什么類型的數(shù)據(jù),其它地方可以根據(jù)這個(gè)類型發(fā)送不同的請求或進(jìn)行不同的操作
第2處是定義前臺列出數(shù)據(jù)(就是常用的list頁面)的router路徑,默認(rèn)就type的復(fù)數(shù),比如balance就是/balances,它會被redux-router處理,并且轉(zhuǎn)到相應(yīng)的組件
第3處是找到相應(yīng)的用于向后臺傳送數(shù)據(jù)的對象,名為clientApi,封裝了后臺提供的web api接口
第4處是與顯示數(shù)據(jù)相關(guān)的通用函數(shù)定義,比如取數(shù)據(jù),按頁取,刪除等
第5處是把前面定義的各種操作函數(shù)組合成一個(gè)對象,返回給調(diào)用者
其實(shí)我覺得這些函數(shù)的細(xì)節(jié)在這里都不用怎么展示,因?yàn)樵诖a分析的時(shí)候,難度不在一個(gè)具體的函數(shù)是怎么實(shí)現(xiàn)的,而是在于骨架和流程是怎么樣的。這里列出了多個(gè)函數(shù)的名字,我還不清楚哪些會用到,所以先不講解,等后面遇到了再把代碼貼出來講解。
routes再看前面剩下的routes是怎么實(shí)現(xiàn)的:
src/features/balances/index.js#L5-L9
// ... import routes from "./routes" export { actions, reducers, routes }
這個(gè)routes對應(yīng)的是routes.js文件:
src/features/balances/routes.js#L1-L4
import { List } from "./components" import { makeRoutes } from "features/shared" export default (store) => makeRoutes(store, "balance", List)
跟前面的action類似,它也是通過調(diào)用一個(gè)通用的函數(shù)再傳入一些具體的參數(shù)過去實(shí)現(xiàn)的,那么在那邊的makeRoutes肯定做了大量的工作。讓我們進(jìn)入features/shared/index.js:
src/features/shared/index.js#L1-L9
// ... import makeRoutes from "./routes" // ... export { actions, reducers, makeRoutes }
只聚焦于makeRoutes:
src/features/shared/routes.js#L5-L44
const makeRoutes = (store, type, List, New, Show, options = {}) => { // 1. const loadPage = () => { store.dispatch(actions[type].fetchAll()) } // 2. const childRoutes = [] if (New) { childRoutes.push({ path: "create", component: New }) } if (options.childRoutes) { childRoutes.push(...options.childRoutes) } if (Show) { childRoutes.push({ path: ":id", component: Show }) } // 3. return { path: options.path || type + "s", component: RoutingContainer, name: options.name || humanize(type + "s"), name_zh: options.name_zh, indexRoute: { component: List, onEnter: (nextState, replace) => { loadPage(nextState, replace) }, onChange: (_, nextState, replace) => { loadPage(nextState, replace) } }, childRoutes: childRoutes } }
分成了4塊:
第1處定義了loadPage的操作,它實(shí)際上要是調(diào)用該type對應(yīng)的action的fetchAll方法(還記得前面action骨架中定義了fetchAll函數(shù)嗎)
第2處根據(jù)傳入的參數(shù)來確定這個(gè)router里到底有哪些routes,比如是否需要“新建”,“顯示”等等
第3處就是返回值,返回了一個(gè)對象,它是可以被redux-router理解的。可以看到它里面有path, 對應(yīng)的組件component,甚至首頁中某些特別時(shí)刻如進(jìn)入或者改變時(shí),要進(jìn)行什么操作。
由于這里調(diào)用了fetchAll,那我們便把前面action里的fetchAll貼出來:
src/features/shared/actions/list.js#L58-L60
const fetchAll = () => { return fetchPage("", -1) }
又調(diào)用到了fetchPage:
src/features/shared/actions/list.js#L39-L55
const fetchPage = (query, pageNumber = 1, options = {}) => { const listId = query.filter || "" pageNumber = parseInt(pageNumber || 1) return (dispatch, getState) => { const getFilterStore = () => getState()[type].queries[listId] || {} const fetchNextPage = () => dispatch(_load(query, getFilterStore(), options)).then((resp) => { if (!resp || resp.type == "ERROR") return return Promise.resolve(resp) }) return dispatch(fetchNextPage) } }
在中間又調(diào)用了_load:
src/features/shared/actions/list.js#L62-L101
const _load = function(query = {}, list = {}, requestOptions) { return function(dispatch) { // ... // 1. if (!refresh && latestResponse) { let responsePage promise = latestResponse.nextPage() .then(resp => { responsePage = resp return dispatch(receive(responsePage)) }) // ... } else { // 2. const params = {} if (query.filter) params.filter = filter if (query.sumBy) params.sumBy = query.sumBy.split(",") promise = dispatch(fetchItems(params)) } // 3. return promise.then((response) => { return dispatch({ type: `APPEND_${type.toUpperCase()}_PAGE`, param: response, refresh: refresh, }) }) // ... } }
這個(gè)函數(shù)還比較復(fù)雜,我進(jìn)行了適當(dāng)簡化,并且分成了3塊:
第1處的if分支處理的是第2頁的情況。拿到數(shù)據(jù)后,會通過receive這個(gè)函數(shù)定義了一個(gè)action傳給dispatch進(jìn)行操作。這個(gè)receive在前面被我省略了,其實(shí)就是定義了一個(gè)type為RECEIVED_${type.toUpperCase()}_ITEMS的action,也就是說,拿到數(shù)據(jù)后,還需要有另一個(gè)地方對它進(jìn)行處理。我們晚點(diǎn)再來討論它。
第2處的else處理的是查詢情況,拿到其中的過濾條件等,傳給fetchItems函數(shù)
第3處的promise就是前面兩處中的一個(gè),也就是拿到數(shù)據(jù)后再進(jìn)行APPEND_${type.toUpperCase()}_PAGE的操作
我們從這里并沒有看到它到底會向比原后臺的哪個(gè)接口發(fā)送請求,它可能被隱藏在了某個(gè)函數(shù)中,比如nextPage或者fetchItems等。我們先看看nextPage:
src/sdk/page.js#L17-L24
nextPage(cb) { let queryOwner = this.client this.memberPath.split(".").forEach((member) => { queryOwner = queryOwner[member] }) return queryOwner.query(this.next, cb) }
可以看到它最后調(diào)用的是client的query方法。其中的client對應(yīng)的是balanceAPI:
src/sdk/api/balances.js#L3-L9
const balancesAPI = (client) => { return { query: (params, cb) => shared.query(client, "balances", "/list-balances", params, {cb}), queryAll: (params, processor, cb) => shared.queryAll(client, "balances", params, processor, cb), } }
可以看到,query最后將調(diào)用后臺的/list-balances接口。
而fetchItems最終也調(diào)用的是同樣的方法:
src/features/shared/actions/list.js#L15-L35
const fetchItems = (params) => { // ... return (dispatch) => { const promise = clientApi().query(params) promise.then( // ... ) return promise } }
所以我們一會兒在分析后臺的時(shí)候,只需要關(guān)注/list-balances就可以了。
這里還剩下一點(diǎn),就是從后臺拿到數(shù)據(jù)后,前端怎么處理,也就是前面第1塊和第3塊中拿到數(shù)據(jù)后的操作。
我們先看一下第1處中的RECEIVED_${type.toUpperCase()}_ITEMS的action是如何被處理的。通過搜索,發(fā)現(xiàn)了:
src/features/shared/reducers.js#L6-L28
export const itemsReducer = (type, idFunc = defaultIdFunc) => (state = {}, action) => { if (action.type == `RECEIVED_${type.toUpperCase()}_ITEMS`) { const newObjects = {} const data = type.toUpperCase() !== "TRANSACTION" ? action.param.data : action.param.data.map(data => ({ ...data, id: data.txId, timestamp: data.blockTime, blockId: data.blockHash, position: data.blockIndex })); (data || []).forEach(item => { if (!item.id) { item.id = idFunc(item) } newObjects[idFunc(item)] = item }) return newObjects } else // ... return state }
可以看到,當(dāng)拿到數(shù)據(jù)后,如果是“轉(zhuǎn)帳”則進(jìn)行一些特殊的操作,否則就直接用。后面的操作,也主要是給每個(gè)元素增加了一個(gè)id,然后放到store里。
那么第3步中的APPEND_${type.toUpperCase()}_PAGE呢?我們找到一些通用的處理代碼:
src/features/shared/reducers.js#L34-L54
export const queryCursorReducer = (type) => (state = {}, action) => { if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) { return action.param } return state } export const queryTimeReducer = (type) => (state = "", action) => { if (action.type == `APPEND_${type.toUpperCase()}_PAGE`) { return moment().format("h:mm:ss a") } return state } export const autocompleteIsLoadedReducer = (type) => (state = false, action) => { if (action.type == `DID_LOAD_${type.toUpperCase()}_AUTOCOMPLETE`) { return true } return state }
這里沒有什么復(fù)雜的操作,主要是把前面送過來的參數(shù)當(dāng)作store新的state傳出去,或者在queryTimeReducer是傳出當(dāng)前時(shí)間,可以把它們理解為一些占位符(默認(rèn)值)。如果針對某一個(gè)具體類型,還可以定義具體的操作。比如我們這里是balance,所以它還會被前面最開始講解的這個(gè)函數(shù)處理:
src/features/balances/reducers.js#L3-L17
const itemsReducer = (state = {}, action) => { if (action.type == "APPEND_BALANCE_PAGE") { const newState = {} action.param.data.forEach((item, index) => { const id = `balance-${index}` newState[id] = { id: `balance-${index}`, ...item } }) return newState } return state }
這個(gè)前面已經(jīng)講了,這里列出來僅供回憶。
那么到這里,我們基本上就已經(jīng)把比原前端中,如何通過分頁列表形式展示數(shù)據(jù)的流程弄清楚了。至于拿到數(shù)據(jù)后,最終如何在頁面上以table的形式展示出來,可以參看https://github.com/freewind/b...,我覺得這里已經(jīng)不需要再講解了。
那么我們準(zhǔn)備進(jìn)入后端。
后端是如何通過/list-balances接口查詢出帳戶余額的跟之前一樣,我們可以很快的找到定義web api接口的地方:
api/api.go#L164-L244
func (a *API) buildHandler() { // ... if a.wallet != nil { // ... m.Handle("/list-balances", jsonHandler(a.listBalances)) // ... // ... }
可以看到,/list-balances對應(yīng)的handler是a.listBalances(外面的jsonHandler是用于處理http方面的東西,以及在Go對象與JSON之間做轉(zhuǎn)換的)
api/query.go#L60-L67
// POST /list-balances func (a *API) listBalances(ctx context.Context) Response { balances, err := a.wallet.GetAccountBalances("") if err != nil { return NewErrorResponse(err) } return NewSuccessResponse(balances) }
這個(gè)方法看起來很簡單,因?yàn)樗恍枰岸藗魅肴魏螀?shù),然后再調(diào)用wallet.GetAccountBalances并傳入空字符串(表示全部帳戶)拿到結(jié)果,并且返回給前端即可:
wallet/indexer.go#L544-L547
// GetAccountBalances return all account balances func (w *Wallet) GetAccountBalances(id string) ([]AccountBalance, error) { return w.indexBalances(w.GetAccountUTXOs("")) }
這里分成了兩步,首先是調(diào)用w.GetAccountUTXOs得到帳戶對應(yīng)的UTXO,然后再根據(jù)它計(jì)算出來余額balances。
UTXO是Unspent Transaction Output,是比特幣采用的一個(gè)概念(在比原鏈中對它進(jìn)行了擴(kuò)展,支持多種資產(chǎn))。其中Transaction可看作是一種數(shù)據(jù)結(jié)構(gòu),記錄了一個(gè)交易的過程,包括若干個(gè)資金輸入和輸出。在比特幣中沒有我們通常熟悉的銀行帳戶那樣有專門的地方記錄余額,而是通過計(jì)算屬于自己的所有未花費(fèi)掉的輸出來算出余額。關(guān)于UTXO網(wǎng)上有很多文章講解,可以自行搜索。
我們繼續(xù)看w.GetAccountUTXOs:
wallet/indexer.go#L525-L542
// GetAccountUTXOs return all account unspent outputs func (w *Wallet) GetAccountUTXOs(id string) []account.UTXO { var accountUTXOs []account.UTXO accountUTXOIter := w.DB.IteratorPrefix([]byte(account.UTXOPreFix + id)) defer accountUTXOIter.Release() for accountUTXOIter.Next() { accountUTXO := account.UTXO{} if err := json.Unmarshal(accountUTXOIter.Value(), &accountUTXO); err != nil { hashKey := accountUTXOIter.Key()[len(account.UTXOPreFix):] log.WithField("UTXO hash", string(hashKey)).Warn("get account UTXO") } else { accountUTXOs = append(accountUTXOs, accountUTXO) } } return accountUTXOs }
這個(gè)方法看起來不是很復(fù)雜,它主要是從數(shù)據(jù)庫中搜索UTXO,然后返回給調(diào)用者繼續(xù)處理。這里的w.DB是指名為wallet的leveldb,我們這段時(shí)間一直在用它。初始化的過程今天就不看了,之前做過多次,大家有需要的話應(yīng)該能自己找到。
然后就是以UTXOPreFix(常量ACU:,表示StandardUTXOKey prefix)作為前綴對數(shù)據(jù)庫進(jìn)行遍歷,把取得的JSON格式的數(shù)據(jù)轉(zhuǎn)換為account.UTXO對象,最后把它們放到數(shù)組里返回給調(diào)用者。
我們再看前面GetAccountBalances方法中的w.indexBalances:
wallet/indexer.go#L559-L609
func (w *Wallet) indexBalances(accountUTXOs []account.UTXO) ([]AccountBalance, error) { // 1. accBalance := make(map[string]map[string]uint64) balances := make([]AccountBalance, 0) // 2. for _, accountUTXO := range accountUTXOs { assetID := accountUTXO.AssetID.String() if _, ok := accBalance[accountUTXO.AccountID]; ok { if _, ok := accBalance[accountUTXO.AccountID][assetID]; ok { accBalance[accountUTXO.AccountID][assetID] += accountUTXO.Amount } else { accBalance[accountUTXO.AccountID][assetID] = accountUTXO.Amount } } else { accBalance[accountUTXO.AccountID] = map[string]uint64{assetID: accountUTXO.Amount} } } // 3. var sortedAccount []string for k := range accBalance { sortedAccount = append(sortedAccount, k) } sort.Strings(sortedAccount) for _, id := range sortedAccount { // 4. var sortedAsset []string for k := range accBalance[id] { sortedAsset = append(sortedAsset, k) } sort.Strings(sortedAsset) // 5. for _, assetID := range sortedAsset { alias := w.AccountMgr.GetAliasByID(id) targetAsset, err := w.AssetReg.GetAsset(assetID) if err != nil { return nil, err } assetAlias := *targetAsset.Alias balances = append(balances, AccountBalance{ Alias: alias, AccountID: id, AssetID: assetID, AssetAlias: assetAlias, Amount: accBalance[id][assetID], AssetDefinition: targetAsset.DefinitionMap, }) } } return balances, nil }
這個(gè)方法看起來很長,但是實(shí)際上做的事情沒那么多,只不過是因?yàn)镚o低效的語法讓它看起來非常龐大。我把它分成了5塊:
第1塊分別定義了后面要用到的一些數(shù)據(jù)結(jié)構(gòu),其中accBalance是一個(gè)兩級的map(AccountID -> AssetID -> AssetAmount),通過對參數(shù)accountUTXOs進(jìn)行遍歷,把相同account和相同asset的數(shù)量累加在一起。balances是用來保存結(jié)果的,是一個(gè)AccountBalance的切片
第2塊就是累加assetAmount,放到accBalance中
對accountId進(jìn)行排序,
對assetId也進(jìn)行排序,這兩處的排序是想讓最后的返回結(jié)果穩(wěn)定(有利于查看及分頁)
經(jīng)過雙層遍歷,拿到了每一個(gè)account的每一種asset的assetAmount,然后再通過w.AccountMgr.GetAliasByID拿到缺少的alias信息,最后生成一個(gè)切片返回。其中GetAliasByID就是從wallet數(shù)據(jù)庫中查詢,比較簡單,就不貼代碼了。
看完這一段代碼之后,我的心情是比較郁悶的,因?yàn)檫@里的代碼看著多,但實(shí)際上都是一些比較低層的邏輯(構(gòu)建、排序、遍歷),在其它的語言中(尤其是支持函數(shù)式的),可能只需要十來行代碼就能搞定,但是這么要寫這么多。而且,我還發(fā)現(xiàn),GO語言通過它獨(dú)特的語法、錯(cuò)誤處理和類型系統(tǒng),讓一些看起來應(yīng)該很簡單的事情(比如抽出來一些可復(fù)用的處理數(shù)據(jù)結(jié)構(gòu)的函數(shù))都變得很麻煩,我試著重構(gòu),居然發(fā)現(xiàn)無從下手。
今天的問題就算是解決了,下次再見。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/24179.html
摘要:前端是如何獲取交易數(shù)據(jù)并顯示出來的我們先在比原的前端代碼庫中尋找。這過程中的推導(dǎo)就不再詳說,需要的話可以看前面講解比原是如何顯示余額的那篇文章。的定義是其中的值是。 作者:freewind 比原項(xiàng)目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇文章中,我們...
摘要:而本文將繼續(xù)討論,比原是如何通過接口來創(chuàng)建帳戶的。把各信息打包在一起,稱之為另外,在第處還是一個(gè)需要注意的。比原在代碼中使用它保存各種數(shù)據(jù),比如區(qū)塊帳戶等。到這里,我們已經(jīng)差不多清楚了比原的是如何根據(jù)用戶提交的參數(shù)來創(chuàng)建帳戶的。 作者:freewind 比原項(xiàng)目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://git...
摘要:所以本文本來是想去研究一下,當(dāng)別的節(jié)點(diǎn)把區(qū)塊數(shù)據(jù)發(fā)給我們之后,我們應(yīng)該怎么處理,現(xiàn)在換成研究比原的是怎么做出來的。進(jìn)去后會看到大量的與相關(guān)的配置。它的功能主要是為了在訪問與的函數(shù)之間增加了一層轉(zhuǎn)換。 作者:freewind 比原項(xiàng)目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlo...
摘要:繼續(xù)看生成地址的方法由于這個(gè)方法里傳過來的是而不是對象,所以還需要再用查一遍,然后,再調(diào)用這個(gè)私有方法創(chuàng)建地址該方法可以分成部分在第塊中主要關(guān)注的是返回值。 作者:freewind 比原項(xiàng)目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在比原的dashboard中...
摘要:如果傳的是,就會在內(nèi)部使用默認(rèn)的隨機(jī)數(shù)生成器生成隨機(jī)數(shù)并生成密鑰。使用的是,生成的是一個(gè)形如這樣的全球唯一的隨機(jī)數(shù)把密鑰以文件形式保存在硬盤上。 作者:freewind 比原項(xiàng)目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我們探討了從瀏覽器的dashb...
閱讀 808·2021-11-24 09:38
閱讀 1011·2021-11-11 11:01
閱讀 3255·2021-10-19 13:22
閱讀 1542·2021-09-22 15:23
閱讀 2844·2021-09-08 09:35
閱讀 2780·2019-08-29 11:31
閱讀 2134·2019-08-26 11:47
閱讀 1578·2019-08-26 11:44