Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:convert';
import 'package:komodo_defi_rpc_methods/src/internal_exports.dart';

/// Generic response details wrapper for task status responses
class ResponseDetails<T, R extends GeneralErrorResponse> {
class ResponseDetails<T, R extends GeneralErrorResponse, D extends Object> {
ResponseDetails({required this.data, required this.error, this.description})
: assert(
[data, error, description].where((e) => e != null).length == 1,
Expand All @@ -14,7 +14,8 @@ class ResponseDetails<T, R extends GeneralErrorResponse> {
final R? error;

// Usually only non-null for in-progress tasks
final String? description;
/// Additional status information for in-progress tasks
final D? description;

void get throwIfError {
if (error != null) {
Expand All @@ -28,7 +29,9 @@ class ResponseDetails<T, R extends GeneralErrorResponse> {
return {
if (data != null) 'data': jsonEncode(data),
if (error != null) 'error': jsonEncode(error),
if (description != null) 'description': description,
if (description != null)
'description':
description is String ? description : jsonEncode(description),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ class AccountBalanceStatusResponse extends BaseResponse {
mmrpc: json.value<String>('mmrpc'),
status: status!,
// details: status == 'Ok' ? AccountBalanceInfo.fromJson(details) : details,
details: ResponseDetails<AccountBalanceInfo, GeneralErrorResponse>(
details: ResponseDetails<
AccountBalanceInfo,
GeneralErrorResponse,
String
>(
data:
status == SyncStatusEnum.success
? AccountBalanceInfo.fromJson(result.value<JsonMap>('details'))
Expand All @@ -106,7 +110,8 @@ class AccountBalanceStatusResponse extends BaseResponse {
}

final SyncStatusEnum status;
final ResponseDetails<AccountBalanceInfo, GeneralErrorResponse> details;
final ResponseDetails<AccountBalanceInfo, GeneralErrorResponse, String>
details;

@override
JsonMap toJson() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,32 +99,44 @@ class GetNewAddressTaskStatusResponse extends BaseResponse {
);
}

final detailsJson = result['details'];
Object? description;
NewAddressInfo? data;
GeneralErrorResponse? error;

if (status == SyncStatusEnum.success) {
data = NewAddressInfo.fromJson(
(detailsJson as JsonMap).value<JsonMap>('new_address'),
);
} else if (status == SyncStatusEnum.error) {
error = GeneralErrorResponse.parse(detailsJson as JsonMap);
} else if (status == SyncStatusEnum.inProgress) {
if (detailsJson is String) {
description = detailsJson;
} else if (detailsJson is JsonMap) {
if (detailsJson.containsKey('ConfirmAddress')) {
description = ConfirmAddressDetails.fromJson(
detailsJson.value<JsonMap>('ConfirmAddress'),
);
} else {
description = detailsJson;
}
}
}

return GetNewAddressTaskStatusResponse(
mmrpc: json.value<String>('mmrpc'),
status: status,
details: ResponseDetails<NewAddressInfo, GeneralErrorResponse>(
data:
status == SyncStatusEnum.success
? NewAddressInfo.fromJson(
result
.value<JsonMap>('details')
.value<JsonMap>('new_address'),
)
: null,
error:
status == SyncStatusEnum.error
? GeneralErrorResponse.parse(result.value<JsonMap>('details'))
: null,
description:
status == SyncStatusEnum.inProgress
? result.value<String>('details')
: null,
details: ResponseDetails<NewAddressInfo, GeneralErrorResponse, Object>(
data: data,
error: error,
description: description,
),
);
}

final SyncStatusEnum status;
final ResponseDetails<NewAddressInfo, GeneralErrorResponse> details;
final ResponseDetails<NewAddressInfo, GeneralErrorResponse, Object> details;

@override
JsonMap toJson() {
Expand All @@ -133,6 +145,42 @@ class GetNewAddressTaskStatusResponse extends BaseResponse {
'result': {'status': status, 'details': details.toJson()},
};
}

/// Convert this RPC response into a [NewAddressState].
NewAddressState toState(int taskId) {
switch (status) {
case SyncStatusEnum.success:
final addr = details.data!;
return NewAddressState(
status: NewAddressStatus.completed,
address: PubkeyInfo(
address: addr.address,
derivationPath: addr.derivationPath,
chain: addr.chain,
balance: addr.balance,
),
taskId: taskId,
);
case SyncStatusEnum.error:
return NewAddressState(
status: NewAddressStatus.error,
error: details.error?.error ?? 'Unknown error',
taskId: taskId,
);
case SyncStatusEnum.inProgress:
return NewAddressState.fromInProgressDescription(
details.description,
taskId,
);
case SyncStatusEnum.notStarted:
// This case should not happen, but if it does, we treat it as an error
return NewAddressState(
status: NewAddressStatus.error,
error: 'Task not started',
taskId: taskId,
);
}
}
}

// Cancel Request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,20 @@ class ContextPrivKeyHDWalletStrategy extends PubkeyStrategy with HDWalletMixin {
balance: newAddress.balance,
);
}

@override
Stream<NewAddressState> getNewAddressStream(
AssetId assetId,
ApiClient client,
) async* {
try {
yield const NewAddressState(status: NewAddressStatus.processing);
final info = await getNewAddress(assetId, client);
yield NewAddressState.completed(info);
} catch (e) {
yield NewAddressState.error('Failed to generate address: $e');
}
}
}

/// HD wallet strategy for Trezor wallets
Expand All @@ -131,10 +145,44 @@ class TrezorHDWalletStrategy extends PubkeyStrategy with HDWalletMixin {
);
}

@override
Stream<NewAddressState> getNewAddressStream(
AssetId assetId,
ApiClient client, {
Duration pollingInterval = const Duration(milliseconds: 200),
}) async* {
final initResponse = await client.rpc.hdWallet.getNewAddressTaskInit(
coin: assetId.id,
accountId: 0,
chain: 'External',
gapLimit: _gapLimit,
);

var finished = false;
while (!finished) {
final status = await client.rpc.hdWallet.getNewAddressTaskStatus(
taskId: initResponse.taskId,
forgetIfFinished: false,
);

final state = status.toState(initResponse.taskId);
yield state;

if (state.status == NewAddressStatus.completed ||
state.status == NewAddressStatus.error ||
state.status == NewAddressStatus.cancelled) {
finished = true;
} else {
await Future<void>.delayed(pollingInterval);
}
}
}

Future<NewAddressInfo> _getNewAddressTask(
AssetId assetId,
ApiClient client,
) async {
ApiClient client, {
Duration pollingInterval = const Duration(milliseconds: 200),
}) async {
final initResponse = await client.rpc.hdWallet.getNewAddressTaskInit(
coin: assetId.id,
accountId: 0,
Expand All @@ -150,7 +198,7 @@ class TrezorHDWalletStrategy extends PubkeyStrategy with HDWalletMixin {
);
result = (status.details..throwIfError).data;

await Future<void>.delayed(const Duration(milliseconds: 100));
await Future<void>.delayed(pollingInterval);
}
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ class SingleAddressStrategy extends PubkeyStrategy {
);
}

@override
Stream<NewAddressState> getNewAddressStream(
AssetId assetId,
ApiClient client,
) async* {
yield NewAddressState.error(
'Single address coins do not support generating new addresses',
);
}

@override
Future<void> scanForNewAddresses(AssetId _, ApiClient __) async {
// No-op for single address coins
Expand Down
111 changes: 107 additions & 4 deletions packages/komodo_defi_sdk/example/lib/screens/asset_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,19 @@ class _AssetPageState extends State<AssetPage> {
Future<void> _generateNewAddress() async {
setState(() => _isLoading = true);
try {
final newPubkey = await _sdk.pubkeys.createNewPubkey(widget.asset);
setState(() {
_pubkeys?.keys.add(newPubkey);
});
final stream = _sdk.pubkeys.createNewPubkeyStream(widget.asset);

final newPubkey = await showDialog<PubkeyInfo>(
context: context,
barrierDismissible: false,
builder: (context) => _NewAddressDialog(stream: stream),
);

if (newPubkey != null) {
setState(() {
_pubkeys?.keys.add(newPubkey);
});
}
} catch (e) {
setState(() => _error = e.toString());
} finally {
Expand Down Expand Up @@ -655,3 +664,97 @@ class __TransactionsSectionState extends State<_TransactionsSection> {
}
}
}

class _NewAddressDialog extends StatefulWidget {
const _NewAddressDialog({required this.stream});

final Stream<NewAddressState> stream;

@override
State<_NewAddressDialog> createState() => _NewAddressDialogState();
}

class _NewAddressDialogState extends State<_NewAddressDialog> {
late final StreamSubscription<NewAddressState> _subscription;
NewAddressState? _state;

@override
void initState() {
super.initState();
_subscription = widget.stream.listen((state) {
setState(() => _state = state);
if (state.status == NewAddressStatus.completed) {
Navigator.of(context).pop(state.address);
} else if (state.status == NewAddressStatus.error ||
state.status == NewAddressStatus.cancelled) {
Navigator.of(context).pop(null);
}
});
}

@override
void dispose() {
_subscription.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
final state = _state;

String message;
if (state == null) {
message = 'Initializing...';
} else {
switch (state.status) {
case NewAddressStatus.initializing:
case NewAddressStatus.processing:
case NewAddressStatus.waitingForDevice:
case NewAddressStatus.waitingForDeviceConfirmation:
case NewAddressStatus.pinRequired:
case NewAddressStatus.passphraseRequired:
message = state.message ?? 'Processing...';
break;
case NewAddressStatus.confirmAddress:
message = 'Confirm the address on your device';
break;
case NewAddressStatus.completed:
message = 'Completed';
break;
case NewAddressStatus.error:
message = state.error ?? 'Error';
break;
case NewAddressStatus.cancelled:
message = 'Cancelled';
break;
}
}

final showAddress = state?.status == NewAddressStatus.confirmAddress;

return AlertDialog(
title: const Text('Generating Address'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (showAddress)
SelectableText(state?.expectedAddress ?? '')
else
const SizedBox(
width: 32,
height: 32,
child: CircularProgressIndicator(),
),
const SizedBox(height: 16),
Text(message, textAlign: TextAlign.center),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
],
);
}
}
13 changes: 13 additions & 0 deletions packages/komodo_defi_sdk/lib/src/pubkeys/pubkey_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ class PubkeyManager {
return strategy.getNewAddress(asset.id, _client);
}

/// Streamed version of [createNewPubkey]
Stream<NewAddressState> createNewPubkeyStream(Asset asset) async* {
await retry(() => _activationManager.activateAsset(asset).last);
final strategy = await _resolvePubkeyStrategy(asset);
if (!strategy.supportsMultipleAddresses) {
yield NewAddressState.error(
'Asset ${asset.id.name} does not support multiple addresses',
);
return;
}
yield* strategy.getNewAddressStream(asset.id, _client);
}

Future<PubkeyStrategy> _resolvePubkeyStrategy(Asset asset) async {
final currentUser = await _auth.currentUser;
if (currentUser == null) {
Expand Down
Loading
Loading