Skip to content

Commit ec78e5c

Browse files
committed
backend: add Transactions implementation to blockbook.
1 parent f71b291 commit ec78e5c

File tree

2 files changed

+251
-4
lines changed

2 files changed

+251
-4
lines changed

backend/coins/eth/blockbook/blockbook.go

Lines changed: 116 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@ import (
2222
"math/big"
2323
"net/http"
2424
"net/url"
25+
"time"
2526

2627
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
28+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
2729
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/erc20"
2830
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/rpcclient"
2931
ethtypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/types"
3032
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
33+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
3134
"github.com/ethereum/go-ethereum"
3235
"github.com/ethereum/go-ethereum/common"
3336
"github.com/ethereum/go-ethereum/core/types"
37+
"github.com/sirupsen/logrus"
3438
"golang.org/x/time/rate"
3539
)
3640

@@ -44,6 +48,8 @@ type Blockbook struct {
4448
url string
4549
httpClient *http.Client
4650
limiter *rate.Limiter
51+
// TODO remove before merging into master?
52+
log *logrus.Entry
4753
}
4854

4955
// NewBlockbook creates a new instance of EtherScan.
@@ -55,6 +61,7 @@ func NewBlockbook(chainId string, httpClient *http.Client) *Blockbook {
5561
url: "https://bb1.shiftcrypto.io/api/",
5662
httpClient: httpClient,
5763
limiter: rate.NewLimiter(rate.Limit(callsPerSec), 1),
64+
log: logging.Get().WithField("ETH Client", "Blockbook"),
5865
}
5966
}
6067

@@ -77,15 +84,15 @@ func (blockbook *Blockbook) call(ctx context.Context, handler string, params url
7784
if err != nil {
7885
return errp.WithStack(err)
7986
}
87+
8088
if err := json.Unmarshal(body, result); err != nil {
8189
return errp.Newf("unexpected response from blockbook: %s", string(body))
8290
}
8391

8492
return nil
8593
}
8694

87-
func (blockbook *Blockbook) address(ctx context.Context, account common.Address, result interface{}) error {
88-
params := url.Values{}
95+
func (blockbook *Blockbook) address(ctx context.Context, account common.Address, params url.Values, result interface{}) error {
8996
address := account.Hex()
9097

9198
addressPath := fmt.Sprintf("address/%s", address)
@@ -103,7 +110,7 @@ func (blockbook *Blockbook) Balance(ctx context.Context, account common.Address)
103110
Balance string `json:"balance"`
104111
}{}
105112

106-
if err := blockbook.address(ctx, account, &result); err != nil {
113+
if err := blockbook.address(ctx, account, url.Values{}, &result); err != nil {
107114
return nil, errp.WithStack(err)
108115
}
109116

@@ -136,9 +143,114 @@ func (blockbook *Blockbook) PendingNonceAt(ctx context.Context, account common.A
136143
return 0, fmt.Errorf("Not yet implemented")
137144
}
138145

146+
// prepareTransactions casts to []accounts.Transactions and removes duplicate entries and sets the
147+
// transaction type (send, receive, send to self) based on the account address.
148+
func prepareTransactions(
149+
isERC20 bool,
150+
blockTipHeight *big.Int,
151+
isInternal bool,
152+
transactions []*Tx, address common.Address) ([]*accounts.TransactionData, error) {
153+
seen := map[string]struct{}{}
154+
155+
// TODO figure out if needed. Etherscan.go uses this to compute the num of confirmations.
156+
// But numConfirmations is already returned by the API call.
157+
_ = blockTipHeight
158+
159+
_ = isInternal // TODO figure out how to deal with internal txs.
160+
161+
castedTransactions := make([]*accounts.TransactionData, 0, len(transactions))
162+
ours := address.Hex()
163+
for _, tx := range transactions {
164+
if _, ok := seen[tx.Txid]; ok {
165+
// Skip duplicate transactions.
166+
continue
167+
}
168+
seen[tx.Txid] = struct{}{}
169+
170+
fee := coin.NewAmount(tx.FeesSat.Int)
171+
timestamp := time.Unix(tx.Blocktime, 0)
172+
status, err := tx.Status()
173+
// TODO do not ignore unconfirmed tx
174+
if status == accounts.TxStatusPending {
175+
continue
176+
}
177+
if err != nil {
178+
return nil, errp.WithStack(err)
179+
}
180+
from := tx.Vin[0].Addresses[0]
181+
var to string
182+
if len(tx.TokenTransfers) > 0 {
183+
to = tx.TokenTransfers[0].To
184+
} else {
185+
to = tx.Vout[0].Addresses[0]
186+
}
187+
if ours != from && ours != to {
188+
return nil, errp.New("transaction does not belong to our account")
189+
}
190+
191+
var txType accounts.TxType
192+
switch {
193+
case ours == from && ours == to:
194+
txType = accounts.TxTypeSendSelf
195+
case ours == from:
196+
txType = accounts.TxTypeSend
197+
default:
198+
txType = accounts.TxTypeReceive
199+
}
200+
201+
addresses, err := tx.Addresses(isERC20)
202+
if err != nil {
203+
return nil, errp.WithStack(err)
204+
}
205+
castedTransaction := &accounts.TransactionData{
206+
Fee: &fee,
207+
FeeIsDifferentUnit: isERC20,
208+
Timestamp: &timestamp,
209+
TxID: tx.Txid,
210+
InternalID: tx.Txid,
211+
Height: tx.Blockheight,
212+
NumConfirmations: int(tx.Confirmations),
213+
NumConfirmationsComplete: ethtypes.NumConfirmationsComplete,
214+
Status: status,
215+
Type: txType,
216+
Amount: tx.Amount(address.Hex(), isERC20),
217+
Gas: tx.EthereumSpecific.GasUsed.Uint64(),
218+
Nonce: &tx.EthereumSpecific.Nonce,
219+
Addresses: addresses,
220+
IsErc20: isERC20,
221+
}
222+
castedTransactions = append(castedTransactions, castedTransaction)
223+
}
224+
return castedTransactions, nil
225+
}
226+
139227
// Transactions implement TransactionSource.
140228
func (blockbook *Blockbook) Transactions(blockTipHeight *big.Int, address common.Address, endBlock *big.Int, erc20Token *erc20.Token) ([]*accounts.TransactionData, error) {
141-
return nil, fmt.Errorf("Not yet implemented")
229+
params := url.Values{}
230+
isERC20 := erc20Token != nil
231+
if isERC20 {
232+
params.Set("contract", erc20Token.ContractAddress().Hex())
233+
}
234+
params.Set("details", "txslight")
235+
if endBlock != nil {
236+
params.Set("endBlock", endBlock.String())
237+
}
238+
result := struct {
239+
Transactions []*Tx `json:"transactions"`
240+
}{}
241+
242+
if err := blockbook.address(context.Background(), address, params, &result); err != nil {
243+
return nil, errp.WithStack(err)
244+
}
245+
246+
transactionsNormal, err := prepareTransactions(isERC20, blockTipHeight, false, result.Transactions, address)
247+
248+
if err != nil {
249+
return nil, errp.WithStack(err)
250+
}
251+
252+
return transactionsNormal, nil
253+
142254
}
143255

144256
// SendTransaction implements rpc.Interface.

backend/coins/eth/blockbook/types.go

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package blockbook
2+
3+
import (
4+
"encoding/json"
5+
"math/big"
6+
7+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
8+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
9+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
10+
)
11+
12+
// TxStatus represents the status of a transaction in the Blockbook API.
13+
type TxStatus int
14+
15+
const (
16+
// TxStatusPending indicates that the transaction is pending.
17+
TxStatusPending TxStatus = -1
18+
// TxStatusOK indicates that the transaction is complete and successful.
19+
TxStatusOK TxStatus = 1
20+
// TxStatusFailure indicates that the transaction has failed.
21+
TxStatusFailure TxStatus = 0
22+
)
23+
24+
// TokenTransfer represents a token transfer in a transaction.
25+
type TokenTransfer struct {
26+
Type string `json:"type"`
27+
From string `json:"from"`
28+
To string `json:"to"`
29+
Contract string `json:"contract"`
30+
Value Amount `json:"value"`
31+
}
32+
33+
// Vin represents an input in a transaction.
34+
type Vin struct {
35+
Txid string `json:"txid"`
36+
Addresses []string `json:"addresses"`
37+
}
38+
39+
// Vout represents an output in a transaction.
40+
type Vout struct {
41+
Txid string `json:"txid,omitempty"`
42+
Value Amount `json:"value"`
43+
Addresses []string `json:"addresses"`
44+
}
45+
46+
// Amount is a wrapper to big.Int to handle JSON unmarshalling.
47+
type Amount struct {
48+
*big.Int
49+
}
50+
51+
// UnmarshalJSON implements the json.Unmarshaler interface for Amount.
52+
func (a *Amount) UnmarshalJSON(data []byte) error {
53+
var s string
54+
if err := json.Unmarshal(data, &s); err != nil {
55+
return errp.WithStack(err)
56+
}
57+
intValue, ok := new(big.Int).SetString(s, 10)
58+
if !ok {
59+
return errp.Newf("could not parse amount %q", s)
60+
}
61+
a.Int = intValue
62+
return nil
63+
}
64+
65+
// Tx holds information about a transaction.
66+
type Tx struct {
67+
Txid string `json:"txid"`
68+
Vin []Vin `json:"vin"`
69+
Vout []Vout `json:"vout"`
70+
Blockhash string `json:"blockHash,omitempty"`
71+
Blockheight int `json:"blockHeight"`
72+
Confirmations uint32 `json:"confirmations"`
73+
Blocktime int64 `json:"blockTime"`
74+
ValueOutSat Amount `json:"value"`
75+
ValueInSat Amount `json:"valueIn,omitempty"`
76+
FeesSat Amount `json:"fees,omitempty"`
77+
TokenTransfers []TokenTransfer `json:"tokenTransfers,omitempty"`
78+
EthereumSpecific *EthereumSpecific `json:"ethereumSpecific,omitempty"`
79+
}
80+
81+
// EthereumSpecific contains ethereum specific transaction data.
82+
type EthereumSpecific struct {
83+
Status TxStatus `json:"status"`
84+
Nonce uint64 `json:"nonce"`
85+
GasLimit *big.Int `json:"gasLimit"`
86+
GasUsed *big.Int `json:"gasUsed,omitempty"`
87+
GasPrice Amount `json:"gasPrice,omitempty"`
88+
}
89+
90+
// Amount returns the total amount of the transaction.
91+
func (tx *Tx) Amount(address string, isERC20 bool) coin.Amount {
92+
if isERC20 {
93+
for _, transfer := range tx.TokenTransfers {
94+
if transfer.Type == "ERC20" {
95+
if transfer.To == address || transfer.From == address {
96+
return coin.NewAmount(transfer.Value.Int)
97+
}
98+
}
99+
}
100+
}
101+
return coin.NewAmount(tx.ValueOutSat.Int)
102+
}
103+
104+
// Addresses returns the receiving address of the transaction.
105+
func (tx *Tx) Addresses(isERC20 bool) ([]accounts.AddressAndAmount, error) {
106+
var address string
107+
switch {
108+
case isERC20:
109+
address = tx.TokenTransfers[0].To
110+
case len(tx.Vout) > 0:
111+
address = tx.Vout[0].Addresses[0]
112+
default:
113+
return nil, errp.New("transaction has no outputs or token transfers")
114+
}
115+
116+
return []accounts.AddressAndAmount{{
117+
Address: address,
118+
Amount: tx.Amount(address, isERC20),
119+
}}, nil
120+
}
121+
122+
// Status returns the status of the transaction.
123+
func (tx *Tx) Status() (accounts.TxStatus, error) {
124+
switch tx.EthereumSpecific.Status {
125+
case TxStatusPending:
126+
return accounts.TxStatusPending, nil
127+
case TxStatusOK:
128+
return accounts.TxStatusComplete, nil
129+
case TxStatusFailure:
130+
return accounts.TxStatusFailed, nil
131+
default:
132+
// This should never happen
133+
return "", errp.Newf("unknown transaction status %d", tx.EthereumSpecific.Status)
134+
}
135+
}

0 commit comments

Comments
 (0)