-
Notifications
You must be signed in to change notification settings - Fork 106
backend: add Transactions implementation to blockbook. #3443
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging-blockbook
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,15 +22,19 @@ import ( | |
"math/big" | ||
"net/http" | ||
"net/url" | ||
"time" | ||
|
||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/erc20" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/rpcclient" | ||
ethtypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/types" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" | ||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
"github.com/sirupsen/logrus" | ||
"golang.org/x/time/rate" | ||
) | ||
|
||
|
@@ -44,6 +48,8 @@ type Blockbook struct { | |
url string | ||
httpClient *http.Client | ||
limiter *rate.Limiter | ||
// TODO remove before merging into master? | ||
log *logrus.Entry | ||
} | ||
|
||
// NewBlockbook creates a new instance of EtherScan. | ||
|
@@ -55,6 +61,7 @@ func NewBlockbook(chainId string, httpClient *http.Client) *Blockbook { | |
url: "https://bb1.shiftcrypto.io/api/", | ||
httpClient: httpClient, | ||
limiter: rate.NewLimiter(rate.Limit(callsPerSec), 1), | ||
log: logging.Get().WithField("ETH Client", "Blockbook"), | ||
} | ||
} | ||
|
||
|
@@ -77,15 +84,15 @@ func (blockbook *Blockbook) call(ctx context.Context, handler string, params url | |
if err != nil { | ||
return errp.WithStack(err) | ||
} | ||
|
||
if err := json.Unmarshal(body, result); err != nil { | ||
return errp.Newf("unexpected response from blockbook: %s", string(body)) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (blockbook *Blockbook) address(ctx context.Context, account common.Address, result interface{}) error { | ||
params := url.Values{} | ||
func (blockbook *Blockbook) address(ctx context.Context, account common.Address, params url.Values, result interface{}) error { | ||
address := account.Hex() | ||
|
||
addressPath := fmt.Sprintf("address/%s", address) | ||
|
@@ -103,7 +110,7 @@ func (blockbook *Blockbook) Balance(ctx context.Context, account common.Address) | |
Balance string `json:"balance"` | ||
}{} | ||
|
||
if err := blockbook.address(ctx, account, &result); err != nil { | ||
if err := blockbook.address(ctx, account, url.Values{}, &result); err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
|
||
|
@@ -136,9 +143,114 @@ func (blockbook *Blockbook) PendingNonceAt(ctx context.Context, account common.A | |
return 0, fmt.Errorf("Not yet implemented") | ||
} | ||
|
||
// prepareTransactions casts to []accounts.Transactions and removes duplicate entries and sets the | ||
// transaction type (send, receive, send to self) based on the account address. | ||
func prepareTransactions( | ||
isERC20 bool, | ||
blockTipHeight *big.Int, | ||
isInternal bool, | ||
transactions []*Tx, address common.Address) ([]*accounts.TransactionData, error) { | ||
seen := map[string]struct{}{} | ||
|
||
// TODO figure out if needed. Etherscan.go uses this to compute the num of confirmations. | ||
// But numConfirmations is already returned by the API call. | ||
_ = blockTipHeight | ||
|
||
_ = isInternal // TODO figure out how to deal with internal txs. | ||
|
||
castedTransactions := make([]*accounts.TransactionData, 0, len(transactions)) | ||
ours := address.Hex() | ||
for _, tx := range transactions { | ||
if _, ok := seen[tx.Txid]; ok { | ||
// Skip duplicate transactions. | ||
continue | ||
} | ||
seen[tx.Txid] = struct{}{} | ||
|
||
fee := coin.NewAmount(tx.FeesSat.Int) | ||
timestamp := time.Unix(tx.Blocktime, 0) | ||
status, err := tx.Status() | ||
// TODO do not ignore unconfirmed tx | ||
if status == accounts.TxStatusPending { | ||
continue | ||
} | ||
if err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
from := tx.Vin[0].Addresses[0] | ||
var to string | ||
if len(tx.TokenTransfers) > 0 { | ||
to = tx.TokenTransfers[0].To | ||
} else { | ||
to = tx.Vout[0].Addresses[0] | ||
} | ||
if ours != from && ours != to { | ||
return nil, errp.New("transaction does not belong to our account") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This errors on my ETH account I use for testing, because some spam tx touched it: https://etherscan.io/tx/0x1a926b5296dc6803a8a07daeebbd6b1b6b476790af5719ec628d5eeca08027f2 This tx has a ton of ERC-1155 token transfers (NFTs?), one of which touches my address. I am not sure so you should check, but I think the Etherscan-based account includes such transactions in the list if it originated from the account, in other words, if the ETH accounts paid the fees for it (which impacts the account balance and portfolio chart calculation). I guess it does not make sense to list "incoming" token transfers in the main ETH account, these are shown separately in the respective ERC20 accounts. |
||
} | ||
|
||
var txType accounts.TxType | ||
switch { | ||
case ours == from && ours == to: | ||
txType = accounts.TxTypeSendSelf | ||
case ours == from: | ||
txType = accounts.TxTypeSend | ||
default: | ||
txType = accounts.TxTypeReceive | ||
} | ||
|
||
addresses, err := tx.Addresses(isERC20) | ||
if err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
castedTransaction := &accounts.TransactionData{ | ||
Fee: &fee, | ||
FeeIsDifferentUnit: isERC20, | ||
Timestamp: ×tamp, | ||
TxID: tx.Txid, | ||
InternalID: tx.Txid, | ||
Height: tx.Blockheight, | ||
NumConfirmations: int(tx.Confirmations), | ||
NumConfirmationsComplete: ethtypes.NumConfirmationsComplete, | ||
Status: status, | ||
Type: txType, | ||
Amount: tx.Amount(address.Hex(), isERC20), | ||
Gas: tx.EthereumSpecific.GasUsed.Uint64(), | ||
Nonce: &tx.EthereumSpecific.Nonce, | ||
Addresses: addresses, | ||
IsErc20: isERC20, | ||
} | ||
castedTransactions = append(castedTransactions, castedTransaction) | ||
} | ||
return castedTransactions, nil | ||
} | ||
|
||
// Transactions implement TransactionSource. | ||
func (blockbook *Blockbook) Transactions(blockTipHeight *big.Int, address common.Address, endBlock *big.Int, erc20Token *erc20.Token) ([]*accounts.TransactionData, error) { | ||
return nil, fmt.Errorf("Not yet implemented") | ||
params := url.Values{} | ||
isERC20 := erc20Token != nil | ||
if isERC20 { | ||
params.Set("contract", erc20Token.ContractAddress().Hex()) | ||
} | ||
params.Set("details", "txslight") | ||
if endBlock != nil { | ||
params.Set("endBlock", endBlock.String()) | ||
} | ||
result := struct { | ||
Transactions []*Tx `json:"transactions"` | ||
}{} | ||
|
||
if err := blockbook.address(context.Background(), address, params, &result); err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
|
||
transactionsNormal, err := prepareTransactions(isERC20, blockTipHeight, false, result.Transactions, address) | ||
|
||
if err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
|
||
return transactionsNormal, nil | ||
|
||
} | ||
|
||
// SendTransaction implements rpc.Interface. | ||
|
@@ -155,8 +267,7 @@ func (blockbook *Blockbook) ERC20Balance(account common.Address, erc20Token *erc | |
} `json:"tokens"` | ||
}{} | ||
|
||
// TODO why is there no context in the signature of this interface method? | ||
if err := blockbook.address(context.Background(), account, &result); err != nil { | ||
if err := blockbook.address(context.Background(), account, url.Values{}, &result); err != nil { | ||
return nil, errp.WithStack(err) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
package blockbook | ||
|
||
import ( | ||
"encoding/json" | ||
"math/big" | ||
|
||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin" | ||
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp" | ||
) | ||
|
||
// TxStatus represents the status of a transaction in the Blockbook API. | ||
type TxStatus int | ||
|
||
const ( | ||
// TxStatusPending indicates that the transaction is pending. | ||
TxStatusPending TxStatus = -1 | ||
// TxStatusOK indicates that the transaction is complete and successful. | ||
TxStatusOK TxStatus = 1 | ||
// TxStatusFailure indicates that the transaction has failed. | ||
TxStatusFailure TxStatus = 0 | ||
) | ||
|
||
// TokenTransfer represents a token transfer in a transaction. | ||
type TokenTransfer struct { | ||
Type string `json:"type"` | ||
From string `json:"from"` | ||
To string `json:"to"` | ||
Contract string `json:"contract"` | ||
Value Amount `json:"value"` | ||
} | ||
|
||
// Vin represents an input in a transaction. | ||
type Vin struct { | ||
Txid string `json:"txid"` | ||
Addresses []string `json:"addresses"` | ||
} | ||
|
||
// Vout represents an output in a transaction. | ||
type Vout struct { | ||
Txid string `json:"txid,omitempty"` | ||
Value Amount `json:"value"` | ||
Addresses []string `json:"addresses"` | ||
} | ||
|
||
// Amount is a wrapper to big.Int to handle JSON unmarshalling. | ||
type Amount struct { | ||
*big.Int | ||
} | ||
|
||
// UnmarshalJSON implements the json.Unmarshaler interface for Amount. | ||
func (a *Amount) UnmarshalJSON(data []byte) error { | ||
var s string | ||
if err := json.Unmarshal(data, &s); err != nil { | ||
return errp.WithStack(err) | ||
} | ||
intValue, ok := new(big.Int).SetString(s, 10) | ||
if !ok { | ||
return errp.Newf("could not parse amount %q", s) | ||
} | ||
a.Int = intValue | ||
return nil | ||
} | ||
|
||
// Tx holds information about a transaction. | ||
type Tx struct { | ||
Txid string `json:"txid"` | ||
Vin []Vin `json:"vin"` | ||
Vout []Vout `json:"vout"` | ||
Blockhash string `json:"blockHash,omitempty"` | ||
Blockheight int `json:"blockHeight"` | ||
Confirmations uint32 `json:"confirmations"` | ||
Blocktime int64 `json:"blockTime"` | ||
ValueOutSat Amount `json:"value"` | ||
ValueInSat Amount `json:"valueIn,omitempty"` | ||
FeesSat Amount `json:"fees,omitempty"` | ||
TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"` | ||
EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"` | ||
} | ||
|
||
// EthereumSpecific contains ethereum specific transaction data. | ||
type EthereumSpecific struct { | ||
Status TxStatus `json:"status"` | ||
Nonce uint64 `json:"nonce"` | ||
GasLimit *big.Int `json:"gasLimit"` | ||
GasUsed *big.Int `json:"gasUsed,omitempty"` | ||
GasPrice Amount `json:"gasPrice,omitempty"` | ||
} | ||
|
||
// Amount returns the total amount of the transaction. | ||
func (tx *Tx) Amount(address string, isERC20 bool) coin.Amount { | ||
if isERC20 { | ||
for _, transfer := range tx.TokenTransfers { | ||
if transfer.Type == "ERC20" { | ||
if transfer.To == address || transfer.From == address { | ||
return coin.NewAmount(transfer.Value.Int) | ||
} | ||
} | ||
} | ||
} | ||
return coin.NewAmount(tx.ValueOutSat.Int) | ||
} | ||
|
||
// Addresses returns the receiving address of the transaction. | ||
func (tx *Tx) Addresses(isERC20 bool) ([]accounts.AddressAndAmount, error) { | ||
var address string | ||
switch { | ||
case isERC20: | ||
address = tx.TokenTransfers[0].To | ||
case len(tx.Vout) > 0: | ||
address = tx.Vout[0].Addresses[0] | ||
default: | ||
return nil, errp.New("transaction has no outputs or token transfers") | ||
} | ||
|
||
return []accounts.AddressAndAmount{{ | ||
Address: address, | ||
Amount: tx.Amount(address, isERC20), | ||
}}, nil | ||
} | ||
|
||
// Status returns the status of the transaction. | ||
func (tx *Tx) Status() (accounts.TxStatus, error) { | ||
switch tx.EthereumSpecific.Status { | ||
case TxStatusPending: | ||
return accounts.TxStatusPending, nil | ||
case TxStatusOK: | ||
return accounts.TxStatusComplete, nil | ||
case TxStatusFailure: | ||
return accounts.TxStatusFailed, nil | ||
default: | ||
// This should never happen | ||
return "", errp.Newf("unknown transaction status %d", tx.EthereumSpecific.Status) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do, my ERC20 accounts don't show any transactions with this PR 😅 I should probably review once it works for ETH and ERC20, or do you want to separate it into two PRs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think since we are on a staging branch, is fine to do it in a separate PR. Definitely need to do some investigation for it :)