This is a sample application for Ethereum based blockchain and Cosmos SDK blockchain which supports basic wallet management (importing mnemonics), and sending tokens.
- Install Starport
- Scaffold a blockchain using
starport app github.com/hello/planet
- Navigate to
planet
folder and runstarport serve
- After it is successfully built, you will see two mnemonics for Alice and Bob along with their wallet addresses in the terminal
- Install Ganache
- Launch Ganache and run Ethereum environment using Quick Start
- This will launch a local Ethereum network on your system
- The application connects to the project created in Infura Dashboard using Ropsten environment
- You can create your own project and pick up your project ID to be passed as
path
parameter in Flutter runtime arguments - Currently, a sample project ID is already connected to the app (BASE_ETH_URL)
- The Cosmos Hub Testnet uses publicly hosted links which will be passed as arguments while running the Flutter app
- Setup Flutter based on your operating system
- Make sure you follow all the steps above and have an all-green output of
flutter doctor
command. - Install and run Anroid Studio once
- (For mac) Install XCode from AppStore
- You can use Android Studio, IntelliJ Idea, or VSCode for Flutter development
- Install Flutter plugin in your chosen IDE
- Clone this repository using
git clone https://github.com/tendermint/flutter.git
- To run it on your
localhost
environment, you will just have to runmain.dart
file in your project. That can be done directly by clicking on the play button in your IDE, or by runningflutter run
from the root of your project. This will run the app on any device or simulator connected to your system. - To run it on the Cosmos Hub Testnet,
run
flutter run --dart-define=BASE_LCD_URL=api.testnet.cosmos.network --dart-define=PORT=443 --dart-define=BASE_ETH_URL=https://ropsten.infura.io/v3/96ac5dcb92d545b6a7ffc3d8af21fde0
, or add additional arguments--dart-define=BASE_LCD_URL=api.testnet.cosmos.network --dart-define=PORT=443 --dart-define=BASE_ETH_URL=https://ropsten.infura.io/v3/96ac5dcb92d545b6a7ffc3d8af21fde0
inside your IDE's configuration formain.dart
and click on play button.
The app follows clean architecture principles and is divided into 4 modules:
UI Module contains flutter widgets, animations, Routes, Pages and all the code that is taking care of how things look like. This module contains no logic at all, it only takes care of HOW things look like.
Home for Presenter
and PresentationModel
classes.
Presenters decide WHEN to display things. They take care of triggering business logic from the domain
module as
well as updating the Presentation Model with relevant data that is then used by the UI.
PresentationModel is storing the data and exposes it to the UI trough the ViewModel
interfaces. This makes sure the UI
is able to access the data in read-only manner and all internals relevant to presentation is not visible.
UseCase
, Repository
and entity classes.
Contains business logic of the application. It decides WHAT should be done upon user's interaction with the app. All
the logic is encapsulated into the UseCase
classes that communicate with the outer world trough Repositories.
Repository interfaces are specified inside the domain
module and are implemented by the data
module. All the data
that domain
operates on is encapsulated into domain entity classes.
Takes care of communicating with outer world and interact with third party libraries. Here we specify the REST Apis interactions, database access, SharedPrefs or external sensor access. All the data is then translated to domain entities that are agnostic of the libraries and technologies used in order to access them.
Below you can find a diagram showing the flow of control for a basic scenario of a login page:
Rule | Explanation |
---|---|
Add proper prefix to the PR title (feat: , fix: , chore: , refactor: ) |
Link |
review your own PR first | reading trough your own pr helps spot obvious errors and it saves time for the reviewer |
Make sure all CI checks pass | CI is meant to catch formatting and lint errors, make use of that and don't force others to do the machine's job :) |
Run the app and test it yourself | Before reviewing a PR or when issuing your own code, make sure to run the app and test it making sure the code doesn't break anything and works correctly |
Don't be afraid to ask questions | Code review is meant not only to find errors in someone's code, but it's also about making sure you understand the code and know what is going on. Don't be afraid to ask questions, if anything looks unclear to you, don't assume that it's your fault, ask a question! :) |
Rule | Explanation |
---|---|
Use trailing commas for method/constructor parameters and definitions | This way each param is in a separate line and adding new params is much easier to read in PRs |
Prefer named parameters | Whenever using more than one param, consider using named parameters, i.e: Bad:getBalances(true,"1283184") , Bad:getBalances(id: "1283184", refresh: true) |
- Performs business logic, communicates with outer world trough repositories
- should contain single public
execute()
method optionally with runtime arguments that performs business logic - all compile-time dependencies should be injected trough constructor
- never use json classes in usecases, always rely communication with APIs trough repositories
- Should be specified as an interface in the
domain/repositories
package, i.e:UserRepository
- Implementation should be placed in the
data/{tech}
package, i.e:RestApiUserRepository
in thedata/rest/
package - It's job is abstract the communication with outer world (APIs, Blockchains, databases, shared prefs).
- Translates the domain models into data models and vice versa. so that use cases can depend solely on the domain entities
- Repository methods should always return the response in the form of
Future<Either<Failure,Result>>
type - the
Result
part needs to be a domain entity or a primitive type, never a json or library-specific class!- Good:
String
,User
,int
- Bad:
UserJson
,FirebaseUser
,FirebaseAuthResponse
- Good:
- the
Failure
should be a class specified in domain entities subfolder (see Failures)
- should be stored in
domain/model/failures
- A specific domain entity that denotes a failure of certain action. Failure should contain a:
type
cause
String toString()
methodDisplayableFailure displayableFailure()
method (useful to be able to display a error dialog stating the cause of a failure)
enum VerifyPasscodeFailureType {
Unknown,
ValidationError,
}
class SomeFailure {
// ignore: avoid_field_initializers_in_const_classes
const SomeFailure.unknown([this.cause]) : type = SomeFailureType.Unknown;
const SomeFailure.validationError(PasscodeValidationFailure fail)
: cause = fail,
type = SomeFailureType.ValidationError;
final SomeFailureType type;
final dynamic cause;
DisplayableFailure displayableFailure() {
switch (type) {
case SomeFailureType.Unknown:
return DisplayableFailure(
title: strings.failureTitle,
message: strings.failureMessage,
);
}
}
@override
String toString() {
return 'SomeFailure{type: $type, cause: $cause}';
}
}
- should be stored in
domain/model
- domain entities should extend from
Equatable
- all fields should be final (so that we encourage immutability)
- all fields should be non-nullable unless the null value makes sense in the given context
class User extends Equatable {
final String username;
const User({
required this.username,
required this.firstName,
required this.lastName,
});
@override
List<Object> get props =>
[
username,
firstName,
lastName,
];
}
- reacts to user interaction, (i.e: all onTap methods are being forwarded to presenter)
- calls use cases
- updates presentation model with new data
- never accesses initialParams from PresentationModel directly, but through the getters in presentationModel
- does not store any data, all the data that it requires is being held inside the presentationModel
- presenter never needs a
BuildContext
, it's always agnostic of the fact it's used in flutter thus should never rely on flutter classes
- interface that exposes data from presentation model to pages
- contains only getters, views are not supposed to mutate any state directly
- should not expose data that is internal for presenters and should not be used by pages directly
- stores data used by presenter and pages
- uses mobx to make all the fields observable
*PresentationModelBase
is a base class we use to store mobx boilerplate code- accepts InitialParams as constructor param
- never accesses initialParams directly
- routes all the user interaction to presenter, i.e:
InkWell(onTap: () => presenter.onTapLogin()
- uses
Observer
widget to listen to changes inViewModel
- accesses data to display from
ViewModel
only - divides the UI into smaller widgets, extracted to a separate files
- all fields in
*Json
classes should be nullable - all fields should be final
- should contain
toDomain()
method that translates the json to domain entity
class UserJson {
UserJson({
required this.username,
required this.firstName,
required this.lastName,
});
factory UserJson.fromJson(Map<String, dynamic> json) =>
UserJson(
username: json['username'] as String? ?? '',
firstName: json['first_name'] as String? ?? '',
lastName: json['last_name'] as String? ?? '',
);
final String? username;
final String? firstName;
final String? lastName;
Balance toBalanceDomain() =>
Balance(
username: username ?? "",
firstName: firstName ?? "",
lastName: lastName ?? "",
);
}