@@ -22,15 +22,19 @@ import (
22
22
"math/big"
23
23
"net/http"
24
24
"net/url"
25
+ "time"
25
26
26
27
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/accounts"
28
+ "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin"
27
29
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/erc20"
28
30
"github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/rpcclient"
29
31
ethtypes "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/eth/types"
30
32
"github.com/BitBoxSwiss/bitbox-wallet-app/util/errp"
33
+ "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging"
31
34
"github.com/ethereum/go-ethereum"
32
35
"github.com/ethereum/go-ethereum/common"
33
36
"github.com/ethereum/go-ethereum/core/types"
37
+ "github.com/sirupsen/logrus"
34
38
"golang.org/x/time/rate"
35
39
)
36
40
@@ -44,6 +48,8 @@ type Blockbook struct {
44
48
url string
45
49
httpClient * http.Client
46
50
limiter * rate.Limiter
51
+ // TODO remove before merging into master?
52
+ log * logrus.Entry
47
53
}
48
54
49
55
// NewBlockbook creates a new instance of EtherScan.
@@ -55,6 +61,7 @@ func NewBlockbook(chainId string, httpClient *http.Client) *Blockbook {
55
61
url : "https://bb1.shiftcrypto.io/api/" ,
56
62
httpClient : httpClient ,
57
63
limiter : rate .NewLimiter (rate .Limit (callsPerSec ), 1 ),
64
+ log : logging .Get ().WithField ("ETH Client" , "Blockbook" ),
58
65
}
59
66
}
60
67
@@ -77,15 +84,15 @@ func (blockbook *Blockbook) call(ctx context.Context, handler string, params url
77
84
if err != nil {
78
85
return errp .WithStack (err )
79
86
}
87
+
80
88
if err := json .Unmarshal (body , result ); err != nil {
81
89
return errp .Newf ("unexpected response from blockbook: %s" , string (body ))
82
90
}
83
91
84
92
return nil
85
93
}
86
94
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 {
89
96
address := account .Hex ()
90
97
91
98
addressPath := fmt .Sprintf ("address/%s" , address )
@@ -103,7 +110,7 @@ func (blockbook *Blockbook) Balance(ctx context.Context, account common.Address)
103
110
Balance string `json:"balance"`
104
111
}{}
105
112
106
- if err := blockbook .address (ctx , account , & result ); err != nil {
113
+ if err := blockbook .address (ctx , account , url. Values {}, & result ); err != nil {
107
114
return nil , errp .WithStack (err )
108
115
}
109
116
@@ -136,9 +143,114 @@ func (blockbook *Blockbook) PendingNonceAt(ctx context.Context, account common.A
136
143
return 0 , fmt .Errorf ("Not yet implemented" )
137
144
}
138
145
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
+
139
227
// Transactions implement TransactionSource.
140
228
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
+
142
254
}
143
255
144
256
// SendTransaction implements rpc.Interface.
@@ -155,8 +267,7 @@ func (blockbook *Blockbook) ERC20Balance(account common.Address, erc20Token *erc
155
267
} `json:"tokens"`
156
268
}{}
157
269
158
- // TODO why is there no context in the signature of this interface method?
159
- if err := blockbook .address (context .Background (), account , & result ); err != nil {
270
+ if err := blockbook .address (context .Background (), account , url.Values {}, & result ); err != nil {
160
271
return nil , errp .WithStack (err )
161
272
}
162
273
0 commit comments