Skip to content
This repository has been archived by the owner on Feb 4, 2022. It is now read-only.

Commit

Permalink
Implement EIP-1559 support (#215)
Browse files Browse the repository at this point in the history
Implement EIP1559 support

Co-authored-by: Luu Duc Dong <luuducdong@Ludo-Macbook-Pro.local>
Co-authored-by: Simon Binder <oss@simonbinder.eu>
  • Loading branch information
3 people authored Nov 4, 2021
1 parent 4f2bed8 commit 2644b64
Show file tree
Hide file tree
Showing 12 changed files with 308 additions and 48 deletions.
10 changes: 1 addition & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.3.2

- Support EIP-1559 transactions.

## 2.3.1

- Fix the `Web3Client.custom` constructor not setting all required fields.
Expand Down
3 changes: 2 additions & 1 deletion lib/src/browser/credentials.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ class MetaMaskCredentials extends CredentialsWithKnownAddress
: address = EthereumAddress.fromHex(hexAddress);

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

Expand Down
18 changes: 18 additions & 0 deletions lib/src/core/block_information.dart
Original file line number Diff line number Diff line change
@@ -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<String, dynamic> json) {
return BlockInformation(
baseFeePerGas: json.containsKey('baseFeePerGas')
? EtherAmount.fromUnitAndValue(
EtherUnit.wei, hexToInt(json['baseFeePerGas'] as String))
: null);
}

bool get isSupportEIP1559 => baseFeePerGas != null;
}
24 changes: 23 additions & 1 deletion lib/src/core/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ class Web3Client {
return _makeRPCCall<String>('net_version').then(int.parse);
}

Future<BigInt> getChainId() {
return _makeRPCCall<String>('eth_chainId').then(BigInt.parse);
}

/// Returns true if the node is actively listening for network connections.
Future<bool> isListeningForNetwork() {
return _makeRPCCall('net_listening');
Expand Down Expand Up @@ -179,6 +183,13 @@ class Web3Client {
.then((s) => hexToInt(s).toInt());
}

Future<BlockInformation> getBlockInformation(
{String blockNumber = 'latest', bool isContainFullObj = true}) {
return _makeRPCCall<Map<String, dynamic>>(
'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
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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),
},
Expand Down
37 changes: 25 additions & 12 deletions lib/src/core/transaction.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,33 @@ class Transaction {
/// have already been sent by [from].
final int? nonce;

final EtherAmount? maxPriorityFeePerGas;
final EtherAmount? maxFeePerGas;

Transaction(
{this.from,
this.to,
this.maxGas,
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<dynamic> parameters,
this.from,
this.maxGas,
this.gasPrice,
this.value,
this.nonce,
}) : to = contract.address,
Transaction.callContract(
{required DeployedContract contract,
required ContractFunction function,
required List<dynamic> parameters,
this.from,
this.maxGas,
this.gasPrice,
this.value,
this.nonce,
this.maxFeePerGas,
this.maxPriorityFeePerGas})
: to = contract.address,
data = function.encodeCall(parameters);

Transaction copyWith(
Expand All @@ -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,
Expand All @@ -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;
}
12 changes: 11 additions & 1 deletion lib/src/core/transaction_information.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class TransactionReceipt {
this.from,
this.to,
this.gasUsed,
this.effectiveGasPrice,
this.logs = const []});

TransactionReceipt.fromMap(Map<String, dynamic> map)
Expand All @@ -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,
Expand Down Expand Up @@ -153,13 +158,16 @@ class TransactionReceipt {
/// Array of logs generated by this transaction.
final List<FilterEvent> 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
Expand All @@ -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
Expand All @@ -191,5 +200,6 @@ class TransactionReceipt {
gasUsed.hashCode ^
contractAddress.hashCode ^
status.hashCode ^
effectiveGasPrice.hashCode ^
logs.hashCode;
}
85 changes: 69 additions & 16 deletions lib/src/core/transaction_signer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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());

Expand All @@ -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(
Expand All @@ -75,8 +77,27 @@ Future<_SigningInput> _fillMissingData({
);
}

Uint8List prependTransactionType(int type, Uint8List transaction) {
return Uint8List(transaction.length + 1)
..[0] = type
..setAll(1, transaction);
}

Future<Uint8List> _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);

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

List<dynamic> _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<dynamic> _encodeToRlp(Transaction transaction, MsgSignature? signature) {
final list = [
transaction.nonce,
Expand Down
Loading

0 comments on commit 2644b64

Please sign in to comment.