Merge pull request #334 from cypherstack/paynyms

Paynyms
This commit is contained in:
Diego Salazar 2023-02-01 16:25:31 -07:00 committed by GitHub
commit bd05d6dddf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 316 additions and 162 deletions

View file

@ -1,5 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/exceptions/main_db/main_db_exception.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/utilities/stack_file_system.dart';
import 'package:tuple/tuple.dart';
@ -40,64 +41,101 @@ class MainDB {
String walletId) =>
isar.addresses.where().walletIdEqualTo(walletId);
Future<void> putAddress(Address address) => isar.writeTxn(() async {
await isar.addresses.put(address);
Future<int> putAddress(Address address) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.put(address);
});
} catch (e) {
throw MainDBException("failed putAddress: $address", e);
}
}
Future<void> putAddresses(List<Address> addresses) => isar.writeTxn(() async {
await isar.addresses.putAll(addresses);
Future<List<int>> putAddresses(List<Address> addresses) async {
try {
return await isar.writeTxn(() async {
return await isar.addresses.putAll(addresses);
});
} catch (e) {
throw MainDBException("failed putAddresses: $addresses", e);
}
}
Future<void> updateOrPutAddresses(List<Address> addresses) async {
Future<List<int>> updateOrPutAddresses(List<Address> addresses) async {
try {
List<int> ids = [];
await isar.writeTxn(() async {
for (final address in addresses) {
final storedAddress = await isar.addresses
.getByValueWalletId(address.value, address.walletId);
int id;
if (storedAddress == null) {
await isar.addresses.put(address);
id = await isar.addresses.put(address);
} else {
address.id = storedAddress.id;
await storedAddress.transactions.load();
final txns = storedAddress.transactions.toList();
await isar.addresses.delete(storedAddress.id);
await isar.addresses.put(address);
id = await isar.addresses.put(address);
address.transactions.addAll(txns);
await address.transactions.save();
}
ids.add(id);
}
});
return ids;
} catch (e) {
throw MainDBException("failed updateOrPutAddresses: $addresses", e);
}
}
Future<Address?> getAddress(String walletId, String address) async {
return isar.addresses.getByValueWalletId(address, walletId);
}
Future<void> updateAddress(Address oldAddress, Address newAddress) =>
isar.writeTxn(() async {
Future<int> updateAddress(Address oldAddress, Address newAddress) async {
try {
return await isar.writeTxn(() async {
newAddress.id = oldAddress.id;
await oldAddress.transactions.load();
final txns = oldAddress.transactions.toList();
await isar.addresses.delete(oldAddress.id);
await isar.addresses.put(newAddress);
final id = await isar.addresses.put(newAddress);
newAddress.transactions.addAll(txns);
await newAddress.transactions.save();
return id;
});
} catch (e) {
throw MainDBException(
"failed updateAddress: from=$oldAddress to=$newAddress", e);
}
}
// transactions
QueryBuilder<Transaction, Transaction, QAfterWhereClause> getTransactions(
String walletId) =>
isar.transactions.where().walletIdEqualTo(walletId);
Future<void> putTransaction(Transaction transaction) =>
isar.writeTxn(() async {
await isar.transactions.put(transaction);
Future<int> putTransaction(Transaction transaction) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.put(transaction);
});
} catch (e) {
throw MainDBException("failed putTransaction: $transaction", e);
}
}
Future<void> putTransactions(List<Transaction> transactions) =>
isar.writeTxn(() async {
await isar.transactions.putAll(transactions);
Future<List<int>> putTransactions(List<Transaction> transactions) async {
try {
return await isar.writeTxn(() async {
return await isar.transactions.putAll(transactions);
});
} catch (e) {
throw MainDBException("failed putTransactions: $transactions", e);
}
}
Future<Transaction?> getTransaction(String walletId, String txid) async {
return isar.transactions.getByTxidWalletId(txid, walletId);
@ -214,7 +252,9 @@ class MainDB {
Future<void> addNewTransactionData(
List<Tuple4<Transaction, List<Output>, List<Input>, Address?>>
transactionsData,
String walletId) async {
String walletId,
) async {
try {
await isar.writeTxn(() async {
for (final data in transactionsData) {
final tx = data.item1;
@ -262,5 +302,8 @@ class MainDB {
}
}
});
} catch (e) {
throw MainDBException("failed addNewTransactionData", e);
}
}
}

View file

@ -0,0 +1,12 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
class MainDBException extends SWException {
MainDBException(super.message, this.originalError);
final Object originalError;
@override
String toString() {
return "$message: originalError=$originalError";
}
}

View file

@ -176,7 +176,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,
@ -186,7 +186,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
Text(
widget.accountLite.nymName,
style: STextStyles.w600_12(context),
style: STextStyles.w600_14(context),
),
FutureBuilder(
future:
@ -204,7 +204,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
),
Text(
"Connected",
style: STextStyles.w500_10(context)
style: STextStyles.w500_12(context)
.copyWith(
color: Theme.of(context)
.extension<StackColors>()!
@ -230,33 +230,33 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
if (snapshot.data!) {
return PrimaryButton(
label: "Send",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circleArrowUpRight,
width: 10,
height: 10,
width: 14,
height: 14,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
iconSpacing: 8,
width: 100,
onPressed: _onSend,
);
} else {
return PrimaryButton(
label: "Connect",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
icon: SvgPicture.asset(
Assets.svg.circlePlusFilled,
width: 10,
height: 10,
width: 13,
height: 13,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextPrimary,
),
iconSpacing: 4,
width: 86,
iconSpacing: 8,
width: 128,
onPressed: _onConnectPressed,
);
}
@ -291,6 +291,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.warningForeground,
fontSize: 12,
),
),
),
@ -321,7 +322,9 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
children: [
Text(
"PayNym address",
style: STextStyles.infoSmall(context),
style: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -332,6 +335,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
@ -346,7 +350,7 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 86,
size: 100,
data: widget.accountLite.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,
@ -375,16 +379,16 @@ class _PaynymDetailsPopupState extends ConsumerState<PaynymDetailsPopup> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
Assets.svg.copy,
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: () async {
await Clipboard.setData(
ClipboardData(

View file

@ -56,7 +56,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
PayNymBot(
paymentCodeString: paynymAccount.codes.first.code,
size: isDesktop ? 56 : 32,
size: isDesktop ? 56 : 36,
),
const SizedBox(
width: 12,
@ -65,7 +65,7 @@ class PaynymQrPopup extends StatelessWidget {
paynymAccount.nymName,
style: isDesktop
? STextStyles.w500_24(context)
: STextStyles.w600_12(context),
: STextStyles.w600_14(context),
),
],
),
@ -87,7 +87,7 @@ class PaynymQrPopup extends StatelessWidget {
children: [
Expanded(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 107),
constraints: const BoxConstraints(minHeight: 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -100,7 +100,9 @@ class PaynymQrPopup extends StatelessWidget {
.extension<StackColors>()!
.textSubtitle1,
)
: STextStyles.infoSmall(context),
: STextStyles.infoSmall(context).copyWith(
fontSize: 12,
),
),
const SizedBox(
height: 6,
@ -113,6 +115,7 @@ class PaynymQrPopup extends StatelessWidget {
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
fontSize: 12,
),
),
const SizedBox(
@ -120,7 +123,7 @@ class PaynymQrPopup extends StatelessWidget {
),
CustomTextButton(
text: "Copy",
textSize: isDesktop ? 18 : 10,
textSize: isDesktop ? 18 : 14,
onTap: () async {
await Clipboard.setData(
ClipboardData(
@ -146,7 +149,7 @@ class PaynymQrPopup extends StatelessWidget {
),
QrImage(
padding: const EdgeInsets.all(0),
size: 107,
size: 130,
data: paynymAccount.codes.first.code,
foregroundColor:
Theme.of(context).extension<StackColors>()!.textDark,

View file

@ -303,7 +303,9 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
.code,
12,
5),
style: STextStyles.label(context),
style: STextStyles.label(context).copyWith(
fontSize: 14,
),
),
const SizedBox(
height: 11,
@ -313,11 +315,11 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Copy",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: CopyIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
@ -350,11 +352,11 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Share",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: ShareIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
@ -387,11 +389,11 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
Expanded(
child: SecondaryButton(
label: "Address",
buttonHeight: ButtonHeight.l,
iconSpacing: 4,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: QrCodeIcon(
width: 10,
height: 10,
width: 12,
height: 12,
color: Theme.of(context)
.extension<StackColors>()!
.textDark,
@ -554,7 +556,7 @@ class _PaynymHomeViewState extends ConsumerState<PaynymHomeView> {
child: child,
),
child: SizedBox(
height: isDesktop ? 56 : 40,
height: isDesktop ? 56 : 48,
width: isDesktop ? 490 : null,
child: Toggle(
onColor: Theme.of(context).extension<StackColors>()!.popupBG,

View file

@ -171,7 +171,7 @@ class _PaynymDetailsPopupState extends ConsumerState<DesktopPaynymDetails> {
children: [
PayNymBot(
paymentCodeString: widget.accountLite.code,
size: 32,
size: 36,
),
const SizedBox(
width: 12,

View file

@ -37,7 +37,7 @@ class _PaynymCardState extends State<PaynymCard> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.paymentCodeString,
),
const SizedBox(
@ -56,7 +56,7 @@ class _PaynymCardState extends State<PaynymCard> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -65,7 +65,7 @@ class _PaynymCardState extends State<PaynymCard> {
Format.shorten(widget.paymentCodeString, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -77,7 +77,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
child: Row(
children: [
PayNymBot(
size: 32,
size: 36,
paymentCodeString: widget.accountLite.code,
),
const SizedBox(
@ -96,7 +96,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
.extension<StackColors>()!
.textFieldActiveText,
)
: STextStyles.w500_12(context),
: STextStyles.w500_14(context),
),
const SizedBox(
height: 2,
@ -105,7 +105,7 @@ class _PaynymCardButtonState extends ConsumerState<PaynymCardButton> {
Format.shorten(widget.accountLite.code, 12, 5),
style: isDesktop
? STextStyles.desktopTextExtraExtraSmall(context)
: STextStyles.w500_12(context).copyWith(
: STextStyles.w500_14(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.textSubtitle1,

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:bech32/bech32.dart';
import 'package:bip32/bip32.dart' as bip32;
@ -980,6 +981,7 @@ class BitcoinWallet extends CoinServiceAPI
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.3, walletId));
await _checkCurrentReceivingAddressesForTransactions();
GlobalEventBus.instance.fire(RefreshPercentChangedEvent(0.4, walletId));
await checkAllCurrentReceivingPaynymAddressesForTransactions();
final fetchFuture = _refreshTransactions();
@ -2372,27 +2374,38 @@ class BitcoinWallet extends CoinServiceAPI
return transactionObject;
}
final int vSizeForOneOutput = (await buildTransaction(
final int vSizeForOneOutput;
try {
vSizeForOneOutput = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [_recipientAddress],
satoshiAmounts: [satoshisBeingUsed - 1],
))["vSize"] as int;
final int vSizeForTwoOutPuts = (await buildTransaction(
} catch (e) {
Logging.instance.log("vSizeForOneOutput: $e", level: LogLevel.Error);
rethrow;
}
final int vSizeForTwoOutPuts;
try {
vSizeForTwoOutPuts = (await buildTransaction(
utxosToUse: utxoObjectsToUse,
utxoSigningData: utxoSigningData,
recipients: [
_recipientAddress,
await _getCurrentAddressForChain(1, DerivePathTypeExt.primaryFor(coin)),
await _getCurrentAddressForChain(
1, DerivePathTypeExt.primaryFor(coin)),
],
satoshiAmounts: [
satoshiAmountToSend,
// this can cause a problem where the output value is negative so commenting out for now
// satoshisBeingUsed - satoshiAmountToSend - 1
// and using dust limit instead
DUST_LIMIT,
], // dust limit is the minimum amount a change output should be
max(0, satoshisBeingUsed - satoshiAmountToSend - 1),
],
))["vSize"] as int;
} catch (e) {
Logging.instance.log("vSizeForTwoOutPuts: $e", level: LogLevel.Error);
rethrow;
}
// Assume 1 output, only for recipient and no change
final feeForOneOutput = estimateTxFee(

View file

@ -148,15 +148,13 @@ mixin PaynymWalletInterface {
btc_dart.NetworkType get networkType => _network;
Future<Address> currentReceivingPaynymAddress(PaymentCode sender) async {
final key = await lookupKey(sender.toString());
final keys = await lookupKey(sender.toString());
final address = await _db
.getAddresses(_walletId)
.filter()
.subTypeEqualTo(AddressSubType.paynymReceive)
.and()
.otherDataEqualTo(key)
.and()
.otherDataIsNotNull()
.anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e))
.sortByDerivationIndexDesc()
.findFirst();
@ -331,15 +329,13 @@ mixin PaynymWalletInterface {
const maxCount = 2147483647;
for (int i = startIndex; i < maxCount; i++) {
final key = await lookupKey(pCode.toString());
final keys = await lookupKey(pCode.toString());
final address = await _db
.getAddresses(_walletId)
.filter()
.subTypeEqualTo(AddressSubType.paynymSend)
.and()
.otherDataEqualTo(key)
.and()
.otherDataIsNotNull()
.anyOf<String, Address>(keys, (q, String e) => q.otherDataEqualTo(e))
.and()
.derivationIndexEqualTo(i)
.findFirst();
@ -1215,16 +1211,17 @@ mixin PaynymWalletInterface {
}
/// look up a key that corresponds to a payment code string
Future<String?> lookupKey(String paymentCodeString) async {
Future<List<String>> lookupKey(String paymentCodeString) async {
final keys =
(await _secureStorage.keys).where((e) => e.startsWith(kPCodeKeyPrefix));
final List<String> result = [];
for (final key in keys) {
final value = await _secureStorage.read(key: key);
if (value == paymentCodeString) {
return key;
result.add(key);
}
}
return null;
return result;
}
/// fetch a payment code string

View file

@ -13,31 +13,31 @@ class STextStyles {
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark3,
fontWeight: FontWeight.w500,
fontSize: 12,
fontSize: 14,
);
}
}
@ -932,6 +932,76 @@ class STextStyles {
}
}
static TextStyle w600_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w600,
fontSize: 14,
);
}
}
static TextStyle w500_14(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oceanBreeze:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.dark:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.oledBlack:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
case ThemeType.fruitSorbet:
return GoogleFonts.inter(
color: _theme(context).textDark,
fontWeight: FontWeight.w500,
fontSize: 14,
);
}
}
static TextStyle w500_12(BuildContext context) {
switch (_theme(context).themeType) {
case ThemeType.light:

View file

@ -272,8 +272,8 @@ class _PaynymFollowToggleButtonState
switch (widget.style) {
case PaynymFollowToggleButtonStyle.primary:
return PrimaryButton(
width: isDesktop ? 120 : 84,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.l,
width: isDesktop ? 120 : 100,
buttonHeight: isDesktop ? ButtonHeight.s : ButtonHeight.xl,
label: isFollowing ? "Unfollow" : "Follow",
onPressed: _onPressed,
);
@ -281,15 +281,15 @@ class _PaynymFollowToggleButtonState
case PaynymFollowToggleButtonStyle.detailsPopup:
return SecondaryButton(
label: isFollowing ? "Unfollow" : "Follow",
buttonHeight: ButtonHeight.l,
buttonHeight: ButtonHeight.xl,
iconSpacing: 8,
icon: SvgPicture.asset(
isFollowing ? Assets.svg.userMinus : Assets.svg.userPlus,
width: 10,
height: 10,
width: 16,
height: 16,
color:
Theme.of(context).extension<StackColors>()!.buttonTextSecondary,
),
iconSpacing: 4,
onPressed: _onPressed,
);

View file

@ -78,6 +78,16 @@ class SecondaryButton extends StatelessWidget {
.buttonTextSecondaryDisabled,
);
}
if (buttonHeight == ButtonHeight.xl) {
return STextStyles.button(context).copyWith(
fontSize: 14,
color: enabled
? Theme.of(context).extension<StackColors>()!.buttonTextSecondary
: Theme.of(context)
.extension<StackColors>()!
.buttonTextSecondaryDisabled,
);
}
return STextStyles.button(context).copyWith(
color: enabled
? Theme.of(context).extension<StackColors>()!.buttonTextSecondary