Merge branch 'majestic_bank' into paynyms

This commit is contained in:
julian 2023-02-07 11:16:26 -06:00
commit 62f7ebbc3c
15 changed files with 801 additions and 15 deletions

View file

@ -0,0 +1,13 @@
import 'package:stackwallet/exceptions/sw_exception.dart';
enum ExchangeExceptionType { generic, serializeResponseError }
class ExchangeException extends SWException {
ExchangeExceptionType type;
ExchangeException(super.message, this.type);
@override
String toString() {
return message;
}
}

View file

@ -0,0 +1,5 @@
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
class MBException extends ExchangeException {
MBException(super.message, super.type);
}

View file

@ -0,0 +1,19 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_object.dart';
class MBLimit extends MBObject {
MBLimit({
required this.currency,
required this.min,
required this.max,
});
final String currency;
final Decimal min;
final Decimal max;
@override
String toString() {
return "MBLimit: { $currency: { min: $min, max: $max } }";
}
}

View file

@ -0,0 +1 @@
abstract class MBObject {}

View file

@ -0,0 +1,43 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_object.dart';
enum MBOrderType {
fixed,
floating,
}
class MBOrder extends MBObject {
MBOrder({
required this.orderId,
required this.fromCurrency,
required this.fromAmount,
required this.receiveCurrency,
required this.receiveAmount,
required this.address,
required this.orderType,
required this.expiration,
required this.createdAt,
});
final String orderId;
final String fromCurrency;
final Decimal fromAmount;
final String receiveCurrency;
final String address;
final Decimal receiveAmount;
final MBOrderType orderType;
/// minutes
final int expiration;
final DateTime createdAt;
bool isExpired() =>
(DateTime.now().difference(createdAt) >= Duration(minutes: expiration));
@override
String toString() {
// todo: full toString
return orderId;
}
}

View file

@ -0,0 +1,21 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_object.dart';
class MBOrderCalculation extends MBObject {
MBOrderCalculation({
required this.fromCurrency,
required this.fromAmount,
required this.receiveCurrency,
required this.receiveAmount,
});
final String fromCurrency;
final Decimal fromAmount;
final String receiveCurrency;
final Decimal receiveAmount;
@override
String toString() {
return "MBOrderCalculation: { $fromCurrency: $fromAmount, $receiveCurrency: $receiveAmount }";
}
}

View file

@ -0,0 +1,32 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_object.dart';
class MBOrderStatus extends MBObject {
MBOrderStatus({
required this.orderId,
required this.status,
required this.fromCurrency,
required this.fromAmount,
required this.receiveCurrency,
required this.receiveAmount,
required this.address,
required this.received,
required this.confirmed,
});
final String orderId;
final String status;
final String fromCurrency;
final Decimal fromAmount;
final String receiveCurrency;
final Decimal receiveAmount;
final String address;
final Decimal received;
final Decimal confirmed;
@override
String toString() {
// todo: full toString
return status;
}
}

View file

@ -0,0 +1,15 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_object.dart';
class MBRate extends MBObject {
MBRate({required this.fromCurrency, required this.toCurrency, required this.rate,});
final String fromCurrency;
final String toCurrency;
final Decimal rate;
@override
String toString() {
return "MBRate: { $fromCurrency-$toCurrency: $rate }";
}
}

View file

@ -11,6 +11,8 @@ import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/background.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import '../../../services/exchange/majestic_bank/majestic_bank_api.dart';
class HiddenSettings extends StatelessWidget {
const HiddenSettings({Key? key}) : super(key: key);
@ -128,6 +130,48 @@ class HiddenSettings extends StatelessWidget {
),
);
}),
// const SizedBox(
// height: 12,
// ),
// Consumer(builder: (_, ref, __) {
// return GestureDetector(
// onTap: () async {
// final x =
// await MajesticBankAPI.instance.getRates();
// print(x);
// },
// child: RoundedWhiteContainer(
// child: Text(
// "Click me",
// style: STextStyles.button(context).copyWith(
// color: Theme.of(context)
// .extension<StackColors>()!
// .accentColorDark),
// ),
// ),
// );
// }),
const SizedBox(
height: 12,
),
Consumer(builder: (_, ref, __) {
return GestureDetector(
onTap: () async {
final x =
await MajesticBankAPI.instance.getLimits();
print(x);
},
child: RoundedWhiteContainer(
child: Text(
"Click me",
style: STextStyles.button(context).copyWith(
color: Theme.of(context)
.extension<StackColors>()!
.accentColorDark),
),
),
);
}),
const SizedBox(
height: 12,
),

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/change_now/cn_exchange_estimate.dart';
import 'package:stackwallet/models/exchange/change_now/estimated_exchange_amount.dart';

View file

@ -55,7 +55,7 @@ class ExchangeDataLoadingService {
}
} else {
Logging.instance.log(
"Failed to load changeNOW fixed rate markets: ${response3.exception?.errorMessage}",
"Failed to load changeNOW fixed rate markets: ${response3.exception?.message}",
level: LogLevel.Error);
ref.read(changeNowFixedInitialLoadStatusStateProvider.state).state =
@ -122,7 +122,7 @@ class ExchangeDataLoadingService {
}
} else {
Logging.instance.log(
"Failed to load changeNOW available floating rate pairs: ${response2.exception?.errorMessage}",
"Failed to load changeNOW available floating rate pairs: ${response2.exception?.message}",
level: LogLevel.Error);
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =
ChangeNowLoadStatus.failed;
@ -130,7 +130,7 @@ class ExchangeDataLoadingService {
}
} else {
Logging.instance.log(
"Failed to load changeNOW currencies: ${response.exception?.errorMessage}",
"Failed to load changeNOW currencies: ${response.exception?.message}",
level: LogLevel.Error);
await Future<void>.delayed(const Duration(seconds: 3));
ref.read(changeNowEstimatedInitialLoadStatusStateProvider.state).state =

View file

@ -1,15 +1,4 @@
enum ExchangeExceptionType { generic, serializeResponseError }
class ExchangeException implements Exception {
String errorMessage;
ExchangeExceptionType type;
ExchangeException(this.errorMessage, this.type);
@override
String toString() {
return errorMessage;
}
}
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
class ExchangeResponse<T> {
late final T? value;

View file

@ -0,0 +1,339 @@
import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_limit.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order_calculation.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order_status.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_rate.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/utilities/logger.dart';
class MajesticBankAPI {
static const String scheme = "https";
static const String authority = "majesticbank.sc";
static const String version = "v1";
static const String refCode = "fixme";
MajesticBankAPI._();
static final MajesticBankAPI _instance = MajesticBankAPI._();
static MajesticBankAPI get instance => _instance;
/// set this to override using standard http client. Useful for testing
http.Client? client;
Uri _buildUri({required String endpoint, Map<String, String>? params}) {
return Uri.https(authority, "/api/$version/$endpoint", params);
}
Future<dynamic> _makeGetRequest(Uri uri) async {
final client = this.client ?? http.Client();
int code = -1;
try {
final response = await client.get(
uri,
);
code = response.statusCode;
print(response.body);
final parsed = jsonDecode(response.body);
return parsed;
} catch (e, s) {
Logging.instance.log(
"_makeRequest($uri) HTTP:$code threw: $e\n$s",
level: LogLevel.Error,
);
rethrow;
}
}
Future<ExchangeResponse<List<MBRate>>> getRates() async {
final uri = _buildUri(
endpoint: "rates",
);
try {
final jsonObject = await _makeGetRequest(uri);
final map = Map<String, dynamic>.from(jsonObject as Map);
final List<MBRate> rates = [];
for (final key in map.keys) {
final currencies = key.split("-");
if (currencies.length == 2) {
final rate = MBRate(
fromCurrency: currencies.first,
toCurrency: currencies.last,
rate: Decimal.parse(map[key].toString()),
);
rates.add(rate);
}
}
return ExchangeResponse(value: rates);
} catch (e, s) {
Logging.instance.log("getRates exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<MBLimit>> getLimit({
required String fromCurrency,
}) async {
final uri = _buildUri(
endpoint: "limits",
params: {
"from_currency": fromCurrency,
},
);
try {
final jsonObject = await _makeGetRequest(uri);
final map = Map<String, dynamic>.from(jsonObject as Map);
final limit = MBLimit(
currency: fromCurrency,
min: Decimal.parse(map["min"].toString()),
max: Decimal.parse(map["max"].toString()),
);
return ExchangeResponse(value: limit);
} catch (e, s) {
Logging.instance
.log("getLimits exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<List<MBLimit>>> getLimits() async {
final uri = _buildUri(
endpoint:
"rates", // limits are included in the rates call for some reason???
);
try {
final jsonObject = await _makeGetRequest(uri);
final map = Map<String, dynamic>.from(jsonObject as Map)["limits"] as Map;
final List<MBLimit> limits = [];
for (final key in map.keys) {
final limit = MBLimit(
currency: key as String,
min: Decimal.parse(map[key]["min"].toString()),
max: Decimal.parse(map[key]["max"].toString()),
);
limits.add(limit);
}
return ExchangeResponse(value: limits);
} catch (e, s) {
Logging.instance
.log("getLimits exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// If [reversed] then the amount is the expected receive_amount, otherwise
/// the amount is assumed to be the from_amount.
Future<ExchangeResponse<MBOrderCalculation>> calculateOrder({
required String amount,
required bool reversed,
required String fromCurrency,
required String receiveCurrency,
}) async {
final params = {
"from_currency": fromCurrency,
"receive_currency": receiveCurrency,
};
if (reversed) {
params["receive_amount"] = amount;
} else {
params["from_amount"] = amount;
}
final uri = _buildUri(
endpoint: "calculate",
params: params,
);
try {
final jsonObject = await _makeGetRequest(uri);
final map = Map<String, dynamic>.from(jsonObject as Map);
final result = MBOrderCalculation(
fromCurrency: map["from_currency"] as String,
fromAmount: Decimal.parse(map["from_amount"].toString()),
receiveCurrency: map["receive_currency"] as String,
receiveAmount: Decimal.parse(map["receive_amount"].toString()),
);
return ExchangeResponse(value: result);
} catch (e, s) {
Logging.instance
.log("calculateOrder exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<MBOrder>> createOrder({
required String fromAmount,
required String fromCurrency,
required String receiveCurrency,
required String receiveAddress,
}) async {
final params = {
"from_amount": fromAmount,
"from_currency": fromCurrency,
"receive_currency": receiveCurrency,
"receive_address": receiveAddress,
"referral_code": refCode,
};
final uri = _buildUri(endpoint: "exchange", params: params);
try {
final now = DateTime.now();
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
final order = MBOrder(
orderId: json["trx"] as String,
fromCurrency: json["from_currency"] as String,
fromAmount: Decimal.parse(json["from_amount"].toString()),
receiveCurrency: json["receive_currency"] as String,
receiveAmount: Decimal.parse(json["receive_amount"].toString()),
address: json["address"] as String,
orderType: MBOrderType.floating,
expiration: json["expiration"] as int,
createdAt: now,
);
return ExchangeResponse(value: order);
} catch (e, s) {
Logging.instance
.log("createOrder exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
/// Fixed rate for 10 minutes, useful for payments.
/// If [reversed] then the amount is the expected receive_amount, otherwise
/// the amount is assumed to be the from_amount.
Future<ExchangeResponse<MBOrder>> createFixedRateOrder({
required String amount,
required String fromCurrency,
required String receiveCurrency,
required String receiveAddress,
required bool reversed,
}) async {
final params = {
"from_currency": fromCurrency,
"receive_currency": receiveCurrency,
"receive_address": receiveAddress,
"referral_code": refCode,
};
if (reversed) {
params["receive_amount"] = amount;
} else {
params["from_amount"] = amount;
}
final uri = _buildUri(endpoint: "pay", params: params);
try {
final now = DateTime.now();
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
final order = MBOrder(
orderId: json["trx"] as String,
fromCurrency: json["from_currency"] as String,
fromAmount: Decimal.parse(json["from_amount"].toString()),
receiveCurrency: json["receive_currency"] as String,
receiveAmount: Decimal.parse(json["receive_amount"].toString()),
address: json["address"] as String,
orderType: MBOrderType.fixed,
expiration: json["expiration"] as int,
createdAt: now,
);
return ExchangeResponse(value: order);
} catch (e, s) {
Logging.instance
.log("createFixedRateOrder exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
Future<ExchangeResponse<MBOrderStatus>> trackOrder({
required String orderId,
}) async {
final uri = _buildUri(endpoint: "track", params: {
"trx": orderId,
});
try {
final jsonObject = await _makeGetRequest(uri);
final json = Map<String, dynamic>.from(jsonObject as Map);
final status = MBOrderStatus(
orderId: json["trx"] as String,
status: json["status"] as String,
fromCurrency: json["from_currency"] as String,
fromAmount: Decimal.parse(json["from_amount"].toString()),
receiveCurrency: json["receive_currency"] as String,
receiveAmount: Decimal.parse(json["receive_amount"].toString()),
address: json["address"] as String,
received: Decimal.parse(json["received"].toString()),
confirmed: Decimal.parse(json["confirmed"].toString()),
);
return ExchangeResponse(value: status);
} catch (e, s) {
Logging.instance
.log("createOrder exception: $e\n$s", level: LogLevel.Error);
return ExchangeResponse(
exception: ExchangeException(
e.toString(),
ExchangeExceptionType.generic,
),
);
}
}
}

View file

@ -0,0 +1,263 @@
import 'package:decimal/decimal.dart';
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/exceptions/exchange/majestic_bank/mb_exception.dart';
import 'package:stackwallet/models/exchange/majestic_bank/mb_order.dart';
import 'package:stackwallet/models/exchange/response_objects/currency.dart';
import 'package:stackwallet/models/exchange/response_objects/estimate.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';
import 'package:stackwallet/models/exchange/response_objects/range.dart';
import 'package:stackwallet/models/exchange/response_objects/trade.dart';
import 'package:stackwallet/services/exchange/exchange.dart';
import 'package:stackwallet/services/exchange/exchange_response.dart';
import 'package:stackwallet/services/exchange/majestic_bank/majestic_bank_api.dart';
import 'package:uuid/uuid.dart';
class MajesticBankExchange extends Exchange {
static const exchangeName = "MajesticBank";
@override
Future<ExchangeResponse<Trade>> createTrade({
required String from,
required String to,
required bool fixedRate,
required Decimal amount,
required String addressTo,
String? extraId,
required String addressRefund,
required String refundExtraId,
String? rateId,
required bool reversed,
}) async {
ExchangeResponse<MBOrder>? response;
if (fixedRate) {
response = await MajesticBankAPI.instance.createFixedRateOrder(
amount: amount.toString(),
fromCurrency: from,
receiveCurrency: to,
receiveAddress: addressTo,
reversed: reversed,
);
} else {
if (reversed) {
return ExchangeResponse(
exception: MBException(
"Reversed trade not available",
ExchangeExceptionType.generic,
),
);
}
response = await MajesticBankAPI.instance.createOrder(
fromAmount: amount.toString(),
fromCurrency: from,
receiveCurrency: to,
receiveAddress: addressTo,
);
}
if (response.value != null) {
final order = response.value!;
final trade = Trade(
uuid: const Uuid().v1(),
tradeId: order.orderId,
rateType: fixedRate ? "fixed" : "floating",
direction: reversed ? "reversed" : "direct",
timestamp: order.createdAt,
updatedAt: order.createdAt,
payInCurrency: order.fromCurrency,
payInAmount: order.fromAmount.toString(),
payInAddress: order.address,
payInNetwork: "",
payInExtraId: "",
payInTxid: "",
payOutCurrency: order.receiveCurrency,
payOutAmount: order.receiveAmount.toString(),
payOutAddress: addressTo,
payOutNetwork: "",
payOutExtraId: "",
payOutTxid: "",
refundAddress: addressRefund,
refundExtraId: refundExtraId,
status: "Waiting",
exchangeName: exchangeName,
);
return ExchangeResponse(value: trade);
} else {
return ExchangeResponse(exception: response.exception!);
}
}
@override
Future<ExchangeResponse<List<Currency>>> getAllCurrencies(
bool fixedRate,
) async {
final response = await MajesticBankAPI.instance.getLimits();
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final List<Currency> currencies = [];
final limits = response.value!;
for (final limit in limits) {
final currency = Currency(
ticker: limit.currency,
name: limit.currency,
network: "",
image: "",
hasExternalId: false,
isFiat: false,
featured: false,
isStable: false,
supportsFixedRate: true,
);
currencies.add(currency);
}
return ExchangeResponse(value: currencies);
}
@override
Future<ExchangeResponse<List<Pair>>> getAllPairs(bool fixedRate) async {
final response = await MajesticBankAPI.instance.getRates();
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final List<Pair> pairs = [];
final rates = response.value!;
for (final rate in rates) {
final pair = Pair(
from: rate.fromCurrency,
fromNetwork: "",
to: rate.toCurrency,
toNetwork: "",
fixedRate: true,
floatingRate: true,
);
pairs.add(pair);
}
return ExchangeResponse(value: pairs);
}
@override
Future<ExchangeResponse<Estimate>> getEstimate(
String from,
String to,
Decimal amount,
bool fixedRate,
bool reversed,
) async {
final response = await MajesticBankAPI.instance.calculateOrder(
amount: amount.toString(),
reversed: reversed,
fromCurrency: from,
receiveCurrency: to,
);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final calc = response.value!;
final estimate = Estimate(
estimatedAmount: reversed ? calc.fromAmount : calc.receiveAmount,
fixedRate: fixedRate,
reversed: reversed,
);
return ExchangeResponse(value: estimate);
}
@override
Future<ExchangeResponse<List<Pair>>> getPairsFor(
String currency,
bool fixedRate,
) async {
final response = await getAllPairs(fixedRate);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final pairs = response.value!.where(
(e) =>
e.from.toUpperCase() == currency.toUpperCase() ||
e.to.toUpperCase() == currency.toUpperCase(),
);
return ExchangeResponse(value: pairs.toList());
}
@override
Future<ExchangeResponse<Range>> getRange(
String from,
String to,
bool fixedRate,
) async {
final response =
await MajesticBankAPI.instance.getLimit(fromCurrency: from);
if (response.value == null) {
return ExchangeResponse(exception: response.exception);
}
final limit = response.value!;
final range = Range(min: limit.min, max: limit.max);
return ExchangeResponse(value: range);
}
@override
Future<ExchangeResponse<Trade>> getTrade(String tradeId) async {
// TODO: implement getTrade
throw UnimplementedError();
}
@override
Future<ExchangeResponse<List<Trade>>> getTrades() async {
// TODO: implement getTrades
throw UnimplementedError();
}
@override
String get name => exchangeName;
@override
Future<ExchangeResponse<Trade>> updateTrade(Trade trade) async {
final response = await MajesticBankAPI.instance.trackOrder(
orderId: trade.tradeId,
);
if (response.value != null) {
final status = response.value!;
final updatedTrade = Trade(
uuid: trade.uuid,
tradeId: status.orderId,
rateType: trade.rateType,
direction: trade.direction,
timestamp: trade.timestamp,
updatedAt: DateTime.now(),
payInCurrency: status.fromCurrency,
payInAmount: status.fromAmount.toString(),
payInAddress: status.address,
payInNetwork: trade.payInNetwork,
payInExtraId: trade.payInExtraId,
payInTxid: trade.payInTxid,
payOutCurrency: status.receiveCurrency,
payOutAmount: status.receiveAmount.toString(),
payOutAddress: trade.payOutAddress,
payOutNetwork: trade.payOutNetwork,
payOutExtraId: trade.payOutExtraId,
payOutTxid: trade.payOutTxid,
refundAddress: trade.refundAddress,
refundExtraId: trade.refundExtraId,
status: status.status,
exchangeName: exchangeName,
);
return ExchangeResponse(value: updatedTrade);
} else {
return ExchangeResponse(exception: response.exception);
}
}
}

View file

@ -3,6 +3,7 @@ import 'dart:convert';
import 'package:decimal/decimal.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:stackwallet/exceptions/exchange/exchange_exception.dart';
import 'package:stackwallet/external_api_keys.dart';
import 'package:stackwallet/models/exchange/response_objects/fixed_rate_market.dart';
import 'package:stackwallet/models/exchange/response_objects/pair.dart';