From 68eeb23294de02167b5b24152fb55de51a0db7d0 Mon Sep 17 00:00:00 2001 From: maria jose Date: Sun, 8 Oct 2023 23:03:38 -0400 Subject: [PATCH] add new table accounts and queries: GetListAccounts, CountAccounts, CreateAccount add new queries in token_transfers: GetTokenTransfersByToAccount, GetTokenTransfersByAccount, CountTokenTransfersByAccount indexer: update method onSetAccount integration test: update TestaccountAPI, add new test TestAPIAccountTokentxs api: update GetTransfers, add countAccounts swagger documentation --- api/accounts.go | 139 +++++++++- api/chain.go | 2 +- apiclient/account.go | 74 ++++- cmd/end2endtest/account.go | 25 ++ test/api_test.go | 252 +++++++++++++++--- vochain/indexer/db/account.sql.go | 84 ++++++ vochain/indexer/db/db.go | 50 ++++ vochain/indexer/db/models.go | 6 + vochain/indexer/db/token_transfers.sql.go | 62 +++++ vochain/indexer/indexer.go | 89 ++++++- vochain/indexer/indexer_test.go | 133 +++++++++ vochain/indexer/indexertypes/types.go | 6 + .../0005_create_table_token_transfers.sql | 7 +- .../migrations/0008_create_table_account.sql | 9 + vochain/indexer/process.go | 2 +- vochain/indexer/queries/account.sql | 20 ++ vochain/indexer/queries/token_transfers.sql | 16 +- vochain/indexer/sqlc.yaml | 2 + 18 files changed, 913 insertions(+), 65 deletions(-) create mode 100644 vochain/indexer/db/account.sql.go create mode 100644 vochain/indexer/migrations/0008_create_table_account.sql create mode 100644 vochain/indexer/queries/account.sql diff --git a/api/accounts.go b/api/accounts.go index b6ce6e269..4841b8034 100644 --- a/api/accounts.go +++ b/api/accounts.go @@ -71,7 +71,7 @@ func (a *API) enableAccountHandlers() error { "/accounts/{accountID}/transfers/page/{page}", "GET", apirest.MethodAccessTypePublic, - a.tokenTransfersHandler, + a.tokenTransfersListHandler, ); err != nil { return err } @@ -83,6 +83,30 @@ func (a *API) enableAccountHandlers() error { ); err != nil { return err } + if err := a.Endpoint.RegisterMethod( + "/accounts/{accountID}/transfers/count", + "GET", + apirest.MethodAccessTypePublic, + a.tokenTransfersCountHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/accounts/count", + "GET", + apirest.MethodAccessTypePublic, + a.accountCountHandler, + ); err != nil { + return err + } + if err := a.Endpoint.RegisterMethod( + "/accounts/page/{page}", + "GET", + apirest.MethodAccessTypePublic, + a.accountListHandler, + ); err != nil { + return err + } return nil } @@ -125,7 +149,7 @@ func (a *API) accountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) er sik, err := a.vocapp.State.SIKFromAddress(addr) if err != nil && !errors.Is(err, state.ErrSIKNotFound) { - log.Warnf("uknown error getting SIK: %v", err) + log.Warnf("unknown error getting SIK: %v", err) return ErrGettingSIK.WithErr(err) } @@ -235,6 +259,33 @@ func (a *API) accountSetHandler(msg *apirest.APIdata, ctx *httprouter.HTTPContex return ctx.Send(data, apirest.HTTPstatusOK) } +// accountCountHandler +// +// @Summary Total number of accounts +// @Description Returns the count of total number of existing accounts +// @Tags Accounts +// @Accept json +// @Produce json +// @Success 200 {object} object{count=int} +// @Router /accounts/count [get] +func (a *API) accountCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + count, err := a.indexer.CountTotalAccounts() + if err != nil { + return err + } + + data, err := json.Marshal( + struct { + Count uint64 `json:"count"` + }{Count: count}, + ) + if err != nil { + return ErrMarshalingServerJSONFailed.WithErr(err) + } + + return ctx.Send(data, apirest.HTTPstatusOK) +} + // electionListHandler // // @Summary List organization elections @@ -349,9 +400,9 @@ func (a *API) electionCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte return ctx.Send(data, apirest.HTTPstatusOK) } -// tokenTransfersHandler +// tokenTransfersListHandler // -// @Summary List account transfers +// @Summary List account received and sent token transfers // @Description Returns the token transfers for an account. A transfer is a token transference from one account to other (excepting the burn address). // @Tags Accounts // @Accept json @@ -360,7 +411,7 @@ func (a *API) electionCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPConte // @Param page path string true "Paginator page" // @Success 200 {object} object{transfers=[]indexertypes.TokenTransferMeta} // @Router /accounts/{accountID}/transfers/page/{page} [get] -func (a *API) tokenTransfersHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { +func (a *API) tokenTransfersListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("accountID"))) if err != nil || accountID == nil { return ErrCantParseAccountID.Withf("%q", ctx.URLParam("accountID")) @@ -380,13 +431,13 @@ func (a *API) tokenTransfersHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCont } } page = page * MaxPageSize - transfers, err := a.indexer.GetTokenTransfersByFromAccount(accountID, int32(page), MaxPageSize) + transfers, err := a.indexer.GetTokenTransfersAccount(accountID, int32(page), MaxPageSize) if err != nil { return ErrCantFetchTokenTransfers.WithErr(err) } data, err := json.Marshal( struct { - Transfers []*indexertypes.TokenTransferMeta `json:"transfers"` + Transfers map[string][]*indexertypes.TokenTransferMeta `json:"transfers"` }{Transfers: transfers}, ) if err != nil { @@ -441,3 +492,77 @@ func (a *API) tokenFeesHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) } return ctx.Send(data, apirest.HTTPstatusOK) } + +// tokenTransfersCountHandler +// +// @Summary Total number of sent and received transactions +// @Description Returns the count of total number of sent and received transactions for an account. A transaction is a token transfer from one account to another existing account +// @Tags Accounts +// @Accept json +// @Produce json +// @Param accountID path string true "Specific accountID" +// @Success 200 {object} object{count=int} "Number of transaction sent and received for the account" +// @Router /accounts/{accountID}/transfers/count [get] +func (a *API) tokenTransfersCountHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + accountID, err := hex.DecodeString(util.TrimHex(ctx.URLParam("accountID"))) + if err != nil || accountID == nil { + return ErrCantParseAccountID.Withf("%q", ctx.URLParam("accountID")) + } + acc, err := a.vocapp.State.GetAccount(common.BytesToAddress(accountID), true) + if acc == nil { + return ErrAccountNotFound + } + if err != nil { + return err + } + + count, err := a.indexer.CountTokenTransfersAccount(accountID) + if err != nil { + return err + } + data, err := json.Marshal( + struct { + Count uint64 `json:"count"` + }{Count: count}, + ) + if err != nil { + return ErrMarshalingServerJSONFailed.WithErr(err) + } + + return ctx.Send(data, apirest.HTTPstatusOK) +} + +// accountListHandler +// +// @Summary List of the existing accounts +// @Description Returns information (address, balance and nonce) of the existing accounts +// @Tags Accounts +// @Accept json +// @Produce json +// @Param page path string true "Paginator page" +// @Success 200 {object} object{accounts=[]indexertypes.Account} +// @Router /accounts/page/{page} [get] +func (a *API) accountListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { + var err error + page := 0 + if ctx.URLParam("page") != "" { + page, err = strconv.Atoi(ctx.URLParam("page")) + if err != nil { + return ErrCantParsePageNumber + } + } + page = page * MaxPageSize + accounts, err := a.indexer.GetListAccounts(int32(page), MaxPageSize) + if err != nil { + return ErrCantFetchTokenTransfers.WithErr(err) + } + data, err := json.Marshal( + struct { + Accounts []indexertypes.Account `json:"accounts"` + }{Accounts: accounts}, + ) + if err != nil { + return ErrMarshalingServerJSONFailed.WithErr(err) + } + return ctx.Send(data, apirest.HTTPstatusOK) +} diff --git a/api/chain.go b/api/chain.go index 14626bccb..85f2f0ddb 100644 --- a/api/chain.go +++ b/api/chain.go @@ -499,7 +499,7 @@ func (a *API) chainTxListPaginated(_ *apirest.APIdata, ctx *httprouter.HTTPConte } return err } - // wrap list in a struct to consistently return list in a object, return empty + // wrap list in a struct to consistently return list in an object, return empty // object if the list does not contains any result type response struct { Txs []*indexertypes.Transaction `json:"transactions"` diff --git a/apiclient/account.go b/apiclient/account.go index 3211f4c55..2fef3d27e 100644 --- a/apiclient/account.go +++ b/apiclient/account.go @@ -10,7 +10,7 @@ import ( "go.vocdoni.io/dvote/data/ipfs" "go.vocdoni.io/dvote/httprouter/apirest" "go.vocdoni.io/dvote/types" - indexertypes "go.vocdoni.io/dvote/vochain/indexer/indexertypes" + "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/proto/build/go/models" "google.golang.org/protobuf/proto" ) @@ -260,20 +260,22 @@ func (c *HTTPclient) AccountSetMetadata(metadata *api.AccountMetadata) (types.He return accv.TxHash, nil } -// GetTransfers returns the list of token transfers associated with an account -func (c *HTTPclient) GetTransfers(from common.Address, page int) ([]*indexertypes.TokenTransferMeta, error) { - resp, code, err := c.Request(HTTPGET, nil, "accounts", from.Hex(), "transfers", "page", strconv.Itoa(page)) +// ListTokenTransfers returns the list of sent and received token transfers associated with an account +func (c *HTTPclient) ListTokenTransfers(account common.Address, page int) (map[string][]*indexertypes.TokenTransferMeta, error) { + resp, code, err := c.Request(HTTPGET, nil, "accounts", account.Hex(), "transfers", "page", strconv.Itoa(page)) if err != nil { return nil, err } if code != apirest.HTTPstatusOK { return nil, fmt.Errorf("%s: %d (%s)", errCodeNot200, code, resp) } - var transfers []*indexertypes.TokenTransferMeta - if err := json.Unmarshal(resp, &transfers); err != nil { + tokenTxs := new(struct { + Transfers map[string][]*indexertypes.TokenTransferMeta `json:"transfers"` + }) + if err := json.Unmarshal(resp, &tokenTxs); err != nil { return nil, err } - return transfers, nil + return tokenTxs.Transfers, nil } // SetSIK function allows to update the Secret Identity Key for the current @@ -417,3 +419,61 @@ func (c *HTTPclient) RegisterSIKForVote(electionId types.HexBytes, proof *Census } return hash, nil } + +// ListAccounts return the account list information (address, balance and nonce) +func (c *HTTPclient) ListAccounts(page int) ([]indexertypes.Account, error) { + resp, code, err := c.Request(HTTPGET, nil, "accounts", "page", strconv.Itoa(page)) + if err != nil { + return nil, err + } + if code != apirest.HTTPstatusOK { + return nil, fmt.Errorf("%s: %d (%s)", errCodeNot200, code, resp) + } + accts := new(struct { + Accounts []indexertypes.Account `json:"accounts"` + }) + + if err := json.Unmarshal(resp, &accts); err != nil { + return nil, err + } + + return accts.Accounts, nil +} + +// CountAccounts returns the total count of exiting accounts +func (c *HTTPclient) CountAccounts() (uint64, error) { + resp, code, err := c.Request(HTTPGET, nil, "accounts", "count") + if err != nil { + return 0, err + } + if code != apirest.HTTPstatusOK { + return 0, fmt.Errorf("%s: %d (%s)", errCodeNot200, code, resp) + } + accts := new(struct { + Count uint64 `json:"count"` + }) + + if err := json.Unmarshal(resp, accts); err != nil { + return 0, err + } + return accts.Count, nil +} + +// CountTokenTransfers returns the total count of transfers sent and received for an account +func (c *HTTPclient) CountTokenTransfers(accountID common.Address) (uint64, error) { + resp, code, err := c.Request(HTTPGET, nil, "accounts", accountID.Hex(), "transfers", "count") + if err != nil { + return 0, err + } + if code != apirest.HTTPstatusOK { + return 0, fmt.Errorf("%s: %d (%s)", errCodeNot200, code, resp) + } + tokentxs := new(struct { + Count uint64 `json:"count"` + }) + + if err := json.Unmarshal(resp, tokentxs); err != nil { + return 0, err + } + return tokentxs.Count, nil +} diff --git a/cmd/end2endtest/account.go b/cmd/end2endtest/account.go index f18c4be23..baded1232 100644 --- a/cmd/end2endtest/account.go +++ b/cmd/end2endtest/account.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/common" "github.com/google/go-cmp/cmp" apipkg "go.vocdoni.io/dvote/api" "go.vocdoni.io/dvote/apiclient" @@ -230,6 +231,13 @@ func testSendTokens(api *apiclient.HTTPclient, aliceKeys, bobKeys *ethereum.Sign return err } + if err := checkTokenTransfersCount(alice, aliceKeys.Address()); err != nil { + return err + } + if err := checkTokenTransfersCount(bob, bobKeys.Address()); err != nil { + return err + } + return nil } @@ -249,6 +257,23 @@ func checkAccountNonceAndBalance(api *apiclient.HTTPclient, expNonce uint32, exp return nil } +func checkTokenTransfersCount(api *apiclient.HTTPclient, address common.Address) error { + tokenTxs, err := api.ListTokenTransfers(address, 0) + if err != nil { + return err + } + countTokenTxs := uint64(len(tokenTxs["Received"]) + len(tokenTxs["Sent"])) + + count, err := api.CountTokenTransfers(address) + if err != nil { + return err + } + if count != countTokenTxs { + return fmt.Errorf("expected %s to match transfers count %d and %d", address, count, countTokenTxs) + } + return nil +} + func ensureAccountExists(api *apiclient.HTTPclient, faucetPkg *models.FaucetPackage) (*apipkg.Account, error) { for i := 0; i < retries; i++ { diff --git a/test/api_test.go b/test/api_test.go index a9c483eb5..1bb7eda68 100644 --- a/test/api_test.go +++ b/test/api_test.go @@ -1,6 +1,7 @@ package test import ( + "encoding/hex" "encoding/json" "fmt" "math/big" @@ -17,6 +18,7 @@ import ( "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/util" "go.vocdoni.io/dvote/vochain" + "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/dvote/vochain/state" "go.vocdoni.io/dvote/vochain/state/electionprice" "go.vocdoni.io/proto/build/go/models" @@ -182,9 +184,7 @@ func TestAPIcensusAndVote(t *testing.T) { qt.Assert(t, v2.VoteID.String(), qt.Equals, v.VoteID.String()) qt.Assert(t, v2.BlockHeight, qt.Equals, uint32(2)) qt.Assert(t, *v2.TransactionIndex, qt.Equals, int32(0)) - - // TODO (painan): check why the voterID is not present on the reply - //qt.Assert(t, v2.VoterID.String(), qt.Equals, voterKey.AddressString()) + qt.Assert(t, v2.VoterID.String(), qt.Equals, hex.EncodeToString(voterKey.Address().Bytes())) } func TestAPIaccount(t *testing.T) { @@ -205,54 +205,53 @@ func TestAPIaccount(t *testing.T) { waitUntilHeight(t, c, 1) // create a new account - signer := ethereum.SignKeys{} - qt.Assert(t, signer.Generate(), qt.IsNil) - - // metdata - meta := &api.AccountMetadata{ - Version: "1.0", - } - metaData, err := json.Marshal(meta) - qt.Assert(t, err, qt.IsNil) - - // transaction - fp, err := vochain.GenerateFaucetPackage(server.Account, signer.Address(), 50) - qt.Assert(t, err, qt.IsNil) - stx := models.SignedTx{} - infoURI := server.Storage.URIprefix() + ipfs.CalculateCIDv1json(metaData) - sik, err := signer.AccountSIK(nil) - qt.Assert(t, err, qt.IsNil) - stx.Tx, err = proto.Marshal(&models.Tx{Payload: &models.Tx_SetAccount{ - SetAccount: &models.SetAccountTx{ - Txtype: models.TxType_CREATE_ACCOUNT, - Nonce: new(uint32), - InfoURI: &infoURI, - Account: signer.Address().Bytes(), - FaucetPackage: fp, - SIK: sik, - }, - }}) - qt.Assert(t, err, qt.IsNil) - stx.Signature, err = signer.SignVocdoniTx(stx.Tx, server.VochainAPP.ChainID()) - qt.Assert(t, err, qt.IsNil) - stxb, err := proto.Marshal(&stx) - qt.Assert(t, err, qt.IsNil) - - // send the transaction and metadata - accSet := api.AccountSet{ - Metadata: metaData, - TxPayload: stxb, - } - resp, code := c.Request("POST", &accSet, "accounts") - qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + initBalance := uint64(80) + signer := createAccount(t, c, server, initBalance) // Block 2 server.VochainAPP.AdvanceTestBlock() waitUntilHeight(t, c, 2) // check the account exist - resp, code = c.Request("GET", nil, "accounts", signer.Address().String()) + resp, code := c.Request("GET", nil, "accounts", signer.Address().String()) + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + // get accounts count + resp, code = c.Request("GET", nil, "accounts", "count") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + countAccts := struct { + Count uint64 `json:"count"` + }{} + + err := json.Unmarshal(resp, &countAccts) + qt.Assert(t, err, qt.IsNil) + + // 2 accounts must exist: the previously new created account and the auxiliary + // account used to transfer to the new account + qt.Assert(t, countAccts.Count, qt.Equals, uint64(2)) + + // get the accounts info + resp, code = c.Request("GET", nil, "accounts", "page", "0") qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + accts := struct { + Accounts []indexertypes.Account `json:"accounts"` + }{} + + err = json.Unmarshal(resp, &accts) + qt.Assert(t, err, qt.IsNil) + + // the second account must be the account created in the test + // due 'ORDER BY balance DESC' in the sql query + gotAcct := accts.Accounts[1] + + // compare the address in list of accounts with the account address previously created + qt.Assert(t, gotAcct.Address.String(), qt.Equals, hex.EncodeToString(signer.Address().Bytes())) + + // compare the balance expected for the new account in the account list + qt.Assert(t, gotAcct.Balance, qt.Equals, initBalance) + } func TestAPIElectionCost(t *testing.T) { @@ -296,6 +295,143 @@ func TestAPIElectionCost(t *testing.T) { 547026) } +func TestAPIAccountTokentxs(t *testing.T) { + server := testcommon.APIserver{} + server.Start(t, + api.ChainHandler, + api.CensusHandler, + api.VoteHandler, + api.AccountHandler, + api.ElectionHandler, + api.WalletHandler, + ) + token1 := uuid.New() + c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1) + + // Block 1 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 1) + + // create a new account + initBalance := uint64(80) + signer := createAccount(t, c, server, initBalance) + + // Block 2 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 2) + + // create another new account + signer2 := createAccount(t, c, server, initBalance) + + // Block 3 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 3) + + // check the account1 exists + resp, code := c.Request("GET", nil, "accounts", signer.Address().String()) + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + // check the account2 exists + resp, code = c.Request("GET", nil, "accounts", signer2.Address().String()) + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + // transaction send token from account 1 to account 2 + amountAcc1toAcct2 := uint64(25) + sendTokensTx(t, c, signer, signer2, server.VochainAPP.ChainID(), 0, amountAcc1toAcct2) + + // Block 4 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 4) + + // transaction send token from account 2 to account 1 + amountAcc2toAcct1 := uint64(10) + sendTokensTx(t, c, signer2, signer, server.VochainAPP.ChainID(), 0, amountAcc2toAcct1) + + // Block 5 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 5) + + // get the token transfers received and sent for account 1 + resp, code = c.Request("GET", nil, "accounts", signer.Address().Hex(), "transfers", "page", "0") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + tokenTxs := new(struct { + Transfers map[string][]*indexertypes.TokenTransferMeta `json:"transfers"` + }) + err := json.Unmarshal(resp, tokenTxs) + qt.Assert(t, err, qt.IsNil) + + // get the total token transfers count for account 1 + resp, code = c.Request("GET", nil, "accounts", signer.Address().Hex(), "transfers", "count") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + countTnsAcc := struct { + Count uint64 `json:"count"` + }{} + err = json.Unmarshal(resp, &countTnsAcc) + qt.Assert(t, err, qt.IsNil) + + totalTokenTxs := uint64(len(tokenTxs.Transfers["Received"]) + len(tokenTxs.Transfers["Sent"])) + + // compare count of total token transfers for the account 1 using the two response + qt.Assert(t, totalTokenTxs, qt.Equals, countTnsAcc.Count) + + // get the token transfers received and sent for account 2 + resp, code = c.Request("GET", nil, "accounts", signer2.Address().Hex(), "transfers", "page", "0") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + tokenTxs2 := new(struct { + Transfers map[string][]*indexertypes.TokenTransferMeta `json:"transfers"` + }) + err = json.Unmarshal(resp, tokenTxs2) + qt.Assert(t, err, qt.IsNil) + + // get the total token transfers count for account 2 + resp, code = c.Request("GET", nil, "accounts", signer2.Address().Hex(), "transfers", "count") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + countTnsAcc2 := struct { + Count uint64 `json:"count"` + }{} + err = json.Unmarshal(resp, &countTnsAcc2) + qt.Assert(t, err, qt.IsNil) + + totalTokenTxs2 := uint64(len(tokenTxs2.Transfers["Received"]) + len(tokenTxs2.Transfers["Sent"])) + // compare count of total token transfers for the account 2 using the two response + qt.Assert(t, totalTokenTxs2, qt.Equals, countTnsAcc2.Count) + + resp, code = c.Request("GET", nil, "accounts", "page", "0") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + accts := struct { + Accounts []indexertypes.Account `json:"accounts"` + }{} + + err = json.Unmarshal(resp, &accts) + qt.Assert(t, err, qt.IsNil) + + // the second account must be the account 2 created in the test + // due 'ORDER BY balance DESC' in the sql query. For this account + // was transfer an initial balance: 80, received from the account: 25 and + // sent to the account 1: 10 tokens + gotAcct2 := accts.Accounts[1] + + // compare the address in list of accounts with the account2 address previously created + qt.Assert(t, gotAcct2.Address.String(), qt.Equals, hex.EncodeToString(signer2.Address().Bytes())) + + // compare the balance expected for the account 2 in the account list + qt.Assert(t, gotAcct2.Balance, qt.Equals, initBalance+amountAcc1toAcct2-amountAcc2toAcct1) + + gotAcct1 := accts.Accounts[2] + + // compare the address in list of accounts with the account1 address previously created + qt.Assert(t, gotAcct1.Address.String(), qt.Equals, hex.EncodeToString(signer.Address().Bytes())) + + // compare the balance expected for the account 1 in the account list + qt.Assert(t, gotAcct1.Balance, qt.Equals, initBalance+amountAcc2toAcct1-amountAcc1toAcct2) + +} + func runAPIElectionCostWithParams(t *testing.T, electionParams electionprice.ElectionParameters, startBlock uint32, initialBalance uint64, @@ -525,3 +661,31 @@ func createAccount(t testing.TB, c *testutil.TestHTTPclient, return &signer } + +func sendTokensTx(t testing.TB, c *testutil.TestHTTPclient, + signerFrom, signerTo *ethereum.SignKeys, chainID string, nonce uint32, amount uint64) { + var err error + stx := models.SignedTx{} + stx.Tx, err = proto.Marshal(&models.Tx{ + Payload: &models.Tx_SendTokens{ + SendTokens: &models.SendTokensTx{ + Txtype: models.TxType_SET_ACCOUNT_INFO_URI, + Nonce: nonce, + From: signerFrom.Address().Bytes(), + To: signerTo.Address().Bytes(), + Value: amount, + }, + }}) + qt.Assert(t, err, qt.IsNil) + + stx.Signature, err = signerFrom.SignVocdoniTx(stx.Tx, chainID) + qt.Assert(t, err, qt.IsNil) + + txData, err := proto.Marshal(&stx) + qt.Assert(t, err, qt.IsNil) + + tx := &api.Transaction{Payload: txData} + + resp, code := c.Request("POST", tx, "chain", "transactions") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) +} diff --git a/vochain/indexer/db/account.sql.go b/vochain/indexer/db/account.sql.go new file mode 100644 index 000000000..205a7803d --- /dev/null +++ b/vochain/indexer/db/account.sql.go @@ -0,0 +1,84 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.23.0 +// source: account.sql + +package indexerdb + +import ( + "context" + "database/sql" + + "go.vocdoni.io/dvote/types" +) + +const countAccounts = `-- name: CountAccounts :one +; + +SELECT COUNT(*) FROM accounts +` + +func (q *Queries) CountAccounts(ctx context.Context) (int64, error) { + row := q.queryRow(ctx, q.countAccountsStmt, countAccounts) + var count int64 + err := row.Scan(&count) + return count, err +} + +const createAccount = `-- name: CreateAccount :execresult +INSERT INTO accounts( + account, balance, nonce +) VALUES ( + ?, ?, ? +) ON CONFLICT(account) + DO UPDATE SET + balance = excluded.balance, + nonce = excluded.nonce +` + +type CreateAccountParams struct { + Account types.AccountID + Balance int64 + Nonce int64 +} + +func (q *Queries) CreateAccount(ctx context.Context, arg CreateAccountParams) (sql.Result, error) { + return q.exec(ctx, q.createAccountStmt, createAccount, arg.Account, arg.Balance, arg.Nonce) +} + +const getListAccounts = `-- name: GetListAccounts :many +; + +SELECT account, balance, nonce +FROM accounts +ORDER BY balance DESC +LIMIT ? OFFSET ? +` + +type GetListAccountsParams struct { + Limit int64 + Offset int64 +} + +func (q *Queries) GetListAccounts(ctx context.Context, arg GetListAccountsParams) ([]Account, error) { + rows, err := q.query(ctx, q.getListAccountsStmt, getListAccounts, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Account + for rows.Next() { + var i Account + if err := rows.Scan(&i.Account, &i.Balance, &i.Nonce); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/vochain/indexer/db/db.go b/vochain/indexer/db/db.go index 056bf6c33..6570998b9 100644 --- a/vochain/indexer/db/db.go +++ b/vochain/indexer/db/db.go @@ -24,6 +24,12 @@ func New(db DBTX) *Queries { func Prepare(ctx context.Context, db DBTX) (*Queries, error) { q := Queries{db: db} var err error + if q.countAccountsStmt, err = db.PrepareContext(ctx, countAccounts); err != nil { + return nil, fmt.Errorf("error preparing query CountAccounts: %w", err) + } + if q.countTokenTransfersAccountStmt, err = db.PrepareContext(ctx, countTokenTransfersAccount); err != nil { + return nil, fmt.Errorf("error preparing query CountTokenTransfersAccount: %w", err) + } if q.countTransactionsStmt, err = db.PrepareContext(ctx, countTransactions); err != nil { return nil, fmt.Errorf("error preparing query CountTransactions: %w", err) } @@ -33,6 +39,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.countVotesByProcessIDStmt, err = db.PrepareContext(ctx, countVotesByProcessID); err != nil { return nil, fmt.Errorf("error preparing query CountVotesByProcessID: %w", err) } + if q.createAccountStmt, err = db.PrepareContext(ctx, createAccount); err != nil { + return nil, fmt.Errorf("error preparing query CreateAccount: %w", err) + } if q.createBlockStmt, err = db.PrepareContext(ctx, createBlock); err != nil { return nil, fmt.Errorf("error preparing query CreateBlock: %w", err) } @@ -60,6 +69,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getLastTransactionsStmt, err = db.PrepareContext(ctx, getLastTransactions); err != nil { return nil, fmt.Errorf("error preparing query GetLastTransactions: %w", err) } + if q.getListAccountsStmt, err = db.PrepareContext(ctx, getListAccounts); err != nil { + return nil, fmt.Errorf("error preparing query GetListAccounts: %w", err) + } if q.getProcessStmt, err = db.PrepareContext(ctx, getProcess); err != nil { return nil, fmt.Errorf("error preparing query GetProcess: %w", err) } @@ -90,6 +102,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { if q.getTokenTransfersByFromAccountStmt, err = db.PrepareContext(ctx, getTokenTransfersByFromAccount); err != nil { return nil, fmt.Errorf("error preparing query GetTokenTransfersByFromAccount: %w", err) } + if q.getTokenTransfersByToAccountStmt, err = db.PrepareContext(ctx, getTokenTransfersByToAccount); err != nil { + return nil, fmt.Errorf("error preparing query GetTokenTransfersByToAccount: %w", err) + } if q.getTransactionStmt, err = db.PrepareContext(ctx, getTransaction); err != nil { return nil, fmt.Errorf("error preparing query GetTransaction: %w", err) } @@ -134,6 +149,16 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) { func (q *Queries) Close() error { var err error + if q.countAccountsStmt != nil { + if cerr := q.countAccountsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing countAccountsStmt: %w", cerr) + } + } + if q.countTokenTransfersAccountStmt != nil { + if cerr := q.countTokenTransfersAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing countTokenTransfersAccountStmt: %w", cerr) + } + } if q.countTransactionsStmt != nil { if cerr := q.countTransactionsStmt.Close(); cerr != nil { err = fmt.Errorf("error closing countTransactionsStmt: %w", cerr) @@ -149,6 +174,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing countVotesByProcessIDStmt: %w", cerr) } } + if q.createAccountStmt != nil { + if cerr := q.createAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing createAccountStmt: %w", cerr) + } + } if q.createBlockStmt != nil { if cerr := q.createBlockStmt.Close(); cerr != nil { err = fmt.Errorf("error closing createBlockStmt: %w", cerr) @@ -194,6 +224,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getLastTransactionsStmt: %w", cerr) } } + if q.getListAccountsStmt != nil { + if cerr := q.getListAccountsStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getListAccountsStmt: %w", cerr) + } + } if q.getProcessStmt != nil { if cerr := q.getProcessStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getProcessStmt: %w", cerr) @@ -244,6 +279,11 @@ func (q *Queries) Close() error { err = fmt.Errorf("error closing getTokenTransfersByFromAccountStmt: %w", cerr) } } + if q.getTokenTransfersByToAccountStmt != nil { + if cerr := q.getTokenTransfersByToAccountStmt.Close(); cerr != nil { + err = fmt.Errorf("error closing getTokenTransfersByToAccountStmt: %w", cerr) + } + } if q.getTransactionStmt != nil { if cerr := q.getTransactionStmt.Close(); cerr != nil { err = fmt.Errorf("error closing getTransactionStmt: %w", cerr) @@ -348,9 +388,12 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar type Queries struct { db DBTX tx *sql.Tx + countAccountsStmt *sql.Stmt + countTokenTransfersAccountStmt *sql.Stmt countTransactionsStmt *sql.Stmt countVotesStmt *sql.Stmt countVotesByProcessIDStmt *sql.Stmt + createAccountStmt *sql.Stmt createBlockStmt *sql.Stmt createProcessStmt *sql.Stmt createTokenFeeStmt *sql.Stmt @@ -360,6 +403,7 @@ type Queries struct { getBlockStmt *sql.Stmt getEntityCountStmt *sql.Stmt getLastTransactionsStmt *sql.Stmt + getListAccountsStmt *sql.Stmt getProcessStmt *sql.Stmt getProcessCountStmt *sql.Stmt getProcessIDsByFinalResultsStmt *sql.Stmt @@ -370,6 +414,7 @@ type Queries struct { getTokenFeesByTxTypeStmt *sql.Stmt getTokenTransferStmt *sql.Stmt getTokenTransfersByFromAccountStmt *sql.Stmt + getTokenTransfersByToAccountStmt *sql.Stmt getTransactionStmt *sql.Stmt getTransactionByHashStmt *sql.Stmt getTxReferenceByBlockHeightAndBlockIndexStmt *sql.Stmt @@ -389,9 +434,12 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { return &Queries{ db: tx, tx: tx, + countAccountsStmt: q.countAccountsStmt, + countTokenTransfersAccountStmt: q.countTokenTransfersAccountStmt, countTransactionsStmt: q.countTransactionsStmt, countVotesStmt: q.countVotesStmt, countVotesByProcessIDStmt: q.countVotesByProcessIDStmt, + createAccountStmt: q.createAccountStmt, createBlockStmt: q.createBlockStmt, createProcessStmt: q.createProcessStmt, createTokenFeeStmt: q.createTokenFeeStmt, @@ -401,6 +449,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getBlockStmt: q.getBlockStmt, getEntityCountStmt: q.getEntityCountStmt, getLastTransactionsStmt: q.getLastTransactionsStmt, + getListAccountsStmt: q.getListAccountsStmt, getProcessStmt: q.getProcessStmt, getProcessCountStmt: q.getProcessCountStmt, getProcessIDsByFinalResultsStmt: q.getProcessIDsByFinalResultsStmt, @@ -411,6 +460,7 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries { getTokenFeesByTxTypeStmt: q.getTokenFeesByTxTypeStmt, getTokenTransferStmt: q.getTokenTransferStmt, getTokenTransfersByFromAccountStmt: q.getTokenTransfersByFromAccountStmt, + getTokenTransfersByToAccountStmt: q.getTokenTransfersByToAccountStmt, getTransactionStmt: q.getTransactionStmt, getTransactionByHashStmt: q.getTransactionByHashStmt, getTxReferenceByBlockHeightAndBlockIndexStmt: q.getTxReferenceByBlockHeightAndBlockIndexStmt, diff --git a/vochain/indexer/db/models.go b/vochain/indexer/db/models.go index 8fa059184..11afd5931 100644 --- a/vochain/indexer/db/models.go +++ b/vochain/indexer/db/models.go @@ -10,6 +10,12 @@ import ( "go.vocdoni.io/dvote/types" ) +type Account struct { + Account types.AccountID + Balance int64 + Nonce int64 +} + type Block struct { Height int64 Time time.Time diff --git a/vochain/indexer/db/token_transfers.sql.go b/vochain/indexer/db/token_transfers.sql.go index 111a404b4..51d24353e 100644 --- a/vochain/indexer/db/token_transfers.sql.go +++ b/vochain/indexer/db/token_transfers.sql.go @@ -13,6 +13,22 @@ import ( "go.vocdoni.io/dvote/types" ) +const countTokenTransfersAccount = `-- name: CountTokenTransfersAccount :one +; + + +SELECT COUNT(*) FROM token_transfers +WHERE to_account = ?1 OR + from_account = ?1 +` + +func (q *Queries) CountTokenTransfersAccount(ctx context.Context, account types.AccountID) (int64, error) { + row := q.queryRow(ctx, q.countTokenTransfersAccountStmt, countTokenTransfersAccount, account) + var count int64 + err := row.Scan(&count) + return count, err +} + const createTokenTransfer = `-- name: CreateTokenTransfer :execresult INSERT INTO token_transfers ( tx_hash, block_height, from_account, @@ -106,3 +122,49 @@ func (q *Queries) GetTokenTransfersByFromAccount(ctx context.Context, arg GetTok } return items, nil } + +const getTokenTransfersByToAccount = `-- name: GetTokenTransfersByToAccount :many +; + +SELECT tx_hash, block_height, from_account, to_account, amount, transfer_time FROM token_transfers +WHERE to_account = ?1 +ORDER BY transfer_time DESC +LIMIT ?3 +OFFSET ?2 +` + +type GetTokenTransfersByToAccountParams struct { + ToAccount types.AccountID + Offset int64 + Limit int64 +} + +func (q *Queries) GetTokenTransfersByToAccount(ctx context.Context, arg GetTokenTransfersByToAccountParams) ([]TokenTransfer, error) { + rows, err := q.query(ctx, q.getTokenTransfersByToAccountStmt, getTokenTransfersByToAccount, arg.ToAccount, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []TokenTransfer + for rows.Next() { + var i TokenTransfer + if err := rows.Scan( + &i.TxHash, + &i.BlockHeight, + &i.FromAccount, + &i.ToAccount, + &i.Amount, + &i.TransferTime, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/vochain/indexer/indexer.go b/vochain/indexer/indexer.go index 2301bae29..cfc225d4c 100644 --- a/vochain/indexer/indexer.go +++ b/vochain/indexer/indexer.go @@ -530,8 +530,18 @@ func (idx *Indexer) OnProcessesStart(pids [][]byte) { } } -// OnSetAccount NOT USED but required for implementing the vochain.EventListener interface -func (*Indexer) OnSetAccount(_ []byte, _ *state.Account) {} +func (idx *Indexer) OnSetAccount(accountAddress []byte, account *state.Account) { + idx.blockMu.Lock() + defer idx.blockMu.Unlock() + queries := idx.blockTxQueries() + if _, err := queries.CreateAccount(context.TODO(), indexerdb.CreateAccountParams{ + Account: accountAddress, + Balance: int64(account.Balance), + Nonce: int64(account.Nonce), + }); err != nil { + log.Errorw(err, "cannot index new account") + } +} func (idx *Indexer) OnTransferTokens(tx *vochaintx.TokenTransfer) { t := time.Now() @@ -699,3 +709,78 @@ func (idx *Indexer) GetTokenFeesByType(txType string, offset, maxItems int32) ([ } return tt, nil } + +// GetTokenTransfersByToAccount returns all the token transfers made to a given account +// from the database, ordered by timestamp and paginated by maxItems and offset +func (idx *Indexer) GetTokenTransfersByToAccount(to []byte, offset, maxItems int32) ([]*indexertypes.TokenTransferMeta, error) { + ttFromDB, err := idx.readOnlyQuery.GetTokenTransfersByToAccount(context.TODO(), indexerdb.GetTokenTransfersByToAccountParams{ + ToAccount: to, + Limit: int64(maxItems), + Offset: int64(offset), + }) + if err != nil { + return nil, err + } + tt := []*indexertypes.TokenTransferMeta{} + for _, t := range ttFromDB { + tt = append(tt, &indexertypes.TokenTransferMeta{ + Amount: uint64(t.Amount), + From: t.FromAccount, + To: t.ToAccount, + Height: uint64(t.BlockHeight), + TxHash: t.TxHash, + Timestamp: t.TransferTime, + }) + } + return tt, nil +} + +// GetTokenTransfersAccount returns all the token transfers made to and from a given account +// from the database, ordered by timestamp and paginated by maxItems and offset +func (idx *Indexer) GetTokenTransfersAccount(acc []byte, offset, maxItems int32) (map[string][]*indexertypes.TokenTransferMeta, error) { + transfersTo, err := idx.GetTokenTransfersByToAccount(acc, offset, maxItems) + if err != nil { + return nil, err + } + transfersFrom, err := idx.GetTokenTransfersByFromAccount(acc, offset, maxItems) + if err != nil { + return nil, err + } + + return map[string][]*indexertypes.TokenTransferMeta{ + "Received": transfersTo, + "Sent": transfersFrom, + }, nil + +} + +// CountTokenTransfersAccount returns the count all the token transfers made from a given account +func (idx *Indexer) CountTokenTransfersAccount(acc []byte) (uint64, error) { + count, err := idx.readOnlyQuery.CountTokenTransfersAccount(context.TODO(), acc) + return uint64(count), err +} + +// CountTotalAccounts returns the total number of accounts indexed. +func (idx *Indexer) CountTotalAccounts() (uint64, error) { + count, err := idx.readOnlyQuery.CountAccounts(context.TODO()) + return uint64(count), err +} + +func (idx *Indexer) GetListAccounts(offset, maxItems int32) ([]indexertypes.Account, error) { + accsFromDB, err := idx.readOnlyQuery.GetListAccounts(context.TODO(), indexerdb.GetListAccountsParams{ + Limit: int64(maxItems), + Offset: int64(offset), + }) + if err != nil { + return nil, err + } + tt := []indexertypes.Account{} + for _, acc := range accsFromDB { + tt = append(tt, indexertypes.Account{ + Address: acc.Account, + Balance: uint64(acc.Balance), + Nonce: uint32(acc.Nonce), + }) + } + return tt, nil +} diff --git a/vochain/indexer/indexer_test.go b/vochain/indexer/indexer_test.go index 0c9a4b252..c1d597043 100644 --- a/vochain/indexer/indexer_test.go +++ b/vochain/indexer/indexer_test.go @@ -1416,6 +1416,139 @@ func TestEndBlock(t *testing.T) { qt.Assert(t, proc.BlockCount, qt.Equals, uint32(100)) } +func TestAccountsList(t *testing.T) { + app := vochain.TestBaseApplication(t) + idx := newTestIndexer(t, app, true) + + keys := make([]*ethereum.SignKeys, 0) + for i := 0; i < 25; i++ { + key := ðereum.SignKeys{} + qt.Assert(t, key.Generate(), qt.IsNil) + keys = append(keys, key) + + err := app.State.SetAccount(key.Address(), &state.Account{ + Account: models.Account{ + Balance: 500, + Nonce: uint32(0), + InfoURI: "ipfs://vocdoni.io", + }, + }) + qt.Assert(t, err, qt.IsNil) + + app.AdvanceTestBlock() + } + + // Test total count accounts + totalAccs, err := idx.CountTotalAccounts() + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, totalAccs, qt.CmpEquals(), uint64(25)) + + last := 0 + for i := 0; i < int(totalAccs); i++ { + accts, err := idx.GetListAccounts(int32(last), 10) + qt.Assert(t, err, qt.IsNil) + + for j, acc := range accts { + qt.Assert(t, acc.Address.String(), qt.Equals, hex.EncodeToString(keys[j+last].Address().Bytes())) + qt.Assert(t, acc.Nonce, qt.Equals, uint32(0)) + qt.Assert(t, acc.Balance, qt.Equals, uint64(500)) + } + last += 10 + } + + // update an existing account + err = app.State.SetAccount(keys[0].Address(), &state.Account{ + Account: models.Account{ + Balance: 600, + Nonce: uint32(1), + InfoURI: "ipfs://vocdoni.io", + }, + }) + qt.Assert(t, err, qt.IsNil) + app.AdvanceTestBlock() + + // verify the updated balance and nonce + accts, err := idx.GetListAccounts(int32(0), 5) + qt.Assert(t, err, qt.IsNil) + // the account in the position 0 must be the updated account balance due it has the major balance + // indexer query has order BY balance DESC + qt.Assert(t, accts[0].Address.String(), qt.Equals, hex.EncodeToString(keys[0].Address().Bytes())) + qt.Assert(t, accts[0].Nonce, qt.Equals, uint32(1)) + qt.Assert(t, accts[0].Balance, qt.Equals, uint64(600)) +} + +func TestTokenTransfers(t *testing.T) { + app := vochain.TestBaseApplication(t) + idx := newTestIndexer(t, app, true) + + keys := make([]*ethereum.SignKeys, 0) + // create 3 accounts + for i := 0; i < 3; i++ { + key := ðereum.SignKeys{} + qt.Assert(t, key.Generate(), qt.IsNil) + keys = append(keys, key) + + err := app.State.SetAccount(key.Address(), &state.Account{ + Account: models.Account{ + Balance: 500, + Nonce: uint32(0), + InfoURI: "ipfs://vocdoni.io", + }, + }) + qt.Assert(t, err, qt.IsNil) + + app.AdvanceTestBlock() + } + + err := app.State.TransferBalance(&vochaintx.TokenTransfer{ + FromAddress: keys[0].Address(), + ToAddress: keys[2].Address(), + Amount: 18, + TxHash: util.RandomBytes(32), + }, false) + qt.Assert(t, err, qt.IsNil) + + app.AdvanceTestBlock() + + err = app.State.TransferBalance(&vochaintx.TokenTransfer{ + FromAddress: keys[1].Address(), + ToAddress: keys[2].Address(), + Amount: 95, + TxHash: util.RandomBytes(32), + }, false) + qt.Assert(t, err, qt.IsNil) + + app.AdvanceTestBlock() + + err = app.State.TransferBalance(&vochaintx.TokenTransfer{ + FromAddress: keys[2].Address(), + ToAddress: keys[1].Address(), + Amount: 5, + TxHash: util.RandomBytes(32), + }, false) + qt.Assert(t, err, qt.IsNil) + + app.AdvanceTestBlock() + + // acct 1 must have only one token transfer received + acc1Tokentx, err := idx.GetTokenTransfersByToAccount(keys[1].Address().Bytes(), 0, 10) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, len(acc1Tokentx), qt.Equals, 1) + qt.Assert(t, acc1Tokentx[0].Amount, qt.Equals, uint64(5)) + + // acct 2 must two token transfers received + acc2Tokentx, err := idx.GetTokenTransfersByToAccount(keys[2].Address().Bytes(), 0, 10) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, len(acc2Tokentx), qt.Equals, 2) + qt.Assert(t, acc2Tokentx[0].Amount, qt.Equals, uint64(95)) + qt.Assert(t, acc2Tokentx[1].Amount, qt.Equals, uint64(18)) + + // acct 0 must zero token transfers received + acc0Tokentx, err := idx.GetTokenTransfersByToAccount(keys[0].Address().Bytes(), 0, 10) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, len(acc0Tokentx), qt.Equals, 0) +} + // friendlyResults translates votes into a matrix of strings func friendlyResults(votes [][]*types.BigInt) [][]string { r := [][]string{} diff --git a/vochain/indexer/indexertypes/types.go b/vochain/indexer/indexertypes/types.go index c2bc60086..2aa797d67 100644 --- a/vochain/indexer/indexertypes/types.go +++ b/vochain/indexer/indexertypes/types.go @@ -221,3 +221,9 @@ type TokenFeeMeta struct { Timestamp time.Time `json:"timestamp"` TxType string `json:"txType"` } + +type Account struct { + Address types.AccountID `json:"address"` + Balance uint64 `json:"balance"` + Nonce uint32 `json:"nonce"` +} diff --git a/vochain/indexer/migrations/0005_create_table_token_transfers.sql b/vochain/indexer/migrations/0005_create_table_token_transfers.sql index 2c8d7ed88..46e83fedd 100644 --- a/vochain/indexer/migrations/0005_create_table_token_transfers.sql +++ b/vochain/indexer/migrations/0005_create_table_token_transfers.sql @@ -5,13 +5,16 @@ CREATE TABLE token_transfers ( from_account BLOB NOT NULL, to_account BLOB NOT NULL, amount INTEGER NOT NULL, - transfer_time DATETIME NOT NULL + transfer_time DATETIME NOT NULL, + + FOREIGN KEY(to_account) REFERENCES accounts(account), + FOREIGN KEY(from_account) REFERENCES accounts(account) ); CREATE INDEX index_from_account_token_transfers ON token_transfers(from_account); -- +goose Down -DROP TABLE token_transfers +DROP TABLE token_transfers; DROP INDEX index_from_account_token_transfers diff --git a/vochain/indexer/migrations/0008_create_table_account.sql b/vochain/indexer/migrations/0008_create_table_account.sql new file mode 100644 index 000000000..ecd144245 --- /dev/null +++ b/vochain/indexer/migrations/0008_create_table_account.sql @@ -0,0 +1,9 @@ +-- +goose Up +CREATE TABLE accounts ( + account BLOB NOT NULL PRIMARY KEY, + balance INTEGER NOT NULL, + nonce INTEGER NOT NULL +); + +-- +goose Down +DROP TABLE accounts \ No newline at end of file diff --git a/vochain/indexer/process.go b/vochain/indexer/process.go index 70d10cb2e..bbc0b9348 100644 --- a/vochain/indexer/process.go +++ b/vochain/indexer/process.go @@ -192,7 +192,7 @@ func (idx *Indexer) newEmptyProcess(pid []byte) error { return nil } -// updateProcess synchronize those fields that can be updated on a existing process +// updateProcess synchronize those fields that can be updated on an existing process // with the information obtained from the Vochain state func (idx *Indexer) updateProcess(ctx context.Context, queries *indexerdb.Queries, pid []byte) error { p, err := idx.App.State.Process(pid, false) diff --git a/vochain/indexer/queries/account.sql b/vochain/indexer/queries/account.sql new file mode 100644 index 000000000..b4e2cd5e2 --- /dev/null +++ b/vochain/indexer/queries/account.sql @@ -0,0 +1,20 @@ +-- name: CreateAccount :execresult +INSERT INTO accounts( + account, balance, nonce +) VALUES ( + ?, ?, ? +) ON CONFLICT(account) + DO UPDATE SET + balance = excluded.balance, + nonce = excluded.nonce +; + +-- name: GetListAccounts :many +SELECT * +FROM accounts +ORDER BY balance DESC +LIMIT ? OFFSET ? +; + +-- name: CountAccounts :one +SELECT COUNT(*) FROM accounts; \ No newline at end of file diff --git a/vochain/indexer/queries/token_transfers.sql b/vochain/indexer/queries/token_transfers.sql index 33a3f1577..387d4db5a 100644 --- a/vochain/indexer/queries/token_transfers.sql +++ b/vochain/indexer/queries/token_transfers.sql @@ -18,4 +18,18 @@ WHERE from_account = sqlc.arg(from_account) ORDER BY transfer_time DESC LIMIT sqlc.arg(limit) OFFSET sqlc.arg(offset) -; \ No newline at end of file +; + +-- name: GetTokenTransfersByToAccount :many +SELECT * FROM token_transfers +WHERE to_account = sqlc.arg(to_account) +ORDER BY transfer_time DESC +LIMIT sqlc.arg(limit) +OFFSET sqlc.arg(offset) +; + + +-- name: CountTokenTransfersAccount :one +SELECT COUNT(*) FROM token_transfers +WHERE to_account = sqlc.arg(account) OR + from_account = sqlc.arg(account); \ No newline at end of file diff --git a/vochain/indexer/sqlc.yaml b/vochain/indexer/sqlc.yaml index 06e755470..78ace9170 100644 --- a/vochain/indexer/sqlc.yaml +++ b/vochain/indexer/sqlc.yaml @@ -31,3 +31,5 @@ sql: go_type: "go.vocdoni.io/dvote/types.AccountID" - column: "token_transfers.tx_hash" go_type: "go.vocdoni.io/dvote/types.Hash" + - column: "accounts.account" + go_type: "go.vocdoni.io/dvote/types.AccountID"