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

Commit

Permalink
Add generated erc20 wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
simolus3 committed May 9, 2021
1 parent ac7d36d commit 42c7909
Show file tree
Hide file tree
Showing 14 changed files with 445 additions and 41 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

- Add `package:web3dart/browser.dart`, a library for using this package in
Ethereum-enabled browsers.
- Add code generator for smart contracts. To use it, just put the generated abi
json into a `.abi.json` file, add a dev-dependency on `build_runner` and run
`(flutter | dart) pub run build_runner build`.
- Add the `package:web3dart/contracts/erc20.dart` library for interacting with an
[ERC-20](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md) smart contract.

## 2.0.0

Expand Down
3 changes: 1 addition & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ analyzer:
- "**/*.g.dart"
# Will be analyzed anyway, nobody knows why ¯\_(ツ)_/¯. We're only analyzing lib/ and test/ as a workaround
- ".dart_tool/build/entrypoint/build.dart"
- "tool/**"

# this should always include all rules. Those we don't use are commented out
linter:
Expand Down Expand Up @@ -170,4 +169,4 @@ linter:
- void_checks
# PUB RULES
- package_names
# - sort_pub_dependencies (we prefer to group them by what they do)d
# - sort_pub_dependencies (we prefer to group them by what they do)
9 changes: 0 additions & 9 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,3 @@ post_process_builders:
defaults:
release_options:
enabled: true

targets:
$default:
builders:
":abi_generator":
enabled: true
generate_for:
- example/**
- lib/**
1 change: 1 addition & 0 deletions example/token.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions lib/contracts/erc20.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export '../src/generated/erc20.g.dart';
61 changes: 61 additions & 0 deletions lib/src/builder/documentation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import 'dart:convert';

import '../../contracts.dart';

/// Parses solidity documentation comments from the generated compiler
/// output.
class Documentation {
final String? contractDetails;
final Map<ContractEvent, String> events;
final Map<ContractFunction, String> functions;

Documentation._(this.contractDetails, this.events, this.functions);

String? forContract() => contractDetails?.asDartDoc;
String? forEvent(ContractEvent e) => events[e]?.asDartDoc;
String? forFunction(ContractFunction m) => functions[m]?.asDartDoc;

static Documentation? fromJson(Map<String, Object?> json, ContractAbi abi) {
if (json['version'] != 1) return null;

final rawEvents = json['events'] as Map;
final rawMethods = json['methods'] as Map;

final details = json['details'] as String?;
final methods = <ContractFunction, String>{};
final events = <ContractEvent, String>{};

for (final event in abi.events) {
final signature = event.stringSignature;
final match = (rawEvents[signature] as Map?)?.details;

if (match != null) {
events[event] = match;
}
}

for (final method in abi.functions) {
final signature = method.encodeName();
final match = (rawMethods[signature] as Map?)?.details;

if (match != null) {
methods[method] = match;
}
}

return Documentation._(details, events, methods);
}
}

extension on Map {
String? get details => this['details'] as String?;
}

extension on String {
String get asDartDoc {
return const LineSplitter()
.convert(this)
.map((line) => '/// $line')
.join('\n');
}
}
64 changes: 47 additions & 17 deletions lib/src/builder/generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'package:dart_style/dart_style.dart';
import 'package:path/path.dart';
import 'package:web3dart/contracts.dart';

import 'documentation.dart';
import 'utils.dart';

class ContractGenerator implements Builder {
Expand All @@ -23,13 +24,27 @@ class ContractGenerator implements Builder {
final withoutExtension =
inputId.path.substring(0, inputId.path.length - '.abi.json'.length);

var abiCode = await buildStep.readAsString(inputId);
// Remove unnecessary whitespace
abiCode = json.encode(json.decode(abiCode));
final source = json.decode(await buildStep.readAsString(inputId));
Documentation? documentation;

String abiCode;
if (source is Map) {
abiCode = json.encode(source['abi']);
} else {
// Remove unnecessary whitespace
abiCode = json.encode(source);
}

final abi = ContractAbi.fromJson(abiCode, _suggestName(withoutExtension));

if (source is Map) {
final doc = source['devdoc'];
if (doc is Map) documentation = Documentation.fromJson(doc.cast(), abi);
}

final outputId = AssetId(inputId.package, '$withoutExtension.g.dart');
await buildStep.writeAsString(outputId, _generateForAbi(abi, abiCode));
await buildStep.writeAsString(
outputId, _generateForAbi(abi, abiCode, documentation));
}

String _suggestName(String pathWithoutExtension) {
Expand All @@ -38,8 +53,8 @@ class ContractGenerator implements Builder {
}

//main method that parses abi to dart code
String _generateForAbi(ContractAbi abi, String abiCode) {
final generation = _ContractGeneration(abi, abiCode);
String _generateForAbi(ContractAbi abi, String abiCode, Documentation? docs) {
final generation = _ContractGeneration(abi, abiCode, docs);
final library = generation.generate();

final emitter = DartEmitter(
Expand All @@ -55,6 +70,7 @@ ${library.accept(emitter)}
class _ContractGeneration {
final ContractAbi _abi;
final String _abiCode;
final Documentation? documentation;

final List<Spec> _additionalSpecs = [];
final Map<ContractFunction, Reference> _functionToResultClass = {};
Expand All @@ -65,7 +81,7 @@ class _ContractGeneration {
// The `client` field, storing a reference to the web3client instance.
static final client = refer('client');

_ContractGeneration(this._abi, this._abiCode);
_ContractGeneration(this._abi, this._abiCode, this.documentation);

Library generate() {
return Library((b) {
Expand All @@ -87,6 +103,9 @@ class _ContractGeneration {
for (final event in _abi.events)
Method((b) => _methodForEvent(event, b))
]);

final details = documentation?.forContract();
if (details != null) b.docs.add(details);
}

void _createContractConstructor(ConstructorBuilder b) {
Expand Down Expand Up @@ -136,6 +155,9 @@ class _ContractGeneration {
..named = true
..required = true));
}

final docs = documentation?.forFunction(fun);
if (docs != null) b.docs.add(docs);
}

List<Parameter> _parametersFor(ContractFunction function) {
Expand Down Expand Up @@ -201,7 +223,8 @@ class _ContractGeneration {
});
}

Reference _generateResultClass(List<FunctionParameter> params, String name) {
Reference _generateResultClass(List<FunctionParameter> params, String name,
{String? docs}) {
final fields = <Field>[];
final initializers = <Code>[];
for (var i = 0; i < params.length; i++) {
Expand All @@ -220,22 +243,27 @@ class _ContractGeneration {
refer(name).assign(refer('response[$i]').castTo(solidityType)).code);
}

_additionalSpecs.add(Class((b) => b
..name = name
..fields.addAll(fields)
..constructors.add(Constructor((b) => b
..requiredParameters.add(Parameter((b) => b
..name = 'response'
..type = listify(dynamicType)))
..initializers.addAll(initializers)))));
_additionalSpecs.add(Class((b) {
b
..name = name
..fields.addAll(fields)
..constructors.add(Constructor((b) => b
..requiredParameters.add(Parameter((b) => b
..name = 'response'
..type = listify(dynamicType)))
..initializers.addAll(initializers)));

if (docs != null) b.docs.add(docs);
}));

return refer(name);
}

void _methodForEvent(ContractEvent event, MethodBuilder b) {
final name = event.name;
final eventClass = _generateResultClass(
event.components.map((e) => e.parameter).toList(), name);
event.components.map((e) => e.parameter).toList(), name,
docs: documentation?.forEvent(event));
final nullableBlockNum = blockNum.rebuild((b) => b.isNullable = true);

final mapper = Method(
Expand All @@ -258,6 +286,8 @@ class _ContractGeneration {

b
..returns = streamOf(eventClass)
..docs.add('/// Returns a live stream of all ${eventClass.symbol} '
'events emitted by this contract.')
..name = '${name.substring(0, 1).toLowerCase()}${name.substring(1)}'
..optionalParameters.add(Parameter((b) => b
..name = 'fromBlock'
Expand Down
18 changes: 7 additions & 11 deletions lib/src/contracts/abi/abi.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,20 +279,16 @@ class ContractEvent {

ContractEvent(this.anonymous, this.name, this.components);

Uint8List? _signature;
/// The user-visible signature of this event, consisting of its name and the
/// type of its parameters.
String get stringSignature {
final parameters = components.map((c) => c.parameter);
return '$name(${_encodeParameters(parameters)})';
}

/// The signature of this event, which is the keccak hash of the event's name
/// followed by it's components.
Uint8List get signature {
if (_signature == null) {
final parameters = components.map((c) => c.parameter);
final encodedName = '$name(${_encodeParameters(parameters)})';

_signature = keccakUtf8(encodedName);
}

return _signature!;
}
late final Uint8List signature = keccakUtf8(stringSignature);

/// Decodes the fields of this event from the event's [topics] and its [data]
/// payload.
Expand Down
Loading

0 comments on commit 42c7909

Please sign in to comment.