diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e6290dd..082c3fe4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,15 +25,6 @@ jobs: PUB_CACHE: ".dart_tool/pub_cache" run: dart pub upgrade - ensure_formatted: - name: "Formatting" - runs-on: ubuntu-latest - container: - image: google/dart - steps: - - uses: actions/checkout@v2 - - run: "dart format --output=none --set-exit-if-changed ." - analyze: name: "Analysis" needs: get_dependencies @@ -45,6 +36,7 @@ jobs: path: .dart_tool key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} - uses: dart-lang/setup-dart@v1 + - run: "dart format --output=none --set-exit-if-changed ." - run: dart analyze --fatal-infos vm_tests: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7013b914..e66bb34c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.3.2 + +- Support EIP-1559 transactions. + ## 2.3.1 - Fix the `Web3Client.custom` constructor not setting all required fields. diff --git a/lib/src/browser/credentials.dart b/lib/src/browser/credentials.dart index 165dc730..c8ce7742 100644 --- a/lib/src/browser/credentials.dart +++ b/lib/src/browser/credentials.dart @@ -22,7 +22,8 @@ class MetaMaskCredentials extends CredentialsWithKnownAddress : address = EthereumAddress.fromHex(hexAddress); @override - Future signToSignature(Uint8List payload, {int? chainId}) { + Future signToSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) { throw UnsupportedError('Signing raw payloads is not supported on MetaMask'); } diff --git a/lib/src/core/block_information.dart b/lib/src/core/block_information.dart new file mode 100644 index 00000000..8ac5a47c --- /dev/null +++ b/lib/src/core/block_information.dart @@ -0,0 +1,18 @@ +import 'package:web3dart/src/crypto/formatting.dart'; +import 'package:web3dart/web3dart.dart'; + +class BlockInformation { + EtherAmount? baseFeePerGas; + + BlockInformation({this.baseFeePerGas}); + + factory BlockInformation.fromJson(Map json) { + return BlockInformation( + baseFeePerGas: json.containsKey('baseFeePerGas') + ? EtherAmount.fromUnitAndValue( + EtherUnit.wei, hexToInt(json['baseFeePerGas'] as String)) + : null); + } + + bool get isSupportEIP1559 => baseFeePerGas != null; +} diff --git a/lib/src/core/client.dart b/lib/src/core/client.dart index 1708342f..a56def6d 100644 --- a/lib/src/core/client.dart +++ b/lib/src/core/client.dart @@ -112,6 +112,10 @@ class Web3Client { return _makeRPCCall('net_version').then(int.parse); } + Future getChainId() { + return _makeRPCCall('eth_chainId').then(BigInt.parse); + } + /// Returns true if the node is actively listening for network connections. Future isListeningForNetwork() { return _makeRPCCall('net_listening'); @@ -179,6 +183,13 @@ class Web3Client { .then((s) => hexToInt(s).toInt()); } + Future getBlockInformation( + {String blockNumber = 'latest', bool isContainFullObj = true}) { + return _makeRPCCall>( + 'eth_getBlockByNumber', [blockNumber, isContainFullObj]) + .then((json) => BlockInformation.fromJson(json)); + } + /// Gets the balance of the account with the specified address. /// /// This function allows specifying a custom block mined in the past to get @@ -271,9 +282,13 @@ class Web3Client { return cred.sendTransaction(transaction); } - final signed = await signTransaction(cred, transaction, + var signed = await signTransaction(cred, transaction, chainId: chainId, fetchChainIdFromNetworkId: fetchChainIdFromNetworkId); + if (transaction.isEIP1559) { + signed = prependTransactionType(0x02, signed); + } + return sendRawTransaction(signed); } @@ -348,6 +363,8 @@ class Web3Client { EtherAmount? value, BigInt? amountOfGas, EtherAmount? gasPrice, + EtherAmount? maxPriorityFeePerGas, + EtherAmount? maxFeePerGas, Uint8List? data, @Deprecated('Parameter is ignored') BlockNum? atBlock, }) async { @@ -360,6 +377,11 @@ class Web3Client { if (amountOfGas != null) 'gas': '0x${amountOfGas.toRadixString(16)}', if (gasPrice != null) 'gasPrice': '0x${gasPrice.getInWei.toRadixString(16)}', + if (maxPriorityFeePerGas != null) + 'maxPriorityFeePerGas': + '0x${maxPriorityFeePerGas.getInWei.toRadixString(16)}', + if (maxFeePerGas != null) + 'maxFeePerGas': '0x${maxFeePerGas.getInWei.toRadixString(16)}', if (value != null) 'value': '0x${value.getInWei.toRadixString(16)}', if (data != null) 'data': bytesToHex(data, include0x: true), }, diff --git a/lib/src/core/transaction.dart b/lib/src/core/transaction.dart index d7c9cce6..d594eaa4 100644 --- a/lib/src/core/transaction.dart +++ b/lib/src/core/transaction.dart @@ -40,6 +40,9 @@ class Transaction { /// have already been sent by [from]. final int? nonce; + final EtherAmount? maxPriorityFeePerGas; + final EtherAmount? maxFeePerGas; + Transaction( {this.from, this.to, @@ -47,19 +50,23 @@ class Transaction { this.gasPrice, this.value, this.data, - this.nonce}); + this.nonce, + this.maxFeePerGas, + this.maxPriorityFeePerGas}); /// Constructs a transaction that can be used to call a contract function. - Transaction.callContract({ - required DeployedContract contract, - required ContractFunction function, - required List parameters, - this.from, - this.maxGas, - this.gasPrice, - this.value, - this.nonce, - }) : to = contract.address, + Transaction.callContract( + {required DeployedContract contract, + required ContractFunction function, + required List parameters, + this.from, + this.maxGas, + this.gasPrice, + this.value, + this.nonce, + this.maxFeePerGas, + this.maxPriorityFeePerGas}) + : to = contract.address, data = function.encodeCall(parameters); Transaction copyWith( @@ -69,7 +76,9 @@ class Transaction { EtherAmount? gasPrice, EtherAmount? value, Uint8List? data, - int? nonce}) { + int? nonce, + EtherAmount? maxPriorityFeePerGas, + EtherAmount? maxFeePerGas}) { return Transaction( from: from ?? this.from, to: to ?? this.to, @@ -78,6 +87,10 @@ class Transaction { value: value ?? this.value, data: data ?? this.data, nonce: nonce ?? this.nonce, + maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, + maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, ); } + + bool get isEIP1559 => maxFeePerGas != null && maxPriorityFeePerGas != null; } diff --git a/lib/src/core/transaction_information.dart b/lib/src/core/transaction_information.dart index b682c9ad..73b38879 100644 --- a/lib/src/core/transaction_information.dart +++ b/lib/src/core/transaction_information.dart @@ -87,6 +87,7 @@ class TransactionReceipt { this.from, this.to, this.gasUsed, + this.effectiveGasPrice, this.logs = const []}); TransactionReceipt.fromMap(Map map) @@ -105,6 +106,10 @@ class TransactionReceipt { cumulativeGasUsed = hexToInt(map['cumulativeGasUsed'] as String), gasUsed = map['gasUsed'] != null ? hexToInt(map['gasUsed'] as String) : null, + effectiveGasPrice = map['effectiveGasPrice'] != null + ? EtherAmount.inWei( + BigInt.parse(map['effectiveGasPrice'] as String)) + : null, contractAddress = map['contractAddress'] != null ? EthereumAddress.fromHex(map['contractAddress'] as String) : null, @@ -153,13 +158,16 @@ class TransactionReceipt { /// Array of logs generated by this transaction. final List logs; + final EtherAmount? effectiveGasPrice; + @override String toString() { return 'TransactionReceipt{transactionHash: ${bytesToHex(transactionHash)}, ' 'transactionIndex: $transactionIndex, blockHash: ${bytesToHex(blockHash)}, ' 'blockNumber: $blockNumber, from: ${from?.hex}, to: ${to?.hex}, ' 'cumulativeGasUsed: $cumulativeGasUsed, gasUsed: $gasUsed, ' - 'contractAddress: ${contractAddress?.hex}, status: $status, logs: $logs}'; + 'contractAddress: ${contractAddress?.hex}, status: $status, ' + 'effectiveGasPrice: $effectiveGasPrice, logs: $logs}'; } @override @@ -177,6 +185,7 @@ class TransactionReceipt { gasUsed == other.gasUsed && contractAddress == other.contractAddress && status == other.status && + effectiveGasPrice == other.effectiveGasPrice && const ListEquality().equals(logs, other.logs); @override @@ -191,5 +200,6 @@ class TransactionReceipt { gasUsed.hashCode ^ contractAddress.hashCode ^ status.hashCode ^ + effectiveGasPrice.hashCode ^ logs.hashCode; } diff --git a/lib/src/core/transaction_signer.dart b/lib/src/core/transaction_signer.dart index c789b38d..dbe16548 100644 --- a/lib/src/core/transaction_signer.dart +++ b/lib/src/core/transaction_signer.dart @@ -23,18 +23,23 @@ Future<_SigningInput> _fillMissingData({ final sender = transaction.from ?? await credentials.extractAddress(); var gasPrice = transaction.gasPrice; - var nonce = transaction.nonce; - if (gasPrice == null || nonce == null) { - if (client == null) { - throw ArgumentError("Can't find suitable gas price and nonce from client " - 'because no client is set. Please specify a gas price on the ' - 'transaction.'); - } - gasPrice ??= await client.getGasPrice(); - nonce ??= await client.getTransactionCount(sender, - atBlock: const BlockNum.pending()); + + if (client == null && + (transaction.nonce == null || + transaction.maxGas == null || + loadChainIdFromNetwork) || + (!transaction.isEIP1559 && gasPrice == null)) { + throw ArgumentError('Client is required to perform network actions'); + } + + if (!transaction.isEIP1559 && gasPrice == null) { + gasPrice = await client!.getGasPrice(); } + final nonce = transaction.nonce ?? + await client! + .getTransactionCount(sender, atBlock: const BlockNum.pending()); + final maxGas = transaction.maxGas ?? await client! .estimateGas( @@ -43,6 +48,8 @@ Future<_SigningInput> _fillMissingData({ data: transaction.data, value: transaction.value, gasPrice: gasPrice, + maxPriorityFeePerGas: transaction.maxPriorityFeePerGas, + maxFeePerGas: transaction.maxFeePerGas, ) .then((bigInt) => bigInt.toInt()); @@ -60,12 +67,7 @@ Future<_SigningInput> _fillMissingData({ if (!loadChainIdFromNetwork) { resolvedChainId = chainId!; } else { - if (client == null) { - throw ArgumentError( - "Can't load chain id from network when no client is set"); - } - - resolvedChainId = await client.getNetworkId(); + resolvedChainId = await client!.getNetworkId(); } return _SigningInput( @@ -75,8 +77,27 @@ Future<_SigningInput> _fillMissingData({ ); } +Uint8List prependTransactionType(int type, Uint8List transaction) { + return Uint8List(transaction.length + 1) + ..[0] = type + ..setAll(1, transaction); +} + Future _signTransaction( Transaction transaction, Credentials c, int? chainId) async { + if (transaction.isEIP1559 && chainId != null) { + final encodedTx = LengthTrackingByteSink(); + encodedTx.addByte(0x02); + encodedTx.add(rlp + .encode(_encodeEIP1559ToRlp(transaction, null, BigInt.from(chainId)))); + + encodedTx.close(); + final signature = await c.signToSignature(encodedTx.asBytes(), + chainId: chainId, isEIP1559: transaction.isEIP1559); + + return uint8ListFromList(rlp.encode( + _encodeEIP1559ToRlp(transaction, signature, BigInt.from(chainId)))); + } final innerSignature = chainId == null ? null : MsgSignature(BigInt.zero, BigInt.zero, chainId); @@ -87,6 +108,38 @@ Future _signTransaction( return uint8ListFromList(rlp.encode(_encodeToRlp(transaction, signature))); } +List _encodeEIP1559ToRlp( + Transaction transaction, MsgSignature? signature, BigInt chainId) { + final list = [ + chainId, + transaction.nonce, + transaction.maxPriorityFeePerGas!.getInWei, + transaction.maxFeePerGas!.getInWei, + transaction.maxGas, + ]; + + if (transaction.to != null) { + list.add(transaction.to!.addressBytes); + } else { + list.add(''); + } + + list + ..add(transaction.value?.getInWei) + ..add(transaction.data); + + list.add([]); // access list + + if (signature != null) { + list + ..add(signature.v) + ..add(signature.r) + ..add(signature.s); + } + + return list; +} + List _encodeToRlp(Transaction transaction, MsgSignature? signature) { final list = [ transaction.nonce, diff --git a/lib/src/credentials/credentials.dart b/lib/src/credentials/credentials.dart index 08fe73b5..b71a777b 100644 --- a/lib/src/credentials/credentials.dart +++ b/lib/src/credentials/credentials.dart @@ -30,8 +30,10 @@ abstract class Credentials { /// bytes representation of the [eth_sign RPC method](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign), /// but without the "Ethereum signed message" prefix. /// The [payload] parameter contains the raw data, not a hash. - Future sign(Uint8List payload, {int? chainId}) async { - final signature = await signToSignature(payload, chainId: chainId); + Future sign(Uint8List payload, + {int? chainId, bool isEIP1559 = false}) async { + final signature = + await signToSignature(payload, chainId: chainId, isEIP1559: isEIP1559); final r = padUint8ListTo32(unsignedIntToBytes(signature.r)); final s = padUint8ListTo32(unsignedIntToBytes(signature.s)); @@ -43,7 +45,8 @@ abstract class Credentials { /// Signs the [payload] with a private key and returns the obtained /// signature. - Future signToSignature(Uint8List payload, {int? chainId}); + Future signToSignature(Uint8List payload, + {int? chainId, bool isEIP1559 = false}); /// Signs an Ethereum specific signature. This method is equivalent to /// [sign], but with a special prefix so that this method can't be used to @@ -124,14 +127,19 @@ class EthPrivateKey extends CredentialsWithKnownAddress { @override Future signToSignature(Uint8List payload, - {int? chainId}) async { + {int? chainId, bool isEIP1559 = false}) async { final signature = secp256k1.sign(keccak256(payload), privateKey); // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L26 // be aware that signature.v already is recovery + 27 - final chainIdV = - chainId != null ? (signature.v - 27 + (chainId * 2 + 35)) : signature.v; - + int chainIdV; + if (isEIP1559) { + chainIdV = signature.v - 27; + } else { + chainIdV = chainId != null + ? (signature.v - 27 + (chainId * 2 + 35)) + : signature.v; + } return MsgSignature(signature.r, signature.s, chainIdV); } diff --git a/lib/web3dart.dart b/lib/web3dart.dart index 8c06f02e..634102d6 100644 --- a/lib/web3dart.dart +++ b/lib/web3dart.dart @@ -9,12 +9,14 @@ import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'package:web3dart/src/utils/length_tracking_byte_sink.dart'; import 'contracts.dart'; import 'credentials.dart'; import 'crypto.dart'; import 'json_rpc.dart'; import 'src/core/amount.dart'; +import 'src/core/block_information.dart'; import 'src/core/block_number.dart'; import 'src/core/sync_information.dart'; import 'src/utils/rlp.dart' as rlp; @@ -24,6 +26,7 @@ export 'contracts.dart'; export 'credentials.dart'; export 'src/core/amount.dart'; +export 'src/core/block_information.dart'; export 'src/core/block_number.dart'; export 'src/core/sync_information.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 8af06724..dcbfa18b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: web3dart description: Dart library to connect to Ethereum clients. Send transactions and interact with smart contracts! -version: 2.3.1 +version: 2.3.2 homepage: https://github.com/simolus3/web3dart environment: diff --git a/test/core/sign_transaction_test.dart b/test/core/sign_transaction_test.dart index 013ac42a..f7f6cb59 100644 --- a/test/core/sign_transaction_test.dart +++ b/test/core/sign_transaction_test.dart @@ -1,9 +1,145 @@ +import 'dart:convert'; + import 'package:http/http.dart'; import 'package:test/test.dart'; import 'package:web3dart/crypto.dart'; +import 'package:web3dart/src/utils/rlp.dart' as rlp; +import 'package:web3dart/src/utils/typed_data.dart'; import 'package:web3dart/web3dart.dart'; +const rawJson = '''[ + { + "nonce":819, + "value":43203529, + "gasLimit":35552, + "maxPriorityFeePerGas":75853, + "maxFeePerGas":121212, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87102f86e048203338301284d8301d97c828ae094000000000000000000000000000000000000aaaa8402933bc980c080a00f924cb68412c8f1cfd74d9b581c71eeaf94fff6abdde3e5b02ca6b2931dcf47a07dd1c50027c3e31f8b565e25ce68a5072110f61fce5eee81b195dd51273c2f83" + }, + { + "nonce":353, + "value":61901619, + "gasLimit":32593, + "maxPriorityFeePerGas":38850, + "maxFeePerGas":136295, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87002f86d048201618297c283021467827f5194000000000000000000000000000000000000aaaa8403b08b3380c080a08caf712f72489da6f1a634b651b4b1c7d9be7d1e8d05ea76c1eccee3bdfb86a5a06aecc106f588ce51e112f5e9ea7aba3e089dc7511718821d0e0cd52f52af4e45" + }, + { + "nonce":985, + "value":32531825, + "gasLimit":68541, + "maxPriorityFeePerGas":66377, + "maxFeePerGas":136097, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87202f86f048203d983010349830213a183010bbd94000000000000000000000000000000000000aaaa8401f0657180c001a08c03a86e85789ee9a1b42fa0a86d316fca262694f8c198df11f194678c2c2d35a028f8e7de02b35014a17b6d28ff8c7e7be6860e7265ac162fb721f1aeae75643c" + }, + { + "nonce":623, + "value":21649799, + "gasLimit":57725, + "maxPriorityFeePerGas":74140, + "maxFeePerGas":81173, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87102f86e0482026f8301219c83013d1582e17d94000000000000000000000000000000000000aaaa84014a598780c001a0b87c4c8c505d2d692ac77ba466547e79dd60fe7ecd303d520bf6e8c7085e3182a06dc7d00f5e68c3f3ebe8ae35a90d46051afde620ac12e43cae9560a29b13e7fb" + }, + { + "nonce":972, + "value":94563383, + "gasLimit":65254, + "maxPriorityFeePerGas":42798, + "maxFeePerGas":103466, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87002f86d048203cc82a72e8301942a82fee694000000000000000000000000000000000000aaaa8405a2ec3780c001a006cf07af78c187db104496c58d679f37fcd2d5790970cecf9a59fe4a5321b375a039f3faafc71479d283a5b1e66a86b19c4bdc516655d89dbe57d9747747c01dfe" + }, + { + "nonce":588, + "value":99359647, + "gasLimit":37274, + "maxPriorityFeePerGas":87890, + "maxFeePerGas":130273, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87102f86e0482024c830157528301fce182919a94000000000000000000000000000000000000aaaa8405ec1b9f80c080a03e2f59ac9ca852034c2c1da35a742ca19fdd910aa5d2ed49ab8ad27a2fcb2b10a03ac1c29c26723c58f91400fb6dfb5f5b837467b1c377541b47dae474dddbe469" + }, + { + "nonce":900, + "value":30402257, + "gasLimit":76053, + "maxPriorityFeePerGas":8714, + "maxFeePerGas":112705, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87102f86e0482038482220a8301b8418301291594000000000000000000000000000000000000aaaa8401cfe6d180c001a0f7ffc5bca2512860f8236360159bf303dcfab71546b6a0032df0306f3739d0c4a05d38fe2c4edebdc1edc157034f780c53a0e5ae089e57220745bd48bcb10cdf87" + }, + { + "nonce":709, + "value":6478043, + "gasLimit":28335, + "maxPriorityFeePerGas":86252, + "maxFeePerGas":94636, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb87002f86d048202c5830150ec830171ac826eaf94000000000000000000000000000000000000aaaa8362d8db80c001a0a61a5710512f346c9996377f7b564ccb64c73a5fdb615499adb1250498f3e01aa002d10429572cecfaa911a58bbe05f2b26e4c3aee3402202153a93692849add11" + }, + { + "nonce":939, + "value":2782905, + "gasLimit":45047, + "maxPriorityFeePerGas":45216, + "maxFeePerGas":91648, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb86f02f86c048203ab82b0a08301660082aff794000000000000000000000000000000000000aaaa832a76b980c001a0191f0f6667a20cefc0b454e344cc01108aafbdc4e4e5ed88fdd1b5d108495b31a020879042b0f8d3807609f18fe42a9820de53c8a0ea1d0a2d50f8f5e92a94f00d" + }, + { + "nonce":119, + "value":65456115, + "gasLimit":62341, + "maxPriorityFeePerGas":24721, + "maxFeePerGas":107729, + "to":"0x000000000000000000000000000000000000aaaa", + "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", + "signedTransactionRLP":"0xb86e02f86b04778260918301a4d182f38594000000000000000000000000000000000000aaaa8403e6c7f380c001a05e40977f4064a2bc08785e422ed8a47b56aa219abe93251d2b3b4d0cf937b8c0a071e600cd15589c3607bd9627314b99e9b5976bd427b774aa685bd0d036b1771e" + } + ]'''; + void main() { + test('sign eip 1559 transaction', () async { + final data = jsonDecode(rawJson) as List; + + await Future.forEach(data, (element) async { + final tx = element as Map; + final credentials = + EthPrivateKey.fromHex(strip0x(tx['privateKey'] as String)); + final transaction = Transaction( + from: await credentials.extractAddress(), + to: EthereumAddress.fromHex(tx['to'] as String), + nonce: tx['nonce'] as int, + maxGas: tx['gasLimit'] as int, + value: EtherAmount.inWei(BigInt.from(tx['value'] as int)), + maxFeePerGas: EtherAmount.fromUnitAndValue( + EtherUnit.wei, BigInt.from(tx['maxFeePerGas'] as int)), + maxPriorityFeePerGas: EtherAmount.fromUnitAndValue( + EtherUnit.wei, BigInt.from(tx['maxPriorityFeePerGas'] as int))); + + final client = Web3Client('', Client()); + final signature = + await client.signTransaction(credentials, transaction, chainId: 4); + + expect( + bytesToHex(uint8ListFromList( + rlp.encode(prependTransactionType(0x02, signature)))), + strip0x(tx['signedTransactionRLP'] as String)); + }); + }); + test('signs transactions', () async { final credentials = EthPrivateKey.fromHex( 'a2fd51b96dc55aeb14b30d55a6b3121c7b9c599500c1beb92a389c3377adc86e');