Add reusable two-factor verification for login and money operations (#32)

This commit is contained in:
Stephan D committed 2026-09-12 12:44:34 +02:00
1 parent b7a58b1a87
commit fb40f860d2
41 files changed
+2439 -43

No files matched your search

@@ -6,7 +6,11 @@ abstract interface class WalletService {
Future<Quote> quote(TransferRequest request);
/// Same key must reconcile uncertain submissions without duplicate execution.
Future<WalletOperation> submit(String quoteId, String idempotencyKey);
Future<WalletOperation> submit(
String quoteId,
String idempotencyKey, {
String? approval,
});
Future<WalletOperation?> findSubmission(String idempotencyKey);
Future<AccountProfile> profile();
Future<AccountProfile> saveLocale(String locale);
@@ -1,5 +1,7 @@
import 'dart:math';
import 'package:sxchnge_auth/sxchnge_auth.dart';
import 'package:sxchnge_kyc/limits_transport.dart';
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
@@ -11,6 +13,7 @@ import 'contracts.dart';
class DummyWalletService implements WalletService {
DummyWalletService(
this.identity, {
this.factors,
this.delay = const Duration(milliseconds: 350),
this.settlement = const Duration(seconds: 2),
DateTime Function()? now,
@@ -18,6 +21,7 @@ class DummyWalletService implements WalletService {
}) : now = now ?? DateTime.now,
_locale = initialLocale;
final IdentityService identity;
final TwoFactorService? factors;
final Duration delay, settlement;
final DateTime Function() now;
final _accounts = [
@@ -159,13 +163,28 @@ class DummyWalletService implements WalletService {
}
@override
Future<WalletOperation> submit(String quoteId, String idempotencyKey) async {
Future<WalletOperation> submit(
String quoteId,
String idempotencyKey, {
String? approval,
}) async {
await _wait();
if (_submissions.containsKey(idempotencyKey))
return WalletMapper.operation(_submissions[idempotencyKey]!);
final q = _quotes[quoteId];
if (q == null) throw const WalletException(WalletError.expiredQuote);
final verified = await _verified();
try {
await factors?.authorize(
FactorPurpose.values.byName(q.request.kind),
q.id,
approval,
);
} catch (_) {
final accepted = _submissions[idempotencyKey];
if (accepted != null) return WalletMapper.operation(accepted);
rethrow;
}
_settle();
if (_submissions.containsKey(idempotencyKey))
return WalletMapper.operation(_submissions[idempotencyKey]!);