diff --git a/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json index 71edca42a1..41c9ded541 100644 --- a/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json +++ b/core/datasource/schemas/com.tangem.datasource.local.txhistory.db.TxHistoryDatabase/1.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "36ff7cc1634c100cadc4b04fc2eba1c9", + "identityHash": "442ac578743a8b624777711cf49c77e2", "entities": [ { "tableName": "express_provider", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `icon_url` TEXT NOT NULL, `provider_url` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `image_large` TEXT NOT NULL, `image_small` TEXT NOT NULL, `terms_of_use` TEXT, `privacy_policy` TEXT, `is_recommended` INTEGER NOT NULL, `slippage` TEXT, `is_exchange_only_within_single_address` INTEGER NOT NULL, `is_extra_id_supported` INTEGER NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -21,16 +21,55 @@ "notNull": true }, { - "fieldPath": "iconUrl", - "columnName": "icon_url", + "fieldPath": "type", + "columnName": "type", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "providerUrl", - "columnName": "provider_url", + "fieldPath": "imageLarge", + "columnName": "image_large", "affinity": "TEXT", "notNull": true + }, + { + "fieldPath": "imageSmall", + "columnName": "image_small", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "termsOfUse", + "columnName": "terms_of_use", + "affinity": "TEXT" + }, + { + "fieldPath": "privacyPolicy", + "columnName": "privacy_policy", + "affinity": "TEXT" + }, + { + "fieldPath": "isRecommended", + "columnName": "is_recommended", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "slippage", + "columnName": "slippage", + "affinity": "TEXT" + }, + { + "fieldPath": "isExchangeOnlyWithinSingleAddress", + "columnName": "is_exchange_only_within_single_address", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isExtraIdSupported", + "columnName": "is_extra_id_supported", + "affinity": "INTEGER", + "notNull": true } ], "primaryKey": { @@ -38,13 +77,11 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "express_exchange", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `from_address` TEXT NOT NULL, `payin_address` TEXT NOT NULL, `payin_extra_id` TEXT, `payout_address` TEXT NOT NULL, `refund_address` TEXT, `refund_extra_id` TEXT, `rate_type` TEXT NOT NULL, `status` TEXT NOT NULL, `external_tx_id` TEXT, `external_tx_status` TEXT, `external_tx_url` TEXT, `payin_hash` TEXT, `payout_hash` TEXT, `refund_network` TEXT, `refund_contract_address` TEXT, `created_at` TEXT NOT NULL, `pay_till` TEXT, `average_duration` INTEGER, `from_contract_address` TEXT NOT NULL, `from_network` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `from_amount` TEXT NOT NULL, `from_actual_amount` TEXT, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT NOT NULL, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `from_address` TEXT, `payin_address` TEXT NOT NULL, `payin_extra_id` TEXT, `payout_address` TEXT NOT NULL, `refund_address` TEXT, `refund_extra_id` TEXT, `rate_type` TEXT NOT NULL, `status` TEXT NOT NULL, `external_tx_id` TEXT, `external_tx_url` TEXT, `payin_hash` TEXT, `payout_hash` TEXT, `refund_network` TEXT, `refund_contract_address` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `pay_till` TEXT, `average_duration` INTEGER, `from_contract_address` TEXT NOT NULL, `from_network` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `from_amount` TEXT NOT NULL, `from_actual_amount` TEXT, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT NOT NULL, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))", "fields": [ { "fieldPath": "txId", @@ -67,8 +104,7 @@ { "fieldPath": "fromAddress", "columnName": "from_address", - "affinity": "TEXT", - "notNull": true + "affinity": "TEXT" }, { "fieldPath": "payinAddress", @@ -79,8 +115,7 @@ { "fieldPath": "payinExtraId", "columnName": "payin_extra_id", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "payoutAddress", @@ -91,14 +126,12 @@ { "fieldPath": "refundAddress", "columnName": "refund_address", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "refundExtraId", "columnName": "refund_extra_id", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "rateType", @@ -115,44 +148,32 @@ { "fieldPath": "externalTxId", "columnName": "external_tx_id", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "externalTxStatus", - "columnName": "external_tx_status", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "externalTxUrl", "columnName": "external_tx_url", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "payinHash", "columnName": "payin_hash", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "payoutHash", "columnName": "payout_hash", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "refundNetwork", "columnName": "refund_network", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "refundContractAddress", "columnName": "refund_contract_address", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "createdAt", @@ -160,17 +181,21 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, { "fieldPath": "payTill", "columnName": "pay_till", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "averageDuration", "columnName": "average_duration", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "from.contractAddress", @@ -199,8 +224,7 @@ { "fieldPath": "from.actualAmount", "columnName": "from_actual_amount", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "to.contractAddress", @@ -229,8 +253,7 @@ { "fieldPath": "to.actualAmount", "columnName": "to_actual_amount", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -241,51 +264,33 @@ }, "indices": [ { - "name": "index_express_exchange_owner_address_from_network_created_at", + "name": "index_express_exchange_owner_address_from_network_from_contract_address_created_at", "unique": false, "columnNames": [ "owner_address", "from_network", + "from_contract_address", "created_at" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_from_network_created_at` ON `${TABLE_NAME}` (`owner_address`, `from_network`, `created_at`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_from_network_from_contract_address_created_at` ON `${TABLE_NAME}` (`owner_address`, `from_network`, `from_contract_address`, `created_at`)" }, { - "name": "index_express_exchange_owner_address_payin_hash", + "name": "index_express_exchange_to_network_to_contract_address_created_at", "unique": false, "columnNames": [ - "owner_address", - "payin_hash" + "to_network", + "to_contract_address", + "created_at" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_payin_hash` ON `${TABLE_NAME}` (`owner_address`, `payin_hash`)" - }, - { - "name": "index_express_exchange_owner_address_payout_hash", - "unique": false, - "columnNames": [ - "owner_address", - "payout_hash" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_payout_hash` ON `${TABLE_NAME}` (`owner_address`, `payout_hash`)" - }, - { - "name": "index_express_exchange_provider_id", - "unique": false, - "columnNames": [ - "provider_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_provider_id` ON `${TABLE_NAME}` (`provider_id`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`to_network`, `to_contract_address`, `created_at`)" } - ], - "foreignKeys": [] + ] }, { "tableName": "express_onramp", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `from_address` TEXT NOT NULL, `payin_address` TEXT NOT NULL, `payin_extra_id` TEXT, `payout_address` TEXT NOT NULL, `refund_address` TEXT, `refund_extra_id` TEXT, `rate_type` TEXT NOT NULL, `status` TEXT NOT NULL, `external_tx_id` TEXT, `external_tx_status` TEXT, `external_tx_url` TEXT, `payin_hash` TEXT, `payout_hash` TEXT, `refund_network` TEXT, `refund_contract_address` TEXT, `created_at` TEXT NOT NULL, `pay_till` TEXT, `average_duration` INTEGER, `from_contract_address` TEXT NOT NULL, `from_network` TEXT NOT NULL, `from_decimals` INTEGER NOT NULL, `from_amount` TEXT NOT NULL, `from_actual_amount` TEXT, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT NOT NULL, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` TEXT NOT NULL, `owner_address` TEXT NOT NULL, `provider_id` TEXT NOT NULL, `payout_address` TEXT NOT NULL, `status` TEXT NOT NULL, `fail_reason` TEXT, `external_tx_id` TEXT, `external_tx_url` TEXT, `payout_hash` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `from_currency_code` TEXT NOT NULL, `from_amount` TEXT NOT NULL, `from_precision` INTEGER NOT NULL, `payment_method` TEXT NOT NULL, `country_code` TEXT NOT NULL, `to_contract_address` TEXT NOT NULL, `to_network` TEXT NOT NULL, `to_decimals` INTEGER NOT NULL, `to_amount` TEXT, `to_actual_amount` TEXT, PRIMARY KEY(`tx_id`))", "fields": [ { "fieldPath": "txId", @@ -305,48 +310,12 @@ "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "fromAddress", - "columnName": "from_address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "payinAddress", - "columnName": "payin_address", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "payinExtraId", - "columnName": "payin_extra_id", - "affinity": "TEXT", - "notNull": false - }, { "fieldPath": "payoutAddress", "columnName": "payout_address", "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "refundAddress", - "columnName": "refund_address", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "refundExtraId", - "columnName": "refund_extra_id", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "rateType", - "columnName": "rate_type", - "affinity": "TEXT", - "notNull": true - }, { "fieldPath": "status", "columnName": "status", @@ -354,46 +323,24 @@ "notNull": true }, { - "fieldPath": "externalTxId", - "columnName": "external_tx_id", - "affinity": "TEXT", - "notNull": false + "fieldPath": "failReason", + "columnName": "fail_reason", + "affinity": "TEXT" }, { - "fieldPath": "externalTxStatus", - "columnName": "external_tx_status", - "affinity": "TEXT", - "notNull": false + "fieldPath": "externalTxId", + "columnName": "external_tx_id", + "affinity": "TEXT" }, { "fieldPath": "externalTxUrl", "columnName": "external_tx_url", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "payinHash", - "columnName": "payin_hash", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "payoutHash", "columnName": "payout_hash", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "refundNetwork", - "columnName": "refund_network", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "refundContractAddress", - "columnName": "refund_contract_address", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "createdAt", @@ -402,46 +349,40 @@ "notNull": true }, { - "fieldPath": "payTill", - "columnName": "pay_till", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "averageDuration", - "columnName": "average_duration", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "from.contractAddress", - "columnName": "from_contract_address", + "fieldPath": "updatedAt", + "columnName": "updated_at", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "from.network", - "columnName": "from_network", + "fieldPath": "fromCurrencyCode", + "columnName": "from_currency_code", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "from.decimals", - "columnName": "from_decimals", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "from.amount", + "fieldPath": "fromAmount", "columnName": "from_amount", "affinity": "TEXT", "notNull": true }, { - "fieldPath": "from.actualAmount", - "columnName": "from_actual_amount", + "fieldPath": "fromPrecision", + "columnName": "from_precision", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "paymentMethod", + "columnName": "payment_method", "affinity": "TEXT", - "notNull": false + "notNull": true + }, + { + "fieldPath": "countryCode", + "columnName": "country_code", + "affinity": "TEXT", + "notNull": true }, { "fieldPath": "to.contractAddress", @@ -464,14 +405,12 @@ { "fieldPath": "to.amount", "columnName": "to_amount", - "affinity": "TEXT", - "notNull": true + "affinity": "TEXT" }, { "fieldPath": "to.actualAmount", "columnName": "to_actual_amount", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -482,37 +421,18 @@ }, "indices": [ { - "name": "index_express_onramp_owner_address_to_network_created_at", + "name": "index_express_onramp_owner_address_to_network_to_contract_address_created_at", "unique": false, "columnNames": [ "owner_address", "to_network", + "to_contract_address", "created_at" ], "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_to_network_created_at` ON `${TABLE_NAME}` (`owner_address`, `to_network`, `created_at`)" - }, - { - "name": "index_express_onramp_owner_address_payout_hash", - "unique": false, - "columnNames": [ - "owner_address", - "payout_hash" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_payout_hash` ON `${TABLE_NAME}` (`owner_address`, `payout_hash`)" - }, - { - "name": "index_express_onramp_provider_id", - "unique": false, - "columnNames": [ - "provider_id" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_provider_id` ON `${TABLE_NAME}` (`provider_id`)" + "createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_owner_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`owner_address`, `to_network`, `to_contract_address`, `created_at`)" } - ], - "foreignKeys": [] + ] }, { "tableName": "express_sync_state", @@ -539,14 +459,12 @@ { "fieldPath": "afterCursor", "columnName": "after_cursor", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "deltaCursor", "columnName": "delta_cursor", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -555,15 +473,12 @@ "type", "address" ] - }, - "indices": [], - "foreignKeys": [] + } } ], - "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '36ff7cc1634c100cadc4b04fc2eba1c9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '442ac578743a8b624777711cf49c77e2')" ] } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 06b3946719..a0ae736f10 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -78,7 +78,7 @@ interface TangemExpressApi { @Header("user-id") userWalletId: String, @Header("refcode") refCode: String?, @Query("txId") txId: String, - ): ApiResponse + ): ApiResponse @POST("exchange-sent") suspend fun exchangeSent( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeItemResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeItemResponse.kt index 80d86db370..7c5d86f608 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeItemResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeItemResponse.kt @@ -12,9 +12,12 @@ data class ExchangeItemResponse( @Json(name = "providerId") val providerId: String, - /** Address from which the source assets were sent */ + /** + * Address from which the `from` assets were taken for the exchange. Optional because the very first + * app versions did not send it; for newer versions it can be considered effectively mandatory. + */ @Json(name = "fromAddress") - val fromAddress: String, + val fromAddress: String?, /** Address to which the source assets were transferred for the exchange */ @Json(name = "payinAddress") @@ -40,17 +43,17 @@ data class ExchangeItemResponse( @Json(name = "rateType") val rateType: String, + /** + * Raw backend status string, kept unparsed so a new value never breaks deserialization. + * Typed view: [com.tangem.domain.express.models.ExpressExchangeStatus]. + */ @Json(name = "status") - val status: Status, + val status: String, /** External transaction ID (CEX only) */ @Json(name = "externalTxId") val externalTxId: String?, - /** Transaction status reported by the provider */ - @Json(name = "externalTxStatus") - val externalTxStatus: String?, - /** URL to view the transaction details (CEX only) */ @Json(name = "externalTxUrl") val externalTxUrl: String?, @@ -75,6 +78,10 @@ data class ExchangeItemResponse( @Json(name = "createdAt") val createdAt: String, + /** Transaction last-update timestamp in ISO-8601 format */ + @Json(name = "updatedAt") + val updatedAt: String, + /** Pay-in expiration timestamp in ISO-8601 format */ @Json(name = "payTill") val payTill: String?, @@ -107,50 +114,4 @@ data class ExchangeItemResponse( @Json(name = "toActualAmount") val toActualAmount: String?, // endregion -) { - - enum class Status { - - @Json(name = "unknown") - UNKNOWN, - - @Json(name = "exchange-tx-sent") - EXCHANGE_TX_SENT, - - @Json(name = "waiting") - WAITING, - - @Json(name = "waiting-tx-hash") - WAITING_TX_HASH, - - @Json(name = "expired") - EXPIRED, - - @Json(name = "confirming") - CONFIRMING, - - @Json(name = "exchanging") - EXCHANGING, - - @Json(name = "sending") - SENDING, - - @Json(name = "finished") - FINISHED, - - @Json(name = "failed") - FAILED, - - @Json(name = "tx-failed") - TX_FAILED, - - @Json(name = "refunded") - REFUNDED, - - @Json(name = "verifying") - VERIFYING, - - @Json(name = "paused") - PAUSED, - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressPagination.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressPagination.kt index 9909a9988d..fa57e12ba6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressPagination.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExpressPagination.kt @@ -9,7 +9,7 @@ data class ExpressPagination( val endCursor: String?, @Json(name = "startDeltaCursor") val startDeltaCursor: String?, - @Json(name = "hasNextPage") + @Json(name = "hasMore") val hasMore: Boolean, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt index f33a5a8348..ab86063026 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt @@ -6,7 +6,7 @@ import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse -import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse +import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO @@ -87,7 +87,7 @@ interface OnrampApi { @Header("user-id") userWalletId: String, @Header("refcode") refCode: String?, @Query("txId") txId: String, - ): ApiResponse + ): ApiResponse @GET("history/onramp") suspend fun getHistory( diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampItemResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampItemResponse.kt index 142191fd97..fa5767bf7b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampItemResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampItemResponse.kt @@ -12,87 +12,49 @@ data class OnrampItemResponse( @Json(name = "providerId") val providerId: String, - /** Address from which the source assets were taken for the exchange */ - @Json(name = "fromAddress") - val fromAddress: String, - - /** Address to which the assets were transferred for the exchange */ - @Json(name = "payinAddress") - val payinAddress: String, - - /** Extra ID used for the pay-in transaction */ - @Json(name = "payinExtraId") - val payinExtraId: String?, - /** Address that received the target assets */ @Json(name = "payoutAddress") val payoutAddress: String, - /** Refund destination address */ - @Json(name = "refundAddress") - val refundAddress: String?, - - /** Extra ID used for refunds */ - @Json(name = "refundExtraId") - val refundExtraId: String?, - - /** Exchange rate type used in the transaction (float, fixed) */ - @Json(name = "rateType") - val rateType: String, - + /** + * Raw backend status string, kept unparsed so a new value never breaks deserialization. + * Typed view: [com.tangem.domain.express.models.ExpressOnrampStatus]. + */ @Json(name = "status") - val status: Status, + val status: String, - /** External transaction ID (CEX only) */ + /** Failure reason reported by the provider */ + @Json(name = "failReason") + val failReason: String?, + + /** External transaction ID reported by the provider in the webhook */ @Json(name = "externalTxId") val externalTxId: String?, - /** Transaction status reported by the provider */ - @Json(name = "externalTxStatus") - val externalTxStatus: String?, - - /** URL to view the transaction details (CEX only) */ + /** URL to view the transaction details on the provider side (not provided by all providers) */ @Json(name = "externalTxUrl") val externalTxUrl: String?, - /** Blockchain hash of the pay-in transaction */ - @Json(name = "payinHash") - val payinHash: String?, - /** Blockchain hash of the payout transaction */ @Json(name = "payoutHash") val payoutHash: String?, - /** Network used for the refund transaction (when status is refunded) */ - @Json(name = "refundNetwork") - val refundNetwork: String?, - - /** Refunded token contract address */ - @Json(name = "refundContractAddress") - val refundContractAddress: String?, - /** Transaction creation timestamp in ISO-8601 format */ @Json(name = "createdAt") val createdAt: String, - /** Pay-in expiration timestamp in ISO-8601 format */ - @Json(name = "payTill") - val payTill: String?, - - /** Average provider exchange duration in seconds */ - @Json(name = "averageDuration") - val averageDuration: Long?, + /** Transaction last-update timestamp in ISO-8601 format */ + @Json(name = "updatedAt") + val updatedAt: String, // endregion - // region fromAsset info - @Json(name = "fromContractAddress") - val fromContractAddress: String, - @Json(name = "fromNetwork") - val fromNetwork: String, - @Json(name = "fromDecimals") - val fromDecimals: Int, + // region fromAsset (fiat) info + @Json(name = "fromCurrencyCode") + val fromCurrencyCode: String, @Json(name = "fromAmount") val fromAmount: String, + @Json(name = "fromPrecision") + val fromPrecision: Int, // endregion // region toAsset info @@ -102,55 +64,19 @@ data class OnrampItemResponse( val toNetwork: String, @Json(name = "toDecimals") val toDecimals: Int, + + /** Provider-promised amount, received from the provider in the webhook */ @Json(name = "toAmount") - val toAmount: String, + val toAmount: String?, + + /** Actual amount delivered to the user, received from the provider in the webhook */ @Json(name = "toActualAmount") val toActualAmount: String?, // endregion -) { - enum class Status { + @Json(name = "paymentMethod") + val paymentMethod: String, - @Json(name = "unknown") - UNKNOWN, - - @Json(name = "exchange-tx-sent") - EXCHANGE_TX_SENT, - - @Json(name = "waiting") - WAITING, - - @Json(name = "waiting-tx-hash") - WAITING_TX_HASH, - - @Json(name = "expired") - EXPIRED, - - @Json(name = "confirming") - CONFIRMING, - - @Json(name = "exchanging") - EXCHANGING, - - @Json(name = "sending") - SENDING, - - @Json(name = "finished") - FINISHED, - - @Json(name = "failed") - FAILED, - - @Json(name = "tx-failed") - TX_FAILED, - - @Json(name = "refunded") - REFUNDED, - - @Json(name = "verifying") - VERIFYING, - - @Json(name = "paused") - PAUSED, - } -} \ No newline at end of file + @Json(name = "countryCode") + val countryCode: String, +) \ No newline at end of file diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressHistoryConverter.kt similarity index 68% rename from data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressHistoryConverter.kt index de66d7e911..2718464884 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressHistoryConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.data.txhistory.repository.converter +package com.tangem.datasource.local.converter import com.tangem.datasource.api.express.models.response.ExchangeItemResponse import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse @@ -10,7 +10,7 @@ import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEnti * * @param ownerAddress address the history was requested for. Stored as the query key. */ -internal fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity { +fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity { return ExpressExchangeEntity( txId = txId, ownerAddress = ownerAddress, @@ -22,15 +22,15 @@ internal fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchang refundAddress = refundAddress, refundExtraId = refundExtraId, rateType = rateType, - status = status.name, + status = status, externalTxId = externalTxId, - externalTxStatus = externalTxStatus, externalTxUrl = externalTxUrl, payinHash = payinHash, payoutHash = payoutHash, refundNetwork = refundNetwork, refundContractAddress = refundContractAddress, createdAt = createdAt, + updatedAt = updatedAt, payTill = payTill, averageDuration = averageDuration, from = ExpressExchangeEntity.AssetEmbedded( @@ -50,36 +50,22 @@ internal fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchang ) } -internal fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity { +fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity { return ExpressOnrampEntity( txId = txId, ownerAddress = ownerAddress, providerId = providerId, - fromAddress = fromAddress, - payinAddress = payinAddress, - payinExtraId = payinExtraId, payoutAddress = payoutAddress, - refundAddress = refundAddress, - refundExtraId = refundExtraId, - rateType = rateType, - status = status.name, + status = status, + failReason = failReason, externalTxId = externalTxId, - externalTxStatus = externalTxStatus, externalTxUrl = externalTxUrl, - payinHash = payinHash, payoutHash = payoutHash, - refundNetwork = refundNetwork, - refundContractAddress = refundContractAddress, createdAt = createdAt, - payTill = payTill, - averageDuration = averageDuration, - from = ExpressOnrampEntity.AssetEmbedded( - contractAddress = fromContractAddress, - network = fromNetwork, - decimals = fromDecimals, - amount = fromAmount, - actualAmount = null, - ), + updatedAt = updatedAt, + fromCurrencyCode = fromCurrencyCode, + fromAmount = fromAmount, + fromPrecision = fromPrecision, to = ExpressOnrampEntity.AssetEmbedded( contractAddress = toContractAddress, network = toNetwork, @@ -87,5 +73,7 @@ internal fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEnt amount = toAmount, actualAmount = toActualAmount, ), + paymentMethod = paymentMethod, + countryCode = countryCode, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressProviderEntityConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressProviderEntityConverter.kt new file mode 100644 index 0000000000..114a5605d9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/converter/ExpressProviderEntityConverter.kt @@ -0,0 +1,26 @@ +package com.tangem.datasource.local.converter + +import com.tangem.datasource.api.express.models.response.ExchangeProvider +import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity + +/** + * Maps an [ExchangeProvider] API response into its persisted [ExpressProviderEntity] representation. + * + * `type` is stored as the [com.tangem.datasource.api.express.models.response.ExchangeProviderType] name + * (DEX / CEX / DEX_BRIDGE / ONRAMP); `slippage` as a plain decimal string. + */ +fun ExchangeProvider.toEntity(): ExpressProviderEntity { + return ExpressProviderEntity( + id = id, + name = name, + type = type.name, + imageLarge = imageLargeUrl, + imageSmall = imageSmallUrl, + termsOfUse = termsOfUse, + privacyPolicy = privacyPolicy, + isRecommended = isRecommended, + slippage = slippage?.toPlainString(), + isExchangeOnlyWithinSingleAddress = isExchangeOnlyWithinSingleAddress, + isExtraIdSupported = isExtraIdSupported, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt index c5185f4cd6..802598fc3d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/dao/ExpressHistoryDao.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.txhistory.db.dao import androidx.room.Dao import androidx.room.Insert +import androidx.room.MapColumn import androidx.room.OnConflictStrategy import androidx.room.Query import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity @@ -21,56 +22,76 @@ interface ExpressHistoryDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun upsertOnramps(items: List) + /** + * All persisted providers keyed by [ExpressProviderEntity.id] + */ + @Query("SELECT * FROM express_provider") + fun getProvidersById(): Flow> + + /** + * Outgoing swaps: the viewed currency is the swap's `from` side, so the row is stored under this + * address ([ExpressExchangeEntity.ownerAddress] == fromAddress). Join to on-chain by `payin_hash`. + * + + * loading the whole table; [activeStatuses] keeps in-progress deals visible even outside the window. + */ @Query( """ - SELECT * - FROM express_exchange + SELECT * FROM express_exchange WHERE owner_address = :ownerAddress + AND from_network = :network + AND from_contract_address = :contract + AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses)) ORDER BY created_at DESC """, ) - fun observeExchanges(ownerAddress: String): Flow> + fun observeOutgoingSwaps( + ownerAddress: String, + network: String, + contract: String, + fromCreatedAtIso: String, + activeStatuses: List, + ): Flow> + /** + * Incoming swaps: the viewed currency is the swap's `to` side. Such a deal was initiated from a + * different coin, so the row is stored under that coin's `owner_address` — hence this query is + * cross-owner, matched by the `to` asset. Join to on-chain by `payout_hash`. + */ @Query( """ - SELECT * - FROM express_onramp - WHERE owner_address = :ownerAddress + SELECT * FROM express_exchange + WHERE to_network = :network + AND to_contract_address = :contract + AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses)) ORDER BY created_at DESC """, ) - fun observeOnramps(ownerAddress: String): Flow> + fun observeIncomingSwaps( + network: String, + contract: String, + fromCreatedAtIso: String, + activeStatuses: List, + ): Flow> + /** + * Onramp is always incoming: [ExpressOnrampEntity.ownerAddress] == payoutAddress. Join by `payout_hash`. + */ @Query( """ - SELECT * - FROM express_exchange + SELECT * FROM express_onramp WHERE owner_address = :ownerAddress - AND payin_hash = :hash - LIMIT 1 + AND to_network = :network + AND to_contract_address = :contract + AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses)) + ORDER BY created_at DESC """, ) - suspend fun findExchangeByPayinHash(ownerAddress: String, hash: String): ExpressExchangeEntity? - - @Query( - """ - SELECT * - FROM express_exchange - WHERE owner_address = :ownerAddress - AND payout_hash = :hash - LIMIT 1 - """, - ) - suspend fun findExchangeByPayoutHash(ownerAddress: String, hash: String): ExpressExchangeEntity? - - @Query( - """ - SELECT * - FROM express_onramp - WHERE owner_address = :ownerAddress - AND payout_hash = :hash - LIMIT 1 - """, - ) - suspend fun findOnrampByPayoutHash(ownerAddress: String, hash: String): ExpressOnrampEntity? + fun observeIncomingOnramps( + ownerAddress: String, + network: String, + contract: String, + fromCreatedAtIso: String, + activeStatuses: List, + ): Flow> } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt index 2b359b07df..9a011acf5b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressExchangeEntity.kt @@ -10,10 +10,11 @@ import androidx.room.* @Entity( tableName = "express_exchange", indices = [ - Index(value = ["owner_address", "from_network", "created_at"]), - Index(value = ["owner_address", "payin_hash"]), - Index(value = ["owner_address", "payout_hash"]), - Index(value = ["provider_id"]), + // Outgoing swaps lookup (observeOutgoingSwaps): owner + from-asset equality, created_at range/sort. + Index(value = ["owner_address", "from_network", "from_contract_address", "created_at"]), + // Incoming (cross-owner) swaps lookup (observeIncomingSwaps): to-asset equality, created_at range/sort. + // No owner filter here, so to_contract_address in the index is what keeps a popular to-network selective. + Index(value = ["to_network", "to_contract_address", "created_at"]), ], ) data class ExpressExchangeEntity( @@ -31,9 +32,12 @@ data class ExpressExchangeEntity( @ColumnInfo(name = "provider_id") val providerId: String, - /** Address from which the source assets were sent */ + /** + * Address from which the `from` assets were taken for the exchange. Optional because the very first + * app versions did not send it; for newer versions it can be considered effectively mandatory. + */ @ColumnInfo(name = "from_address") - val fromAddress: String, + val fromAddress: String?, /** Address to which the source assets were transferred for the exchange */ @ColumnInfo(name = "payin_address") @@ -62,20 +66,8 @@ data class ExpressExchangeEntity( val rateType: String, /** - * unknown - * exchange-tx-sent - * waiting - * waiting-tx-hash - * expired - * confirming - * exchanging - * sending - * finished - * failed - * tx-failed - * refunded - * verifying - * paused + * Raw backend status string, persisted as-is (kept unparsed so a new value never breaks anything). + * Typed view: [com.tangem.domain.express.models.ExpressExchangeStatus]. */ @ColumnInfo(name = "status") val status: String, @@ -84,10 +76,6 @@ data class ExpressExchangeEntity( @ColumnInfo(name = "external_tx_id") val externalTxId: String?, - /** Transaction status reported by the provider */ - @ColumnInfo(name = "external_tx_status") - val externalTxStatus: String?, - /** URL to view the transaction details (CEX only) */ @ColumnInfo(name = "external_tx_url") val externalTxUrl: String?, @@ -112,6 +100,10 @@ data class ExpressExchangeEntity( @ColumnInfo(name = "created_at") val createdAt: String, + /** Transaction last-update timestamp in ISO-8601 format */ + @ColumnInfo(name = "updated_at") + val updatedAt: String, + /** Pay-in expiration timestamp in ISO-8601 format */ @ColumnInfo(name = "pay_till") val payTill: String?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt index f777aa3531..07829651de 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressOnrampEntity.kt @@ -10,9 +10,8 @@ import androidx.room.* @Entity( tableName = "express_onramp", indices = [ - Index(value = ["owner_address", "to_network", "created_at"]), - Index(value = ["owner_address", "payout_hash"]), - Index(value = ["provider_id"]), + // Incoming onramp lookup (observeIncomingOnramps): owner + to-asset equality, created_at range/sort. + Index(value = ["owner_address", "to_network", "to_contract_address", "created_at"]), ], ) data class ExpressOnrampEntity( @@ -30,100 +29,61 @@ data class ExpressOnrampEntity( @ColumnInfo(name = "provider_id") val providerId: String, - /** Address from which the source assets were taken for the exchange */ - @ColumnInfo(name = "from_address") - val fromAddress: String, - - /** Address to which the assets were transferred for the exchange */ - @ColumnInfo(name = "payin_address") - val payinAddress: String, - - /** Extra ID used for the pay-in transaction */ - @ColumnInfo(name = "payin_extra_id") - val payinExtraId: String?, - /** Address that received the target assets */ @ColumnInfo(name = "payout_address") val payoutAddress: String, - /** Refund destination address */ - @ColumnInfo(name = "refund_address") - val refundAddress: String?, - - /** Extra ID used for refunds */ - @ColumnInfo(name = "refund_extra_id") - val refundExtraId: String?, - /** - * fixed / float - */ - @ColumnInfo(name = "rate_type") - val rateType: String, - - /** - * unknown - * exchange-tx-sent - * waiting - * waiting-tx-hash - * expired - * confirming - * exchanging - * sending - * finished - * failed - * tx-failed - * refunded - * verifying - * paused + * Raw backend status string, persisted as-is (kept unparsed so a new value never breaks anything). + * Typed view: [com.tangem.domain.express.models.ExpressOnrampStatus]. */ @ColumnInfo(name = "status") val status: String, - /** External transaction ID (CEX only) */ + /** Failure reason reported by the provider */ + @ColumnInfo(name = "fail_reason") + val failReason: String?, + + /** External transaction ID reported by the provider in the webhook */ @ColumnInfo(name = "external_tx_id") val externalTxId: String?, - /** Transaction status reported by the provider */ - @ColumnInfo(name = "external_tx_status") - val externalTxStatus: String?, - - /** URL to view the transaction details (CEX only) */ + /** URL to view the transaction details on the provider side (not provided by all providers) */ @ColumnInfo(name = "external_tx_url") val externalTxUrl: String?, - /** Blockchain hash of the pay-in transaction */ - @ColumnInfo(name = "payin_hash") - val payinHash: String?, - /** Blockchain hash of the payout transaction */ @ColumnInfo(name = "payout_hash") val payoutHash: String?, - /** Network used for the refund transaction (when status is refunded) */ - @ColumnInfo(name = "refund_network") - val refundNetwork: String?, - - /** Refunded token contract address */ - @ColumnInfo(name = "refund_contract_address") - val refundContractAddress: String?, - /** Transaction creation timestamp in ISO-8601 format */ @ColumnInfo(name = "created_at") val createdAt: String, - /** Pay-in expiration timestamp in ISO-8601 format */ - @ColumnInfo(name = "pay_till") - val payTill: String?, + /** Transaction last-update timestamp in ISO-8601 format */ + @ColumnInfo(name = "updated_at") + val updatedAt: String, - /** Average provider exchange duration in seconds */ - @ColumnInfo(name = "average_duration") - val averageDuration: Long?, + /** Fiat currency code of the source funds */ + @ColumnInfo(name = "from_currency_code") + val fromCurrencyCode: String, - @Embedded(prefix = "from_") - val from: AssetEmbedded, + /** Fiat amount of the source funds */ + @ColumnInfo(name = "from_amount") + val fromAmount: String, + + /** Number of decimal places of the source fiat currency */ + @ColumnInfo(name = "from_precision") + val fromPrecision: Int, @Embedded(prefix = "to_") val to: AssetEmbedded, + + @ColumnInfo(name = "payment_method") + val paymentMethod: String, + + @ColumnInfo(name = "country_code") + val countryCode: String, ) { data class AssetEmbedded( @@ -137,10 +97,11 @@ data class ExpressOnrampEntity( @ColumnInfo(name = "decimals") val decimals: Int, + /** Provider-promised amount. Present only if the provider reported it */ @ColumnInfo(name = "amount") - val amount: String, + val amount: String?, - /** Actual provider-confirmed amount. Present only for the [ExpressOnrampEntity.to] asset */ + /** Actual provider-confirmed amount delivered to the user */ @ColumnInfo(name = "actual_amount") val actualAmount: String?, ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt index 55c8458134..6e96815a68 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/txhistory/db/entity/express/ExpressProviderEntity.kt @@ -4,9 +4,13 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -@Entity( - tableName = "express_provider", -) +/** + * Persisted representation of an express provider. + * + * Mirrors [com.tangem.datasource.api.express.models.response.ExchangeProvider]. Mapped into + * [com.tangem.domain.express.models.ExpressProvider] when read back. + */ +@Entity(tableName = "express_provider") data class ExpressProviderEntity( @PrimaryKey @@ -16,9 +20,34 @@ data class ExpressProviderEntity( @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "icon_url") - val iconUrl: String, + /** Raw provider type (`dex` / `cex` / `dex-bridge` / `onramp`). Typed view: ExpressProviderType. */ + @ColumnInfo(name = "type") + val type: String, - @ColumnInfo(name = "provider_url") - val providerUrl: String, + /** Large logo image URL. */ + @ColumnInfo(name = "image_large") + val imageLarge: String, + + /** Small logo image URL. */ + @ColumnInfo(name = "image_small") + val imageSmall: String, + + @ColumnInfo(name = "terms_of_use") + val termsOfUse: String?, + + @ColumnInfo(name = "privacy_policy") + val privacyPolicy: String?, + + @ColumnInfo(name = "is_recommended") + val isRecommended: Boolean, + + /** Raw decimal string (BigDecimal) or `null`. */ + @ColumnInfo(name = "slippage") + val slippage: String?, + + @ColumnInfo(name = "is_exchange_only_within_single_address") + val isExchangeOnlyWithinSingleAddress: Boolean, + + @ColumnInfo(name = "is_extra_id_supported") + val isExtraIdSupported: Boolean, ) \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/local/converter/ExpressHistoryConverterTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/local/converter/ExpressHistoryConverterTest.kt new file mode 100644 index 0000000000..6fd3ee0094 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/local/converter/ExpressHistoryConverterTest.kt @@ -0,0 +1,272 @@ +package com.tangem.datasource.local.converter + +import com.google.common.truth.Truth +import com.tangem.datasource.api.express.models.response.ExchangeItemResponse +import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class ExpressHistoryConverterTest { + + @Test + fun `GIVEN exchange item WHEN toEntity THEN all transaction fields are mapped`() { + // GIVEN + val item = createExchangeItem() + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.txId).isEqualTo(item.txId) + Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS) + Truth.assertThat(entity.providerId).isEqualTo(item.providerId) + Truth.assertThat(entity.fromAddress).isEqualTo(item.fromAddress) + Truth.assertThat(entity.payinAddress).isEqualTo(item.payinAddress) + Truth.assertThat(entity.payinExtraId).isEqualTo(item.payinExtraId) + Truth.assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress) + Truth.assertThat(entity.refundAddress).isEqualTo(item.refundAddress) + Truth.assertThat(entity.refundExtraId).isEqualTo(item.refundExtraId) + Truth.assertThat(entity.rateType).isEqualTo(item.rateType) + Truth.assertThat(entity.externalTxId).isEqualTo(item.externalTxId) + Truth.assertThat(entity.externalTxUrl).isEqualTo(item.externalTxUrl) + Truth.assertThat(entity.payinHash).isEqualTo(item.payinHash) + Truth.assertThat(entity.payoutHash).isEqualTo(item.payoutHash) + Truth.assertThat(entity.refundNetwork).isEqualTo(item.refundNetwork) + Truth.assertThat(entity.refundContractAddress).isEqualTo(item.refundContractAddress) + Truth.assertThat(entity.createdAt).isEqualTo(item.createdAt) + Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt) + Truth.assertThat(entity.payTill).isEqualTo(item.payTill) + Truth.assertThat(entity.averageDuration).isEqualTo(item.averageDuration) + } + + @Test + fun `GIVEN exchange item WHEN toEntity THEN status is stored as raw string`() { + // GIVEN + val item = createExchangeItem(status = "finished") + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.status).isEqualTo("finished") + } + + @Test + fun `GIVEN exchange item WHEN toEntity THEN from and to assets are mapped`() { + // GIVEN + val item = createExchangeItem() + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress) + Truth.assertThat(entity.from.network).isEqualTo(item.fromNetwork) + Truth.assertThat(entity.from.decimals).isEqualTo(item.fromDecimals) + Truth.assertThat(entity.from.amount).isEqualTo(item.fromAmount) + // `from` asset never carries an actual amount + Truth.assertThat(entity.from.actualAmount).isNull() + + Truth.assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress) + Truth.assertThat(entity.to.network).isEqualTo(item.toNetwork) + Truth.assertThat(entity.to.decimals).isEqualTo(item.toDecimals) + Truth.assertThat(entity.to.amount).isEqualTo(item.toAmount) + Truth.assertThat(entity.to.actualAmount).isEqualTo(item.toActualAmount) + } + + @Test + fun `GIVEN exchange item with null optional fields WHEN toEntity THEN nulls are preserved`() { + // GIVEN + val item = createExchangeItem( + payinExtraId = null, + refundAddress = null, + refundExtraId = null, + externalTxId = null, + externalTxUrl = null, + payinHash = null, + payoutHash = null, + refundNetwork = null, + refundContractAddress = null, + payTill = null, + averageDuration = null, + toActualAmount = null, + ) + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.payinExtraId).isNull() + Truth.assertThat(entity.refundAddress).isNull() + Truth.assertThat(entity.refundExtraId).isNull() + Truth.assertThat(entity.externalTxId).isNull() + Truth.assertThat(entity.externalTxUrl).isNull() + Truth.assertThat(entity.payinHash).isNull() + Truth.assertThat(entity.payoutHash).isNull() + Truth.assertThat(entity.refundNetwork).isNull() + Truth.assertThat(entity.refundContractAddress).isNull() + Truth.assertThat(entity.payTill).isNull() + Truth.assertThat(entity.averageDuration).isNull() + Truth.assertThat(entity.to.actualAmount).isNull() + } + + @Test + fun `GIVEN onramp item WHEN toEntity THEN all transaction fields are mapped`() { + // GIVEN + val item = createOnrampItem() + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.txId).isEqualTo(item.txId) + Truth.assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS) + Truth.assertThat(entity.providerId).isEqualTo(item.providerId) + Truth.assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress) + Truth.assertThat(entity.failReason).isEqualTo(item.failReason) + Truth.assertThat(entity.externalTxId).isEqualTo(item.externalTxId) + Truth.assertThat(entity.externalTxUrl).isEqualTo(item.externalTxUrl) + Truth.assertThat(entity.payoutHash).isEqualTo(item.payoutHash) + Truth.assertThat(entity.createdAt).isEqualTo(item.createdAt) + Truth.assertThat(entity.updatedAt).isEqualTo(item.updatedAt) + Truth.assertThat(entity.fromCurrencyCode).isEqualTo(item.fromCurrencyCode) + Truth.assertThat(entity.fromAmount).isEqualTo(item.fromAmount) + Truth.assertThat(entity.fromPrecision).isEqualTo(item.fromPrecision) + Truth.assertThat(entity.paymentMethod).isEqualTo(item.paymentMethod) + Truth.assertThat(entity.countryCode).isEqualTo(item.countryCode) + } + + @Test + fun `GIVEN onramp item WHEN toEntity THEN status is stored as raw string`() { + // GIVEN + val item = createOnrampItem(status = "waiting-for-payment") + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.status).isEqualTo("waiting-for-payment") + } + + @Test + fun `GIVEN onramp item WHEN toEntity THEN to asset is mapped`() { + // GIVEN + val item = createOnrampItem() + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress) + Truth.assertThat(entity.to.network).isEqualTo(item.toNetwork) + Truth.assertThat(entity.to.decimals).isEqualTo(item.toDecimals) + Truth.assertThat(entity.to.amount).isEqualTo(item.toAmount) + Truth.assertThat(entity.to.actualAmount).isEqualTo(item.toActualAmount) + } + + @Test + fun `GIVEN onramp item with null optional fields WHEN toEntity THEN nulls are preserved`() { + // GIVEN + val item = createOnrampItem( + failReason = null, + externalTxId = null, + externalTxUrl = null, + payoutHash = null, + toAmount = null, + toActualAmount = null, + ) + + // WHEN + val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) + + // THEN + Truth.assertThat(entity.failReason).isNull() + Truth.assertThat(entity.externalTxId).isNull() + Truth.assertThat(entity.externalTxUrl).isNull() + Truth.assertThat(entity.payoutHash).isNull() + Truth.assertThat(entity.to.amount).isNull() + Truth.assertThat(entity.to.actualAmount).isNull() + } + + private fun createExchangeItem( + status: String = "waiting", + payinExtraId: String? = "payin-extra", + refundAddress: String? = "refund-address", + refundExtraId: String? = "refund-extra", + externalTxId: String? = "external-tx-id", + externalTxUrl: String? = "https://provider.example/tx", + payinHash: String? = "payin-hash", + payoutHash: String? = "payout-hash", + refundNetwork: String? = "ethereum", + refundContractAddress: String? = "0xrefund", + payTill: String? = "2026-06-01T00:10:00Z", + averageDuration: Long? = 600L, + toActualAmount: String? = "0.99", + ) = ExchangeItemResponse( + txId = "exchange-tx-1", + providerId = "changelly", + fromAddress = "0xfrom", + payinAddress = "0xpayin", + payinExtraId = payinExtraId, + payoutAddress = "0xpayout", + refundAddress = refundAddress, + refundExtraId = refundExtraId, + rateType = "float", + status = status, + externalTxId = externalTxId, + externalTxUrl = externalTxUrl, + payinHash = payinHash, + payoutHash = payoutHash, + refundNetwork = refundNetwork, + refundContractAddress = refundContractAddress, + createdAt = "2026-06-01T00:00:00Z", + updatedAt = "2026-06-01T00:05:00Z", + payTill = payTill, + averageDuration = averageDuration, + fromContractAddress = "0xfromContract", + fromNetwork = "ethereum", + fromDecimals = 18, + fromAmount = "1.0", + toContractAddress = "0xtoContract", + toNetwork = "bitcoin", + toDecimals = 8, + toAmount = "1.0", + toActualAmount = toActualAmount, + ) + + private fun createOnrampItem( + status: String = "waiting-for-payment", + failReason: String? = "fail-reason", + externalTxId: String? = "external-tx-id", + externalTxUrl: String? = "https://provider.example/tx", + payoutHash: String? = "payout-hash", + toAmount: String? = "0.001", + toActualAmount: String? = "0.00099", + ) = OnrampItemResponse( + txId = "onramp-tx-1", + providerId = "mercuryo", + payoutAddress = "0xpayout", + status = status, + failReason = failReason, + externalTxId = externalTxId, + externalTxUrl = externalTxUrl, + payoutHash = payoutHash, + createdAt = "2026-06-01T00:00:00Z", + updatedAt = "2026-06-01T00:05:00Z", + fromCurrencyCode = "USD", + fromAmount = "100.0", + fromPrecision = 2, + toContractAddress = "0xtoContract", + toNetwork = "bitcoin", + toDecimals = 8, + toAmount = toAmount, + toActualAmount = toActualAmount, + paymentMethod = "card", + countryCode = "US", + ) + + private companion object { + const val OWNER_ADDRESS = "0xowner" + } +} \ No newline at end of file diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index 360ebda6c1..fd4abccfa9 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -5,7 +5,9 @@ import com.tangem.data.express.converter.ExpressProviderConverter import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType @@ -16,6 +18,7 @@ import com.tangem.utils.logging.TangemLogger internal class DefaultExpressRepository( private val tangemExpressApi: TangemExpressApi, + private val expressHistoryDao: ExpressHistoryDao, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : ExpressRepository { @@ -26,13 +29,17 @@ internal class DefaultExpressRepository( ): List = with(dispatchers.io) { safeApiCall( call = { - tangemExpressApi.getProviders( + val providers = tangemExpressApi.getProviders( userWalletId = userWallet.walletId.stringValue, refCode = ExpressUtils.getRefCode( userWallet = userWallet, appPreferencesStore = appPreferencesStore, ), - ).getOrThrow().map(ExpressProviderConverter()::convert) + ).getOrThrow() + + expressHistoryDao.upsertProviders(providers.map { it.toEntity() }) + + providers.map(ExpressProviderConverter()::convert) .filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes } }, onError = { error -> diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt index c696e08aec..0814d23c42 100644 --- a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.express.ExpressErrorResolver import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.ExpressServiceFetcher @@ -36,11 +37,13 @@ internal object ExpressDataModule { @Singleton fun provideExpressRepository( tangemExpressApi: TangemExpressApi, + expressHistoryDao: ExpressHistoryDao, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, ): ExpressRepository { return DefaultExpressRepository( tangemExpressApi = tangemExpressApi, + expressHistoryDao = expressHistoryDao, appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, ) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 7d5e1ad71d..41c0e75aa1 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -35,6 +36,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet @@ -72,12 +74,13 @@ internal class DefaultOnrampRepository( private val currenciesStore: OnrampCurrenciesStore, private val walletManagersFacade: WalletManagersFacade, private val dataSignatureVerifier: DataSignatureVerifier, + private val expressHistoryDao: ExpressHistoryDao, moshi: Moshi, ) : OnrampRepository { private val currencyConverter = CurrencyConverter() private val countryConverter = CountryConverter(currencyConverter) - private val statusConverter = StatusConverter() + private val statusConverter = StatusConverter(moshi) private val paymentMethodsConverter = PaymentMethodConverter() private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java) private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java) @@ -162,6 +165,8 @@ internal class DefaultOnrampRepository( ) .getOrThrow() + expressHistoryDao.upsertOnramps(listOf(response.toEntity(ownerAddress = response.payoutAddress))) + statusConverter.convert(response) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt index 70805a77fe..ae53c8244f 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/StatusConverter.kt @@ -1,16 +1,21 @@ package com.tangem.data.onramp.converters -import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse +import com.tangem.datasource.api.onramp.models.response.Status import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.utils.converter.Converter -internal class StatusConverter : Converter { - override fun convert(value: OnrampStatusResponse): OnrampStatus { +internal class StatusConverter(moshi: Moshi) : Converter { + + private val responseStatusAdapter = moshi.adapter(Status::class.java) + + override fun convert(value: OnrampItemResponse): OnrampStatus { return OnrampStatus( txId = value.txId, providerId = value.providerId, payoutAddress = value.payoutAddress, - status = OnrampStatus.Status.valueOf(value.status.name), + status = OnrampStatus.Status.valueOf(value.status.toResponseStatus().name), failReason = value.failReason, externalTxId = value.externalTxId, externalTxUrl = value.externalTxUrl, @@ -20,11 +25,14 @@ internal class StatusConverter : Converter { fromAmount = value.fromAmount, toContractAddress = value.toContractAddress, toNetwork = value.toNetwork, - toDecimals = value.toDecimals, + toDecimals = value.toDecimals.toString(), toAmount = value.toAmount, toActualAmount = value.toActualAmount, paymentMethod = value.paymentMethod, countryCode = value.countryCode, ) } + + private fun String.toResponseStatus(): Status = responseStatusAdapter.fromJsonValue(this) + ?: error("Unknown onramp status: $this") } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 1bc9b8d1ea..5b8c718f93 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -26,6 +26,7 @@ import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsSto import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.onramp.repositories.* @@ -56,6 +57,7 @@ internal object OnrampDataModule { walletManagersFacade: WalletManagersFacade, dataSignatureVerifier: DataSignatureVerifier, onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore, + expressHistoryDao: ExpressHistoryDao, @NetworkMoshi moshi: Moshi, ): OnrampRepository { return DefaultOnrampRepository( @@ -71,6 +73,7 @@ internal object OnrampDataModule { countriesStore = countriesStore, walletManagersFacade = walletManagersFacade, dataSignatureVerifier = dataSignatureVerifier, + expressHistoryDao = expressHistoryDao, moshi = moshi, ) } diff --git a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepository.kt b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepository.kt index 354df46bd6..cd24fe0123 100644 --- a/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepository.kt +++ b/data/txhistory/src/main/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepository.kt @@ -1,6 +1,5 @@ package com.tangem.data.txhistory.repository -import com.tangem.data.txhistory.repository.converter.toEntity import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse @@ -12,6 +11,7 @@ import com.tangem.datasource.api.onramp.OnrampApi import com.tangem.datasource.api.onramp.models.response.OnrampHistoryDeltaResponse import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse +import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity diff --git a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepositoryTest.kt b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepositoryTest.kt index 6be369403c..061d953409 100644 --- a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepositoryTest.kt +++ b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/ExpressHistoryRepositoryTest.kt @@ -1,7 +1,7 @@ package com.tangem.data.txhistory.repository import com.google.common.truth.Truth.assertThat -import com.tangem.data.txhistory.repository.converter.toEntity +import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.express.TangemExpressApi @@ -305,15 +305,15 @@ internal class ExpressHistoryRepositoryTest { refundAddress = null, refundExtraId = null, rateType = "float", - status = ExchangeItemResponse.Status.FINISHED, + status = "finished", externalTxId = null, - externalTxStatus = null, externalTxUrl = null, payinHash = "payin-hash", payoutHash = "payout-hash", refundNetwork = null, refundContractAddress = null, createdAt = "2026-06-01T00:00:00Z", + updatedAt = "2026-06-01T00:05:00Z", payTill = null, averageDuration = null, fromContractAddress = "0xfromContract", @@ -330,33 +330,24 @@ internal class ExpressHistoryRepositoryTest { private fun createOnrampItem(txId: String = "onramp-tx-1") = OnrampItemResponse( txId = txId, providerId = "mercuryo", - fromAddress = "0xfrom", - payinAddress = "0xpayin", - payinExtraId = null, payoutAddress = "0xpayout", - refundAddress = null, - refundExtraId = null, - rateType = "fixed", - status = OnrampItemResponse.Status.FINISHED, + status = "finished", + failReason = null, externalTxId = null, - externalTxStatus = null, externalTxUrl = null, - payinHash = "payin-hash", payoutHash = "payout-hash", - refundNetwork = null, - refundContractAddress = null, createdAt = "2026-06-01T00:00:00Z", - payTill = null, - averageDuration = null, - fromContractAddress = "0xfromContract", - fromNetwork = "usd", - fromDecimals = 2, + updatedAt = "2026-06-01T00:05:00Z", + fromCurrencyCode = "USD", fromAmount = "100.0", + fromPrecision = 2, toContractAddress = "0xtoContract", toNetwork = "bitcoin", toDecimals = 8, toAmount = "0.001", toActualAmount = "0.99", + paymentMethod = "card", + countryCode = "US", ) private companion object { diff --git a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverterTest.kt b/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverterTest.kt deleted file mode 100644 index a9e137adaf..0000000000 --- a/data/txhistory/src/test/kotlin/com/tangem/data/txhistory/repository/converter/ExpressHistoryConverterTest.kt +++ /dev/null @@ -1,316 +0,0 @@ -package com.tangem.data.txhistory.repository.converter - -import com.google.common.truth.Truth.assertThat -import com.tangem.datasource.api.express.models.response.ExchangeItemResponse -import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ExpressHistoryConverterTest { - - @Test - fun `GIVEN exchange item WHEN toEntity THEN all transaction fields are mapped`() { - // GIVEN - val item = createExchangeItem() - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.txId).isEqualTo(item.txId) - assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS) - assertThat(entity.providerId).isEqualTo(item.providerId) - assertThat(entity.fromAddress).isEqualTo(item.fromAddress) - assertThat(entity.payinAddress).isEqualTo(item.payinAddress) - assertThat(entity.payinExtraId).isEqualTo(item.payinExtraId) - assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress) - assertThat(entity.refundAddress).isEqualTo(item.refundAddress) - assertThat(entity.refundExtraId).isEqualTo(item.refundExtraId) - assertThat(entity.rateType).isEqualTo(item.rateType) - assertThat(entity.externalTxId).isEqualTo(item.externalTxId) - assertThat(entity.externalTxStatus).isEqualTo(item.externalTxStatus) - assertThat(entity.externalTxUrl).isEqualTo(item.externalTxUrl) - assertThat(entity.payinHash).isEqualTo(item.payinHash) - assertThat(entity.payoutHash).isEqualTo(item.payoutHash) - assertThat(entity.refundNetwork).isEqualTo(item.refundNetwork) - assertThat(entity.refundContractAddress).isEqualTo(item.refundContractAddress) - assertThat(entity.createdAt).isEqualTo(item.createdAt) - assertThat(entity.payTill).isEqualTo(item.payTill) - assertThat(entity.averageDuration).isEqualTo(item.averageDuration) - } - - @Test - fun `GIVEN exchange item WHEN toEntity THEN status is stored as enum name`() { - // GIVEN - val item = createExchangeItem(status = ExchangeItemResponse.Status.FINISHED) - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.status).isEqualTo("FINISHED") - } - - @Test - fun `GIVEN exchange item WHEN toEntity THEN from and to assets are mapped`() { - // GIVEN - val item = createExchangeItem() - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress) - assertThat(entity.from.network).isEqualTo(item.fromNetwork) - assertThat(entity.from.decimals).isEqualTo(item.fromDecimals) - assertThat(entity.from.amount).isEqualTo(item.fromAmount) - // `from` asset never carries an actual amount - assertThat(entity.from.actualAmount).isNull() - - assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress) - assertThat(entity.to.network).isEqualTo(item.toNetwork) - assertThat(entity.to.decimals).isEqualTo(item.toDecimals) - assertThat(entity.to.amount).isEqualTo(item.toAmount) - assertThat(entity.to.actualAmount).isEqualTo(item.toActualAmount) - } - - @Test - fun `GIVEN exchange item with null optional fields WHEN toEntity THEN nulls are preserved`() { - // GIVEN - val item = createExchangeItem( - payinExtraId = null, - refundAddress = null, - refundExtraId = null, - externalTxId = null, - externalTxStatus = null, - externalTxUrl = null, - payinHash = null, - payoutHash = null, - refundNetwork = null, - refundContractAddress = null, - payTill = null, - averageDuration = null, - toActualAmount = null, - ) - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.payinExtraId).isNull() - assertThat(entity.refundAddress).isNull() - assertThat(entity.refundExtraId).isNull() - assertThat(entity.externalTxId).isNull() - assertThat(entity.externalTxStatus).isNull() - assertThat(entity.externalTxUrl).isNull() - assertThat(entity.payinHash).isNull() - assertThat(entity.payoutHash).isNull() - assertThat(entity.refundNetwork).isNull() - assertThat(entity.refundContractAddress).isNull() - assertThat(entity.payTill).isNull() - assertThat(entity.averageDuration).isNull() - assertThat(entity.to.actualAmount).isNull() - } - - @Test - fun `GIVEN onramp item WHEN toEntity THEN all transaction fields are mapped`() { - // GIVEN - val item = createOnrampItem() - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.txId).isEqualTo(item.txId) - assertThat(entity.ownerAddress).isEqualTo(OWNER_ADDRESS) - assertThat(entity.providerId).isEqualTo(item.providerId) - assertThat(entity.fromAddress).isEqualTo(item.fromAddress) - assertThat(entity.payinAddress).isEqualTo(item.payinAddress) - assertThat(entity.payinExtraId).isEqualTo(item.payinExtraId) - assertThat(entity.payoutAddress).isEqualTo(item.payoutAddress) - assertThat(entity.refundAddress).isEqualTo(item.refundAddress) - assertThat(entity.refundExtraId).isEqualTo(item.refundExtraId) - assertThat(entity.rateType).isEqualTo(item.rateType) - assertThat(entity.externalTxId).isEqualTo(item.externalTxId) - assertThat(entity.externalTxStatus).isEqualTo(item.externalTxStatus) - assertThat(entity.externalTxUrl).isEqualTo(item.externalTxUrl) - assertThat(entity.payinHash).isEqualTo(item.payinHash) - assertThat(entity.payoutHash).isEqualTo(item.payoutHash) - assertThat(entity.refundNetwork).isEqualTo(item.refundNetwork) - assertThat(entity.refundContractAddress).isEqualTo(item.refundContractAddress) - assertThat(entity.createdAt).isEqualTo(item.createdAt) - assertThat(entity.payTill).isEqualTo(item.payTill) - assertThat(entity.averageDuration).isEqualTo(item.averageDuration) - } - - @Test - fun `GIVEN onramp item WHEN toEntity THEN status is stored as enum name`() { - // GIVEN - val item = createOnrampItem(status = OnrampItemResponse.Status.WAITING_TX_HASH) - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.status).isEqualTo("WAITING_TX_HASH") - } - - @Test - fun `GIVEN onramp item WHEN toEntity THEN from and to assets are mapped`() { - // GIVEN - val item = createOnrampItem() - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress) - assertThat(entity.from.network).isEqualTo(item.fromNetwork) - assertThat(entity.from.decimals).isEqualTo(item.fromDecimals) - assertThat(entity.from.amount).isEqualTo(item.fromAmount) - assertThat(entity.from.actualAmount).isNull() - - assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress) - assertThat(entity.to.network).isEqualTo(item.toNetwork) - assertThat(entity.to.decimals).isEqualTo(item.toDecimals) - assertThat(entity.to.amount).isEqualTo(item.toAmount) - assertThat(entity.to.actualAmount).isEqualTo(item.toActualAmount) - } - - @Test - fun `GIVEN onramp item with null optional fields WHEN toEntity THEN nulls are preserved`() { - // GIVEN - val item = createOnrampItem( - payinExtraId = null, - refundAddress = null, - refundExtraId = null, - externalTxId = null, - externalTxStatus = null, - externalTxUrl = null, - payinHash = null, - payoutHash = null, - refundNetwork = null, - refundContractAddress = null, - payTill = null, - averageDuration = null, - toActualAmount = null, - ) - - // WHEN - val entity = item.toEntity(ownerAddress = OWNER_ADDRESS) - - // THEN - assertThat(entity.payinExtraId).isNull() - assertThat(entity.refundAddress).isNull() - assertThat(entity.refundExtraId).isNull() - assertThat(entity.externalTxId).isNull() - assertThat(entity.externalTxStatus).isNull() - assertThat(entity.externalTxUrl).isNull() - assertThat(entity.payinHash).isNull() - assertThat(entity.payoutHash).isNull() - assertThat(entity.refundNetwork).isNull() - assertThat(entity.refundContractAddress).isNull() - assertThat(entity.payTill).isNull() - assertThat(entity.averageDuration).isNull() - assertThat(entity.to.actualAmount).isNull() - } - - private fun createExchangeItem( - status: ExchangeItemResponse.Status = ExchangeItemResponse.Status.WAITING, - payinExtraId: String? = "payin-extra", - refundAddress: String? = "refund-address", - refundExtraId: String? = "refund-extra", - externalTxId: String? = "external-tx-id", - externalTxStatus: String? = "external-status", - externalTxUrl: String? = "https://provider.example/tx", - payinHash: String? = "payin-hash", - payoutHash: String? = "payout-hash", - refundNetwork: String? = "ethereum", - refundContractAddress: String? = "0xrefund", - payTill: String? = "2026-06-01T00:10:00Z", - averageDuration: Long? = 600L, - toActualAmount: String? = "0.99", - ) = ExchangeItemResponse( - txId = "exchange-tx-1", - providerId = "changelly", - fromAddress = "0xfrom", - payinAddress = "0xpayin", - payinExtraId = payinExtraId, - payoutAddress = "0xpayout", - refundAddress = refundAddress, - refundExtraId = refundExtraId, - rateType = "float", - status = status, - externalTxId = externalTxId, - externalTxStatus = externalTxStatus, - externalTxUrl = externalTxUrl, - payinHash = payinHash, - payoutHash = payoutHash, - refundNetwork = refundNetwork, - refundContractAddress = refundContractAddress, - createdAt = "2026-06-01T00:00:00Z", - payTill = payTill, - averageDuration = averageDuration, - fromContractAddress = "0xfromContract", - fromNetwork = "ethereum", - fromDecimals = 18, - fromAmount = "1.0", - toContractAddress = "0xtoContract", - toNetwork = "bitcoin", - toDecimals = 8, - toAmount = "1.0", - toActualAmount = toActualAmount, - ) - - private fun createOnrampItem( - status: OnrampItemResponse.Status = OnrampItemResponse.Status.WAITING, - payinExtraId: String? = "payin-extra", - refundAddress: String? = "refund-address", - refundExtraId: String? = "refund-extra", - externalTxId: String? = "external-tx-id", - externalTxStatus: String? = "external-status", - externalTxUrl: String? = "https://provider.example/tx", - payinHash: String? = "payin-hash", - payoutHash: String? = "payout-hash", - refundNetwork: String? = "ethereum", - refundContractAddress: String? = "0xrefund", - payTill: String? = "2026-06-01T00:10:00Z", - averageDuration: Long? = 600L, - toActualAmount: String? = "0.99", - ) = OnrampItemResponse( - txId = "onramp-tx-1", - providerId = "mercuryo", - fromAddress = "0xfrom", - payinAddress = "0xpayin", - payinExtraId = payinExtraId, - payoutAddress = "0xpayout", - refundAddress = refundAddress, - refundExtraId = refundExtraId, - rateType = "fixed", - status = status, - externalTxId = externalTxId, - externalTxStatus = externalTxStatus, - externalTxUrl = externalTxUrl, - payinHash = payinHash, - payoutHash = payoutHash, - refundNetwork = refundNetwork, - refundContractAddress = refundContractAddress, - createdAt = "2026-06-01T00:00:00Z", - payTill = payTill, - averageDuration = averageDuration, - fromContractAddress = "0xfromContract", - fromNetwork = "usd", - fromDecimals = 2, - fromAmount = "100.0", - toContractAddress = "0xtoContract", - toNetwork = "bitcoin", - toDecimals = 8, - toAmount = "0.001", - toActualAmount = toActualAmount, - ) - - private companion object { - const val OWNER_ADDRESS = "0xowner" - } -} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 2a5c95d5a0..4d35b9c0fe 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -18,10 +18,12 @@ import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency @@ -52,13 +54,14 @@ internal class DefaultSwapRepository( private val dataSignatureVerifier: DataSignatureVerifier, private val appPreferencesStore: AppPreferencesStore, private val rampStateManager: RampStateManager, + private val expressHistoryDao: ExpressHistoryDao, moshi: Moshi, ) : SwapRepository { private val expressDataConverter = ExpressDataConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter() - private val exchangeStatusConverter = ExchangeStatusConverter() + private val exchangeStatusConverter = ExchangeStatusConverter(moshi) private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) override suspend fun getPairs( @@ -238,18 +241,21 @@ internal class DefaultSwapRepository( either { catch( block = { - exchangeStatusConverter.convert( - tangemExpressApi - .getExchangeStatus( - userWalletId = userWalletId.stringValue, - refCode = ExpressUtils.getRefCode( - userWallet = userWallet, - appPreferencesStore = appPreferencesStore, - ), - txId = txId, - ) - .getOrThrow(), - ) + val response = tangemExpressApi + .getExchangeStatus( + userWalletId = userWalletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + txId = txId, + ) + .getOrThrow() + + val entity = response.toEntity(ownerAddress = response.fromAddress.orEmpty()) + expressHistoryDao.upsertExchanges(listOf(entity)) + + exchangeStatusConverter.convert(response) }, catch = { exception -> TangemLogger.e("getExchangeStatus error", exception) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index a1cd69d8cb..092c7ff2a1 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -1,24 +1,33 @@ package com.tangem.feature.swap.converters -import com.tangem.datasource.api.express.models.response.ExchangeStatusResponse +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.express.models.response.ExchangeItemResponse import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.utils.converter.Converter +import org.joda.time.DateTime +import com.tangem.datasource.api.express.models.response.ExchangeStatus as ResponseExchangeStatus -internal class ExchangeStatusConverter : Converter { - override fun convert(value: ExchangeStatusResponse): ExchangeStatusModel { +internal class ExchangeStatusConverter(moshi: Moshi) : Converter { + + private val responseStatusAdapter = moshi.adapter(ResponseExchangeStatus::class.java) + + override fun convert(value: ExchangeItemResponse): ExchangeStatusModel { return ExchangeStatusModel( providerId = value.providerId, - status = ExchangeStatus.entries.firstOrNull { - it.name.lowercase() == value.status.name.lowercase() - }, + status = value.status.toExchangeStatus(), txId = value.externalTxId, txExternalUrl = value.externalTxUrl, txExternalId = value.externalTxId, refundNetwork = value.refundNetwork, refundContractAddress = value.refundContractAddress, - createdAt = value.createdAt, - averageDuration = value.averageDuration, + createdAt = runCatching { DateTime.parse(value.createdAt) }.getOrNull(), + averageDuration = value.averageDuration?.toInt(), ) } + + private fun String.toExchangeStatus(): ExchangeStatus? { + val responseStatus = responseStatusAdapter.fromJsonValue(this) ?: return null + return ExchangeStatus.entries.firstOrNull { it.name == responseStatus.name } + } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index c9cdcbbc86..ca24786ea1 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -10,6 +10,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade @@ -43,6 +44,7 @@ internal class SwapDataModule { @NetworkMoshi moshi: Moshi, appPreferencesStore: AppPreferencesStore, rampStateManager: RampStateManager, + expressHistoryDao: ExpressHistoryDao, ): SwapRepository { return DefaultSwapRepository( tangemExpressApi = tangemExpressApi, @@ -53,6 +55,7 @@ internal class SwapDataModule { moshi = moshi, appPreferencesStore = appPreferencesStore, rampStateManager = rampStateManager, + expressHistoryDao = expressHistoryDao, ) }