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

Implement EIP1559 support #215

Merged
merged 4 commits into from
Nov 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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