github.com/decred/politeia@v1.4.0/politeiawww/legacy/dcrdata.go (about) 1 // Copyright (c) 2017-2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package legacy 6 7 import ( 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "sort" 14 "strconv" 15 "strings" 16 "time" 17 18 "github.com/decred/dcrd/chaincfg/v3" 19 "github.com/decred/dcrd/dcrutil/v3" 20 "github.com/decred/politeia/util" 21 ) 22 23 const ( 24 dcrdataTimeout = 3 * time.Second // Dcrdata request timeout 25 ) 26 27 func (p *Politeiawww) dcrdataHostHTTP() string { 28 return fmt.Sprintf("https://%v/api", p.cfg.DcrdataHost) 29 } 30 31 func (p *Politeiawww) dcrdataHostWS() string { 32 return fmt.Sprintf("wss://%v/ps", p.cfg.DcrdataHost) 33 } 34 35 // BETransaction is an object representing a transaction; it's 36 // part of the data returned from the URL for the block explorer 37 // when fetching the transactions for an address. 38 type BETransaction struct { 39 TxId string `json:"txid"` // Transaction id 40 Vin []BETransactionVin `json:"vin"` // Transaction inputs 41 Vout []BETransactionVout `json:"vout"` // Transaction outputs 42 Confirmations uint64 `json:"confirmations"` // Number of confirmations 43 Timestamp int64 `json:"time"` // Transaction timestamp 44 } 45 46 // BETransactionVin holds the transaction prevOut address information 47 type BETransactionVin struct { 48 PrevOut BETransactionPrevOut `json:"prevOut"` // Previous transaction output 49 } 50 51 // BETransactionPrevOut holds the information about the inputs' previous addresses. 52 // This will allow one to check for dev subsidy origination, etc. 53 type BETransactionPrevOut struct { 54 Addresses []string `json:"addresses"` // Array of transaction input addresses 55 } 56 57 // BETransactionVout holds the transaction amount information. 58 type BETransactionVout struct { 59 Amount json.Number `json:"value"` // Transaction amount (in DCR) 60 ScriptPubkey BETransactionScriptPubkey `json:"scriptPubkey"` // Transaction script info 61 } 62 63 // BETransactionScriptPubkey holds the script info for a 64 // transaction. 65 type BETransactionScriptPubkey struct { 66 Addresses []string `json:"addresses"` // Array of transaction input addresses 67 } 68 69 // TxDetails is an object representing a transaction. 70 // XXX This was previously being used to standardize the different responses 71 // from the dcrdata and insight APIs. Support for the insight API was removed 72 // but parts of politeiawww still consume this struct so it has remained. 73 type TxDetails struct { 74 Address string // Transaction address 75 TxID string // Transacion ID 76 Amount uint64 // Transaction amount (in atoms) 77 Timestamp int64 // Transaction timestamp 78 Confirmations uint64 // Number of confirmations 79 InputAddresses []string /// An array of all addresses from previous outputs 80 } 81 82 func makeRequest(ctx context.Context, url string, timeout time.Duration) ([]byte, error) { 83 req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 84 if err != nil { 85 return nil, fmt.Errorf("unable to create request: %v", err) 86 } 87 88 client := &http.Client{ 89 Timeout: timeout * time.Second, 90 } 91 response, err := client.Do(req) 92 if err != nil { 93 return nil, err 94 } 95 defer response.Body.Close() 96 97 if response.StatusCode != http.StatusOK { 98 body, err := io.ReadAll(response.Body) 99 if err != nil { 100 return nil, fmt.Errorf("dcrdata error: %v %v %v", 101 response.StatusCode, url, err) 102 } 103 return nil, fmt.Errorf("dcrdata error: %v %v %s", 104 response.StatusCode, url, body) 105 } 106 107 return io.ReadAll(response.Body) 108 } 109 110 func fetchTxWithBE(ctx context.Context, url string, address string, minimumAmount uint64, txnotbefore int64, minConfirmationsRequired uint64) (string, uint64, error) { 111 responseBody, err := makeRequest(ctx, url, dcrdataTimeout) 112 if err != nil { 113 return "", 0, err 114 } 115 116 transactions := make([]BETransaction, 0) 117 err = json.Unmarshal(responseBody, &transactions) 118 if err != nil { 119 return "", 0, err 120 } 121 122 for _, v := range transactions { 123 if v.Timestamp < txnotbefore { 124 continue 125 } 126 if v.Confirmations < minConfirmationsRequired { 127 continue 128 } 129 130 for _, vout := range v.Vout { 131 amount, err := util.DcrStringToAtoms(vout.Amount.String()) 132 if err != nil { 133 return "", 0, err 134 } 135 136 if amount < minimumAmount { 137 continue 138 } 139 140 for _, addr := range vout.ScriptPubkey.Addresses { 141 if address == addr { 142 return v.TxId, amount, nil 143 } 144 } 145 } 146 } 147 148 return "", 0, nil 149 } 150 151 // fetchTxWithBlockExplorers uses public block explorers to look for a 152 // transaction for the given address that equals or exceeds the given amount, 153 // occurs after the txnotbefore time and has the minimum number of confirmations. 154 func fetchTxWithBlockExplorers(ctx context.Context, params *chaincfg.Params, address string, amount uint64, txnotbefore int64, minConfirmations uint64, dcrdataURL string) (string, uint64, error) { 155 // pre-validate that the passed address, amount, and tx are at least 156 // somewhat valid before querying the explorers 157 addr, err := dcrutil.DecodeAddress(address, params) 158 if err != nil { 159 return "", 0, fmt.Errorf("invalid address %v: %v", addr, err) 160 } 161 162 // Construct proper dcrdata url 163 dcrdataURL += "/address/" + address 164 165 explorerURL := dcrdataURL + "/raw" 166 167 // Fetch transaction from dcrdata 168 txID, amount, err := fetchTxWithBE(ctx, explorerURL, address, amount, 169 txnotbefore, minConfirmations) 170 if err != nil { 171 return "", 0, fmt.Errorf("failed to fetch from dcrdata: %v", err) 172 } 173 174 return txID, amount, nil 175 } 176 177 func fetchTxsWithBE(ctx context.Context, url string) ([]BETransaction, error) { 178 responseBody, err := makeRequest(ctx, url, dcrdataTimeout) 179 if err != nil { 180 return nil, err 181 } 182 183 transactions := make([]BETransaction, 0) 184 err = json.Unmarshal(responseBody, &transactions) 185 if err != nil { 186 return nil, fmt.Errorf("Unmarshal []BETransaction: %v", err) 187 } 188 189 return transactions, nil 190 } 191 192 func convertBETransactionToTxDetails(address string, tx BETransaction) (*TxDetails, error) { 193 var amount uint64 194 for _, vout := range tx.Vout { 195 amt, err := util.DcrStringToAtoms(vout.Amount.String()) 196 if err != nil { 197 return nil, err 198 } 199 200 for _, addr := range vout.ScriptPubkey.Addresses { 201 if address == addr { 202 amount += amt 203 } 204 } 205 } 206 inputAddresses := make([]string, 0, 1064) 207 for _, vin := range tx.Vin { 208 inputAddresses = append(inputAddresses, vin.PrevOut.Addresses...) 209 } 210 211 return &TxDetails{ 212 Address: address, 213 TxID: tx.TxId, 214 Amount: amount, 215 Confirmations: tx.Confirmations, 216 Timestamp: tx.Timestamp, 217 InputAddresses: inputAddresses, 218 }, nil 219 } 220 221 // fetchTxsForAddress fetches the transactions that have been sent to the 222 // provided wallet address from the dcrdata block explorer 223 func fetchTxsForAddress(ctx context.Context, params *chaincfg.Params, address string, dcrdataURL string) ([]TxDetails, error) { 224 // Get block explorer URL 225 addr, err := dcrutil.DecodeAddress(address, params) 226 if err != nil { 227 return nil, fmt.Errorf("invalid address %v: %v", addr, err) 228 } 229 230 // Construct proper dcrdata url 231 dcrdataURL += "/address/" + address 232 233 explorerURL := dcrdataURL + "/raw" 234 235 // Fetch using dcrdata block explorer 236 dcrdataTxs, err := fetchTxsWithBE(ctx, explorerURL) 237 if err != nil { 238 return nil, fmt.Errorf("failed to fetch from dcrdata: %v", err) 239 } 240 txs := make([]TxDetails, 0, len(dcrdataTxs)) 241 for _, tx := range dcrdataTxs { 242 txDetail, err := convertBETransactionToTxDetails(address, tx) 243 if err != nil { 244 return nil, fmt.Errorf("convertBETransactionToTxDetails: %v", 245 tx.TxId) 246 } 247 txs = append(txs, *txDetail) 248 } 249 return txs, nil 250 } 251 252 // fetchTxsForAddressNotBefore fetches all transactions for a wallet address 253 // that occurred after the passed in notBefore timestamp. 254 func fetchTxsForAddressNotBefore(ctx context.Context, params *chaincfg.Params, address string, notBefore int64, dcrdataURL string) ([]TxDetails, error) { 255 // Get block explorer URL 256 addr, err := dcrutil.DecodeAddress(address, params) 257 if err != nil { 258 return nil, fmt.Errorf("invalid address %v: %v", addr, err) 259 } 260 261 // Construct proper dcrdata url 262 dcrdataURL += "/address/" + address 263 264 // Fetch all txs for the passed in wallet address 265 // that were sent after the notBefore timestamp 266 var ( 267 targetTxs []TxDetails 268 count = 10 269 skip = 0 270 done = false 271 ) 272 for !done { 273 // Fetch a page of user payment txs 274 url := dcrdataURL + "/count/" + strconv.Itoa(count) + 275 "/skip/" + strconv.Itoa(skip) + "/raw" 276 dcrdataTxs, err := fetchTxsWithBE(ctx, url) 277 if err != nil { 278 return nil, fmt.Errorf("fetchDcrdataAddress: %v", err) 279 } 280 // Convert transactions to TxDetails 281 txs := make([]TxDetails, len(dcrdataTxs)) 282 for _, tx := range dcrdataTxs { 283 txDetails, err := convertBETransactionToTxDetails(address, tx) 284 if err != nil { 285 return nil, fmt.Errorf("convertBETransactionToTxDetails: %v", 286 tx.TxId) 287 } 288 txs = append(txs, *txDetails) 289 } 290 if len(txs) == 0 { 291 done = true 292 continue 293 } 294 // Sanity check. Txs should already be sorted 295 // in reverse chronological order. 296 sort.SliceStable(txs, func(i, j int) bool { 297 return txs[i].Timestamp > txs[j].Timestamp 298 }) 299 300 // Verify txs are within notBefore limit 301 for _, tx := range txs { 302 if tx.Timestamp > notBefore { 303 targetTxs = append(targetTxs, tx) 304 } else { 305 // We have reached the notBefore 306 // limit; stop requesting txs 307 done = true 308 break 309 } 310 } 311 312 skip += count 313 } 314 315 return targetTxs, nil 316 } 317 318 // fetchTx fetches a given transaction based on the provided txid. 319 func fetchTx(ctx context.Context, params *chaincfg.Params, address, txid, dcrdataURL string) (*TxDetails, error) { 320 // Get block explorer URLs 321 addr, err := dcrutil.DecodeAddress(address, params) 322 if err != nil { 323 return nil, fmt.Errorf("invalid address %v: %v", addr, err) 324 } 325 326 // Construct proper dcrdata url} 327 dcrdataURL += "/address/" + address 328 329 primaryURL := dcrdataURL + "/raw" 330 331 log.Debugf("fetching tx %s %s from primary %s\n", address, txid, primaryURL) 332 // Try the primary (dcrdata) 333 primaryTxs, err := fetchTxsWithBE(ctx, primaryURL) 334 if err != nil { 335 return nil, fmt.Errorf("failed to fetch from dcrdata: %v", err) 336 } 337 for _, tx := range primaryTxs { 338 if strings.TrimSpace(tx.TxId) != strings.TrimSpace(txid) { 339 continue 340 } 341 txDetail, err := convertBETransactionToTxDetails(address, tx) 342 if err != nil { 343 return nil, fmt.Errorf("convertBETransactionToTxDetails: %v", 344 tx.TxId) 345 } 346 return txDetail, nil 347 } 348 return nil, nil 349 }