diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_address.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_address.dart new file mode 100644 index 00000000..123f92cd --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_address.dart @@ -0,0 +1,52 @@ +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; + +/// Structured address used within orderbook responses. +class OrderAddress { + const OrderAddress({required this.addressData, required this.addressType}); + + factory OrderAddress.fromJson(JsonMap json) { + final addressData = json.valueOrNull('address_data'); + final typeValue = json.valueOrNull('address_type'); + + if (typeValue == null) { + throw ArgumentError('Key "address_type" not found in Map'); + } + + return OrderAddress( + addressData: addressData, + addressType: OrderAddressType.fromJson(typeValue), + ); + } + + /// Address payload when nested under `address_data`. + final String? addressData; + + /// Address type descriptor (e.g. Transparent, Shielded). + final OrderAddressType addressType; + + Map toJson() => { + 'address_data': addressData, + 'address_type': addressType.toJson(), + }; +} + +/// Available address types returned by the orderbook API. +enum OrderAddressType { + transparent('Transparent'), + shielded('Shielded'); + + const OrderAddressType(this.value); + + final String value; + + /// Parses an [OrderAddressType] from its JSON representation. + static OrderAddressType fromJson(String source) { + return OrderAddressType.values.firstWhere( + (type) => type.value.toLowerCase() == source.toLowerCase(), + orElse: () => throw ArgumentError('Unknown address type: $source'), + ); + } + + /// Converts this enum to its JSON string representation. + String toJson() => value; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_info.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_info.dart index 1f6bf326..068c0466 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_info.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/orderbook/order_info.dart @@ -1,187 +1,141 @@ +import 'package:komodo_defi_rpc_methods/src/common_structures/orderbook/order_address.dart'; +import 'package:komodo_defi_rpc_methods/src/common_structures/primitive/numeric_value.dart'; +import 'package:komodo_defi_rpc_methods/src/common_structures/trading/order_status.dart' + show OrderConfirmationSettings; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -import 'package:rational/rational.dart'; -import '../primitive/mm2_rational.dart'; -import '../primitive/fraction.dart'; /// Represents information about an order in the orderbook. -/// +/// /// This class contains all the essential details about a trading order, /// including pricing, volume constraints, and metadata about the order creator. /// It's used to represent both bid and ask orders in orderbook responses. class OrderInfo { /// Creates a new [OrderInfo] instance. - /// - /// All parameters are required and represent core order attributes: - /// - [uuid]: Unique identifier for the order - /// - [price]: The price per unit in rel coin - /// - [maxVolume]: Maximum volume available for this order - /// - [minVolume]: Minimum volume that must be traded - /// - [pubkey]: Public key of the order creator - /// - [age]: Age of the order in seconds - /// - [zcredits]: Zero-knowledge credits associated with the order - /// - [coin]: The coin being offered in this order - /// - [address]: The address associated with this order - OrderInfo({ - required this.uuid, - required this.price, - required this.maxVolume, - required this.minVolume, - required this.pubkey, - required this.age, - required this.zcredits, - required this.coin, - required this.address, - this.priceFraction, - this.priceRat, - this.maxVolumeFraction, - this.maxVolumeRat, - this.minVolumeFraction, - this.minVolumeRat, + /// + /// All parameters are optional to allow partial payloads from the API. + const OrderInfo({ + this.uuid, + this.address, + this.baseMaxVolume, + this.baseMaxVolumeAggregated, + this.baseMinVolume, + this.coin, + this.confSettings, + this.isMine, + this.price, + this.pubkey, + this.relMaxVolume, + this.relMaxVolumeAggregated, + this.relMinVolume, }); /// Creates an [OrderInfo] instance from a JSON map. - /// - /// Expects the following keys in the JSON: - /// - `uuid`: String - Unique order identifier - /// - `price`: String - Price per unit - /// - `max_volume`: String - Maximum tradeable volume - /// - `min_volume`: String - Minimum tradeable volume - /// - `pubkey`: String - Order creator's public key - /// - `age`: int - Order age in seconds - /// - `zcredits`: int - Zero-knowledge credits - /// - `coin`: String - Coin ticker - /// - `address`: String - Associated address + /// + /// Parses only the v2 orderbook schema fields without legacy fallbacks. factory OrderInfo.fromJson(JsonMap json) { + final priceJson = json.valueOrNull('price'); + final baseMaxVolumeJson = json.valueOrNull('base_max_volume'); + final baseMaxVolumeAggrJson = json.valueOrNull( + 'base_max_volume_aggr', + ); + final baseMinVolumeJson = json.valueOrNull('base_min_volume'); + final relMaxVolumeJson = json.valueOrNull('rel_max_volume'); + final relMaxVolumeAggrJson = json.valueOrNull( + 'rel_max_volume_aggr', + ); + final relMinVolumeJson = json.valueOrNull('rel_min_volume'); + final addressJson = json.valueOrNull('address'); + final confSettingsJson = json.valueOrNull('conf_settings'); + return OrderInfo( - uuid: json.value('uuid'), - price: json.value('price'), - maxVolume: json.value('max_volume'), - minVolume: json.value('min_volume'), - pubkey: json.value('pubkey'), - age: json.value('age'), - zcredits: json.value('zcredits'), - coin: json.value('coin'), - address: json.value('address'), - priceFraction: - json.valueOrNull('price_fraction') != null - ? Fraction.fromJson(json.value('price_fraction')) - : null, - priceRat: - json.valueOrNull>('price_rat') != null - ? rationalFromMm2(json.value>('price_rat')) - : null, - maxVolumeFraction: - json.valueOrNull('max_volume_fraction') != null - ? Fraction.fromJson(json.value('max_volume_fraction')) - : null, - maxVolumeRat: - json.valueOrNull>('max_volume_rat') != null - ? rationalFromMm2(json.value>('max_volume_rat')) - : null, - minVolumeFraction: - json.valueOrNull('min_volume_fraction') != null - ? Fraction.fromJson(json.value('min_volume_fraction')) - : null, - minVolumeRat: - json.valueOrNull>('min_volume_rat') != null - ? rationalFromMm2(json.value>('min_volume_rat')) - : null, + uuid: json.valueOrNull('uuid'), + coin: json.valueOrNull('coin'), + pubkey: json.valueOrNull('pubkey'), + isMine: json.valueOrNull('is_mine'), + price: priceJson != null ? NumericValue.fromJson(priceJson) : null, + baseMaxVolume: baseMaxVolumeJson != null + ? NumericValue.fromJson(baseMaxVolumeJson) + : null, + baseMaxVolumeAggregated: baseMaxVolumeAggrJson != null + ? NumericValue.fromJson(baseMaxVolumeAggrJson) + : null, + baseMinVolume: baseMinVolumeJson != null + ? NumericValue.fromJson(baseMinVolumeJson) + : null, + relMaxVolume: relMaxVolumeJson != null + ? NumericValue.fromJson(relMaxVolumeJson) + : null, + relMaxVolumeAggregated: relMaxVolumeAggrJson != null + ? NumericValue.fromJson(relMaxVolumeAggrJson) + : null, + relMinVolume: relMinVolumeJson != null + ? NumericValue.fromJson(relMinVolumeJson) + : null, + address: addressJson != null ? OrderAddress.fromJson(addressJson) : null, + confSettings: confSettingsJson != null + ? OrderConfirmationSettings.fromJson(confSettingsJson) + : null, ); } - /// Unique identifier for this order. - /// - /// This UUID is used to reference the order in subsequent operations - /// such as order matching or cancellation. - final String uuid; - - /// The price per unit for this order. - /// - /// Expressed as a string to maintain precision. This represents the - /// exchange rate between the base and rel coins. - final String price; - - /// Maximum volume available for trading in this order. - /// - /// This is the total amount of the coin that can be traded through - /// this order. Expressed as a string to maintain precision. - final String maxVolume; - - /// Minimum volume that must be traded. - /// - /// Orders cannot be partially filled below this threshold. This helps - /// prevent dust trades and ensures economically viable transactions. - /// Expressed as a string to maintain precision. - final String minVolume; - - /// Public key of the order creator. - /// - /// This identifies the node that created the order and is used for - /// P2P communication during swap negotiation. - final String pubkey; - - /// Age of the order in seconds. - /// - /// Indicates how long ago this order was created. Useful for sorting - /// orders by recency or implementing time-based order preferences. - final int age; - - /// Zero-knowledge credits associated with this order. - /// - /// Used in privacy-enhanced trading to manage reputation and trading - /// privileges without revealing identity. - final int zcredits; - - /// The coin ticker for this order. - /// - /// Identifies which coin is being offered in this order. - final String coin; - - /// The address associated with this order. - /// - /// This is typically the address that will receive funds in a swap - /// involving this order. - final String address; - - /// Optional fractional representation of the price - final Fraction? priceFraction; - - /// Optional rational representation of the price - final Rational? priceRat; - - /// Optional fractional representation of the maximum volume - final Fraction? maxVolumeFraction; - - /// Optional rational representation of the maximum volume - final Rational? maxVolumeRat; - - /// Optional fractional representation of the minimum volume - final Fraction? minVolumeFraction; - - /// Optional rational representation of the minimum volume - final Rational? minVolumeRat; + /// Unique identifier for this order, if provided. + final String? uuid; + + /// Optional structured address information for the order maker. + final OrderAddress? address; + + /// Optional maximum base volume. + final NumericValue? baseMaxVolume; + + /// Optional aggregated maximum base volume across orderbook depth. + final NumericValue? baseMaxVolumeAggregated; + + /// Optional minimum base volume. + final NumericValue? baseMinVolume; + + /// Optional coin ticker. + final String? coin; + + /// Optional confirmation settings supplied by the API. + final OrderConfirmationSettings? confSettings; + + /// Indicates whether the order belongs to the current wallet. + final bool? isMine; + + /// Optional price for the order. + final NumericValue? price; + + /// Optional public key of the order creator. + final String? pubkey; + + /// Optional maximum rel volume. + final NumericValue? relMaxVolume; + + /// Optional aggregated maximum rel volume across orderbook depth. + final NumericValue? relMaxVolumeAggregated; + + /// Optional minimum rel volume. + final NumericValue? relMinVolume; /// Converts this [OrderInfo] instance to a JSON map. - /// + /// /// The resulting map can be serialized to JSON and will contain all /// the order information in the expected API format. - Map toJson() => { - 'uuid': uuid, - 'price': price, - 'max_volume': maxVolume, - 'min_volume': minVolume, - 'pubkey': pubkey, - 'age': age, - 'zcredits': zcredits, - 'coin': coin, - 'address': address, - if (priceFraction != null) 'price_fraction': priceFraction!.toJson(), - if (priceRat != null) 'price_rat': rationalToMm2(priceRat!), - if (maxVolumeFraction != null) - 'max_volume_fraction': maxVolumeFraction!.toJson(), - if (maxVolumeRat != null) 'max_volume_rat': rationalToMm2(maxVolumeRat!), - if (minVolumeFraction != null) - 'min_volume_fraction': minVolumeFraction!.toJson(), - if (minVolumeRat != null) 'min_volume_rat': rationalToMm2(minVolumeRat!), - }; -} \ No newline at end of file + Map toJson() { + return { + 'uuid': ?uuid, + 'coin': ?coin, + 'pubkey': ?pubkey, + 'is_mine': ?isMine, + 'price': ?price?.toJson(), + 'base_max_volume': ?baseMaxVolume?.toJson(), + 'base_max_volume_aggr': ?baseMaxVolumeAggregated?.toJson(), + 'base_min_volume': ?baseMinVolume?.toJson(), + 'rel_max_volume': ?relMaxVolume?.toJson(), + 'rel_max_volume_aggr': ?relMaxVolumeAggregated?.toJson(), + 'rel_min_volume': ?relMinVolume?.toJson(), + 'address': ?address?.toJson(), + 'conf_settings': ?confSettings?.toJson(), + }; + } +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/pagination.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/pagination.dart index 1c5f072b..97597d44 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/pagination.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/pagination/pagination.dart @@ -1,5 +1,18 @@ class Pagination { Pagination({this.fromId, this.pageNumber}); + + factory Pagination.fromJson(Map json) { + final dynamic rawFromId = + json['FromId'] ?? json['from_id'] ?? json['fromId']; + final dynamic rawPageNumber = + json['PageNumber'] ?? json['page_number'] ?? json['pageNumber']; + + return Pagination( + fromId: rawFromId?.toString(), + pageNumber: rawPageNumber is num ? rawPageNumber.toInt() : null, + ); + } + final String? fromId; final int? pageNumber; diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/numeric_value.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/numeric_value.dart index 76b602d0..5c9f7420 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/numeric_value.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/primitive/numeric_value.dart @@ -1,25 +1,82 @@ -// class NumericValue { -// final String decimal; -// final List> rational; -// final Map fraction; - -// NumericValue({ -// required this.decimal, -// required this.rational, -// required this.fraction, -// }); - -// factory NumericValue.fromJson(Map json) => NumericValue( -// decimal: json['decimal'], -// rational: List>.from( -// json['rational'].map((x) => List.from(x))), -// fraction: Map.from(json['fraction']) -// .map((k, v) => MapEntry(k, v.toString())), -// ); - -// Map toJson() => { -// 'decimal': decimal, -// 'rational': rational, -// 'fraction': fraction, -// }; -// } +import 'package:komodo_defi_rpc_methods/src/common_structures/primitive/fraction.dart'; +import 'package:komodo_defi_rpc_methods/src/common_structures/primitive/mm2_rational.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:rational/rational.dart'; + +/// Represents a numeric value returned by MM2 APIs that can include +/// decimal, fraction, and rational representations. +class NumericValue { + NumericValue({required this.decimal, this.fraction, this.rational}); + + /// Parses a [NumericValue] from a JSON map. + factory NumericValue.fromJson(JsonMap json) { + final decimalValue = + json.valueOrNull('decimal') ?? json['decimal']?.toString(); + + if (decimalValue == null) { + throw ArgumentError('Key "decimal" not found in Map'); + } + + final fractionJson = json.valueOrNull('fraction'); + final rationalJson = json.valueOrNull>('rational'); + + return NumericValue( + decimal: decimalValue, + fraction: fractionJson != null ? Fraction.fromJson(fractionJson) : null, + rational: rationalJson != null ? rationalFromMm2(rationalJson) : null, + ); + } + + /// Attempts to parse a [NumericValue] from any supported JSON structure. + /// + /// Returns `null` if the input is null or cannot be parsed. + static NumericValue? tryParse(dynamic data) { + if (data == null) return null; + if (data is NumericValue) return data; + + if (data is String) { + return NumericValue(decimal: data); + } + + if (data is num) { + return NumericValue(decimal: data.toString()); + } + + JsonMap? asMap; + + if (data is JsonMap) { + asMap = data; + } else if (data is Map) { + asMap = {}; + data.forEach((key, value) { + asMap![key.toString()] = value; + }); + } + + if (asMap != null) { + try { + return NumericValue.fromJson(asMap); + } catch (_) { + return null; + } + } + + return null; + } + + /// Decimal string representation of the numeric value. + final String decimal; + + /// Fractional representation, if available. + final Fraction? fraction; + + /// Rational representation, if available. + final Rational? rational; + + /// Converts this numeric value back to JSON format used by MM2 APIs. + Map toJson() => { + 'decimal': decimal, + 'fraction': ?fraction?.toJson(), + if (rational != null) 'rational': rationalToMm2(rational!), + }; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/transaction_history/my_tx_history.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/transaction_history/my_tx_history.dart index 58f9fdde..c4ce56cf 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/transaction_history/my_tx_history.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/transaction_history/my_tx_history.dart @@ -87,11 +87,13 @@ class MyTxHistoryResponse extends BaseResponse { required this.total, required this.totalPages, required this.pageNumber, + required this.pagingOptions, required this.transactions, }); factory MyTxHistoryResponse.parse(Map json) { final result = json.value('result'); + final pagingOptionsJson = result.valueOrNull('paging_options'); return MyTxHistoryResponse( mmrpc: json.valueOrNull('mmrpc'), currentBlock: result.value('current_block'), @@ -104,11 +106,13 @@ class MyTxHistoryResponse extends BaseResponse { total: result.value('total'), totalPages: result.value('total_pages'), pageNumber: result.valueOrNull('page_number'), - transactions: - result - .value('transactions') - .map(TransactionInfo.fromJson) - .toList(), + pagingOptions: pagingOptionsJson != null + ? Pagination.fromJson(pagingOptionsJson) + : null, + transactions: result + .value('transactions') + .map(TransactionInfo.fromJson) + .toList(), ); } @@ -122,6 +126,7 @@ class MyTxHistoryResponse extends BaseResponse { total: 0, totalPages: 0, pageNumber: null, + pagingOptions: null, transactions: const [], ); @@ -133,6 +138,7 @@ class MyTxHistoryResponse extends BaseResponse { final int total; final int totalPages; final int? pageNumber; + final Pagination? pagingOptions; final List transactions; @override @@ -147,6 +153,7 @@ class MyTxHistoryResponse extends BaseResponse { 'total': total, 'total_pages': totalPages, if (pageNumber != null) 'page_number': pageNumber, + if (pagingOptions != null) 'paging_options': pagingOptions!.toJson(), 'transactions': transactions.map((tx) => tx.toJson()).toList(), }, }; diff --git a/packages/komodo_defi_rpc_methods/test/fixtures/orderbook/orderbook_response.json b/packages/komodo_defi_rpc_methods/test/fixtures/orderbook/orderbook_response.json new file mode 100644 index 00000000..315cb989 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/test/fixtures/orderbook/orderbook_response.json @@ -0,0 +1,246 @@ +{ + "mmrpc": "2.0", + "result": { + "asks": [ + { + "coin": "DGB", + "address": { + "address_type": "Transparent", + "address_data": "DEsCggcN3WNmaTkF2WpqoMQqx4JGQrLbPS" + }, + "price": { + "decimal": "0.0002658065", + "rational": [ + [1, [531613]], + [1, [2000000000]] + ], + "fraction": { + "numer": "531613", + "denom": "2000000000" + } + }, + "pubkey": "03de96cb66dcfaceaa8b3d4993ce8914cd5fe84e3fd53cefdae45add8032792a12", + "uuid": "1115d7f2-a7b9-4ab1-913f-497db2549a2b", + "is_mine": false, + "base_max_volume": { + "decimal": "90524.256020352", + "rational": [ + [1, [2846113615, 164]], + [1, [7812500]] + ], + "fraction": { + "numer": "707220750159", + "denom": "7812500" + } + }, + "base_min_volume": { + "decimal": "0.3762135237475381527539770472129161626973004798603495399849138376977237200745655204067620618758382508", + "rational": [ + [1, [200000]], + [1, [531613]] + ], + "fraction": { + "numer": "200000", + "denom": "531613" + } + }, + "rel_max_volume": { + "decimal": "24.061935657873693888", + "rational": [ + [1, [4213143411, 87536811]], + [1, [3466432512, 3637978]] + ], + "fraction": { + "numer": "375967744654276467", + "denom": "15625000000000000" + } + }, + "rel_min_volume": { + "decimal": "0.0001", + "rational": [ + [1, [1]], + [1, [10000]] + ], + "fraction": { + "numer": "1", + "denom": "10000" + } + }, + "conf_settings": { + "base_confs": 7, + "base_nota": false, + "rel_confs": 2, + "rel_nota": false + }, + "base_max_volume_aggr": { + "decimal": "133319.023345413", + "rational": [ + [1, [3238477573, 31040]], + [1, [1000000000]] + ], + "fraction": { + "numer": "133319023345413", + "denom": "1000000000" + } + }, + "rel_max_volume_aggr": { + "decimal": "35.2500366381728643576", + "rational": [ + [1, [473921343, 1669176307, 2]], + [1, [2436694016, 291038304]] + ], + "fraction": { + "numer": "44062545797716080447", + "denom": "1250000000000000000" + } + } + } + ], + "base": "DGB", + "bids": [ + { + "coin": "DASH", + "address": { + "address_type": "Transparent", + "address_data": "XcYdfQgeuM5f5V2LNo9g8o8p3rPPbKwwCg" + }, + "price": { + "decimal": "0.0002544075418788651605521516540338523799763700988224165198319218986992534200426899830070024093907274001", + "rational": [ + [1, [1410065408, 2]], + [1, [3765089107, 9151]] + ], + "fraction": { + "numer": "10000000000", + "denom": "39307010814803" + } + }, + "pubkey": "0315d9c51c657ab1be4ae9d3ab6e76a619d3bccfe830d5363fa168424c0d044732", + "uuid": "e9e4feb2-60b4-4184-8294-591687171e6b", + "is_mine": false, + "base_max_volume": { + "decimal": "15449.5309493280527473176", + "rational": [ + [1, [161102659, 3869502237, 1046]], + [1, [2436694016, 291038304]] + ], + "fraction": { + "numer": "19311913686660065934147", + "denom": "1250000000000000000" + } + }, + "base_min_volume": { + "decimal": "0.39307010814803", + "rational": [ + [1, [3765089107, 9151]], + [1, [276447232, 23283]] + ], + "fraction": { + "numer": "39307010814803", + "denom": "100000000000000" + } + }, + "rel_max_volume": { + "decimal": "3.930477192", + "rational": [ + [1, [491309649]], + [1, [125000000]] + ], + "fraction": { + "numer": "491309649", + "denom": "125000000" + } + }, + "rel_min_volume": { + "decimal": "0.0001", + "rational": [ + [1, [1]], + [1, [10000]] + ], + "fraction": { + "numer": "1", + "denom": "10000" + } + }, + "conf_settings": { + "base_confs": 7, + "base_nota": false, + "rel_confs": 2, + "rel_nota": false + }, + "base_max_volume_aggr": { + "decimal": "15449.5309493280527473176", + "rational": [ + [1, [161102659, 3869502237, 1046]], + [1, [2436694016, 291038304]] + ], + "fraction": { + "numer": "19311913686660065934147", + "denom": "1250000000000000000" + } + }, + "rel_max_volume_aggr": { + "decimal": "3.930477192", + "rational": [ + [1, [491309649]], + [1, [125000000]] + ], + "fraction": { + "numer": "491309649", + "denom": "125000000" + } + } + } + ], + "net_id": 8762, + "num_asks": 3, + "num_bids": 3, + "rel": "DASH", + "timestamp": 1694183345, + "total_asks_base_vol": { + "decimal": "133319.023345413", + "rational": [ + [1, [3238477573, 31040]], + [1, [1000000000]] + ], + "fraction": { + "numer": "133319023345413", + "denom": "1000000000" + } + }, + "total_asks_rel_vol": { + "decimal": "35.2500366381728643576", + "rational": [ + [1, [473921343, 1669176307, 2]], + [1, [2436694016, 291038304]] + ], + "fraction": { + "numer": "44062545797716080447", + "denom": "1250000000000000000" + } + }, + "total_bids_base_vol": { + "decimal": "59100.6554157135128550633", + "rational": [ + [1, [1422777577, 2274178813, 32038]], + [1, [2313682944, 2328306436]] + ], + "fraction": { + "numer": "591006554157135128550633", + "denom": "10000000000000000000" + } + }, + "total_bids_rel_vol": { + "decimal": "14.814675225", + "rational": [ + [1, [592587009]], + [1, [40000000]] + ], + "fraction": { + "numer": "592587009", + "denom": "40000000" + } + } + }, + "id": 42 +} diff --git a/packages/komodo_defi_rpc_methods/test/src/common_structures/orderbook/order_info_test.dart b/packages/komodo_defi_rpc_methods/test/src/common_structures/orderbook/order_info_test.dart new file mode 100644 index 00000000..2c938fca --- /dev/null +++ b/packages/komodo_defi_rpc_methods/test/src/common_structures/orderbook/order_info_test.dart @@ -0,0 +1,90 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:komodo_defi_rpc_methods/src/common_structures/orderbook/order_address.dart'; +import 'package:komodo_defi_rpc_methods/src/common_structures/orderbook/order_info.dart'; +import 'package:komodo_defi_rpc_methods/src/common_structures/primitive/fraction.dart'; +import 'package:rational/rational.dart'; +import 'package:test/test.dart'; + +Map loadFixture(String relativePath) { + final contents = File('test/fixtures/$relativePath').readAsStringSync(); + return jsonDecode(contents) as Map; +} + +void main() { + late Map askJson; + + setUpAll(() { + final fixture = loadFixture('orderbook/orderbook_response.json'); + final result = fixture['result'] as Map; + askJson = Map.from( + (result['asks'] as List).first as Map, + ); + }); + + group('OrderInfo.fromJson', () { + test('parses ask payload from fixture verbatim', () { + final info = OrderInfo.fromJson(askJson); + + expect(info.uuid, '1115d7f2-a7b9-4ab1-913f-497db2549a2b'); + expect(info.coin, 'DGB'); + expect( + info.pubkey, + '03de96cb66dcfaceaa8b3d4993ce8914cd5fe84e3fd53cefdae45add8032792a12', + ); + expect(info.isMine, isFalse); + + expect(info.price!.decimal, '0.0002658065'); + expect(info.price!.fraction, isA()); + expect(info.price!.fraction?.numer, '531613'); + expect(info.price!.fraction?.denom, '2000000000'); + expect( + info.price!.rational, + Rational(BigInt.from(531613), BigInt.from(2000000000)), + ); + + expect(info.baseMaxVolume!.decimal, '90524.256020352'); + expect(info.baseMaxVolume!.fraction?.numer, '707220750159'); + expect(info.baseMaxVolume!.fraction?.denom, '7812500'); + expect(info.baseMaxVolumeAggregated!.decimal, '133319.023345413'); + + expect( + info.baseMinVolume!.decimal, + '0.3762135237475381527539770472129161626973004798603495399849138376977237200745655204067620618758382508', + ); + + expect(info.relMaxVolume!.decimal, '24.061935657873693888'); + expect(info.relMaxVolumeAggregated!.decimal, '35.2500366381728643576'); + expect(info.relMinVolume!.decimal, '0.0001'); + + expect(info.address!.addressType, OrderAddressType.transparent); + expect(info.address!.addressData, 'DEsCggcN3WNmaTkF2WpqoMQqx4JGQrLbPS'); + + expect(info.confSettings!.baseConfs, 7); + expect(info.confSettings!.baseNota, isFalse); + expect(info.confSettings!.relConfs, 2); + expect(info.confSettings!.relNota, isFalse); + }); + }); + + group('OrderInfo serialization', () { + test('toJson emits fixture-compliant structure', () { + final info = OrderInfo.fromJson(askJson); + final json = info.toJson(); + + expect(json, equals(askJson)); + }); + + test('supports round-trip serialization', () { + final info = OrderInfo.fromJson(askJson); + final serialized = info.toJson(); + final reparsed = OrderInfo.fromJson( + Map.from(serialized), + ); + + expect(reparsed.toJson(), equals(serialized)); + expect(reparsed.toJson(), equals(askJson)); + }); + }); +} diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart index cf407af9..7ac61005 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart @@ -110,6 +110,11 @@ class EtherscanTransactionStrategy extends TransactionHistoryStrategy { total: allTransactions.length, totalPages: (allTransactions.length / paginatedResults.pageSize).ceil(), pageNumber: pagination is PagePagination ? pagination.pageNumber : null, + pagingOptions: switch (pagination) { + final PagePagination p => Pagination(pageNumber: p.pageNumber), + final TransactionBasedPagination t => Pagination(fromId: t.fromId), + _ => null, + }, transactions: paginatedResults.transactions, ); } catch (e) { diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/zhtlc_transaction_strategy.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/zhtlc_transaction_strategy.dart index 1347210d..6e3490ef 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/zhtlc_transaction_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/strategies/zhtlc_transaction_strategy.dart @@ -6,8 +6,9 @@ class ZhtlcTransactionStrategy extends TransactionHistoryStrategy { @override Set get supportedPaginationModes => { - PagePagination, - }; + PagePagination, + TransactionBasedPagination, + }; @override Future fetchTransactionHistory( @@ -17,18 +18,25 @@ class ZhtlcTransactionStrategy extends TransactionHistoryStrategy { ) async { validatePagination(pagination); - if (pagination is! PagePagination) { - throw UnsupportedError( - 'ZHTLC only supports page-based pagination', - ); - } + final ({int limit, Pagination pagingOptions}) requestParams = + switch (pagination) { + final PagePagination p => ( + limit: p.itemsPerPage, + pagingOptions: Pagination(pageNumber: p.pageNumber), + ), + final TransactionBasedPagination t => ( + limit: t.itemCount, + pagingOptions: Pagination(fromId: t.fromId), + ), + _ => throw UnsupportedError( + 'Pagination mode ${pagination.runtimeType} not supported', + ), + }; return client.rpc.transactionHistory.zCoinTxHistory( coin: asset.id.id, - limit: pagination.itemsPerPage, - pagingOptions: Pagination( - pageNumber: pagination.pageNumber, - ), + limit: requestParams.limit, + pagingOptions: requestParams.pagingOptions, ); } diff --git a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart index c29f6e91..aefe6318 100644 --- a/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart +++ b/packages/komodo_defi_sdk/lib/src/transaction_history/transaction_history_strategies.dart @@ -8,22 +8,24 @@ import 'package:komodo_defi_types/komodo_defi_types.dart'; class TransactionHistoryStrategyFactory { TransactionHistoryStrategyFactory( PubkeyManager pubkeyManager, - KomodoDefiLocalAuth auth, - ) : _strategies = [ - EtherscanTransactionStrategy(pubkeyManager: pubkeyManager), - V2TransactionStrategy(auth), - const LegacyTransactionStrategy(), - const ZhtlcTransactionStrategy(), - ]; + KomodoDefiLocalAuth auth, { + List? strategies, + }) : _strategies = + strategies ?? + [ + EtherscanTransactionStrategy(pubkeyManager: pubkeyManager), + V2TransactionStrategy(auth), + const LegacyTransactionStrategy(), + const ZhtlcTransactionStrategy(), + ]; final List _strategies; TransactionHistoryStrategy forAsset(Asset asset) { final strategy = _strategies.firstWhere( (strategy) => strategy.supportsAsset(asset), - orElse: () => throw UnsupportedError( - 'No strategy found for asset ${asset.id.id}', - ), + orElse: () => + throw UnsupportedError('No strategy found for asset ${asset.id.id}'), ); return strategy; @@ -38,9 +40,9 @@ class V2TransactionStrategy extends TransactionHistoryStrategy { @override Set get supportedPaginationModes => { - PagePagination, - TransactionBasedPagination, - }; + PagePagination, + TransactionBasedPagination, + }; // TODO: Consider for the future how multi-account support will be handled. // The HistoryTarget could be added to the abstract strategy, but only if @@ -58,13 +60,13 @@ class V2TransactionStrategy extends TransactionHistoryStrategy { return switch (pagination) { final PagePagination p => client.rpc.transactionHistory.myTxHistory( - coin: asset.id.id, - limit: p.itemsPerPage, - pagingOptions: Pagination(pageNumber: p.pageNumber), - target: isHdWallet - ? const HdHistoryTarget.accountId(0) - : IguanaHistoryTarget(), - ), + coin: asset.id.id, + limit: p.itemsPerPage, + pagingOptions: Pagination(pageNumber: p.pageNumber), + target: isHdWallet + ? const HdHistoryTarget.accountId(0) + : IguanaHistoryTarget(), + ), final TransactionBasedPagination t => client.rpc.transactionHistory.myTxHistory( coin: asset.id.id, @@ -75,8 +77,8 @@ class V2TransactionStrategy extends TransactionHistoryStrategy { : IguanaHistoryTarget(), ), _ => throw UnsupportedError( - 'Pagination mode ${pagination.runtimeType} not supported', - ), + 'Pagination mode ${pagination.runtimeType} not supported', + ), }; } @@ -97,9 +99,9 @@ class LegacyTransactionStrategy extends TransactionHistoryStrategy { @override Set get supportedPaginationModes => { - PagePagination, - TransactionBasedPagination, - }; + PagePagination, + TransactionBasedPagination, + }; @override Future fetchTransactionHistory( @@ -111,10 +113,10 @@ class LegacyTransactionStrategy extends TransactionHistoryStrategy { return switch (pagination) { final PagePagination p => client.rpc.transactionHistory.myTxHistoryLegacy( - coin: asset.id.id, - limit: p.itemsPerPage, - pageNumber: p.pageNumber, - ), + coin: asset.id.id, + limit: p.itemsPerPage, + pageNumber: p.pageNumber, + ), final TransactionBasedPagination t => client.rpc.transactionHistory.myTxHistoryLegacy( coin: asset.id.id, @@ -122,8 +124,8 @@ class LegacyTransactionStrategy extends TransactionHistoryStrategy { fromId: t.fromId, ), _ => throw UnsupportedError( - 'Pagination mode ${pagination.runtimeType} not supported', - ), + 'Pagination mode ${pagination.runtimeType} not supported', + ), }; } diff --git a/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart new file mode 100644 index 00000000..fc6d3358 --- /dev/null +++ b/packages/komodo_defi_sdk/test/transaction_history/transaction_history_strategies_test.dart @@ -0,0 +1,74 @@ +import 'package:komodo_defi_local_auth/komodo_defi_local_auth.dart'; +import 'package:komodo_defi_sdk/src/pubkeys/pubkey_manager.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/etherscan_transaction_history_strategy.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/strategies/zhtlc_transaction_strategy.dart'; +import 'package:komodo_defi_sdk/src/transaction_history/transaction_history_strategies.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:test/test.dart'; + +class _MockPubkeyManager extends Mock implements PubkeyManager {} + +class _MockLocalAuth extends Mock implements KomodoDefiLocalAuth {} + +Asset _createZhtlcAsset() { + final protocol = ZhtlcProtocol.fromJson({ + 'type': 'ZHTLC', + 'electrum_servers': [ + {'url': 'lightwalletd.pirate.black', 'port': 9067, 'protocol': 'SSL'}, + ], + }); + + return Asset( + id: AssetId( + id: 'ARRR', + name: 'Pirate Chain', + symbol: AssetSymbol(assetConfigId: 'ARRR'), + chainId: AssetChainId(chainId: 1), + derivationPath: null, + subClass: CoinSubClass.zhtlc, + ), + protocol: protocol, + isWalletOnly: false, + signMessagePrefix: null, + ); +} + +void main() { + late PubkeyManager pubkeyManager; + late KomodoDefiLocalAuth auth; + + setUp(() { + pubkeyManager = _MockPubkeyManager(); + auth = _MockLocalAuth(); + }); + + group('TransactionHistoryStrategyFactory', () { + test('selects ZHTLC strategy for ZHTLC asset', () { + final factory = TransactionHistoryStrategyFactory(pubkeyManager, auth); + final asset = _createZhtlcAsset(); + + final strategy = factory.forAsset(asset); + + expect(strategy, isA()); + }); + + test('ZHTLC strategy wins regardless of registration order', () { + final asset = _createZhtlcAsset(); + final factory = TransactionHistoryStrategyFactory( + pubkeyManager, + auth, + strategies: [ + const LegacyTransactionStrategy(), + V2TransactionStrategy(auth), + EtherscanTransactionStrategy(pubkeyManager: pubkeyManager), + const ZhtlcTransactionStrategy(), + ], + ); + + final strategy = factory.forAsset(asset); + + expect(strategy, isA()); + }); + }); +} diff --git a/packages/komodo_defi_sdk/test/zcash_params/test_helpers/mock_classes.dart b/packages/komodo_defi_sdk/test/zcash_params/test_helpers/mock_classes.dart index b59b0d97..5ea2377a 100644 --- a/packages/komodo_defi_sdk/test/zcash_params/test_helpers/mock_classes.dart +++ b/packages/komodo_defi_sdk/test/zcash_params/test_helpers/mock_classes.dart @@ -4,8 +4,6 @@ import 'dart:typed_data'; import 'package:http/http.dart' as http; import 'package:http/src/byte_stream.dart'; -import 'package:komodo_defi_sdk/src/zcash_params/models/download_progress.dart'; -import 'package:komodo_defi_sdk/src/zcash_params/models/zcash_params_config.dart'; import 'package:komodo_defi_sdk/src/zcash_params/services/zcash_params_download_service.dart'; import 'package:mocktail/mocktail.dart'; @@ -310,7 +308,7 @@ class TestUtils { completer.complete(events); } }, - onError: (error) { + onError: (Object error) { subscription.cancel(); completer.completeError(error); }, diff --git a/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart b/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart index 9bdd8d08..86744414 100644 --- a/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart +++ b/packages/komodo_defi_types/lib/src/transactions/transaction_history_strategy.dart @@ -34,22 +34,4 @@ abstract class TransactionHistoryStrategy { ); } } - - /// Helper method to convert legacy pagination parameters to TransactionPagination - TransactionPagination _getLegacyPagination({ - String? fromId, - int? pageNumber, - int limit = 10, - }) { - if (fromId != null) { - return TransactionBasedPagination( - fromId: fromId, - itemCount: limit, - ); - } - return PagePagination( - pageNumber: pageNumber ?? 1, - itemsPerPage: limit, - ); - } } diff --git a/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart b/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart index ee957e74..a19875c1 100644 --- a/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart +++ b/packages/komodo_defi_types/lib/src/utils/json_type_utils.dart @@ -126,6 +126,13 @@ T? _traverseJson( if (parsed != null) return parsed as T; } + // Rounding precision loss is not a concern when converting int to String + // This is safe because int to String conversion is always exact. + // “For any int i, it is guaranteed that i == int.parse(i.toString()).” + if (T == String && value is int) { + return value.toString() as T; + } + // Handle lossy casts if allowed if (lossyCast && T == String && value is num) { return value.toString() as T; diff --git a/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart b/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart index bf25432a..50d405eb 100644 --- a/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart +++ b/packages/komodo_ui/lib/src/defi/withdraw/source_address_field.dart @@ -167,7 +167,7 @@ class _LoadingState extends StatelessWidget { Text( 'Fetching your ${asset.id.name} addresses', style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + color: theme.textTheme.bodyMedium?.color?.withValues(alpha: 0.7), ), ), ], diff --git a/playground/lib/kdf_operations/kdf_operations_server_native.dart b/playground/lib/kdf_operations/kdf_operations_server_native.dart index e8ba0bc4..295db53b 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_native.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_native.dart @@ -48,4 +48,7 @@ class KdfHttpServerOperations implements IKdfOperations { Future isAvailable(IKdfHostConfig hostConfig) async { throw UnsupportedError('Unknown platforms are not supported'); } + + @override + void dispose() { } } diff --git a/playground/lib/kdf_operations/kdf_operations_server_stub.dart b/playground/lib/kdf_operations/kdf_operations_server_stub.dart index e8ba0bc4..295db53b 100644 --- a/playground/lib/kdf_operations/kdf_operations_server_stub.dart +++ b/playground/lib/kdf_operations/kdf_operations_server_stub.dart @@ -48,4 +48,7 @@ class KdfHttpServerOperations implements IKdfOperations { Future isAvailable(IKdfHostConfig hostConfig) async { throw UnsupportedError('Unknown platforms are not supported'); } + + @override + void dispose() { } }