Skip to content

Commit 3776e83

Browse files
committed
backend: add Transactions implementation to blockbook.
1 parent 52ee531 commit 3776e83

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
@@ -23,15 +23,19 @@ import (
2323
"net/http"
2424
"net/url"
2525
"path"
26+
"time"
2627

2728
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
29+
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
2830
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/erc20"
2931
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/rpcclient"
3032
ethtypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/types"
3133
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
34+
"github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
3235
"github.com/ethereum/go-ethereum"
3336
"github.com/ethereum/go-ethereum/common"
3437
"github.com/ethereum/go-ethereum/core/types"
38+
"github.com/sirupsen/logrus"
3539
"golang.org/x/time/rate"
3640
)
3741

@@ -45,6 +49,8 @@ type Blockbook struct {
4549
url string
4650
httpClient *http.Client
4751
limiter *rate.Limiter
52+
// TODO remove before merging into master?
53+
log *logrus.Entry
4854
}
4955

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

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

8391
return nil
8492
}
8593

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

9097
if err := blockbook.call(ctx, path.Join("address", address), params, result); err != nil {
@@ -100,7 +107,7 @@ func (blockbook *Blockbook) Balance(ctx context.Context, account common.Address)
100107
Balance string `json:"balance"`
101108
}{}
102109

103-
if err := blockbook.address(ctx, account, &result); err != nil {
110+
if err := blockbook.address(ctx, account, url.Values{}, &result); err != nil {
104111
return nil, errp.WithStack(err)
105112
}
106113

@@ -122,9 +129,114 @@ func (blockbook *Blockbook) PendingNonceAt(ctx context.Context, account common.A
122129
return 0, fmt.Errorf("Not yet implemented")
123130
}
124131

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

130242
// 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)