diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index 3fce297..d7fd112 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -30,6 +30,13 @@
+
+
+
+
+
+
+
@@ -44,6 +51,13 @@
+
+
+
+
+
+
+
@@ -86,6 +100,13 @@
+
+
+
+
+
+
+
@@ -93,6 +114,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -142,6 +177,13 @@
+
+
+
+
+
+
+
@@ -156,6 +198,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -184,6 +240,13 @@
+
+
+
+
+
+
+
@@ -254,6 +317,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -289,6 +366,13 @@
+
+
+
+
+
+
+
@@ -324,6 +408,13 @@
+
+
+
+
+
+
+
@@ -450,26 +541,35 @@
+
+
+
+
+
+
+
+
+
@@ -480,15 +580,19 @@
+
+
+
+
diff --git a/README.md b/README.md
index 004e847..9b9ee33 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,15 @@
-# sol_pay_gen
+# Solana Pay QR generator
QR code generator for Solana Pay
-## Getting Started
+## Checklist
-This project is a starting point for a Flutter application.
+- ✅ Validation
-A few resources to get you started if this is your first Flutter project:
-
-- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
-- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
-
-For help getting started with Flutter development, view the
-[online documentation](https://docs.flutter.dev/), which offers tutorials,
-samples, guidance on mobile development, and a full API reference.
+- 🚧 Download QR code
+- 🚧 SPL token picker
+- 🚧 Upload custom icon
+- 🚧 Adaptive layout
+- 🚧 How it works page
+- 🚧 Transaction request
+- 🚧 Transactions history
diff --git a/lib/data/base/text_value.dart b/lib/data/base/text_value.dart
new file mode 100644
index 0000000..019ae13
--- /dev/null
+++ b/lib/data/base/text_value.dart
@@ -0,0 +1,22 @@
+import '../error/input_error.dart';
+
+class TextValue {
+ TextValue({
+ this.text = "",
+ this.error,
+ });
+
+ final String text;
+ final InputError? error;
+
+ TextValue copyWith({
+ String? text,
+ InputError? error,
+ }) =>
+ TextValue(
+ text: text ?? this.text,
+ error: error ?? this.error,
+ );
+
+ bool isValid() => error == null;
+}
diff --git a/lib/data/error/input_error.dart b/lib/data/error/input_error.dart
new file mode 100644
index 0000000..b552079
--- /dev/null
+++ b/lib/data/error/input_error.dart
@@ -0,0 +1,28 @@
+abstract class InputError {
+ abstract String text;
+}
+
+class NotANumber extends InputError {
+ @override
+ String text = "Field must be a number";
+}
+
+class EmptyAmount extends InputError {
+ @override
+ String text = "Amount must be more than 0 if present";
+}
+
+class RequiredAmount extends InputError {
+ @override
+ String text = "Field is required";
+}
+
+class KeyNotBase58Encoded extends InputError {
+ @override
+ String text = "Invalid value, must be base58 encoded";
+}
+
+class KeyLengthInvalid extends InputError {
+ @override
+ String text = "Key value must be between 32 and 44 characters";
+}
diff --git a/lib/data/solana_pay_request.dart b/lib/data/solana_pay_request.dart
index fe21801..18fc5da 100644
--- a/lib/data/solana_pay_request.dart
+++ b/lib/data/solana_pay_request.dart
@@ -9,11 +9,11 @@ class SolanaPayRequest {
this.splToken,
});
- String address;
- String? label;
- String? splToken;
- String? message;
- String? amount;
- String? reference;
- String? memo;
+ final String address;
+ final String? label;
+ final String? splToken;
+ final String? message;
+ final String? amount;
+ final String? reference;
+ final String? memo;
}
diff --git a/lib/design/input/base_input.dart b/lib/design/input/base_input.dart
index 0f5b151..66e72a2 100644
--- a/lib/design/input/base_input.dart
+++ b/lib/design/input/base_input.dart
@@ -5,6 +5,7 @@ class BaseInput extends StatelessWidget {
final ValueChanged? _onChanged;
final TextInputType? _keyboardType;
final bool _focusable;
+ final String? _error;
const BaseInput({
super.key,
@@ -12,18 +13,20 @@ class BaseInput extends StatelessWidget {
required onChanged,
keyboardType,
focusable,
+ error,
}) : _labelText = labelText,
_onChanged = onChanged,
_keyboardType = keyboardType,
- _focusable = focusable ?? true;
+ _focusable = focusable ?? true,
+ _error = error;
@override
Widget build(BuildContext context) {
return TextField(
decoration: InputDecoration(
- border: const OutlineInputBorder(),
- labelText: _labelText,
- ),
+ border: const OutlineInputBorder(),
+ labelText: _labelText,
+ errorText: _error),
onChanged: _onChanged,
keyboardType: _keyboardType,
focusNode: _focusable ? null : AlwaysDisabledFocusNode(),
diff --git a/lib/di/blocs_providers.dart b/lib/di/blocs_providers.dart
index 8d369e4..bc486b8 100644
--- a/lib/di/blocs_providers.dart
+++ b/lib/di/blocs_providers.dart
@@ -2,24 +2,27 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sol_pay_gen/domain/generate_transfer_request_qr_use_case.dart';
import 'package:sol_pay_gen/feature/qr/bloc/qr_generator_cubit.dart';
+import 'package:sol_pay_gen/validator/keys_validator.dart';
+import 'package:sol_pay_gen/validator/number_validator.dart';
import '../data/transfer/transfer_request_repository.dart';
import '../feature/input/bloc/parameters_input_cubit.dart';
MultiBlocProvider getBlocProviders({required StatelessWidget child}) {
- ParametersInputCubit parametersInputCubit = ParametersInputCubit();
-
return MultiBlocProvider(
providers: [
BlocProvider(
- create: (_) => parametersInputCubit,
+ create: (BuildContext context) => ParametersInputCubit(
+ context.read(),
+ context.read(),
+ ),
),
BlocProvider(
create: (BuildContext context) => QrGeneratorCubit(
generateTransferRequestQrUseCase: GenerateTransferRequestQrUseCase(
context.read(),
),
- parametersInputCubit: parametersInputCubit,
+ parametersInputCubit: context.read(),
),
)
],
diff --git a/lib/di/repositories_providers.dart b/lib/di/repositories_providers.dart
index b34d30b..9de3dcd 100644
--- a/lib/di/repositories_providers.dart
+++ b/lib/di/repositories_providers.dart
@@ -1,5 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:sol_pay_gen/data/transfer/transfer_request_repository.dart';
+import 'package:sol_pay_gen/validator/keys_validator.dart';
+import 'package:sol_pay_gen/validator/number_validator.dart';
MultiRepositoryProvider getRepositoryProviders({
required MultiBlocProvider blocProviders,
@@ -7,8 +9,14 @@ MultiRepositoryProvider getRepositoryProviders({
return MultiRepositoryProvider(
providers: [
RepositoryProvider(
- create: (context) => DefaultTransferRequestRepository(),
+ create: (_) => DefaultTransferRequestRepository(),
),
+ RepositoryProvider(
+ create: (_) => DefaultNumberValidator(),
+ ),
+ RepositoryProvider(
+ create: (_) => DefaultKeysValidator(),
+ )
],
child: blocProviders,
);
diff --git a/lib/feature/input/bloc/parameters_input_cubit.dart b/lib/feature/input/bloc/parameters_input_cubit.dart
index f51c07a..e798ad4 100644
--- a/lib/feature/input/bloc/parameters_input_cubit.dart
+++ b/lib/feature/input/bloc/parameters_input_cubit.dart
@@ -1,13 +1,23 @@
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:sol_pay_gen/data/base/text_value.dart';
import 'package:sol_pay_gen/feature/input/bloc/parameters_input_state.dart';
+import '../../../data/error/input_error.dart';
+import '../../../validator/keys_validator.dart';
+import '../../../validator/number_validator.dart';
+
class ParametersInputCubit extends Cubit {
- ParametersInputCubit()
- : super(
- const ParametersInputState(
- address: "",
- amount: null,
- reference: null,
+ final NumberValidator _numberValidator;
+ final KeysValidator _keysValidator;
+
+ ParametersInputCubit(
+ this._numberValidator,
+ this._keysValidator,
+ ) : super(
+ ParametersInputState(
+ address: TextValue(),
+ amount: TextValue(),
+ reference: TextValue(),
memo: null,
message: null,
label: null,
@@ -16,11 +26,21 @@ class ParametersInputCubit extends Cubit {
);
void onAddressChange(String address) {
- emit(state.copyWith(address: address));
+ emit(state.copyWith(
+ address: TextValue(
+ text: address,
+ error: _validateAddress(address),
+ ),
+ ));
}
void onAmountChange(String amount) {
- emit(state.copyWith(amount: amount));
+ emit(state.copyWith(
+ amount: TextValue(
+ text: amount,
+ error: amount.isEmpty ? null : _numberValidator.validateAmount(amount),
+ ),
+ ));
}
void onLabelChange(String label) {
@@ -32,7 +52,12 @@ class ParametersInputCubit extends Cubit {
}
void onReferenceChange(String reference) {
- emit(state.copyWith(reference: reference));
+ emit(state.copyWith(
+ reference: TextValue(
+ text: reference,
+ error: reference.isEmpty ? null : _keysValidator.validateKey(reference),
+ ),
+ ));
}
void onSplTokenChange(String token) {
@@ -42,4 +67,18 @@ class ParametersInputCubit extends Cubit {
void onMemoChange(String memo) {
emit(state.copyWith(memo: memo));
}
+
+ void onValidate() {
+ emit(state.copyWith(
+ address: state.address.copyWith(
+ error: _validateAddress(state.address.text),
+ ),
+ ));
+ }
+
+ InputError? _validateAddress(String address) {
+ return address.isEmpty
+ ? RequiredAmount()
+ : _keysValidator.validateKey(address);
+ }
}
diff --git a/lib/feature/input/bloc/parameters_input_state.dart b/lib/feature/input/bloc/parameters_input_state.dart
index c8ad789..d9d5b7c 100644
--- a/lib/feature/input/bloc/parameters_input_state.dart
+++ b/lib/feature/input/bloc/parameters_input_state.dart
@@ -1,9 +1,10 @@
import 'package:equatable/equatable.dart';
+import 'package:sol_pay_gen/data/base/text_value.dart';
class ParametersInputState extends Equatable {
- final String address;
- final String? amount;
- final String? reference;
+ final TextValue address;
+ final TextValue amount;
+ final TextValue reference;
final String? label;
final String? message;
final String? memo;
@@ -30,11 +31,11 @@ class ParametersInputState extends Equatable {
];
ParametersInputState copyWith({
- String? address,
- String? amount,
+ TextValue? address,
+ TextValue? amount,
String? label,
String? message,
- String? reference,
+ TextValue? reference,
String? memo,
String? splTokenAddress,
}) =>
@@ -47,4 +48,7 @@ class ParametersInputState extends Equatable {
memo: memo ?? this.memo,
splTokenAddress: splTokenAddress ?? this.splTokenAddress,
);
+
+ bool isValid() =>
+ amount.isValid() && address.isValid() && reference.isValid();
}
diff --git a/lib/feature/input/parameters_input_screen.dart b/lib/feature/input/parameters_input_screen.dart
index 4ef71b2..55978f4 100644
--- a/lib/feature/input/parameters_input_screen.dart
+++ b/lib/feature/input/parameters_input_screen.dart
@@ -49,6 +49,7 @@ class InputBody extends StatelessWidget {
children: [
BaseInput(
labelText: 'Receive wallet address',
+ error: state.address.error?.text,
onChanged: (address) => context
.read()
.onAddressChange(address),
@@ -57,6 +58,7 @@ class InputBody extends StatelessWidget {
BaseInput(
labelText: 'Amount',
keyboardType: TextInputType.number,
+ error: state.amount.error?.text,
onChanged: (address) => context
.read()
.onAmountChange(address),
@@ -77,6 +79,7 @@ class InputBody extends StatelessWidget {
const Padding(padding: EdgeInsets.only(top: 16.0)),
BaseInput(
labelText: 'Reference',
+ error: state.reference.error?.text,
onChanged: (address) => context
.read()
.onReferenceChange(address),
@@ -101,7 +104,10 @@ class InputBody extends StatelessWidget {
Padding(
padding: const EdgeInsets.all(24.0),
child: MaterialButton(
- onPressed: () => context.read().onGenerate(),
+ onPressed: () {
+ context.read().onValidate();
+ context.read().onGenerate();
+ },
color: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0),
diff --git a/lib/feature/qr/bloc/qr_generator_cubit.dart b/lib/feature/qr/bloc/qr_generator_cubit.dart
index a5828f8..b6d84a9 100644
--- a/lib/feature/qr/bloc/qr_generator_cubit.dart
+++ b/lib/feature/qr/bloc/qr_generator_cubit.dart
@@ -19,18 +19,21 @@ class QrGeneratorCubit extends Cubit {
void onGenerate() async {
ParametersInputState inputState = _parametersInputCubit.state;
- SolanaPayRequest request = SolanaPayRequest(
- address: inputState.address,
- label: inputState.label,
- message: inputState.message,
- amount: inputState.amount,
- reference: inputState.reference,
- memo: inputState.memo,
- splToken: inputState.splTokenAddress,
- );
- String qrCode = _generateTransferRequestQrUseCase.execute(request);
+ if (inputState.isValid()) {
+ SolanaPayRequest request = SolanaPayRequest(
+ address: inputState.address.text,
+ label: inputState.label,
+ message: inputState.message,
+ amount: inputState.amount.text,
+ reference: inputState.reference.text,
+ memo: inputState.memo,
+ splToken: inputState.splTokenAddress,
+ );
- emit(QrCode(qrCode));
+ String qrCode = _generateTransferRequestQrUseCase.execute(request);
+
+ emit(QrCode(qrCode));
+ }
}
}
diff --git a/lib/validator/keys_validator.dart b/lib/validator/keys_validator.dart
new file mode 100644
index 0000000..ae7cc5f
--- /dev/null
+++ b/lib/validator/keys_validator.dart
@@ -0,0 +1,27 @@
+import 'package:solana/base58.dart';
+
+import '../data/error/input_error.dart';
+
+abstract class KeysValidator {
+ InputError? validateKey(String key);
+}
+
+class DefaultKeysValidator extends KeysValidator {
+ // https://docs.solana.com/cli/transfer-tokens#:~:text=The%20public%20key%20is%20a,from%2032%20to%2044%20characters.
+ static const minSolanaKeyLength = 32;
+ static const maxSolanaKeyLength = 44;
+
+ @override
+ InputError? validateKey(String key) {
+ if (key.length < minSolanaKeyLength || key.length > maxSolanaKeyLength) {
+ return KeyLengthInvalid();
+ } else {
+ try {
+ base58decode(key);
+ return null;
+ } on FormatException catch (_) {
+ return KeyNotBase58Encoded();
+ }
+ }
+ }
+}
diff --git a/lib/validator/number_validator.dart b/lib/validator/number_validator.dart
new file mode 100644
index 0000000..cfe8b25
--- /dev/null
+++ b/lib/validator/number_validator.dart
@@ -0,0 +1,21 @@
+import '../data/error/input_error.dart';
+
+abstract class NumberValidator {
+ InputError? validateAmount(String amount);
+}
+
+class DefaultNumberValidator extends NumberValidator {
+ @override
+ InputError? validateAmount(String amount) {
+ double? parsedAmount = double.tryParse(amount);
+
+ if (parsedAmount == null) {
+ return NotANumber();
+ }
+ if (parsedAmount == 0) {
+ return EmptyAmount();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/pubspec.lock b/pubspec.lock
index 903bafd..b0a9240 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -33,6 +33,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.11.0"
+ bip39:
+ dependency: transitive
+ description:
+ name: bip39
+ sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.6"
bloc:
dependency: transitive
description:
@@ -49,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.1"
+ borsh_annotation:
+ dependency: transitive
+ description:
+ name: borsh_annotation
+ sha256: "8c2cc353cb99a12b6c4f9c69e3640d2e18f5127628391658b9fceb96d4fec4d6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.1+4"
characters:
dependency: transitive
description:
@@ -97,6 +113,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.3"
+ cryptography:
+ dependency: transitive
+ description:
+ name: cryptography
+ sha256: df156c5109286340817d21fa7b62f9140f17915077127dd70f8bd7a2a0997a35
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
cupertino_icons:
dependency: "direct main"
description:
@@ -105,6 +129,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
+ decimal:
+ dependency: transitive
+ description:
+ name: decimal
+ sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.3"
+ ed25519_hd_key:
+ dependency: transitive
+ description:
+ name: ed25519_hd_key
+ sha256: c5c9f11a03f5789bf9dcd9ae88d641571c802640851f1cacdb13123f171b3a26
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
equatable:
dependency: "direct main"
description:
@@ -155,6 +195,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ freezed_annotation:
+ dependency: transitive
+ description:
+ name: freezed_annotation
+ sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
frontend_server_client:
dependency: transitive
description:
@@ -171,6 +219,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.2"
+ hex:
+ dependency: transitive
+ description:
+ name: hex
+ sha256: "4e7cd54e4b59ba026432a6be2dd9d96e4c5205725194997193bf871703b82c4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.13.6"
http_multi_server:
dependency: transitive
description:
@@ -203,6 +267,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.7"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.8.1"
lints:
dependency: transitive
description:
@@ -283,6 +355,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.8.3"
+ pinenacl:
+ dependency: transitive
+ description:
+ name: pinenacl
+ sha256: "3a5503637587d635647c93ea9a8fecf48a420cc7deebe6f1fc85c2a5637ab327"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
+ pointycastle:
+ dependency: transitive
+ description:
+ name: pointycastle
+ sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.7.3"
pool:
dependency: transitive
description:
@@ -323,6 +411,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
+ rational:
+ dependency: transitive
+ description:
+ name: rational
+ sha256: ba58e9e18df9abde280e8b10051e4bce85091e41e8e7e411b6cde2e738d357cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.2"
shelf:
dependency: transitive
description:
@@ -360,6 +456,14 @@ packages:
description: flutter
source: sdk
version: "0.0.99"
+ solana:
+ dependency: "direct main"
+ description:
+ name: solana
+ sha256: "24f6e87a28035ce7ce54a488f7476432fc5f9ffccd70547f514a2ec9c6251c87"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.30.0"
source_map_stack_trace:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 97068b4..5567ddc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -17,6 +17,7 @@ dependencies:
equatable: ^2.0.5
cupertino_icons: ^1.0.2
pretty_qr_code: ^2.0.3
+ solana: ^0.30.0
dev_dependencies:
flutter_test: