├── dart_test.yaml ├── example ├── pubspec.yaml └── lib │ ├── main.dart │ ├── token.abi.json │ ├── contracts.dart │ └── token.g.dart ├── lib ├── src │ ├── utils │ │ ├── equality.dart │ │ ├── typed_data.dart │ │ ├── uuid.dart │ │ ├── length_tracking_byte_sink.dart │ │ └── rlp.dart │ ├── crypto │ │ ├── keccak.dart │ │ ├── random_bridge.dart │ │ ├── formatting.dart │ │ └── secp256k1.dart │ ├── credentials │ │ ├── did.dart │ │ ├── credentials.dart │ │ └── wallet.dart │ ├── contracts │ │ ├── generated_contract.dart │ │ ├── deployed_contract.dart │ │ └── abi │ │ │ ├── tuple.dart │ │ │ ├── types.dart │ │ │ ├── integers.dart │ │ │ └── arrays.dart │ └── core │ │ ├── block_number.dart │ │ ├── transaction.dart │ │ ├── transaction_signer.dart │ │ ├── transaction_information.dart │ │ └── filters.dart ├── web3dart.dart └── json_rpc.dart ├── .github └── workflows │ ├── publish_pubdev.yml │ └── ci.yml ├── test ├── contracts │ └── abi │ │ ├── utils.dart │ │ ├── integers_test.dart │ │ ├── array_of_dynamic_type.dart │ │ ├── data │ │ ├── integers.dart │ │ └── basic_abi_tests.dart │ │ ├── event_test.dart │ │ ├── types_test.dart │ │ ├── encoding_test.dart │ │ ├── tuple_test.dart │ │ └── functions_test.dart ├── core │ ├── client_test.dart │ ├── block_parameter_test.dart │ ├── event_filter_test.dart │ ├── transaction_information_test.dart │ └── sign_transaction_test.dart ├── crypto │ ├── formatting_test.dart │ ├── random_bridge_test.dart │ └── secp256k1_test.dart ├── utils │ ├── rlp_test.dart │ └── rlp_test_vectors.dart ├── credentials │ ├── wallet_test.dart │ ├── private_key_test.dart │ ├── ethr_did_test.dart │ ├── address_test.dart │ ├── example_keystores.dart │ └── public_key_test.dart ├── json_rpc_test.dart └── mock_client.dart ├── pubspec.yaml ├── LICENSE ├── .gitignore ├── analysis_options.yaml ├── CHANGELOG.md └── README.md /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | expensive: 3 | timeout: 2x 4 | 5 | #override_platforms: 6 | # firefox: 7 | # settings: 8 | # arguments: -headless -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | publish_to: none 3 | version: 0.0.1 4 | homepage: https://pwa.ir 5 | repository: https://github.com/xclud/web3dart 6 | 7 | environment: 8 | sdk: ">=2.12.0 <4.0.0" 9 | 10 | dependencies: 11 | web3dart: 12 | path: ../ 13 | http: ^1.1.0 14 | wallet: ^0.0.17 15 | web_socket_channel: ^2.2.0 16 | 17 | dev_dependencies: 18 | test: ^1.22.0 19 | coverage: ^1.1.0 20 | lints: ^2.0.0 21 | -------------------------------------------------------------------------------- /lib/src/utils/equality.dart: -------------------------------------------------------------------------------- 1 | bool equals(List? left, List? right) { 2 | if (identical(left, right)) return true; 3 | 4 | if (left == null || right == null) { 5 | return false; 6 | } 7 | 8 | if (left.length != right.length) { 9 | return false; 10 | } 11 | 12 | for (int i = 0; i < left.length; i++) { 13 | if (left[i] != right[i]) { 14 | return false; 15 | } 16 | } 17 | 18 | return true; 19 | } 20 | -------------------------------------------------------------------------------- /lib/src/crypto/keccak.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | final KeccakDigest keccakDigest = KeccakDigest(256); 4 | 5 | Uint8List keccak256(Uint8List input) { 6 | keccakDigest.reset(); 7 | return keccakDigest.process(input); 8 | } 9 | 10 | Uint8List keccakUtf8(String input) { 11 | return keccak256(uint8ListFromList(utf8.encode(input))); 12 | } 13 | 14 | Uint8List keccakAscii(String input) { 15 | return keccak256(ascii.encode(input)); 16 | } 17 | -------------------------------------------------------------------------------- /lib/src/utils/typed_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | Uint8List uint8ListFromList(List data) { 4 | if (data is Uint8List) return data; 5 | 6 | return Uint8List.fromList(data); 7 | } 8 | 9 | Uint8List padUint8ListTo32(Uint8List data) { 10 | assert(data.length <= 32); 11 | if (data.length == 32) return data; 12 | 13 | // todo there must be a faster way to do this? 14 | return Uint8List(32)..setRange(32 - data.length, 32, data); 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/publish_pubdev.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Pub.dev 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | publishing: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: "Checkout" 12 | uses: actions/checkout@v2 # required! 13 | 14 | - name: "web3dart" 15 | uses: k-paxian/dart-package-publisher@master 16 | with: 17 | credentialJson: ${{ secrets.CREDENTIAL_JSON }} 18 | format: true 19 | -------------------------------------------------------------------------------- /test/contracts/abi/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:test/test.dart'; 4 | 5 | import 'package:web3dart/web3dart.dart'; 6 | 7 | void expectEncodes(AbiType type, T data, String encoded) { 8 | final buffer = LengthTrackingByteSink(); 9 | type.encode(data, buffer); 10 | 11 | expect(bytesToHex(buffer.asBytes(), include0x: false), encoded); 12 | } 13 | 14 | ByteBuffer bufferFromHex(String hex) { 15 | return hexToBytes(hex).buffer; 16 | } 17 | -------------------------------------------------------------------------------- /test/core/client_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | import '../mock_client.dart'; 5 | 6 | void main() { 7 | test('getClientVersion', () async { 8 | final client = MockClient( 9 | expectAsync2((method, data) { 10 | expect(method, 'web3_clientVersion'); 11 | return 'dart-web3dart-test'; 12 | }), 13 | ); 14 | final web3 = Web3Client('', client); 15 | addTearDown(web3.dispose); 16 | 17 | expect(web3.getClientVersion(), completion('dart-web3dart-test')); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /test/crypto/formatting_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | void main() { 5 | test('strip 0x prefix', () { 6 | expect(strip0x('0x12F312319235'), '12F312319235'); 7 | expect(strip0x('123123'), '123123'); 8 | }); 9 | 10 | test('hexToDartInt', () { 11 | expect(hexToDartInt('0x123'), 0x123); 12 | expect(hexToDartInt('0xff'), 0xff); 13 | expect(hexToDartInt('abcdef'), 0xabcdef); 14 | }); 15 | 16 | test('bytesToHex', () { 17 | expect(bytesToHex([3], padToEvenLength: true), '03'); 18 | expect(bytesToHex([3], forcePadLength: 3), '003'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/contracts/abi/integers_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | void main() { 7 | group('encodes', () { 8 | test('negative int32 values', () { 9 | const type = IntType(length: 32); 10 | expectEncodes(type, BigInt.from(-200), '${'f' * 62}38'); 11 | }); 12 | }); 13 | 14 | group('decodes', () { 15 | test('negative int32 values', () { 16 | const type = IntType(length: 32); 17 | final decoded = type.decode(bufferFromHex('${'f' * 62}38'), 0); 18 | expect(decoded.bytesRead, 32); 19 | expect(decoded.data, BigInt.from(-200)); 20 | }); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: web3dart 2 | description: Dart library to connect to Ethereum clients. Send transactions and interact with smart contracts! 3 | version: 3.0.1 4 | homepage: https://pwa.ir 5 | repository: https://github.com/xclud/web3dart 6 | 7 | environment: 8 | sdk: ">=2.12.0 <4.0.0" 9 | 10 | dependencies: 11 | pointycastle: ^4.0.0 12 | sec: ^1.1.1 13 | http: ">=0.13.1 <2.0.0" 14 | uuid: ^4.0.0 15 | json_rpc_2: ^4.0.0 16 | stream_transform: ^2.1.0 17 | stream_channel: ^2.1.2 18 | eip55: ^1.0.3 19 | eip1559: ^0.6.2 20 | typed_data: ^1.4.0 21 | convert: ^3.1.1 22 | wallet: ^0.0.18 23 | 24 | dev_dependencies: 25 | test: ^1.25.0 26 | lints: ^5.0.0 27 | async: ^2.13.0 28 | -------------------------------------------------------------------------------- /test/core/block_parameter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | // https://github.com/ethereum/wiki/wiki/JSON-RPC#the-default-block-parameter 5 | const blockParameters = { 6 | 'latest': BlockNum.current(), 7 | 'earliest': BlockNum.genesis(), 8 | 'pending': BlockNum.pending(), 9 | '0x40': BlockNum.exact(64), 10 | }; 11 | 12 | void main() { 13 | test('block parameters encode', () { 14 | blockParameters.forEach((encoded, block) { 15 | expect(block.toBlockParam(), encoded); 16 | }); 17 | }); 18 | 19 | test('pending block param is pending', () { 20 | expect(const BlockNum.pending().isPending, true); 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /test/utils/rlp_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | 6 | import 'rlp_test_vectors.dart' as data; 7 | 8 | void main() { 9 | final testContent = json.decode(data.content) as Map; 10 | 11 | for (final key in testContent.keys) { 12 | test('$key', () { 13 | final data = testContent[key]; 14 | final input = _mapTestData(data['in']); 15 | final output = data['out'] as String; 16 | 17 | expect(bytesToHex(encode(input), include0x: true), output); 18 | }); 19 | } 20 | } 21 | 22 | dynamic _mapTestData(dynamic data) { 23 | if (data is String && data.startsWith('#')) { 24 | return BigInt.parse(data.substring(1)); 25 | } 26 | 27 | return data; 28 | } 29 | -------------------------------------------------------------------------------- /lib/src/utils/uuid.dart: -------------------------------------------------------------------------------- 1 | import 'dart:typed_data'; 2 | 3 | import 'package:convert/convert.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | const _uuid = Uuid(); 7 | 8 | /// Formats the [uuid] bytes as an uuid. 9 | String formatUuid(List uuid) => Uuid.unparse(uuid); 10 | 11 | /// Generates a v4 uuid. 12 | Uint8List generateUuidV4() { 13 | final buffer = Uint8List(16); 14 | _uuid.v4buffer(buffer); 15 | return buffer; 16 | } 17 | 18 | Uint8List parseUuid(String uuid) { 19 | // Unfortunately, package:uuid is to strict when parsing uuids, the example 20 | // ids don't work 21 | final withoutDashes = uuid.replaceAll('-', ''); 22 | final asBytes = hex.decode(withoutDashes); 23 | return asBytes is Uint8List ? asBytes : Uint8List.fromList(asBytes); 24 | } 25 | -------------------------------------------------------------------------------- /lib/src/utils/length_tracking_byte_sink.dart: -------------------------------------------------------------------------------- 1 | part of 'package:web3dart/web3dart.dart'; 2 | 3 | class LengthTrackingByteSink extends ByteConversionSinkBase { 4 | final Uint8Buffer _buffer = Uint8Buffer(); 5 | int _length = 0; 6 | 7 | int get length => _length; 8 | 9 | Uint8List asBytes() { 10 | return _buffer.buffer.asUint8List(0, _length); 11 | } 12 | 13 | @override 14 | void add(List chunk) { 15 | _buffer.addAll(chunk); 16 | _length += chunk.length; 17 | } 18 | 19 | void addByte(int byte) { 20 | _buffer.add(byte); 21 | _length++; 22 | } 23 | 24 | void setRange(int start, int end, List content) { 25 | _buffer.setRange(start, end, content); 26 | } 27 | 28 | @override 29 | void close() { 30 | // no-op, never used 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/credentials/wallet_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | 6 | import 'example_keystores.dart' as data; 7 | 8 | void main() { 9 | final wallets = json.decode(data.content) as Map; 10 | 11 | wallets.forEach((dynamic testName, dynamic content) { 12 | test( 13 | 'unlocks wallet $testName', 14 | () { 15 | final password = content['password'] as String; 16 | final privateKey = content['priv'] as String; 17 | final walletData = content['json'] as Map; 18 | 19 | final wallet = Wallet.fromJson(json.encode(walletData), password); 20 | expect(bytesToHex(wallet.privateKey.privateKey), privateKey); 21 | 22 | final encodedWallet = json.decode(wallet.toJson()) as Map; 23 | 24 | expect( 25 | encodedWallet['crypto']['ciphertext'], 26 | walletData['crypto']['ciphertext'], 27 | ); 28 | }, 29 | tags: 'expensive', 30 | ); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Simon Binder 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:http/http.dart'; 4 | import 'package:wallet/wallet.dart'; 5 | import 'package:web3dart/web3dart.dart'; 6 | 7 | const String privateKey = 8 | 'a2fd51b96dc55aeb14b30d55a6b3121c7b9c599500c1beb92a389c3377adc86e'; 9 | const String rpcUrl = 'http://localhost:7545'; 10 | 11 | Future main() async { 12 | // start a client we can use to send transactions 13 | final client = Web3Client(rpcUrl, Client()); 14 | 15 | final credentials = EthPrivateKey.fromHex(privateKey); 16 | final address = credentials.address; 17 | 18 | print(address.eip55With0x); 19 | print(await client.getBalance(address)); 20 | 21 | await client.sendTransaction( 22 | credentials, 23 | Transaction( 24 | to: EthereumAddress.fromHex('0xC914Bb2ba888e3367bcecEb5C2d99DF7C7423706'), 25 | gasPrice: EtherAmount.inWei(BigInt.one), 26 | maxGas: 100000, 27 | value: EtherAmount.fromInt(EtherUnit.ether, 1), 28 | ), 29 | ); 30 | 31 | await client.dispose(); 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/credentials/did.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | class EthrDID { 4 | const EthrDID(this.did); 5 | 6 | factory EthrDID.fromPublicKeyEncoded({ 7 | required EthPrivateKey credentials, 8 | required String chainNameOrId, 9 | }) { 10 | var did = 'did:ethr:$chainNameOrId:${bytesToHex( 11 | credentials.publicKey.getEncoded(), 12 | include0x: true, 13 | )}'; 14 | return EthrDID(did); 15 | } 16 | 17 | factory EthrDID.fromEthereumAddress({ 18 | required EthereumAddress address, 19 | required String chainNameOrId, 20 | }) { 21 | var did = 'did:ethr:$chainNameOrId:${address.eip55With0x}'; 22 | return EthrDID(did); 23 | } 24 | 25 | /// `identifier` is Ethereum address, public key or a full did:ethr representing Identity 26 | /// 27 | /// https://github.com/uport-project/ethr-did#configuration 28 | factory EthrDID.fromIdentifier({ 29 | required String identifier, 30 | required String chainNameOrId, 31 | }) { 32 | var did = 'did:ethr:$chainNameOrId:$identifier'; 33 | return EthrDID(did); 34 | } 35 | 36 | final String did; 37 | } 38 | -------------------------------------------------------------------------------- /test/contracts/abi/array_of_dynamic_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | const encoded = 7 | // first string starts at offset 64 = 0x40 8 | '0000000000000000000000000000000000000000000000000000000000000040' 9 | // second string starts at offset 128 = 0x80 10 | '0000000000000000000000000000000000000000000000000000000000000080' 11 | // utf8('Hello').length = 5 12 | '0000000000000000000000000000000000000000000000000000000000000005' 13 | // utf-8 encoding of 'hello', right-padded to fill 32 bytes 14 | '48656c6c6f000000000000000000000000000000000000000000000000000000' 15 | // utf8('world').length = 5 16 | '0000000000000000000000000000000000000000000000000000000000000005' 17 | // utf-8 encoding of 'world', again with padding 18 | '776f726c64000000000000000000000000000000000000000000000000000000'; 19 | 20 | void main() { 21 | const type = FixedLengthArray(type: StringType(), length: 2); 22 | 23 | test('encodes', () { 24 | expectEncodes(type, ['Hello', 'world'], encoded); 25 | }); 26 | 27 | test('decodes', () { 28 | final decoded = type.decode(bufferFromHex(encoded), 0); 29 | 30 | expect(decoded.bytesRead, encoded.length ~/ 2); 31 | expect(decoded.data, ['Hello', 'world']); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /test/contracts/abi/data/integers.dart: -------------------------------------------------------------------------------- 1 | // Note: Our parser will pick up the D suffix as a BigInt (radix 10) and H as a 2 | // hex BigInt 3 | const content = r''' 4 | { 5 | "uInts": { 6 | "args": [ 7 | 0, 8 | "7fffffffffffffffH", 9 | "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffH" 10 | ], 11 | "result": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 12 | "types": ["uint256", "uint256", "uint256"] 13 | }, 14 | "ints": { 15 | "args": [ 16 | 0, 17 | "9223372036854775807D", 18 | "-9223372036854775808D", 19 | -1 20 | ], 21 | "result": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 22 | "types": ["int256", "int256", "int256", "int256"] 23 | }, 24 | "booleans": { 25 | "args": [ 26 | false, 27 | true 28 | ], 29 | "result": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 30 | "types": ["bool", "bool"] 31 | } 32 | } 33 | '''; 34 | -------------------------------------------------------------------------------- /test/contracts/abi/event_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | import 'package:wallet/wallet.dart'; 4 | 5 | void main() { 6 | final event = ContractEvent(false, 'Transfer', const [ 7 | EventComponent(FunctionParameter('from', AddressType()), true), 8 | EventComponent(FunctionParameter('to', AddressType()), true), 9 | EventComponent(FunctionParameter('amount', UintType()), false), 10 | ]); 11 | 12 | test('creates signature', () { 13 | expect( 14 | bytesToHex(event.signature), 15 | 'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', 16 | ); 17 | }); 18 | 19 | test('decodes return data', () { 20 | const topics = [ 21 | '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', 22 | '0x000000000000000000000000Dd611f2b2CaF539aC9e12CF84C09CB9bf81CA37F', 23 | '0x0000000000000000000000006c87E1a114C3379BEc929f6356c5263d62542C13', 24 | ]; 25 | const data = 26 | '0x0000000000000000000000000000000000000000000000000000000000001234'; 27 | 28 | final decoded = event.decodeResults(topics, data); 29 | 30 | expect(decoded, [ 31 | EthereumAddress.fromHex('0xDd611f2b2CaF539aC9e12CF84C09CB9bf81CA37F'), 32 | EthereumAddress.fromHex('0x6c87E1a114C3379BEc929f6356c5263d62542C13'), 33 | BigInt.from(0x1234), 34 | ]); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/credentials/private_key_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | 6 | void main() { 7 | test('signs messages', () { 8 | final key = EthPrivateKey( 9 | hexToBytes( 10 | 'a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6', 11 | ), 12 | ); 13 | final signature = 14 | key.signPersonalMessageToUint8List(ascii.encode('A test message')); 15 | 16 | expect( 17 | bytesToHex(signature), 18 | '0464eee9e2fe1a10ffe48c78b80de1ed8dcf996f3f60955cb2e03cb21903d93006624da478b3f862582e85b31c6a21c6cae2eee2bd50f55c93c4faad9d9c8d7f1c', 19 | ); 20 | }); 21 | 22 | test('signs message for chainId', () { 23 | // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/test/index.js#L532 24 | final key = EthPrivateKey( 25 | hexToBytes( 26 | '3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1', 27 | ), 28 | ); 29 | final signature = key.signToUint8List( 30 | hexToBytes( 31 | '0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1', 32 | ), 33 | chainId: 3, 34 | ); 35 | 36 | expect( 37 | bytesToHex(signature), 38 | '99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9' 39 | '129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66' 40 | '29', 41 | ); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /lib/src/crypto/random_bridge.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | /// Utility to use dart:math's Random class to generate numbers used by 4 | /// pointycastle. 5 | class RandomBridge implements SecureRandom { 6 | RandomBridge(this.dartRandom); 7 | Random dartRandom; 8 | 9 | @override 10 | String get algorithmName => 'DartRandom'; 11 | 12 | @override 13 | BigInt nextBigInteger(int bitLength) { 14 | final fullBytes = bitLength ~/ 8; 15 | final remainingBits = bitLength % 8; 16 | 17 | // Generate a number from the full bytes. Then, prepend a smaller number 18 | // covering the remaining bits. 19 | final main = bytesToUnsignedInt(nextBytes(fullBytes)); 20 | final additional = dartRandom.nextInt(1 << remainingBits); 21 | return main + (BigInt.from(additional) << (fullBytes * 8)); 22 | } 23 | 24 | @override 25 | Uint8List nextBytes(int count) { 26 | final list = Uint8List(count); 27 | 28 | for (var i = 0; i < list.length; i++) { 29 | list[i] = nextUint8(); 30 | } 31 | 32 | return list; 33 | } 34 | 35 | @override 36 | int nextUint16() => dartRandom.nextInt(1 << 16); 37 | 38 | @override 39 | int nextUint32() { 40 | // this is 2^32. We can't write 1 << 32 because that evaluates to 0 on js 41 | return dartRandom.nextInt(4294967296); 42 | } 43 | 44 | @override 45 | int nextUint8() => dartRandom.nextInt(1 << 8); 46 | 47 | @override 48 | void seed(CipherParameters params) { 49 | // ignore, dartRandom will already be seeded if wanted 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /test/contracts/abi/data/basic_abi_tests.dart: -------------------------------------------------------------------------------- 1 | const content = r''' 2 | { 3 | "GithubWikiTest": { 4 | "args": [ 5 | 291, 6 | [ 7 | 1110, 8 | 1929 9 | ], 10 | "0x31323334353637383930", 11 | "Hello, world!" 12 | ], 13 | "result": "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000", 14 | "types": [ 15 | "uint256", 16 | "uint32[]", 17 | "bytes10", 18 | "string" 19 | ] 20 | }, 21 | "SingleInteger": { 22 | "args": [ 23 | 98127491 24 | ], 25 | "result": "0000000000000000000000000000000000000000000000000000000005d94e83", 26 | "types": [ 27 | "uint256" 28 | ] 29 | }, 30 | "IntegerAndAddress": { 31 | "args": [ 32 | 324124, 33 | "@cd2a3d9f938e13cd947ec05abc7fe734df8dd826" 34 | ], 35 | "result": "000000000000000000000000000000000000000000000000000000000004f21c000000000000000000000000cd2a3d9f938e13cd947ec05abc7fe734df8dd826", 36 | "types": [ 37 | "uint256", 38 | "address" 39 | ] 40 | } 41 | } 42 | '''; 43 | -------------------------------------------------------------------------------- /test/credentials/ethr_did_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | import 'package:wallet/wallet.dart'; 4 | 5 | void main() { 6 | group('Generate ethr DID', () { 7 | test('from Ethereum Address', () { 8 | var address = 9 | EthereumAddress.fromHex('0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb'); 10 | var ethrDID = 11 | EthrDID.fromEthereumAddress(address: address, chainNameOrId: '0x1'); 12 | expect( 13 | ethrDID.did, 14 | 'did:ethr:0x1:0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 15 | ); 16 | }); 17 | 18 | test('from PublicKey Encoded', () { 19 | final key = EthPrivateKey( 20 | hexToBytes( 21 | 'a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6', 22 | ), 23 | ); 24 | var ethrDID = 25 | EthrDID.fromPublicKeyEncoded(credentials: key, chainNameOrId: '0x1'); 26 | expect( 27 | bytesToHex(key.publicKey.getEncoded(), include0x: true), 28 | '0x02506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba', 29 | ); 30 | expect( 31 | ethrDID.did, 32 | 'did:ethr:0x1:0x02506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aaba', 33 | ); 34 | }); 35 | 36 | test('from Identifier', () { 37 | var ethrDID = EthrDID.fromIdentifier( 38 | identifier: 'web3dart', 39 | chainNameOrId: 'goerli', 40 | ); 41 | expect( 42 | ethrDID.did, 43 | 'did:ethr:goerli:web3dart', 44 | ); 45 | }); 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | get_dependencies: 11 | name: "Get dependencies" 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: dart-lang/setup-dart@v1 16 | - name: "Print Dart SDK Version" 17 | run: dart --version 18 | - uses: actions/cache@v4 19 | with: 20 | path: .dart_tool 21 | key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} 22 | - name: "Get dependencies" 23 | env: 24 | PUB_CACHE: ".dart_tool/pub_cache" 25 | run: dart pub upgrade 26 | 27 | analyze: 28 | name: "Analysis" 29 | needs: get_dependencies 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v4 33 | - uses: actions/cache@v4 34 | with: 35 | path: .dart_tool 36 | key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} 37 | - uses: dart-lang/setup-dart@v1 38 | - run: "dart format --output=none --set-exit-if-changed ." 39 | - run: dart analyze --fatal-infos 40 | 41 | browser_tests: 42 | name: "Unit Tests (Browser)" 43 | needs: get_dependencies 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v4 47 | - uses: actions/cache@v4 48 | with: 49 | path: .dart_tool 50 | key: dart-dependencies-${{ hashFiles('pubspec.yaml') }} 51 | - uses: dart-lang/setup-dart@v1 52 | - run: dart test -x expensive --platform chrome,firefox 53 | 54 | -------------------------------------------------------------------------------- /lib/src/contracts/generated_contract.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | /// Base classes for generated contracts. 4 | /// 5 | /// web3dart can generate contract classes from abi specifications. For more 6 | /// information, see its readme! 7 | abstract class GeneratedContract { 8 | /// Constructor. 9 | GeneratedContract(this.self, this.client, this.chainId); 10 | final DeployedContract self; 11 | final Web3Client client; 12 | final int? chainId; 13 | 14 | /// Returns whether the [function] has the [expected] selector. 15 | /// 16 | /// This is used in an assert in the generated code. 17 | bool checkSignature(ContractFunction function, String expected) { 18 | return bytesToHex(function.selector) == expected; 19 | } 20 | 21 | Future> read( 22 | ContractFunction function, 23 | List params, 24 | BlockNum? atBlock, 25 | ) { 26 | return client.call( 27 | contract: self, 28 | function: function, 29 | params: params, 30 | atBlock: atBlock, 31 | ); 32 | } 33 | 34 | Future write( 35 | Credentials credentials, 36 | Transaction? base, 37 | ContractFunction function, 38 | List parameters, 39 | ) { 40 | final transaction = base?.copyWith( 41 | data: function.encodeCall(parameters), 42 | to: self.address, 43 | ) ?? 44 | Transaction.callContract( 45 | contract: self, 46 | function: function, 47 | parameters: parameters, 48 | ); 49 | 50 | return client.sendTransaction( 51 | credentials, 52 | transaction, 53 | chainId: chainId, 54 | fetchChainIdFromNetworkId: chainId == null, 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /test/contracts/abi/types_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | final abiTypes = { 5 | 'uint256': const UintType(), 6 | 'int32': const IntType(length: 32), 7 | 'bool': const BoolType(), 8 | 'bytes16[]': const DynamicLengthArray(type: FixedBytes(16)), 9 | 'bytes[16]': const FixedLengthArray(type: DynamicBytes(), length: 16), 10 | '(bool,uint8,string)': 11 | const TupleType([BoolType(), UintType(length: 8), StringType()]), 12 | '(uint256,(bool,bytes8)[6])[]': const DynamicLengthArray( 13 | type: TupleType([ 14 | UintType(), 15 | FixedLengthArray( 16 | type: TupleType([ 17 | BoolType(), 18 | FixedBytes(8), 19 | ]), 20 | length: 6, 21 | ), 22 | ]), 23 | ), 24 | }; 25 | 26 | final invalidTypes = [ 27 | 'uint512', 28 | 'bööl', 29 | '(uint,string', 30 | 'uint19', 31 | 'int32[three]', 32 | ]; 33 | 34 | void main() { 35 | test('calculates padding length', () { 36 | expect(calculatePadLength(0), 32); 37 | expect(calculatePadLength(0, allowEmpty: true), 0); 38 | expect(calculatePadLength(32), 0); 39 | expect(calculatePadLength(5), 27); 40 | expect(calculatePadLength(5, allowEmpty: true), 27); 41 | expect(calculatePadLength(40), 24); 42 | }); 43 | 44 | test('parses ABI types', () { 45 | abiTypes.forEach((key, type) { 46 | expect(parseAbiType(key), type, reason: 'parsAbiType($key)'); 47 | expect(type.name, key); 48 | }); 49 | }); 50 | 51 | test('rejects invalid types', () { 52 | for (final invalid in invalidTypes) { 53 | expect( 54 | () => parseAbiType(invalid), 55 | throwsA(anything), 56 | reason: '$invalid is not a valid type', 57 | ); 58 | } 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.lock 4 | *.log 5 | *.pyc 6 | *.swp 7 | .DS_Store 8 | .atom/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | build/ 33 | 34 | # Android related 35 | **/android/**/gradle-wrapper.jar 36 | **/android/.gradle 37 | **/android/captures/ 38 | **/android/gradlew 39 | **/android/gradlew.bat 40 | **/android/local.properties 41 | **/android/**/GeneratedPluginRegistrant.java 42 | 43 | # iOS/XCode related 44 | **/ios/**/*.mode1v3 45 | **/ios/**/*.mode2v3 46 | **/ios/**/*.moved-aside 47 | **/ios/**/*.pbxuser 48 | **/ios/**/*.perspectivev3 49 | **/ios/**/*sync/ 50 | **/ios/**/.sconsign.dblite 51 | **/ios/**/.tags* 52 | **/ios/**/.vagrant/ 53 | **/ios/**/DerivedData/ 54 | **/ios/**/Icon? 55 | **/ios/**/Pods/ 56 | **/ios/**/.symlinks/ 57 | **/ios/**/profile 58 | **/ios/**/xcuserdata 59 | **/ios/.generated/ 60 | **/ios/Flutter/App.framework 61 | **/ios/Flutter/Flutter.framework 62 | **/ios/Flutter/Flutter.podspec 63 | **/ios/Flutter/Generated.xcconfig 64 | **/ios/Flutter/app.flx 65 | **/ios/Flutter/app.zip 66 | **/ios/Flutter/flutter_assets/ 67 | **/ios/Flutter/flutter_export_environment.sh 68 | **/ios/ServiceDefinitions.json 69 | **/ios/Runner/GeneratedPluginRegistrant.* 70 | 71 | # Exceptions to above rules. 72 | !**/ios/**/default.mode1v3 73 | !**/ios/**/default.mode2v3 74 | !**/ios/**/default.pbxuser 75 | !**/ios/**/default.perspectivev3 76 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 77 | /example/*.lock 78 | -------------------------------------------------------------------------------- /test/crypto/random_bridge_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'package:test/test.dart'; 3 | import 'package:web3dart/web3dart.dart'; 4 | 5 | class MockRandom implements Random { 6 | // using BigInt because 1 << 32 is 0 in js 7 | static final _twoToThePowerOf32 = BigInt.one << 32; 8 | 9 | final List nextIntResponses = []; 10 | 11 | @override 12 | bool nextBool() { 13 | throw UnimplementedError(); 14 | } 15 | 16 | @override 17 | double nextDouble() { 18 | throw UnimplementedError(); 19 | } 20 | 21 | @override 22 | int nextInt(int max) { 23 | if (BigInt.from(max) > _twoToThePowerOf32) { 24 | // generating numbers in [0;1<<32] is supported by the RNG implemented in 25 | // dart. 26 | fail( 27 | 'RandomBridge called Random.nextInt with an upper bound that is ' 28 | 'to high: $max', 29 | ); 30 | } 31 | 32 | if (nextIntResponses.isNotEmpty) { 33 | return nextIntResponses.removeAt(0); 34 | } else { 35 | return max ~/ 2; 36 | } 37 | } 38 | } 39 | 40 | void main() { 41 | final random = MockRandom(); 42 | 43 | test('delegates simple operations', () { 44 | expect(RandomBridge(random).nextUint8(), 1 << 7); 45 | expect(RandomBridge(random).nextUint16(), 1 << 15); 46 | expect(RandomBridge(random).nextUint32(), 1 << 31); 47 | }); 48 | 49 | test('generates bytes', () { 50 | random.nextIntResponses.addAll([4, 4, 4, 4, 4]); 51 | 52 | expect(RandomBridge(random).nextBytes(5), [4, 4, 4, 4, 4]); 53 | }); 54 | 55 | test('generates big integers', () { 56 | random.nextIntResponses.addAll([84, 12]); 57 | expect(RandomBridge(random).nextBigInteger(13).toInt(), (12 << 8) + 84); 58 | }); 59 | 60 | test('nextBigInteger is never negative', () { 61 | final random = RandomBridge(Random()); 62 | for (var i = 1; i < 500; i++) { 63 | expect(random.nextBigInteger(i), isA()); 64 | } 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /test/json_rpc_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:http/http.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:web3dart/json_rpc.dart'; 7 | 8 | final uri = Uri.parse('url'); 9 | 10 | void main() { 11 | late MockClient client; 12 | 13 | setUp(() { 14 | client = MockClient(); 15 | }); 16 | 17 | test('encodes and sends requests', () async { 18 | await JsonRPC('url', client).call('eth_gasPrice', ['param', 'another']); 19 | 20 | final request = client.request!; 21 | expect( 22 | request.headers, 23 | containsPair('Content-Type', startsWith('application/json')), 24 | ); 25 | }); 26 | 27 | test('increments request id', () async { 28 | final rpc = JsonRPC('url', client); 29 | await rpc.call('eth_gasPrice', ['param', 'another']); 30 | await rpc.call('eth_gasPrice', ['param', 'another']); 31 | 32 | final lastRequest = client.request!; 33 | expect( 34 | lastRequest.finalize().bytesToString(), 35 | completion(contains('"id":2')), 36 | ); 37 | }); 38 | 39 | test('throws errors', () { 40 | final rpc = JsonRPC('url', client); 41 | client.nextResponse = StreamedResponse( 42 | Stream.value( 43 | utf8.encode( 44 | '{"id": 1, "jsonrpc": "2.0", ' 45 | '"error": {"code": 1, "message": "Message", "data": "data"}}', 46 | ), 47 | ), 48 | 200, 49 | ); 50 | 51 | expect(rpc.call('eth_gasPrice'), throwsException); 52 | }); 53 | } 54 | 55 | class MockClient extends BaseClient { 56 | StreamedResponse? nextResponse; 57 | BaseRequest? request; 58 | 59 | @override 60 | Future send(BaseRequest request) { 61 | this.request = request; 62 | return Future.value( 63 | nextResponse ?? 64 | StreamedResponse( 65 | Stream.value( 66 | utf8.encode('{"id": 1, "jsonrpc": "2.0", "result": "0x1"}'), 67 | ), 68 | 200, 69 | ), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/src/core/block_number.dart: -------------------------------------------------------------------------------- 1 | /// For operations that are reading data from the blockchain without making a 2 | /// transaction that would modify it, the Ethereum client can read that data 3 | /// from previous states of the blockchain as well. This class specifies which 4 | /// state to use. 5 | class BlockNum { 6 | /// Use the state of the blockchain at the block specified. 7 | const BlockNum.exact(this.blockNum) : useAbsolute = true; 8 | 9 | /// Use the state of the blockchain with the first block 10 | const BlockNum.genesis() 11 | : useAbsolute = false, 12 | blockNum = 0; 13 | 14 | /// Use the state of the blockchain as of the latest mined block. 15 | const BlockNum.current() 16 | : useAbsolute = false, 17 | blockNum = 1; 18 | 19 | /// Use the current state of the blockchain, including pending transactions 20 | /// that have not yet been mined. 21 | const BlockNum.pending() 22 | : useAbsolute = false, 23 | blockNum = 2; 24 | 25 | final bool useAbsolute; 26 | final int blockNum; 27 | 28 | bool get isPending => !useAbsolute && blockNum == 2; 29 | 30 | /// Generates the block parameter as it is accepted by the Ethereum client. 31 | String toBlockParam() { 32 | if (useAbsolute) return '0x${blockNum.toRadixString(16)}'; 33 | 34 | switch (blockNum) { 35 | case 0: 36 | return 'earliest'; 37 | case 1: 38 | return 'latest'; 39 | case 2: 40 | return 'pending'; 41 | default: 42 | return 'latest'; //Can't happen, though 43 | } 44 | } 45 | 46 | @override 47 | String toString() { 48 | if (useAbsolute) return blockNum.toString(); 49 | 50 | return toBlockParam(); 51 | } 52 | 53 | @override 54 | bool operator ==(Object other) => 55 | identical(this, other) || 56 | other is BlockNum && 57 | runtimeType == other.runtimeType && 58 | useAbsolute == other.useAbsolute && 59 | blockNum == other.blockNum; 60 | 61 | @override 62 | int get hashCode => useAbsolute.hashCode ^ blockNum.hashCode; 63 | } 64 | -------------------------------------------------------------------------------- /lib/web3dart.dart: -------------------------------------------------------------------------------- 1 | library web3dart; 2 | 3 | import 'dart:async'; 4 | import 'dart:typed_data'; 5 | import 'dart:math'; 6 | import 'dart:convert'; 7 | import 'package:convert/convert.dart'; 8 | import 'package:pointycastle/export.dart'; 9 | import 'package:sec/sec.dart'; 10 | import 'package:typed_data/typed_data.dart'; 11 | import 'package:web3dart/src/utils/equality.dart' as eq; 12 | import 'package:http/http.dart'; 13 | import 'package:json_rpc_2/json_rpc_2.dart' as rpc; 14 | import 'package:stream_channel/stream_channel.dart'; 15 | import 'package:stream_transform/stream_transform.dart'; 16 | import 'package:eip1559/eip1559.dart' as eip1559; 17 | import 'package:wallet/wallet.dart'; 18 | 19 | import 'package:pointycastle/key_derivators/pbkdf2.dart' as pbkdf2; 20 | import 'package:pointycastle/key_derivators/scrypt.dart' as scrypt; 21 | import 'package:pointycastle/src/utils.dart' as p_utils; 22 | import 'package:web3dart/web3dart.dart' as secp256k1; 23 | 24 | import 'json_rpc.dart'; 25 | import 'src/core/block_number.dart'; 26 | 27 | import 'src/utils/rlp.dart' as rlp; 28 | import 'src/utils/typed_data.dart'; 29 | import 'src/utils/uuid.dart'; 30 | 31 | export 'src/core/block_number.dart'; 32 | export 'src/utils/rlp.dart'; 33 | export 'src/utils/typed_data.dart'; 34 | 35 | part 'src/core/client.dart'; 36 | part 'src/core/filters.dart'; 37 | part 'src/core/transaction.dart'; 38 | part 'src/core/transaction_information.dart'; 39 | part 'src/core/transaction_signer.dart'; 40 | part 'src/utils/length_tracking_byte_sink.dart'; 41 | 42 | part 'src/credentials/credentials.dart'; 43 | part 'src/credentials/did.dart'; 44 | part 'src/credentials/wallet.dart'; 45 | 46 | part 'src/contracts/deployed_contract.dart'; 47 | part 'src/contracts/generated_contract.dart'; 48 | part 'src/contracts/abi/abi.dart'; 49 | part 'src/contracts/abi/arrays.dart'; 50 | part 'src/contracts/abi/integers.dart'; 51 | part 'src/contracts/abi/tuple.dart'; 52 | part 'src/contracts/abi/types.dart'; 53 | 54 | part 'src/crypto/formatting.dart'; 55 | part 'src/crypto/keccak.dart'; 56 | part 'src/crypto/random_bridge.dart'; 57 | part 'src/crypto/secp256k1.dart'; 58 | -------------------------------------------------------------------------------- /example/lib/token.abi.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "inputs": [], 4 | "payable": false, 5 | "stateMutability": "nonpayable", 6 | "type": "constructor", 7 | "signature": "constructor" 8 | }, 9 | { 10 | "anonymous": false, 11 | "inputs": [ 12 | { 13 | "indexed": true, 14 | "name": "_from", 15 | "type": "address" 16 | }, 17 | { 18 | "indexed": true, 19 | "name": "_to", 20 | "type": "address" 21 | }, 22 | { 23 | "indexed": false, 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "Transfer", 29 | "type": "event", 30 | "signature": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" 31 | }, 32 | { 33 | "constant": false, 34 | "inputs": [ 35 | { 36 | "name": "receiver", 37 | "type": "address" 38 | }, 39 | { 40 | "name": "amount", 41 | "type": "uint256" 42 | } 43 | ], 44 | "name": "sendCoin", 45 | "outputs": [ 46 | { 47 | "name": "sufficient", 48 | "type": "bool" 49 | } 50 | ], 51 | "payable": false, 52 | "stateMutability": "nonpayable", 53 | "type": "function", 54 | "signature": "0x90b98a11" 55 | }, 56 | { 57 | "constant": true, 58 | "inputs": [ 59 | { 60 | "name": "addr", 61 | "type": "address" 62 | } 63 | ], 64 | "name": "getBalanceInEth", 65 | "outputs": [ 66 | { 67 | "name": "", 68 | "type": "uint256" 69 | } 70 | ], 71 | "payable": false, 72 | "stateMutability": "view", 73 | "type": "function", 74 | "signature": "0x7bd703e8" 75 | }, 76 | { 77 | "constant": true, 78 | "inputs": [ 79 | { 80 | "name": "addr", 81 | "type": "address" 82 | } 83 | ], 84 | "name": "getBalance", 85 | "outputs": [ 86 | { 87 | "name": "", 88 | "type": "uint256" 89 | } 90 | ], 91 | "payable": false, 92 | "stateMutability": "view", 93 | "type": "function", 94 | "signature": "0xf8b2cb4f" 95 | } 96 | ] -------------------------------------------------------------------------------- /lib/src/contracts/deployed_contract.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | /// Helper class that defines a contract with a known ABI that has been deployed 4 | /// on a Ethereum blockchain. 5 | /// 6 | /// A future version of this library will automatically generate subclasses of 7 | /// this based on the abi given, making it easier to call methods in contracts. 8 | class DeployedContract { 9 | /// Constructor. 10 | DeployedContract(this.abi, this.address); 11 | 12 | /// The lower-level ABI of this contract used to encode data to send in 13 | /// transactions when calling this contract. 14 | final ContractAbi abi; 15 | 16 | /// The Ethereum address at which this contract is reachable. 17 | final EthereumAddress address; 18 | 19 | /// Get a list of all functions defined by the contract ABI. 20 | List get functions => abi.functions; 21 | 22 | /// A list of all events defined in the contract ABI. 23 | List get events => abi.events; 24 | 25 | /// Finds all external or public functions defined by the contract that have 26 | /// the given name. As solidity supports function overloading, this will 27 | /// return a list as only a combination of name and types will uniquely find 28 | /// a function. 29 | Iterable findFunctionsByName(String name) => 30 | functions.where((f) => f.name == name); 31 | 32 | /// Finds the external or public function defined by the contract that has the 33 | /// provided [name]. 34 | /// 35 | /// If no, or more than one function matches that description, this method 36 | /// will throw. 37 | ContractFunction function(String name) => 38 | functions.singleWhere((f) => f.name == name); 39 | 40 | /// Finds the event defined by the contract that has the matching [name]. 41 | /// 42 | /// If no, or more than one event matches that name, this method will throw. 43 | ContractEvent event(String name) => events.singleWhere((e) => e.name == name); 44 | 45 | /// Finds all methods that are constructors of this contract. 46 | /// 47 | /// Note that the library at the moment does not support creating contracts. 48 | Iterable get constructors => 49 | functions.where((t) => t.isConstructor); 50 | } 51 | -------------------------------------------------------------------------------- /test/contracts/abi/encoding_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:test/test.dart'; 5 | 6 | import 'package:web3dart/web3dart.dart'; 7 | import 'package:wallet/wallet.dart'; 8 | 9 | import 'data/basic_abi_tests.dart' as basic; 10 | import 'data/integers.dart' as ints; 11 | 12 | import 'utils.dart'; 13 | 14 | void main() { 15 | _runTests(basic.content); 16 | _runTests(ints.content); 17 | } 18 | 19 | void _runTests(String content) { 20 | final parsed = json.decode(content) as Map; 21 | 22 | for (final testCase in parsed.keys) { 23 | group('ABI - $testCase', () { 24 | final testVector = parsed[testCase] as Map; 25 | 26 | final types = (testVector['types'] as List) 27 | .cast() 28 | .map(parseAbiType) 29 | .toList(); 30 | final tupleWrapper = TupleType(types); 31 | final result = testVector['result'] as String; 32 | final input = _mapFromTest(testVector['args']); 33 | 34 | test('encodes', () { 35 | expectEncodes(tupleWrapper, input, result); 36 | }); 37 | 38 | test('decodes', () { 39 | expect(tupleWrapper.decode(bufferFromHex(result), 0).data, input); 40 | }); 41 | }); 42 | } 43 | } 44 | 45 | /// Maps types from an Ethereum abi test vector to types that are understood by 46 | /// web3dart: 47 | /// - [int] will be mapped to [BigInt] 48 | /// - a [String] starting with "0x" to [Uint8List] 49 | /// - Strings starting with "@" will be interpreted as [EthereumAddress] 50 | /// - Strings ending with "H" as [BigInt] 51 | dynamic _mapFromTest(dynamic input) { 52 | if (input is int) return BigInt.from(input); 53 | 54 | if (input is String) { 55 | if (input.startsWith('0x')) return hexToBytes(input); 56 | if (input.startsWith('@')) { 57 | return EthereumAddress.fromHex(input.substring(1)); 58 | } 59 | if (input.endsWith('H')) { 60 | return BigInt.parse(input.substring(0, input.length - 1), radix: 16); 61 | } 62 | if (input.endsWith('D')) { 63 | return BigInt.parse(input.substring(0, input.length - 1)); 64 | } 65 | } 66 | 67 | if (input is List) return input.map(_mapFromTest).toList(); 68 | 69 | return input; 70 | } 71 | -------------------------------------------------------------------------------- /test/mock_client.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:async/async.dart'; 4 | import 'package:http/http.dart'; 5 | import 'package:test/test.dart'; 6 | 7 | class MockClient extends BaseClient { 8 | MockClient(this.handler); 9 | static final _jsonUtf8 = json.fuse(utf8); 10 | 11 | final Object? Function(String method, Object? payload) handler; 12 | 13 | @override 14 | Future post( 15 | Uri url, { 16 | Map? headers, 17 | Object? body, 18 | Encoding? encoding, 19 | }) async { 20 | if (body is! String) { 21 | fail('Invalid request, expected string as request body'); 22 | } 23 | 24 | final data = json.decode(body) as Map; 25 | if (data['jsonrpc'] != '2.0') { 26 | fail('Expected request to contain correct jsonrpc key'); 27 | } 28 | 29 | final id = data['id']; 30 | final method = data['method'] as String; 31 | final params = data['params']; 32 | final response = { 33 | 'body': body, 34 | 'id': id, 35 | 'result': handler(method, params), 36 | }; 37 | 38 | return Response(json.encode(response), 200); 39 | } 40 | 41 | @override 42 | Future send(BaseRequest request) async { 43 | final data = await _jsonUtf8.decoder.bind(request.finalize()).first; 44 | 45 | if (data is! Map) { 46 | fail('Invalid request, expected JSON map'); 47 | } 48 | 49 | if (data['jsonrpc'] != '2.0') { 50 | fail('Expected request to contain correct jsonrpc key'); 51 | } 52 | 53 | final id = data['id']; 54 | final method = data['method'] as String; 55 | final params = data['params']; 56 | 57 | final response = Result(() => handler(method, params)); 58 | 59 | return StreamedResponse( 60 | _jsonUtf8.encoder.bind( 61 | Stream.value( 62 | { 63 | 'jsonrpc': '2.0', 64 | if (response is ValueResult) 'result': response.value, 65 | if (response is ErrorResult) 66 | 'error': { 67 | 'code': -1, 68 | 'message': '${response.error}', 69 | }, 70 | 'id': id, 71 | }, 72 | ), 73 | ), 74 | 200, 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/credentials/address_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:wallet/wallet.dart'; 3 | 4 | // https://eips.ethereum.org/EIPS/eip-55#test-cases 5 | const _lowerCaseToEip55 = { 6 | '0x52908400098527886e0f7030069857d2e4169ee7': 7 | '0x52908400098527886E0F7030069857D2E4169EE7', 8 | '0x8617e340b3d01fa5f11f306f4090fd50e238070d': 9 | '0x8617E340B3D01FA5F11F306F4090FD50E238070D', 10 | '0xde709f2102306220921060314715629080e2fb77': 11 | '0xde709f2102306220921060314715629080e2fb77', 12 | '0x27b1fdb04752bbc536007a920d24acb045561c26': 13 | '0x27b1fdb04752bbc536007a920d24acb045561c26', 14 | '0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed': 15 | '0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed', 16 | '0xfb6916095ca1df60bb79ce92ce3ea74c37c5d359': 17 | '0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359', 18 | '0xdbf03b407c01e7cd3cbea99509d93f8dddc8c6fb': 19 | '0xdbF03B407c01E7cD3CBea99509d93f8DDDC8C6FB', 20 | '0xd1220a0cf47c7b9be7a2e6ba89f429762e7b9adb': 21 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb', 22 | }; 23 | 24 | const _addressValidation = { 25 | '0x4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031373037373336343236393430': 26 | false, 27 | '4d7920656d61696c206973206a6f686e40646f652e636f6d202d2031373037373336343236393430': 28 | false, 29 | '0x4d7920656d61696c206973206a6f686e40646f650': false, 30 | '4d7920656d61696c206973206a6f686e40646f650': false, 31 | '0x52908400098527886e0f7030069857d2e4169ee7': true, 32 | '52908400098527886e0f7030069857d2e4169ee7': true, 33 | '0xD1220A0cf47c7B9Be7A2E6BA89F429762e7b9aDb': true, 34 | }; 35 | 36 | void main() { 37 | bool _isValid(String address) { 38 | try { 39 | EthereumAddress.fromHex(address); 40 | return true; 41 | } catch (e) { 42 | return false; 43 | } 44 | } 45 | 46 | group('accepts and parses EIP 55', () { 47 | _lowerCaseToEip55.forEach((lower, eip55) { 48 | test('parses $lower -> $eip55', () { 49 | expect(EthereumAddress.fromHex(lower).eip55With0x, eip55); 50 | }); 51 | }); 52 | 53 | _addressValidation.forEach((address, valid) { 54 | test('parses $address -> valid $valid', () { 55 | expect(_isValid(address), valid); 56 | }); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/contracts/abi/tuple_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | import 'utils.dart'; 5 | 6 | // https://solidity.readthedocs.io/en/develop/abi-spec.html#examples 7 | 8 | const dynamicTuple = TupleType([ 9 | StringType(), 10 | BoolType(), 11 | DynamicLengthArray(type: UintType()), 12 | ]); 13 | 14 | final dynamicData = [ 15 | 'dave', 16 | true, 17 | [BigInt.from(1), BigInt.from(2), BigInt.from(3)], 18 | ]; 19 | 20 | const dynamicEncoded = 21 | '0000000000000000000000000000000000000000000000000000000000000060' 22 | '0000000000000000000000000000000000000000000000000000000000000001' 23 | '00000000000000000000000000000000000000000000000000000000000000a0' 24 | '0000000000000000000000000000000000000000000000000000000000000004' 25 | '6461766500000000000000000000000000000000000000000000000000000000' 26 | '0000000000000000000000000000000000000000000000000000000000000003' 27 | '0000000000000000000000000000000000000000000000000000000000000001' 28 | '0000000000000000000000000000000000000000000000000000000000000002' 29 | '0000000000000000000000000000000000000000000000000000000000000003'; 30 | 31 | const staticTuple = TupleType([ 32 | UintType(length: 32), 33 | BoolType(), 34 | ]); 35 | 36 | final staticData = [BigInt.from(0x45), true]; 37 | 38 | const staticEncoded = 39 | '0000000000000000000000000000000000000000000000000000000000000045' 40 | '0000000000000000000000000000000000000000000000000000000000000001'; 41 | 42 | void main() { 43 | test('reports name', () { 44 | expect(dynamicTuple.name, '(string,bool,uint256[])'); 45 | expect(staticTuple.name, '(uint32,bool)'); 46 | }); 47 | 48 | test('reports encoding length', () { 49 | expect(dynamicTuple.encodingLength.isDynamic, true); 50 | expect(staticTuple.encodingLength.length, 2 * sizeUnitBytes); 51 | }); 52 | 53 | test('encodes values', () { 54 | expectEncodes(staticTuple, staticData, staticEncoded); 55 | expectEncodes(dynamicTuple, dynamicData, dynamicEncoded); 56 | }); 57 | 58 | test('decodes values', () { 59 | expect( 60 | staticTuple.decode(bufferFromHex(staticEncoded), 0).data, 61 | staticData, 62 | ); 63 | expect( 64 | dynamicTuple.decode(bufferFromHex(dynamicEncoded), 0).data, 65 | dynamicData, 66 | ); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/crypto/formatting.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | /// If present, removes the 0x from the start of a hex-string. 4 | String strip0x(String hex) { 5 | if (hex.startsWith('0x')) return hex.substring(2); 6 | return hex; 7 | } 8 | 9 | /// Converts the [bytes] given as a list of integers into a hexadecimal 10 | /// representation. 11 | /// 12 | /// If any of the bytes is outside of the range [0, 256], the method will throw. 13 | /// The outcome of this function will prefix a 0 if it would otherwise not be 14 | /// of even length. If [include0x] is set, it will prefix "0x" to the hexadecimal 15 | /// representation. If [forcePadLength] is set, the hexadecimal representation 16 | /// will be expanded with zeroes until the desired length is reached. The "0x" 17 | /// prefix does not count for the length. 18 | String bytesToHex( 19 | List bytes, { 20 | bool include0x = false, 21 | int? forcePadLength, 22 | bool padToEvenLength = false, 23 | }) { 24 | var encoded = hex.encode(bytes); 25 | 26 | if (forcePadLength != null) { 27 | assert(forcePadLength >= encoded.length); 28 | 29 | final padding = forcePadLength - encoded.length; 30 | encoded = ('0' * padding) + encoded; 31 | } 32 | 33 | if (padToEvenLength && encoded.length % 2 != 0) { 34 | encoded = '0$encoded'; 35 | } 36 | 37 | return (include0x ? '0x' : '') + encoded; 38 | } 39 | 40 | /// Converts the hexadecimal string, which can be prefixed with 0x, to a byte 41 | /// sequence. 42 | Uint8List hexToBytes(String hexStr) { 43 | final bytes = hex.decode(strip0x(hexStr)); 44 | if (bytes is Uint8List) return bytes; 45 | 46 | return Uint8List.fromList(bytes); 47 | } 48 | 49 | Uint8List unsignedIntToBytes(BigInt number) { 50 | assert(!number.isNegative); 51 | return p_utils.encodeBigIntAsUnsigned(number); 52 | } 53 | 54 | BigInt bytesToUnsignedInt(Uint8List bytes) { 55 | return p_utils.decodeBigIntWithSign(1, bytes); 56 | } 57 | 58 | ///Converts the bytes from that list (big endian) to a (potentially signed) 59 | /// BigInt. 60 | BigInt bytesToInt(List bytes) => p_utils.decodeBigInt(bytes); 61 | 62 | Uint8List intToBytes(BigInt number) => p_utils.encodeBigInt(number); 63 | 64 | ///Takes the hexadecimal input and creates a [BigInt]. 65 | BigInt hexToInt(String hex) { 66 | return BigInt.parse(strip0x(hex), radix: 16); 67 | } 68 | 69 | /// Converts the hexadecimal input and creates an [int]. 70 | int hexToDartInt(String hex) { 71 | return int.parse(strip0x(hex), radix: 16); 72 | } 73 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | analyzer: 3 | exclude: 4 | - "**/*.g.dart" 5 | language: 6 | strict-raw-types: false 7 | strong-mode: 8 | implicit-casts: true 9 | implicit-dynamic: false 10 | errors: 11 | always_declare_return_types: error 12 | always_specify_types: error 13 | annotate_overrides: error 14 | argument_type_not_assignable: error 15 | avoid_function_literals_in_foreach_calls: error 16 | avoid_renaming_method_parameters: error 17 | avoid_types_on_closure_parameters: error 18 | avoid_unnecessary_containers: error 19 | await_only_futures: error 20 | body_might_complete_normally_nullable: error 21 | camel_case_types: error 22 | curly_braces_in_flow_control_structures: error 23 | dead_code: error 24 | duplicate_import: error 25 | file_names: error 26 | implicit_dynamic_function: ignore 27 | implicit_dynamic_parameter: error 28 | implicit_dynamic_list_literal: ignore 29 | implicit_dynamic_map_literal: ignore 30 | implicit_dynamic_method: ignore 31 | implicit_dynamic_type: ignore 32 | implicit_dynamic_variable: ignore 33 | invalid_assignment: error 34 | missing_return: error 35 | must_be_immutable: error 36 | prefer_adjacent_string_concatenation: error 37 | prefer_const_constructors: error 38 | prefer_const_constructors_in_immutables: error 39 | prefer_const_declarations: error 40 | prefer_collection_literals: error 41 | prefer_contains: error 42 | prefer_const_literals_to_create_immutables: error 43 | prefer_interpolation_to_compose_strings: error 44 | prefer_is_empty: error 45 | prefer_final_fields: error 46 | prefer_single_quotes: error 47 | public_member_api_docs: error 48 | require_trailing_commas: error 49 | sized_box_for_whitespace: error 50 | sort_constructors_first: error 51 | sort_unnamed_constructors_first: error 52 | todo: ignore 53 | use_function_type_syntax_for_parameters: error 54 | use_key_in_widget_constructors: error 55 | unnecessary_import: error 56 | unnecessary_type_check: error 57 | unnecessary_string_interpolations: error 58 | unnecessary_this: error 59 | unused_element: error 60 | unused_import: error 61 | unused_local_variable: error 62 | use_rethrow_when_possible: error 63 | linter: 64 | rules: 65 | avoid_print: false 66 | prefer_interpolation_to_compose_strings: true 67 | prefer_single_quotes: true 68 | public_member_api_docs: flase 69 | require_trailing_commas: true 70 | # always_use_package_imports: true 71 | sort_constructors_first: true 72 | sort_unnamed_constructors_first: true 73 | -------------------------------------------------------------------------------- /lib/src/utils/rlp.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:web3dart/src/utils/typed_data.dart'; 5 | 6 | import '../../web3dart.dart' show LengthTrackingByteSink, unsignedIntToBytes; 7 | 8 | void _encodeString(Uint8List string, LengthTrackingByteSink builder) { 9 | // For a single byte in [0x00, 0x7f], that byte is its own RLP encoding 10 | if (string.length == 1 && string[0] <= 0x7f) { 11 | builder.addByte(string[0]); 12 | return; 13 | } 14 | 15 | // If a string is between 0 and 55 bytes long, its encoding is 0x80 plus 16 | // its length, followed by the actual string 17 | if (string.length <= 55) { 18 | builder 19 | ..addByte(0x80 + string.length) 20 | ..add(string); 21 | return; 22 | } 23 | 24 | // More than 55 bytes long, RLP is (0xb7 + length of encoded length), followed 25 | // by the length, followed by the actual string 26 | final length = string.length; 27 | final encodedLength = unsignedIntToBytes(BigInt.from(length)); 28 | 29 | builder 30 | ..addByte(0xb7 + encodedLength.length) 31 | ..add(encodedLength) 32 | ..add(string); 33 | } 34 | 35 | void encodeList(List list, LengthTrackingByteSink builder) { 36 | final subBuilder = LengthTrackingByteSink(); 37 | for (final item in list) { 38 | _encodeToBuffer(item, subBuilder); 39 | } 40 | 41 | final length = subBuilder.length; 42 | if (length <= 55) { 43 | builder 44 | ..addByte(0xc0 + length) 45 | ..add(subBuilder.asBytes()); 46 | return; 47 | } else { 48 | final encodedLength = unsignedIntToBytes(BigInt.from(length)); 49 | 50 | builder 51 | ..addByte(0xf7 + encodedLength.length) 52 | ..add(encodedLength) 53 | ..add(subBuilder.asBytes()); 54 | return; 55 | } 56 | } 57 | 58 | void _encodeInt(BigInt val, LengthTrackingByteSink builder) { 59 | if (val == BigInt.zero) { 60 | _encodeString(Uint8List(0), builder); 61 | } else { 62 | _encodeString(unsignedIntToBytes(val), builder); 63 | } 64 | } 65 | 66 | void _encodeToBuffer(dynamic value, LengthTrackingByteSink builder) { 67 | if (value is Uint8List) { 68 | _encodeString(value, builder); 69 | } else if (value is List) { 70 | encodeList(value, builder); 71 | } else if (value is BigInt) { 72 | _encodeInt(value, builder); 73 | } else if (value is int) { 74 | _encodeInt(BigInt.from(value), builder); 75 | } else if (value is String) { 76 | _encodeString(uint8ListFromList(utf8.encode(value)), builder); 77 | } else { 78 | throw UnsupportedError('$value cannot be rlp-encoded'); 79 | } 80 | } 81 | 82 | List encode(dynamic value) { 83 | final builder = LengthTrackingByteSink(); 84 | _encodeToBuffer(value, builder); 85 | 86 | return builder.asBytes(); 87 | } 88 | -------------------------------------------------------------------------------- /example/lib/contracts.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: public_member_api_docs 2 | 3 | import 'package:http/http.dart'; 4 | import 'package:wallet/wallet.dart'; 5 | import 'package:web3dart/web3dart.dart'; 6 | import 'package:web_socket_channel/web_socket_channel.dart'; 7 | 8 | import 'token.g.dart'; 9 | 10 | const String rpcUrl = 'http://localhost:8545'; 11 | final wsUrl = Uri.parse('ws://localhost:8545'); 12 | 13 | const String privateKey = 14 | '9a43d93a50b622761d88c80c90567c02c82442746335a01b72f49b3c867c037d'; 15 | 16 | final EthereumAddress contractAddr = 17 | EthereumAddress.fromHex('0xeE9312C22890e0Bd9a9bB37Fd17572180F4Fc68a'); 18 | final EthereumAddress receiver = 19 | EthereumAddress.fromHex('0x6c87E1a114C3379BEc929f6356c5263d62542C13'); 20 | 21 | /* 22 | Examples that deal with contracts. The contract used here is from the truffle 23 | example: 24 | 25 | contract MetaCoin { 26 | mapping (address => uint) balances; 27 | 28 | event Transfer(address indexed _from, address indexed _to, uint256 _value); 29 | 30 | constructor() public { 31 | balances[tx.origin] = 10000; 32 | } 33 | 34 | function sendCoin(address receiver, uint amount) public returns(bool sufficient) { 35 | if (balances[msg.sender] < amount) return false; 36 | balances[msg.sender] -= amount; 37 | balances[receiver] += amount; 38 | emit Transfer(msg.sender, receiver, amount); 39 | return true; 40 | } 41 | 42 | function getBalanceInEth(address addr) public view returns(uint){ 43 | return ConvertLib.convert(getBalance(addr),2); 44 | } 45 | 46 | function getBalance(address addr) public view returns(uint) { 47 | return balances[addr]; 48 | } 49 | } 50 | 51 | The ABI of this contract is available at abi.json 52 | To generate contract classes, add a dependency on web3dart and build_runner. 53 | Running `dart pub run build_runner build` (or `flutter pub ...` if you're using 54 | Flutter) will generate classes for an .abi.json file. 55 | */ 56 | 57 | Future main() async { 58 | // establish a connection to the ethereum rpc node. The socketConnector 59 | // property allows more efficient event streams over websocket instead of 60 | // http-polls. However, the socketConnector property is experimental. 61 | final client = Web3Client( 62 | rpcUrl, 63 | Client(), 64 | socketConnector: () { 65 | return WebSocketChannel.connect(wsUrl).cast(); 66 | }, 67 | ); 68 | final credentials = EthPrivateKey.fromHex(privateKey); 69 | final ownAddress = credentials.address; 70 | 71 | // read the contract abi and tell web3dart where it's deployed (contractAddr) 72 | final token = Token(address: contractAddr, client: client); 73 | 74 | // listen for the Transfer event when it's emitted by the contract above 75 | final subscription = token.transferEvents().take(1).listen((event) { 76 | print('${event.from} sent ${event.value} MetaCoins to ${event.to}!'); 77 | }); 78 | 79 | // check our balance in MetaCoins by calling the appropriate function 80 | final balance = await token.getBalance(ownAddress); 81 | print('We have $balance MetaCoins'); 82 | 83 | // send all our MetaCoins to the other address by calling the sendCoin 84 | // function 85 | await token.sendCoin(receiver, balance, credentials: credentials); 86 | 87 | await subscription.asFuture(); 88 | await subscription.cancel(); 89 | 90 | await client.dispose(); 91 | } 92 | -------------------------------------------------------------------------------- /lib/json_rpc.dart: -------------------------------------------------------------------------------- 1 | library json_rpc; 2 | 3 | import 'dart:async'; 4 | import 'dart:convert'; 5 | 6 | import 'package:http/http.dart'; 7 | 8 | // ignore: one_member_abstracts 9 | 10 | /// RPC Service base class. 11 | abstract class RpcService { 12 | /// Constructor. 13 | RpcService(this.url); 14 | 15 | /// Url. 16 | final String url; 17 | 18 | /// Performs an RPC request, asking the server to execute the function with 19 | /// the given name and the associated parameters, which need to be encodable 20 | /// with the [json] class of dart:convert. 21 | /// 22 | /// When the request is successful, an [RPCResponse] with the request id and 23 | /// the data from the server will be returned. If not, an RPCError will be 24 | /// thrown. Other errors might be thrown if an IO-Error occurs. 25 | Future call(String function, [List? params]); 26 | } 27 | 28 | /// Json RPC Service. 29 | class JsonRPC extends RpcService { 30 | /// Constructor. 31 | JsonRPC(String url, this.client) : super(url); 32 | 33 | /// Http client. 34 | final Client client; 35 | 36 | int _currentRequestId = 1; 37 | 38 | /// Performs an RPC request, asking the server to execute the function with 39 | /// the given name and the associated parameters, which need to be encodable 40 | /// with the [json] class of dart:convert. 41 | /// 42 | /// When the request is successful, an [RPCResponse] with the request id and 43 | /// the data from the server will be returned. If not, an RPCError will be 44 | /// thrown. Other errors might be thrown if an IO-Error occurs. 45 | @override 46 | Future call(String function, [List? params]) async { 47 | params ??= []; 48 | 49 | final requestPayload = { 50 | 'jsonrpc': '2.0', 51 | 'method': function, 52 | 'params': params, 53 | 'id': _currentRequestId++, 54 | }; 55 | 56 | final response = await client.post( 57 | Uri.parse(url), 58 | headers: {'Content-Type': 'application/json'}, 59 | body: json.encode(requestPayload), 60 | ); 61 | 62 | final data = json.decode(response.body) as Map; 63 | 64 | if (data.containsKey('error')) { 65 | final error = data['error']; 66 | 67 | final code = error['code'] as int; 68 | final message = error['message'] as String; 69 | final errorData = error['data']; 70 | 71 | throw RPCError(code, message, errorData); 72 | } 73 | 74 | final id = data['id'] as int; 75 | final result = data['result']; 76 | return RPCResponse(id, result); 77 | } 78 | } 79 | 80 | /// Response from the server to an rpc request. Contains the id of the request 81 | /// and the corresponding result as sent by the server. 82 | class RPCResponse { 83 | /// Constructor. 84 | const RPCResponse(this.id, this.result); 85 | 86 | /// Id. 87 | final int id; 88 | 89 | /// Result. 90 | final dynamic result; 91 | } 92 | 93 | /// Exception thrown when an the server returns an error code to an rpc request. 94 | class RPCError implements Exception { 95 | /// Constructor. 96 | const RPCError(this.errorCode, this.message, this.data); 97 | 98 | /// Error code. 99 | final int errorCode; 100 | 101 | /// Message. 102 | final String message; 103 | 104 | /// Data. 105 | final dynamic data; 106 | 107 | @override 108 | String toString() { 109 | return 'RPCError: got code $errorCode with msg "$message".'; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /test/credentials/example_keystores.dart: -------------------------------------------------------------------------------- 1 | const content = ''' 2 | { 3 | "test1": { 4 | "json": { 5 | "crypto" : { 6 | "cipher" : "aes-128-ctr", 7 | "cipherparams" : { 8 | "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" 9 | }, 10 | "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", 11 | "kdf" : "pbkdf2", 12 | "kdfparams" : { 13 | "c" : 262144, 14 | "dklen" : 32, 15 | "prf" : "hmac-sha256", 16 | "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" 17 | }, 18 | "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" 19 | }, 20 | "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", 21 | "version" : 3 22 | }, 23 | "password": "testpassword", 24 | "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" 25 | }, 26 | "test2": { 27 | "json": { 28 | "crypto" : { 29 | "cipher" : "aes-128-ctr", 30 | "cipherparams" : { 31 | "iv" : "83dbcc02d8ccb40e466191a123791e0e" 32 | }, 33 | "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", 34 | "kdf" : "scrypt", 35 | "kdfparams" : { 36 | "dklen" : 32, 37 | "n" : 262144, 38 | "r" : 1, 39 | "p" : 8, 40 | "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" 41 | }, 42 | "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" 43 | }, 44 | "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", 45 | "version" : 3 46 | }, 47 | "password": "testpassword", 48 | "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" 49 | }, 50 | "python_generated_test_with_odd_iv": { 51 | "json": { 52 | "version": 3, 53 | "crypto": { 54 | "ciphertext": "ee75456c006b1e468133c5d2a916bacd3cf515ced4d9b021b5c59978007d1e87", 55 | "version": 1, 56 | "kdf": "pbkdf2", 57 | "kdfparams": { 58 | "dklen": 32, 59 | "c": 262144, 60 | "prf": "hmac-sha256", 61 | "salt": "504490577620f64f43d73f29479c2cf0" 62 | }, 63 | "mac": "196815708465de9af7504144a1360d08874fc3c30bb0e648ce88fbc36830d35d", 64 | "cipherparams": { 65 | "iv": "514ccc8c4fb3e60e5538e0cf1e27c233" 66 | }, 67 | "cipher": "aes-128-ctr" 68 | }, 69 | "id": "98d193c7-5174-4c7c-5345-c1daf95477b5" 70 | }, 71 | "password": "foo", 72 | "priv": "0101010101010101010101010101010101010101010101010101010101010101" 73 | }, 74 | "evilnonce": { 75 | "json": { 76 | "version": 3, 77 | "crypto": { 78 | "ciphertext": "d69313b6470ac1942f75d72ebf8818a0d484ac78478a132ee081cd954d6bd7a9", 79 | "cipherparams": { 80 | "iv": "ffffffffffffffffffffffffffffffff" 81 | }, 82 | "kdf": "pbkdf2", 83 | "kdfparams": { 84 | "dklen": 32, 85 | "c": 262144, 86 | "prf": "hmac-sha256", 87 | "salt": "c82ef14476014cbf438081a42709e2ed" 88 | }, 89 | "mac": "cf6bfbcc77142a22c4a908784b4a16f1023a1d0e2aff404c20158fa4f1587177", 90 | "cipher": "aes-128-ctr", 91 | "version": 1 92 | }, 93 | "id": "abb67040-8dbe-0dad-fc39-2b082ef0ee5f" 94 | }, 95 | "password": "bar", 96 | "priv": "0202020202020202020202020202020202020202020202020202020202020202" 97 | } 98 | } 99 | '''; 100 | -------------------------------------------------------------------------------- /test/core/event_filter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | import 'package:wallet/wallet.dart'; 4 | 5 | import '../mock_client.dart'; 6 | 7 | void main() { 8 | const alice = 9 | '0x000000000000000000000000Dd611f2b2CaF539aC9e12CF84C09CB9bf81CA37F'; 10 | const bob = 11 | '0x0000000000000000000000006c87E1a114C3379BEc929f6356c5263d62542C13'; 12 | const contract = '0x16c5785ac562ff41e2dcfdf829c5a142f1fccd7d'; 13 | 14 | final testCases = [ 15 | { 16 | 'name': 'one topic', 17 | 'input': [ 18 | [alice], 19 | ], 20 | 'expected': [ 21 | [alice], 22 | ], 23 | }, 24 | { 25 | 'name': 'two topics one item', 26 | 'input': [ 27 | [alice, bob], 28 | ], 29 | 'expected': [ 30 | [alice, bob], 31 | ], 32 | }, 33 | { 34 | 'name': 'two topics two items', 35 | 'input': [ 36 | [alice], 37 | [bob], 38 | ], 39 | 'expected': [ 40 | [alice], 41 | [bob], 42 | ], 43 | }, 44 | { 45 | 'name': 'two topics first null', 46 | 'input': [ 47 | [], 48 | [bob], 49 | ], 50 | 'expected': [ 51 | null, 52 | [bob], 53 | ], 54 | }, 55 | { 56 | 'name': 'three topics first null', 57 | 'input': [ 58 | [], 59 | [alice], 60 | [bob], 61 | ], 62 | 'expected': [ 63 | null, 64 | [alice], 65 | [bob], 66 | ], 67 | }, 68 | { 69 | 'name': 'three topics second null', 70 | 'input': [ 71 | [alice], 72 | [], 73 | [bob], 74 | ], 75 | 'expected': [ 76 | [alice], 77 | null, 78 | [bob], 79 | ], 80 | } 81 | ]; 82 | 83 | Future runFilterTest(dynamic input, dynamic expected) async { 84 | final client = MockClient( 85 | expectAsync2((method, params) { 86 | expect(method, 'eth_getLogs'); 87 | 88 | // verify that the topics are sent to eth_getLogs in the correct format 89 | final actual = ((params as List)[0])['topics']; 90 | expect(actual, expected); 91 | 92 | // return a valid response from eth_getLogs 93 | return [ 94 | {'address': contract}, 95 | ]; 96 | }), 97 | ); 98 | 99 | final web3 = Web3Client('', client); 100 | addTearDown(web3.dispose); 101 | 102 | // Dart typing will not allow an empty list to be added so when an empty 103 | // list is encountered, a list containing a single string is added and then 104 | // the single string in that list is removed. 105 | // The type is required to ensure `topics` is forced to List> 106 | 107 | // ignore: omit_local_variable_types 108 | final List> topics = []; 109 | input.forEach((dynamic element) { 110 | if (element.length == 0) { 111 | topics.add(['dummy string element']); 112 | topics.last.remove('dummy string element'); 113 | } else { 114 | topics.add(element as List); 115 | } 116 | }); 117 | 118 | final filter = FilterOptions( 119 | fromBlock: const BlockNum.genesis(), 120 | toBlock: const BlockNum.current(), 121 | address: EthereumAddress.fromHex(contract), 122 | topics: topics, 123 | ); 124 | 125 | await web3.getLogs(filter); 126 | } 127 | 128 | // test each test case in the list of test cases 129 | for (final testCase in testCases) { 130 | test('filters test with ${testCase['name']}', () async { 131 | await runFilterTest(testCase['input'], testCase['expected']); 132 | }); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /example/lib/token.g.dart: -------------------------------------------------------------------------------- 1 | // Generated code, do not modify. Run `build_runner build` to re-generate! 2 | // @dart=2.12 3 | import 'package:web3dart/web3dart.dart' as _i1; 4 | 5 | final _contractAbi = _i1.ContractAbi.fromJson( 6 | '[{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor","signature":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event","signature":"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"},{"constant":false,"inputs":[{"name":"receiver","type":"address"},{"name":"amount","type":"uint256"}],"name":"sendCoin","outputs":[{"name":"sufficient","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function","signature":"0x90b98a11"},{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"getBalanceInEth","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function","signature":"0x7bd703e8"},{"constant":true,"inputs":[{"name":"addr","type":"address"}],"name":"getBalance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function","signature":"0xf8b2cb4f"}]', 7 | 'Token'); 8 | 9 | class Token extends _i1.GeneratedContract { 10 | Token( 11 | {required _i1.EthereumAddress address, 12 | required _i1.Web3Client client, 13 | int? chainId}) 14 | : super(_i1.DeployedContract(_contractAbi, address), client, chainId); 15 | 16 | /// The optional [transaction] parameter can be used to override parameters 17 | /// like the gas price, nonce and max gas. The `data` and `to` fields will be 18 | /// set by the contract. 19 | Future sendCoin(_i1.EthereumAddress receiver, BigInt amount, 20 | {required _i1.Credentials credentials, 21 | _i1.Transaction? transaction}) async { 22 | final function = self.abi.functions[1]; 23 | assert(checkSignature(function, '90b98a11')); 24 | final params = [receiver, amount]; 25 | return write(credentials, transaction, function, params); 26 | } 27 | 28 | /// The optional [atBlock] parameter can be used to view historical data. When 29 | /// set, the function will be evaluated in the specified block. By default, the 30 | /// latest on-chain block will be used. 31 | Future getBalanceInEth(_i1.EthereumAddress addr, 32 | {_i1.BlockNum? atBlock}) async { 33 | final function = self.abi.functions[2]; 34 | assert(checkSignature(function, '7bd703e8')); 35 | final params = [addr]; 36 | final response = await read(function, params, atBlock); 37 | return (response[0] as BigInt); 38 | } 39 | 40 | /// The optional [atBlock] parameter can be used to view historical data. When 41 | /// set, the function will be evaluated in the specified block. By default, the 42 | /// latest on-chain block will be used. 43 | Future getBalance(_i1.EthereumAddress addr, 44 | {_i1.BlockNum? atBlock}) async { 45 | final function = self.abi.functions[3]; 46 | assert(checkSignature(function, 'f8b2cb4f')); 47 | final params = [addr]; 48 | final response = await read(function, params, atBlock); 49 | return (response[0] as BigInt); 50 | } 51 | 52 | /// Returns a live stream of all Transfer events emitted by this contract. 53 | Stream transferEvents( 54 | {_i1.BlockNum? fromBlock, _i1.BlockNum? toBlock}) { 55 | final event = self.event('Transfer'); 56 | final filter = _i1.FilterOptions.events( 57 | contract: self, event: event, fromBlock: fromBlock, toBlock: toBlock); 58 | return client.events(filter).map((_i1.FilterEvent result) { 59 | final decoded = event.decodeResults(result.topics!, result.data!); 60 | return Transfer(decoded); 61 | }); 62 | } 63 | } 64 | 65 | class Transfer { 66 | Transfer(List response) 67 | : from = (response[0] as _i1.EthereumAddress), 68 | to = (response[1] as _i1.EthereumAddress), 69 | value = (response[2] as BigInt); 70 | 71 | final _i1.EthereumAddress from; 72 | 73 | final _i1.EthereumAddress to; 74 | 75 | final BigInt value; 76 | } 77 | -------------------------------------------------------------------------------- /lib/src/core/transaction.dart: -------------------------------------------------------------------------------- 1 | part of 'package:web3dart/web3dart.dart'; 2 | 3 | class Transaction { 4 | Transaction({ 5 | this.from, 6 | this.to, 7 | this.maxGas, 8 | this.gasPrice, 9 | this.value, 10 | this.data, 11 | this.nonce, 12 | this.maxFeePerGas, 13 | this.maxPriorityFeePerGas, 14 | }); 15 | 16 | /// Constructs a transaction that can be used to call a contract function. 17 | Transaction.callContract({ 18 | required DeployedContract contract, 19 | required ContractFunction function, 20 | required List parameters, 21 | this.from, 22 | this.maxGas, 23 | this.gasPrice, 24 | this.value, 25 | this.nonce, 26 | this.maxFeePerGas, 27 | this.maxPriorityFeePerGas, 28 | }) : to = contract.address, 29 | data = function.encodeCall(parameters); 30 | 31 | /// The address of the sender of this transaction. 32 | /// 33 | /// This can be set to null, in which case the client will use the address 34 | /// belonging to the credentials used to this transaction. 35 | final EthereumAddress? from; 36 | 37 | /// The recipient of this transaction, or null for transactions that create a 38 | /// contract. 39 | final EthereumAddress? to; 40 | 41 | /// The maximum amount of gas to spend. 42 | /// 43 | /// If [maxGas] is `null`, this library will ask the rpc node to estimate a 44 | /// reasonable spending via [Web3Client.estimateGas]. 45 | /// 46 | /// Gas that is not used but included in [maxGas] will be returned. 47 | final int? maxGas; 48 | 49 | /// How much ether to spend on a single unit of gas. Can be null, in which 50 | /// case the rpc server will choose this value. 51 | final EtherAmount? gasPrice; 52 | 53 | /// How much ether to send to [to]. This can be null, as some transactions 54 | /// that call a contracts method won't have to send ether. 55 | final EtherAmount? value; 56 | 57 | /// For transactions that call a contract function or create a contract, 58 | /// contains the hashed function name and the encoded parameters or the 59 | /// compiled contract code, respectively. 60 | final Uint8List? data; 61 | 62 | /// The nonce of this transaction. A nonce is incremented per sender and 63 | /// transaction to make sure the same transaction can't be sent more than 64 | /// once. 65 | /// 66 | /// If null, it will be determined by checking how many transactions 67 | /// have already been sent by [from]. 68 | final int? nonce; 69 | 70 | final EtherAmount? maxPriorityFeePerGas; 71 | final EtherAmount? maxFeePerGas; 72 | 73 | Transaction copyWith({ 74 | EthereumAddress? from, 75 | EthereumAddress? to, 76 | int? maxGas, 77 | EtherAmount? gasPrice, 78 | EtherAmount? value, 79 | Uint8List? data, 80 | int? nonce, 81 | EtherAmount? maxPriorityFeePerGas, 82 | EtherAmount? maxFeePerGas, 83 | }) { 84 | return Transaction( 85 | from: from ?? this.from, 86 | to: to ?? this.to, 87 | maxGas: maxGas ?? this.maxGas, 88 | gasPrice: gasPrice ?? this.gasPrice, 89 | value: value ?? this.value, 90 | data: data ?? this.data, 91 | nonce: nonce ?? this.nonce, 92 | maxFeePerGas: maxFeePerGas ?? this.maxFeePerGas, 93 | maxPriorityFeePerGas: maxPriorityFeePerGas ?? this.maxPriorityFeePerGas, 94 | ); 95 | } 96 | 97 | bool get isEIP1559 => maxFeePerGas != null || maxPriorityFeePerGas != null; 98 | 99 | /// The transaction pre-image. 100 | /// 101 | /// The hash of this is the digest which needs to be signed to 102 | /// authorize this transaction. 103 | Uint8List getUnsignedSerialized({ 104 | int? chainId = 1, 105 | }) { 106 | if (isEIP1559 && chainId != null) { 107 | final encodedTx = LengthTrackingByteSink(); 108 | encodedTx.addByte(0x02); 109 | encodedTx.add( 110 | rlp.encode(_encodeEIP1559ToRlp(this, null, BigInt.from(chainId))), 111 | ); 112 | 113 | encodedTx.close(); 114 | 115 | return encodedTx.asBytes(); 116 | } 117 | 118 | final innerSignature = chainId == null 119 | ? null 120 | : MsgSignature(BigInt.zero, BigInt.zero, chainId); 121 | 122 | return uint8ListFromList(rlp.encode(_encodeToRlp(this, innerSignature))); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /lib/src/contracts/abi/tuple.dart: -------------------------------------------------------------------------------- 1 | part of '../../../web3dart.dart'; 2 | 3 | /// Tuple Type 4 | class TupleType extends AbiType> { 5 | /// Constructor. 6 | const TupleType(this.types); 7 | 8 | /// The types used to encode the individual components of this tuple. 9 | final List types; 10 | 11 | @override 12 | String get name { 13 | final nameBuffer = StringBuffer('('); 14 | 15 | for (var i = 0; i < types.length; i++) { 16 | if (i != 0) { 17 | nameBuffer.write(','); 18 | } 19 | nameBuffer.write(types[i].name); 20 | } 21 | 22 | nameBuffer.write(')'); 23 | return nameBuffer.toString(); 24 | } 25 | 26 | @override 27 | EncodingLengthInfo get encodingLength { 28 | var trackedLength = 0; 29 | 30 | // tuples are dynamic iff any of their member types is dynamic. Otherwise, 31 | // it's just all static members concatenated, together. 32 | for (final type in types) { 33 | final length = type.encodingLength; 34 | if (length.isDynamic) return const EncodingLengthInfo.dynamic(); 35 | 36 | trackedLength += length.length!; 37 | } 38 | 39 | return EncodingLengthInfo(trackedLength); 40 | } 41 | 42 | @override 43 | void encode(List data, LengthTrackingByteSink buffer) { 44 | // Formal definition of the encoding: https://solidity.readthedocs.io/en/develop/abi-spec.html#formal-specification-of-the-encoding 45 | assert(data.length == types.length); 46 | 47 | // first, encode all non-dynamic values. For each dynamic value we 48 | // encounter, encode its position instead. Then, encode all the dynamic 49 | // values. 50 | var currentDynamicOffset = 0; 51 | final dynamicHeaderPositions = List.filled(data.length, -1); 52 | 53 | for (var i = 0; i < data.length; i++) { 54 | final payload = data[i]; 55 | final type = types[i]; 56 | 57 | if (type.encodingLength.isDynamic) { 58 | // just write a bunch of zeroes, we later have to encode the relative 59 | // offset here. 60 | dynamicHeaderPositions[i] = buffer.length; 61 | buffer.add(Uint8List(sizeUnitBytes)); 62 | 63 | currentDynamicOffset += sizeUnitBytes; 64 | } else { 65 | final lengthBefore = buffer.length; 66 | type.encode(payload, buffer); 67 | 68 | currentDynamicOffset += buffer.length - lengthBefore; 69 | } 70 | } 71 | 72 | // now that the heads are written, write tails for the dynamic values 73 | for (var i = 0; i < data.length; i++) { 74 | if (!types[i].encodingLength.isDynamic) continue; 75 | 76 | // replace the 32 zero-bytes with the actual encoded offset 77 | const UintType().encodeReplace( 78 | dynamicHeaderPositions[i], 79 | BigInt.from(currentDynamicOffset), 80 | buffer, 81 | ); 82 | 83 | final lengthBefore = buffer.length; 84 | types[i].encode(data[i], buffer); 85 | currentDynamicOffset += buffer.length - lengthBefore; 86 | } 87 | } 88 | 89 | @override 90 | DecodingResult decode(ByteBuffer buffer, int offset) { 91 | final decoded = []; 92 | var headersLength = 0; 93 | var dynamicLength = 0; 94 | 95 | for (final type in types) { 96 | if (type.encodingLength.isDynamic) { 97 | final positionResult = 98 | const UintType().decode(buffer, offset + headersLength); 99 | headersLength += positionResult.bytesRead; 100 | 101 | final position = positionResult.data.toInt(); 102 | 103 | final dataResult = type.decode(buffer, offset + position); 104 | dynamicLength += dataResult.bytesRead; 105 | decoded.add(dataResult.data); 106 | } else { 107 | final result = type.decode(buffer, offset + headersLength); 108 | headersLength += result.bytesRead; 109 | decoded.add(result.data); 110 | } 111 | } 112 | 113 | return DecodingResult(decoded, headersLength + dynamicLength); 114 | } 115 | 116 | @override 117 | int get hashCode => 37 * types.hashCode; 118 | 119 | @override 120 | bool operator ==(other) { 121 | return identical(this, other) || (other is TupleType && _equalTypes(other)); 122 | } 123 | 124 | bool _equalTypes(TupleType o) { 125 | if (o.types.length != types.length) return false; 126 | 127 | for (var i = 0; i < types.length; i++) { 128 | if (types[i] != o.types[i]) return false; 129 | } 130 | return true; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/core/transaction_information_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | import 'package:wallet/wallet.dart'; 6 | 7 | void main() { 8 | test('parses full object', () async { 9 | final parsed = TransactionReceipt.fromMap( 10 | json.decode( 11 | ''' 12 | { 13 | "blockHash": "0x5548b5f215b99674c7f23c9a701a005b5c18e4a963b55163eddada54562ac521", 14 | "blockNumber": "0x18", 15 | "contractAddress": "0x6671e02bb8bd3a234b13d79d1c285a9df657233d", 16 | "cumulativeGasUsed": "0x4cc5f", 17 | "from": "0xf8c59caf9bb8a7a2991160b592ac123108d88f7b", 18 | "gasUsed": "0x4cc5f", 19 | "logs": [ 20 | { 21 | "logIndex": "0x1", 22 | "blockNumber": "0x1b4", 23 | "blockHash": "0x8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d", 24 | "transactionHash": "0xdf829c5a142f1fccd7d8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcf", 25 | "transactionIndex": "0x0", 26 | "address": "0x16c5785ac562ff41e2dcfdf829c5a142f1fccd7d", 27 | "data": "0x0000000000000000000000000000000000000000000000000000000000000000", 28 | "topics": ["0x59ebeb90bc63057b6515673c3ecf9438e5058bca0f92585014eced636878c9a5"] 29 | } 30 | ], 31 | "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 32 | "root": "0x89628cd74b7246a144781e0f537bac145df645945c213f82ab45f4c6729f1e4c", 33 | "to": "0xf8c59caf9bb8a7a2991160b592ac123108d88f7b", 34 | "transactionHash": "0xb75a96c4751ff03b1bdcf5300e80a45e788e52650b0a4e2294e7496c215f4c9d", 35 | "transactionIndex": "0x18", 36 | "status": "0x1" 37 | } 38 | ''', 39 | ) as Map, 40 | ); 41 | expect( 42 | parsed, 43 | TransactionReceipt( 44 | transactionHash: hexToBytes( 45 | '0xb75a96c4751ff03b1bdcf5300e80a45e788e52650b0a4e2294e7496c215f4c9d', 46 | ), 47 | transactionIndex: 24, 48 | blockHash: hexToBytes( 49 | '0x5548b5f215b99674c7f23c9a701a005b5c18e4a963b55163eddada54562ac521', 50 | ), 51 | cumulativeGasUsed: BigInt.from(314463), 52 | blockNumber: const BlockNum.exact(24), 53 | contractAddress: EthereumAddress.fromHex( 54 | '0x6671e02bb8bd3a234b13d79d1c285a9df657233d', 55 | ), 56 | status: true, 57 | from: EthereumAddress.fromHex( 58 | '0xf8c59caf9bb8a7a2991160b592ac123108d88f7b', 59 | ), 60 | to: EthereumAddress.fromHex( 61 | '0xf8c59caf9bb8a7a2991160b592ac123108d88f7b', 62 | ), 63 | gasUsed: BigInt.from(314463), 64 | logs: [ 65 | FilterEvent( 66 | removed: false, 67 | logIndex: 1, 68 | blockNum: 436, 69 | blockHash: 70 | '0x8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcfdf829c5a142f1fccd7d', 71 | transactionHash: 72 | '0xdf829c5a142f1fccd7d8216c5785ac562ff41e2dcfdf5785ac562ff41e2dcf', 73 | transactionIndex: 0, 74 | address: EthereumAddress.fromHex( 75 | '0x16c5785ac562ff41e2dcfdf829c5a142f1fccd7d', 76 | ), 77 | data: 78 | '0x0000000000000000000000000000000000000000000000000000000000000000', 79 | topics: [ 80 | '0x59ebeb90bc63057b6515673c3ecf9438e5058bca0f92585014eced636878c9a5', 81 | ], 82 | ), 83 | ], 84 | ), 85 | ); 86 | }); 87 | test('parses incomplete object', () async { 88 | final parsed = TransactionReceipt.fromMap( 89 | json.decode( 90 | ''' 91 | { 92 | "blockHash": "0x5548b5f215b99674c7f23c9a701a005b5c18e4a963b55163eddada54562ac521", 93 | "cumulativeGasUsed": "0x4cc5f", 94 | "transactionHash": "0xb75a96c4751ff03b1bdcf5300e80a45e788e52650b0a4e2294e7496c215f4c9d", 95 | "transactionIndex": "0x18" 96 | } 97 | ''', 98 | ) as Map, 99 | ); 100 | expect( 101 | parsed, 102 | TransactionReceipt( 103 | transactionHash: hexToBytes( 104 | '0xb75a96c4751ff03b1bdcf5300e80a45e788e52650b0a4e2294e7496c215f4c9d', 105 | ), 106 | transactionIndex: 24, 107 | blockHash: hexToBytes( 108 | '0x5548b5f215b99674c7f23c9a701a005b5c18e4a963b55163eddada54562ac521', 109 | ), 110 | cumulativeGasUsed: BigInt.from(314463), 111 | ), 112 | ); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /test/credentials/public_key_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:web3dart/web3dart.dart'; 3 | 4 | final pubKeys = { 5 | '038e5d1fccb6b800b4e0fde5080a8c3628a302c4767e7687bea79ba24c6ac268e2': 6 | '048e5d1fccb6b800b4e0fde5080a8c3628a302c4767e7687bea79ba24c6ac268e275392bb442ebba2b92e1bb00668d1c34f69c0e51aba49e3a0189f9674d80a8a1', 7 | '02c776abf37ebf543d1c28f1e44fb745128f51a7b897defb949de34e0735da1e29': 8 | '04c776abf37ebf543d1c28f1e44fb745128f51a7b897defb949de34e0735da1e29c757332d95941fca1cd0ed3e791f1234513d98fdfb24d2192ed5b61bf2391536', 9 | '02139b830244fe67ae534002a16f2ca227e6f8d1e9aafba8a83f9a58bf14aab619': 10 | '04139b830244fe67ae534002a16f2ca227e6f8d1e9aafba8a83f9a58bf14aab6195d7706766285ab1f71872e32f663e173eec70003c0172b798588b6b070223ac2', 11 | '02b3b865d9a86cf9521da22d135b0ee37d7acec35e83a14729c9c612110883db2a': 12 | '04b3b865d9a86cf9521da22d135b0ee37d7acec35e83a14729c9c612110883db2a9f67f78dc2c91b06c5cfd96ea568cb3907a424aa8c5ac99459140b3432357ffa', 13 | '02f76454105bf1bcc09d928930776f67c4ddc67dd3e19d66b93fa850fbfb2ec0c4': 14 | '04f76454105bf1bcc09d928930776f67c4ddc67dd3e19d66b93fa850fbfb2ec0c4d6ffc9b3c6f96809884b36c8ea3a0b5d7d7e8812a246c1b6f051d568df30986c', 15 | '0294409b98171941dfa953e22d508252ac9dce24211a6051cfc5288c7f2bd82419': 16 | '0494409b98171941dfa953e22d508252ac9dce24211a6051cfc5288c7f2bd82419543fe6afca1d98a5ac2b8c7c9c9223075e8d67ee971c6b57558dbefb763fa598', 17 | '0299208f9a219712054e93dd34ecf6cb7d3878e4df6f437164d3e7fb443b52a3ed': 18 | '0499208f9a219712054e93dd34ecf6cb7d3878e4df6f437164d3e7fb443b52a3ed07c3381510c8d48e1529b90bcf565ff7c8db221cafdad821549d8add4b188914', 19 | '02f29766aba359386368bec0e5f123f9c596b736f915e1963f58ede0b14b3cb99f': 20 | '04f29766aba359386368bec0e5f123f9c596b736f915e1963f58ede0b14b3cb99f746b6f6c50ca16d5b894cc6cb6b2323ab9978cf0de17754e619b693000266e66', 21 | '03d2046ccc091c5e62599e0f09fc6c410b56156e9202b0731bf8c4b6aa2a8db09e': 22 | '04d2046ccc091c5e62599e0f09fc6c410b56156e9202b0731bf8c4b6aa2a8db09ef9474b2a080a05d6a0fb7e200845a7cbb0b310ed08945835fd105fc94eeaf8c1', 23 | '03fd0bc424274448d2162329932094275da0746c34e776b530a11d3749eb209a02': 24 | '04fd0bc424274448d2162329932094275da0746c34e776b530a11d3749eb209a02aece16c649ece7d7054b81c332ea78549d3c78d6d949cbb42ec8ef953beaa6a5', 25 | '0311c757b9920c0cfa5165d3b8881cc26c15db92ef7d5f1a54d670189b14432f74': 26 | '0411c757b9920c0cfa5165d3b8881cc26c15db92ef7d5f1a54d670189b14432f7459cfbcf639d67437e9d51d1cf5d1a23715286ae0a31c5e21e2e69cc7906e7677', 27 | '03fede584566a6ad53e5d6c5b606b4e348a61dda8c7ca07b06f1095d8bdc28556c': 28 | '04fede584566a6ad53e5d6c5b606b4e348a61dda8c7ca07b06f1095d8bdc28556c8cedb3b168504bbf5fb408783183ddf062bf238422290c300ce92da06302b155', 29 | '03a5e614c13cf554740d9511d788edc7ebac939d6d10c2636a9ed2c05648c31bf8': 30 | '04a5e614c13cf554740d9511d788edc7ebac939d6d10c2636a9ed2c05648c31bf8d08c0fb9f672bac94125ce4dd9f748349118f7277540b31ac1532fcd8f8a6ac7', 31 | '03b2a1f693ba80940033c8950ac7b260506ce824317ab8300bc4235f4fb574de01': 32 | '04b2a1f693ba80940033c8950ac7b260506ce824317ab8300bc4235f4fb574de015d849bbc860ae2955f89ff50f54861ec1877eb07832c69f92b30124918fa5cb3', 33 | '0285a52a6c98faa9c460311b525f3e3206e2bb3da8bb4d7f6d9d0d26f156ab3899': 34 | '0485a52a6c98faa9c460311b525f3e3206e2bb3da8bb4d7f6d9d0d26f156ab3899dfd3e79ada5840387c8ba5407c8750b605f7ed121ad317f8e3694b9356c33996', 35 | '0317f33f63c22171fccb30ff14f73c32245b262b9d564e60367a80f64556a7dc70': 36 | '0417f33f63c22171fccb30ff14f73c32245b262b9d564e60367a80f64556a7dc708cfac410459d79c956ba9930b051080273da7fe4d1bc46bd5fe30a6c39087561', 37 | '03ae643be85eacad9956164aa4cef4b98118cb56848eb98fde2111c27d0c887d5a': 38 | '04ae643be85eacad9956164aa4cef4b98118cb56848eb98fde2111c27d0c887d5a88b5af6e90c7b37eb1984fe3602cf18b4013d107f5203562198c5796343de38f', 39 | '027ca219a794f0e45278d1f5ddaf1c637dc38e3a64744fff1058ed56e7e9674855': 40 | '047ca219a794f0e45278d1f5ddaf1c637dc38e3a64744fff1058ed56e7e9674855f992b9305101f28eeaa8f981f752b91c4570b1ef7c2ac8e5bbe86d151836c202', 41 | '03d62489b549324a78c61fe19e967296ea0e3b368244f5edb2b307bd19124b02c5': 42 | '04d62489b549324a78c61fe19e967296ea0e3b368244f5edb2b307bd19124b02c5bb5443fe589b9f0be9e0d91bb10b89ee214c066ad92ee71dccd1af3345913ca1', 43 | '036c5c3d1144c2809abdc64c93e674c0b060c792f0a3dda71589fa9f6743d6f816': 44 | '046c5c3d1144c2809abdc64c93e674c0b060c792f0a3dda71589fa9f6743d6f8162375ea09d9f3b8bf191c97c00bc3d2c67db279e838a282e3c5464d93a0933c4d', 45 | }; 46 | 47 | void main() { 48 | test('decompresses secp256k1 public keys', () { 49 | pubKeys.forEach((compressed, uncompressed) { 50 | var bytes = hexToBytes(compressed); 51 | var pubKeyBytes = decompressPublicKey(bytes); 52 | expect(bytesToHex(pubKeyBytes), uncompressed); 53 | 54 | bytes = hexToBytes(uncompressed); 55 | pubKeyBytes = compressPublicKey(bytes); 56 | expect(bytesToHex(pubKeyBytes), compressed); 57 | }); 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /test/crypto/secp256k1_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | 6 | const Map _privateKeysToAddress = { 7 | 'a2fd51b96dc55aeb14b30d55a6b3121c7b9c599500c1beb92a389c3377adc86e': 8 | '76778e046D73a5B8ce3d03749cE6B1b3D6a12E36', 9 | 'f1f7a560cf6a730df8404eca67e28c1d61a611634417aaa45aa3e2bec84dd71b': 10 | 'C914Bb2ba888e3367bcecEb5C2d99DF7C7423706', 11 | '07a0eeacaf4eb0a43a75c4da0c22c22ab1b4bcc29cac198432b93fa71ec62b39': 12 | 'ea7624696aA08Cec594e292eC16dcFC5dA9bDa1f', 13 | }; 14 | 15 | void main() { 16 | _privateKeysToAddress.forEach((key, address) { 17 | test('finds correct address for private key', () { 18 | final publicKey = privateKeyBytesToPublic(hexToBytes(key)); 19 | final foundAddress = publicKeyToAddress(publicKey); 20 | 21 | expect(bytesToHex(foundAddress), equalsIgnoringCase(address)); 22 | }); 23 | }); 24 | 25 | test('finds public key for private key', () { 26 | expect( 27 | bytesToHex( 28 | privateKeyBytesToPublic( 29 | hexToBytes( 30 | 'a392604efc2fad9c0b3da43b5f698a2e3f270f170d859912be0d54742275c5f6', 31 | ), 32 | ), 33 | ), 34 | '506bc1dc099358e5137292f4efdd57e400f29ba5132aa5d12b18dac1c1f6aab' 35 | 'a645c0b7b58158babbfa6c6cd5a48aa7340a8749176b120e8516216787a13dc76', 36 | ); 37 | }); 38 | 39 | test('produces a valid signature', () { 40 | // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/test/index.js#L523 41 | final hashedPayload = hexToBytes( 42 | '82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28', 43 | ); 44 | final privKey = hexToBytes( 45 | '3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1', 46 | ); 47 | final sig = sign(hashedPayload, privKey); 48 | 49 | expect( 50 | sig.r, 51 | hexToInt( 52 | '99e71a99cb2270b8cac5254f9e99b6210c6c10224a1579cf389ef88b20a1abe9', 53 | ), 54 | ); 55 | expect( 56 | sig.s, 57 | hexToInt( 58 | '129ff05af364204442bdb53ab6f18a99ab48acc9326fa689f228040429e3ca66', 59 | ), 60 | ); 61 | expect(sig.v, 27); 62 | }); 63 | 64 | test('signatures recover the public key of the signer', () { 65 | final messages = [ 66 | 'Hello world', 67 | 'UTF8 chars ©âèíöu ∂øµ€', 68 | '🚀✨🌎', 69 | DateTime.now().toString(), 70 | ]; 71 | final privateKeys = [ 72 | '3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1', 73 | 'a69ab6a98f9c6a98b9a6b8e9b6a8e69c6ea96b5050eb77a17e3ba685805aeb88', 74 | 'ca7eb9798e79c8799ea79aec7be98a7b9a7c98ae7b98061a53be764a85b8e785', 75 | '192b519765c9589a6b8c9a486ab938cba9638ab876056237649264b9cb96d88f', 76 | 'b6a8f6a96931ad89d3a98e69ad6b98794673615b74675d7b5a674ba82b648a6d', 77 | ]; 78 | 79 | for (final message in messages) { 80 | final messageHash = keccak256(Uint8List.fromList(utf8.encode(message))); 81 | 82 | for (final privateKey in privateKeys) { 83 | final publicKey = privateKeyBytesToPublic(hexToBytes(privateKey)); 84 | final signature = sign(messageHash, hexToBytes(privateKey)); 85 | 86 | final recoveredPublicKey = ecRecover(messageHash, signature); 87 | expect(bytesToHex(publicKey), bytesToHex(recoveredPublicKey)); 88 | } 89 | } 90 | }); 91 | 92 | test('signature validity can be properly verified', () { 93 | final messages = [ 94 | 'Hello world', 95 | 'UTF8 chars ©âèíöu ∂øµ€', 96 | '🚀✨🌎', 97 | DateTime.now().toString(), 98 | ]; 99 | final privateKeys = [ 100 | '3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1', 101 | 'a69ab6a98f9c6a98b9a6b8e9b6a8e69c6ea96b5050eb77a17e3ba685805aeb88', 102 | 'ca7eb9798e79c8799ea79aec7be98a7b9a7c98ae7b98061a53be764a85b8e785', 103 | '192b519765c9589a6b8c9a486ab938cba9638ab876056237649264b9cb96d88f', 104 | 'b6a8f6a96931ad89d3a98e69ad6b98794673615b74675d7b5a674ba82b648a6d', 105 | ]; 106 | final invalidPublicKeys = [ 107 | '1cb507195305b0c70da9f0a60f06ae8d605a80f0abc05a08df50a50f8e085da0f5a8f0e508adf510f0b1827538649a7bc79a47d49ae64b06ac60a96195231241', 108 | '0c7a0980f1803b09c88a4c78a4186d48a76739a34a685075a084179a46c96a5d8705a0845a365254a34679a67413567a426ca1436e5758f96a57a5f78a4321a7', 109 | 'c6b6a1c431b4a374d38a549ef7a659e7505fa07e574648a63537a6d546f85a48e73765a37a4d64c976a449a64e853a75684e9a75d964a8563e684a66ea058494', 110 | 'ba969f86a968e76ba9769f6a98e6f98a6d9876f9a87b9f876eb987ac6b98a6d98f5a97645865e37a4264d3a63865187687917619876bc9a876d986fa9b861972', 111 | 'a9173ba7961b6fdb37d618036b0abd8a7e6b9a7f6b98e769a861982639451982739ba9afd9e8a487146728354198a9fda9e481239145972364597a9da5976129', 112 | ]; 113 | 114 | for (final message in messages) { 115 | final messageHash = keccak256(Uint8List.fromList(utf8.encode(message))); 116 | 117 | for (final privateKey in privateKeys) { 118 | final originalPublicKey = 119 | privateKeyBytesToPublic(hexToBytes(privateKey)); 120 | final signature = sign(messageHash, hexToBytes(privateKey)); 121 | 122 | expect( 123 | isValidSignature(messageHash, signature, originalPublicKey), 124 | isTrue, 125 | reason: 'The signature should be valid', 126 | ); 127 | 128 | for (final invalidPublicKey in invalidPublicKeys) { 129 | expect( 130 | isValidSignature( 131 | messageHash, 132 | signature, 133 | hexToBytes(invalidPublicKey), 134 | ), 135 | isFalse, 136 | reason: 'The signature should be invalid', 137 | ); 138 | } 139 | } 140 | } 141 | }); 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/credentials/credentials.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | /// Anything that can sign payloads with a private key. 4 | abstract class Credentials { 5 | static const _messagePrefix = '\u0019Ethereum Signed Message:\n'; 6 | 7 | /// Whether these [Credentials] are safe to be copied to another isolate and 8 | /// can operate there. 9 | /// If this getter returns true, the client might chose to perform the 10 | /// expensive signing operations on another isolate. 11 | bool get isolateSafe => false; 12 | 13 | EthereumAddress get address; 14 | 15 | /// Signs the [payload] with a private key. The output will be like the 16 | /// bytes representation of the [eth_sign RPC method](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign), 17 | /// but without the "Ethereum signed message" prefix. 18 | /// The [payload] parameter contains the raw data, not a hash. 19 | Uint8List signToUint8List( 20 | Uint8List payload, { 21 | int? chainId, 22 | bool isEIP1559 = false, 23 | }) { 24 | final signature = 25 | signToEcSignature(payload, chainId: chainId, isEIP1559: isEIP1559); 26 | 27 | final r = padUint8ListTo32(unsignedIntToBytes(signature.r)); 28 | final s = padUint8ListTo32(unsignedIntToBytes(signature.s)); 29 | final v = unsignedIntToBytes(BigInt.from(signature.v)); 30 | 31 | // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L63 32 | return uint8ListFromList(r + s + v); 33 | } 34 | 35 | /// Signs the [payload] with a private key and returns the obtained 36 | /// signature. 37 | MsgSignature signToEcSignature( 38 | Uint8List payload, { 39 | int? chainId, 40 | bool isEIP1559 = false, 41 | }); 42 | 43 | /// Signs an Ethereum specific signature. This method is equivalent to 44 | /// [signToUint8List], but with a special prefix so that this method can't be used to 45 | /// sign, for instance, transactions. 46 | Uint8List signPersonalMessageToUint8List(Uint8List payload, {int? chainId}) { 47 | final prefix = _messagePrefix + payload.length.toString(); 48 | final prefixBytes = ascii.encode(prefix); 49 | 50 | // will be a Uint8List, see the documentation of Uint8List.+ 51 | final concat = uint8ListFromList(prefixBytes + payload); 52 | 53 | return signToUint8List(concat, chainId: chainId); 54 | } 55 | } 56 | 57 | /// Credentials where the [address] is known synchronously. 58 | abstract class CredentialsWithKnownAddress extends Credentials { 59 | /// The ethereum address belonging to this credential. 60 | @override 61 | EthereumAddress get address; 62 | } 63 | 64 | /// Interface for [Credentials] that don't sign transactions locally, for 65 | /// instance because the private key is not known to this library. 66 | abstract class CustomTransactionSender extends Credentials { 67 | Future sendTransaction(Transaction transaction); 68 | } 69 | 70 | /// Credentials that can sign payloads with an Ethereum private key. 71 | class EthPrivateKey extends CredentialsWithKnownAddress { 72 | /// Creates a private key from a byte array representation. 73 | /// 74 | /// The bytes are interpreted as an unsigned integer forming the private key. 75 | EthPrivateKey(this.privateKey) 76 | : privateKeyInt = bytesToUnsignedInt(privateKey); 77 | 78 | /// Parses a private key from a hexadecimal representation. 79 | EthPrivateKey.fromHex(String hex) : this(hexToBytes(hex)); 80 | 81 | /// Creates a private key from the underlying number. 82 | EthPrivateKey.fromInt(this.privateKeyInt) 83 | : privateKey = unsignedIntToBytes(privateKeyInt); 84 | 85 | /// Creates a new, random private key from the [random] number generator. 86 | /// 87 | /// For security reasons, it is very important that the random generator used 88 | /// is cryptographically secure. The private key could be reconstructed by 89 | /// someone else otherwise. Just using [Random()] is a very bad idea! At least 90 | /// use [Random.secure()]. 91 | factory EthPrivateKey.createRandom(Random random) { 92 | final key = generateNewPrivateKey(random); 93 | return EthPrivateKey(intToBytes(key)); 94 | } 95 | 96 | /// ECC's d private parameter. 97 | final BigInt privateKeyInt; 98 | final Uint8List privateKey; 99 | EthereumAddress? _cachedAddress; 100 | 101 | @override 102 | final bool isolateSafe = true; 103 | 104 | @override 105 | EthereumAddress get address { 106 | return _cachedAddress ??= 107 | EthereumAddress(publicKeyToAddress(privateKeyToPublic(privateKeyInt))); 108 | } 109 | 110 | /// Get the encoded public key in an (uncompressed) byte representation. 111 | Uint8List get encodedPublicKey => privateKeyToPublic(privateKeyInt); 112 | 113 | /// The public key corresponding to this private key. 114 | ECPoint get publicKey => (params.G * privateKeyInt)!; 115 | 116 | @override 117 | MsgSignature signToEcSignature( 118 | Uint8List payload, { 119 | int? chainId, 120 | bool isEIP1559 = false, 121 | }) { 122 | final signature = secp256k1.sign(keccak256(payload), privateKey); 123 | 124 | // https://github.com/ethereumjs/ethereumjs-util/blob/8ffe697fafb33cefc7b7ec01c11e3a7da787fe0e/src/signature.ts#L26 125 | // be aware that signature.v already is recovery + 27 126 | int chainIdV; 127 | if (isEIP1559) { 128 | chainIdV = signature.v - 27; 129 | } else { 130 | chainIdV = chainId != null 131 | ? (signature.v - 27 + (chainId * 2 + 35)) 132 | : signature.v; 133 | } 134 | return MsgSignature(signature.r, signature.s, chainIdV); 135 | } 136 | 137 | @override 138 | bool operator ==(Object other) => 139 | identical(this, other) || 140 | other is EthPrivateKey && 141 | runtimeType == other.runtimeType && 142 | eq.equals(privateKey, other.privateKey); 143 | 144 | @override 145 | int get hashCode => privateKey.hashCode; 146 | } 147 | -------------------------------------------------------------------------------- /test/contracts/abi/functions_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:test/test.dart'; 4 | import 'package:web3dart/web3dart.dart'; 5 | 6 | void main() { 7 | const baz = ContractFunction('baz', [ 8 | FunctionParameter('number', UintType(length: 32)), 9 | FunctionParameter('flag', BoolType()), 10 | ]); 11 | const bar = ContractFunction('bar', [ 12 | FunctionParameter( 13 | 'xy', 14 | FixedLengthArray(type: FixedBytes(3), length: 2), 15 | ), 16 | ]); 17 | 18 | const sam = ContractFunction( 19 | 'sam', 20 | [ 21 | FunctionParameter('b1', DynamicBytes()), 22 | FunctionParameter('b2', BoolType()), 23 | FunctionParameter('b3', DynamicLengthArray(type: UintType())), 24 | ], 25 | outputs: [ 26 | FunctionParameter('b1', DynamicBytes()), 27 | FunctionParameter('b2', BoolType()), 28 | FunctionParameter('b3', DynamicLengthArray(type: UintType())), 29 | ], 30 | ); 31 | 32 | test('parses contract abi', () { 33 | // Taken from: https://solidity.readthedocs.io/en/develop/abi-spec.html#handling-tuple-types 34 | 35 | final abi = ContractAbi.fromJson( 36 | ''' 37 | [ 38 | { 39 | "name": "f", 40 | "type": "function", 41 | "inputs": [ 42 | { 43 | "name": "s", 44 | "type": "tuple", 45 | "components": [ 46 | { 47 | "name": "a", 48 | "type": "uint256" 49 | }, 50 | { 51 | "name": "b", 52 | "type": "uint256[]" 53 | }, 54 | { 55 | "name": "c", 56 | "type": "tuple[]", 57 | "components": [ 58 | { 59 | "name": "x", 60 | "type": "uint256" 61 | }, 62 | { 63 | "name": "y", 64 | "type": "uint256" 65 | } 66 | ] 67 | } 68 | ] 69 | }, 70 | { 71 | "name": "t", 72 | "type": "tuple", 73 | "components": [ 74 | { 75 | "name": "x", 76 | "type": "uint256" 77 | }, 78 | { 79 | "name": "y", 80 | "type": "uint256" 81 | } 82 | ] 83 | }, 84 | { 85 | "name": "a", 86 | "type": "uint256" 87 | } 88 | ], 89 | "outputs": [] 90 | } 91 | ] 92 | ''', 93 | 'name', 94 | ); 95 | 96 | // Declaration of the function in solidity: 97 | // struct S { uint a; uint[] b; T[] c; } 98 | // struct T { uint x; uint y; } 99 | // function f(S memory s, T memory t, uint a) public; 100 | final function = abi.functions.single; 101 | 102 | expect(function.name, 'f'); 103 | expect(function.outputs, isEmpty); 104 | expect(function.isPayable, false); 105 | expect(function.isConstant, false); 106 | expect(function.isConstructor, false); 107 | expect(function.isDefault, false); 108 | 109 | final s = function.parameters[0]; 110 | final t = function.parameters[1]; 111 | final a = function.parameters[2]; 112 | 113 | expect(s.name, 's'); 114 | expect(t.name, 't'); 115 | expect(a.name, 'a'); 116 | 117 | // todo write expects for s and t. These are so annoying to write, maybe 118 | // just override hashCode and equals? 119 | 120 | expect( 121 | () { 122 | final type = a.type; 123 | return type is UintType && type.length == 256; 124 | }(), 125 | true, 126 | ); 127 | }); 128 | 129 | test('functions en- and decode data', () { 130 | expect(baz.encodeName(), equals('baz(uint32,bool)')); 131 | expect( 132 | bytesToHex(baz.encodeCall([BigInt.from(69), true]), include0x: true), 133 | '0xcdcd77c0' 134 | '0000000000000000000000000000000000000000000000000000000000000045' 135 | '0000000000000000000000000000000000000000000000000000000000000001', 136 | ); 137 | 138 | expect(bar.encodeName(), equals('bar(bytes3[2])')); 139 | expect( 140 | bytesToHex( 141 | bar.encodeCall([ 142 | [ 143 | uint8ListFromList(utf8.encode('abc')), 144 | uint8ListFromList(utf8.encode('def')), 145 | ] 146 | ]), 147 | include0x: true, 148 | ), 149 | '0xfce353f6' 150 | '6162630000000000000000000000000000000000000000000000000000000000' 151 | '6465660000000000000000000000000000000000000000000000000000000000', 152 | ); 153 | 154 | expect( 155 | bytesToHex( 156 | sam.encodeCall([ 157 | uint8ListFromList(utf8.encode('dave')), 158 | true, 159 | [BigInt.from(1), BigInt.from(2), BigInt.from(3)], 160 | ]), 161 | include0x: true, 162 | ), 163 | '0xa5643bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003', 164 | ); 165 | 166 | expect( 167 | sam 168 | .decodeReturnValues( 169 | '0x0000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003', 170 | ) 171 | .first, 172 | equals(utf8.encode('dave')), 173 | ); 174 | }); 175 | } 176 | -------------------------------------------------------------------------------- /lib/src/core/transaction_signer.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | class _SigningInput { 4 | _SigningInput({ 5 | required this.transaction, 6 | required this.credentials, 7 | this.chainId, 8 | }); 9 | 10 | final Transaction transaction; 11 | final Credentials credentials; 12 | final int? chainId; 13 | } 14 | 15 | Future<_SigningInput> _fillMissingData({ 16 | required Credentials credentials, 17 | required Transaction transaction, 18 | int? chainId, 19 | bool loadChainIdFromNetwork = false, 20 | Web3Client? client, 21 | }) async { 22 | if (loadChainIdFromNetwork && chainId != null) { 23 | throw ArgumentError( 24 | "You can't specify loadChainIdFromNetwork and specify a custom chain id!", 25 | ); 26 | } 27 | 28 | final sender = transaction.from ?? credentials.address; 29 | var gasPrice = transaction.gasPrice; 30 | 31 | if (client == null && 32 | (transaction.nonce == null || 33 | transaction.maxGas == null || 34 | loadChainIdFromNetwork || 35 | (!transaction.isEIP1559 && gasPrice == null))) { 36 | throw ArgumentError('Client is required to perform network actions'); 37 | } 38 | 39 | if (!transaction.isEIP1559 && gasPrice == null) { 40 | gasPrice = await client!.getGasPrice(); 41 | } 42 | 43 | var maxFeePerGas = transaction.maxFeePerGas; 44 | var maxPriorityFeePerGas = transaction.maxPriorityFeePerGas; 45 | 46 | if (transaction.isEIP1559) { 47 | maxPriorityFeePerGas ??= await _getMaxPriorityFeePerGas(); 48 | maxFeePerGas ??= await _getMaxFeePerGas( 49 | client!, 50 | maxPriorityFeePerGas.getInWei, 51 | ); 52 | } 53 | 54 | final nonce = transaction.nonce ?? 55 | await client! 56 | .getTransactionCount(sender, atBlock: const BlockNum.pending()); 57 | 58 | final maxGas = transaction.maxGas ?? 59 | await client! 60 | .estimateGas( 61 | sender: sender, 62 | to: transaction.to, 63 | data: transaction.data, 64 | value: transaction.value, 65 | gasPrice: gasPrice, 66 | maxPriorityFeePerGas: maxPriorityFeePerGas, 67 | maxFeePerGas: maxFeePerGas, 68 | ) 69 | .then((bigInt) => bigInt.toInt()); 70 | 71 | // apply default values to null fields 72 | final modifiedTransaction = transaction.copyWith( 73 | value: transaction.value ?? EtherAmount.zero(), 74 | maxGas: maxGas, 75 | from: sender, 76 | data: transaction.data ?? Uint8List(0), 77 | gasPrice: gasPrice, 78 | nonce: nonce, 79 | maxPriorityFeePerGas: maxPriorityFeePerGas, 80 | maxFeePerGas: maxFeePerGas, 81 | ); 82 | 83 | int resolvedChainId; 84 | if (!loadChainIdFromNetwork) { 85 | resolvedChainId = chainId!; 86 | } else { 87 | resolvedChainId = await client!.getNetworkId(); 88 | } 89 | 90 | return _SigningInput( 91 | transaction: modifiedTransaction, 92 | credentials: credentials, 93 | chainId: resolvedChainId, 94 | ); 95 | } 96 | 97 | Uint8List prependTransactionType(int type, Uint8List transaction) { 98 | return Uint8List(transaction.length + 1) 99 | ..[0] = type 100 | ..setAll(1, transaction); 101 | } 102 | 103 | Uint8List signTransactionRaw( 104 | Transaction transaction, 105 | Credentials c, { 106 | int? chainId = 1, 107 | }) { 108 | final encoded = transaction.getUnsignedSerialized(chainId: chainId); 109 | final signature = c.signToEcSignature( 110 | encoded, 111 | chainId: chainId, 112 | isEIP1559: transaction.isEIP1559, 113 | ); 114 | 115 | if (transaction.isEIP1559 && chainId != null) { 116 | return uint8ListFromList( 117 | rlp.encode( 118 | _encodeEIP1559ToRlp(transaction, signature, BigInt.from(chainId)), 119 | ), 120 | ); 121 | } 122 | return uint8ListFromList(rlp.encode(_encodeToRlp(transaction, signature))); 123 | } 124 | 125 | List _encodeEIP1559ToRlp( 126 | Transaction transaction, 127 | MsgSignature? signature, 128 | BigInt chainId, 129 | ) { 130 | final list = [ 131 | chainId, 132 | transaction.nonce, 133 | transaction.maxPriorityFeePerGas!.getInWei, 134 | transaction.maxFeePerGas!.getInWei, 135 | transaction.maxGas, 136 | ]; 137 | 138 | if (transaction.to != null) { 139 | list.add(transaction.to!.value); 140 | } else { 141 | list.add(''); 142 | } 143 | 144 | list 145 | ..add(transaction.value?.getInWei) 146 | ..add(transaction.data); 147 | 148 | list.add([]); // access list 149 | 150 | if (signature != null) { 151 | list 152 | ..add(signature.v) 153 | ..add(signature.r) 154 | ..add(signature.s); 155 | } 156 | 157 | return list; 158 | } 159 | 160 | List _encodeToRlp(Transaction transaction, MsgSignature? signature) { 161 | final list = [ 162 | transaction.nonce, 163 | transaction.gasPrice?.getInWei, 164 | transaction.maxGas, 165 | ]; 166 | 167 | if (transaction.to != null) { 168 | list.add(transaction.to!.value); 169 | } else { 170 | list.add(''); 171 | } 172 | 173 | list 174 | ..add(transaction.value?.getInWei) 175 | ..add(transaction.data); 176 | 177 | if (signature != null) { 178 | list 179 | ..add(signature.v) 180 | ..add(signature.r) 181 | ..add(signature.s); 182 | } 183 | 184 | return list; 185 | } 186 | 187 | Future _getMaxPriorityFeePerGas() { 188 | // We may want to compute this more accurately in the future, 189 | // using the formula "check if the base fee is correct". 190 | // See: https://eips.ethereum.org/EIPS/eip-1559 191 | return Future.value(EtherAmount.inWei(BigInt.from(1000000000))); 192 | } 193 | 194 | // Max Fee = (2 * Base Fee) + Max Priority Fee 195 | Future _getMaxFeePerGas( 196 | Web3Client client, 197 | BigInt maxPriorityFeePerGas, 198 | ) async { 199 | final blockInformation = await client.getBlockInformation(); 200 | final baseFeePerGas = blockInformation.baseFeePerGas; 201 | 202 | if (baseFeePerGas == null) { 203 | return EtherAmount.zero(); 204 | } 205 | 206 | return EtherAmount.inWei( 207 | baseFeePerGas.getInWei * BigInt.from(2) + maxPriorityFeePerGas, 208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /lib/src/contracts/abi/types.dart: -------------------------------------------------------------------------------- 1 | part of '../../../web3dart.dart'; 2 | 3 | /// The length of the encoding of a solidity type is always a multiplicative of 4 | /// this unit size. 5 | const int sizeUnitBytes = 32; 6 | 7 | /// A type that can be encoded and decoded as specified in the solidity ABI, 8 | /// available at https://solidity.readthedocs.io/en/develop/abi-spec.html 9 | abstract class AbiType { 10 | /// Constructor. 11 | const AbiType(); 12 | 13 | /// The name of this type, as it would appear in a method signature in the 14 | /// solidity ABI. 15 | String get name; 16 | 17 | /// Information about how long the encoding will be. 18 | EncodingLengthInfo get encodingLength; 19 | 20 | /// Writes [data] into the [buffer]. 21 | void encode(T data, LengthTrackingByteSink buffer); 22 | 23 | /// Decode. 24 | DecodingResult decode(ByteBuffer buffer, int offset); 25 | } 26 | 27 | /// Information about whether the length of an encoding depends on the data 28 | /// (dynamic) or is fixed (static). If it's static, also contains information 29 | /// about the length of the encoding. 30 | class EncodingLengthInfo { 31 | /// Constructor. 32 | const EncodingLengthInfo(this.length); 33 | 34 | /// Constructor. 35 | const EncodingLengthInfo.dynamic() : length = null; 36 | 37 | /// When this encoding length is not [isDynamic], the length (in bytes) of 38 | /// an encoded payload. Otherwise null. 39 | final int? length; 40 | 41 | /// Whether the length of the encoding will depend on the data being encoded. 42 | /// 43 | /// Types that have that property are called "dynamic types" in the solidity 44 | /// abi encoding and are treated differently when being a part of a tuple or 45 | /// an array. 46 | bool get isDynamic => length == null; 47 | } 48 | 49 | /// Calculates the amount of padding bytes needed so that the length of the 50 | /// padding plus the [bodyLength] is a multiplicative of [sizeUnitBytes]. If 51 | /// [allowEmpty] (defaults to false) is true, an empty length is allowed. 52 | /// Otherwise an empty [bodyLength] will be given a full [sizeUnitBytes] 53 | /// padding. 54 | int calculatePadLength(int bodyLength, {bool allowEmpty = false}) { 55 | assert(bodyLength >= 0); 56 | 57 | if (bodyLength == 0 && !allowEmpty) return sizeUnitBytes; 58 | 59 | final remainder = bodyLength % sizeUnitBytes; 60 | return remainder == 0 ? 0 : sizeUnitBytes - remainder; 61 | } 62 | 63 | /// Decoding Result. 64 | class DecodingResult { 65 | /// Constructor. 66 | DecodingResult(this.data, this.bytesRead); 67 | 68 | /// Data. 69 | final T data; 70 | 71 | /// Bytes read. 72 | final int bytesRead; 73 | 74 | @override 75 | String toString() { 76 | return 'DecodingResult($data, $bytesRead)'; 77 | } 78 | 79 | @override 80 | int get hashCode => data.hashCode * 31 + bytesRead.hashCode; 81 | 82 | @override 83 | bool operator ==(other) { 84 | return identical(this, other) || 85 | (other is DecodingResult && 86 | other.data == data && 87 | other.bytesRead == bytesRead); 88 | } 89 | } 90 | 91 | // some ABI types that are easy to construct because they have a fixed name 92 | const Map _easyTypes = { 93 | 'uint': UintType(), 94 | 'int': IntType(), 95 | 'address': AddressType(), 96 | 'bool': BoolType(), 97 | 'function': FunctionType(), 98 | 'bytes': DynamicBytes(), 99 | 'string': StringType(), 100 | }; 101 | 102 | final RegExp _trailingDigits = RegExp(r'^(?:\D|\d)*\D(\d*)$'); 103 | 104 | /// Array RegExp. 105 | final RegExp array = RegExp(r'^(.*)\[(\d*)\]$'); 106 | final RegExp _tuple = RegExp(r'^\((.*)\)$'); 107 | 108 | int _trailingNumber(String str) { 109 | final match = _trailingDigits.firstMatch(str); 110 | return int.parse(match!.group(1)!); 111 | } 112 | 113 | final _openingParenthesis = '('.codeUnitAt(0); 114 | final _closingParenthesis = ')'.codeUnitAt(0); 115 | final _comma = ','.codeUnitAt(0); 116 | 117 | /// Parses an ABI type from its [AbiType.name]. 118 | AbiType parseAbiType(String name) { 119 | if (_easyTypes.containsKey(name)) return _easyTypes[name]!; 120 | 121 | final arrayMatch = array.firstMatch(name); 122 | if (arrayMatch != null) { 123 | final type = parseAbiType(arrayMatch.group(1)!); 124 | final length = arrayMatch.group(2)!; 125 | 126 | if (length.isEmpty) { 127 | // T[], dynamic length then 128 | return DynamicLengthArray(type: type); 129 | } else { 130 | return FixedLengthArray(type: type, length: int.parse(length)); 131 | } 132 | } 133 | 134 | final tupleMatch = _tuple.firstMatch(name); 135 | if (tupleMatch != null) { 136 | final inner = tupleMatch.group(1)!; 137 | final types = []; 138 | 139 | // types are separated by a comma. However, we can't just inner.split(') 140 | // because tuples might be nested: (bool, (uint, string)) 141 | var openParenthesises = 0; 142 | final typeBuffer = StringBuffer(); 143 | 144 | for (final char in inner.codeUnits) { 145 | if (char == _comma && openParenthesises == 0) { 146 | types.add(parseAbiType(typeBuffer.toString())); 147 | typeBuffer.clear(); 148 | } else { 149 | typeBuffer.writeCharCode(char); 150 | 151 | if (char == _openingParenthesis) { 152 | openParenthesises++; 153 | } else if (char == _closingParenthesis) { 154 | openParenthesises--; 155 | } 156 | } 157 | } 158 | 159 | if (typeBuffer.isNotEmpty) { 160 | if (openParenthesises != 0) { 161 | throw ArgumentError( 162 | 'Could not parse abi type because of mismatched brackets: $name', 163 | ); 164 | } 165 | types.add(parseAbiType(typeBuffer.toString())); 166 | } 167 | 168 | return TupleType(types); 169 | } 170 | 171 | if (name.startsWith('uint')) { 172 | return UintType(length: _trailingNumber(name))..validate(); 173 | } else if (name.startsWith('int')) { 174 | return IntType(length: _trailingNumber(name))..validate(); 175 | } else if (name.startsWith('bytes')) { 176 | return FixedBytes(_trailingNumber(name))..validate(); 177 | } 178 | 179 | throw ArgumentError('Could not parse abi type with name: $name'); 180 | } 181 | -------------------------------------------------------------------------------- /lib/src/contracts/abi/integers.dart: -------------------------------------------------------------------------------- 1 | part of '../../../web3dart.dart'; 2 | 3 | const int _ethereumAddressByteLength = 20; 4 | 5 | abstract class _IntTypeBase extends AbiType { 6 | const _IntTypeBase(this.length) 7 | : assert(length % 8 == 0), 8 | assert(0 < length && length <= 256); 9 | 10 | /// The length of this uint, int bits. Must be a multiple of 8. 11 | final int length; 12 | 13 | @override 14 | EncodingLengthInfo get encodingLength => 15 | const EncodingLengthInfo(sizeUnitBytes); 16 | 17 | String get _namePrefix; 18 | @override 19 | String get name => _namePrefix + length.toString(); 20 | 21 | void validate() { 22 | if (length % 8 != 0 || length < 0 || length > 256) { 23 | throw Exception('Invalid length for int type: was $length'); 24 | } 25 | } 26 | 27 | @override 28 | DecodingResult decode(ByteBuffer buffer, int offset) { 29 | // we're always going to read a 32-byte block for integers 30 | return DecodingResult( 31 | _decode32Bytes(buffer.asUint8List(offset, sizeUnitBytes)), 32 | sizeUnitBytes, 33 | ); 34 | } 35 | 36 | BigInt _decode32Bytes(Uint8List data); 37 | 38 | @override 39 | String toString() { 40 | return '$runtimeType(length = $length)'; 41 | } 42 | } 43 | 44 | /// The solidity uint<M> type that encodes unsigned integers. 45 | class UintType extends _IntTypeBase { 46 | /// Constructor. 47 | const UintType({int length = 256}) : super(length); 48 | 49 | @override 50 | String get _namePrefix => 'uint'; 51 | 52 | @override 53 | void encode(BigInt data, LengthTrackingByteSink buffer) { 54 | assert(data < BigInt.one << length); 55 | assert(!data.isNegative); 56 | 57 | final bytes = unsignedIntToBytes(data); 58 | final padLen = calculatePadLength(bytes.length); 59 | buffer 60 | ..add(Uint8List(padLen)) // will be filled with 0 61 | ..add(bytes); 62 | } 63 | 64 | /// Encode Replace. 65 | void encodeReplace( 66 | int startIndex, 67 | BigInt data, 68 | LengthTrackingByteSink buffer, 69 | ) { 70 | final bytes = unsignedIntToBytes(data); 71 | final padLen = calculatePadLength(bytes.length); 72 | 73 | buffer 74 | ..setRange(startIndex, startIndex + padLen, Uint8List(padLen)) 75 | ..setRange(startIndex + padLen, startIndex + sizeUnitBytes, bytes); 76 | } 77 | 78 | @override 79 | BigInt _decode32Bytes(Uint8List data) { 80 | // The padded zeroes won't make a difference when parsing so we can ignore 81 | // them. 82 | return bytesToUnsignedInt(data); 83 | } 84 | 85 | @override 86 | int get hashCode => 31 * length; 87 | 88 | @override 89 | bool operator ==(other) { 90 | return identical(this, other) || 91 | (other is UintType && other.length == length); 92 | } 93 | } 94 | 95 | /// Solidity address type 96 | class AddressType extends AbiType { 97 | /// Constructor. 98 | const AddressType(); 99 | 100 | static const _paddingLen = sizeUnitBytes - _ethereumAddressByteLength; 101 | 102 | @override 103 | EncodingLengthInfo get encodingLength => 104 | const EncodingLengthInfo(sizeUnitBytes); 105 | 106 | @override 107 | String get name => 'address'; 108 | 109 | @override 110 | void encode(EthereumAddress data, LengthTrackingByteSink buffer) { 111 | buffer 112 | ..add(Uint8List(_paddingLen)) 113 | ..add(data.value); 114 | } 115 | 116 | @override 117 | DecodingResult decode(ByteBuffer buffer, int offset) { 118 | final addressBytes = buffer.asUint8List( 119 | offset + _paddingLen, 120 | _ethereumAddressByteLength, 121 | ); 122 | return DecodingResult(EthereumAddress(addressBytes), sizeUnitBytes); 123 | } 124 | 125 | @override 126 | int get hashCode => runtimeType.hashCode; 127 | 128 | @override 129 | bool operator ==(other) { 130 | return other.runtimeType == AddressType; 131 | } 132 | } 133 | 134 | /// Solidity bool type 135 | class BoolType extends AbiType { 136 | /// Constructor. 137 | const BoolType(); 138 | static final Uint8List _false = Uint8List(sizeUnitBytes); 139 | static final Uint8List _true = Uint8List(sizeUnitBytes) 140 | ..[sizeUnitBytes - 1] = 1; 141 | 142 | @override 143 | EncodingLengthInfo get encodingLength => 144 | const EncodingLengthInfo(sizeUnitBytes); 145 | 146 | @override 147 | String get name => 'bool'; 148 | 149 | @override 150 | void encode(bool data, LengthTrackingByteSink buffer) { 151 | buffer.add(data ? _true : _false); 152 | } 153 | 154 | @override 155 | DecodingResult decode(ByteBuffer buffer, int offset) { 156 | final decoded = buffer.asUint8List(offset, sizeUnitBytes); 157 | final value = (decoded[sizeUnitBytes - 1] & 1) == 1; 158 | 159 | return DecodingResult(value, sizeUnitBytes); 160 | } 161 | 162 | @override 163 | int get hashCode => runtimeType.hashCode; 164 | 165 | @override 166 | bool operator ==(Object other) { 167 | return other.runtimeType == BoolType; 168 | } 169 | } 170 | 171 | /// The solidity int<M> types that encodes twos-complement integers. 172 | class IntType extends _IntTypeBase { 173 | /// Constructor. 174 | const IntType({int length = 256}) : super(length); 175 | @override 176 | String get _namePrefix => 'int'; 177 | 178 | @override 179 | void encode(BigInt data, LengthTrackingByteSink buffer) { 180 | final negative = data.isNegative; 181 | Uint8List bytesData; 182 | 183 | if (negative) { 184 | // twos complement 185 | bytesData = unsignedIntToBytes((BigInt.one << length) + data); 186 | } else { 187 | bytesData = unsignedIntToBytes(data); 188 | } 189 | 190 | final padLen = calculatePadLength(bytesData.length); 191 | 192 | // signed expansion: use 0b11111111 when negative, 0 otherwise 193 | if (negative) { 194 | buffer.add(List.filled(padLen, 0xFF)); 195 | } else { 196 | buffer.add(Uint8List(padLen)); // will be filled with zeroes 197 | } 198 | 199 | buffer.add(bytesData); 200 | } 201 | 202 | @override 203 | BigInt _decode32Bytes(Uint8List data) { 204 | return bytesToInt(data); 205 | } 206 | 207 | @override 208 | int get hashCode => 37 * length; 209 | 210 | @override 211 | bool operator ==(other) { 212 | return identical(this, other) || 213 | (other is IntType && other.length == length); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.1 2 | 3 | * Improve package score. 4 | 5 | ## 3.0.0 6 | 7 | * Refactor and cleanup deprecated methods/classes. 8 | 9 | ## 2.7.3 10 | 11 | * Address parsing regex bug fixed. 12 | 13 | ## 2.7.2 14 | 15 | * Minor improvements. 16 | 17 | ## 2.7.1 18 | 19 | * Nullable topics. 20 | 21 | ## 2.7.0 22 | 23 | * Use http package version ^1.1.0. 24 | 25 | ## 2.6.1 26 | 27 | * EIP-1559. 28 | 29 | ## 2.5.3 30 | 31 | * Ditch dependency on collection. 32 | 33 | ## 2.5.2 34 | 35 | * Fix type-error to get useful RpcError. 36 | 37 | ## 2.5.1 38 | 39 | * Remove build.yaml that was pointing to obsolete builder. 40 | 41 | ## 2.5.0 42 | 43 | * Deprecate async methods. 44 | 45 | ## 2.4.1 46 | 47 | * Update dependencies. 48 | 49 | ## 2.4.0 50 | 51 | * Refactor parts of the project into [ERC20](https://pub.dev/packages/erc20) and [Web3_Browser](https://pub.dev/packages/web3_browser) 52 | 53 | ## 2.3.5 54 | 55 | * Ensuring quality and performance. 56 | 57 | ## 2.3.4 58 | 59 | * Adds `name`, `symbol` and `decimals` functions to ERC20. 60 | 61 | ## 2.3.3 62 | 63 | * Fix signing legacy transactions without gas and without a client. 64 | 65 | ## 2.3.2 66 | 67 | * Support EIP-1559 transactions. 68 | 69 | ## 2.3.1 70 | 71 | * Fix the `Web3Client.custom` constructor not setting all required fields. 72 | 73 | ## 2.3.0 74 | 75 | * Support overloaded methods for generated contracts 76 | 77 | ## 2.2.0 78 | 79 | * Add `EthPrivateKey.publicKey` getters 80 | * Fix `window.ethereum` always being non-null, even if no provider is available 81 | 82 | ## 2.1.4 83 | 84 | * Fix a generator crash for unexpected `devdoc` values 85 | 86 | ## 2.1.3 87 | 88 | * Fix `EthPrivateKey.createRandom` sometimes failing 89 | 90 | ## 2.1.2 91 | 92 | * Fix contract generation for events 93 | * Don't generate a method for the fallback method 94 | * Fix parsing contract abis in the presence of unknown function types 95 | 96 | ## 2.1.1 97 | 98 | * Respect the `value` parameter in `estimateGas` 99 | 100 | ## 2.1.0 101 | 102 | * Add `package:web3dart/browser.dart`, a library for using this package in 103 | Ethereum-enabled browsers. 104 | * Add code generator for smart contracts. To use it, just put the generated abi 105 | json into a `.abi.json` file, add a dev-dependency on `build_runner` and run 106 | `(flutter | dart) pub run build_runner build`. 107 | * Add the `package:web3dart/contracts/erc20.dart` library for interacting with an 108 | [ERC-20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) smart contract. 109 | 110 | ## 2.0.0 111 | 112 | * __Breaking__: Renamed `TransactionReceipt.fromJson` to `TransactionReceipt.fromMap` 113 | * __Breaking__: Removed the `backgroundIsolate` option from `Web3Client`. 114 | For background isolates, instead use `runner: await IsolateRunner.spawn()` from `package:isolate`. 115 | * __Breaking__: Changed `TransactionInformation.r` and `TransactionInformation.s` from `Uint8List` to 116 | `BigInt` 117 | * __Breaking__: When not setting the `maxGas` argument, this library will now estimate it instead of using 118 | a fixed upper bound. 119 | * Migrate to null safety 120 | * Add `ecRecover` and `isValidSignature` to verify messages. Thanks, [brickpop](https://github.com/brickpop)! 121 | * Add `compressPublicKey` and `decompressPublicKey` to obtain a compressed or expanded version of keys. 122 | * Add `getLogs` method to `Web3Client`. Thanks, [jmank88](https://github.com/jmank88)! 123 | * Add `sendRawTransaction` to send a raw, signed transaction. 124 | * Fix `hexToDartInt` not actually parsing hex ([#81](https://github.com/xclud/web3dart/issues/81)) 125 | * Support for background isolates is temporarily disabled until `package:isolate` migrates to null safety 126 | 127 | ## 1.2.3 128 | 129 | * include a `0x` for hex data in `eth_estimateGas` * thanks, [@Botary](https://github.com/Botary) 130 | 131 | ## 1.2.2 132 | 133 | * Fixed a bug when decoding negative integers ([#73](https://github.com/xclud/web3dart/issues/73)) 134 | 135 | ## 1.2.0 136 | 137 | * Added `estimateGas` method on `Web3Client` to estimate the amount of gas that 138 | would be used by a transaction. 139 | 140 | In 1.2.1, the `atBlock` parameter on `estimateGas` was deprecated and will be ignored. 141 | 142 | ## 1.1.1, 1.1.1+1 143 | 144 | * Fix parsing transaction receipts when the block number is not yet available. 145 | Thanks to [@chart21](https://github.com/chart21) for the fix. 146 | * Fix a typo that made it impossible to load the coinbase address. Thanks to 147 | [@modulovalue](https://github.com/modulovalue) for the fix. 148 | 149 | ## 1.1.0 150 | 151 | * Added `getTransactionReceipt` to get more detailed information about a 152 | transaction, including whether it was executed successfully or not. 153 | 154 | ## 1.0.0 155 | 156 | Basically a complete rewrite of the library * countless bug fixes, a more fluent 157 | and consistent api and more features: 158 | 159 | * experimental api to perform expensive operations in a background isolate. Set 160 | `enableBackgroundIsolate` to true when creating a `Web3Client` to try it out. 161 | * Events! Use `addedBlocks`, `pendingTransactions` and `events` for auto-updating 162 | streams. 163 | * The client now has a `dispose()` method which should be called to stop the 164 | background isolate and terminate all running streams. 165 | 166 | This version contains breaking changes! Here is an overview listing some of them. 167 | 168 | | Before | Updated API | 169 | | :------------* | -----:| 170 | | Creating credentials via `Credentials.fromPrivateKeyHex` | Use the `EthPrivateKey` class or, even better, `client.credentialsFromPrivateKey` | 171 | | Sending transactions or calling contract functions | The api has been changed to just a single methods instead of a transaction builder. See the examples for details. | 172 | | Low-level cryptographic operations like signing, hashing and converting hex <-> byte array <-> integer | Not available in the core library. Import `package:web3dart/crypto.dart` instead | 173 | 174 | If you run into problems after updating, please [create an issue](https://github.com/xclud/web3dart/issues/new). 175 | 176 | ## 0.4.4 177 | 178 | * Added `getTransactionByHash` method * thank you, [maxholman](https://github.com/maxholman)! 179 | * Allow a different N parameter for scrypt when creating new wallets. 180 | 181 | ## 0.4.0 182 | 183 | * New APIs allowing for a simpler access to wallets, credentials and addresses 184 | * More examples in the README 185 | 186 | ## 0.2.1 187 | 188 | * More solidity types, not with encoding. 189 | 190 | ## 0.2 191 | 192 | * Send transactions and call messages from smart contracts on the 193 | Blockchain. 194 | 195 | ## 0.1 196 | 197 | * Create new Ethereum accounts 198 | 199 | ## 0.0.2 200 | 201 | * Send and sign transactions 202 | 203 | ## 0.0.1 204 | 205 | * Initial version, created by Stagehand 206 | -------------------------------------------------------------------------------- /lib/src/crypto/secp256k1.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | final ECDomainParameters params = ECCurve_secp256k1(); 4 | final BigInt _halfCurveOrder = params.n >> 1; 5 | 6 | /// Generates a public key for the given private key using the ecdsa curve which 7 | /// Ethereum uses. 8 | Uint8List privateKeyBytesToPublic(Uint8List privateKey) { 9 | return privateKeyToPublic(bytesToUnsignedInt(privateKey)); 10 | } 11 | 12 | /// Generates a public key for the given private key using the ecdsa curve which 13 | /// Ethereum uses. 14 | Uint8List privateKeyToPublic(BigInt privateKey) { 15 | final p = (params.G * privateKey)!; 16 | 17 | //skip the type flag, https://github.com/ethereumjs/ethereumjs-util/blob/master/index.js#L319 18 | return Uint8List.view(p.getEncoded(false).buffer, 1); 19 | } 20 | 21 | /// Generates a new private key using the random instance provided. Please make 22 | /// sure you're using a cryptographically secure generator. 23 | BigInt generateNewPrivateKey(Random random) { 24 | final generator = ECKeyGenerator(); 25 | 26 | final keyParams = ECKeyGeneratorParameters(params); 27 | 28 | generator.init(ParametersWithRandom(keyParams, RandomBridge(random))); 29 | 30 | final key = generator.generateKeyPair(); 31 | final privateKey = key.privateKey; 32 | return privateKey.d!; 33 | } 34 | 35 | /// Constructs the Ethereum address associated with the given public key by 36 | /// taking the lower 160 bits of the key's sha3 hash. 37 | Uint8List publicKeyToAddress(Uint8List publicKey) { 38 | assert(publicKey.length == 64); 39 | final hashed = keccak256(publicKey); 40 | assert(hashed.length == 32); 41 | return hashed.sublist(12, 32); 42 | } 43 | 44 | /// Signatures used to sign Ethereum transactions and messages. 45 | class MsgSignature { 46 | MsgSignature(this.r, this.s, this.v); 47 | final BigInt r; 48 | final BigInt s; 49 | final int v; 50 | } 51 | 52 | /// Signs the hashed data in [messageHash] using the given private key. 53 | MsgSignature sign(Uint8List messageHash, Uint8List privateKey) { 54 | final digest = SHA256Digest(); 55 | final signer = ECDSASigner(null, HMac(digest, 64)); 56 | final key = ECPrivateKey(bytesToUnsignedInt(privateKey), params); 57 | 58 | signer.init(true, PrivateKeyParameter(key)); 59 | var sig = signer.generateSignature(messageHash) as ECSignature; 60 | 61 | /* 62 | This is necessary because if a message can be signed by (r, s), it can also 63 | be signed by (r, -s (mod N)) which N being the order of the elliptic function 64 | used. In order to ensure transactions can't be tampered with (even though it 65 | would be harmless), Ethereum only accepts the signature with the lower value 66 | of s to make the signature for the message unique. 67 | More details at 68 | https://github.com/web3j/web3j/blob/master/crypto/src/main/java/org/web3j/crypto/ECDSASignature.java#L27 69 | */ 70 | if (sig.s.compareTo(_halfCurveOrder) > 0) { 71 | final canonicalisedS = params.n - sig.s; 72 | sig = ECSignature(sig.r, canonicalisedS); 73 | } 74 | 75 | final publicKey = bytesToUnsignedInt(privateKeyBytesToPublic(privateKey)); 76 | 77 | final recId = EC.secp256k1.calculateRecoveryId(publicKey, sig, messageHash); 78 | 79 | if (recId == null) { 80 | throw Exception( 81 | 'Could not construct a recoverable key. This should never happen', 82 | ); 83 | } 84 | 85 | return MsgSignature(sig.r, sig.s, recId + 27); 86 | } 87 | 88 | /// Given an arbitrary message hash and an Ethereum message signature encoded in bytes, returns 89 | /// the public key that was used to sign it. 90 | /// https://github.com/web3j/web3j/blob/c0b7b9c2769a466215d416696021aa75127c2ff1/crypto/src/main/java/org/web3j/crypto/Sign.java#L241 91 | Uint8List ecRecover(Uint8List messageHash, MsgSignature signatureData) { 92 | final r = padUint8ListTo32(unsignedIntToBytes(signatureData.r)); 93 | final s = padUint8ListTo32(unsignedIntToBytes(signatureData.s)); 94 | assert(r.length == 32); 95 | assert(s.length == 32); 96 | 97 | final header = signatureData.v & 0xFF; 98 | // The header byte: 0x1B = first key with even y, 0x1C = first key with odd y, 99 | // 0x1D = second key with even y, 0x1E = second key with odd y 100 | if (header < 27 || header > 34) { 101 | throw Exception('Header byte out of range: $header'); 102 | } 103 | 104 | final sig = ECSignature(signatureData.r, signatureData.s); 105 | 106 | final recId = header - 27; 107 | final pubKey = _recoverFromSignature(recId, sig, messageHash, params); 108 | if (pubKey == null) { 109 | throw Exception('Could not recover public key from signature'); 110 | } 111 | return unsignedIntToBytes(pubKey); 112 | } 113 | 114 | /// Given an arbitrary message hash, an Ethereum message signature encoded in bytes and 115 | /// a public key encoded in bytes, confirms whether that public key was used to sign 116 | /// the message or not. 117 | bool isValidSignature( 118 | Uint8List messageHash, 119 | MsgSignature signatureData, 120 | Uint8List publicKey, 121 | ) { 122 | final recoveredPublicKey = ecRecover(messageHash, signatureData); 123 | return bytesToHex(publicKey) == bytesToHex(recoveredPublicKey); 124 | } 125 | 126 | /// Given a byte array computes its compressed version and returns it as a byte array, 127 | /// including the leading 02 or 03 128 | Uint8List compressPublicKey(Uint8List compressedPubKey) { 129 | return Uint8List.view( 130 | params.curve.decodePoint(compressedPubKey)!.getEncoded(true).buffer, 131 | ); 132 | } 133 | 134 | /// Given a byte array computes its expanded version and returns it as a byte array, 135 | /// including the leading 04 136 | Uint8List decompressPublicKey(Uint8List compressedPubKey) { 137 | return Uint8List.view( 138 | params.curve.decodePoint(compressedPubKey)!.getEncoded(false).buffer, 139 | ); 140 | } 141 | 142 | BigInt? _recoverFromSignature( 143 | int recId, 144 | ECSignature sig, 145 | Uint8List msg, 146 | ECDomainParameters params, 147 | ) { 148 | final n = params.n; 149 | final i = BigInt.from(recId ~/ 2); 150 | final x = sig.r + (i * n); 151 | 152 | //Parameter q of curve 153 | final prime = BigInt.parse( 154 | 'fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f', 155 | radix: 16, 156 | ); 157 | if (x.compareTo(prime) >= 0) return null; 158 | 159 | final R = _decompressKey(x, (recId & 1) == 1, params.curve); 160 | if (!(R * n)!.isInfinity) return null; 161 | 162 | final e = bytesToUnsignedInt(msg); 163 | 164 | final eInv = (BigInt.zero - e) % n; 165 | final rInv = sig.r.modInverse(n); 166 | final srInv = (rInv * sig.s) % n; 167 | final eInvrInv = (rInv * eInv) % n; 168 | 169 | final q = (params.G * eInvrInv)! + (R * srInv); 170 | 171 | final bytes = q!.getEncoded(false); 172 | return bytesToUnsignedInt(bytes.sublist(1)); 173 | } 174 | 175 | ECPoint _decompressKey(BigInt xBN, bool yBit, ECCurve c) { 176 | List x9IntegerToBytes(BigInt s, int qLength) { 177 | //https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/asn1/x9/X9IntegerConverter.java#L45 178 | final bytes = intToBytes(s); 179 | 180 | if (qLength < bytes.length) { 181 | return bytes.sublist(0, bytes.length - qLength); 182 | } else if (qLength > bytes.length) { 183 | final tmp = List.filled(qLength, 0); 184 | 185 | final offset = qLength - bytes.length; 186 | for (var i = 0; i < bytes.length; i++) { 187 | tmp[i + offset] = bytes[i]; 188 | } 189 | 190 | return tmp; 191 | } 192 | 193 | return bytes; 194 | } 195 | 196 | final compEnc = x9IntegerToBytes(xBN, 1 + ((c.fieldSize + 7) ~/ 8)); 197 | compEnc[0] = yBit ? 0x03 : 0x02; 198 | return c.decodePoint(compEnc)!; 199 | } 200 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pub package](https://img.shields.io/pub/v/web3dart)](https://pub.dartlang.org/packages/web3dart) 2 | [![likes](https://img.shields.io/pub/likes/web3dart)](https://pub.dartlang.org/packages/web3dart/score) 3 | [![points](https://img.shields.io/pub/points/web3dart)](https://pub.dartlang.org/packages/web3dart/score) 4 | [![popularity](https://img.shields.io/pub/popularity/web3dart)](https://pub.dartlang.org/packages/web3dart/score) 5 | [![license](https://img.shields.io/github/license/xclud/web3dart)](https://pub.dartlang.org/packages/web3dart) 6 | [![stars](https://img.shields.io/github/stars/simolus3/web3dart)](https://github.com/xclud/web3dart/stargazers) 7 | [![forks](https://img.shields.io/github/forks/simolus3/web3dart)](https://github.com/xclud/web3dart/network/members) 8 | [![sdk version](https://badgen.net/pub/sdk-version/web3dart)](https://pub.dartlang.org/packages/web3dart) 9 | 10 | A dart library that connects to interact with the Ethereum blockchain. It connects 11 | to an Ethereum node to send transactions, interact with smart contracts and much 12 | more! 13 | 14 | ## Features 15 | 16 | - Connect to an Ethereum node with the rpc-api, call common methods 17 | - Send signed Ethereum transactions 18 | - Generate private keys, setup new Ethereum addresses 19 | - Call functions on smart contracts and listen for contract events 20 | - Code generation based on smart contract ABI for easier interaction 21 | 22 | ### TODO 23 | 24 | - Encode all supported solidity types, although only (u)fixed, 25 | which are not commonly used, are not supported at the moment. 26 | 27 | ## Usage 28 | 29 | ### Credentials and Wallets 30 | 31 | In order to send transactions on the Ethereum network, some credentials 32 | are required. The library supports raw private keys and v3 wallet files. 33 | 34 | ```dart 35 | import 'dart:math'; //used for the random number generator 36 | 37 | import 'package:web3dart/web3dart.dart'; 38 | // You can create Credentials from private keys 39 | Credentials fromHex = EthPrivateKey.fromHex("c87509a[...]dc0d3"); 40 | 41 | // Or generate a new key randomly 42 | var rng = Random.secure(); 43 | Credentials random = EthPrivateKey.createRandom(rng); 44 | 45 | // In either way, the library can derive the public key and the address 46 | // from a private key: 47 | var address = credentials.address; 48 | print(address.hex); 49 | ``` 50 | 51 | Another way to obtain `Credentials` which the library uses to sign 52 | transactions is the usage of a wallet file. Wallets store a private 53 | key securely and require a password to unlock. The library has experimental 54 | support for version 3 wallets commonly generated by other Ethereum clients: 55 | 56 | ```dart 57 | import 'dart:io'; 58 | import 'package:web3dart/web3dart.dart'; 59 | 60 | String content = File("wallet.json").readAsStringSync(); 61 | Wallet wallet = Wallet.fromJson(content, "testpassword"); 62 | 63 | Credentials unlocked = wallet.privateKey; 64 | // You can now use these credentials to sign transactions or messages 65 | ``` 66 | 67 | You can also create Wallet files with this library. To do so, you first need 68 | the private key you want to encrypt and a desired password. Then, create 69 | your wallet with 70 | 71 | ```dart 72 | Wallet wallet = Wallet.createNew(credentials, "password", random); 73 | print(wallet.toJson()); 74 | ``` 75 | 76 | You can also write `wallet.toJson()` into a file which you can later open 77 | with [MyEtherWallet](https://www.myetherwallet.com/#view-wallet-info) 78 | (select Keystore / JSON File) or other Ethereum clients like geth. 79 | 80 | #### Custom credentials 81 | 82 | If you want to integrate `web3dart` with other wallet providers, you can implement 83 | `Credentials` and override the appropriate methods. 84 | 85 | ### Connecting to an RPC server 86 | 87 | The library won't send signed transactions to miners itself. Instead, 88 | it relies on an RPC client to do that. You can use a public RPC API like 89 | [infura](https://infura.io/), setup your own using [geth](https://github.com/ethereum/go-ethereum/wiki/geth) 90 | or, if you just want to test things out, use a private testnet with 91 | [truffle](https://www.trufflesuite.com/) and [ganache](https://www.trufflesuite.com/ganache). All these options will give you 92 | an RPC endpoint to which the library can connect. 93 | 94 | ```dart 95 | import 'package:http/http.dart'; //You can also import the browser version 96 | import 'package:web3dart/web3dart.dart'; 97 | 98 | var apiUrl = "http://localhost:7545"; //Replace with your API 99 | 100 | var httpClient = Client(); 101 | var ethClient = Web3Client(apiUrl, httpClient); 102 | 103 | var credentials = EthPrivateKey.fromHex("0x..."); 104 | var address = credentials.address; 105 | 106 | // You can now call rpc methods. This one will query the amount of Ether you own 107 | EtherAmount balance = ethClient.getBalance(address); 108 | print(balance.getValueInUnit(EtherUnit.ether)); 109 | ``` 110 | 111 | ## Sending transactions 112 | 113 | Of course, this library supports creating, signing and sending Ethereum 114 | transactions: 115 | 116 | ```dart 117 | import 'package:web3dart/web3dart.dart'; 118 | 119 | /// [...], you need to specify the url and your client, see example above 120 | var ethClient = Web3Client(apiUrl, httpClient); 121 | 122 | var credentials = EthPrivateKey.fromHex("0x..."); 123 | 124 | await client.sendTransaction( 125 | credentials, 126 | Transaction( 127 | to: EthereumAddress.fromHex('0xC91...3706'), 128 | gasPrice: EtherAmount.inWei(BigInt.one), 129 | maxGas: 100000, 130 | value: EtherAmount.fromUnitAndValue(EtherUnit.ether, 1), 131 | ), 132 | ); 133 | ``` 134 | 135 | Missing data, like the gas price, the sender and a transaction nonce will be 136 | obtained from the connected node when not explicitly specified. If you only need 137 | the signed transaction but don't intend to send it, you can use 138 | `client.signTransaction`. 139 | 140 | ### Smart contracts 141 | 142 | The library can parse the abi of a smart contract and send data to it. It can also 143 | listen for events emitted by smart contracts. See [this file](https://github.com/xclud/web3dart/blob/development/example/contracts.dart) 144 | for an example. 145 | 146 | ### Dart Code Generator 147 | 148 | By using [Dart's build system](https://github.com/dart-lang/build/), web3dart can 149 | generate Dart code to easily access smart contracts. 150 | 151 | Install web3dart_builders package 152 | 153 | ```dart 154 | pub add web3dart_builders --dev 155 | ``` 156 | 157 | To use this feature, put a contract abi json somewhere into `lib/`. 158 | The filename has to end with `.abi.json`. 159 | Then, add a `dev_dependency` on the `build_runner` package and run 160 | 161 | ```dart 162 | pub run build_runner build 163 | ``` 164 | 165 | You'll now find a `.g.dart` file containing code to interact with the contract. 166 | 167 | #### Optional: Ignore naming suggestions for generated files 168 | 169 | If importing contract ABIs with function names that don't follow dart's naming conventions, the dart analyzer will (by default) be unhappy about it, and show warnings. 170 | This can be mitigated by excluding all the generated files from being analyzed. 171 | Note that this has the side effect of suppressing serious errors as well, should there exist any. (There shouldn't as these files are automatically generated). 172 | 173 | Create a file named `analysis_options.yaml` in the root directory of your project: 174 | 175 | ```dart 176 | analyzer: 177 | exclude: 178 | - '**/*.g.dart' 179 | ``` 180 | 181 | See [Customizing static analysis](https://dart.dev/guides/language/analysis-options) for advanced options. 182 | 183 | ## Feature requests and bugs 184 | 185 | Please file feature requests and bugs at the [issue tracker][tracker]. 186 | If you want to contribute to this library, please submit a Pull Request. 187 | 188 | [tracker]: https://github.com/xclud/web3dart/issues/new 189 | 190 | ## Testing 191 | 192 | ```dart 193 | dart test test 194 | ``` -------------------------------------------------------------------------------- /lib/src/core/transaction_information.dart: -------------------------------------------------------------------------------- 1 | part of 'package:web3dart/web3dart.dart'; 2 | 3 | class TransactionInformation { 4 | TransactionInformation.fromMap(Map map) 5 | : blockHash = map['blockHash'] as String?, 6 | blockNumber = map['blockNumber'] != null 7 | ? BlockNum.exact(int.parse(map['blockNumber'] as String)) 8 | : const BlockNum.pending(), 9 | from = EthereumAddress.fromHex(map['from'] as String), 10 | gas = int.parse(map['gas'] as String), 11 | gasPrice = EtherAmount.inWei(BigInt.parse(map['gasPrice'] as String)), 12 | hash = map['hash'] as String, 13 | input = hexToBytes(map['input'] as String), 14 | nonce = int.parse(map['nonce'] as String), 15 | to = map['to'] != null 16 | ? EthereumAddress.fromHex(map['to'] as String) 17 | : null, 18 | transactionIndex = map['transactionIndex'] != null 19 | ? int.parse(map['transactionIndex'] as String) 20 | : null, 21 | value = EtherAmount.inWei(BigInt.parse(map['value'] as String)), 22 | v = int.parse(map['v'] as String), 23 | r = hexToInt(map['r'] as String), 24 | s = hexToInt(map['s'] as String); 25 | 26 | /// The hash of the block containing this transaction. If this transaction has 27 | /// not been mined yet and is thus in no block, it will be `null` 28 | final String? blockHash; 29 | 30 | /// [BlockNum] of the block containing this transaction, or [BlockNum.pending] 31 | /// when the transaction is not part of any block yet. 32 | final BlockNum blockNumber; 33 | 34 | /// The sender of this transaction. 35 | final EthereumAddress from; 36 | 37 | /// How many units of gas have been used in this transaction. 38 | final int gas; 39 | 40 | /// The amount of Ether that was used to pay for one unit of gas. 41 | final EtherAmount gasPrice; 42 | 43 | /// A hash of this transaction, in hexadecimal representation. 44 | final String hash; 45 | 46 | /// The data sent with this transaction. 47 | final Uint8List input; 48 | 49 | /// The nonce of this transaction. A nonce is incremented per sender and 50 | /// transaction to make sure the same transaction can't be sent more than 51 | /// once. 52 | final int nonce; 53 | 54 | /// Address of the receiver. `null` when its a contract creation transaction 55 | final EthereumAddress? to; 56 | 57 | /// Integer of the transaction's index position in the block. `null` when it's 58 | /// pending. 59 | int? transactionIndex; 60 | 61 | /// The amount of Ether sent with this transaction. 62 | final EtherAmount value; 63 | 64 | /// A cryptographic recovery id which can be used to verify the authenticity 65 | /// of this transaction together with the signature [r] and [s] 66 | final int v; 67 | 68 | /// ECDSA signature r 69 | final BigInt r; 70 | 71 | /// ECDSA signature s 72 | final BigInt s; 73 | 74 | /// The ECDSA full signature used to sign this transaction. 75 | MsgSignature get signature => MsgSignature(r, s, v); 76 | } 77 | 78 | class TransactionReceipt { 79 | TransactionReceipt({ 80 | required this.transactionHash, 81 | required this.transactionIndex, 82 | required this.blockHash, 83 | required this.cumulativeGasUsed, 84 | this.blockNumber = const BlockNum.pending(), 85 | this.contractAddress, 86 | this.status, 87 | this.from, 88 | this.to, 89 | this.gasUsed, 90 | this.effectiveGasPrice, 91 | this.logs = const [], 92 | }); 93 | 94 | TransactionReceipt.fromMap(Map map) 95 | : transactionHash = hexToBytes(map['transactionHash'] as String), 96 | transactionIndex = hexToDartInt(map['transactionIndex'] as String), 97 | blockHash = hexToBytes(map['blockHash'] as String), 98 | blockNumber = map['blockNumber'] != null 99 | ? BlockNum.exact(int.parse(map['blockNumber'] as String)) 100 | : const BlockNum.pending(), 101 | from = map['from'] != null 102 | ? EthereumAddress.fromHex(map['from'] as String) 103 | : null, 104 | to = map['to'] != null 105 | ? EthereumAddress.fromHex(map['to'] as String) 106 | : null, 107 | cumulativeGasUsed = hexToInt(map['cumulativeGasUsed'] as String), 108 | gasUsed = 109 | map['gasUsed'] != null ? hexToInt(map['gasUsed'] as String) : null, 110 | effectiveGasPrice = map['effectiveGasPrice'] != null 111 | ? EtherAmount.inWei( 112 | BigInt.parse(map['effectiveGasPrice'] as String), 113 | ) 114 | : null, 115 | contractAddress = map['contractAddress'] != null 116 | ? EthereumAddress.fromHex(map['contractAddress'] as String) 117 | : null, 118 | status = map['status'] != null 119 | ? (hexToDartInt(map['status'] as String) == 1) 120 | : null, 121 | logs = map['logs'] != null 122 | ? (map['logs'] as List) 123 | .map( 124 | (dynamic log) => 125 | FilterEvent.fromMap(log as Map), 126 | ) 127 | .toList() 128 | : []; 129 | 130 | /// Hash of the transaction (32 bytes). 131 | final Uint8List transactionHash; 132 | 133 | /// Index of the transaction's position in the block. 134 | final int transactionIndex; 135 | 136 | /// Hash of the block where this transaction is in (32 bytes). 137 | final Uint8List blockHash; 138 | 139 | /// Block number where this transaction is in. 140 | final BlockNum blockNumber; 141 | 142 | /// Address of the sender. 143 | final EthereumAddress? from; 144 | 145 | /// Address of the receiver or `null` if it was a contract creation 146 | /// transaction. 147 | final EthereumAddress? to; 148 | 149 | /// The total amount of gas used when this transaction was executed in the 150 | /// block. 151 | final BigInt cumulativeGasUsed; 152 | 153 | /// The amount of gas used by this specific transaction alone. 154 | final BigInt? gasUsed; 155 | 156 | /// The address of the contract created if the transaction was a contract 157 | /// creation. `null` otherwise. 158 | final EthereumAddress? contractAddress; 159 | 160 | /// Whether this transaction was executed successfully. 161 | final bool? status; 162 | 163 | /// Array of logs generated by this transaction. 164 | final List logs; 165 | 166 | final EtherAmount? effectiveGasPrice; 167 | 168 | @override 169 | String toString() { 170 | return 'TransactionReceipt{transactionHash: ${bytesToHex(transactionHash)}, ' 171 | 'transactionIndex: $transactionIndex, blockHash: ${bytesToHex(blockHash)}, ' 172 | 'blockNumber: $blockNumber, from: ${from?.with0x}, to: ${to?.with0x}, ' 173 | 'cumulativeGasUsed: $cumulativeGasUsed, gasUsed: $gasUsed, ' 174 | 'contractAddress: ${contractAddress?.with0x}, status: $status, ' 175 | 'effectiveGasPrice: $effectiveGasPrice, logs: $logs}'; 176 | } 177 | 178 | @override 179 | bool operator ==(Object other) => 180 | identical(this, other) || 181 | other is TransactionReceipt && 182 | runtimeType == other.runtimeType && 183 | eq.equals(transactionHash, other.transactionHash) && 184 | transactionIndex == other.transactionIndex && 185 | eq.equals(blockHash, other.blockHash) && 186 | blockNumber == other.blockNumber && 187 | from == other.from && 188 | to == other.to && 189 | cumulativeGasUsed == other.cumulativeGasUsed && 190 | gasUsed == other.gasUsed && 191 | contractAddress == other.contractAddress && 192 | status == other.status && 193 | effectiveGasPrice == other.effectiveGasPrice && 194 | eq.equals(logs, other.logs); 195 | 196 | @override 197 | int get hashCode => 198 | transactionHash.hashCode ^ 199 | transactionIndex.hashCode ^ 200 | blockHash.hashCode ^ 201 | blockNumber.hashCode ^ 202 | from.hashCode ^ 203 | to.hashCode ^ 204 | cumulativeGasUsed.hashCode ^ 205 | gasUsed.hashCode ^ 206 | contractAddress.hashCode ^ 207 | status.hashCode ^ 208 | effectiveGasPrice.hashCode ^ 209 | logs.hashCode; 210 | } 211 | -------------------------------------------------------------------------------- /test/utils/rlp_test_vectors.dart: -------------------------------------------------------------------------------- 1 | // wrapped in Dart file so that we can load it on the web 2 | const content = r''' 3 | { 4 | "emptystring": { 5 | "in": "", 6 | "out": "0x80" 7 | }, 8 | "bytestring00": { 9 | "in": "\u0000", 10 | "out": "0x00" 11 | }, 12 | "bytestring01": { 13 | "in": "\u0001", 14 | "out": "0x01" 15 | }, 16 | "bytestring7F": { 17 | "in": "\u007F", 18 | "out": "0x7f" 19 | }, 20 | "shortstring": { 21 | "in": "dog", 22 | "out": "0x83646f67" 23 | }, 24 | "shortstring2": { 25 | "in": "Lorem ipsum dolor sit amet, consectetur adipisicing eli", 26 | "out": "0xb74c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c69" 27 | }, 28 | "longstring": { 29 | "in": "Lorem ipsum dolor sit amet, consectetur adipisicing elit", 30 | "out": "0xb8384c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e7365637465747572206164697069736963696e6720656c6974" 31 | }, 32 | "longstring2": { 33 | "in": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur mauris magna, suscipit sed vehicula non, iaculis faucibus tortor. Proin suscipit ultricies malesuada. Duis tortor elit, dictum quis tristique eu, ultrices at risus. Morbi a est imperdiet mi ullamcorper aliquet suscipit nec lorem. Aenean quis leo mollis, vulputate elit varius, consequat enim. Nulla ultrices turpis justo, et posuere urna consectetur nec. Proin non convallis metus. Donec tempor ipsum in mauris congue sollicitudin. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Suspendisse convallis sem vel massa faucibus, eget lacinia lacus tempor. Nulla quis ultricies purus. Proin auctor rhoncus nibh condimentum mollis. Aliquam consequat enim at metus luctus, a eleifend purus egestas. Curabitur at nibh metus. Nam bibendum, neque at auctor tristique, lorem libero aliquet arcu, non interdum tellus lectus sit amet eros. Cras rhoncus, metus ac ornare cursus, dolor justo ultrices metus, at ullamcorper volutpat", 34 | "out": "0xb904004c6f72656d20697073756d20646f6c6f722073697420616d65742c20636f6e73656374657475722061646970697363696e6720656c69742e20437572616269747572206d6175726973206d61676e612c20737573636970697420736564207665686963756c61206e6f6e2c20696163756c697320666175636962757320746f72746f722e2050726f696e20737573636970697420756c74726963696573206d616c6573756164612e204475697320746f72746f7220656c69742c2064696374756d2071756973207472697374697175652065752c20756c7472696365732061742072697375732e204d6f72626920612065737420696d70657264696574206d6920756c6c616d636f7270657220616c6971756574207375736369706974206e6563206c6f72656d2e2041656e65616e2071756973206c656f206d6f6c6c69732c2076756c70757461746520656c6974207661726975732c20636f6e73657175617420656e696d2e204e756c6c6120756c74726963657320747572706973206a7573746f2c20657420706f73756572652075726e6120636f6e7365637465747572206e65632e2050726f696e206e6f6e20636f6e76616c6c6973206d657475732e20446f6e65632074656d706f7220697073756d20696e206d617572697320636f6e67756520736f6c6c696369747564696e2e20566573746962756c756d20616e746520697073756d207072696d697320696e206661756369627573206f726369206c756374757320657420756c74726963657320706f737565726520637562696c69612043757261653b2053757370656e646973736520636f6e76616c6c69732073656d2076656c206d617373612066617563696275732c2065676574206c6163696e6961206c616375732074656d706f722e204e756c6c61207175697320756c747269636965732070757275732e2050726f696e20617563746f722072686f6e637573206e69626820636f6e64696d656e74756d206d6f6c6c69732e20416c697175616d20636f6e73657175617420656e696d206174206d65747573206c75637475732c206120656c656966656e6420707572757320656765737461732e20437572616269747572206174206e696268206d657475732e204e616d20626962656e64756d2c206e6571756520617420617563746f72207472697374697175652c206c6f72656d206c696265726f20616c697175657420617263752c206e6f6e20696e74657264756d2074656c6c7573206c65637475732073697420616d65742065726f732e20437261732072686f6e6375732c206d65747573206163206f726e617265206375727375732c20646f6c6f72206a7573746f20756c747269636573206d657475732c20617420756c6c616d636f7270657220766f6c7574706174" 35 | }, 36 | "zero": { 37 | "in": 0, 38 | "out": "0x80" 39 | }, 40 | "smallint": { 41 | "in": 1, 42 | "out": "0x01" 43 | }, 44 | "smallint2": { 45 | "in": 16, 46 | "out": "0x10" 47 | }, 48 | "smallint3": { 49 | "in": 79, 50 | "out": "0x4f" 51 | }, 52 | "smallint4": { 53 | "in": 127, 54 | "out": "0x7f" 55 | }, 56 | "mediumint1": { 57 | "in": 128, 58 | "out": "0x8180" 59 | }, 60 | "mediumint2": { 61 | "in": 1000, 62 | "out": "0x8203e8" 63 | }, 64 | "mediumint3": { 65 | "in": 100000, 66 | "out": "0x830186a0" 67 | }, 68 | "mediumint4": { 69 | "in": "#83729609699884896815286331701780722", 70 | "out": "0x8f102030405060708090a0b0c0d0e0f2" 71 | }, 72 | "mediumint5": { 73 | "in": "#105315505618206987246253880190783558935785933862974822347068935681", 74 | "out": "0x9c0100020003000400050006000700080009000a000b000c000d000e01" 75 | }, 76 | "emptylist": { 77 | "in": [], 78 | "out": "0xc0" 79 | }, 80 | "stringlist": { 81 | "in": [ "dog", "god", "cat" ], 82 | "out": "0xcc83646f6783676f6483636174" 83 | }, 84 | "multilist": { 85 | "in": [ "zw", [ 4 ], 1 ], 86 | "out": "0xc6827a77c10401" 87 | }, 88 | "shortListMax1": { 89 | "in": [ "asdf", "qwer", "zxcv", "asdf","qwer", "zxcv", "asdf", "qwer", "zxcv", "asdf", "qwer"], 90 | "out": "0xf784617364668471776572847a78637684617364668471776572847a78637684617364668471776572847a78637684617364668471776572" 91 | }, 92 | "longList1" : { 93 | "in" : [ 94 | ["asdf","qwer","zxcv"], 95 | ["asdf","qwer","zxcv"], 96 | ["asdf","qwer","zxcv"], 97 | ["asdf","qwer","zxcv"] 98 | ], 99 | "out": "0xf840cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376" 100 | }, 101 | "longList2" : { 102 | "in" : [ 103 | ["asdf","qwer","zxcv"], 104 | ["asdf","qwer","zxcv"], 105 | ["asdf","qwer","zxcv"], 106 | ["asdf","qwer","zxcv"], 107 | ["asdf","qwer","zxcv"], 108 | ["asdf","qwer","zxcv"], 109 | ["asdf","qwer","zxcv"], 110 | ["asdf","qwer","zxcv"], 111 | ["asdf","qwer","zxcv"], 112 | ["asdf","qwer","zxcv"], 113 | ["asdf","qwer","zxcv"], 114 | ["asdf","qwer","zxcv"], 115 | ["asdf","qwer","zxcv"], 116 | ["asdf","qwer","zxcv"], 117 | ["asdf","qwer","zxcv"], 118 | ["asdf","qwer","zxcv"], 119 | ["asdf","qwer","zxcv"], 120 | ["asdf","qwer","zxcv"], 121 | ["asdf","qwer","zxcv"], 122 | ["asdf","qwer","zxcv"], 123 | ["asdf","qwer","zxcv"], 124 | ["asdf","qwer","zxcv"], 125 | ["asdf","qwer","zxcv"], 126 | ["asdf","qwer","zxcv"], 127 | ["asdf","qwer","zxcv"], 128 | ["asdf","qwer","zxcv"], 129 | ["asdf","qwer","zxcv"], 130 | ["asdf","qwer","zxcv"], 131 | ["asdf","qwer","zxcv"], 132 | ["asdf","qwer","zxcv"], 133 | ["asdf","qwer","zxcv"], 134 | ["asdf","qwer","zxcv"] 135 | ], 136 | "out": "0xf90200cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376cf84617364668471776572847a786376" 137 | }, 138 | "listsoflists": { 139 | "in": [ [ [], [] ], [] ], 140 | "out": "0xc4c2c0c0c0" 141 | }, 142 | "listsoflists2": { 143 | "in": [ [], [[]], [ [], [[]] ] ], 144 | "out": "0xc7c0c1c0c3c0c1c0" 145 | }, 146 | "dictTest1" : { 147 | "in" : [ 148 | ["key1", "val1"], 149 | ["key2", "val2"], 150 | ["key3", "val3"], 151 | ["key4", "val4"] 152 | ], 153 | "out" : "0xecca846b6579318476616c31ca846b6579328476616c32ca846b6579338476616c33ca846b6579348476616c34" 154 | }, 155 | "bigint": { 156 | "in": "#115792089237316195423570985008687907853269984665640564039457584007913129639936", 157 | "out": "0xa1010000000000000000000000000000000000000000000000000000000000000000" 158 | } 159 | } 160 | '''; 161 | -------------------------------------------------------------------------------- /lib/src/contracts/abi/arrays.dart: -------------------------------------------------------------------------------- 1 | part of '../../../web3dart.dart'; 2 | 3 | /// The bytes<M> solidity type, which stores up to 32 bytes. 4 | class FixedBytes extends AbiType { 5 | /// Constructor. 6 | const FixedBytes(this.length) : assert(0 <= length && length <= 32); 7 | 8 | /// The amount of bytes to store, between 0 and 32 (both inclusive). 9 | final int length; 10 | 11 | @override 12 | String get name => 'bytes$length'; 13 | 14 | // the encoding length does not depend on this.length, as it will always be 15 | // padded to 32 bytes 16 | @override 17 | EncodingLengthInfo get encodingLength => 18 | const EncodingLengthInfo(sizeUnitBytes); 19 | 20 | /// Validate. 21 | void validate() { 22 | if (length < 0 || length > 32) { 23 | throw Exception('Invalid length for bytes: was $length'); 24 | } 25 | } 26 | 27 | @override 28 | void encode(Uint8List data, LengthTrackingByteSink buffer) { 29 | assert( 30 | data.length == length, 31 | 'Invalid length: Tried to encode ${data.length} bytes, but expected exactly $length', 32 | ); 33 | final paddingBytes = calculatePadLength(length); 34 | 35 | buffer 36 | ..add(data) 37 | ..add(Uint8List(paddingBytes)); 38 | } 39 | 40 | @override 41 | DecodingResult decode(ByteBuffer buffer, int offset) { 42 | return DecodingResult( 43 | buffer.asUint8List(offset, length), 44 | sizeUnitBytes, 45 | ); 46 | } 47 | 48 | @override 49 | int get hashCode => 29 * length; 50 | 51 | @override 52 | bool operator ==(other) { 53 | return identical(this, other) || 54 | (other is FixedBytes && other.length == length); 55 | } 56 | } 57 | 58 | /// Function Type 59 | class FunctionType extends FixedBytes { 60 | /// 20 bytes for address, 4 for function name 61 | const FunctionType() : super(24); 62 | 63 | @override 64 | int get hashCode => runtimeType.hashCode; 65 | 66 | @override 67 | bool operator ==(other) { 68 | return other.runtimeType == FunctionType; 69 | } 70 | } 71 | 72 | /// The solidity bytes type, which decodes byte arrays of arbitrary length. 73 | class DynamicBytes extends AbiType { 74 | /// Constructor. 75 | const DynamicBytes(); 76 | 77 | @override 78 | String get name => 'bytes'; 79 | 80 | @override 81 | EncodingLengthInfo get encodingLength => const EncodingLengthInfo.dynamic(); 82 | 83 | @override 84 | void encode(Uint8List data, LengthTrackingByteSink buffer) { 85 | const UintType().encode(BigInt.from(data.length), buffer); 86 | 87 | final padding = calculatePadLength(data.length, allowEmpty: true); 88 | 89 | buffer 90 | ..add(data) 91 | ..add(Uint8List(padding)); 92 | } 93 | 94 | @override 95 | DecodingResult decode(ByteBuffer buffer, int offset) { 96 | final lengthResult = const UintType().decode(buffer, offset); 97 | final length = lengthResult.data.toInt(); 98 | final padding = calculatePadLength(length, allowEmpty: true); 99 | 100 | // first 32 bytes are taken for the encoded size, read from there 101 | return DecodingResult( 102 | buffer.asUint8List(offset + sizeUnitBytes, length), 103 | sizeUnitBytes + length + padding, 104 | ); 105 | } 106 | 107 | @override 108 | int get hashCode => runtimeType.hashCode; 109 | 110 | @override 111 | bool operator ==(other) { 112 | return other.runtimeType == DynamicBytes; 113 | } 114 | } 115 | 116 | /// The solidity string type, which utf-8 encodes strings 117 | class StringType extends AbiType { 118 | /// Constructor. 119 | const StringType(); 120 | 121 | @override 122 | String get name => 'string'; 123 | @override 124 | EncodingLengthInfo get encodingLength => const EncodingLengthInfo.dynamic(); 125 | 126 | @override 127 | void encode(String data, LengthTrackingByteSink buffer) { 128 | const DynamicBytes().encode(uint8ListFromList(utf8.encode(data)), buffer); 129 | } 130 | 131 | @override 132 | DecodingResult decode(ByteBuffer buffer, int offset) { 133 | final bytesResult = const DynamicBytes().decode(buffer, offset); 134 | 135 | return DecodingResult(utf8.decode(bytesResult.data), bytesResult.bytesRead); 136 | } 137 | 138 | @override 139 | int get hashCode => runtimeType.hashCode; 140 | 141 | @override 142 | bool operator ==(other) { 143 | return other.runtimeType == StringType; 144 | } 145 | } 146 | 147 | /// Base class for (non-byte) arrays in solidity. 148 | abstract class BaseArrayType extends AbiType> { 149 | const BaseArrayType._(this.type); 150 | 151 | /// The inner abi type. 152 | final AbiType type; 153 | } 154 | 155 | /// The solidity T\[k\] type for arrays whose length is known. 156 | class FixedLengthArray extends BaseArrayType { 157 | /// Constructor. 158 | const FixedLengthArray({required AbiType type, required this.length}) 159 | : super._(type); 160 | 161 | /// Length. 162 | final int length; 163 | 164 | @override 165 | String get name => '${type.name}[$length]'; 166 | 167 | @override 168 | EncodingLengthInfo get encodingLength { 169 | if (type.encodingLength.isDynamic) { 170 | return const EncodingLengthInfo.dynamic(); 171 | } 172 | return EncodingLengthInfo(type.encodingLength.length! * length); 173 | } 174 | 175 | @override 176 | void encode(List data, LengthTrackingByteSink buffer) { 177 | assert(data.length == length); 178 | 179 | if (encodingLength.isDynamic) { 180 | const lengthEncoder = UintType(); 181 | 182 | final startPosition = buffer.length; 183 | var currentOffset = data.length * sizeUnitBytes; 184 | 185 | // first, write a bunch of zeroes were the length will be written later. 186 | buffer.add(Uint8List(data.length * sizeUnitBytes)); 187 | 188 | for (var i = 0; i < length; i++) { 189 | // write the actual position into the slot reserved earlier 190 | lengthEncoder.encodeReplace( 191 | startPosition + i * sizeUnitBytes, 192 | BigInt.from(currentOffset), 193 | buffer, 194 | ); 195 | 196 | final lengthBefore = buffer.length; 197 | type.encode(data[i], buffer); 198 | currentOffset += buffer.length - lengthBefore; 199 | } 200 | } else { 201 | for (final elem in data) { 202 | type.encode(elem, buffer); 203 | } 204 | } 205 | } 206 | 207 | @override 208 | DecodingResult> decode(ByteBuffer buffer, int offset) { 209 | final decoded = []; 210 | var headersLength = 0; 211 | var dynamicLength = 0; 212 | 213 | if (encodingLength.isDynamic) { 214 | for (var i = 0; i < length; i++) { 215 | final positionResult = 216 | const UintType().decode(buffer, offset + headersLength); 217 | headersLength += positionResult.bytesRead; 218 | 219 | final position = positionResult.data.toInt(); 220 | 221 | final dataResult = type.decode(buffer, offset + position); 222 | dynamicLength += dataResult.bytesRead; 223 | decoded.add(dataResult.data); 224 | } 225 | } else { 226 | for (var i = 0; i < length; i++) { 227 | final result = type.decode(buffer, offset + headersLength); 228 | headersLength += result.bytesRead; 229 | 230 | decoded.add(result.data); 231 | } 232 | } 233 | 234 | return DecodingResult(decoded, headersLength + dynamicLength); 235 | } 236 | 237 | @override 238 | int get hashCode => 41 * length + 5 * type.hashCode; 239 | 240 | @override 241 | bool operator ==(other) { 242 | return identical(this, other) || 243 | (other is FixedLengthArray && 244 | other.length == length && 245 | other.type == type); 246 | } 247 | } 248 | 249 | /// The solidity T[] type for arrays with an dynamic length. 250 | class DynamicLengthArray extends BaseArrayType { 251 | /// Constructor. 252 | const DynamicLengthArray({required AbiType type}) : super._(type); 253 | 254 | @override 255 | EncodingLengthInfo get encodingLength => const EncodingLengthInfo.dynamic(); 256 | @override 257 | String get name => '${type.name}[]'; 258 | 259 | @override 260 | void encode(List data, LengthTrackingByteSink buffer) { 261 | const UintType().encode(BigInt.from(data.length), buffer); 262 | FixedLengthArray(type: type, length: data.length).encode(data, buffer); 263 | } 264 | 265 | @override 266 | DecodingResult> decode(ByteBuffer buffer, int offset) { 267 | final lengthResult = const UintType().decode(buffer, offset); 268 | 269 | final arrayType = 270 | FixedLengthArray(type: type, length: lengthResult.data.toInt()); 271 | final dataResult = 272 | arrayType.decode(buffer, offset + lengthResult.bytesRead); 273 | 274 | return DecodingResult( 275 | dataResult.data, 276 | lengthResult.bytesRead + dataResult.bytesRead, 277 | ); 278 | } 279 | 280 | @override 281 | int get hashCode => 31 * type.hashCode; 282 | 283 | @override 284 | bool operator ==(Object other) { 285 | return identical(this, other) || 286 | (other is DynamicLengthArray && other.type == type); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /lib/src/credentials/wallet.dart: -------------------------------------------------------------------------------- 1 | part of '../../web3dart.dart'; 2 | 3 | abstract class _KeyDerivator { 4 | Uint8List deriveKey(Uint8List password); 5 | 6 | String get name; 7 | Map encode(); 8 | } 9 | 10 | class _PBDKDF2KeyDerivator extends _KeyDerivator { 11 | _PBDKDF2KeyDerivator(this.iterations, this.salt, this.dklen); 12 | final int iterations; 13 | final Uint8List salt; 14 | final int dklen; 15 | 16 | // The docs (https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition) 17 | // say that HMAC with SHA-256 is the only mac supported at the moment 18 | static final Mac mac = HMac(SHA256Digest(), 64); 19 | 20 | @override 21 | Uint8List deriveKey(Uint8List password) { 22 | final impl = pbkdf2.PBKDF2KeyDerivator(mac) 23 | ..init(Pbkdf2Parameters(salt, iterations, dklen)); 24 | 25 | return impl.process(password); 26 | } 27 | 28 | @override 29 | Map encode() { 30 | return { 31 | 'c': iterations, 32 | 'dklen': dklen, 33 | 'prf': 'hmac-sha256', 34 | 'salt': bytesToHex(salt), 35 | }; 36 | } 37 | 38 | @override 39 | final String name = 'pbkdf2'; 40 | } 41 | 42 | class _ScryptKeyDerivator extends _KeyDerivator { 43 | _ScryptKeyDerivator(this.dklen, this.n, this.r, this.p, this.salt); 44 | final int dklen; 45 | final int n; 46 | final int r; 47 | final int p; 48 | final Uint8List salt; 49 | 50 | @override 51 | Uint8List deriveKey(Uint8List password) { 52 | final impl = scrypt.Scrypt()..init(ScryptParameters(n, r, p, dklen, salt)); 53 | 54 | return impl.process(password); 55 | } 56 | 57 | @override 58 | Map encode() { 59 | return { 60 | 'dklen': dklen, 61 | 'n': n, 62 | 'r': r, 63 | 'p': p, 64 | 'salt': bytesToHex(salt), 65 | }; 66 | } 67 | 68 | @override 69 | final String name = 'scrypt'; 70 | } 71 | 72 | /// Represents a wallet file. Wallets are used to securely store credentials 73 | /// like a private key belonging to an Ethereum address. The private key in a 74 | /// wallet is encrypted with a secret password that needs to be known in order 75 | /// to obtain the private key. 76 | class Wallet { 77 | const Wallet._( 78 | this.privateKey, 79 | this._derivator, 80 | this._password, 81 | this._iv, 82 | this._id, 83 | ); 84 | 85 | /// Creates a new wallet wrapping the specified [credentials] by encrypting 86 | /// the private key with the [password]. The [random] instance, which should 87 | /// be cryptographically secure, is used to generate encryption keys. 88 | /// You can configure the parameter N of the scrypt algorithm if you need to. 89 | /// The default value for [scryptN] is 8192. Be aware that this N must be a 90 | /// power of two. 91 | factory Wallet.createNew( 92 | EthPrivateKey credentials, 93 | String password, 94 | Random random, { 95 | int scryptN = 8192, 96 | int p = 1, 97 | }) { 98 | final passwordBytes = Uint8List.fromList(utf8.encode(password)); 99 | final dartRandom = RandomBridge(random); 100 | 101 | final salt = dartRandom.nextBytes(32); 102 | final derivator = _ScryptKeyDerivator(32, scryptN, 8, p, salt); 103 | 104 | final uuid = generateUuidV4(); 105 | 106 | final iv = dartRandom.nextBytes(128 ~/ 8); 107 | 108 | return Wallet._(credentials, derivator, passwordBytes, iv, uuid); 109 | } 110 | 111 | /// Reads and unlocks the wallet denoted in the json string given with the 112 | /// specified [password]. [encoded] must be the String contents of a valid 113 | /// v3 Ethereum wallet file. 114 | factory Wallet.fromJson(String encoded, String password) { 115 | /* 116 | In order to read the wallet and obtain the secret key stored in it, we 117 | need to do the following: 118 | 1: Key Derivation: Based on the key derivator specified (either pbdkdf2 or 119 | scryt), we need to use the password to obtain the aes key used to 120 | decrypt the private key. 121 | 2: Using the obtained aes key and the iv parameter, decrypt the private 122 | key stored in the wallet. 123 | */ 124 | 125 | final data = json.decode(encoded); 126 | 127 | // Ensure version is 3, only version that we support at the moment 128 | final version = data['version']; 129 | if (version != 3) { 130 | throw ArgumentError.value( 131 | version, 132 | 'version', 133 | 'Library only supports ' 134 | 'version 3 of wallet files at the moment. However, the following value' 135 | ' has been given:', 136 | ); 137 | } 138 | 139 | final crypto = data['crypto'] ?? data['Crypto']; 140 | 141 | final kdf = crypto['kdf'] as String; 142 | _KeyDerivator derivator; 143 | 144 | switch (kdf) { 145 | case 'pbkdf2': 146 | final derParams = crypto['kdfparams'] as Map; 147 | 148 | if (derParams['prf'] != 'hmac-sha256') { 149 | throw ArgumentError( 150 | 'Invalid prf supplied with the pdf: was ${derParams["prf"]}, expected hmac-sha256', 151 | ); 152 | } 153 | 154 | derivator = _PBDKDF2KeyDerivator( 155 | derParams['c'] as int, 156 | Uint8List.fromList(hexToBytes(derParams['salt'] as String)), 157 | derParams['dklen'] as int, 158 | ); 159 | 160 | break; 161 | case 'scrypt': 162 | final derParams = crypto['kdfparams'] as Map; 163 | derivator = _ScryptKeyDerivator( 164 | derParams['dklen'] as int, 165 | derParams['n'] as int, 166 | derParams['r'] as int, 167 | derParams['p'] as int, 168 | Uint8List.fromList(hexToBytes(derParams['salt'] as String)), 169 | ); 170 | break; 171 | default: 172 | throw ArgumentError( 173 | 'Wallet file uses $kdf as key derivation function, which is not supported.', 174 | ); 175 | } 176 | 177 | // Now that we have the derivator, let's obtain the aes key: 178 | final encodedPassword = Uint8List.fromList(utf8.encode(password)); 179 | final derivedKey = derivator.deriveKey(encodedPassword); 180 | final aesKey = Uint8List.fromList(derivedKey.sublist(0, 16)); 181 | 182 | final encryptedPrivateKey = hexToBytes(crypto['ciphertext'] as String); 183 | 184 | //Validate the derived key with the mac provided 185 | final derivedMac = _generateMac(derivedKey, encryptedPrivateKey); 186 | if (derivedMac != crypto['mac']) { 187 | throw ArgumentError( 188 | 'Could not unlock wallet file. You either supplied the wrong password or the file is corrupted', 189 | ); 190 | } 191 | 192 | // We only support this mode at the moment 193 | if (crypto['cipher'] != 'aes-128-ctr') { 194 | throw ArgumentError( 195 | 'Wallet file uses ${crypto["cipher"]} as cipher, but only aes-128-ctr is supported.', 196 | ); 197 | } 198 | final iv = 199 | Uint8List.fromList(hexToBytes(crypto['cipherparams']['iv'] as String)); 200 | 201 | // Decrypt the private key 202 | 203 | final aes = _initCipher(false, aesKey, iv); 204 | 205 | final privateKey = aes.process(Uint8List.fromList(encryptedPrivateKey)); 206 | final credentials = EthPrivateKey(privateKey); 207 | 208 | final id = parseUuid(data['id'] as String); 209 | 210 | return Wallet._(credentials, derivator, encodedPassword, iv, id); 211 | } 212 | 213 | /// The credentials stored in this wallet file 214 | final EthPrivateKey privateKey; 215 | 216 | /// The key derivator used to obtain the aes decryption key from the password 217 | final _KeyDerivator _derivator; 218 | 219 | final Uint8List _password; 220 | final Uint8List _iv; 221 | 222 | final Uint8List _id; 223 | 224 | /// Gets the random uuid assigned to this wallet file 225 | String get uuid => formatUuid(_id); 226 | 227 | /// Encrypts the private key using the secret specified earlier and returns 228 | /// a json representation of its data as a v3-wallet file. 229 | String toJson() { 230 | final ciphertextBytes = _encryptPrivateKey(); 231 | 232 | final map = { 233 | 'crypto': { 234 | 'cipher': 'aes-128-ctr', 235 | 'cipherparams': {'iv': bytesToHex(_iv)}, 236 | 'ciphertext': bytesToHex(ciphertextBytes), 237 | 'kdf': _derivator.name, 238 | 'kdfparams': _derivator.encode(), 239 | 'mac': _generateMac(_derivator.deriveKey(_password), ciphertextBytes), 240 | }, 241 | 'id': uuid, 242 | 'version': 3, 243 | }; 244 | 245 | return json.encode(map); 246 | } 247 | 248 | static String _generateMac(List dk, List ciphertext) { 249 | final macBody = [...dk.sublist(16, 32), ...ciphertext]; 250 | 251 | return bytesToHex(keccak256(uint8ListFromList(macBody))); 252 | } 253 | 254 | static CTRStreamCipher _initCipher( 255 | bool forEncryption, 256 | Uint8List key, 257 | Uint8List iv, 258 | ) { 259 | return CTRStreamCipher(AESEngine()) 260 | ..init(false, ParametersWithIV(KeyParameter(key), iv)); 261 | } 262 | 263 | List _encryptPrivateKey() { 264 | final derived = _derivator.deriveKey(_password); 265 | final aesKey = Uint8List.view(derived.buffer, 0, 16); 266 | 267 | final aes = _initCipher(true, aesKey, _iv); 268 | return aes.process(privateKey.privateKey); 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /test/core/sign_transaction_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:typed_data'; 3 | 4 | import 'package:http/http.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:web3dart/src/utils/rlp.dart' as rlp; 7 | import 'package:web3dart/web3dart.dart'; 8 | import 'package:wallet/wallet.dart'; 9 | 10 | const rawJson = '''[ 11 | { 12 | "nonce":819, 13 | "value":43203529, 14 | "gasLimit":35552, 15 | "maxPriorityFeePerGas":75853, 16 | "maxFeePerGas":121212, 17 | "to":"0x000000000000000000000000000000000000aaaa", 18 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 19 | "signedTransactionRLP":"0xb87102f86e048203338301284d8301d97c828ae094000000000000000000000000000000000000aaaa8402933bc980c080a00f924cb68412c8f1cfd74d9b581c71eeaf94fff6abdde3e5b02ca6b2931dcf47a07dd1c50027c3e31f8b565e25ce68a5072110f61fce5eee81b195dd51273c2f83" 20 | }, 21 | { 22 | "nonce":353, 23 | "value":61901619, 24 | "gasLimit":32593, 25 | "maxPriorityFeePerGas":38850, 26 | "maxFeePerGas":136295, 27 | "to":"0x000000000000000000000000000000000000aaaa", 28 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 29 | "signedTransactionRLP":"0xb87002f86d048201618297c283021467827f5194000000000000000000000000000000000000aaaa8403b08b3380c080a08caf712f72489da6f1a634b651b4b1c7d9be7d1e8d05ea76c1eccee3bdfb86a5a06aecc106f588ce51e112f5e9ea7aba3e089dc7511718821d0e0cd52f52af4e45" 30 | }, 31 | { 32 | "nonce":985, 33 | "value":32531825, 34 | "gasLimit":68541, 35 | "maxPriorityFeePerGas":66377, 36 | "maxFeePerGas":136097, 37 | "to":"0x000000000000000000000000000000000000aaaa", 38 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 39 | "signedTransactionRLP":"0xb87202f86f048203d983010349830213a183010bbd94000000000000000000000000000000000000aaaa8401f0657180c001a08c03a86e85789ee9a1b42fa0a86d316fca262694f8c198df11f194678c2c2d35a028f8e7de02b35014a17b6d28ff8c7e7be6860e7265ac162fb721f1aeae75643c" 40 | }, 41 | { 42 | "nonce":623, 43 | "value":21649799, 44 | "gasLimit":57725, 45 | "maxPriorityFeePerGas":74140, 46 | "maxFeePerGas":81173, 47 | "to":"0x000000000000000000000000000000000000aaaa", 48 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 49 | "signedTransactionRLP":"0xb87102f86e0482026f8301219c83013d1582e17d94000000000000000000000000000000000000aaaa84014a598780c001a0b87c4c8c505d2d692ac77ba466547e79dd60fe7ecd303d520bf6e8c7085e3182a06dc7d00f5e68c3f3ebe8ae35a90d46051afde620ac12e43cae9560a29b13e7fb" 50 | }, 51 | { 52 | "nonce":972, 53 | "value":94563383, 54 | "gasLimit":65254, 55 | "maxPriorityFeePerGas":42798, 56 | "maxFeePerGas":103466, 57 | "to":"0x000000000000000000000000000000000000aaaa", 58 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 59 | "signedTransactionRLP":"0xb87002f86d048203cc82a72e8301942a82fee694000000000000000000000000000000000000aaaa8405a2ec3780c001a006cf07af78c187db104496c58d679f37fcd2d5790970cecf9a59fe4a5321b375a039f3faafc71479d283a5b1e66a86b19c4bdc516655d89dbe57d9747747c01dfe" 60 | }, 61 | { 62 | "nonce":588, 63 | "value":99359647, 64 | "gasLimit":37274, 65 | "maxPriorityFeePerGas":87890, 66 | "maxFeePerGas":130273, 67 | "to":"0x000000000000000000000000000000000000aaaa", 68 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 69 | "signedTransactionRLP":"0xb87102f86e0482024c830157528301fce182919a94000000000000000000000000000000000000aaaa8405ec1b9f80c080a03e2f59ac9ca852034c2c1da35a742ca19fdd910aa5d2ed49ab8ad27a2fcb2b10a03ac1c29c26723c58f91400fb6dfb5f5b837467b1c377541b47dae474dddbe469" 70 | }, 71 | { 72 | "nonce":900, 73 | "value":30402257, 74 | "gasLimit":76053, 75 | "maxPriorityFeePerGas":8714, 76 | "maxFeePerGas":112705, 77 | "to":"0x000000000000000000000000000000000000aaaa", 78 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 79 | "signedTransactionRLP":"0xb87102f86e0482038482220a8301b8418301291594000000000000000000000000000000000000aaaa8401cfe6d180c001a0f7ffc5bca2512860f8236360159bf303dcfab71546b6a0032df0306f3739d0c4a05d38fe2c4edebdc1edc157034f780c53a0e5ae089e57220745bd48bcb10cdf87" 80 | }, 81 | { 82 | "nonce":709, 83 | "value":6478043, 84 | "gasLimit":28335, 85 | "maxPriorityFeePerGas":86252, 86 | "maxFeePerGas":94636, 87 | "to":"0x000000000000000000000000000000000000aaaa", 88 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 89 | "signedTransactionRLP":"0xb87002f86d048202c5830150ec830171ac826eaf94000000000000000000000000000000000000aaaa8362d8db80c001a0a61a5710512f346c9996377f7b564ccb64c73a5fdb615499adb1250498f3e01aa002d10429572cecfaa911a58bbe05f2b26e4c3aee3402202153a93692849add11" 90 | }, 91 | { 92 | "nonce":939, 93 | "value":2782905, 94 | "gasLimit":45047, 95 | "maxPriorityFeePerGas":45216, 96 | "maxFeePerGas":91648, 97 | "to":"0x000000000000000000000000000000000000aaaa", 98 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 99 | "signedTransactionRLP":"0xb86f02f86c048203ab82b0a08301660082aff794000000000000000000000000000000000000aaaa832a76b980c001a0191f0f6667a20cefc0b454e344cc01108aafbdc4e4e5ed88fdd1b5d108495b31a020879042b0f8d3807609f18fe42a9820de53c8a0ea1d0a2d50f8f5e92a94f00d" 100 | }, 101 | { 102 | "nonce":119, 103 | "value":65456115, 104 | "gasLimit":62341, 105 | "maxPriorityFeePerGas":24721, 106 | "maxFeePerGas":107729, 107 | "to":"0x000000000000000000000000000000000000aaaa", 108 | "privateKey":"0x8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 109 | "signedTransactionRLP":"0xb86e02f86b04778260918301a4d182f38594000000000000000000000000000000000000aaaa8403e6c7f380c001a05e40977f4064a2bc08785e422ed8a47b56aa219abe93251d2b3b4d0cf937b8c0a071e600cd15589c3607bd9627314b99e9b5976bd427b774aa685bd0d036b1771e" 110 | } 111 | ]'''; 112 | 113 | void main() { 114 | test('sign eip 1559 transaction', () async { 115 | final data = jsonDecode(rawJson) as List; 116 | 117 | await Future.forEach(data, (element) async { 118 | final tx = element as Map; 119 | final credentials = 120 | EthPrivateKey.fromHex(strip0x(tx['privateKey'] as String)); 121 | final transaction = Transaction( 122 | from: credentials.address, 123 | to: EthereumAddress.fromHex(tx['to'] as String), 124 | nonce: tx['nonce'] as int, 125 | maxGas: tx['gasLimit'] as int, 126 | value: EtherAmount.inWei(BigInt.from(tx['value'] as int)), 127 | maxFeePerGas: EtherAmount.fromBigInt( 128 | EtherUnit.wei, 129 | BigInt.from(tx['maxFeePerGas'] as int), 130 | ), 131 | maxPriorityFeePerGas: EtherAmount.fromBigInt( 132 | EtherUnit.wei, 133 | BigInt.from(tx['maxPriorityFeePerGas'] as int), 134 | ), 135 | ); 136 | 137 | final client = Web3Client('', Client()); 138 | final signature = 139 | await client.signTransaction(credentials, transaction, chainId: 4); 140 | 141 | expect( 142 | bytesToHex( 143 | uint8ListFromList( 144 | rlp.encode(prependTransactionType(0x02, signature)), 145 | ), 146 | ), 147 | strip0x(tx['signedTransactionRLP'] as String), 148 | ); 149 | }); 150 | }); 151 | 152 | test('sign eip 1559 transaction without client', () { 153 | final data = jsonDecode(rawJson) as List; 154 | 155 | Future.forEach(data, (element) { 156 | final tx = element as Map; 157 | final credentials = 158 | EthPrivateKey.fromHex(strip0x(tx['privateKey'] as String)); 159 | final transaction = Transaction( 160 | from: credentials.address, 161 | to: EthereumAddress.fromHex(tx['to'] as String), 162 | nonce: tx['nonce'] as int, 163 | maxGas: tx['gasLimit'] as int, 164 | value: EtherAmount.inWei(BigInt.from(tx['value'] as int)), 165 | maxFeePerGas: EtherAmount.fromBigInt( 166 | EtherUnit.wei, 167 | BigInt.from(tx['maxFeePerGas'] as int), 168 | ), 169 | maxPriorityFeePerGas: EtherAmount.fromBigInt( 170 | EtherUnit.wei, 171 | BigInt.from(tx['maxPriorityFeePerGas'] as int), 172 | ), 173 | data: tx['data'] ?? Uint8List(0), 174 | ); 175 | 176 | final signature = 177 | signTransactionRaw(transaction, credentials, chainId: 4); 178 | 179 | expect( 180 | bytesToHex( 181 | uint8ListFromList( 182 | rlp.encode(prependTransactionType(0x02, signature)), 183 | ), 184 | ), 185 | strip0x(tx['signedTransactionRLP'] as String), 186 | ); 187 | }); 188 | }); 189 | 190 | test('signs transactions', () async { 191 | final credentials = EthPrivateKey.fromHex( 192 | 'a2fd51b96dc55aeb14b30d55a6b3121c7b9c599500c1beb92a389c3377adc86e', 193 | ); 194 | final transaction = Transaction( 195 | from: credentials.address, 196 | to: EthereumAddress.fromHex('0xC914Bb2ba888e3367bcecEb5C2d99DF7C7423706'), 197 | nonce: 0, 198 | gasPrice: EtherAmount.inWei(BigInt.one), 199 | maxGas: 10, 200 | value: EtherAmount.inWei(BigInt.from(10)), 201 | ); 202 | 203 | final client = Web3Client('', Client()); 204 | final signature = await client.signTransaction(credentials, transaction); 205 | 206 | expect( 207 | bytesToHex(signature), 208 | 'f85d80010a94c914bb2ba888e3367bceceb5c2d99df7c74237060a8025a0a78c2f8b0f95c33636b2b1b91d3d23844fba2ec1b2168120ad64b84565b94bcda0365ecaff22197e3f21816cf9d428d695087ad3a8b7f93456cd48311d71402578', 209 | ); 210 | }); 211 | 212 | // example from https://github.com/ethereum/EIPs/issues/155 213 | test('signs eip 155 transaction', () async { 214 | final credentials = EthPrivateKey.fromHex( 215 | '0x4646464646464646464646464646464646464646464646464646464646464646', 216 | ); 217 | 218 | final transaction = Transaction( 219 | nonce: 9, 220 | gasPrice: EtherAmount.inWei(BigInt.from(20000000000)), 221 | maxGas: 21000, 222 | to: EthereumAddress.fromHex('0x3535353535353535353535353535353535353535'), 223 | value: EtherAmount.inWei(BigInt.from(1000000000000000000)), 224 | ); 225 | 226 | final client = Web3Client('', Client()); 227 | final signature = await client.signTransaction(credentials, transaction); 228 | 229 | expect( 230 | bytesToHex(signature), 231 | 'f86c098504a817c800825208943535353535353535353535353535353535353535880' 232 | 'de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d' 233 | '3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf55' 234 | '5c9f3dc64214b297fb1966a3b6d83', 235 | ); 236 | }); 237 | } 238 | -------------------------------------------------------------------------------- /lib/src/core/filters.dart: -------------------------------------------------------------------------------- 1 | part of 'package:web3dart/web3dart.dart'; 2 | 3 | class _FilterCreationParams { 4 | _FilterCreationParams(this.method, this.params); 5 | final String method; 6 | final List params; 7 | } 8 | 9 | class _PubSubCreationParams { 10 | _PubSubCreationParams(this.params); 11 | final List params; 12 | } 13 | 14 | abstract class _Filter { 15 | bool get supportsPubSub => true; 16 | 17 | _FilterCreationParams create(); 18 | _PubSubCreationParams createPubSub(); 19 | T parseChanges(dynamic log); 20 | } 21 | 22 | class _NewBlockFilter extends _Filter { 23 | @override 24 | bool get supportsPubSub => false; 25 | 26 | @override 27 | _FilterCreationParams create() { 28 | return _FilterCreationParams('eth_newBlockFilter', []); 29 | } 30 | 31 | @override 32 | String parseChanges(dynamic log) { 33 | return log as String; 34 | } 35 | 36 | @override 37 | _PubSubCreationParams createPubSub() { 38 | // the pub-sub subscription for new blocks isn't universally supported by 39 | // ethereum nodes, so let's not implement it just yet. 40 | return _PubSubCreationParams(List.empty()); 41 | } 42 | } 43 | 44 | class _PendingTransactionsFilter extends _Filter { 45 | @override 46 | bool get supportsPubSub => false; 47 | 48 | @override 49 | _FilterCreationParams create() { 50 | return _FilterCreationParams('eth_newPendingTransactionFilter', []); 51 | } 52 | 53 | @override 54 | String parseChanges(dynamic log) { 55 | return log as String; 56 | } 57 | 58 | @override 59 | _PubSubCreationParams createPubSub() { 60 | return _PubSubCreationParams(List.empty()); 61 | } 62 | } 63 | 64 | /// Options for event filters created with [Web3Client.events]. 65 | class FilterOptions { 66 | FilterOptions({this.fromBlock, this.toBlock, this.address, this.topics}); 67 | 68 | FilterOptions.events({ 69 | required DeployedContract contract, 70 | required ContractEvent event, 71 | this.fromBlock, 72 | this.toBlock, 73 | }) : address = contract.address, 74 | topics = [ 75 | [bytesToHex(event.signature, padToEvenLength: true, include0x: true)], 76 | ]; 77 | 78 | /// The earliest block which should be considered for this filter. Optional, 79 | /// the default value is [BlockNum.current]. 80 | /// 81 | /// Use [BlockNum.current] for the last mined block or 82 | /// [BlockNum.pending] for not yet mined transactions. 83 | final BlockNum? fromBlock; 84 | 85 | /// The last block which should be considered for this filter. Optional, the 86 | /// default value is [BlockNum.current]. 87 | /// 88 | /// Use [BlockNum.current] for the last mined block or 89 | /// [BlockNum.pending] for not yet mined transactions. 90 | final BlockNum? toBlock; 91 | 92 | /// The optional address to limit this filter to. If not null, only logs 93 | /// emitted from the contract at [address] will be considered. Otherwise, all 94 | /// log events will be reported. 95 | final EthereumAddress? address; 96 | 97 | /// The topics that must be present in the event to be included in this 98 | /// filter. The topics must be represented as a hexadecimal value prefixed 99 | /// with "0x". The encoding must have an even number of digits. 100 | /// 101 | /// Topics are order-dependent. A transaction with a log with topics \[A, B\] 102 | /// will be matched by the following topic filters: 103 | /// - \[\], which matches anything 104 | /// - \[A\], which matches "A" in the first position and anything after 105 | /// - \[null, B\], which matches logs that have anything in their first 106 | /// position, B in their second position and anything after 107 | /// - \[A, B\], which matches A in first position, B in second position (and 108 | /// anything after) 109 | /// - \[\[A, B\], \[A, B\]\]: Matches (A or B) in first position AND (A or B) 110 | /// in second position (and anything after). 111 | /// 112 | /// The events sent by solidity contracts are encoded like this: The first 113 | /// topic is the hash of the event signature (except for anonymous events). 114 | /// All further topics are the encoded values of the indexed parameters of the 115 | /// event. See https://solidity.readthedocs.io/en/develop/contracts.html#events 116 | /// for a detailed description. 117 | final List>? topics; 118 | } 119 | 120 | /// A log event emitted in a transaction. 121 | class FilterEvent { 122 | FilterEvent({ 123 | this.removed, 124 | this.logIndex, 125 | this.transactionIndex, 126 | this.transactionHash, 127 | this.blockHash, 128 | this.blockNum, 129 | this.address, 130 | this.data, 131 | this.topics, 132 | }); 133 | 134 | FilterEvent.fromMap(Map log) 135 | : removed = log['removed'] as bool? ?? false, 136 | logIndex = log['logIndex'] != null 137 | ? hexToInt(log['logIndex'] as String).toInt() 138 | : null, 139 | transactionIndex = log['transactionIndex'] != null 140 | ? hexToInt(log['transactionIndex'] as String).toInt() 141 | : null, 142 | transactionHash = log['transactionHash'] != null 143 | ? log['transactionHash'] as String 144 | : null, 145 | blockHash = 146 | log['blockHash'] != null ? log['blockHash'] as String : null, 147 | blockNum = log['blockNumber'] != null 148 | ? hexToInt(log['blockNumber'] as String).toInt() 149 | : null, 150 | address = EthereumAddress.fromHex(log['address'] as String), 151 | data = log['data'] as String?, 152 | topics = (log['topics'] as List?)?.cast(); 153 | 154 | /// Whether the log was removed, due to a chain reorganization. False if it's 155 | /// a valid log. 156 | final bool? removed; 157 | 158 | /// Log index position in the block. `null` when the transaction which caused 159 | /// this log has not yet been mined. 160 | final int? logIndex; 161 | 162 | /// Transaction index position in the block. 163 | /// `null` when the transaction which caused this log has not yet been mined. 164 | final int? transactionIndex; 165 | 166 | /// Hash of the transaction which caused this log. `null` when it's pending. 167 | final String? transactionHash; 168 | 169 | /// Hash of the block where this log was in. `null` when it's pending. 170 | final String? blockHash; 171 | 172 | /// The block number of the block where this log was in. `null` when it's 173 | /// pending. 174 | final int? blockNum; 175 | 176 | /// The address (of the smart contract) from which this log originated. 177 | final EthereumAddress? address; 178 | 179 | /// The data blob of this log, hex-encoded. 180 | /// 181 | /// For solidity events, this contains all non-indexed parameters of the 182 | /// event. 183 | final String? data; 184 | 185 | /// The topics of this event, hex-encoded. 186 | /// 187 | /// For solidity events, the first topic is a hash of the event signature 188 | /// (except for anonymous events). All further topics are the encoded 189 | /// values of indexed parameters. 190 | final List? topics; 191 | 192 | @override 193 | String toString() { 194 | return 'FilterEvent(' 195 | 'removed=$removed,' 196 | 'logIndex=$logIndex,' 197 | 'transactionIndex=$transactionIndex,' 198 | 'transactionHash=$transactionHash,' 199 | 'blockHash=$blockHash,' 200 | 'blockNum=$blockNum,' 201 | 'address=$address,' 202 | 'data=$data,' 203 | 'topics=$topics' 204 | ')'; 205 | } 206 | 207 | @override 208 | bool operator ==(Object other) => 209 | identical(this, other) || 210 | other is FilterEvent && 211 | runtimeType == other.runtimeType && 212 | removed == other.removed && 213 | logIndex == other.logIndex && 214 | transactionIndex == other.transactionIndex && 215 | transactionHash == other.transactionHash && 216 | blockHash == other.blockHash && 217 | blockNum == other.blockNum && 218 | address == other.address && 219 | data == other.data && 220 | eq.equals(topics, other.topics); 221 | 222 | @override 223 | int get hashCode => 224 | removed.hashCode ^ 225 | logIndex.hashCode ^ 226 | transactionIndex.hashCode ^ 227 | transactionHash.hashCode ^ 228 | blockHash.hashCode ^ 229 | blockNum.hashCode ^ 230 | address.hashCode ^ 231 | data.hashCode ^ 232 | topics.hashCode; 233 | } 234 | 235 | class _EventFilter extends _Filter { 236 | _EventFilter(this.options); 237 | final FilterOptions options; 238 | 239 | @override 240 | _FilterCreationParams create() { 241 | return _FilterCreationParams('eth_newFilter', [_createParamsObject(true)]); 242 | } 243 | 244 | @override 245 | _PubSubCreationParams createPubSub() { 246 | return _PubSubCreationParams([ 247 | 'logs', 248 | _createParamsObject(false), 249 | ]); 250 | } 251 | 252 | dynamic _createParamsObject(bool includeFromAndTo) { 253 | final encodedOptions = {}; 254 | if (options.fromBlock != null && includeFromAndTo) { 255 | encodedOptions['fromBlock'] = options.fromBlock?.toBlockParam(); 256 | } 257 | if (options.toBlock != null && includeFromAndTo) { 258 | encodedOptions['toBlock'] = options.toBlock?.toBlockParam(); 259 | } 260 | if (options.address != null) { 261 | encodedOptions['address'] = options.address?.with0x; 262 | } 263 | if (options.topics != null) { 264 | final topics = []; 265 | options.topics?.forEach((e) => topics.add(e.isEmpty ? null : e)); 266 | encodedOptions['topics'] = topics; 267 | } 268 | 269 | return encodedOptions; 270 | } 271 | 272 | @override 273 | FilterEvent parseChanges(dynamic log) { 274 | return FilterEvent.fromMap(log as Map); 275 | } 276 | } 277 | 278 | const _pingDuration = Duration(seconds: 2); 279 | 280 | class _FilterEngine { 281 | _FilterEngine(this._client); 282 | 283 | final List<_InstantiatedFilter> _filters = []; 284 | final Web3Client _client; 285 | 286 | RpcService get _rpc => _client._jsonRpc; 287 | 288 | Timer? _ticker; 289 | bool _isRefreshing = false; 290 | bool _clearingBecauseSocketClosed = false; 291 | 292 | final List _pendingUnsubcriptions = []; 293 | 294 | Stream addFilter(_Filter filter) { 295 | final pubSubAvailable = _client.socketConnector != null; 296 | 297 | late _InstantiatedFilter instantiated; 298 | instantiated = _InstantiatedFilter( 299 | filter, filter.supportsPubSub && pubSubAvailable, () { 300 | _pendingUnsubcriptions.add(uninstall(instantiated)); 301 | }); 302 | 303 | instantiated._controller.onListen = () { 304 | _filters.add(instantiated); 305 | 306 | if (instantiated.isPubSub) { 307 | _registerToPubSub(instantiated, filter.createPubSub()); 308 | } else { 309 | _registerToAPI(instantiated); 310 | _startTicking(); 311 | } 312 | }; 313 | 314 | return instantiated._controller.stream; 315 | } 316 | 317 | Future _registerToAPI(_InstantiatedFilter filter) async { 318 | final request = filter.filter.create(); 319 | 320 | try { 321 | final response = await _rpc.call(request.method, request.params); 322 | filter.id = response.result as String; 323 | } on RPCError catch (e, s) { 324 | filter._controller.addError(e, s); 325 | await filter._controller.close(); 326 | _filters.remove(filter); 327 | } 328 | } 329 | 330 | Future _registerToPubSub( 331 | _InstantiatedFilter filter, 332 | _PubSubCreationParams params, 333 | ) async { 334 | final peer = _client._connectWithPeer(); 335 | 336 | try { 337 | final response = await peer?.sendRequest('eth_subscribe', params.params); 338 | filter.id = response as String; 339 | } on rpc.RpcException catch (e, s) { 340 | filter._controller.addError(e, s); 341 | await filter._controller.close(); 342 | _filters.remove(filter); 343 | } 344 | } 345 | 346 | void _startTicking() { 347 | _ticker ??= Timer.periodic(_pingDuration, (_) => _refreshFilters()); 348 | } 349 | 350 | Future _refreshFilters() async { 351 | if (_isRefreshing) return; 352 | _isRefreshing = true; 353 | 354 | try { 355 | final filterSnapshot = List.of(_filters); 356 | 357 | for (final filter in filterSnapshot) { 358 | final updatedData = 359 | await _rpc.call('eth_getFilterChanges', [filter.id]); 360 | 361 | for (final payload in updatedData.result) { 362 | if (!filter._controller.isClosed) { 363 | _parseAndAdd(filter, payload); 364 | } 365 | } 366 | } 367 | } finally { 368 | _isRefreshing = false; 369 | } 370 | } 371 | 372 | void handlePubSubNotification(rpc.Parameters params) { 373 | final id = params['subscription'].asString; 374 | final result = params['result'].value; 375 | 376 | final filter = _filters.singleWhere((f) => f.isPubSub && f.id == id); 377 | // orElse: () => null); 378 | _parseAndAdd(filter, result); 379 | } 380 | 381 | void handleConnectionClosed() { 382 | try { 383 | _clearingBecauseSocketClosed = true; 384 | final pubSubFilters = _filters.where((f) => f.isPubSub).toList(); 385 | 386 | pubSubFilters.forEach(uninstall); 387 | } finally { 388 | _clearingBecauseSocketClosed = false; 389 | } 390 | } 391 | 392 | void _parseAndAdd(_InstantiatedFilter filter, dynamic payload) { 393 | final parsed = filter.filter.parseChanges(payload); 394 | filter._controller.add(parsed); 395 | } 396 | 397 | Future uninstall(_InstantiatedFilter filter) async { 398 | await filter._controller.close(); 399 | _filters.remove(filter); 400 | 401 | if (filter.isPubSub && !_clearingBecauseSocketClosed) { 402 | final connection = _client._connectWithPeer(); 403 | await connection?.sendRequest('eth_unsubscribe', [filter.id]); 404 | } else { 405 | await _rpc.call('eth_uninstallFilter', [filter.id]); 406 | } 407 | } 408 | 409 | Future dispose() async { 410 | _ticker?.cancel(); 411 | final remainingFilters = List.of(_filters); 412 | 413 | await Future.forEach(remainingFilters, uninstall); 414 | await Future.wait(_pendingUnsubcriptions); 415 | 416 | _pendingUnsubcriptions.clear(); 417 | } 418 | } 419 | 420 | class _InstantiatedFilter { 421 | _InstantiatedFilter(this.filter, this.isPubSub, Function() onCancel) 422 | : _controller = StreamController(onCancel: onCancel); 423 | 424 | /// The id of this filter. This value will be obtained from the API after the 425 | /// filter has been set up and is `null` before that. 426 | String? id; 427 | final _Filter filter; 428 | 429 | /// Whether the filter is listening on a websocket connection. 430 | final bool isPubSub; 431 | 432 | final StreamController _controller; 433 | } 434 | --------------------------------------------------------------------------------