diff --git a/lib/src/crypto/evm/entities/abi/erc/erc1155.dart b/lib/src/crypto/evm/entities/abi/erc/erc1155.dart new file mode 100644 index 000000000..9970b9e7f --- /dev/null +++ b/lib/src/crypto/evm/entities/abi/erc/erc1155.dart @@ -0,0 +1,385 @@ +import 'dart:typed_data'; + +import 'package:walletkit_dart/walletkit_dart.dart'; + +final contractAbiErc1155 = ContractABI.fromAbi('''[ + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "amounts", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + } +] + +'''); + +class ERC1155Contract extends InternalContract { + ERC1155Contract({ + required super.contractAddress, + required super.rpc, + }) : super( + abi: contractAbiErc1155, + ); + + Future balanceOf({ + required String address, + required BigInt tokenID, + }) async { + final function = abi.functions[0]; + final response = await readSafe( + function: function.addValues(values: [address, tokenID]), + ); + return response.outputs.first.castValue(); + } + + Future> balanceOfBatch({ + required List accounts, + required List tokenIDs, + }) async { + if (accounts.length != tokenIDs.length) { + throw ArgumentError('accounts and tokenIDs must have the same length'); + } + final function = abi.functions[1]; + final response = await readSafe( + function: function.addValues(values: [accounts, tokenIDs]), + ); + return response.outputs.first.castValue>(); + } + + Future getUri({ + required BigInt tokenID, + }) async { + final function = abi.functions[7]; + final response = await readSafe( + function: function.addValues(values: [tokenID]), + ); + return response.outputs.first.castValue(); + } + + Future safeTransferFrom({ + required String sender, + required String to, + required BigInt tokenID, + required BigInt amount, + required Uint8List seed, + Uint8List? data, + EvmFeeInformation? feeInfo, + List? accessList, + }) async { + final function = abi.functions[4]; + return await interact( + function: function.addValues( + values: [sender, to, tokenID, amount, data ?? Uint8List(0)]), + sender: sender, + seed: seed, + feeInfo: feeInfo, + accessList: accessList, + ); + } +} diff --git a/lib/src/crypto/evm/entities/abi/erc20_contract.dart b/lib/src/crypto/evm/entities/abi/erc/erc20_contract.dart similarity index 100% rename from lib/src/crypto/evm/entities/abi/erc20_contract.dart rename to lib/src/crypto/evm/entities/abi/erc/erc20_contract.dart diff --git a/lib/src/crypto/evm/entities/abi/erc721_contract.dart b/lib/src/crypto/evm/entities/abi/erc/erc721_contract.dart similarity index 100% rename from lib/src/crypto/evm/entities/abi/erc721_contract.dart rename to lib/src/crypto/evm/entities/abi/erc/erc721_contract.dart diff --git a/lib/src/crypto/evm/entities/abi/gameItemsContract.dart b/lib/src/crypto/evm/entities/abi/gameItemsContract.dart new file mode 100644 index 000000000..f934a240a --- /dev/null +++ b/lib/src/crypto/evm/entities/abi/gameItemsContract.dart @@ -0,0 +1,490 @@ +import 'package:walletkit_dart/src/crypto/evm/entities/contract/contract_abi.dart'; + +final gameItemsContract = ContractABI.fromAbi(''' +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "tokenId", + "type": "uint256" + } + ], + "name": "ERC1155InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC1155InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "idsLength", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "valuesLength", + "type": "uint256" + } + ], + "name": "ERC1155InvalidArrayLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "ERC1155InvalidOperator", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC1155InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC1155InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC1155MissingApprovalForAll", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "ApprovalForAll", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "indexed": false, + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + } + ], + "name": "TransferBatch", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "TransferSingle", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "string", + "name": "value", + "type": "string" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "URI", + "type": "event" + }, + { + "inputs": [], + "name": "GOLD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SHIELD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SILVER", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SWORD", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "THORS_HAMMER", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "accounts", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + } + ], + "name": "balanceOfBatch", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + } + ], + "name": "isApprovedForAll", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "ids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "values", + "type": "uint256[]" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeBatchTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "id", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "safeTransferFrom", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bool", + "name": "approved", + "type": "bool" + } + ], + "name": "setApprovalForAll", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes4", + "name": "interfaceId", + "type": "bytes4" + } + ], + "name": "supportsInterface", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "uri", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } + ] +'''); diff --git a/lib/src/crypto/evm/entities/transactions/etherscan_transaction.dart b/lib/src/crypto/evm/entities/transactions/etherscan_transaction.dart index a7c867ab7..50bcc11f4 100644 --- a/lib/src/crypto/evm/entities/transactions/etherscan_transaction.dart +++ b/lib/src/crypto/evm/entities/transactions/etherscan_transaction.dart @@ -171,6 +171,142 @@ base class EtherscanTransaction extends EVMTransaction { throw UnsupportedError("Invalid JSON for EtherscanTransaction"); } + factory EtherscanTransaction.fromJsonErc1155( + Json json, { + required EvmCoinEntity currency, + required String address, + }) { + EtherscanTransaction _createTransaction({ + required String block_s, + required String timeStamp_s, + required String hash, + required String from, + required String to, + required String gas_s, + required String gasUsed_s, + required String gasPrice_s, + required String contractAddress, + required String? symbol, + required String? name, + required String tokenID_s, + required String input, + String? tokenValue, + }) { + final block = block_s.toIntOrNull ?? -1; + final confirmations = block_s.toIntOrNull ?? -1; + final timeMilli = timeStamp_s.toIntOrNull ?? -1; + final token = ERC1155Entity( + name: name ?? "N.Av", + symbol: symbol ?? "N.Av", + contractAddress: contractAddress, + chainID: currency.chainID, + tokenId: BigInt.tryParse(tokenID_s) ?? BigInt.from(-1), + ); + final amount = Amount( + value: tokenValue != null + ? (BigInt.tryParse(tokenValue) ?? BigInt.from(0)) + : BigInt.from(0), + decimals: 0, + ); + final fee = Amount( + value: gasPrice_s.toBigInt * gasUsed_s.toBigInt, + decimals: currency.decimals, + ); + final gasPrice = Amount( + value: gasPrice_s.toBigInt, + decimals: currency.decimals, + ); + final transferMethod = + TransactionTransferMethod.fromAddress(address, to, from); + + return EtherscanTransaction( + hash: hash, + block: block, + confirmations: confirmations, + timeMilli: timeMilli * 1000, + amount: amount, + fee: fee, + gasUsed: gasUsed_s.toInt, + sender: from, + recipient: to, + gas: gas_s.toInt, + gasPrice: gasPrice, + transferMethod: transferMethod, + token: token, + status: ConfirmationStatus.fromConfirmations(confirmations), + input: input.hexToBytesWithPrefixOrNull ?? Uint8List(0), + ); + } + + if (json + case { + 'blockNumber': String block_s, + 'timeStamp': String timeStamp_s, + 'hash': String hash, + 'from': String from, + 'to': String to, + 'tokenValue': String value_s, + 'gas': String gas_s, + 'gasUsed': String gasUsed_s, + 'gasPrice': String gasPrice_s, + 'contractAddress': String contractAddress, + 'tokenSymbol': String? symbol, + 'tokenName': String? name, + 'tokenID': String tokenID_s, + 'input': String? input, + }) { + return _createTransaction( + block_s: block_s, + timeStamp_s: timeStamp_s, + hash: hash, + from: from, + to: to, + gas_s: gas_s, + gasUsed_s: gasUsed_s, + gasPrice_s: gasPrice_s, + contractAddress: contractAddress, + symbol: symbol, + name: name, + tokenID_s: tokenID_s, + input: input ?? "", + tokenValue: value_s, + ); + } + if (json + case { + 'blockNumber': String block_s, + 'timeStamp': String timeStamp_s, + 'hash': String hash, + 'from': String from, + 'to': String to, + 'gas': String gas_s, + 'gasUsed': String gasUsed_s, + 'gasPrice': String gasPrice_s, + 'contractAddress': String contractAddress, + 'tokenSymbol': String? symbol, + 'tokenName': String? name, + 'tokenID': String tokenID_s, + 'input': String input, + }) { + return _createTransaction( + block_s: block_s, + timeStamp_s: timeStamp_s, + hash: hash, + from: from, + to: to, + gas_s: gas_s, + gasUsed_s: gasUsed_s, + gasPrice_s: gasPrice_s, + contractAddress: contractAddress, + symbol: symbol, + name: name, + tokenID_s: tokenID_s, + input: input, + ); + } + throw UnsupportedError("Invalid JSON for EtherscanTransaction"); + } + factory EtherscanTransaction.fromJsonErc20( Json json, { required EvmCoinEntity currency, diff --git a/lib/src/crypto/evm/evm.dart b/lib/src/crypto/evm/evm.dart index 5c71e275d..aefdc627b 100644 --- a/lib/src/crypto/evm/evm.dart +++ b/lib/src/crypto/evm/evm.dart @@ -28,9 +28,11 @@ export 'entities/contract/parameter_type/function_parameter_type.dart'; export 'entities/contract/contract_function.dart'; // ERC20 -export 'entities/abi/erc20_contract.dart'; +export 'entities/abi/erc/erc20_contract.dart'; // ERC721 -export 'entities/abi/erc721_contract.dart'; +export 'entities/abi/erc/erc721_contract.dart'; +// ERC1155 +export 'entities/abi/erc/erc1155.dart'; // ENS export 'entities/abi/ens/ens_registry_contract.dart'; export 'entities/abi/ens/ens_resolver_contract.dart'; diff --git a/lib/src/crypto/evm/repositories/etherscan/etherscan_explorer.dart b/lib/src/crypto/evm/repositories/etherscan/etherscan_explorer.dart index 4bd063509..afd26fdc5 100644 --- a/lib/src/crypto/evm/repositories/etherscan/etherscan_explorer.dart +++ b/lib/src/crypto/evm/repositories/etherscan/etherscan_explorer.dart @@ -71,6 +71,22 @@ class EtherscanExplorer extends EtherscanRepository { .addOptionalParameter('offset', offset) .addOptionalParameter('sort', sorting?.name); + String buildERC1155TransactionEndpoint({ + required String address, + required String contractAddress, + int? startblock, + int? endblock, + int? page, + int? offset, + Sorting? sorting, + }) => + "$base&module=account&action=token1155tx&contractaddress=$contractAddress&address=$address" + .addOptionalParameter('page', page) + .addOptionalParameter('offset', offset) + .addOptionalParameter('startblock', startblock) + .addOptionalParameter('endblock', endblock) + .addOptionalParameter('sort', sorting?.name); + /// /// Fetch all Transactions for the given [token] on the given [address] /// @@ -98,7 +114,37 @@ class EtherscanExplorer extends EtherscanRepository { tx, token: currency, address: address, - ) + ), + ]; + } + + Future> fetchERC1155Transactions({ + required String contractAddress, + required String address, + int? startblock, + int? endblock, + int? page, + int? offset, + Sorting? sorting, + }) async { + final endpoint = buildERC1155TransactionEndpoint( + address: address, + contractAddress: contractAddress, + startblock: startblock, + endblock: endblock, + page: page, + offset: offset, + sorting: sorting, + ); + final txResults = await fetchEtherscanWithRatelimitRetries(endpoint); + + return [ + for (final tx in txResults) + EtherscanTransaction.fromJsonErc1155( + tx, + address: address, + currency: currency, + ), ]; } @@ -308,4 +354,22 @@ class ZeniqScanExplorer extends EtherscanExplorer { .addOptionalParameter('offset', offset) .addOptionalParameter('sort', sorting?.name); } + + @override + String buildERC1155TransactionEndpoint({ + required String address, + required String contractAddress, + int? startblock, + int? endblock, + int? page, + int? offset, + Sorting? sorting, + }) { + return "$base&module=account&action=tokentx&address=$address&contractaddress=$contractAddress" + .addOptionalParameter('start_block', startblock) + .addOptionalParameter('end_block', endblock) + .addOptionalParameter('page', page) + .addOptionalParameter('offset', offset) + .addOptionalParameter('sort', sorting?.name); + } } diff --git a/lib/src/crypto/evm/repositories/rpc/evm_rpc_interface.dart b/lib/src/crypto/evm/repositories/rpc/evm_rpc_interface.dart index f3f66ea51..c49459050 100644 --- a/lib/src/crypto/evm/repositories/rpc/evm_rpc_interface.dart +++ b/lib/src/crypto/evm/repositories/rpc/evm_rpc_interface.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:walletkit_dart/src/common/logger.dart'; +import 'package:walletkit_dart/src/crypto/evm/entities/abi/erc/erc1155.dart'; import 'package:walletkit_dart/src/crypto/evm/entities/block_number.dart'; import 'package:walletkit_dart/src/crypto/evm/repositories/rpc/queued_rpc_interface.dart'; import 'package:walletkit_dart/src/domain/exceptions.dart'; @@ -102,6 +103,66 @@ final class EvmRpcInterface { return Amount(value: balance, decimals: token.decimals); } + /// + /// Fetch Balance of ERC1155 Token + /// + Future fetchERC1155BalanceOfToken({ + required String address, + required BigInt tokenID, + required String contractAddress, + }) async { + final erc1155Contract = ERC1155Contract( + contractAddress: contractAddress, + rpc: this, + ); + final balance = await erc1155Contract.balanceOf( + address: address, + tokenID: tokenID, + ); + + return Amount(value: balance, decimals: 0); + } + + /// + /// Fetch Batch Balance of ERC1155 Tokens + /// + Future> fetchERC1155BatchBalanceOfTokens({ + required List accounts, + required List tokenIDs, + required String contractAddress, + }) async { + final erc1155Contract = ERC1155Contract( + contractAddress: contractAddress, + rpc: this, + ); + + final balances = await erc1155Contract.balanceOfBatch( + accounts: accounts, + tokenIDs: tokenIDs, + ); + + return balances; + } + + /// + /// Fetch Uri of ERC115 Token + /// + Future fetchERC1155UriOfToken({ + required BigInt tokenID, + required String contractAddress, + }) async { + final erc1155Contract = ERC1155Contract( + contractAddress: contractAddress, + rpc: this, + ); + + final uri = await erc1155Contract.getUri( + tokenID: tokenID, + ); + + return uri; + } + Future<(Amount, int)> estimateNetworkFees({ required String recipient, required String sender, @@ -211,6 +272,32 @@ final class EvmRpcInterface { ); } + /// + /// Send ERC1155 Token + /// + Future sendERC1155Token({ + required TransferIntent intent, + required String contractAddress, + required BigInt tokenID, + required String from, + required Uint8List seed, + }) async { + final erc1155Contract = ERC1155Contract( + contractAddress: contractAddress, + rpc: this, + ); + + return erc1155Contract.safeTransferFrom( + sender: from, + to: intent.recipient, + tokenID: tokenID, + amount: intent.amount.value, + seed: seed, + feeInfo: intent.feeInfo, + accessList: intent.accessList, + ); + } + Future getPriorityFee() async { final priorityFee = await performTask( (client) => client.getPriorityFee(), diff --git a/lib/src/crypto/wallet_utils.dart b/lib/src/crypto/wallet_utils.dart index e1d123906..f9221af77 100644 --- a/lib/src/crypto/wallet_utils.dart +++ b/lib/src/crypto/wallet_utils.dart @@ -47,6 +47,25 @@ Future getTokenInfo({ } } +Future isErc1155({ + required String contractAddress, + required EvmRpcInterface rpc, + required String address, +}) async { + bool isErc1155 = false; + try { + await rpc.fetchERC1155BalanceOfToken( + address: address, + tokenID: BigInt.from(0), + contractAddress: contractAddress, + ); + isErc1155 = true; + } catch (e) { + isErc1155 = false; + } + return isErc1155; +} + Uint8List publicKeyToAddress(Uint8List publicKey) { // 1. Ensure the public key is in the correct format if (publicKey.length == 64) { diff --git a/lib/src/domain/entities/coin_entity.dart b/lib/src/domain/entities/coin_entity.dart index e7086ab5b..784b5b0fd 100644 --- a/lib/src/domain/entities/coin_entity.dart +++ b/lib/src/domain/entities/coin_entity.dart @@ -26,8 +26,12 @@ class CoinEntity { ERC20Entity? get asEthBased => this is ERC20Entity ? this as ERC20Entity : null; + ERC1155Entity? get asERC1155 => + this is ERC1155Entity ? this as ERC1155Entity : null; + bool get isEvm => this is EvmCoinEntity; bool get isERC20 => this is ERC20Entity; + bool get isERC1155 => this is ERC1155Entity; bool get isUTXO => switch (this) { btcCoin || ltcCoin || zeniqCoin || bchCoin || ec8Coin => true, _ => false, @@ -62,6 +66,14 @@ class CoinEntity { 'contractAddress': ethBasedToken.contractAddress, 'allowDeletion': ethBasedToken.allowDeletion, }, + ERC1155Entity erc1155Token => { + 'name': erc1155Token.name, + 'symbol': erc1155Token.symbol, + 'contractAddress': erc1155Token.contractAddress, + 'tokenId': erc1155Token.tokenId.toString(), + 'allowDeletion': erc1155Token.allowDeletion, + 'chainID': erc1155Token.chainID, + }, EvmCoinEntity evmEntity => { 'name': evmEntity.name, 'symbol': evmEntity.symbol, @@ -93,6 +105,21 @@ class CoinEntity { contractAddress: contractAddress, allowDeletion: json['allowDeletion'] ?? true, ), + { + "name": String name, + "symbol": String symbol, + "contractAddress": String contractAddress, + "tokenId": String tokenId, + "chainID": int chainID, + } => + ERC1155Entity( + name: name, + symbol: symbol, + contractAddress: contractAddress, + tokenId: BigInt.parse(tokenId), + chainID: chainID, + allowDeletion: json['allowDeletion'] ?? true, + ), { 'name': String name, 'symbol': String symbol, @@ -122,6 +149,7 @@ class CoinEntity { class EvmCoinEntity extends CoinEntity { final int chainID; + final bool? allowDeletion; @override String get identifier => "$name:$symbol:$decimals:$chainID"; @@ -136,6 +164,7 @@ class EvmCoinEntity extends CoinEntity { return other is EvmCoinEntity && other is! ERC20Entity && other is! ERC721Entity && + other is! ERC1155Entity && other.chainID == chainID; } @@ -144,12 +173,92 @@ class EvmCoinEntity extends CoinEntity { required super.symbol, required super.decimals, required this.chainID, + this.allowDeletion = false, }); + + EvmCoinEntity copyWith({ + String? name, + String? symbol, + int? decimals, + int? chainID, + bool? allowDeletion, + }) { + return EvmCoinEntity( + name: name ?? this.name, + symbol: symbol ?? this.symbol, + decimals: decimals ?? this.decimals, + chainID: chainID ?? this.chainID, + allowDeletion: allowDeletion ?? this.allowDeletion, + ); + } +} + +class ERC1155Entity extends EvmCoinEntity { + final String contractAddress; + final BigInt tokenId; + + const ERC1155Entity({ + required super.name, + required super.symbol, + required super.chainID, + required this.contractAddress, + required this.tokenId, + super.allowDeletion, + }) : super(decimals: 0); + + String get lowerCaseAddress => contractAddress.toLowerCase(); + + @override + int get hashCode => + lowerCaseAddress.hashCode ^ chainID.hashCode ^ tokenId.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ERC1155Entity && + other.lowerCaseAddress == lowerCaseAddress && + other.chainID == chainID && + other.tokenId == tokenId; + } + + ERC1155Entity copyWith({ + String? name, + String? symbol, + int? decimals, + String? contractAddress, + BigInt? tokenId, + bool? allowDeletion, + int? chainID, + }) { + return ERC1155Entity( + name: name ?? this.name, + symbol: symbol ?? this.symbol, + contractAddress: contractAddress ?? this.contractAddress, + tokenId: tokenId ?? this.tokenId, + allowDeletion: allowDeletion ?? this.allowDeletion, + chainID: chainID ?? this.chainID, + ); + } + + factory ERC1155Entity.fromJson( + Map json, { + required bool allowDeletion, + required int chainID, + }) { + return ERC1155Entity( + name: json['name'], + symbol: json['symbol'], + contractAddress: json['contractAddress'], + tokenId: BigInt.parse(json['tokenId']), + allowDeletion: allowDeletion, + chainID: chainID, + ); + } } class ERC20Entity extends EvmCoinEntity { final String contractAddress; - final bool? allowDeletion; const ERC20Entity({ required super.name, @@ -157,7 +266,7 @@ class ERC20Entity extends EvmCoinEntity { required super.decimals, required super.chainID, required this.contractAddress, - this.allowDeletion = false, + super.allowDeletion, }); String get lowerCaseAddress => contractAddress.toLowerCase(); diff --git a/lib/src/domain/entities/token_info.dart b/lib/src/domain/entities/token_info.dart index b84d6ff7e..c2168567c 100644 --- a/lib/src/domain/entities/token_info.dart +++ b/lib/src/domain/entities/token_info.dart @@ -7,6 +7,7 @@ class TokenInfo { final String symbol; final String name; final String contractAddress; + final int? id; const TokenInfo({ required this.decimals, @@ -14,5 +15,6 @@ class TokenInfo { required this.symbol, required this.name, required this.contractAddress, + this.id, }); } diff --git a/test/ci/evm/evm_explorer_test.dart b/test/ci/evm/evm_explorer_test.dart index e0255a324..becd9df0f 100644 --- a/test/ci/evm/evm_explorer_test.dart +++ b/test/ci/evm/evm_explorer_test.dart @@ -54,6 +54,16 @@ void main() { }, ); + test('test erc1155 fetching zsc', () async { + final transactions = await zeniqScan.fetchERC1155Transactions( + address: arbitrumTestWallet, + contractAddress: "0xB868a4d85c3f7207106145eB41444c5313C97D86", + ); + + print('ERC1155 Transactions: $transactions'); + expect(transactions, isNotEmpty); + }); + test('Test Ethereum Etherscan Fetching', () async { /// /// Balances diff --git a/test/ci/evm/evm_rpc_test.dart b/test/ci/evm/evm_rpc_test.dart index f4a89415a..49fff102f 100644 --- a/test/ci/evm/evm_rpc_test.dart +++ b/test/ci/evm/evm_rpc_test.dart @@ -3,6 +3,7 @@ import 'package:test/test.dart'; import 'package:walletkit_dart/walletkit_dart.dart'; import '../../rpc_test_config.dart'; import '../../shared_rpc_tests.dart'; +import '../../utils.dart'; void main() { final arbitrumParams = RPCTestParameters(token: arbitrum); @@ -56,4 +57,64 @@ void main() { description: "Binance Network RPC Tests", config: RPCLiveTestConfig(networkType: BNBNetwork), params: bnbParams); + + test('test erc1155 balance of token', () async { + final balance = await zeniqSmartChainRPC.fetchERC1155BalanceOfToken( + address: arbitrumTestWallet, + tokenID: BigInt.from(2), + contractAddress: "0xB868a4d85c3f7207106145eB41444c5313C97D86", + ); + + print('Balance: ${balance.value}'); + expect(balance, isNotNull); + expect(balance.value, greaterThanOrEqualTo(BigInt.zero)); + }); + + test('test erc1155 batch balance of tokens', () async { + final balances = await zeniqSmartChainRPC.fetchERC1155BatchBalanceOfTokens( + accounts: [ + arbitrumTestWallet, + arbitrumTestWallet, + arbitrumTestWallet, + arbitrumTestWallet, + arbitrumTestWallet, + ], + tokenIDs: [ + BigInt.from(0), + BigInt.from(1), + BigInt.from(2), + BigInt.from(3), + BigInt.from(4) + ], + contractAddress: "0xB868a4d85c3f7207106145eB41444c5313C97D86", + ); + + print('Balances: $balances'); + expect(balances, isNotNull); + }); + + test('test uri of erc1155 tokens', () async { + final uri = await ethereumRPC.fetchERC1155UriOfToken( + tokenID: BigInt.from(1), + contractAddress: "0x1ca3262009b21F944e6b92a2a88D039D06F1acFa", + ); + + print('URI: $uri'); + expect(uri, isNotNull); + }); + + test("is erc1155", () async { + bool isERC1155 = false; + try { + await ethereumRPC.fetchERC1155BalanceOfToken( + address: arbitrumTestWallet, + tokenID: BigInt.from(0), + contractAddress: "0x1ca3262009b21F944e6b92a2a88D039D06F1acFa"); + isERC1155 = true; + } catch (e) { + isERC1155 = false; + } + + print('isERC1155: $isERC1155'); + }); } diff --git a/test/no_ci/arb_test.dart b/test/no_ci/arb_test.dart index a48d0cef9..bf546a9fb 100644 --- a/test/no_ci/arb_test.dart +++ b/test/no_ci/arb_test.dart @@ -38,4 +38,23 @@ void main() { print("Hash: $hash"); }); + + test('test to transfer erc1155 asset', () async { + final intent = TransferIntent( + recipient: arbitrumTestWallet, + amount: Amount.convert(value: 100, decimals: 0), + token: nullToken, + memo: null, + ); + + final hash = await zeniqSmartChainRPC.sendERC1155Token( + contractAddress: "0xB868a4d85c3f7207106145eB41444c5313C97D86", + from: arbitrumTestWallet, + tokenID: BigInt.from(0), + intent: intent, + seed: testSeed, + ); + + print("Hash: $hash"); + }); }