CakeWallet/lib/anonpay/anonpay_api.dart
cyan 5082dc20f3
CW-519 Enable built-in Tor (#1950)
* tor wip

* Enable tor on iOS

* Prevent app lag when node is exceptionally slow (usually over tor)

* fix: logic in daemonBlockchainHeight refresh
fix: storing tor state

* Pin ledger_flutter_plus dependency to fix builds

* bump arti version

* wip

* add single httpclient

* route everything I was able to catch trough the built-in tor node

* Enable proxy for http.Client [run tests]

* add tor proxy support to cw_evm, cw_tron and cw_polygon [run tests]

* remove log pollution, cleanup [skip slack]

* fix tests not working in latest main [skip slack] [run tests]

* remove cw_wownero import

* fix build issues

* migrate all remaining calls to use ProxyWrapper
add a CI action to enforce using ProxyWrapper instead of http/http.dart to prevent leaks

* fix tor background sync (will work on test builds after #2142 is merged and this PR is rebased on top)

* wip [skip ci]

* relicense to GPLv3 add socks5 license, build fixes

* use ProxyWrapper instead of http in robinhood

* Revert "relicense to GPLv3"

* feat(cw_bitcoin): support socks proxy and CakeTor

* fix(tor): migrate OCP and EVM over to ProxyWrapper()

* chore: cleanup
fix: show tor loading screen when app is starting

* fix: tor switch properly dismisses fullscreen loading dialog
fix: connectToNode after tor startup on app start

* fix(tor): status check for xmr/wow/zano

* fix(tor): onramper request fix

* fix(api): ServicesResponse is now being cached and doesn't fetch data everytime DashboardViewModel is being rebuilt
fix(tor): do not fallback to clearnet when tor failed.
fix(tor): do not leak connections during app startup
chore: refactor bootstrap() function to be separated into bootstrapOffline and bootstrapOnline
fix(cw_bitcoin): migrate payjoin to use ProxyWrapper

* [skip ci] remove print

* address comments from review

* fix: derusting tor implementation

Instead of rust-based Arti I've moved back to the
OG C++ tor implementation.
This fixed all issues we had with Tor.
- onion services now work
- all requests are going through without random errors
- we don't have to navigate a maze of multiple forks of multiple packages
- fully working `torrc` config file (probably will be needed for Tari).
- logging for Tor client
- and so on.

feat: network logging tab
feat: use built-in proxy on Tails - this should resolve all issues for Tails users (needs testing though)

* fix conflicts with main
bump https to fix build issue
relax store() call

* fix(cw_wownero): tor connection
fix(tor): connection issues

* fix(cw_evm): add missing chainId
fix(cw_core): solana rpc fix

* feat: mark tor as experimental
fix: drop anonpay onion authority
fix: drop fiatapi onion authority
fix: drop trocador onion authority
fix: disable networkimage when tor is enabled
fix: handle cakepay errors gracefully

* fix re-formatting [skip ci]

* changes from review

* Delete android/.kotlin/sessions/kotlin-compiler-2468481326039681181.salive

* fix missing imports

* Update pubspec_base.yaml

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
2025-06-20 22:56:18 +03:00

204 lines
6.6 KiB
Dart

import 'dart:convert';
import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart';
import 'package:cake_wallet/anonpay/anonpay_request.dart';
import 'package:cake_wallet/anonpay/anonpay_status_response.dart';
import 'package:cake_wallet/core/fiat_conversion_service.dart';
import 'package:cake_wallet/entities/fiat_currency.dart';
import 'package:cake_wallet/exchange/limits.dart';
import 'package:cw_core/utils/proxy_wrapper.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
class AnonPayApi {
const AnonPayApi({
this.useTorOnly = false,
required this.wallet,
});
final bool useTorOnly;
final WalletBase wallet;
static const anonpayRef = secrets.anonPayReferralCode;
static const clearNetAuthority = 'trocador.app';
// static const onionApiAuthority = 'tqzngtf2hybjbexznel6dhgsvbynjzezoybvtv6iofomx7gchqfssgqd.onion';
static const onionApiAuthority = clearNetAuthority;
static const markup = secrets.trocadorExchangeMarkup;
static const anonPayPath = '/anonpay';
static const anonPayStatus = '/anonpay/status';
static const coinPath = 'api/coin';
static const apiKey = secrets.trocadorApiKey;
Future<AnonpayStatusResponse> paymentStatus(String id) async {
final response = await ProxyWrapper().get(
clearnetUri: Uri.https(clearNetAuthority, "$anonPayStatus/$id"),
onionUri: Uri.https(onionApiAuthority, "$anonPayStatus/$id"),
);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final status = responseJSON['Status'] as String;
final fiatAmount = responseJSON['Fiat_Amount'] as double?;
final fiatEquiv = responseJSON['Fiat_Equiv'] as String?;
final amountTo = responseJSON['AmountTo'] as double?;
final coinTo = responseJSON['CoinTo'] as String;
final address = responseJSON['Address'] as String;
return AnonpayStatusResponse(
status: status,
fiatAmount: fiatAmount,
amountTo: amountTo,
coinTo: coinTo,
address: address,
fiatEquiv: fiatEquiv,
);
}
Future<AnonpayInvoiceInfo> createInvoice(AnonPayRequest request) async {
final description = Uri.encodeComponent(request.description);
final body = <String, dynamic>{
'ticker_to': request.cryptoCurrency.title.toLowerCase(),
'network_to': _networkFor(request.cryptoCurrency),
'address': request.address,
'name': request.name,
'description': description,
'email': request.email,
'ref': anonpayRef,
'markup': markup,
'direct': 'False',
};
if (request.amount != null) {
body['amount'] = request.amount;
}
if (request.fiatEquivalent != null) {
body['fiat_equiv'] = request.fiatEquivalent;
}
final response = await ProxyWrapper().get(
clearnetUri: Uri.https(clearNetAuthority, anonPayPath, body),
onionUri: Uri.https(onionApiAuthority, anonPayPath, body),
);
final responseJSON = json.decode(response.body) as Map<String, dynamic>;
final id = responseJSON['ID'] as String;
final url = responseJSON['url'] as String;
final urlOnion = responseJSON['url_onion'] as String;
final statusUrl = responseJSON['status_url'] as String;
final statusUrlOnion = responseJSON['status_url_onion'] as String;
final statusInfo = await paymentStatus(id);
return AnonpayInvoiceInfo(
invoiceId: id,
clearnetUrl: url,
onionUrl: urlOnion,
status: statusInfo.status,
fiatAmount: statusInfo.fiatAmount,
fiatEquiv: statusInfo.fiatEquiv,
amountTo: statusInfo.amountTo,
coinTo: statusInfo.coinTo,
address: statusInfo.address,
clearnetStatusUrl: statusUrl,
onionStatusUrl: statusUrlOnion,
walletId: wallet.id,
createdAt: DateTime.now(),
provider: 'Trocador AnonPay invoice',
);
}
Future<AnonpayDonationLinkInfo> generateDonationLink(AnonPayRequest request) async {
final body = <String, dynamic>{
'ticker_to': request.cryptoCurrency.title.toLowerCase(),
'network_to': _networkFor(request.cryptoCurrency),
'address': request.address,
'ref': anonpayRef,
'direct': 'True',
};
if (request.name.isNotEmpty) {
body['name'] = request.name;
}
if (request.description.isNotEmpty) {
body['description'] = request.description;
}
if (request.email.isNotEmpty) {
body['email'] = request.email;
}
final clearnetUrl = Uri.https(clearNetAuthority, anonPayPath, body);
final onionUrl = Uri.https(onionApiAuthority, anonPayPath, body);
return AnonpayDonationLinkInfo(
clearnetUrl: clearnetUrl.toString(),
onionUrl: onionUrl.toString(),
address: request.address,
);
}
Future<Limits> fetchLimits({
FiatCurrency? fiatCurrency,
required CryptoCurrency cryptoCurrency,
}) async {
double fiatRate = 0.0;
if (fiatCurrency != null) {
fiatRate = await FiatConversionService.fetchPrice(
crypto: cryptoCurrency,
fiat: fiatCurrency,
torOnly: useTorOnly,
);
}
final params = <String, String>{
'api_key': apiKey,
'ticker': cryptoCurrency.title.toLowerCase(),
'name': cryptoCurrency.name,
};
final response = await ProxyWrapper().get(
clearnetUri: Uri.https(clearNetAuthority, coinPath, params),
onionUri: Uri.https(onionApiAuthority, coinPath, params),
);
final responseJSON = json.decode(response.body) as List<dynamic>;
if (response.statusCode != 200) {
throw Exception('Unexpected http status: ${response.statusCode}');
}
if (responseJSON.isEmpty) {
throw Exception('No data');
}
final coinJson = responseJSON.first as Map<String, dynamic>;
final minimum = coinJson['minimum'] as double;
final maximum = coinJson['maximum'] as double;
if (fiatCurrency != null) {
return Limits(
min: double.tryParse((minimum * fiatRate).toStringAsFixed(2)),
max: double.tryParse((maximum * fiatRate).toStringAsFixed(2)),
);
}
return Limits(
min: minimum,
max: maximum,
);
}
String _networkFor(CryptoCurrency currency) {
switch (currency) {
case CryptoCurrency.usdt:
return CryptoCurrency.btc.title.toLowerCase();
case CryptoCurrency.eth:
return 'ERC20';
default:
return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet';
}
}
String _normalizeTag(String tag) {
switch (tag) {
case 'ETH':
return 'ERC20';
default:
return tag.toLowerCase();
}
}
}