From b840ceb57a641d203b07cecfdef0c1748916e61e Mon Sep 17 00:00:00 2001 From: ToniRamirezM Date: Thu, 29 Oct 2020 10:57:49 +0100 Subject: [PATCH] Account API --- api/account.go | 82 +++++++++++++++ api/account_test.go | 166 +++++++++++++++++++++++++++++++ api/accountcreationauths_test.go | 2 - api/api.go | 2 +- api/api_test.go | 3 + api/exits.go | 2 +- api/handlers.go | 8 -- api/parsers.go | 36 ++++++- api/swagger.yml | 15 +++ api/txshistory.go | 2 +- api/txspool_test.go | 2 - db/historydb/historydb.go | 96 +++++++++++++++++- db/historydb/views.go | 49 +++++++++ db/migrations/0001.sql | 1 + 14 files changed, 447 insertions(+), 19 deletions(-) create mode 100644 api/account.go create mode 100644 api/account_test.go diff --git a/api/account.go b/api/account.go new file mode 100644 index 0000000..919ec30 --- /dev/null +++ b/api/account.go @@ -0,0 +1,82 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/hermeznetwork/hermez-node/apitypes" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" +) + +func getAccount(c *gin.Context) { + // Get Addr + idx, err := parseParamIdx(c) + if err != nil { + retBadReq(err, c) + return + } + apiAccount, err := h.GetAccountAPI(*idx) + if err != nil { + retSQLErr(err, c) + return + } + + // Get balance from stateDB + account, err := s.GetAccount(*idx) + if err != nil { + retSQLErr(err, c) + return + } + + apiAccount.Balance = apitypes.NewBigIntStr(account.Balance) + + c.JSON(http.StatusOK, apiAccount) +} + +func getAccounts(c *gin.Context) { + // Account filters + tokenIDs, addr, bjj, err := parseAccountFilters(c) + if err != nil { + retBadReq(err, c) + return + } + // Pagination + fromItem, order, limit, err := parsePagination(c) + if err != nil { + retBadReq(err, c) + return + } + + // Fetch Accounts from historyDB + apiAccounts, pagination, err := h.GetAccountsAPI(tokenIDs, addr, bjj, fromItem, limit, order) + if err != nil { + retSQLErr(err, c) + return + } + + // Get balances from stateDB + for x, apiAccount := range apiAccounts { + idx, err := stringToIdx(string(apiAccount.Idx), "Account Idx") + if err != nil { + retSQLErr(err, c) + return + } + account, err := s.GetAccount(*idx) + if err != nil { + retSQLErr(err, c) + return + } + apiAccounts[x].Balance = apitypes.NewBigIntStr(account.Balance) + } + + // Build succesfull response + type accountResponse struct { + Accounts []historydb.AccountAPI `json:"accounts"` + Pagination *db.Pagination `json:"pagination"` + } + c.JSON(http.StatusOK, &accountResponse{ + Accounts: apiAccounts, + Pagination: pagination, + }) +} diff --git a/api/account_test.go b/api/account_test.go new file mode 100644 index 0000000..90e34f6 --- /dev/null +++ b/api/account_test.go @@ -0,0 +1,166 @@ +package api + +import ( + "fmt" + "strconv" + "testing" + + "github.com/hermeznetwork/hermez-node/apitypes" + "github.com/hermeznetwork/hermez-node/common" + "github.com/hermeznetwork/hermez-node/db" + "github.com/hermeznetwork/hermez-node/db/historydb" + "github.com/mitchellh/copystructure" + "github.com/stretchr/testify/assert" +) + +type testAccount struct { + ItemID int `json:"itemId"` + Idx apitypes.HezIdx `json:"accountIndex"` + BatchNum common.BatchNum `json:"batchNum"` + PublicKey apitypes.HezBJJ `json:"bjj"` + EthAddr apitypes.HezEthAddr `json:"hezEthereumAddress"` + Nonce common.Nonce `json:"nonce"` + Balance *apitypes.BigIntStr `json:"balance"` + Token historydb.TokenWithUSD `json:"token"` +} + +type testAccountsResponse struct { + Accounts []testAccount `json:"accounts"` + Pagination *db.Pagination `json:"pagination"` +} + +func genTestAccounts(accounts []common.Account, tokens []historydb.TokenWithUSD) []testAccount { + tAccounts := []testAccount{} + for x, account := range accounts { + token := getTokenByID(account.TokenID, tokens) + tAccount := testAccount{ + ItemID: x + 1, + Idx: apitypes.HezIdx(idxToHez(account.Idx, token.Symbol)), + PublicKey: apitypes.NewHezBJJ(account.PublicKey), + EthAddr: apitypes.NewHezEthAddr(account.EthAddr), + Nonce: account.Nonce, + Balance: apitypes.NewBigIntStr(account.Balance), + Token: token, + } + tAccounts = append(tAccounts, tAccount) + } + return tAccounts +} + +func (t *testAccountsResponse) GetPagination() *db.Pagination { + if t.Accounts[0].ItemID < t.Accounts[len(t.Accounts)-1].ItemID { + t.Pagination.FirstReturnedItem = t.Accounts[0].ItemID + t.Pagination.LastReturnedItem = t.Accounts[len(t.Accounts)-1].ItemID + } else { + t.Pagination.LastReturnedItem = t.Accounts[0].ItemID + t.Pagination.FirstReturnedItem = t.Accounts[len(t.Accounts)-1].ItemID + } + return t.Pagination +} + +func (t *testAccountsResponse) Len() int { return len(t.Accounts) } + +func TestGetAccounts(t *testing.T) { + endpoint := apiURL + "accounts" + fetchedAccounts := []testAccount{} + + appendIter := func(intr interface{}) { + for i := 0; i < len(intr.(*testAccountsResponse).Accounts); i++ { + tmp, err := copystructure.Copy(intr.(*testAccountsResponse).Accounts[i]) + if err != nil { + panic(err) + } + fetchedAccounts = append(fetchedAccounts, tmp.(testAccount)) + } + } + + limit := 5 + stringIds := strconv.Itoa(int(tc.tokens[2].TokenID)) + "," + strconv.Itoa(int(tc.tokens[5].TokenID)) + "," + strconv.Itoa(int(tc.tokens[6].TokenID)) + + // Filter by BJJ + path := fmt.Sprintf("%s?BJJ=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].PublicKey, limit) + err := doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Greater(t, len(fetchedAccounts), 0) + assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) + fetchedAccounts = []testAccount{} + // Filter by ethAddr + path = fmt.Sprintf("%s?hermezEthereumAddress=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].EthAddr, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Greater(t, len(fetchedAccounts), 0) + assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) + fetchedAccounts = []testAccount{} + // both filters (incompatible) + path = fmt.Sprintf("%s?hermezEthereumAddress=%s&BJJ=%s&limit=%d&fromItem=", endpoint, tc.accounts[0].EthAddr, tc.accounts[0].PublicKey, limit) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + fetchedAccounts = []testAccount{} + // Filter by token IDs + path = fmt.Sprintf("%s?tokenIds=%s&limit=%d&fromItem=", endpoint, stringIds, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Greater(t, len(fetchedAccounts), 0) + assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) + fetchedAccounts = []testAccount{} + // Token Ids + bjj + path = fmt.Sprintf("%s?tokenIds=%s&BJJ=%s&limit=%d&fromItem=", endpoint, stringIds, tc.accounts[0].PublicKey, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Greater(t, len(fetchedAccounts), 0) + assert.LessOrEqual(t, len(fetchedAccounts), len(tc.accounts)) + fetchedAccounts = []testAccount{} + // No filters (checks response content) + path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) + err = doGoodReqPaginated(path, historydb.OrderAsc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Equal(t, len(tc.accounts), len(fetchedAccounts)) + for i := 0; i < len(fetchedAccounts); i++ { + fetchedAccounts[i].Token.ItemID = 0 + if tc.accounts[i].Token.USDUpdate != nil { + assert.Less(t, fetchedAccounts[i].Token.USDUpdate.Unix()-3, tc.accounts[i].Token.USDUpdate.Unix()) + fetchedAccounts[i].Token.USDUpdate = tc.accounts[i].Token.USDUpdate + } + assert.Equal(t, tc.accounts[i], fetchedAccounts[i]) + } + + // No filters Reverse Order (checks response content) + reversedAccounts := []testAccount{} + appendIter = func(intr interface{}) { + for i := 0; i < len(intr.(*testAccountsResponse).Accounts); i++ { + tmp, err := copystructure.Copy(intr.(*testAccountsResponse).Accounts[i]) + if err != nil { + panic(err) + } + reversedAccounts = append(reversedAccounts, tmp.(testAccount)) + } + } + err = doGoodReqPaginated(path, historydb.OrderDesc, &testAccountsResponse{}, appendIter) + assert.NoError(t, err) + assert.Equal(t, len(reversedAccounts), len(fetchedAccounts)) + for i := 0; i < len(fetchedAccounts); i++ { + reversedAccounts[i].Token.ItemID = 0 + fetchedAccounts[len(fetchedAccounts)-1-i].Token.ItemID = 0 + if reversedAccounts[i].Token.USDUpdate != nil { + assert.Less(t, fetchedAccounts[len(fetchedAccounts)-1-i].Token.USDUpdate.Unix()-3, reversedAccounts[i].Token.USDUpdate.Unix()) + fetchedAccounts[len(fetchedAccounts)-1-i].Token.USDUpdate = reversedAccounts[i].Token.USDUpdate + } + assert.Equal(t, reversedAccounts[i], fetchedAccounts[len(fetchedAccounts)-1-i]) + } + + // Test GetAccount + path = fmt.Sprintf("%s/%s", endpoint, fetchedAccounts[2].Idx) + account := testAccount{} + assert.NoError(t, doGoodReq("GET", path, nil, &account)) + account.Token.ItemID = 0 + assert.Equal(t, fetchedAccounts[2], account) + + // 400 + path = fmt.Sprintf("%s/hez:12345", endpoint) + err = doBadReq("GET", path, nil, 400) + assert.NoError(t, err) + // 404 + path = fmt.Sprintf("%s/hez:10:12345", endpoint) + err = doBadReq("GET", path, nil, 404) + assert.NoError(t, err) +} diff --git a/api/accountcreationauths_test.go b/api/accountcreationauths_test.go index 145ee44..dd4391c 100644 --- a/api/accountcreationauths_test.go +++ b/api/accountcreationauths_test.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/hex" "encoding/json" - "fmt" "math/big" "testing" "time" @@ -41,7 +40,6 @@ func TestAccountCreationAuth(t *testing.T) { jsonAuthBytes, err := json.Marshal(auth) assert.NoError(t, err) jsonAuthReader := bytes.NewReader(jsonAuthBytes) - fmt.Println(string(jsonAuthBytes)) assert.NoError( t, doGoodReq( "POST", diff --git a/api/api.go b/api/api.go index 8e7f793..579cfbd 100644 --- a/api/api.go +++ b/api/api.go @@ -51,7 +51,7 @@ func SetAPIEndpoints( if explorerEndpoints { // Account server.GET("/accounts", getAccounts) - server.GET("/accounts/:hermezEthereumAddress/:accountIndex", getAccount) + server.GET("/accounts/:accountIndex", getAccount) server.GET("/exits", getExits) server.GET("/exits/:batchNum/:accountIndex", getExit) // Transaction diff --git a/api/api_test.go b/api/api_test.go index e9ea583..4bf8661 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -37,6 +37,7 @@ type testCommon struct { batches []testBatch fullBatches []testFullBatch coordinators []historydb.CoordinatorAPI + accounts []testAccount usrAddr string usrBjj string accs []common.Account @@ -65,6 +66,7 @@ func TestMain(m *testing.M) { router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml") // HistoryDB pass := os.Getenv("POSTGRES_PASS") + database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez") if err != nil { panic(err) @@ -294,6 +296,7 @@ func TestMain(m *testing.M) { batches: testBatches, fullBatches: fullBatches, coordinators: coordinators, + accounts: genTestAccounts(accs, tokensUSD), usrAddr: ethAddrToHez(usrAddr), usrBjj: bjjToString(usrBjj), accs: accs, diff --git a/api/exits.go b/api/exits.go index 3e5a258..3badc0f 100644 --- a/api/exits.go +++ b/api/exits.go @@ -11,7 +11,7 @@ import ( func getExits(c *gin.Context) { // Get query parameters // Account filters - tokenID, addr, bjj, idx, err := parseAccountFilters(c) + tokenID, addr, bjj, idx, err := parseExitFilters(c) if err != nil { retBadReq(err, c) return diff --git a/api/handlers.go b/api/handlers.go index d3edc05..e40766a 100644 --- a/api/handlers.go +++ b/api/handlers.go @@ -31,14 +31,6 @@ var ( ErrNillBidderAddr = errors.New("biderAddr can not be nil") ) -func getAccounts(c *gin.Context) { - -} - -func getAccount(c *gin.Context) { - -} - func getNextForgers(c *gin.Context) { } diff --git a/api/parsers.go b/api/parsers.go index 36e34f2..dbf56bf 100644 --- a/api/parsers.go +++ b/api/parsers.go @@ -142,7 +142,7 @@ func parseIdx(c querier) (*common.Idx, error) { return stringToIdx(idxStr, name) } -func parseAccountFilters(c querier) (*common.TokenID, *ethCommon.Address, *babyjub.PublicKey, *common.Idx, error) { +func parseExitFilters(c querier) (*common.TokenID, *ethCommon.Address, *babyjub.PublicKey, *common.Idx, error) { // TokenID tid, err := parseQueryUint("tokenId", nil, 0, maxUint32, c) if err != nil { @@ -235,6 +235,40 @@ func parseSlotFilters(c querier) (*int64, *int64, *ethCommon.Address, *bool, err return minSlotNum, maxSlotNum, wonByEthereumAddress, finishedAuction, nil } +func parseAccountFilters(c querier) ([]common.TokenID, *ethCommon.Address, *babyjub.PublicKey, error) { + // TokenID + idsStr := c.Query("tokenIds") + var tokenIDs []common.TokenID + if idsStr != "" { + ids := strings.Split(idsStr, ",") + + for _, id := range ids { + idUint, err := strconv.Atoi(id) + if err != nil { + return nil, nil, nil, err + } + tokenID := common.TokenID(idUint) + tokenIDs = append(tokenIDs, tokenID) + } + } + // Hez Eth addr + addr, err := parseQueryHezEthAddr(c) + if err != nil { + return nil, nil, nil, err + } + // BJJ + bjj, err := parseQueryBJJ(c) + if err != nil { + return nil, nil, nil, err + } + if addr != nil && bjj != nil { + return nil, nil, nil, + errors.New("bjj and hermezEthereumAddress params are incompatible") + } + + return tokenIDs, addr, bjj, nil +} + // Param parsers type paramer interface { diff --git a/api/swagger.yml b/api/swagger.yml index b0c7015..65c3d15 100644 --- a/api/swagger.yml +++ b/api/swagger.yml @@ -2145,6 +2145,8 @@ components: type: object description: State tree leaf. It contains balance and nonce of an account. properties: + itemId: + $ref: '#/components/schemas/ItemId' accountIndex: $ref: '#/components/schemas/AccountIndex' nonce: @@ -2157,6 +2159,15 @@ components: $ref: '#/components/schemas/HezEthereumAddress' token: $ref: '#/components/schemas/Token' + additionaProperties: false + required: + - itemId + - accountIndex + - nonce + - balance + - bjj + - hezEthereumAddress + - token Accounts: type: object properties: @@ -2167,6 +2178,10 @@ components: $ref: '#/components/schemas/Account' pagination: $ref: '#/components/schemas/PaginationInfo' + additionalProperties: false + required: + - accounts + - pagination Slot: type: object description: Slot information. diff --git a/api/txshistory.go b/api/txshistory.go index 0a6a0e6..d8203d8 100644 --- a/api/txshistory.go +++ b/api/txshistory.go @@ -10,7 +10,7 @@ import ( func getHistoryTxs(c *gin.Context) { // Get query parameters - tokenID, addr, bjj, idx, err := parseAccountFilters(c) + tokenID, addr, bjj, idx, err := parseExitFilters(c) if err != nil { retBadReq(err, c) return diff --git a/api/txspool_test.go b/api/txspool_test.go index 4adb937..fd2d2f3 100644 --- a/api/txspool_test.go +++ b/api/txspool_test.go @@ -3,7 +3,6 @@ package api import ( "bytes" "encoding/json" - "fmt" "math/big" "testing" "time" @@ -96,7 +95,6 @@ func genTestPoolTx(accs []common.Account, privKs []babyjub.PrivateKey, tokens [] poolTxsToSend = []testPoolTxSend{} poolTxsToReceive = []testPoolTxReceive{} for _, poolTx := range poolTxs { - fmt.Println(poolTx) // common.PoolL2Tx ==> testPoolTxSend token := getTokenByID(poolTx.TokenID, tokens) genSendTx := testPoolTxSend{ diff --git a/db/historydb/historydb.go b/db/historydb/historydb.go index 71e0aa8..ecffbc3 100644 --- a/db/historydb/historydb.go +++ b/db/historydb/historydb.go @@ -1332,9 +1332,7 @@ func (hdb *HistoryDB) AddBlockSCData(blockData *common.BlockData) (err error) { // GetCoordinatorAPI returns a coordinator by its bidderAddr func (hdb *HistoryDB) GetCoordinatorAPI(bidderAddr ethCommon.Address) (*CoordinatorAPI, error) { coordinator := &CoordinatorAPI{} - err := meddler.QueryRow( - hdb.db, coordinator, `SELECT * FROM coordinator WHERE bidder_addr = $1;`, bidderAddr, - ) + err := meddler.QueryRow(hdb.db, coordinator, "SELECT * FROM coordinator WHERE bidder_addr = $1;", bidderAddr) return coordinator, err } @@ -1392,3 +1390,95 @@ func (hdb *HistoryDB) GetAuctionVars() (*common.AuctionVariables, error) { ) return auctionVars, err } + +// GetAccountAPI returns an account by its index +func (hdb *HistoryDB) GetAccountAPI(idx common.Idx) (*AccountAPI, error) { + account := &AccountAPI{} + err := meddler.QueryRow(hdb.db, account, `SELECT account.item_id, hez_idx(account.idx, token.symbol) as idx, account.batch_num, account.bjj, account.eth_addr, + token.token_id, token.item_id AS token_item_id, token.eth_block_num AS token_block, + token.eth_addr as token_eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update + FROM account INNER JOIN token ON account.token_id = token.token_id WHERE idx = $1;`, idx) + + if err != nil { + return nil, err + } + + return account, nil +} + +// GetAccountsAPI returns a list of accounts from the DB and pagination info +func (hdb *HistoryDB) GetAccountsAPI(tokenIDs []common.TokenID, ethAddr *ethCommon.Address, bjj *babyjub.PublicKey, fromItem, limit *uint, order string) ([]AccountAPI, *db.Pagination, error) { + if ethAddr != nil && bjj != nil { + return nil, nil, errors.New("ethAddr and bjj are incompatible") + } + var query string + var args []interface{} + queryStr := `SELECT account.item_id, hez_idx(account.idx, token.symbol) as idx, account.batch_num, account.bjj, account.eth_addr, + token.token_id, token.item_id AS token_item_id, token.eth_block_num AS token_block, + token.eth_addr as token_eth_addr, token.name, token.symbol, token.decimals, token.usd, token.usd_update, + COUNT(*) OVER() AS total_items, MIN(account.item_id) OVER() AS first_item, MAX(account.item_id) OVER() AS last_item + FROM account INNER JOIN token ON account.token_id = token.token_id ` + // Apply filters + nextIsAnd := false + // ethAddr filter + if ethAddr != nil { + queryStr += "WHERE account.eth_addr = ? " + nextIsAnd = true + args = append(args, ethAddr) + } else if bjj != nil { // bjj filter + queryStr += "WHERE account.bjj = ? " + nextIsAnd = true + args = append(args, bjj) + } + // tokenID filter + if len(tokenIDs) > 0 { + if nextIsAnd { + queryStr += "AND " + } else { + queryStr += "WHERE " + } + queryStr += "account.token_id IN (?) " + args = append(args, tokenIDs) + nextIsAnd = true + } + if fromItem != nil { + if nextIsAnd { + queryStr += "AND " + } else { + queryStr += "WHERE " + } + if order == OrderAsc { + queryStr += "account.item_id >= ? " + } else { + queryStr += "account.item_id <= ? " + } + args = append(args, fromItem) + } + // pagination + queryStr += "ORDER BY account.item_id " + if order == OrderAsc { + queryStr += " ASC " + } else { + queryStr += " DESC " + } + queryStr += fmt.Sprintf("LIMIT %d;", *limit) + query, argsQ, err := sqlx.In(queryStr, args...) + if err != nil { + return nil, nil, err + } + query = hdb.db.Rebind(query) + + accounts := []*AccountAPI{} + if err := meddler.QueryAll(hdb.db, &accounts, query, argsQ...); err != nil { + return nil, nil, err + } + if len(accounts) == 0 { + return nil, nil, sql.ErrNoRows + } + + return db.SlicePtrsToSlice(accounts).([]AccountAPI), &db.Pagination{ + TotalItems: accounts[0].TotalItems, + FirstItem: accounts[0].FirstItem, + LastItem: accounts[0].LastItem, + }, nil +} diff --git a/db/historydb/views.go b/db/historydb/views.go index dd23a00..60bf50e 100644 --- a/db/historydb/views.go +++ b/db/historydb/views.go @@ -228,6 +228,55 @@ type CoordinatorAPI struct { LastItem int `json:"-" meddler:"last_item"` } +// AccountAPI is a representation of a account with additional information +// required by the API +type AccountAPI struct { + ItemID int `meddler:"item_id"` + Idx apitypes.HezIdx `meddler:"idx"` + BatchNum common.BatchNum `meddler:"batch_num"` + PublicKey apitypes.HezBJJ `meddler:"bjj"` + EthAddr apitypes.HezEthAddr `meddler:"eth_addr"` + Nonce common.Nonce `meddler:"-"` // max of 40 bits used + Balance *apitypes.BigIntStr `meddler:"-"` // max of 192 bits used + TotalItems int `meddler:"total_items"` + FirstItem int `meddler:"first_item"` + LastItem int `meddler:"last_item"` + TokenID common.TokenID `meddler:"token_id"` + TokenItemID int `meddler:"token_item_id"` + TokenEthBlockNum int64 `meddler:"token_block"` + TokenEthAddr ethCommon.Address `meddler:"token_eth_addr"` + TokenName string `meddler:"name"` + TokenSymbol string `meddler:"symbol"` + TokenDecimals uint64 `meddler:"decimals"` + TokenUSD *float64 `meddler:"usd"` + TokenUSDUpdate *time.Time `meddler:"usd_update"` +} + +// MarshalJSON is used to neast some of the fields of AccountAPI +// without the need of auxiliar structs +func (account AccountAPI) MarshalJSON() ([]byte, error) { + jsonAccount := map[string]interface{}{ + "itemId": account.ItemID, + "accountIndex": account.Idx, + "nonce": account.Nonce, + "balance": account.Balance, + "bjj": account.PublicKey, + "hezEthereumAddress": account.EthAddr, + "token": map[string]interface{}{ + "id": account.TokenID, + "itemId": account.TokenItemID, + "ethereumBlockNum": account.TokenEthBlockNum, + "ethereumAddress": account.TokenEthAddr, + "name": account.TokenName, + "symbol": account.TokenSymbol, + "decimals": account.TokenDecimals, + "USD": account.TokenUSD, + "fiatUpdate": account.TokenUSDUpdate, + }, + } + return json.Marshal(jsonAccount) +} + // BatchAPI is a representation of a batch with additional information // required by the API, and extracted by joining block table type BatchAPI struct { diff --git a/db/migrations/0001.sql b/db/migrations/0001.sql index f7332de..e3a3a57 100644 --- a/db/migrations/0001.sql +++ b/db/migrations/0001.sql @@ -94,6 +94,7 @@ LANGUAGE plpgsql; -- +migrate StatementEnd CREATE TABLE account ( + item_id SERIAL, idx BIGINT PRIMARY KEY, token_id INT NOT NULL REFERENCES token (token_id) ON DELETE CASCADE, batch_num BIGINT NOT NULL REFERENCES batch (batch_num) ON DELETE CASCADE,