use trueblocks api to grab transactions

This commit is contained in:
julian 2023-03-23 12:19:51 -06:00
parent be11b18eb8
commit 40cceed8e6
4 changed files with 376 additions and 140 deletions

View file

@ -1,82 +1,152 @@
import 'package:stackwallet/utilities/logger.dart';
/// address : "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"
/// blockNumber : 16484149
/// logIndex : 61
/// topics : ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x0000000000000000000000003a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","0x000000000000000000000000c5e81fc2401b8104966637d5334cbce92f01dbf7"]
/// data : "0x0000000000000000000000000000000000000000000000002dac1c4be587d800"
/// articulatedLog : {"name":"Transfer","inputs":{"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"}}
/// compressedLog : "{name:Transfer|inputs:{_amount:3291036540000000000|_from:0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597|_to:0xc5e81fc2401b8104966637d5334cbce92f01dbf7}}"
/// transactionHash : "0x5b59559a77fa5f1c70528d41f4fa2e5fa5a00b21fc2f3bc26b208b3062e46333"
/// transactionIndex : 25
class EthTokenTxDTO {
final String blockHash;
final int blockNumber;
final int confirmations;
final String contractAddress;
final int cumulativeGasUsed;
final String from;
final int gas;
final BigInt gasPrice;
final int gasUsed;
final String hash;
final String input;
final int logIndex;
final int nonce;
final int timeStamp;
final String to;
final int tokenDecimal;
final String tokenName;
final String tokenSymbol;
final int transactionIndex;
final BigInt value;
EthTokenTxDTO({
required this.blockHash,
class EthTokenTxDto {
EthTokenTxDto({
required this.address,
required this.blockNumber,
required this.confirmations,
required this.contractAddress,
required this.cumulativeGasUsed,
required this.from,
required this.gas,
required this.gasPrice,
required this.gasUsed,
required this.hash,
required this.input,
required this.logIndex,
required this.nonce,
required this.timeStamp,
required this.to,
required this.tokenDecimal,
required this.tokenName,
required this.tokenSymbol,
required this.topics,
required this.data,
required this.articulatedLog,
required this.compressedLog,
required this.transactionHash,
required this.transactionIndex,
required this.value,
});
factory EthTokenTxDTO.fromMap({
required Map<String, dynamic> map,
}) {
try {
return EthTokenTxDTO(
blockHash: map["blockHash"] as String,
blockNumber: int.parse(map["blockNumber"] as String),
confirmations: int.parse(map["confirmations"] as String),
contractAddress: map["contractAddress"] as String,
cumulativeGasUsed: int.parse(map["cumulativeGasUsed"] as String),
from: map["from"] as String,
gas: int.parse(map["gas"] as String),
gasPrice: BigInt.parse(map["gasPrice"] as String),
gasUsed: int.parse(map["gasUsed"] as String),
hash: map["hash"] as String,
input: map["input"] as String,
logIndex: int.parse(map["logIndex"] as String? ?? "-1"),
nonce: int.parse(map["nonce"] as String),
timeStamp: int.parse(map["timeStamp"] as String),
to: map["to"] as String,
tokenDecimal: int.parse(map["tokenDecimal"] as String),
tokenName: map["tokenName"] as String,
tokenSymbol: map["tokenSymbol"] as String,
transactionIndex: int.parse(map["transactionIndex"] as String),
value: BigInt.parse(map["value"] as String),
EthTokenTxDto.fromMap(Map json)
: address = json['address'] as String,
blockNumber = json['blockNumber'] as int,
logIndex = json['logIndex'] as int,
topics = List<String>.from(json['topics'] as List),
data = json['data'] as String,
articulatedLog = ArticulatedLog.fromJson(json['articulatedLog']),
compressedLog = json['compressedLog'] as String,
transactionHash = json['transactionHash'] as String,
transactionIndex = json['transactionIndex'] as int;
final String address;
final int blockNumber;
final int logIndex;
final List<String> topics;
final String data;
final ArticulatedLog articulatedLog;
final String compressedLog;
final String transactionHash;
final int transactionIndex;
EthTokenTxDto copyWith({
String? address,
int? blockNumber,
int? logIndex,
List<String>? topics,
String? data,
ArticulatedLog? articulatedLog,
String? compressedLog,
String? transactionHash,
int? transactionIndex,
}) =>
EthTokenTxDto(
address: address ?? this.address,
blockNumber: blockNumber ?? this.blockNumber,
logIndex: logIndex ?? this.logIndex,
topics: topics ?? this.topics,
data: data ?? this.data,
articulatedLog: articulatedLog ?? this.articulatedLog,
compressedLog: compressedLog ?? this.compressedLog,
transactionHash: transactionHash ?? this.transactionHash,
transactionIndex: transactionIndex ?? this.transactionIndex,
);
} catch (e, s) {
Logging.instance.log(
"EthTokenTxDTO.fromMap() failed: $e\n$s",
level: LogLevel.Fatal,
);
rethrow;
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['address'] = address;
map['blockNumber'] = blockNumber;
map['logIndex'] = logIndex;
map['topics'] = topics;
map['data'] = data;
map['articulatedLog'] = articulatedLog.toJson();
map['compressedLog'] = compressedLog;
map['transactionHash'] = transactionHash;
map['transactionIndex'] = transactionIndex;
return map;
}
}
/// name : "Transfer"
/// inputs : {"_amount":"3291036540000000000","_from":"0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597","_to":"0xc5e81fc2401b8104966637d5334cbce92f01dbf7"}
class ArticulatedLog {
ArticulatedLog({
required this.name,
required this.inputs,
});
ArticulatedLog.fromJson(dynamic json)
: name = json['name'] as String,
inputs = Inputs.fromJson(json['inputs']);
final String name;
final Inputs inputs;
ArticulatedLog copyWith({
String? name,
Inputs? inputs,
}) =>
ArticulatedLog(
name: name ?? this.name,
inputs: inputs ?? this.inputs,
);
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['name'] = name;
map['inputs'] = inputs.toJson();
return map;
}
}
/// _amount : "3291036540000000000"
/// _from : "0x3a5cc8689d1b0cef2c317bc5c0ad6ce88b27d597"
/// _to : "0xc5e81fc2401b8104966637d5334cbce92f01dbf7"
///
class Inputs {
Inputs({
required this.amount,
required this.from,
required this.to,
});
Inputs.fromJson(dynamic json)
: amount = json['_amount'] as String,
from = json['_from'] as String,
to = json['_to'] as String;
final String amount;
final String from;
final String to;
Inputs copyWith({
String? amount,
String? from,
String? to,
}) =>
Inputs(
amount: amount ?? this.amount,
from: from ?? this.from,
to: to ?? this.to,
);
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['_amount'] = amount;
map['_from'] = from;
map['_to'] = to;
return map;
}
}

View file

@ -0,0 +1,115 @@
import 'dart:convert';
class EthTokenTxExtraDTO {
EthTokenTxExtraDTO({
required this.blockHash,
required this.blockNumber,
required this.from,
required this.gas,
required this.gasCost,
required this.gasPrice,
required this.gasUsed,
required this.hash,
required this.input,
required this.nonce,
required this.timestamp,
required this.to,
required this.transactionIndex,
required this.value,
});
factory EthTokenTxExtraDTO.fromMap(Map<String, dynamic> map) =>
EthTokenTxExtraDTO(
hash: map['hash'] as String,
blockHash: map['blockHash'] as String,
blockNumber: map['blockNumber'] as int,
transactionIndex: map['transactionIndex'] as int,
timestamp: map['timestamp'] as int,
from: map['from'] as String,
to: map['to'] as String,
value: int.parse(map['value'] as String),
gas: map['gas'] as int,
gasPrice: map['gasPrice'] as int,
nonce: map['nonce'] as int,
input: map['input'] as String,
gasCost: map['gasCost'] as int,
gasUsed: map['gasUsed'] as int,
);
factory EthTokenTxExtraDTO.fromJsonString(String jsonString) =>
EthTokenTxExtraDTO.fromMap(
Map<String, dynamic>.from(
jsonDecode(jsonString) as Map,
),
);
final String hash;
final String blockHash;
final int blockNumber;
final int transactionIndex;
final int timestamp;
final String from;
final String to;
final int value;
final int gas;
final int gasPrice;
final String input;
final int nonce;
final int gasCost;
final int gasUsed;
EthTokenTxExtraDTO copyWith({
String? hash,
String? blockHash,
int? blockNumber,
int? transactionIndex,
int? timestamp,
String? from,
String? to,
int? value,
int? gas,
int? gasPrice,
int? nonce,
String? input,
int? gasCost,
int? gasUsed,
}) =>
EthTokenTxExtraDTO(
hash: hash ?? this.hash,
blockHash: blockHash ?? this.blockHash,
blockNumber: blockNumber ?? this.blockNumber,
transactionIndex: transactionIndex ?? this.transactionIndex,
timestamp: timestamp ?? this.timestamp,
from: from ?? this.from,
to: to ?? this.to,
value: value ?? this.value,
gas: gas ?? this.gas,
gasPrice: gasPrice ?? this.gasPrice,
nonce: nonce ?? this.nonce,
input: input ?? this.input,
gasCost: gasCost ?? this.gasCost,
gasUsed: gasUsed ?? this.gasUsed,
);
Map<String, dynamic> toMap() {
final map = <String, dynamic>{};
map['hash'] = hash;
map['blockHash'] = blockHash;
map['blockNumber'] = blockNumber;
map['transactionIndex'] = transactionIndex;
map['timestamp'] = timestamp;
map['from'] = from;
map['to'] = to;
map['value'] = value;
map['gas'] = gas;
map['gasPrice'] = gasPrice;
map['input'] = input;
map['nonce'] = nonce;
map['gasCost'] = gasCost;
map['gasUsed'] = gasUsed;
return map;
}
@override
String toString() => jsonEncode(toMap());
}

View file

@ -4,12 +4,12 @@ import 'dart:math';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_tx_dto.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/utilities/default_nodes.dart';
import 'package:stackwallet/utilities/eth_commons.dart';
import 'package:stackwallet/utilities/extensions/extensions.dart';
import 'package:stackwallet/utilities/logger.dart';
class EthApiException with Exception {
@ -36,9 +36,8 @@ abstract class EthereumAPI {
static String stackURI = "$stackBaseServer/eth/mainnet/api";
// static const blockScout = "https://blockscout.com/eth/mainnet/api";
static const etherscanApi =
"https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update
// static const etherscanApi =
// "https://api.etherscan.io/api"; //TODO - Once our server has abi functionality update
static const gasTrackerUrl =
"https://blockscout.com/eth/mainnet/api/v1/gas-price-oracle";
@ -48,17 +47,10 @@ abstract class EthereumAPI {
try {
final response = await get(
Uri.parse(
// "$blockScout?module=account&action=txlist&address=$address"));
// "stackURI?module=account&action=txlist&address=$address"));
"$stackBaseServer/export?addrs=$address",
),
);
// "$etherscanApi?module=account&action=txlist&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP"));
final BigInt myReceivingAddressInt = address.toBigIntFromHex;
// print(response.body);
if (response.statusCode == 200) {
if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map;
@ -68,23 +60,6 @@ abstract class EthereumAPI {
for (final map in list!) {
final txn = EthTxDTO.fromMap(Map<String, dynamic>.from(map as Map));
final logs = map["receipt"]["logs"] as List;
for (final log in logs) {
final map = log as Map;
final contractAddress = map["address"] as String;
final topics = List<String>.from(map["topics"] as List);
for (int i = 0; i < topics.length; i++) {
if (topics[i].toBigIntFromHex == myReceivingAddressInt) {
print("================================================");
print("Contract: $contractAddress");
Logger.print((log).toString(), normalLength: false);
}
}
}
if (txn.hasToken == 0) {
txns.add(txn);
}
@ -112,7 +87,7 @@ abstract class EthereumAPI {
);
} catch (e, s) {
Logging.instance.log(
"getEthTransactions(): $e\n$s",
"getEthTransactions($address): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(
@ -122,44 +97,41 @@ abstract class EthereumAPI {
}
}
static Future<EthereumResponse<List<EthTokenTxDTO>>> getTokenTransactions({
required String address,
String? contractAddress,
int? startBlock,
int? endBlock,
// todo add more params?
}) async {
static Future<EthereumResponse<List<EthTokenTxExtraDTO>>>
getEthTokenTransactionsByTxids(List<String> txids) async {
try {
String uriString =
// "$blockScout?module=account&action=tokentx&address=$address";
// "stackURI?module=account&action=tokentx&address=$address";
"$etherscanApi?module=account&action=tokentx&address=$address&apikey=EG6J7RJIQVSTP2BS59D3TY2G55YHS5F2HP";
if (contractAddress != null) {
uriString += "&contractAddress=$contractAddress";
}
final uri = Uri.parse(uriString);
final response = await get(uri);
final response = await get(
Uri.parse(
"$stackBaseServer/transactions?transactions=${txids.join(" ")}",
),
);
if (response.statusCode == 200) {
final json = jsonDecode(response.body);
if (json["message"] == "OK") {
final result =
List<Map<String, dynamic>>.from(json["result"] as List);
final List<EthTokenTxDTO> tokenTxns = [];
for (final map in result) {
tokenTxns.add(EthTokenTxDTO.fromMap(map: map));
}
if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map;
final list = json["data"] as List?;
final List<EthTokenTxExtraDTO> txns = [];
for (final map in list!) {
final txn = EthTokenTxExtraDTO.fromMap(
Map<String, dynamic>.from(map as Map),
);
txns.add(txn);
}
return EthereumResponse(
tokenTxns,
txns,
null,
);
} else {
throw EthApiException(json["message"] as String);
throw EthApiException(
"getEthTransaction($txids) response is empty but status code is "
"${response.statusCode}",
);
}
} else {
throw EthApiException(
"getTokenTransactions($address) failed with status code: "
"getEthTransaction($txids) failed with status code: "
"${response.statusCode}",
);
}
@ -170,7 +142,63 @@ abstract class EthereumAPI {
);
} catch (e, s) {
Logging.instance.log(
"getTokenTransactions(): $e\n$s",
"getEthTransaction($txids): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(
null,
EthApiException(e.toString()),
);
}
}
static Future<EthereumResponse<List<EthTokenTxDto>>> getTokenTransactions({
required String address,
required String tokenContractAddress,
}) async {
try {
final response = await get(
Uri.parse(
"$stackBaseServer/export?addrs=$address&emitter=$tokenContractAddress&logs=true",
),
);
if (response.statusCode == 200) {
if (response.body.isNotEmpty) {
final json = jsonDecode(response.body) as Map;
final list = json["data"] as List?;
final List<EthTokenTxDto> txns = [];
for (final map in list!) {
final txn =
EthTokenTxDto.fromMap(Map<String, dynamic>.from(map as Map));
txns.add(txn);
}
return EthereumResponse(
txns,
null,
);
} else {
throw EthApiException(
"getTokenTransactions($address, $tokenContractAddress) response is empty but status code is "
"${response.statusCode}",
);
}
} else {
throw EthApiException(
"getTokenTransactions($address, $tokenContractAddress) failed with status code: "
"${response.statusCode}",
);
}
} on EthApiException catch (e) {
return EthereumResponse(
null,
e,
);
} catch (e, s) {
Logging.instance.log(
"getTokenTransactions($address, $tokenContractAddress): $e\n$s",
level: LogLevel.Error,
);
return EthereumResponse(

View file

@ -6,6 +6,8 @@ import 'package:flutter/widgets.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_dto.dart';
import 'package:stackwallet/dto/ethereum/eth_token_tx_extra_dto.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
@ -282,23 +284,44 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
.findAll();
Future<void> _refreshTransactions() async {
String addressString = await currentReceivingAddress;
String addressString =
checksumEthereumAddress(await currentReceivingAddress);
final response = await EthereumAPI.getTokenTransactions(
address: addressString,
contractAddress: tokenContract.address,
tokenContractAddress: tokenContract.address,
);
if (response.value == null) {
throw response.exception ??
Exception("Failed to fetch token transaction data");
}
final response2 = await EthereumAPI.getEthTokenTransactionsByTxids(
response.value!.map((e) => e.transactionHash).toList(),
);
if (response2.value == null) {
throw response2.exception ??
Exception("Failed to fetch token transactions");
}
final List<Tuple2<EthTokenTxDto, EthTokenTxExtraDTO>> data = [];
for (final tokenDto in response.value!) {
data.add(
Tuple2(
tokenDto,
response2.value!.firstWhere(
(e) => e.hash == tokenDto.transactionHash,
),
),
);
}
final List<Tuple2<Transaction, Address?>> txnsData = [];
for (final tx in response.value!) {
for (final tuple in data) {
bool isIncoming;
if (checksumEthereumAddress(tx.from) == addressString) {
if (checksumEthereumAddress(tuple.item1.articulatedLog.inputs.from) ==
addressString) {
isIncoming = false;
} else {
isIncoming = true;
@ -306,17 +329,17 @@ class EthTokenWallet extends ChangeNotifier with EthTokenCache {
final txn = Transaction(
walletId: ethWallet.walletId,
txid: tx.hash,
timestamp: tx.timeStamp,
txid: tuple.item1.transactionHash,
timestamp: tuple.item2.timestamp,
type: isIncoming ? TransactionType.incoming : TransactionType.outgoing,
subType: TransactionSubType.ethToken,
amount: tx.value.toInt(),
fee: tx.gasUsed * tx.gasPrice.toInt(),
height: tx.blockNumber,
amount: int.parse(tuple.item1.articulatedLog.inputs.amount),
fee: tuple.item2.gasUsed * tuple.item2.gasPrice.toInt(),
height: tuple.item1.blockNumber,
isCancelled: false,
isLelantus: false,
slateId: null,
otherData: tx.contractAddress,
otherData: tuple.item1.address,
inputs: [],
outputs: [],
);