Add saved payout destination UX and service contracts (#54)
This commit is contained in:
1 parent
a5689876ff
commit
24af794112
49 files changed
+4687
-62
No files matched your search
@@ -94,10 +94,15 @@ class BackendWalletService implements WalletService {
|
||||
@override
|
||||
Future<WalletSnapshot> fetch() => _mapped(send(''), BackendMapper.snapshot);
|
||||
@override
|
||||
Future<Quote> quote(TransferRequest request) => _mapped(
|
||||
send('/quote', body: BackendMapper.request(request)),
|
||||
BackendMapper.quote,
|
||||
);
|
||||
Future<Quote> quote(TransferRequest request) async {
|
||||
if (request.payoutDestination != null)
|
||||
throw const WalletException(WalletError.unsupported);
|
||||
return _mapped(
|
||||
send('/quote', body: BackendMapper.request(request)),
|
||||
BackendMapper.quote,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WalletOperation> submit(
|
||||
String quoteId,
|
||||
|
||||
@@ -5,6 +5,9 @@ import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||||
import 'package:sxchnge_kyc/limits_transport.dart';
|
||||
import 'package:sxchnge_kyc/sxchnge_kyc.dart';
|
||||
|
||||
import '../destinations/dummy.dart';
|
||||
import '../destinations/models.dart';
|
||||
import '../destinations/transport.dart';
|
||||
import '../models/wallet.dart';
|
||||
import '../transport/wallet.dart';
|
||||
import 'contracts.dart';
|
||||
@@ -14,13 +17,17 @@ class DummyWalletService implements WalletService {
|
||||
DummyWalletService(
|
||||
this.identity, {
|
||||
this.factors,
|
||||
DummyDestinationService? destinations,
|
||||
this.delay = const Duration(milliseconds: 350),
|
||||
this.settlement = const Duration(seconds: 2),
|
||||
DateTime Function()? now,
|
||||
String initialLocale = 'en',
|
||||
}) : now = now ?? DateTime.now,
|
||||
}) : destinations = destinations ?? DummyDestinationService(now: now),
|
||||
now = now ?? DateTime.now,
|
||||
_locale = initialLocale;
|
||||
final IdentityService identity;
|
||||
final DummyDestinationService destinations;
|
||||
final _destinationLeases = <String, DestinationLease>{};
|
||||
final TwoFactorService? factors;
|
||||
final Duration delay, settlement;
|
||||
final DateTime Function() now;
|
||||
@@ -59,6 +66,10 @@ class DummyWalletService implements WalletService {
|
||||
};
|
||||
int _eur(int minor, String id) => (minor * _weight(id) + 1099) ~/ 1100;
|
||||
void _settle() {
|
||||
_quotes.removeWhere((_, quote) => !now().isBefore(quote.expires));
|
||||
_destinationLeases.removeWhere(
|
||||
(_, lease) => !now().isBefore(lease.expiresAt),
|
||||
);
|
||||
for (final o in _submissions.values) {
|
||||
if (o.status != 'pending' || now().isBefore(o.ready)) continue;
|
||||
o.status = o.fail ? 'failed' : 'completed';
|
||||
@@ -133,9 +144,19 @@ class DummyWalletService implements WalletService {
|
||||
if (r.kind == TransferKind.exchange &&
|
||||
(r.source == r.destination || r.destination == null))
|
||||
throw const WalletException(WalletError.unsupported);
|
||||
if (r.kind == TransferKind.payout &&
|
||||
(r.recipient.trim().length < 6 || r.recipient.length > 140))
|
||||
throw const WalletException(WalletError.invalidRecipient);
|
||||
DestinationLease? lease;
|
||||
if (r.kind == TransferKind.payout) {
|
||||
if (r.payoutDestination == null)
|
||||
throw const WalletException(WalletError.invalidRecipient);
|
||||
try {
|
||||
lease = await destinations.prepare(
|
||||
r.payoutDestination!,
|
||||
PayoutContext(r.source, amount: r.amount),
|
||||
);
|
||||
} on DestinationException {
|
||||
throw const WalletException(WalletError.destinationChanged);
|
||||
}
|
||||
}
|
||||
if (r.kind == TransferKind.exchange) _account(r.destination!);
|
||||
final fee = switch (r.kind) {
|
||||
TransferKind.topUp => 0,
|
||||
@@ -146,19 +167,30 @@ class DummyWalletService implements WalletService {
|
||||
final d = r.kind == TransferKind.exchange ? _weight(r.destination!) : 1;
|
||||
final q = QuoteDTO(
|
||||
'quote-${++_serial}',
|
||||
WalletMapper.request(r),
|
||||
WalletMapper.request(r, includePayoutInput: false),
|
||||
fee,
|
||||
r.kind == TransferKind.topUp ? r.amount : r.amount + fee,
|
||||
r.amount * n ~/ d,
|
||||
now().add(const Duration(seconds: 60)),
|
||||
n,
|
||||
d,
|
||||
payoutTerms: lease == null
|
||||
? null
|
||||
: PayoutTermsDTO(r.amount, 2, switch (lease.snapshot) {
|
||||
CryptoDestination d => d.assetLabel,
|
||||
RussianBankDestination() => 'RUB',
|
||||
CardDestination() => _account(r.source).currency,
|
||||
}, settlement.inSeconds),
|
||||
payoutSnapshot: lease == null
|
||||
? null
|
||||
: DestinationMapper.details(lease.snapshot),
|
||||
);
|
||||
if (q.credit <= 0) throw const WalletException(WalletError.invalidAmount);
|
||||
final verified = await _verified();
|
||||
_settle();
|
||||
_check(q, verified);
|
||||
_quotes[q.id] = q;
|
||||
if (lease != null) _destinationLeases[q.id] = lease;
|
||||
return WalletMapper.quote(q);
|
||||
}
|
||||
|
||||
@@ -193,6 +225,17 @@ class DummyWalletService implements WalletService {
|
||||
if (_usedQuotes.contains(q.id))
|
||||
throw const WalletException(WalletError.expiredQuote);
|
||||
_check(q, verified);
|
||||
final lease = _destinationLeases[q.id];
|
||||
if (q.request.kind == 'payout' && lease == null)
|
||||
throw const WalletException(WalletError.expiredQuote);
|
||||
if (lease != null) {
|
||||
try {
|
||||
destinations.accept(lease);
|
||||
} on DestinationException {
|
||||
throw const WalletException(WalletError.destinationChanged);
|
||||
}
|
||||
}
|
||||
// No asynchronous gap between destination acceptance and financial mutation.
|
||||
_usedQuotes.add(q.id);
|
||||
if (q.request.kind != 'topUp')
|
||||
_account(q.request.source).available -= q.debit;
|
||||
@@ -205,6 +248,7 @@ class DummyWalletService implements WalletService {
|
||||
);
|
||||
failNextOperation = false;
|
||||
_submissions[idempotencyKey] = o;
|
||||
_destinationLeases.remove(q.id);
|
||||
if (loseNextResponse) {
|
||||
loseNextResponse = false;
|
||||
throw const WalletException(WalletError.uncertain);
|
||||
|
||||
Reference in new issue
Block a user