Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 177 additions & 0 deletions lib/pages/spark_names/sub_widgets/spark_name_details.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Expand All @@ -7,10 +9,12 @@ import '../../../providers/db/drift_provider.dart';
import '../../../providers/db/main_db_provider.dart';
import '../../../providers/global/wallets_provider.dart';
import '../../../themes/stack_colors.dart';
import '../../../utilities/show_loading.dart';
import '../../../utilities/text_styles.dart';
import '../../../utilities/util.dart';
import '../../../wallets/isar/providers/wallet_info_provider.dart';
import '../../../wallets/wallet/wallet_mixin_interfaces/spark_interface.dart';
import '../../../wl_gen/interfaces/lib_spark_interface.dart';
import '../../../widgets/background.dart';
import '../../../widgets/conditional_parent.dart';
import '../../../widgets/custom_buttons/app_bar_icon_button.dart';
Expand All @@ -19,6 +23,8 @@ import '../../../widgets/desktop/desktop_dialog_close_button.dart';
import '../../../widgets/desktop/primary_button.dart';
import '../../../widgets/dialogs/s_dialog.dart';
import '../../../widgets/rounded_container.dart';
import '../../../widgets/textfields/adaptive_text_field.dart';
import '../../signing/signing_view.dart';
import '../../wallet_view/transaction_views/transaction_details_view.dart'
as tvd;
import '../buy_spark_name_view.dart';
Expand Down Expand Up @@ -131,6 +137,32 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
}
}

Future<void> _proveOwnership() async {
if (_lock) return;
_lock = true;
try {
await showDialog<void>(
context: context,
builder: (context) => SDialog(
margin: Util.isDesktop
? null
: EdgeInsets.fromLTRB(
16,
16,
16,
16 + MediaQuery.viewInsetsOf(context).bottom,
),
child: _SparkNameOwnershipProofDialog(
walletId: widget.walletId,
address: name.address,
),
),
);
} finally {
_lock = false;
}
}

@override
void initState() {
super.initState();
Expand Down Expand Up @@ -435,6 +467,18 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
],
),
),
if (!_isViewOnlyWallet) ...[
const _Div(),
Padding(
padding: Util.isDesktop
? const EdgeInsets.all(16)
: EdgeInsets.zero,
child: PrimaryButton(
label: "Prove ownership",
onPressed: _proveOwnership,
),
),
],
],
);
},
Expand All @@ -444,6 +488,139 @@ class _SparkNameDetailsViewState extends ConsumerState<SparkNameDetailsView> {
}
}

class _SparkNameOwnershipProofDialog extends ConsumerStatefulWidget {
const _SparkNameOwnershipProofDialog({
required this.walletId,
required this.address,
});

final String walletId;
final String address;

@override
ConsumerState<_SparkNameOwnershipProofDialog> createState() =>
_SparkNameOwnershipProofDialogState();
}

class _SparkNameOwnershipProofDialogState
extends ConsumerState<_SparkNameOwnershipProofDialog> {
final _messageController = TextEditingController();

String _proof = "";
String? _messageError;
bool _isGenerating = false;

@override
void dispose() {
_messageController.dispose();
super.dispose();
}

void _onMessageChanged(String message) {
final maxLength = libSpark.maxSparkMessageLengthBytes;
final byteLength = utf8.encode(message).length;

setState(() {
_proof = "";
_messageError = byteLength > maxLength
? "Message exceeds $maxLength UTF-8 bytes"
: null;
});
}

Future<void> _generateProof() async {
if (_isGenerating ||
_messageController.text.isEmpty ||
_messageError != null) {
return;
}

setState(() => _isGenerating = true);
Exception? exception;
final proof = await showLoading(
whileFuture:
(ref.read(pWallets).getWallet(widget.walletId) as SparkInterface)
.createSparkAddressOwnershipProof(
address: widget.address,
message: _messageController.text,
),
context: context,
message: "Creating proof...",
onException: (e) => exception = e,
);

if (!mounted) return;
setState(() => _isGenerating = false);

if (exception != null) {
await showSignVerifyError(exception!, context: context);
} else if (proof != null) {
setState(() => _proof = proof);
}
}

@override
Widget build(BuildContext context) {
final canGenerate =
!_isGenerating &&
_messageController.text.isNotEmpty &&
_messageError == null;

return SizedBox(
width: Util.isDesktop ? 520 : null,
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"Prove ownership",
style: Util.isDesktop
? STextStyles.desktopH3(context)
: STextStyles.pageTitleH2(context),
),
if (Util.isDesktop) const DesktopDialogCloseButton(),
],
),
const SizedBox(height: 20),
Text("Message", style: STextStyles.w500_14(context)),
const SizedBox(height: 8),
AdaptiveTextField(
controller: _messageController,
minLines: 3,
maxLines: 5,
errorText: _messageError,
onChangedComprehensive: _onMessageChanged,
),
if (_proof.isNotEmpty) ...[
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Proof", style: STextStyles.w500_14(context)),
SimpleCopyButton(data: _proof),
],
),
const SizedBox(height: 8),
SelectableText(_proof, style: STextStyles.w500_14(context)),
],
const SizedBox(height: 24),
PrimaryButton(
label: "Create proof",
enabled: canGenerate,
onPressed: canGenerate ? _generateProof : null,
),
],
),
),
);
}
}

class _Div extends StatelessWidget {
const _Div({super.key});

Expand Down
90 changes: 87 additions & 3 deletions lib/wallets/wallet/wallet_mixin_interfaces/spark_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,25 @@ bool shouldSubtractSparkFeeFromAmount({

void initSparkLogging(Level level) => libSpark.initSparkLogging(level);

({String? proof, String? error}) _createSparkAddressOwnershipProof(
({String message, String privateKeyHex, int spendKeyIndex, int diversifier})
args,
) {
try {
return (
proof: libSpark.createSparkAddressOwnershipProof(
message: args.message,
privateKeyHex: args.privateKeyHex,
spendKeyIndex: args.spendKeyIndex,
diversifier: args.diversifier,
),
error: null,
);
} catch (e) {
return (proof: null, error: e.toString());
}
}

abstract class _SparkIsolate {
static Isolate? _isolate;
static SendPort? _sendPort;
Expand Down Expand Up @@ -189,6 +208,8 @@ Future<R> computeWithLibSparkLogging<M, R>(

mixin SparkInterface<T extends ElectrumXCurrencyInterface>
on Bip39HDWallet<T>, ElectrumXInterface<T> {
static const _sparkNameLookAheadCount = 100;

Address? _currentSparkAddress;

String? _viewKeyHex;
Expand Down Expand Up @@ -1872,12 +1893,10 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
// some look ahead
// TODO revisit this and clean up (track pre gen'd addresses instead of
// generating every time) arbitrary number of addresses
const lookAheadCount = 100;

// force unwrap optional should be fine here. If not then the
// eclosing function is being called somewhere it probably shouldn't be.
int diversifier = _currentSparkAddress!.derivationIndex;
final maxDiversifier = diversifier + lookAheadCount;
final maxDiversifier = diversifier + _sparkNameLookAheadCount;

while (diversifier < maxDiversifier) {
// change address check
Expand Down Expand Up @@ -2915,6 +2934,71 @@ mixin SparkInterface<T extends ElectrumXCurrencyInterface>
return txData;
}

Future<String> createSparkAddressOwnershipProof({
required String address,
required String message,
}) async {
if (isViewOnly) {
throw Exception(
"Cannot create an ownership proof from a view only wallet",
);
}

final messageLength = utf8.encode(message).length;
if (messageLength == 0 ||
messageLength > libSpark.maxSparkMessageLengthBytes) {
throw Exception(
"Message must contain between 1 and "
"${libSpark.maxSparkMessageLengthBytes} UTF-8 bytes",
);
}

Address? sparkAddress = await mainDB.getAddress(walletId, address);
if (sparkAddress == null) {
final currentDiversifier =
(await getCurrentReceivingSparkAddress())?.derivationIndex;
if (currentDiversifier != null) {
var diversifier = currentDiversifier;
final maxDiversifier = diversifier + _sparkNameLookAheadCount;
while (diversifier < maxDiversifier) {
if (diversifier == libSpark.sparkChange) {
diversifier++;
}
final candidate = await _generateSparkAddress(diversifier++);
if (candidate.value == address) {
sparkAddress = candidate;
break;
}
}
}
}
if (sparkAddress == null || sparkAddress.type != AddressType.spark) {
throw Exception("Spark address does not belong to this wallet");
}
if (sparkAddress.derivationIndex < 0) {
throw Exception("Spark address diversifier is unavailable");
}

final root = await getRootHDNode();
final privateKeyHex = root
.derivePath(sparkDerivationPath)
.privateKey
.data
.toHex;

final result =
await computeWithLibSparkLogging(_createSparkAddressOwnershipProof, (
message: message,
privateKeyHex: privateKeyHex,
spendKeyIndex: sparkIndex,
diversifier: sparkAddress.derivationIndex,
));
if (result.error != null) {
throw Exception(result.error);
}
return result.proof!;
}

@override
Future<void> updateBalance() async {
// call to super to update transparent balance
Expand Down
8 changes: 8 additions & 0 deletions lib/wl_gen/interfaces/lib_spark_interface.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ abstract class LibSparkInterface {
int get maxNameRegistrationLengthYears;
int get maxNameLength;
int get maxAdditionalInfoLengthBytes;
int get maxSparkMessageLengthBytes;
String get nameRegexString;
String get stage3CommunityFundAddressMainNet;
String get stage3CommunityFundAddressTestNet;
Expand Down Expand Up @@ -77,6 +78,13 @@ abstract class LibSparkInterface {
required bool ignoreProof,
});

String createSparkAddressOwnershipProof({
required String message,
required String privateKeyHex,
required int spendKeyIndex,
required int diversifier,
});

Uint8List getSparkNameCommitment({
required Uint8List serializedSparkNameData,
});
Expand Down
6 changes: 3 additions & 3 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1035,9 +1035,9 @@ packages:
dependency: "direct main"
description:
path: "."
ref: e017e62766908e9714c5309761183e8be8b37799
resolved-ref: e017e62766908e9714c5309761183e8be8b37799
url: "https://github.com/cypherstack/flutter_libsparkmobile.git"
ref: 9d628691842d50cf5c8b69aa3f9cb17d3afd55b0
resolved-ref: 9d628691842d50cf5c8b69aa3f9cb17d3afd55b0
url: "https://github.com/navidR/flutter_libsparkmobile.git"
source: git
version: "0.1.0"
flutter_lints:
Expand Down
4 changes: 2 additions & 2 deletions scripts/app_config/templates/pubspec.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ dependencies:
# %%ENABLE_FIRO%%
# flutter_libsparkmobile:
# git:
# url: https://github.com/cypherstack/flutter_libsparkmobile.git
# ref: e017e62766908e9714c5309761183e8be8b37799
# url: https://github.com/navidR/flutter_libsparkmobile.git
# ref: 9d628691842d50cf5c8b69aa3f9cb17d3afd55b0
# %%END_ENABLE_FIRO%%

# %%ENABLE_EPIC%%
Expand Down
Loading
Loading