Skip to content

Commit

Permalink
Merge pull request #762 from threefoldtech/development_add_bridge
Browse files Browse the repository at this point in the history
  • Loading branch information
zaelgohary authored Dec 12, 2024
2 parents f33d6af + 788fa07 commit 77cd9ab
Show file tree
Hide file tree
Showing 16 changed files with 740 additions and 17 deletions.
Binary file added app/assets/stellar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
Binary file removed app/assets/tft_icon.png
Binary file not shown.
2 changes: 1 addition & 1 deletion app/lib/apps/news/news_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class _NewsScreenState extends State<NewsScreen> {
Row(
children: [
Image.asset(
'assets/tft_icon.png',
'assets/tf_chain.png',
color: Theme.of(context).colorScheme.onSurface,
height: 20,
width: 20,
Expand Down
3 changes: 3 additions & 0 deletions app/lib/helpers/flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class Flags {
Globals().refreshBalance = int.parse(
(await Flags().getFlagValueByFeatureName('refresh-balance'))
.toString());

Globals().bridgeTFTAddress =
(await Flags().getFlagValueByFeatureName('bridge-address'))!;
}

Future<bool> hasFlagValueByFeatureName(String name) async {
Expand Down
1 change: 1 addition & 0 deletions app/lib/helpers/globals.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class Globals {
String chainUrl = '';
String gridproxyUrl = '';
String activationUrl = '';
String bridgeTFTAddress = '';
String relayUrl = '';
String termsAndConditionsUrl = '';
String newsUrl = '';
Expand Down
2 changes: 2 additions & 0 deletions app/lib/models/wallet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ enum WalletType { NATIVE, IMPORTED }

enum ChainType { Stellar, TFChain }

enum BridgeOperation { Withdraw, Deposit }

class Wallet {
Wallet({
required this.name,
Expand Down
304 changes: 304 additions & 0 deletions app/lib/screens/wallets/bridge.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:threebotlogin/helpers/globals.dart';
import 'package:threebotlogin/helpers/transaction_helpers.dart';
import 'package:threebotlogin/models/wallet.dart';
import 'package:threebotlogin/providers/wallets_provider.dart';
import 'package:threebotlogin/screens/wallets/contacts.dart';
import 'package:threebotlogin/services/stellar_service.dart';
import 'package:threebotlogin/widgets/wallets/bridge_confirmation.dart';
import 'package:threebotlogin/widgets/wallets/swap_transaction_widget.dart';
import 'package:validators/validators.dart';
import 'package:threebotlogin/services/stellar_service.dart' as Stellar;
import 'package:threebotlogin/services/tfchain_service.dart' as TFChain;

class WalletBridgeScreen extends StatefulWidget {
const WalletBridgeScreen(
{super.key, required this.wallet, required this.allWallets});
final Wallet wallet;
final List<Wallet> allWallets;

@override
State<WalletBridgeScreen> createState() => _WalletBridgeScreenState();
}

class _WalletBridgeScreenState extends State<WalletBridgeScreen> {
final fromController = TextEditingController();
final toController = TextEditingController();
final amountController = TextEditingController();
BridgeOperation transactionType = BridgeOperation.Withdraw;
bool isWithdraw = true;
String? toAddressError;
String? amountError;
bool reloadBalance = true;

@override
void initState() {
fromController.text = widget.wallet.tfchainAddress;
_reloadBalances();
super.initState();
}

@override
void dispose() {
fromController.dispose();
toController.dispose();
amountController.dispose();
reloadBalance = false;
super.dispose();
}

_loadTFChainBalance() async {
final chainUrl = Globals().chainUrl;
final balance =
await TFChain.getBalance(chainUrl, widget.wallet.tfchainAddress);
widget.wallet.tfchainBalance =
balance.toString() == '0.0' ? '0' : balance.toString();
setState(() {});
}

_loadStellarBalance() async {
widget.wallet.stellarBalance =
(await Stellar.getBalance(widget.wallet.stellarSecret)).toString();
setState(() {});
}

_reloadBalances() async {
if (!reloadBalance) return;
final refreshBalance = Globals().refreshBalance;
final WalletsNotifier walletRef =
ProviderScope.containerOf(context, listen: false)
.read(walletsNotifier.notifier);
final wallet = walletRef.getUpdatedWallet(widget.wallet.name)!;
widget.wallet.tfchainBalance = wallet.tfchainBalance;
widget.wallet.stellarBalance = wallet.stellarBalance;
setState(() {});
await Future.delayed(Duration(seconds: refreshBalance));
await _reloadBalances();
}

onTransactionChange(BridgeOperation type) {
transactionType = type;
isWithdraw = transactionType == BridgeOperation.Withdraw ? true : false;
fromController.text = isWithdraw
? widget.wallet.tfchainAddress
: widget.wallet.stellarAddress;
toController.text = '';
toAddressError = null;
amountError = null;
setState(() {});
}

Future<bool> _validateToAddress() async {
final toAddress = toController.text.trim();
toAddressError = null;
if (toAddress.isEmpty) {
toAddressError = "Address can't be empty";
return false;
}

if (!isWithdraw) {
if (toAddress.length != 48) {
toAddressError = 'Address length should be 48 characters';
return false;
}
final twinId = await TFChain.getTwinIdByQueryClient(toAddress);
if (twinId == 0) {
toAddressError = 'Address must have a twin ID';
return false;
}
}

if (isWithdraw) {
if (!isValidStellarAddress(toAddress)) {
toAddressError = 'Invaild Stellar address';
return false;
}
if (toAddress == Globals().bridgeTFTAddress) {
toAddressError = "Bridge address can't be the destination";
return false;
}
final toAddrBalance = await Stellar.getBalanceByAccountId(toAddress);
if (toAddrBalance == '-1') {
toAddressError = 'Address must be active and have TFT trustline';
return false;
}
}
return true;
}

bool _validateAmount() {
final amount = amountController.text.trim();
amountError = null;

if (amount.isEmpty) {
amountError = "Amount can't be empty";
return false;
}
if (!isFloat(amount)) {
amountError = 'Amount should have numeric values only';
return false;
}
if (double.parse(amount) < 2) {
amountError = "Amount can't be less than 2";
return false;
}
if (isWithdraw) {
if (double.parse(amount) > double.parse(widget.wallet.tfchainBalance)) {
amountError = "Amount shouldn't be more than the wallet balance";
return false;
}
}

if (!isWithdraw) {
if (double.parse(amount) > double.parse(widget.wallet.stellarBalance)) {
amountError = "Amount shouldn't be more than the wallet balance";
return false;
}
}
return true;
}

Future<bool> _validate() async {
final validAddress = await _validateToAddress();
final validAmount = _validateAmount();
setState(() {});
return validAddress && validAmount;
}

void _selectToAddress(String address) {
toController.text = address;
setState(() {});
}

@override
Widget build(BuildContext context) {
String balance = isWithdraw
? widget.wallet.tfchainBalance
: widget.wallet.stellarBalance;
final bool disableDeposit = widget.wallet.stellarBalance == '-1';

if (disableDeposit && !isWithdraw) {
onTransactionChange(BridgeOperation.Withdraw);
}

return Scaffold(
appBar: AppBar(title: const Text('Bridge')),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(children: [
const SizedBox(height: 10),
SwapTransactionWidget(
bridgeOperation: transactionType,
onTransactionChange: onTransactionChange,
disableDeposit: disableDeposit),
const SizedBox(height: 20),
ListTile(
title: TextField(
readOnly: true,
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
controller: fromController,
decoration: InputDecoration(
labelText: 'From (name: ${widget.wallet.name})',
)),
),
const SizedBox(height: 10),
ListTile(
title: TextField(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
controller: toController,
decoration: InputDecoration(
labelText: 'To',
errorText: toAddressError,
suffixIcon: IconButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ContactsScreen(
chainType: isWithdraw
? ChainType.Stellar
: ChainType.TFChain,
currentWalletAddress: fromController.text,
wallets: isWithdraw
? widget.allWallets
.where((w) =>
double.parse(w.stellarBalance) >=
0)
.toList()
: widget.allWallets,
onSelectToAddress: _selectToAddress),
));
},
icon: const Icon(Icons.person)))),
),
const SizedBox(height: 10),
ListTile(
title: TextField(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
keyboardType:
const TextInputType.numberWithOptions(decimal: true),
controller: amountController,
decoration: InputDecoration(
labelText: 'Amount (Balance: ${formatAmount(balance)})',
hintText: '100',
suffixText: 'TFT',
errorText: amountError)),
subtitle: Text('Max Fee: ${!isWithdraw ? 1.1 : 1.01} TFT'),
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
child: ElevatedButton(
onPressed: () async {
if (await _validate()) {
await _bridge_confirmation();
}
},
style: ElevatedButton.styleFrom(),
child: SizedBox(
width: double.infinity,
child: Text(
'Submit',
style: Theme.of(context).textTheme.titleLarge!.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
),
),
),
]),
),
),
);
}

_bridge_confirmation() async {
final memoText =
!isWithdraw ? await TFChain.getMemo(toController.text.trim()) : null;
showModalBottomSheet(
isScrollControlled: true,
useSafeArea: true,
isDismissible: false,
constraints: const BoxConstraints(maxWidth: double.infinity),
context: context,
builder: (ctx) => BridgeConfirmationWidget(
bridgeOperation: transactionType,
secret: isWithdraw
? widget.wallet.tfchainSecret
: widget.wallet.stellarSecret,
from: fromController.text.trim(),
to: toController.text.trim(),
amount: amountController.text.trim(),
memo: memoText,
reloadBalance:
isWithdraw ? _loadTFChainBalance : _loadStellarBalance,
));
}
}
8 changes: 4 additions & 4 deletions app/lib/screens/wallets/contacts.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import 'package:threebotlogin/services/contact_service.dart';
import 'package:threebotlogin/widgets/wallets/add_edit_contact.dart';
import 'package:threebotlogin/widgets/wallets/contacts_widget.dart';

class ContractsScreen extends StatefulWidget {
const ContractsScreen(
class ContactsScreen extends StatefulWidget {
const ContactsScreen(
{super.key,
required this.chainType,
required this.currentWalletAddress,
Expand All @@ -19,10 +19,10 @@ class ContractsScreen extends StatefulWidget {
final void Function(String address) onSelectToAddress;

@override
State<ContractsScreen> createState() => _ContractsScreenState();
State<ContactsScreen> createState() => _ContactsScreenState();
}

class _ContractsScreenState extends State<ContractsScreen> {
class _ContactsScreenState extends State<ContactsScreen> {
List<PkidContact> myWalletContacts = [];
List<PkidContact> myPkidContacts = [];

Expand Down
2 changes: 1 addition & 1 deletion app/lib/screens/wallets/send.dart
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ class _WalletSendScreenState extends State<WalletSendScreen> {
suffixIcon: IconButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ContractsScreen(
builder: (context) => ContactsScreen(
chainType: chainType,
currentWalletAddress:
fromController.text,
Expand Down
Loading

0 comments on commit 77cd9ab

Please sign in to comment.