From 11f56a9d447d17c851bf9fa36846b8eea0a42e10 Mon Sep 17 00:00:00 2001 From: vcoolish <32914239+vcoolish@users.noreply.github.com> Date: Tue, 13 Aug 2019 22:39:15 +0300 Subject: [PATCH] Vechain block observer added (#254) * Vechain block observer added * minor fixes * Minimized code in client and formatted * comments for exported functions added * added comments * bugs fixed * added transaction normalization test * code formatted * redesigned normalize function * Fix gosimple rules --- platform/vechain/api.go | 156 ++++++++++++++++++++++++++++++++++- platform/vechain/api_test.go | 133 ++++++++++++++++++++++++++--- platform/vechain/client.go | 54 ++++++++---- platform/vechain/model.go | 60 +++++++++++++- 4 files changed, 370 insertions(+), 33 deletions(-) diff --git a/platform/vechain/api.go b/platform/vechain/api.go index 66ead5e44..903824813 100644 --- a/platform/vechain/api.go +++ b/platform/vechain/api.go @@ -26,6 +26,37 @@ func (p *Platform) Coin() coin.Coin { } const VeThorContract = "0x0000000000000000000000000000456e65726779" +const VeThorTransferEvent = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + +// CurrentBlockNumber implementation of interface function which gets a current blockchain height +func (p *Platform) CurrentBlockNumber() (int64, error) { + cbi, err := p.client.GetCurrentBlockInfo() + if err != nil { + return 0, err + } + return cbi.BestBlockNum, nil +} + +// GetBlockByNumber implementation of interface function which gets a block for push notification +func (p *Platform) GetBlockByNumber(num int64) (*blockatlas.Block, error) { + block, err := p.client.GetBlockByNumber(num) + if err != nil { + return nil, err + } + + transactionsChan := p.getTransactions(block.Transactions) + + var txs []blockatlas.Tx + for t := range transactionsChan { + txs = append(txs, NormalizeTransaction(t)...) + } + + return &blockatlas.Block{ + Number: num, + ID: block.ID, + Txs: txs, + }, nil +} func (p *Platform) GetTxsByAddress(address string) (blockatlas.TxPage, error) { return p.getTxsByAddress(address) @@ -89,6 +120,32 @@ func (p *Platform) getTransactionReceipt(ids []string) chan *TransferReceipt { return receiptsChan } +func (p *Platform) getTransactions(ids []string) chan *NativeTransaction { + receiptsChan := make(chan *NativeTransaction, len(ids)) + + sem := util.NewSemaphore(16) + var wg sync.WaitGroup + wg.Add(len(ids)) + for _, id := range ids { + go func(id string) { + defer wg.Done() + sem.Acquire() + defer sem.Release() + receipt, err := p.client.GetTransactionByID(id) + if err != nil { + logrus.WithError(err).WithField("platform", "vechain"). + Warnf("Failed to get transaction for %s", id) + } + receiptsChan <- receipt + }(id) + } + + wg.Wait() + close(receiptsChan) + + return receiptsChan +} + func findTransferReceiptByTxID(receiptsChan chan *TransferReceipt, txID string) TransferReceipt { var transferReceipt TransferReceipt @@ -127,6 +184,13 @@ func (p *Platform) getTxsByAddress(address string) ([]blockatlas.Tx, error) { return txs, nil } +func formatHexToAddress(hex string) string { + if len(hex) > 26 { + return "0x" + hex[26:] + } + return hex +} + func NormalizeTransfer(receipt *TransferReceipt, clause *Clause) (tx blockatlas.Tx, ok bool) { feeBase10, err := util.HexToDecimal(receipt.Receipt.Paid) if err != nil { @@ -150,7 +214,7 @@ func NormalizeTransfer(receipt *TransferReceipt, clause *Clause) (tx blockatlas. Date: int64(time), Type: blockatlas.TxTransfer, Block: block, - Status: receipt.Receipt.Status(), + Status: ReceiptStatus(receipt.Receipt.Reverted), Sequence: block, Meta: blockatlas.Transfer{ Value: blockatlas.Amount(valueBase10), @@ -184,7 +248,7 @@ func NormalizeTokenTransfer(t *TokenTransfer, receipt *TransferReceipt) (tx bloc Date: t.Timestamp, Type: blockatlas.TxNativeTokenTransfer, Block: block, - Status: receipt.Receipt.Status(), + Status: ReceiptStatus(receipt.Receipt.Reverted), Sequence: block, Meta: blockatlas.NativeTokenTransfer{ Name: "VeThor Token", @@ -197,3 +261,91 @@ func NormalizeTokenTransfer(t *TokenTransfer, receipt *TransferReceipt) (tx bloc }, }, true } + +// NormalizeTransaction converts a VeChain VTHO token transaction into the generic model +func NormalizeTransaction(t *NativeTransaction) (txs []blockatlas.Tx) { + + for outputIndex, output := range t.Receipt.Outputs { + //Normalizes vtho transfer events in given transaction + for eventIndex, event := range output.Events { + if len(event.Topics) == 3 && event.Topics[0] == VeThorTransferEvent { + + feeBase10, err := util.HexToDecimal(t.Receipt.Paid) + if err != nil { + continue + } + + valueBase10, err := util.HexToDecimal(t.Receipt.Outputs[outputIndex].Events[eventIndex].Data) + if err != nil { + continue + } + fee := blockatlas.Amount(feeBase10) + value := blockatlas.Amount(valueBase10) + fromHex := t.Receipt.Outputs[outputIndex].Events[eventIndex].Topics[1] + toHex := t.Receipt.Outputs[outputIndex].Events[eventIndex].Topics[2] + from := formatHexToAddress(fromHex) + to := formatHexToAddress(toHex) + block := t.Block + + txs = append(txs, blockatlas.Tx{ + ID: t.ID, + Coin: coin.VET, + From: from, + To: to, + Fee: fee, + Date: t.Timestamp, + Type: blockatlas.TxNativeTokenTransfer, + Block: block, + Status: ReceiptStatus(t.Receipt.Reverted), + Sequence: block, + Meta: blockatlas.NativeTokenTransfer{ + Name: "VeThor Token", + Symbol: "VTHO", + TokenID: VeThorContract, + Decimals: 18, + Value: value, + From: from, + To: to, + }, + }) + } + } + //Normalizes transfers in given transaction + for transferIndex := range output.Transfers { + feeBase10, err := util.HexToDecimal(t.Receipt.Paid) + if err != nil { + continue + } + + transfer := t.Receipt.Outputs[outputIndex].Transfers[transferIndex] + valueBase10, err := util.HexToDecimal(transfer.Amount) + if err != nil { + continue + } + + fee := blockatlas.Amount(feeBase10) + time := t.Timestamp + block := t.Block + + txs = append(txs, blockatlas.Tx{ + ID: t.ID, + Coin: coin.VET, + From: transfer.Sender, + To: transfer.Recipient, + Fee: fee, + Date: time, + Type: blockatlas.TxTransfer, + Block: block, + Status: ReceiptStatus(t.Receipt.Reverted), + Sequence: block, + Meta: blockatlas.Transfer{ + Value: blockatlas.Amount(valueBase10), + Symbol: coin.Coins[coin.VET].Symbol, + Decimals: coin.Coins[coin.VET].Decimals, + }, + }) + } + } + + return txs +} diff --git a/platform/vechain/api_test.go b/platform/vechain/api_test.go index 1364ba2a6..8dca07dcd 100644 --- a/platform/vechain/api_test.go +++ b/platform/vechain/api_test.go @@ -3,10 +3,9 @@ package vechain import ( "bytes" "encoding/json" - "testing" - "github.com/trustwallet/blockatlas" "github.com/trustwallet/blockatlas/coin" + "testing" ) const transferReceipt = `{ @@ -29,7 +28,7 @@ const transferFailedReceipt = `{ "timestamp": 1556569300, "receipt": { "paid": "0x1236efcbcbb340000", - "reverted": true + "reverted": false } }` @@ -41,11 +40,81 @@ const transferClause = `{ const tokenTransfer = ` { "amount": "0x00000000000000000000000000000000000000000000000d8d726b7177a80000", - "block": 2465269, + "block": 2620166, "origin": "0xb853d6a965fbc047aaa9f04d774d53861d7ed653", "receiver": "0x9f3742c2c2fe66c7fca08d77d2262c22e3d56ac8", - "timestamp": 1555009870, - "txId": "0xd17dd968610fb4a39ab02a5d8827b26f4cdcd147fb4a4f7a5d5ab14066525d4b" + "timestamp": 1556569300, + "txId": "0x2b8776bd4679fa2afa28b55d66d4f6c7c77522fc878ce294d25e32475b704517" +} +` + +const transaction = ` +{ + "block":2620166, + "blockRef":"0x003578d93e73a9ca", + "chainTag":74, + "clauses":[ + { + "data":"0xa9059cbb0000000000000000000000009f3742c2c2fe66c7fca08d77d2262c22e3d56ac8000000000000000000000000000000000000000000000008ac7230489e800000", + "numValue":0, + "to":"0x0000000000000000000000000000456e65726779", + "txClauseIndex":0, + "txId":"0x2b8776bd4679fa2afa28b55d66d4f6c7c77522fc878ce294d25e32475b704517", + "value":"0x0" + } + ], + "expiration":720, + "gas":160000, + "gasPriceCoef":0, + "id":"0x2b8776bd4679fa2afa28b55d66d4f6c7c77522fc878ce294d25e32475b704517", + "meta":{ + "blockID":"0x003578db7b662faecc743f3a401515eef5baebe16c27e635f79bcfca3b8a39dc", + "blockNumber":3504347, + "blockTimestamp":1565458520 + }, + "nonce":"0x8e60abce86ae", + "numClauses":1, + "origin":"0xb853d6a965fbc047aaa9f04d774d53861d7ed653", + "receipt":{ + "gasPayer":"0xa760bdcbf6c2935d2f1591a38f23251619f802ad", + "gasUsed":36582, + "meta":{ + "blockID":"0x003578db7b662faecc743f3a401515eef5baebe16c27e635f79bcfca3b8a39dc", + "blockNumber":3504347, + "blockTimestamp":1565458520, + "txID":"0x2b8776bd4679fa2afa28b55d66d4f6c7c77522fc878ce294d25e32475b704517", + "txOrigin":"0xb853d6a965fbc047aaa9f04d774d53861d7ed653" + }, + "outputs":[ + { + "events":[ + { + "address":"0x0000000000000000000000000000456e65726779", + "topics":[ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000b853d6a965fbc047aaa9f04d774d53861d7ed653", + "0x0000000000000000000000009f3742c2c2fe66c7fca08d77d2262c22e3d56ac8" + ], + "data":"0x00000000000000000000000000000000000000000000000d8d726b7177a80000" + } + ], + "transfers":[ + { + "sender":"0xb853d6a965fbc047aaa9f04d774d53861d7ed653", + "recipient":"0xda623049a13df5c8a24f0d7713f4add4ab136b1f", + "amount":"0x29bde5885d7ac80000" + } + ] + } + ], + "paid":"0x1236efcbcbb340000", + "reverted":false, + "reward":"0x984d9c8dd8008000" + }, + "reverted":0, + "size":191, + "timestamp":1556569300, + "totalValue":0 } ` @@ -66,16 +135,16 @@ var expectedTransferTrx = blockatlas.Tx{ } var expectedVeThorTrx = blockatlas.Tx{ - ID: "0xd17dd968610fb4a39ab02a5d8827b26f4cdcd147fb4a4f7a5d5ab14066525d4b", + ID: "0x2b8776bd4679fa2afa28b55d66d4f6c7c77522fc878ce294d25e32475b704517", Coin: coin.VET, From: "0xb853d6a965fbc047aaa9f04d774d53861d7ed653", To: "0x9f3742c2c2fe66c7fca08d77d2262c22e3d56ac8", Fee: "21000000000000000000", - Date: 1555009870, + Date: 1556569300, Type: blockatlas.TxNativeTokenTransfer, - Status: "failed", - Sequence: 2465269, - Block: 2465269, + Status: "completed", + Sequence: 2620166, + Block: 2620166, Meta: blockatlas.NativeTokenTransfer{ Name: "VeThor Token", Symbol: "VTHO", @@ -115,7 +184,7 @@ func TestNormalizeTransfer(t *testing.T) { var readyTx blockatlas.Tx normTx, ok := NormalizeTransfer(&receipt, &clause) if !ok { - t.Fatal("VeChain: Can't normalize transaction", readyTx) + t.Fatal("VeChain: Can't normalize transfer", readyTx) } readyTx = normTx @@ -164,7 +233,7 @@ func TestNormalizeTokenTransfer(t *testing.T) { var readyTx blockatlas.Tx normTx, ok := NormalizeTokenTransfer(&tt, &receipt) if !ok { - t.Fatal("VeChain: Can't normalize token transaction", readyTx) + t.Fatal("VeChain: Can't normalize token transfer", readyTx) } readyTx = normTx @@ -185,3 +254,41 @@ func TestNormalizeTokenTransfer(t *testing.T) { } } } + +func TestNormalizeTransaction(t *testing.T) { + var tests = []struct { + Transaction string + ExpectedTransaction []blockatlas.Tx + }{ + {transaction, []blockatlas.Tx{expectedVeThorTrx, expectedTransferTrx}}, + } + + for _, test := range tests { + var transaction NativeTransaction + + tErr := json.Unmarshal([]byte(test.Transaction), &transaction) + if tErr != nil { + t.Fatal(tErr) + } + + var readyTxs []blockatlas.Tx + + readyTxs = append(readyTxs, NormalizeTransaction(&transaction)...) + + actual, err := json.Marshal(&readyTxs) + if err != nil { + t.Fatal(err) + } + + expectedTransactions, err := json.Marshal(&test.ExpectedTransaction) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(actual, expectedTransactions) { + println(string(actual)) + println(string(expectedTransactions)) + t.Error("Transactions not equal") + } + } +} diff --git a/platform/vechain/client.go b/platform/vechain/client.go index 225c38602..0acbde130 100644 --- a/platform/vechain/client.go +++ b/platform/vechain/client.go @@ -4,20 +4,40 @@ import ( "encoding/json" "fmt" "github.com/sirupsen/logrus" + "github.com/trustwallet/blockatlas/client" "io/ioutil" "net/http" + "net/url" ) +//Client model contains client instance and base url type Client struct { HTTPClient *http.Client URL string } +// GetCurrentBlockInfo get request function which returns current blockchain status model +func (c *Client) GetCurrentBlockInfo() (cbi *CurrentBlockInfo, err error) { + err = client.Request(c.HTTPClient, c.URL, "clientInit", url.Values{}, &cbi) + + return cbi, err +} + +// GetBlockByNumber get request function which returns block model requested by number +func (c *Client) GetBlockByNumber(num int64) (block *Block, err error) { + path := fmt.Sprintf("blocks/%d", num) + + err = client.Request(c.HTTPClient, c.URL, path, url.Values{}, &block) + + return block, err +} + +// GetTransactions get request function which returns a VET transfer transactions for given address func (c *Client) GetTransactions(address string) (TransferTx, error) { var transfers TransferTx - url := fmt.Sprintf("%s/transactions?address=%s&count=25&offset=0", c.URL, address) - resp, err := c.HTTPClient.Get(url) + path := fmt.Sprintf("%s/transactions?address=%s&count=25&offset=0", c.URL, address) + resp, err := c.HTTPClient.Get(path) if err != nil { logrus.WithError(err).Error("VeChain: Failed HTTP get transactions") return transfers, err @@ -39,13 +59,14 @@ func (c *Client) GetTransactions(address string) (TransferTx, error) { return transfers, nil } +// GetTokenTransfers get request function which returns a token transfer transactions for given address func (c *Client) GetTokenTransfers(address string) (TokenTransferTxs, error) { var transfers TokenTransferTxs url := fmt.Sprintf("%s/tokenTransfers?address=%s&count=25&offset=0", c.URL, address) resp, err := c.HTTPClient.Get(url) if err != nil { - logrus.WithError(err).Error("VeChain: Failed HTTP get token trasnfer transactions") + logrus.WithError(err).Error("VeChain: Failed HTTP get token transfer transactions") return transfers, err } defer resp.Body.Close() @@ -65,19 +86,20 @@ func (c *Client) GetTokenTransfers(address string) (TokenTransferTxs, error) { return transfers, nil } -func (c *Client) GetTransactionReceipt(id string) (*TransferReceipt, error) { - url := fmt.Sprintf("%s/transactions/%s", c.URL, id) - resp, err := c.HTTPClient.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() +// GetTransactionReceipt get request function which returns a transaction for given id and parses it to TransferReceipt +func (c *Client) GetTransactionReceipt(id string) (receipt *TransferReceipt, err error) { + path := fmt.Sprintf("transactions/%s", id) - var receipt TransferReceipt - err = json.NewDecoder(resp.Body).Decode(&receipt) - if err != nil { - return nil, err - } + err = client.Request(c.HTTPClient, c.URL, path, url.Values{}, &receipt) + + return receipt, err +} + +// GetTransactionByID get request function which returns a transaction for given id and parses it to NativeTransaction +func (c *Client) GetTransactionByID(id string) (transaction *NativeTransaction, err error) { + path := fmt.Sprintf("transactions/%s", id) + + err = client.Request(c.HTTPClient, c.URL, path, url.Values{}, &transaction) - return &receipt, nil + return transaction, err } diff --git a/platform/vechain/model.go b/platform/vechain/model.go index d353c7b63..4f6b7c758 100644 --- a/platform/vechain/model.go +++ b/platform/vechain/model.go @@ -38,8 +38,9 @@ type Receipt struct { Reverted bool `json:"reverted"` } -func (r *Receipt) Status() string { - if r.Reverted { +// ReceiptStatus function that describes transaction status +func ReceiptStatus(r bool) string { + if r { return blockatlas.StatusFailed } return blockatlas.StatusCompleted @@ -58,3 +59,58 @@ type TokenTransfer struct { Timestamp int64 `json:"timestamp"` TxID string `json:"txId"` } + +// CurrentBlockInfo type is a model with current blockchain height +type CurrentBlockInfo struct { + BestBlockNum int64 `json:"bestBlockNum"` +} + +// Block type is a VeChain block model +type Block struct { + ID string `json:"Id"` + Transactions []string `json:"transactions"` +} + +// Event type is a field in native transaction with contract call info +type Event struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` +} + +// Transfer type is a field in native transaction with VET transfer data +type Transfer struct { + Sender string `json:"sender"` + Recipient string `json:"recipient"` + Amount string `json:"amount"` +} + +// Output type is a field in native transaction +type Output struct { + Events []Event `json:"events"` + Transfers []Transfer `json:"transfers"` +} + +// TransactionReceipt type for parsing receipt info +type TransactionReceipt struct { + Outputs []Output `json:"outputs"` + Paid string `json:"paid"` + Reverted bool `json:"reverted"` +} + +// NativeTransaction type for Native VeChain transaction with full transfer info +type NativeTransaction struct { + Block uint64 `json:"block"` + Clauses []Clause `json:"clauses"` + ID string `json:"id"` + Origin string `json:"origin"` + Receipt TransactionReceipt `json:"receipt"` + Reverted int64 `json:"reverted"` + Timestamp int64 `json:"timestamp"` +} + +// Error model for request error +type Error struct { + Code int64 `json:"code"` + Message string `json:"message"` +}