Skip to content
This repository was archived by the owner on Feb 7, 2025. It is now read-only.

Commit 2644b64

Browse files
ludowkmLuu Duc Dongsimolus3
authored
Implement EIP-1559 support (simolus3#215)
Implement EIP1559 support Co-authored-by: Luu Duc Dong <[email protected]> Co-authored-by: Simon Binder <[email protected]>
1 parent 4f2bed8 commit 2644b64

12 files changed

+308
-48
lines changed

.github/workflows/ci.yml

+1-9
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,6 @@ jobs:
2525
PUB_CACHE: ".dart_tool/pub_cache"
2626
run: dart pub upgrade
2727

28-
ensure_formatted:
29-
name: "Formatting"
30-
runs-on: ubuntu-latest
31-
container:
32-
image: google/dart
33-
steps:
34-
- uses: actions/checkout@v2
35-
- run: "dart format --output=none --set-exit-if-changed ."
36-
3728
analyze:
3829
name: "Analysis"
3930
needs: get_dependencies
@@ -45,6 +36,7 @@ jobs:
4536
path: .dart_tool
4637
key: dart-dependencies-${{ hashFiles('pubspec.yaml') }}
4738
- uses: dart-lang/setup-dart@v1
39+
- run: "dart format --output=none --set-exit-if-changed ."
4840
- run: dart analyze --fatal-infos
4941

5042
vm_tests:

CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 2.3.2
2+
3+
- Support EIP-1559 transactions.
4+
15
## 2.3.1
26

37
- Fix the `Web3Client.custom` constructor not setting all required fields.

lib/src/browser/credentials.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ class MetaMaskCredentials extends CredentialsWithKnownAddress
2222
: address = EthereumAddress.fromHex(hexAddress);
2323

2424
@override
25-
Future<MsgSignature> signToSignature(Uint8List payload, {int? chainId}) {
25+
Future<MsgSignature> signToSignature(Uint8List payload,
26+
{int? chainId, bool isEIP1559 = false}) {
2627
throw UnsupportedError('Signing raw payloads is not supported on MetaMask');
2728
}
2829

lib/src/core/block_information.dart

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import 'package:web3dart/src/crypto/formatting.dart';
2+
import 'package:web3dart/web3dart.dart';
3+
4+
class BlockInformation {
5+
EtherAmount? baseFeePerGas;
6+
7+
BlockInformation({this.baseFeePerGas});
8+
9+
factory BlockInformation.fromJson(Map<String, dynamic> json) {
10+
return BlockInformation(
11+
baseFeePerGas: json.containsKey('baseFeePerGas')
12+
? EtherAmount.fromUnitAndValue(
13+
EtherUnit.wei, hexToInt(json['baseFeePerGas'] as String))
14+
: null);
15+
}
16+
17+
bool get isSupportEIP1559 => baseFeePerGas != null;
18+
}

lib/src/core/client.dart

+23-1
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ class Web3Client {
112112
return _makeRPCCall<String>('net_version').then(int.parse);
113113
}
114114

115+
Future<BigInt> getChainId() {
116+
return _makeRPCCall<String>('eth_chainId').then(BigInt.parse);
117+
}
118+
115119
/// Returns true if the node is actively listening for network connections.
116120
Future<bool> isListeningForNetwork() {
117121
return _makeRPCCall('net_listening');
@@ -179,6 +183,13 @@ class Web3Client {
179183
.then((s) => hexToInt(s).toInt());
180184
}
181185

186+
Future<BlockInformation> getBlockInformation(
187+
{String blockNumber = 'latest', bool isContainFullObj = true}) {
188+
return _makeRPCCall<Map<String, dynamic>>(
189+
'eth_getBlockByNumber', [blockNumber, isContainFullObj])
190+
.then((json) => BlockInformation.fromJson(json));
191+
}
192+
182193
/// Gets the balance of the account with the specified address.
183194
///
184195
/// This function allows specifying a custom block mined in the past to get
@@ -271,9 +282,13 @@ class Web3Client {
271282
return cred.sendTransaction(transaction);
272283
}
273284

274-
final signed = await signTransaction(cred, transaction,
285+
var signed = await signTransaction(cred, transaction,
275286
chainId: chainId, fetchChainIdFromNetworkId: fetchChainIdFromNetworkId);
276287

288+
if (transaction.isEIP1559) {
289+
signed = prependTransactionType(0x02, signed);
290+
}
291+
277292
return sendRawTransaction(signed);
278293
}
279294

@@ -348,6 +363,8 @@ class Web3Client {
348363
EtherAmount? value,
349364
BigInt? amountOfGas,
350365
EtherAmount? gasPrice,
366+
EtherAmount? maxPriorityFeePerGas,
367+
EtherAmount? maxFeePerGas,
351368
Uint8List? data,
352369
@Deprecated('Parameter is ignored') BlockNum? atBlock,
353370
}) async {
@@ -360,6 +377,11 @@ class Web3Client {
360377
if (amountOfGas != null) 'gas': '0x${amountOfGas.toRadixString(16)}',
361378
if (gasPrice != null)
362379
'gasPrice': '0x${gasPrice.getInWei.toRadixString(16)}',
380+
if (maxPriorityFeePerGas != null)
381+
'maxPriorityFeePerGas':
382+
'0x${maxPriorityFeePerGas.getInWei.toRadixString(16)}',
383+
if (maxFeePerGas != null)
384+
'maxFeePerGas': '0x${maxFeePerGas.getInWei.toRadixString(16)}',
363385
if (value != null) 'value': '0x${value.getInWei.toRadixString(16)}',
364386
if (data != null) 'data': bytesToHex(data, include0x: true),
365387
},

lib/src/core/transaction.dart

+25-12
Original file line numberDiff line numberDiff line change
@@ -40,26 +40,33 @@ class Transaction {
4040
/// have already been sent by [from].
4141
final int? nonce;
4242

43+
final EtherAmount? maxPriorityFeePerGas;
44+
final EtherAmount? maxFeePerGas;
45+
4346
Transaction(
4447
{this.from,
4548
this.to,
4649
this.maxGas,
4750
this.gasPrice,
4851
this.value,
4952
this.data,
50-
this.nonce});
53+
this.nonce,
54+
this.maxFeePerGas,
55+
this.maxPriorityFeePerGas});
5156

5257
/// Constructs a transaction that can be used to call a contract function.
53-
Transaction.callContract({
54-
required DeployedContract contract,
55-
required ContractFunction function,
56-
required List<dynamic> parameters,
57-
this.from,
58-
this.maxGas,
59-
this.gasPrice,
60-
this.value,
61-
this.nonce,
62-
}) : to = contract.address,
58+
Transaction.callContract(
59+
{required DeployedContract contract,
60+
required ContractFunction function,
61+
required List<dynamic> parameters,
62+
this.from,
63+
this.maxGas,
64+
this.gasPrice,
65+
this.value,
66+
this.nonce,
67+
this.maxFeePerGas,
68+
this.maxPriorityFeePerGas})
69+
: to = contract.address,
6370
data = function.encodeCall(parameters);
6471

6572
Transaction copyWith(
@@ -69,7 +76,9 @@ class Transaction {
6976
EtherAmount? gasPrice,
7077
EtherAmount? value,
7178
Uint8List? data,
72-
int? nonce}) {
79+
int? nonce,
80+
EtherAmount? maxPriorityFeePerGas,
81+
EtherAmount? maxFeePerGas}) {
7382
return Transaction(
7483
from: from ?? this.from,
7584
to: to ?? this.to,
@@ -78,6 +87,10 @@ class Transaction {
7887
value: value ?? this.value,
7988
data: data ?? this.data,
8089
nonce: nonce ?? this.nonce,
90+
maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas,
91+
maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas,
8192
);
8293
}
94+
95+
bool get isEIP1559 => maxFeePerGas != null && maxPriorityFeePerGas != null;
8396
}

lib/src/core/transaction_information.dart

+11-1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class TransactionReceipt {
8787
this.from,
8888
this.to,
8989
this.gasUsed,
90+
this.effectiveGasPrice,
9091
this.logs = const []});
9192

9293
TransactionReceipt.fromMap(Map<String, dynamic> map)
@@ -105,6 +106,10 @@ class TransactionReceipt {
105106
cumulativeGasUsed = hexToInt(map['cumulativeGasUsed'] as String),
106107
gasUsed =
107108
map['gasUsed'] != null ? hexToInt(map['gasUsed'] as String) : null,
109+
effectiveGasPrice = map['effectiveGasPrice'] != null
110+
? EtherAmount.inWei(
111+
BigInt.parse(map['effectiveGasPrice'] as String))
112+
: null,
108113
contractAddress = map['contractAddress'] != null
109114
? EthereumAddress.fromHex(map['contractAddress'] as String)
110115
: null,
@@ -153,13 +158,16 @@ class TransactionReceipt {
153158
/// Array of logs generated by this transaction.
154159
final List<FilterEvent> logs;
155160

161+
final EtherAmount? effectiveGasPrice;
162+
156163
@override
157164
String toString() {
158165
return 'TransactionReceipt{transactionHash: ${bytesToHex(transactionHash)}, '
159166
'transactionIndex: $transactionIndex, blockHash: ${bytesToHex(blockHash)}, '
160167
'blockNumber: $blockNumber, from: ${from?.hex}, to: ${to?.hex}, '
161168
'cumulativeGasUsed: $cumulativeGasUsed, gasUsed: $gasUsed, '
162-
'contractAddress: ${contractAddress?.hex}, status: $status, logs: $logs}';
169+
'contractAddress: ${contractAddress?.hex}, status: $status, '
170+
'effectiveGasPrice: $effectiveGasPrice, logs: $logs}';
163171
}
164172

165173
@override
@@ -177,6 +185,7 @@ class TransactionReceipt {
177185
gasUsed == other.gasUsed &&
178186
contractAddress == other.contractAddress &&
179187
status == other.status &&
188+
effectiveGasPrice == other.effectiveGasPrice &&
180189
const ListEquality().equals(logs, other.logs);
181190

182191
@override
@@ -191,5 +200,6 @@ class TransactionReceipt {
191200
gasUsed.hashCode ^
192201
contractAddress.hashCode ^
193202
status.hashCode ^
203+
effectiveGasPrice.hashCode ^
194204
logs.hashCode;
195205
}

lib/src/core/transaction_signer.dart

+69-16
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,23 @@ Future<_SigningInput> _fillMissingData({
2323

2424
final sender = transaction.from ?? await credentials.extractAddress();
2525
var gasPrice = transaction.gasPrice;
26-
var nonce = transaction.nonce;
27-
if (gasPrice == null || nonce == null) {
28-
if (client == null) {
29-
throw ArgumentError("Can't find suitable gas price and nonce from client "
30-
'because no client is set. Please specify a gas price on the '
31-
'transaction.');
32-
}
33-
gasPrice ??= await client.getGasPrice();
34-
nonce ??= await client.getTransactionCount(sender,
35-
atBlock: const BlockNum.pending());
26+
27+
if (client == null &&
28+
(transaction.nonce == null ||
29+
transaction.maxGas == null ||
30+
loadChainIdFromNetwork) ||
31+
(!transaction.isEIP1559 && gasPrice == null)) {
32+
throw ArgumentError('Client is required to perform network actions');
33+
}
34+
35+
if (!transaction.isEIP1559 && gasPrice == null) {
36+
gasPrice = await client!.getGasPrice();
3637
}
3738

39+
final nonce = transaction.nonce ??
40+
await client!
41+
.getTransactionCount(sender, atBlock: const BlockNum.pending());
42+
3843
final maxGas = transaction.maxGas ??
3944
await client!
4045
.estimateGas(
@@ -43,6 +48,8 @@ Future<_SigningInput> _fillMissingData({
4348
data: transaction.data,
4449
value: transaction.value,
4550
gasPrice: gasPrice,
51+
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
52+
maxFeePerGas: transaction.maxFeePerGas,
4653
)
4754
.then((bigInt) => bigInt.toInt());
4855

@@ -60,12 +67,7 @@ Future<_SigningInput> _fillMissingData({
6067
if (!loadChainIdFromNetwork) {
6168
resolvedChainId = chainId!;
6269
} else {
63-
if (client == null) {
64-
throw ArgumentError(
65-
"Can't load chain id from network when no client is set");
66-
}
67-
68-
resolvedChainId = await client.getNetworkId();
70+
resolvedChainId = await client!.getNetworkId();
6971
}
7072

7173
return _SigningInput(
@@ -75,8 +77,27 @@ Future<_SigningInput> _fillMissingData({
7577
);
7678
}
7779

80+
Uint8List prependTransactionType(int type, Uint8List transaction) {
81+
return Uint8List(transaction.length + 1)
82+
..[0] = type
83+
..setAll(1, transaction);
84+
}
85+
7886
Future<Uint8List> _signTransaction(
7987
Transaction transaction, Credentials c, int? chainId) async {
88+
if (transaction.isEIP1559 && chainId != null) {
89+
final encodedTx = LengthTrackingByteSink();
90+
encodedTx.addByte(0x02);
91+
encodedTx.add(rlp
92+
.encode(_encodeEIP1559ToRlp(transaction, null, BigInt.from(chainId))));
93+
94+
encodedTx.close();
95+
final signature = await c.signToSignature(encodedTx.asBytes(),
96+
chainId: chainId, isEIP1559: transaction.isEIP1559);
97+
98+
return uint8ListFromList(rlp.encode(
99+
_encodeEIP1559ToRlp(transaction, signature, BigInt.from(chainId))));
100+
}
80101
final innerSignature =
81102
chainId == null ? null : MsgSignature(BigInt.zero, BigInt.zero, chainId);
82103

@@ -87,6 +108,38 @@ Future<Uint8List> _signTransaction(
87108
return uint8ListFromList(rlp.encode(_encodeToRlp(transaction, signature)));
88109
}
89110

111+
List<dynamic> _encodeEIP1559ToRlp(
112+
Transaction transaction, MsgSignature? signature, BigInt chainId) {
113+
final list = [
114+
chainId,
115+
transaction.nonce,
116+
transaction.maxPriorityFeePerGas!.getInWei,
117+
transaction.maxFeePerGas!.getInWei,
118+
transaction.maxGas,
119+
];
120+
121+
if (transaction.to != null) {
122+
list.add(transaction.to!.addressBytes);
123+
} else {
124+
list.add('');
125+
}
126+
127+
list
128+
..add(transaction.value?.getInWei)
129+
..add(transaction.data);
130+
131+
list.add([]); // access list
132+
133+
if (signature != null) {
134+
list
135+
..add(signature.v)
136+
..add(signature.r)
137+
..add(signature.s);
138+
}
139+
140+
return list;
141+
}
142+
90143
List<dynamic> _encodeToRlp(Transaction transaction, MsgSignature? signature) {
91144
final list = [
92145
transaction.nonce,

0 commit comments

Comments
 (0)