摘要:是,是比特幣采用的一個概念在比原鏈中對它進行了擴展,支持多種資產。在比特幣中沒有我們通常熟悉的銀行帳戶那樣有專門的地方記錄余額,而是通過計算屬于自己的所有未花費掉的輸出來算出余額。
作者:freewind
比原項目倉庫:
Github地址:https://github.com/Bytom/bytom
Gitee地址:https://gitee.com/BytomBlockc...
在前幾篇里,我們研究了比原是如何通過web api接口來創建密鑰、帳戶和地址的,今天我們繼續看一下,比原是如何顯示帳戶余額的。
在Dashboard中,左側有一欄名為"Balances"(余額),點擊后,我們可以看到每個帳戶當前有多少余額,如下圖:
這又是怎么實現的呢?我們還是和以前一樣,把它分成兩個部分:
前端是如何向后端發送請求的
后端接收到請求數據后,是如何去查詢出帳戶余額的
前端是如何向后端發送請求的對應這個功能的前端代碼遠比想像中復雜,我花了很多功夫才把邏輯理清楚,主要原因是它是一種通用的展示方式:以表格的形式來展示一個數組中多個元素的內容。不過在上圖所展示的例子中,這個數組只有一個元素而已。
首先需要提醒的是,這里涉及到Redux和Redux-router的很多知識,如果不熟悉的話,最好能先去找點文檔和例子看看,把里面的一些基本概念弄清楚。比如,
在Redux中,通常會有一個叫store的數據結構,像一個巨大的JSON對象,持有整個應用所有需要的數據;
我們需要寫很多reducer,它們就是store的轉換器,根據當前傳入的store返回一個新的內容不同的store,store在不同時刻的內容可以看作不同的state
action是用來向reducer傳遞數據的,reducer將根據action的類型和參數來做不同的轉換
dispatch是Redux提供的,我們一般不能直接調用reducer,而是調用dispatch,把action傳給它,它會幫我們拿到當前的store,并且把它(或者一部分)和action一起傳給reducer去做轉換
redux-router會提供一個reduxConnect函數,幫我們把store跟react的組件連接起來,使得我們在React組件中,可以方便的去dispatch
另外,在Chrome中,有兩個插件可以方便我們去調試React+Redux:
React DevTools: https://chrome.google.com/web...
Redux DevTools: https://chrome.google.com/web...
下面將結合前端源代碼來分析一下過程,因為在邏輯上可以看作存在幾條線,所以我們將分開追蹤。
reducers首先我們發現在啟動的地方,初始化了store:
src/app.js#L17-L18
// Start app export const store = configureStore()
并且在里面創建store的時候,還創建了reducer:
src/configureStore.js#L13-L37
export default function() { const store = createStore( makeRootReducer(), ... return store }
進入makeRootReducer:
src/reducers.js#L18-L62
// ... import { reducers as balance } from "features/balances" // ... const makeRootReducer = () => (state, action) => { // ... return combineReducers({ // ... balance, // ... })(state, action) }
這個函數的最后實際上會把多個組件需要的reducer合并在一起,但是我把其它的都省略了,只留下了今天要研究的balance。
而這個balance是來自于"features/balances"暴露出來的reducers:
src/features/balances/index.js#L5-L9
import reducers from "./reducers" export { actions, reducers, routes }
可以看到除了reducers,它還暴露了別的,那些我們一會兒再研究。先看reducers,它對應于reducers.js:
src/features/balances/reducers.js#L30-L33
export default combineReducers({ items: itemsReducer, queries: queriesReducer })
可以看到,它是把兩種作用的reducer合并起來了,一個是跟操作元素相關的,另一個是用來記錄查詢狀態的(是否查詢過)。
我們先看元素相關的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 }
可以看到,當傳過來的參數action的type是APPEND_BALANCE_PAGE時,就會把action.param.data中包含的元素放到一個新創建的state中,并且以索引順序給它們起了id,且在id前面加了balance-方便追蹤。比如我們在Chrome的Redux DevTools插件中就可以看到:
經過這個reducer處理后產生的新store中就包含了與balance相關的數據,它們可以用于在別處拿出來顯示在React組件中。這點我們在后面會看到。
再看另一個與查詢相關的queriesReducer:
src/features/balances/reducers.js#L19-L27
const queriesReducer = (state = {}, action) => { if (action.type == "APPEND_BALANCE_PAGE") { return { loadedOnce: true } } return state }
這個比較簡單,它關心的action.type跟前面一樣,也是APPEND_BALANCE_PAGE。返回的loadedOnce的作用是告訴前端有沒有向后臺查詢過,這樣可以用于控制比如提示信息的顯示等。
與balance相關的reducer就只有這些了,看起來還是比較簡單的。
actions在前面,我們看到在balance中除了reducer,還定義了actions:
src/features/balances/index.js#L5-L9
import actions from "./actions" // ... export { actions, reducers, routes }
其中的actions對應的是actions.js:
src/features/balances/actions.js#L1-L2
import { baseListActions } from "features/shared/actions" export default baseListActions("balance")
可以看到,它實際上是利用了一個項目內共享的action來產生自己的action,讓我們找到baseListActions:
src/features/shared/actions/index.js#L1-L9
// ... import baseListActions from "./list" export { // ... baseListActions, }
繼續,先讓我們省略掉一些代碼,看看骨架:
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` }, } }
這個函數比較大,它是一個通用的用來分頁分元素來展示數據的。為了方便理解,我們先把一些細節代碼注釋掉了,只留下了骨架,并且標注了6塊內容:
第1處需要關注的是,這是一個函數,可以被外界調用,所以前面才可以baseListActions("balance"),傳進來的第一個參數是用來表示這是什么類型的數據,其它地方可以根據這個類型發送不同的請求或進行不同的操作
第2處是定義前臺列出數據(就是常用的list頁面)的router路徑,默認就type的復數,比如balance就是/balances,它會被redux-router處理,并且轉到相應的組件
第3處是找到相應的用于向后臺傳送數據的對象,名為clientApi,封裝了后臺提供的web api接口
第4處是與顯示數據相關的通用函數定義,比如取數據,按頁取,刪除等
第5處是把前面定義的各種操作函數組合成一個對象,返回給調用者
其實我覺得這些函數的細節在這里都不用怎么展示,因為在代碼分析的時候,難度不在一個具體的函數是怎么實現的,而是在于骨架和流程是怎么樣的。這里列出了多個函數的名字,我還不清楚哪些會用到,所以先不講解,等后面遇到了再把代碼貼出來講解。
routes再看前面剩下的routes是怎么實現的:
src/features/balances/index.js#L5-L9
// ... import routes from "./routes" export { actions, reducers, routes }
這個routes對應的是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類似,它也是通過調用一個通用的函數再傳入一些具體的參數過去實現的,那么在那邊的makeRoutes肯定做了大量的工作。讓我們進入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的操作,它實際上要是調用該type對應的action的fetchAll方法(還記得前面action骨架中定義了fetchAll函數嗎)
第2處根據傳入的參數來確定這個router里到底有哪些routes,比如是否需要“新建”,“顯示”等等
第3處就是返回值,返回了一個對象,它是可以被redux-router理解的。可以看到它里面有path, 對應的組件component,甚至首頁中某些特別時刻如進入或者改變時,要進行什么操作。
由于這里調用了fetchAll,那我們便把前面action里的fetchAll貼出來:
src/features/shared/actions/list.js#L58-L60
const fetchAll = () => { return fetchPage("", -1) }
又調用到了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) } }
在中間又調用了_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, }) }) // ... } }
這個函數還比較復雜,我進行了適當簡化,并且分成了3塊:
第1處的if分支處理的是第2頁的情況。拿到數據后,會通過receive這個函數定義了一個action傳給dispatch進行操作。這個receive在前面被我省略了,其實就是定義了一個type為RECEIVED_${type.toUpperCase()}_ITEMS的action,也就是說,拿到數據后,還需要有另一個地方對它進行處理。我們晚點再來討論它。
第2處的else處理的是查詢情況,拿到其中的過濾條件等,傳給fetchItems函數
第3處的promise就是前面兩處中的一個,也就是拿到數據后再進行APPEND_${type.toUpperCase()}_PAGE的操作
我們從這里并沒有看到它到底會向比原后臺的哪個接口發送請求,它可能被隱藏在了某個函數中,比如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) }
可以看到它最后調用的是client的query方法。其中的client對應的是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最后將調用后臺的/list-balances接口。
而fetchItems最終也調用的是同樣的方法:
src/features/shared/actions/list.js#L15-L35
const fetchItems = (params) => { // ... return (dispatch) => { const promise = clientApi().query(params) promise.then( // ... ) return promise } }
所以我們一會兒在分析后臺的時候,只需要關注/list-balances就可以了。
這里還剩下一點,就是從后臺拿到數據后,前端怎么處理,也就是前面第1塊和第3塊中拿到數據后的操作。
我們先看一下第1處中的RECEIVED_${type.toUpperCase()}_ITEMS的action是如何被處理的。通過搜索,發現了:
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 }
可以看到,當拿到數據后,如果是“轉帳”則進行一些特殊的操作,否則就直接用。后面的操作,也主要是給每個元素增加了一個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 }
這里沒有什么復雜的操作,主要是把前面送過來的參數當作store新的state傳出去,或者在queryTimeReducer是傳出當前時間,可以把它們理解為一些占位符(默認值)。如果針對某一個具體類型,還可以定義具體的操作。比如我們這里是balance,所以它還會被前面最開始講解的這個函數處理:
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 }
這個前面已經講了,這里列出來僅供回憶。
那么到這里,我們基本上就已經把比原前端中,如何通過分頁列表形式展示數據的流程弄清楚了。至于拿到數據后,最終如何在頁面上以table的形式展示出來,可以參看https://github.com/freewind/b...,我覺得這里已經不需要再講解了。
那么我們準備進入后端。
后端是如何通過/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對應的handler是a.listBalances(外面的jsonHandler是用于處理http方面的東西,以及在Go對象與JSON之間做轉換的)
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) }
這個方法看起來很簡單,因為它不需要前端傳入任何參數,然后再調用wallet.GetAccountBalances并傳入空字符串(表示全部帳戶)拿到結果,并且返回給前端即可:
wallet/indexer.go#L544-L547
// GetAccountBalances return all account balances func (w *Wallet) GetAccountBalances(id string) ([]AccountBalance, error) { return w.indexBalances(w.GetAccountUTXOs("")) }
這里分成了兩步,首先是調用w.GetAccountUTXOs得到帳戶對應的UTXO,然后再根據它計算出來余額balances。
UTXO是Unspent Transaction Output,是比特幣采用的一個概念(在比原鏈中對它進行了擴展,支持多種資產)。其中Transaction可看作是一種數據結構,記錄了一個交易的過程,包括若干個資金輸入和輸出。在比特幣中沒有我們通常熟悉的銀行帳戶那樣有專門的地方記錄余額,而是通過計算屬于自己的所有未花費掉的輸出來算出余額。關于UTXO網上有很多文章講解,可以自行搜索。
我們繼續看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 }
這個方法看起來不是很復雜,它主要是從數據庫中搜索UTXO,然后返回給調用者繼續處理。這里的w.DB是指名為wallet的leveldb,我們這段時間一直在用它。初始化的過程今天就不看了,之前做過多次,大家有需要的話應該能自己找到。
然后就是以UTXOPreFix(常量ACU:,表示StandardUTXOKey prefix)作為前綴對數據庫進行遍歷,把取得的JSON格式的數據轉換為account.UTXO對象,最后把它們放到數組里返回給調用者。
我們再看前面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 }
這個方法看起來很長,但是實際上做的事情沒那么多,只不過是因為Go低效的語法讓它看起來非常龐大。我把它分成了5塊:
第1塊分別定義了后面要用到的一些數據結構,其中accBalance是一個兩級的map(AccountID -> AssetID -> AssetAmount),通過對參數accountUTXOs進行遍歷,把相同account和相同asset的數量累加在一起。balances是用來保存結果的,是一個AccountBalance的切片
第2塊就是累加assetAmount,放到accBalance中
對accountId進行排序,
對assetId也進行排序,這兩處的排序是想讓最后的返回結果穩定(有利于查看及分頁)
經過雙層遍歷,拿到了每一個account的每一種asset的assetAmount,然后再通過w.AccountMgr.GetAliasByID拿到缺少的alias信息,最后生成一個切片返回。其中GetAliasByID就是從wallet數據庫中查詢,比較簡單,就不貼代碼了。
看完這一段代碼之后,我的心情是比較郁悶的,因為這里的代碼看著多,但實際上都是一些比較低層的邏輯(構建、排序、遍歷),在其它的語言中(尤其是支持函數式的),可能只需要十來行代碼就能搞定,但是這么要寫這么多。而且,我還發現,GO語言通過它獨特的語法、錯誤處理和類型系統,讓一些看起來應該很簡單的事情(比如抽出來一些可復用的處理數據結構的函數)都變得很麻煩,我試著重構,居然發現無從下手。
今天的問題就算是解決了,下次再見。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/24179.html
摘要:前端是如何獲取交易數據并顯示出來的我們先在比原的前端代碼庫中尋找。這過程中的推導就不再詳說,需要的話可以看前面講解比原是如何顯示余額的那篇文章。的定義是其中的值是。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇文章中,我們...
摘要:而本文將繼續討論,比原是如何通過接口來創建帳戶的。把各信息打包在一起,稱之為另外,在第處還是一個需要注意的。比原在代碼中使用它保存各種數據,比如區塊帳戶等。到這里,我們已經差不多清楚了比原的是如何根據用戶提交的參數來創建帳戶的。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://git...
摘要:所以本文本來是想去研究一下,當別的節點把區塊數據發給我們之后,我們應該怎么處理,現在換成研究比原的是怎么做出來的。進去后會看到大量的與相關的配置。它的功能主要是為了在訪問與的函數之間增加了一層轉換。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlo...
摘要:繼續看生成地址的方法由于這個方法里傳過來的是而不是對象,所以還需要再用查一遍,然后,再調用這個私有方法創建地址該方法可以分成部分在第塊中主要關注的是返回值。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在比原的dashboard中...
摘要:如果傳的是,就會在內部使用默認的隨機數生成器生成隨機數并生成密鑰。使用的是,生成的是一個形如這樣的全球唯一的隨機數把密鑰以文件形式保存在硬盤上。 作者:freewind 比原項目倉庫: Github地址:https://github.com/Bytom/bytom Gitee地址:https://gitee.com/BytomBlockc... 在前一篇,我們探討了從瀏覽器的dashb...
閱讀 796·2021-11-24 09:38
閱讀 998·2021-11-11 11:01
閱讀 3236·2021-10-19 13:22
閱讀 1524·2021-09-22 15:23
閱讀 2828·2021-09-08 09:35
閱讀 2766·2019-08-29 11:31
閱讀 2119·2019-08-26 11:47
閱讀 1563·2019-08-26 11:44