What changed, and why it matters
This commit adds a new cryptocurrency exchange integration called CypherGoat to the Stack Wallet app. It lets users compare rates and create swaps through the CypherGoat service. The change is a normal feature addition, but it does introduce a new external API dependency and sends user-provided wallet addresses and trade details to api.cyphergoat.com. There is no direct evidence of a security vulnerability in the code itself, but any new third-party integration carries standard risks: the server could be compromised, the connection could be intercepted if certificate pinning is absent, or the API could return malicious data that the app might not fully validate.
Treat this as a routine feature commit, but perform a security-focused review of the new integration before release: verify TLS/certificate handling, confirm the HTTP client enforces HTTPS and validates server certificates, review JSON parsing for type confusion or missing fields, ensure user addresses and amounts are not logged, and assess CypherGoat's server security and data-handling practices. Consider adding response schema validation and pinning if not already present elsewhere in the project.
Security signals we found
New third-party API integration added (api.cyphergoat.com)
User wallet addresses and trade amounts transmitted to external service
Optional Tor proxy support present but not mandatory
No certificate pinning visible in the supplied diff
JSON response parsing relies on null-aware casts and default values
Static coin list embedded in source code may become stale
API key and affiliate constants added to build template
Evidence from the diff
The patch implements CypherGoatExchange and CypherGoatAPI, wiring them into the existing exchange UI and data-loading service. API calls use an HTTP client over HTTPS to api.cyphergoat.com, with optional Tor proxying when the app has the Tor feature and the user enabled it. Endpoints /estimate, /swap, and /transaction accept coin/network/amount/address parameters and parse JSON responses into CgEstimate and CgTransaction objects. The code does not appear to perform certificate pinning, input sanitization beyond null-aware casts, or strict response schema validation. API keys and an affiliate tag are read from external_api_keys.dart. The commit is a feature addition; no CVE, advisory, vendor security statement, or researcher attribution is present in the supplied materials.
Changed components
lib/services/exchange/cyphergoat/cyphergoat_api.dartlib/services/exchange/cyphergoat/cyphergoat_exchange.dartlib/services/exchange/cyphergoat/response_objects/cg_estimate.dartlib/services/exchange/cyphergoat/response_objects/cg_transaction.dartlib/pages/exchange_view/exchange_form.dartlib/pages/exchange_view/sub_widgets/exchange_provider_options.dartlib/services/exchange/exchange.dartlib/services/exchange/exchange_data_loading_service.dartlib/utilities/assets.dartscripts/prebuild.ps1scripts/prebuild.shInspect captured patch +926 / −2
diff --git a/lib/pages/exchange_view/exchange_form.dart b/lib/pages/exchange_view/exchange_form.dart
index fb1fa41..4d4fe4e 100644
--- a/lib/pages/exchange_view/exchange_form.dart
+++ b/lib/pages/exchange_view/exchange_form.dart
@@ -27,6 +27,7 @@ import '../../models/isar/models/ethereum/eth_contract.dart';
import '../../pages_desktop_specific/desktop_exchange/exchange_steps/step_scaffold.dart';
import '../../providers/providers.dart';
import '../../services/exchange/change_now/change_now_exchange.dart';
+import '../../services/exchange/cyphergoat/cyphergoat_exchange.dart';
import '../../services/exchange/exchange.dart';
import '../../services/exchange/exchange_data_loading_service.dart';
import '../../services/exchange/exchange_response.dart';
@@ -86,6 +87,7 @@ class _ExchangeFormState extends ConsumerState<ExchangeForm> {
TrocadorExchange.instance,
NanswapExchange.instance,
WizardSwapExchange.instance,
+ CypherGoatExchange.instance,
];
}
}
diff --git a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
index b7fad4d..e943b00 100644
--- a/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
+++ b/lib/pages/exchange_view/sub_widgets/exchange_provider_options.dart
@@ -14,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../models/exchange/aggregate_currency.dart';
import '../../../providers/providers.dart';
import '../../../services/exchange/change_now/change_now_exchange.dart';
+import '../../../services/exchange/cyphergoat/cyphergoat_exchange.dart';
import '../../../services/exchange/exchange.dart';
import '../../../services/exchange/exolix/exolix_exchange.dart';
import '../../../services/exchange/nanswap/nanswap_exchange.dart';
@@ -103,6 +104,11 @@ class _ExchangeProviderOptionsState
sendCurrency: sendCurrency,
receiveCurrency: receivingCurrency,
);
+ final showCypherGoat = exchangeSupported(
+ exchangeName: CypherGoatExchange.exchangeName,
+ sendCurrency: sendCurrency,
+ receiveCurrency: receivingCurrency,
+ );
return RoundedWhiteContainer(
padding: isDesktop ? const EdgeInsets.all(0) : const EdgeInsets.all(12),
@@ -116,6 +122,7 @@ class _ExchangeProviderOptionsState
if (showTrocador) TrocadorExchange.instance,
if (showNanswap) NanswapExchange.instance,
if (showWizardSwap) WizardSwapExchange.instance,
+ if (showCypherGoat) CypherGoatExchange.instance,
],
fixedRate: widget.fixedRate,
reversed: widget.reversed,
diff --git a/lib/services/exchange/cyphergoat/cyphergoat_api.dart b/lib/services/exchange/cyphergoat/cyphergoat_api.dart
new file mode 100644
index 0000000..b421a6b
--- /dev/null
+++ b/lib/services/exchange/cyphergoat/cyphergoat_api.dart
@@ -0,0 +1,224 @@
+import 'dart:convert';
+
+import '../../../app_config.dart';
+import '../../../exceptions/exchange/exchange_exception.dart';
+import '../../../external_api_keys.dart';
+import '../../../networking/http.dart';
+import '../../../utilities/logger.dart';
+import '../../../utilities/prefs.dart';
+import '../../tor_service.dart';
+import '../exchange_response.dart';
+import 'response_objects/cg_estimate.dart';
+import 'response_objects/cg_transaction.dart';
+
+const kCypherGoatSource = "stackwallet";
+
+abstract class CypherGoatAPI {
+ static const String authority = "api.cyphergoat.com";
+
+ static const HTTP _client = HTTP();
+
+ static Uri _buildUri({
+ required String path,
+ Map<String, String>? params,
+ }) {
+ return Uri.https(authority, path, params);
+ }
+
+ static Future<dynamic> _makeGetRequest(Uri uri) async {
+ int code = -1;
+ try {
+ final headers = <String, String>{
+ "Content-Type": "application/json",
+ "Accept": "application/json",
+ };
+ if (kCypherGoatApiKey.isNotEmpty) {
+ headers["Authorization"] = "Bearer $kCypherGoatApiKey";
+ }
+
+ final response = await _client.get(
+ url: uri,
+ headers: headers,
+ proxyInfo: !AppConfig.hasFeature(AppFeature.tor)
+ ? null
+ : Prefs.instance.useTor
+ ? TorService.sharedInstance.getProxyInfo()
+ : null,
+ );
+
+ code = response.code;
+
+ final json = jsonDecode(response.body);
+
+ if (code != 200) {
+ final errMsg = (json is Map ? json["error"] : null) as String?;
+ throw Exception(errMsg ?? "HTTP $code: ${response.body}");
+ }
+
+ return json;
+ } catch (e, s) {
+ Logging.instance.e(
+ "CypherGoatAPI GET $uri HTTP:$code threw:",
+ error: e,
+ stackTrace: s,
+ );
+ rethrow;
+ }
+ }
+
+ /// GET /estimate
+ /// Returns all exchange provider estimates for the given pair and amount.
+ static Future<ExchangeResponse<({CgEstimatesResponse rates, double min})>>
+ getEstimate({
+ required String coin1,
+ required String network1,
+ required String coin2,
+ required String network2,
+ required String amount,
+ }) async {
+ final params = <String, String>{
+ "coin1": coin1.toLowerCase(),
+ "network1": network1.toLowerCase(),
+ "coin2": coin2.toLowerCase(),
+ "network2": network2.toLowerCase(),
+ "amount": amount,
+ "best": "false",
+ };
+
+ if (kCypherGoatApiKey.isNotEmpty) {
+ params["api_key"] = kCypherGoatApiKey;
+ }
+
+ final uri = _buildUri(path: "/estimate", params: params);
+
+ try {
+ final json = await _makeGetRequest(uri);
+ final map = Map<String, dynamic>.from(json as Map);
+
+ final ratesMap = map["rates"] as Map?;
+ if (ratesMap == null) {
+ throw Exception("Missing 'rates' in estimate response");
+ }
+
+ final rates = CgEstimatesResponse.fromMap(
+ Map<String, dynamic>.from(ratesMap),
+ );
+ final min = (map["min"] as num?)?.toDouble() ?? rates.min;
+
+ return ExchangeResponse(value: (rates: rates, min: min));
+ } catch (e, s) {
+ Logging.instance.e(
+ "CypherGoatAPI.getEstimate() exception:",
+ error: e,
+ stackTrace: s,
+ );
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ /// GET /swap
+ /// Creates a swap with the specified exchange partner.
+ static Future<ExchangeResponse<CgTransaction>> createSwap({
+ required String coin1,
+ required String network1,
+ required String coin2,
+ required String network2,
+ required String amount,
+ required String partner,
+ required String address,
+ String? estimateId,
+ }) async {
+ final params = <String, String>{
+ "coin1": coin1.toLowerCase(),
+ "network1": network1.toLowerCase(),
+ "coin2": coin2.toLowerCase(),
+ "network2": network2.toLowerCase(),
+ "amount": amount,
+ "partner": partner,
+ "address": address,
+ "source": kCypherGoatSource,
+ };
+
+ if (kCypherGoatAffiliate.isNotEmpty) {
+ params["affiliate"] = kCypherGoatAffiliate;
+ }
+ if (estimateId != null && estimateId.isNotEmpty) {
+ params["estimateid"] = estimateId;
+ }
+ if (kCypherGoatApiKey.isNotEmpty) {
+ params["api_key"] = kCypherGoatApiKey;
+ }
+
+ final uri = _buildUri(path: "/swap", params: params);
+
+ try {
+ final json = await _makeGetRequest(uri);
+ final map = Map<String, dynamic>.from(json as Map);
+
+ final txMap = map["transaction"] as Map?;
+ if (txMap == null) {
+ throw Exception("Missing 'transaction' in swap response");
+ }
+
+ return ExchangeResponse(
+ value: CgTransaction.fromMap(Map<String, dynamic>.from(txMap)),
+ );
+ } catch (e, s) {
+ Logging.instance.e(
+ "CypherGoatAPI.createSwap() exception:",
+ error: e,
+ stackTrace: s,
+ );
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ /// GET /transaction
+ /// Fetches transaction details by CGID.
+ static Future<ExchangeResponse<CgTransaction>> getTransaction({
+ required String cgid,
+ }) async {
+ final params = <String, String>{"id": cgid};
+ if (kCypherGoatApiKey.isNotEmpty) {
+ params["api_key"] = kCypherGoatApiKey;
+ }
+
+ final uri = _buildUri(path: "/transaction", params: params);
+
+ try {
+ final json = await _makeGetRequest(uri);
+ final map = Map<String, dynamic>.from(json as Map);
+
+ final txMap = map["transaction"] as Map?;
+ if (txMap == null) {
+ throw Exception("Missing 'transaction' in response");
+ }
+
+ return ExchangeResponse(
+ value: CgTransaction.fromMap(Map<String, dynamic>.from(txMap)),
+ );
+ } catch (e, s) {
+ Logging.instance.e(
+ "CypherGoatAPI.getTransaction($cgid) exception:",
+ error: e,
+ stackTrace: s,
+ );
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/services/exchange/cyphergoat/cyphergoat_exchange.dart b/lib/services/exchange/cyphergoat/cyphergoat_exchange.dart
new file mode 100644
index 0000000..0764c2c
--- /dev/null
+++ b/lib/services/exchange/cyphergoat/cyphergoat_exchange.dart
@@ -0,0 +1,514 @@
+import 'package:decimal/decimal.dart';
+import 'package:uuid/uuid.dart';
+
+import '../../../app_config.dart';
+import '../../../exceptions/exchange/exchange_exception.dart';
+import '../../../models/exchange/response_objects/estimate.dart';
+import '../../../models/exchange/response_objects/range.dart';
+import '../../../models/exchange/response_objects/trade.dart';
+import '../../../models/isar/exchange_cache/currency.dart';
+import '../../../models/isar/exchange_cache/pair.dart';
+import '../exchange.dart';
+import '../exchange_response.dart';
+import 'cyphergoat_api.dart';
+
+class _CgCoin {
+ final String ticker;
+ final String name;
+ final String network;
+ final double? min;
+
+ const _CgCoin({
+ required this.ticker,
+ required this.name,
+ required this.network,
+ this.min,
+ });
+}
+
+// Static coin list derived from CypherGoat's coins.json.
+const List<_CgCoin> _kCgCoins = [
+ _CgCoin(ticker: 'btc', name: 'Bitcoin', network: 'btc', min: 4.449e-05),
+ _CgCoin(ticker: 'btc', name: 'Bitcoin (Lightning)', network: 'lightning', min: 4.449e-05),
+ _CgCoin(ticker: 'eth', name: 'Ethereum', network: 'eth', min: 0.001114),
+ _CgCoin(ticker: 'xmr', name: 'Monero', network: 'xmr', min: 0.01886),
+ _CgCoin(ticker: 'ltc', name: 'Litecoin', network: 'ltc', min: 0.04444),
+ _CgCoin(ticker: 'bch', name: 'Bitcoin Cash', network: 'bch'),
+ _CgCoin(ticker: 'doge', name: 'Dogecoin', network: 'doge', min: 22.59),
+ _CgCoin(ticker: 'bnb', name: 'Binance Coin', network: 'bnb', min: 0.005711),
+ _CgCoin(ticker: 'sol', name: 'Solana', network: 'sol', min: 0.0238),
+ _CgCoin(ticker: 'xtz', name: 'Tezos', network: 'xtz', min: 6.336),
+ _CgCoin(ticker: 'ada', name: 'Cardano', network: 'ada', min: 5.868),
+ _CgCoin(ticker: 'xrp', name: 'Ripple', network: 'xrp', min: 1.678),
+ _CgCoin(ticker: 'trx', name: 'Tron', network: 'trx', min: 14.58),
+ _CgCoin(ticker: 'link', name: 'Chainlink', network: 'link', min: 0.2014),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Ethereum)', network: 'usdc'),
+ _CgCoin(ticker: 'xno', name: 'Nano', network: 'xno', min: 5.393),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Polygon)', network: 'poly'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Solana)', network: 'sol'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Algorand)', network: 'algo'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (BSC)', network: 'bsc'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Optimism)', network: 'op'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Base)', network: 'base'),
+ _CgCoin(ticker: 'usdc', name: 'USDC (Tron)', network: 'tron'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (Ethereum)', network: 'eth'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (Tron)', network: 'tron'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (Polygon)', network: 'poly'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (BSC)', network: 'bsc'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (Solana)', network: 'sol'),
+ _CgCoin(ticker: 'usdt', name: 'Tether USD (Algorand)', network: 'algo'),
+ _CgCoin(ticker: 'busd', name: 'Binance USD (BSC)', network: 'bsc'),
+ _CgCoin(ticker: 'busd', name: 'Binance USD (Ethereum)', network: 'eth'),
+ _CgCoin(ticker: 'dai', name: 'Dai (Ethereum)', network: 'eth'),
+ _CgCoin(ticker: 'dai', name: 'Dai (BSC)', network: 'bsc'),
+ _CgCoin(ticker: 'dai', name: 'Dai (Polygon)', network: 'poly'),
+ _CgCoin(ticker: 'dai', name: 'Dai (Optimism)', network: 'op'),
+ _CgCoin(ticker: 'tusd', name: 'True USD', network: 'tusd'),
+ _CgCoin(ticker: 'tusd', name: 'True USD (Tron)', network: 'tron'),
+ _CgCoin(ticker: 'shib', name: 'Shiba Inu', network: 'shib', min: 10000),
+ _CgCoin(ticker: 'dot', name: 'Polkadot', network: 'dot', min: 1.272),
+ _CgCoin(ticker: 'etc', name: 'Ethereum Classic', network: 'etc', min: 0.2318),
+ _CgCoin(ticker: 'zec', name: 'Zcash', network: 'zec', min: 0.2),
+ _CgCoin(ticker: 'hive', name: 'Hive', network: 'hive', min: 24.06),
+ _CgCoin(ticker: 'bdx', name: 'Beldex', network: 'bdx', min: 65.93),
+ _CgCoin(ticker: 'wow', name: 'Wownero', network: 'wow', min: 163.8),
+ _CgCoin(ticker: 'ban', name: 'Banano', network: 'banano', min: 2614.0),
+ _CgCoin(ticker: 'arrr', name: 'Pirate Chain', network: 'arrr', min: 4.8),
+ _CgCoin(ticker: 'arrrbsc', name: 'Pirate Chain (BSC)', network: 'arrrbsc', min: 4.8),
+ _CgCoin(ticker: 'dcr', name: 'Decred', network: 'dcr', min: 0.3045),
+ _CgCoin(ticker: 'aave', name: 'Aave', network: 'aave', min: 0.01574),
+ _CgCoin(ticker: 'avax', name: 'Avalanche', network: 'avax', min: 0.4263),
+ _CgCoin(ticker: 'bat', name: 'Basic Attention Token', network: 'bat', min: 32.09),
+ _CgCoin(ticker: 'link', name: 'Chainlink (BSC)', network: 'bsc', min: 0.2014),
+ _CgCoin(ticker: 'gusd', name: 'Gemini Dollar', network: 'gusd'),
+ _CgCoin(ticker: 'paxg', name: 'Paxos Gold', network: 'paxg', min: 0.002),
+ _CgCoin(ticker: 'hbar', name: 'Hedera', network: 'hbar', min: 12),
+ _CgCoin(ticker: 'ark', name: 'Ark', network: 'ark', min: 10.96),
+ _CgCoin(ticker: 'firo', name: 'Firo', network: 'firo', min: 14.24),
+ _CgCoin(ticker: 'wbtc', name: 'Wrapped Bitcoin', network: 'wbtc', min: 4.444e-05),
+ _CgCoin(ticker: '1inch', name: '1inch', network: '1inch', min: 19.87),
+ _CgCoin(ticker: 'dash', name: 'Dash', network: 'dash', min: 0.2152),
+ _CgCoin(ticker: 'zano', name: 'Zano', network: 'zano', min: 0.3358),
+ _CgCoin(ticker: 'tel', name: 'Telcoin', network: 'tel', min: 1001.0),
+ _CgCoin(ticker: 'leo', name: 'Leo Token', network: 'leo', min: 2),
+ _CgCoin(ticker: 'fusd', name: 'Freedom Dollar', network: 'fusd', min: 25),
+ _CgCoin(ticker: 'apt', name: 'Aptos', network: 'apt', min: 1.134),
+ _CgCoin(ticker: 'sui', name: 'Sui', network: 'sui', min: 1.449),
+ _CgCoin(ticker: 'nvdax', name: 'NVIDIA xStock', network: 'nvdax', min: 0.8),
+ _CgCoin(ticker: 'spyx', name: 'SP500 xStock', network: 'spyx', min: 0.3),
+ _CgCoin(ticker: 'tslax', name: 'TSLA xStock', network: 'tslax', min: 0.4),
+ _CgCoin(ticker: 'qqqx', name: 'Nasdaq xStock', network: 'qqqx', min: 0.3),
+ _CgCoin(ticker: 'crclx', name: 'Circle xStock', network: 'crclx', min: 1.3),
+ _CgCoin(ticker: 'mstrx', name: 'MicroStrategy xStock', network: 'mstrx', min: 0.4),
+ _CgCoin(ticker: 'aaplx', name: 'Apple xStock', network: 'aaplx', min: 0.6),
+ _CgCoin(ticker: 'coinx', name: 'Coinbase xStock', network: 'coinx', min: 0.5),
+ _CgCoin(ticker: 'googlx', name: 'Alphabet xStock', network: 'googlx', min: 0.7),
+ _CgCoin(ticker: 'amznx', name: 'Amazon xStock', network: 'amznx', min: 0.6),
+ _CgCoin(ticker: 'metax', name: 'Meta xStock', network: 'metax', min: 0.2),
+ _CgCoin(ticker: 'hoodx', name: 'Robinhood xStock', network: 'hoodx', min: 1.3),
+ _CgCoin(ticker: 'gmex', name: 'Gamestop xStock', network: 'gmex', min: 5),
+];
+
+class CypherGoatExchange extends Exchange {
+ CypherGoatExchange._();
+
+ static CypherGoatExchange? _instance;
+ static CypherGoatExchange get instance =>
+ _instance ??= CypherGoatExchange._();
+
+ static const exchangeName = "CypherGoat";
+
+ @override
+ String get name => exchangeName;
+
+ @override
+ bool get supportsRefundAddress => false;
+
+ @override
+ Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
+ bool fixedRate,
+ ) async {
+ try {
+ if (fixedRate) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ "CypherGoat does not support fixed rate",
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+
+ final currencies = _kCgCoins
+ .map(
+ (c) => Currency(
+ exchangeName: exchangeName,
+ ticker: c.ticker,
+ name: c.name,
+ network: c.network,
+ image: "",
+ isFiat: false,
+ rateType: SupportedRateType.estimated,
+ isStackCoin: AppConfig.isStackCoin(c.ticker),
+ tokenContract: null,
+ isAvailable: true,
+ ),
+ )
+ .toList();
+
+ return ExchangeResponse(value: currencies);
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future<ExchangeResponse<Range>> getRange(
+ String from,
+ String? fromNetwork,
+ String to,
+ String? toNetwork,
+ bool fixedRate,
+ ) async {
+ try {
+ if (fixedRate) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ "CypherGoat does not support fixed rate",
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+
+ // Use the min from the static coin list as a quick offline fallback.
+ final coin = _kCgCoins.where(
+ (c) =>
+ c.ticker.toLowerCase() == from.toLowerCase() &&
+ (fromNetwork == null ||
+ c.network.toLowerCase() == fromNetwork.toLowerCase()),
+ );
+
+ Decimal? min;
+ if (coin.isNotEmpty && coin.first.min != null) {
+ min = Decimal.parse(coin.first.min.toString());
+ }
+
+ // Fetch live min from the API.
+ final response = await CypherGoatAPI.getEstimate(
+ coin1: from,
+ network1: fromNetwork ?? from,
+ coin2: to,
+ network2: toNetwork ?? to,
+ amount: (min ?? Decimal.one).toString(),
+ );
+
+ if (response.value != null) {
+ final liveMin = response.value!.min;
+ if (liveMin > 0) {
+ min = Decimal.parse(liveMin.toString());
+ }
+ }
+
+ return ExchangeResponse(value: Range(min: min, max: null));
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future<ExchangeResponse<List<Estimate>>> getEstimates(
+ String from,
+ String? fromNetwork,
+ String to,
+ String? toNetwork,
+ Decimal amount,
+ bool fixedRate,
+ bool reversed,
+ ) async {
+ try {
+ if (fixedRate) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ "CypherGoat does not support fixed rate",
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ if (reversed) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ "CypherGoat does not support reversed estimates",
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+
+ final response = await CypherGoatAPI.getEstimate(
+ coin1: from,
+ network1: fromNetwork ?? from,
+ coin2: to,
+ network2: toNetwork ?? to,
+ amount: amount.toString(),
+ );
+
+ if (response.value == null) {
+ return ExchangeResponse(exception: response.exception);
+ }
+
+ final data = response.value!;
+ final estimateIdStr = data.rates.estimateId.toString();
+
+ final estimates = data.rates.results
+ .where((r) => r.amount > 0)
+ .map(
+ (r) => Estimate(
+ estimatedAmount: Decimal.parse(r.amount.toString()),
+ fixedRate: false,
+ reversed: false,
+ exchangeProvider: r.exchange,
+ rateId: estimateIdStr,
+ ),
+ )
+ .toList();
+
+ estimates.sort(
+ (a, b) => b.estimatedAmount.compareTo(a.estimatedAmount),
+ );
+
+ if (estimates.isEmpty) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ "No rates available for this pair",
+ ExchangeExceptionType.orderNotFound,
+ ),
+ );
+ }
+
+ return ExchangeResponse(value: estimates);
+ } on ExchangeException catch (e) {
+ return ExchangeResponse(exception: e);
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future<ExchangeResponse<Trade>> createTrade({
+ required String from,
+ required String to,
+ required String? fromNetwork,
+ required String? toNetwork,
+ required bool fixedRate,
+ required Decimal amount,
+ required String addressTo,
+ String? extraId,
+ required String addressRefund,
+ required String refundExtraId,
+ Estimate? estimate,
+ required bool reversed,
+ }) async {
+ try {
+ if (fixedRate) {
+ throw ExchangeException(
+ "CypherGoat does not support fixed rate",
+ ExchangeExceptionType.generic,
+ );
+ }
+ if (reversed) {
+ throw ExchangeException(
+ "CypherGoat does not support reversed trades",
+ ExchangeExceptionType.generic,
+ );
+ }
+ if (estimate == null) {
+ throw ExchangeException(
+ "An estimate is required to create a CypherGoat trade",
+ ExchangeExceptionType.generic,
+ );
+ }
+
+ final response = await CypherGoatAPI.createSwap(
+ coin1: from,
+ network1: fromNetwork ?? from,
+ coin2: to,
+ network2: toNetwork ?? to,
+ amount: amount.toString(),
+ partner: estimate.exchangeProvider,
+ address: addressTo,
+ estimateId: estimate.rateId,
+ );
+
+ if (response.value == null) {
+ return ExchangeResponse(exception: response.exception);
+ }
+
+ final tx = response.value!;
+
+ return ExchangeResponse(
+ value: Trade(
+ uuid: const Uuid().v1(),
+ tradeId: tx.cgid.isNotEmpty ? tx.cgid : tx.id,
+ rateType: "estimated",
+ direction: "direct",
+ timestamp: tx.createdAt,
+ updatedAt: tx.createdAt,
+ payInCurrency: tx.coin1.toUpperCase(),
+ payInAmount: tx.sendAmount.toString(),
+ payInAddress: tx.address,
+ payInNetwork: tx.network1,
+ payInExtraId: tx.memo,
+ payInTxid: "",
+ payOutCurrency: tx.coin2.toUpperCase(),
+ payOutAmount: tx.estimateAmount.toString(),
+ payOutAddress: tx.destinationAddress,
+ payOutNetwork: tx.network2,
+ payOutExtraId: "",
+ payOutTxid: "",
+ refundAddress: "",
+ refundExtraId: "",
+ status: tx.status,
+ exchangeName: exchangeName,
+ ),
+ );
+ } on ExchangeException catch (e) {
+ return ExchangeResponse(exception: e);
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
+ try {
+ final response = await CypherGoatAPI.getTransaction(cgid: tradeId);
+
+ if (response.value == null) {
+ return ExchangeResponse(exception: response.exception);
+ }
+
+ final tx = response.value!;
+
+ return ExchangeResponse(
+ value: Trade(
+ uuid: const Uuid().v1(),
+ tradeId: tx.cgid.isNotEmpty ? tx.cgid : tradeId,
+ rateType: "estimated",
+ direction: "direct",
+ timestamp: tx.createdAt,
+ updatedAt: DateTime.now(),
+ payInCurrency: tx.coin1.toUpperCase(),
+ payInAmount: tx.sendAmount.toString(),
+ payInAddress: tx.address,
+ payInNetwork: tx.network1,
+ payInExtraId: tx.memo,
+ payInTxid: "",
+ payOutCurrency: tx.coin2.toUpperCase(),
+ payOutAmount: tx.estimateAmount.toString(),
+ payOutAddress: tx.destinationAddress,
+ payOutNetwork: tx.network2,
+ payOutExtraId: "",
+ payOutTxid: "",
+ refundAddress: "",
+ refundExtraId: "",
+ status: tx.status,
+ exchangeName: exchangeName,
+ ),
+ );
+ } on ExchangeException catch (e) {
+ return ExchangeResponse(exception: e);
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+
+ @override
+ Future<ExchangeResponse<List<Trade>>> getTrades() async {
+ throw UnimplementedError(
+ "CypherGoat does not provide a trade history endpoint",
+ );
+ }
+
+ @override
+ Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
+ try {
+ final response = await CypherGoatAPI.getTransaction(cgid: trade.tradeId);
+
+ if (response.value == null) {
+ return ExchangeResponse(exception: response.exception);
+ }
+
+ final tx = response.value!;
+
+ return ExchangeResponse(
+ value: Trade(
+ uuid: trade.uuid,
+ tradeId: trade.tradeId,
+ rateType: trade.rateType,
+ direction: trade.direction,
+ timestamp: trade.timestamp,
+ updatedAt: DateTime.now(),
+ payInCurrency: tx.coin1.isNotEmpty
+ ? tx.coin1.toUpperCase()
+ : trade.payInCurrency,
+ payInAmount: tx.sendAmount > 0
+ ? tx.sendAmount.toString()
+ : trade.payInAmount,
+ payInAddress:
+ tx.address.isNotEmpty ? tx.address : trade.payInAddress,
+ payInNetwork: trade.payInNetwork,
+ payInExtraId: tx.memo.isNotEmpty ? tx.memo : trade.payInExtraId,
+ payInTxid: trade.payInTxid,
+ payOutCurrency: tx.coin2.isNotEmpty
+ ? tx.coin2.toUpperCase()
+ : trade.payOutCurrency,
+ payOutAmount: tx.estimateAmount > 0
+ ? tx.estimateAmount.toString()
+ : trade.payOutAmount,
+ payOutAddress: tx.destinationAddress.isNotEmpty
+ ? tx.destinationAddress
+ : trade.payOutAddress,
+ payOutNetwork: trade.payOutNetwork,
+ payOutExtraId: trade.payOutExtraId,
+ payOutTxid: trade.payOutTxid,
+ refundAddress: trade.refundAddress,
+ refundExtraId: trade.refundExtraId,
+ status: tx.status.isNotEmpty ? tx.status : trade.status,
+ exchangeName: exchangeName,
+ ),
+ );
+ } on ExchangeException catch (e) {
+ return ExchangeResponse(exception: e);
+ } catch (e) {
+ return ExchangeResponse(
+ exception: ExchangeException(
+ e.toString(),
+ ExchangeExceptionType.generic,
+ ),
+ );
+ }
+ }
+}
diff --git a/lib/services/exchange/cyphergoat/response_objects/cg_estimate.dart b/lib/services/exchange/cyphergoat/response_objects/cg_estimate.dart
new file mode 100644
index 0000000..6eedb6b
--- /dev/null
+++ b/lib/services/exchange/cyphergoat/response_objects/cg_estimate.dart
@@ -0,0 +1,54 @@
+class CgEstimateResult {
+ final String exchange;
+ final double amount;
+ final int kycScore;
+ final bool safeRouteOk;
+ final double safeRouteScore;
+
+ CgEstimateResult({
+ required this.exchange,
+ required this.amount,
+ required this.kycScore,
+ required this.safeRouteOk,
+ required this.safeRouteScore,
+ });
+
+ factory CgEstimateResult.fromMap(Map<String, dynamic> map) {
+ return CgEstimateResult(
+ exchange: map["Exchange"] as String? ?? "",
+ amount: (map["Amount"] as num?)?.toDouble() ?? 0.0,
+ kycScore: (map["KYCScore"] as num?)?.toInt() ?? 0,
+ safeRouteOk: map["SafeRouteOK"] as bool? ?? false,
+ safeRouteScore: (map["SafeRouteScore"] as num?)?.toDouble() ?? 0.0,
+ );
+ }
+}
+
+class CgEstimatesResponse {
+ final List<CgEstimateResult> results;
+ final double min;
+ final double tradeValueFiat;
+ final double tradeValueBtc;
+ final int estimateId;
+
+ CgEstimatesResponse({
+ required this.results,
+ required this.min,
+ required this.tradeValueFiat,
+ required this.tradeValueBtc,
+ required this.estimateId,
+ });
+
+ factory CgEstimatesResponse.fromMap(Map<String, dynamic> map) {
+ final resultsRaw = map["Results"] as List<dynamic>? ?? [];
+ return CgEstimatesResponse(
+ results: resultsRaw
+ .map((e) => CgEstimateResult.fromMap(Map<String, dynamic>.from(e as Map)))
+ .toList(),
+ min: (map["Min"] as num?)?.toDouble() ?? 0.0,
+ tradeValueFiat: (map["TradeValue_fiat"] as num?)?.toDouble() ?? 0.0,
+ tradeValueBtc: (map["TradeValue_btc"] as num?)?.toDouble() ?? 0.0,
+ estimateId: (map["EstimateId"] as num?)?.toInt() ?? 0,
+ );
+ }
+}
diff --git a/lib/services/exchange/cyphergoat/response_objects/cg_transaction.dart b/lib/services/exchange/cyphergoat/response_objects/cg_transaction.dart
new file mode 100644
index 0000000..e82817d
--- /dev/null
+++ b/lib/services/exchange/cyphergoat/response_objects/cg_transaction.dart
@@ -0,0 +1,90 @@
+class CgTransaction {
+ final String coin1;
+ final String coin2;
+ final String network1;
+ final String network2;
+ final String address;
+ final double estimateAmount;
+ final String provider;
+ final String id;
+ final double sendAmount;
+ final String track;
+ final String status;
+ final String kyc;
+ final String token;
+ final bool done;
+ final String cgid;
+ final DateTime createdAt;
+ final String affiliate;
+ final String memo;
+ final String source;
+ final String destinationAddress;
+ final bool payment;
+ final DateTime? completedAt;
+ final int estimateId;
+
+ CgTransaction({
+ required this.coin1,
+ required this.coin2,
+ required this.network1,
+ required this.network2,
+ required this.address,
+ required this.estimateAmount,
+ required this.provider,
+ required this.id,
+ required this.sendAmount,
+ required this.track,
+ required this.status,
+ required this.kyc,
+ required this.token,
+ required this.done,
+ required this.cgid,
+ required this.createdAt,
+ required this.affiliate,
+ required this.memo,
+ required this.source,
+ required this.destinationAddress,
+ required this.payment,
+ required this.completedAt,
+ required this.estimateId,
+ });
+
+ // Go's zero time ("0001-01-01T00:00:00Z") is returned when the field isn't
+ // set yet; treat it as now rather than storing year 1.
+ static DateTime _parseDate(String? s) {
+ if (s == null) return DateTime.now();
+ final dt = DateTime.tryParse(s);
+ if (dt == null || dt.year <= 1) return DateTime.now();
+ return dt;
+ }
+
+ factory CgTransaction.fromMap(Map<String, dynamic> map) {
+ return CgTransaction(
+ coin1: map["Coin1"] as String? ?? "",
+ coin2: map["Coin2"] as String? ?? "",
+ network1: map["Network1"] as String? ?? "",
+ network2: map["Network2"] as String? ?? "",
+ address: map["Address"] as String? ?? "",
+ estimateAmount: (map["EstimateAmount"] as num?)?.toDouble() ?? 0.0,
+ provider: map["Provider"] as String? ?? "",
+ id: map["Id"] as String? ?? "",
+ sendAmount: (map["SendAmount"] as num?)?.toDouble() ?? 0.0,
+ track: map["Track"] as String? ?? "",
+ status: map["Status"] as String? ?? "waiting",
+ kyc: map["KYC"] as String? ?? "",
+ token: map["Token"] as String? ?? "",
+ done: map["Done"] as bool? ?? false,
+ cgid: map["CGID"] as String? ?? "",
+ createdAt: _parseDate(map["CreatedAt"] as String?),
+ affiliate: map["Affiliate"] as String? ?? "",
+ memo: map["Memo"] as String? ?? "",
+ source: map["Source"] as String? ?? "",
+ destinationAddress: map["DestinationAddress"] as String? ?? "",
+ payment: map["Payment"] as bool? ?? false,
+ completedAt: map["CompletedAt"] != null
+ ? DateTime.tryParse(map["CompletedAt"] as String)
+ : null,
+ estimateId: (map["EstimateId"] as num?)?.toInt() ?? 0,
+ );
+ }
+}
diff --git a/lib/services/exchange/exchange.dart b/lib/services/exchange/exchange.dart
index 85a3f8f..18a1261 100644
--- a/lib/services/exchange/exchange.dart
+++ b/lib/services/exchange/exchange.dart
@@ -15,6 +15,7 @@ import '../../models/exchange/response_objects/range.dart';
import '../../models/exchange/response_objects/trade.dart';
import '../../models/isar/exchange_cache/currency.dart';
import 'change_now/change_now_exchange.dart';
+import 'cyphergoat/cyphergoat_exchange.dart';
import 'exchange_response.dart';
import 'exolix/exolix_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
@@ -41,6 +42,8 @@ abstract class Exchange {
return WizardSwapExchange.instance;
case ExolixExchange.exchangeName:
return ExolixExchange.instance;
+ case CypherGoatExchange.exchangeName:
+ return CypherGoatExchange.instance;
default:
final split = name.split(" ");
if (split.length >= 2) {
diff --git a/lib/services/exchange/exchange_data_loading_service.dart b/lib/services/exchange/exchange_data_loading_service.dart
index 4f067f8..7410a01 100644
--- a/lib/services/exchange/exchange_data_loading_service.dart
+++ b/lib/services/exchange/exchange_data_loading_service.dart
@@ -25,6 +25,7 @@ import '../../utilities/logger.dart';
import '../../utilities/prefs.dart';
import '../../utilities/stack_file_system.dart';
import 'change_now/change_now_exchange.dart';
+import 'cyphergoat/cyphergoat_exchange.dart';
import 'exolix/exolix_exchange.dart';
import 'nanswap/nanswap_exchange.dart';
import 'trocador/trocador_exchange.dart';
@@ -211,6 +212,7 @@ class ExchangeDataLoadingService {
loadNanswapCurrencies(),
loadWizardSwapCurrencies(),
loadExolixCurrencies(),
+ loadCypherGoatCurrencies(),
];
// If using Tor, don't load data for exchanges which don't support Tor.
@@ -372,6 +374,30 @@ class ExchangeDataLoadingService {
// }
// }
+ Future<void> loadCypherGoatCurrencies() async {
+ if (_isar == null) {
+ await initDB();
+ }
+ final responseCurrencies = await CypherGoatExchange.instance
+ .getAllCurrencies(false);
+
+ if (responseCurrencies.value != null) {
+ await (await isar).writeTxn(() async {
+ final idsToDelete = await (await isar).currencies
+ .where()
+ .exchangeNameEqualTo(CypherGoatExchange.exchangeName)
+ .idProperty()
+ .findAll();
+ await (await isar).currencies.deleteAll(idsToDelete);
+ await (await isar).currencies.putAll(responseCurrencies.value!);
+ });
+ } else {
+ Logging.instance.w(
+ "loadCypherGoatCurrencies: $responseCurrencies",
+ );
+ }
+ }
+
// Future<void> loadMajesticBankCurrencies() async {
// if (_isar == null) {
// await initDB();
diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart
index 9d322d3..0719d54 100644
--- a/lib/utilities/assets.dart
+++ b/lib/utilities/assets.dart
@@ -11,6 +11,7 @@
import 'package:flutter/material.dart';
import '../services/exchange/change_now/change_now_exchange.dart';
+import '../services/exchange/cyphergoat/cyphergoat_exchange.dart';
import '../services/exchange/exolix/exolix_exchange.dart';
import '../services/exchange/nanswap/nanswap_exchange.dart';
import '../services/exchange/simpleswap/simpleswap_exchange.dart';
@@ -52,6 +53,7 @@ class _EXCHANGE {
String get wizard => "${_path}wizard.svg";
String get exolix => "${_path}exolix.png";
+ String get cypherGoat => "${_path}cyphergoat.svg";
String getIconFor({required String exchangeName}) {
switch (exchangeName) {
@@ -69,6 +71,8 @@ class _EXCHANGE {
return wizard;
case ExolixExchange.exchangeName:
return exolix;
+ case CypherGoatExchange.exchangeName:
+ return cypherGoat;
default:
throw ArgumentError(
"Invalid exchange name passed to "
diff --git a/scripts/prebuild.ps1 b/scripts/prebuild.ps1
index b9ff36a..d1e5be5 100644
--- a/scripts/prebuild.ps1
+++ b/scripts/prebuild.ps1
@@ -2,7 +2,7 @@
$KEYS = "..\lib\external_api_keys.dart"
if (-not (Test-Path $KEYS)) {
Write-Host "prebuild.ps1: creating template lib/external_api_keys.dart file"
- "const kChangeNowApiKey = '';" + "`nconst kSimpleSwapApiKey = '';" + "`nconst kNanswapApiKey = '';" + "`nconst kNanoSwapRpcApiKey = '';" + "`nconst kWizSwapApiKey = '';" + "`nconst kShopInBitAccessKey = '';" + "`nconst kShopInBitPartnerSecret = '';" + "`nconst kCakePayApiToken = '';" + "`nconst kExolixApiKey = '';" | Out-File $KEYS -Encoding UTF8
+ "const kChangeNowApiKey = '';" + "`nconst kSimpleSwapApiKey = '';" + "`nconst kNanswapApiKey = '';" + "`nconst kNanoSwapRpcApiKey = '';" + "`nconst kWizSwapApiKey = '';" + "`nconst kShopInBitAccessKey = '';" + "`nconst kShopInBitPartnerSecret = '';" + "`nconst kCakePayApiToken = '';" + "`nconst kExolixApiKey = '';" + "`nconst kCypherGoatApiKey = '';" + "`nconst kCypherGoatAffiliate = '';" | Out-File $KEYS -Encoding UTF8
}
# Create template wallet test parameter files if they don't already exist
diff --git a/scripts/prebuild.sh b/scripts/prebuild.sh
index 6aeaca6..c6babf2 100755
--- a/scripts/prebuild.sh
+++ b/scripts/prebuild.sh
@@ -4,7 +4,7 @@
KEYS=../lib/external_api_keys.dart
if ! test -f "$KEYS"; then
echo 'prebuild.sh: creating template lib/external_api_keys.dart file'
- printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\nconst kNanswapApiKey = "";\nconst kNanoSwapRpcApiKey = "";\nconst kWizSwapApiKey = "";\nconst kShopInBitAccessKey = "";\nconst kShopInBitPartnerSecret = "";\nconst kCakePayApiToken = "";\nconst kExolixApiKey = "";\n' > $KEYS
+ printf 'const kChangeNowApiKey = "";\nconst kSimpleSwapApiKey = "";\nconst kNanswapApiKey = "";\nconst kNanoSwapRpcApiKey = "";\nconst kWizSwapApiKey = "";\nconst kShopInBitAccessKey = "";\nconst kShopInBitPartnerSecret = "";\nconst kCakePayApiToken = "";\nconst kExolixApiKey = "";\nconst kCypherGoatApiKey = "";\nconst kCypherGoatAffiliate = "";\n' > $KEYS
fi
# Create template wallet test parameter files if they don't already exist
Why this scored 32/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.