Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 17:43:43 +04:00
parent 6e92e35a38
commit 4d0cb6a243
24 changed files with 697 additions and 887 deletions

View file

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "36ff7cc1634c100cadc4b04fc2eba1c9", "identityHash": "442ac578743a8b624777711cf49c77e2",
"entities": [ "entities": [
{ {
"tableName": "express_provider", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -21,16 +21,55 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "iconUrl", "fieldPath": "type",
"columnName": "icon_url", "columnName": "type",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "providerUrl", "fieldPath": "imageLarge",
"columnName": "provider_url", "columnName": "image_large",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "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": { "primaryKey": {
@ -38,13 +77,11 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
}, }
"indices": [],
"foreignKeys": []
}, },
{ {
"tableName": "express_exchange", "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": [ "fields": [
{ {
"fieldPath": "txId", "fieldPath": "txId",
@ -67,8 +104,7 @@
{ {
"fieldPath": "fromAddress", "fieldPath": "fromAddress",
"columnName": "from_address", "columnName": "from_address",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": true
}, },
{ {
"fieldPath": "payinAddress", "fieldPath": "payinAddress",
@ -79,8 +115,7 @@
{ {
"fieldPath": "payinExtraId", "fieldPath": "payinExtraId",
"columnName": "payin_extra_id", "columnName": "payin_extra_id",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "payoutAddress", "fieldPath": "payoutAddress",
@ -91,14 +126,12 @@
{ {
"fieldPath": "refundAddress", "fieldPath": "refundAddress",
"columnName": "refund_address", "columnName": "refund_address",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "refundExtraId", "fieldPath": "refundExtraId",
"columnName": "refund_extra_id", "columnName": "refund_extra_id",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "rateType", "fieldPath": "rateType",
@ -115,44 +148,32 @@
{ {
"fieldPath": "externalTxId", "fieldPath": "externalTxId",
"columnName": "external_tx_id", "columnName": "external_tx_id",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
},
{
"fieldPath": "externalTxStatus",
"columnName": "external_tx_status",
"affinity": "TEXT",
"notNull": false
}, },
{ {
"fieldPath": "externalTxUrl", "fieldPath": "externalTxUrl",
"columnName": "external_tx_url", "columnName": "external_tx_url",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "payinHash", "fieldPath": "payinHash",
"columnName": "payin_hash", "columnName": "payin_hash",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "payoutHash", "fieldPath": "payoutHash",
"columnName": "payout_hash", "columnName": "payout_hash",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "refundNetwork", "fieldPath": "refundNetwork",
"columnName": "refund_network", "columnName": "refund_network",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "refundContractAddress", "fieldPath": "refundContractAddress",
"columnName": "refund_contract_address", "columnName": "refund_contract_address",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "createdAt", "fieldPath": "createdAt",
@ -160,17 +181,21 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{
"fieldPath": "updatedAt",
"columnName": "updated_at",
"affinity": "TEXT",
"notNull": true
},
{ {
"fieldPath": "payTill", "fieldPath": "payTill",
"columnName": "pay_till", "columnName": "pay_till",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "averageDuration", "fieldPath": "averageDuration",
"columnName": "average_duration", "columnName": "average_duration",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "from.contractAddress", "fieldPath": "from.contractAddress",
@ -199,8 +224,7 @@
{ {
"fieldPath": "from.actualAmount", "fieldPath": "from.actualAmount",
"columnName": "from_actual_amount", "columnName": "from_actual_amount",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "to.contractAddress", "fieldPath": "to.contractAddress",
@ -229,8 +253,7 @@
{ {
"fieldPath": "to.actualAmount", "fieldPath": "to.actualAmount",
"columnName": "to_actual_amount", "columnName": "to_actual_amount",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -241,51 +264,33 @@
}, },
"indices": [ "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, "unique": false,
"columnNames": [ "columnNames": [
"owner_address", "owner_address",
"from_network", "from_network",
"from_contract_address",
"created_at" "created_at"
], ],
"orders": [], "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, "unique": false,
"columnNames": [ "columnNames": [
"owner_address", "to_network",
"payin_hash" "to_contract_address",
"created_at"
], ],
"orders": [], "orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_owner_address_payin_hash` ON `${TABLE_NAME}` (`owner_address`, `payin_hash`)" "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`)"
},
{
"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`)"
} }
], ]
"foreignKeys": []
}, },
{ {
"tableName": "express_onramp", "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": [ "fields": [
{ {
"fieldPath": "txId", "fieldPath": "txId",
@ -305,48 +310,12 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "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", "fieldPath": "payoutAddress",
"columnName": "payout_address", "columnName": "payout_address",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "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", "fieldPath": "status",
"columnName": "status", "columnName": "status",
@ -354,46 +323,24 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "externalTxId", "fieldPath": "failReason",
"columnName": "external_tx_id", "columnName": "fail_reason",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "externalTxStatus", "fieldPath": "externalTxId",
"columnName": "external_tx_status", "columnName": "external_tx_id",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "externalTxUrl", "fieldPath": "externalTxUrl",
"columnName": "external_tx_url", "columnName": "external_tx_url",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
},
{
"fieldPath": "payinHash",
"columnName": "payin_hash",
"affinity": "TEXT",
"notNull": false
}, },
{ {
"fieldPath": "payoutHash", "fieldPath": "payoutHash",
"columnName": "payout_hash", "columnName": "payout_hash",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
},
{
"fieldPath": "refundNetwork",
"columnName": "refund_network",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "refundContractAddress",
"columnName": "refund_contract_address",
"affinity": "TEXT",
"notNull": false
}, },
{ {
"fieldPath": "createdAt", "fieldPath": "createdAt",
@ -402,46 +349,40 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "payTill", "fieldPath": "updatedAt",
"columnName": "pay_till", "columnName": "updated_at",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "averageDuration",
"columnName": "average_duration",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "from.contractAddress",
"columnName": "from_contract_address",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "from.network", "fieldPath": "fromCurrencyCode",
"columnName": "from_network", "columnName": "from_currency_code",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "from.decimals", "fieldPath": "fromAmount",
"columnName": "from_decimals",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "from.amount",
"columnName": "from_amount", "columnName": "from_amount",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "from.actualAmount", "fieldPath": "fromPrecision",
"columnName": "from_actual_amount", "columnName": "from_precision",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "paymentMethod",
"columnName": "payment_method",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": false "notNull": true
},
{
"fieldPath": "countryCode",
"columnName": "country_code",
"affinity": "TEXT",
"notNull": true
}, },
{ {
"fieldPath": "to.contractAddress", "fieldPath": "to.contractAddress",
@ -464,14 +405,12 @@
{ {
"fieldPath": "to.amount", "fieldPath": "to.amount",
"columnName": "to_amount", "columnName": "to_amount",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": true
}, },
{ {
"fieldPath": "to.actualAmount", "fieldPath": "to.actualAmount",
"columnName": "to_actual_amount", "columnName": "to_actual_amount",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -482,37 +421,18 @@
}, },
"indices": [ "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, "unique": false,
"columnNames": [ "columnNames": [
"owner_address", "owner_address",
"to_network", "to_network",
"to_contract_address",
"created_at" "created_at"
], ],
"orders": [], "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`)" "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`)"
},
{
"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`)"
} }
], ]
"foreignKeys": []
}, },
{ {
"tableName": "express_sync_state", "tableName": "express_sync_state",
@ -539,14 +459,12 @@
{ {
"fieldPath": "afterCursor", "fieldPath": "afterCursor",
"columnName": "after_cursor", "columnName": "after_cursor",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "deltaCursor", "fieldPath": "deltaCursor",
"columnName": "delta_cursor", "columnName": "delta_cursor",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@ -555,15 +473,12 @@
"type", "type",
"address" "address"
] ]
}, }
"indices": [],
"foreignKeys": []
} }
], ],
"views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View file

@ -78,7 +78,7 @@ interface TangemExpressApi {
@Header("user-id") userWalletId: String, @Header("user-id") userWalletId: String,
@Header("refcode") refCode: String?, @Header("refcode") refCode: String?,
@Query("txId") txId: String, @Query("txId") txId: String,
): ApiResponse<ExchangeStatusResponse> ): ApiResponse<ExchangeItemResponse>
@POST("exchange-sent") @POST("exchange-sent")
suspend fun exchangeSent( suspend fun exchangeSent(

View file

@ -12,9 +12,12 @@ data class ExchangeItemResponse(
@Json(name = "providerId") @Json(name = "providerId")
val providerId: String, 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") @Json(name = "fromAddress")
val fromAddress: String, val fromAddress: String?,
/** Address to which the source assets were transferred for the exchange */ /** Address to which the source assets were transferred for the exchange */
@Json(name = "payinAddress") @Json(name = "payinAddress")
@ -40,17 +43,17 @@ data class ExchangeItemResponse(
@Json(name = "rateType") @Json(name = "rateType")
val rateType: String, 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") @Json(name = "status")
val status: Status, val status: String,
/** External transaction ID (CEX only) */ /** External transaction ID (CEX only) */
@Json(name = "externalTxId") @Json(name = "externalTxId")
val externalTxId: String?, 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 (CEX only) */
@Json(name = "externalTxUrl") @Json(name = "externalTxUrl")
val externalTxUrl: String?, val externalTxUrl: String?,
@ -75,6 +78,10 @@ data class ExchangeItemResponse(
@Json(name = "createdAt") @Json(name = "createdAt")
val createdAt: String, 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 */ /** Pay-in expiration timestamp in ISO-8601 format */
@Json(name = "payTill") @Json(name = "payTill")
val payTill: String?, val payTill: String?,
@ -107,50 +114,4 @@ data class ExchangeItemResponse(
@Json(name = "toActualAmount") @Json(name = "toActualAmount")
val toActualAmount: String?, val toActualAmount: String?,
// endregion // 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,
}
}

View file

@ -9,7 +9,7 @@ data class ExpressPagination(
val endCursor: String?, val endCursor: String?,
@Json(name = "startDeltaCursor") @Json(name = "startDeltaCursor")
val startDeltaCursor: String?, val startDeltaCursor: String?,
@Json(name = "hasNextPage") @Json(name = "hasMore")
val hasMore: Boolean, val hasMore: Boolean,
) )

View file

@ -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.OnrampHistoryDeltaResponse
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse 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.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.OnrampCountryDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO
import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO
@ -87,7 +87,7 @@ interface OnrampApi {
@Header("user-id") userWalletId: String, @Header("user-id") userWalletId: String,
@Header("refcode") refCode: String?, @Header("refcode") refCode: String?,
@Query("txId") txId: String, @Query("txId") txId: String,
): ApiResponse<OnrampStatusResponse> ): ApiResponse<OnrampItemResponse>
@GET("history/onramp") @GET("history/onramp")
suspend fun getHistory( suspend fun getHistory(

View file

@ -12,87 +12,49 @@ data class OnrampItemResponse(
@Json(name = "providerId") @Json(name = "providerId")
val providerId: String, 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 */ /** Address that received the target assets */
@Json(name = "payoutAddress") @Json(name = "payoutAddress")
val payoutAddress: String, val payoutAddress: String,
/** Refund destination address */ /**
@Json(name = "refundAddress") * Raw backend status string, kept unparsed so a new value never breaks deserialization.
val refundAddress: String?, * Typed view: [com.tangem.domain.express.models.ExpressOnrampStatus].
*/
/** 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,
@Json(name = "status") @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") @Json(name = "externalTxId")
val externalTxId: String?, val externalTxId: String?,
/** Transaction status reported by the provider */ /** URL to view the transaction details on the provider side (not provided by all providers) */
@Json(name = "externalTxStatus")
val externalTxStatus: String?,
/** URL to view the transaction details (CEX only) */
@Json(name = "externalTxUrl") @Json(name = "externalTxUrl")
val externalTxUrl: String?, val externalTxUrl: String?,
/** Blockchain hash of the pay-in transaction */
@Json(name = "payinHash")
val payinHash: String?,
/** Blockchain hash of the payout transaction */ /** Blockchain hash of the payout transaction */
@Json(name = "payoutHash") @Json(name = "payoutHash")
val payoutHash: String?, 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 */ /** Transaction creation timestamp in ISO-8601 format */
@Json(name = "createdAt") @Json(name = "createdAt")
val createdAt: String, val createdAt: String,
/** Pay-in expiration timestamp in ISO-8601 format */ /** Transaction last-update timestamp in ISO-8601 format */
@Json(name = "payTill") @Json(name = "updatedAt")
val payTill: String?, val updatedAt: String,
/** Average provider exchange duration in seconds */
@Json(name = "averageDuration")
val averageDuration: Long?,
// endregion // endregion
// region fromAsset info // region fromAsset (fiat) info
@Json(name = "fromContractAddress") @Json(name = "fromCurrencyCode")
val fromContractAddress: String, val fromCurrencyCode: String,
@Json(name = "fromNetwork")
val fromNetwork: String,
@Json(name = "fromDecimals")
val fromDecimals: Int,
@Json(name = "fromAmount") @Json(name = "fromAmount")
val fromAmount: String, val fromAmount: String,
@Json(name = "fromPrecision")
val fromPrecision: Int,
// endregion // endregion
// region toAsset info // region toAsset info
@ -102,55 +64,19 @@ data class OnrampItemResponse(
val toNetwork: String, val toNetwork: String,
@Json(name = "toDecimals") @Json(name = "toDecimals")
val toDecimals: Int, val toDecimals: Int,
/** Provider-promised amount, received from the provider in the webhook */
@Json(name = "toAmount") @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") @Json(name = "toActualAmount")
val toActualAmount: String?, val toActualAmount: String?,
// endregion // endregion
) {
enum class Status { @Json(name = "paymentMethod")
val paymentMethod: String,
@Json(name = "unknown") @Json(name = "countryCode")
UNKNOWN, val countryCode: String,
)
@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,
}
}

View file

@ -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.express.models.response.ExchangeItemResponse
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse 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. * @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( return ExpressExchangeEntity(
txId = txId, txId = txId,
ownerAddress = ownerAddress, ownerAddress = ownerAddress,
@ -22,15 +22,15 @@ internal fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchang
refundAddress = refundAddress, refundAddress = refundAddress,
refundExtraId = refundExtraId, refundExtraId = refundExtraId,
rateType = rateType, rateType = rateType,
status = status.name, status = status,
externalTxId = externalTxId, externalTxId = externalTxId,
externalTxStatus = externalTxStatus,
externalTxUrl = externalTxUrl, externalTxUrl = externalTxUrl,
payinHash = payinHash, payinHash = payinHash,
payoutHash = payoutHash, payoutHash = payoutHash,
refundNetwork = refundNetwork, refundNetwork = refundNetwork,
refundContractAddress = refundContractAddress, refundContractAddress = refundContractAddress,
createdAt = createdAt, createdAt = createdAt,
updatedAt = updatedAt,
payTill = payTill, payTill = payTill,
averageDuration = averageDuration, averageDuration = averageDuration,
from = ExpressExchangeEntity.AssetEmbedded( 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( return ExpressOnrampEntity(
txId = txId, txId = txId,
ownerAddress = ownerAddress, ownerAddress = ownerAddress,
providerId = providerId, providerId = providerId,
fromAddress = fromAddress,
payinAddress = payinAddress,
payinExtraId = payinExtraId,
payoutAddress = payoutAddress, payoutAddress = payoutAddress,
refundAddress = refundAddress, status = status,
refundExtraId = refundExtraId, failReason = failReason,
rateType = rateType,
status = status.name,
externalTxId = externalTxId, externalTxId = externalTxId,
externalTxStatus = externalTxStatus,
externalTxUrl = externalTxUrl, externalTxUrl = externalTxUrl,
payinHash = payinHash,
payoutHash = payoutHash, payoutHash = payoutHash,
refundNetwork = refundNetwork,
refundContractAddress = refundContractAddress,
createdAt = createdAt, createdAt = createdAt,
payTill = payTill, updatedAt = updatedAt,
averageDuration = averageDuration, fromCurrencyCode = fromCurrencyCode,
from = ExpressOnrampEntity.AssetEmbedded( fromAmount = fromAmount,
contractAddress = fromContractAddress, fromPrecision = fromPrecision,
network = fromNetwork,
decimals = fromDecimals,
amount = fromAmount,
actualAmount = null,
),
to = ExpressOnrampEntity.AssetEmbedded( to = ExpressOnrampEntity.AssetEmbedded(
contractAddress = toContractAddress, contractAddress = toContractAddress,
network = toNetwork, network = toNetwork,
@ -87,5 +73,7 @@ internal fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEnt
amount = toAmount, amount = toAmount,
actualAmount = toActualAmount, actualAmount = toActualAmount,
), ),
paymentMethod = paymentMethod,
countryCode = countryCode,
) )
} }

View file

@ -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,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.datasource.local.txhistory.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert import androidx.room.Insert
import androidx.room.MapColumn
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
@ -21,56 +22,76 @@ interface ExpressHistoryDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertOnramps(items: List<ExpressOnrampEntity>) suspend fun upsertOnramps(items: List<ExpressOnrampEntity>)
/**
* All persisted providers keyed by [ExpressProviderEntity.id]
*/
@Query("SELECT * FROM express_provider")
fun getProvidersById(): Flow<Map<@MapColumn(columnName = "id") String, ExpressProviderEntity>>
/**
* 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( @Query(
""" """
SELECT * SELECT * FROM express_exchange
FROM express_exchange
WHERE owner_address = :ownerAddress 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 ORDER BY created_at DESC
""", """,
) )
fun observeExchanges(ownerAddress: String): Flow<List<ExpressExchangeEntity>> fun observeOutgoingSwaps(
ownerAddress: String,
network: String,
contract: String,
fromCreatedAtIso: String,
activeStatuses: List<String>,
): Flow<List<ExpressExchangeEntity>>
/**
* 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( @Query(
""" """
SELECT * SELECT * FROM express_exchange
FROM express_onramp WHERE to_network = :network
WHERE owner_address = :ownerAddress AND to_contract_address = :contract
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
ORDER BY created_at DESC ORDER BY created_at DESC
""", """,
) )
fun observeOnramps(ownerAddress: String): Flow<List<ExpressOnrampEntity>> fun observeIncomingSwaps(
network: String,
contract: String,
fromCreatedAtIso: String,
activeStatuses: List<String>,
): Flow<List<ExpressExchangeEntity>>
/**
* Onramp is always incoming: [ExpressOnrampEntity.ownerAddress] == payoutAddress. Join by `payout_hash`.
*/
@Query( @Query(
""" """
SELECT * SELECT * FROM express_onramp
FROM express_exchange
WHERE owner_address = :ownerAddress WHERE owner_address = :ownerAddress
AND payin_hash = :hash AND to_network = :network
LIMIT 1 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? fun observeIncomingOnramps(
ownerAddress: String,
@Query( network: String,
""" contract: String,
SELECT * fromCreatedAtIso: String,
FROM express_exchange activeStatuses: List<String>,
WHERE owner_address = :ownerAddress ): Flow<List<ExpressOnrampEntity>>
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?
} }

View file

@ -10,10 +10,11 @@ import androidx.room.*
@Entity( @Entity(
tableName = "express_exchange", tableName = "express_exchange",
indices = [ indices = [
Index(value = ["owner_address", "from_network", "created_at"]), // Outgoing swaps lookup (observeOutgoingSwaps): owner + from-asset equality, created_at range/sort.
Index(value = ["owner_address", "payin_hash"]), Index(value = ["owner_address", "from_network", "from_contract_address", "created_at"]),
Index(value = ["owner_address", "payout_hash"]), // Incoming (cross-owner) swaps lookup (observeIncomingSwaps): to-asset equality, created_at range/sort.
Index(value = ["provider_id"]), // 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( data class ExpressExchangeEntity(
@ -31,9 +32,12 @@ data class ExpressExchangeEntity(
@ColumnInfo(name = "provider_id") @ColumnInfo(name = "provider_id")
val providerId: String, 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") @ColumnInfo(name = "from_address")
val fromAddress: String, val fromAddress: String?,
/** Address to which the source assets were transferred for the exchange */ /** Address to which the source assets were transferred for the exchange */
@ColumnInfo(name = "payin_address") @ColumnInfo(name = "payin_address")
@ -62,20 +66,8 @@ data class ExpressExchangeEntity(
val rateType: String, val rateType: String,
/** /**
* unknown * Raw backend status string, persisted as-is (kept unparsed so a new value never breaks anything).
* exchange-tx-sent * Typed view: [com.tangem.domain.express.models.ExpressExchangeStatus].
* waiting
* waiting-tx-hash
* expired
* confirming
* exchanging
* sending
* finished
* failed
* tx-failed
* refunded
* verifying
* paused
*/ */
@ColumnInfo(name = "status") @ColumnInfo(name = "status")
val status: String, val status: String,
@ -84,10 +76,6 @@ data class ExpressExchangeEntity(
@ColumnInfo(name = "external_tx_id") @ColumnInfo(name = "external_tx_id")
val externalTxId: String?, 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 (CEX only) */
@ColumnInfo(name = "external_tx_url") @ColumnInfo(name = "external_tx_url")
val externalTxUrl: String?, val externalTxUrl: String?,
@ -112,6 +100,10 @@ data class ExpressExchangeEntity(
@ColumnInfo(name = "created_at") @ColumnInfo(name = "created_at")
val createdAt: String, 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 */ /** Pay-in expiration timestamp in ISO-8601 format */
@ColumnInfo(name = "pay_till") @ColumnInfo(name = "pay_till")
val payTill: String?, val payTill: String?,

View file

@ -10,9 +10,8 @@ import androidx.room.*
@Entity( @Entity(
tableName = "express_onramp", tableName = "express_onramp",
indices = [ indices = [
Index(value = ["owner_address", "to_network", "created_at"]), // Incoming onramp lookup (observeIncomingOnramps): owner + to-asset equality, created_at range/sort.
Index(value = ["owner_address", "payout_hash"]), Index(value = ["owner_address", "to_network", "to_contract_address", "created_at"]),
Index(value = ["provider_id"]),
], ],
) )
data class ExpressOnrampEntity( data class ExpressOnrampEntity(
@ -30,100 +29,61 @@ data class ExpressOnrampEntity(
@ColumnInfo(name = "provider_id") @ColumnInfo(name = "provider_id")
val providerId: String, 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 */ /** Address that received the target assets */
@ColumnInfo(name = "payout_address") @ColumnInfo(name = "payout_address")
val payoutAddress: String, 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 * 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 = "rate_type")
val rateType: String,
/**
* unknown
* exchange-tx-sent
* waiting
* waiting-tx-hash
* expired
* confirming
* exchanging
* sending
* finished
* failed
* tx-failed
* refunded
* verifying
* paused
*/ */
@ColumnInfo(name = "status") @ColumnInfo(name = "status")
val status: String, 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") @ColumnInfo(name = "external_tx_id")
val externalTxId: String?, val externalTxId: String?,
/** Transaction status reported by the provider */ /** URL to view the transaction details on the provider side (not provided by all providers) */
@ColumnInfo(name = "external_tx_status")
val externalTxStatus: String?,
/** URL to view the transaction details (CEX only) */
@ColumnInfo(name = "external_tx_url") @ColumnInfo(name = "external_tx_url")
val externalTxUrl: String?, val externalTxUrl: String?,
/** Blockchain hash of the pay-in transaction */
@ColumnInfo(name = "payin_hash")
val payinHash: String?,
/** Blockchain hash of the payout transaction */ /** Blockchain hash of the payout transaction */
@ColumnInfo(name = "payout_hash") @ColumnInfo(name = "payout_hash")
val payoutHash: String?, 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 */ /** Transaction creation timestamp in ISO-8601 format */
@ColumnInfo(name = "created_at") @ColumnInfo(name = "created_at")
val createdAt: String, val createdAt: String,
/** Pay-in expiration timestamp in ISO-8601 format */ /** Transaction last-update timestamp in ISO-8601 format */
@ColumnInfo(name = "pay_till") @ColumnInfo(name = "updated_at")
val payTill: String?, val updatedAt: String,
/** Average provider exchange duration in seconds */ /** Fiat currency code of the source funds */
@ColumnInfo(name = "average_duration") @ColumnInfo(name = "from_currency_code")
val averageDuration: Long?, val fromCurrencyCode: String,
@Embedded(prefix = "from_") /** Fiat amount of the source funds */
val from: AssetEmbedded, @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_") @Embedded(prefix = "to_")
val to: AssetEmbedded, val to: AssetEmbedded,
@ColumnInfo(name = "payment_method")
val paymentMethod: String,
@ColumnInfo(name = "country_code")
val countryCode: String,
) { ) {
data class AssetEmbedded( data class AssetEmbedded(
@ -137,10 +97,11 @@ data class ExpressOnrampEntity(
@ColumnInfo(name = "decimals") @ColumnInfo(name = "decimals")
val decimals: Int, val decimals: Int,
/** Provider-promised amount. Present only if the provider reported it */
@ColumnInfo(name = "amount") @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") @ColumnInfo(name = "actual_amount")
val actualAmount: String?, val actualAmount: String?,
) )

View file

@ -4,9 +4,13 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey 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( data class ExpressProviderEntity(
@PrimaryKey @PrimaryKey
@ -16,9 +20,34 @@ data class ExpressProviderEntity(
@ColumnInfo(name = "name") @ColumnInfo(name = "name")
val name: String, val name: String,
@ColumnInfo(name = "icon_url") /** Raw provider type (`dex` / `cex` / `dex-bridge` / `onramp`). Typed view: ExpressProviderType. */
val iconUrl: String, @ColumnInfo(name = "type")
val type: String,
@ColumnInfo(name = "provider_url") /** Large logo image URL. */
val providerUrl: String, @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,
) )

View file

@ -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"
}
}

View file

@ -5,7 +5,9 @@ import com.tangem.data.express.converter.ExpressProviderConverter
import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.exchangeservice.swap.ExpressUtils 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.AppPreferencesStore
import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao
import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressProviderType
@ -16,6 +18,7 @@ import com.tangem.utils.logging.TangemLogger
internal class DefaultExpressRepository( internal class DefaultExpressRepository(
private val tangemExpressApi: TangemExpressApi, private val tangemExpressApi: TangemExpressApi,
private val expressHistoryDao: ExpressHistoryDao,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val dispatchers: CoroutineDispatcherProvider, private val dispatchers: CoroutineDispatcherProvider,
) : ExpressRepository { ) : ExpressRepository {
@ -26,13 +29,17 @@ internal class DefaultExpressRepository(
): List<ExpressProvider> = with(dispatchers.io) { ): List<ExpressProvider> = with(dispatchers.io) {
safeApiCall( safeApiCall(
call = { call = {
tangemExpressApi.getProviders( val providers = tangemExpressApi.getProviders(
userWalletId = userWallet.walletId.stringValue, userWalletId = userWallet.walletId.stringValue,
refCode = ExpressUtils.getRefCode( refCode = ExpressUtils.getRefCode(
userWallet = userWallet, userWallet = userWallet,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
), ),
).getOrThrow().map(ExpressProviderConverter()::convert) ).getOrThrow()
expressHistoryDao.upsertProviders(providers.map { it.toEntity() })
providers.map(ExpressProviderConverter()::convert)
.filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes } .filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes }
}, },
onError = { error -> onError = { error ->

View file

@ -9,6 +9,7 @@ import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.api.express.models.response.ExpressErrorResponse
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.ExpressErrorResolver
import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.ExpressRepository
import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.ExpressServiceFetcher
@ -36,11 +37,13 @@ internal object ExpressDataModule {
@Singleton @Singleton
fun provideExpressRepository( fun provideExpressRepository(
tangemExpressApi: TangemExpressApi, tangemExpressApi: TangemExpressApi,
expressHistoryDao: ExpressHistoryDao,
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
): ExpressRepository { ): ExpressRepository {
return DefaultExpressRepository( return DefaultExpressRepository(
tangemExpressApi = tangemExpressApi, tangemExpressApi = tangemExpressApi,
expressHistoryDao = expressHistoryDao,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
dispatchers = dispatchers, dispatchers = dispatchers,
) )

View file

@ -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.api.onramp.models.response.model.PaymentMethodDTO
import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils 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.countries.OnrampCountriesStore
import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore
import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore 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.getObject
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject 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.express.models.ExpressAsset
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
@ -72,12 +74,13 @@ internal class DefaultOnrampRepository(
private val currenciesStore: OnrampCurrenciesStore, private val currenciesStore: OnrampCurrenciesStore,
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
private val dataSignatureVerifier: DataSignatureVerifier, private val dataSignatureVerifier: DataSignatureVerifier,
private val expressHistoryDao: ExpressHistoryDao,
moshi: Moshi, moshi: Moshi,
) : OnrampRepository { ) : OnrampRepository {
private val currencyConverter = CurrencyConverter() private val currencyConverter = CurrencyConverter()
private val countryConverter = CountryConverter(currencyConverter) private val countryConverter = CountryConverter(currencyConverter)
private val statusConverter = StatusConverter() private val statusConverter = StatusConverter(moshi)
private val paymentMethodsConverter = PaymentMethodConverter() private val paymentMethodsConverter = PaymentMethodConverter()
private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java) private val onrampDataAdapter = moshi.adapter(OnrampDataJson::class.java)
private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java) private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java)
@ -162,6 +165,8 @@ internal class DefaultOnrampRepository(
) )
.getOrThrow() .getOrThrow()
expressHistoryDao.upsertOnramps(listOf(response.toEntity(ownerAddress = response.payoutAddress)))
statusConverter.convert(response) statusConverter.convert(response)
} }

View file

@ -1,16 +1,21 @@
package com.tangem.data.onramp.converters 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.domain.onramp.model.OnrampStatus
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> { internal class StatusConverter(moshi: Moshi) : Converter<OnrampItemResponse, OnrampStatus> {
override fun convert(value: OnrampStatusResponse): OnrampStatus {
private val responseStatusAdapter = moshi.adapter(Status::class.java)
override fun convert(value: OnrampItemResponse): OnrampStatus {
return OnrampStatus( return OnrampStatus(
txId = value.txId, txId = value.txId,
providerId = value.providerId, providerId = value.providerId,
payoutAddress = value.payoutAddress, payoutAddress = value.payoutAddress,
status = OnrampStatus.Status.valueOf(value.status.name), status = OnrampStatus.Status.valueOf(value.status.toResponseStatus().name),
failReason = value.failReason, failReason = value.failReason,
externalTxId = value.externalTxId, externalTxId = value.externalTxId,
externalTxUrl = value.externalTxUrl, externalTxUrl = value.externalTxUrl,
@ -20,11 +25,14 @@ internal class StatusConverter : Converter<OnrampStatusResponse, OnrampStatus> {
fromAmount = value.fromAmount, fromAmount = value.fromAmount,
toContractAddress = value.toContractAddress, toContractAddress = value.toContractAddress,
toNetwork = value.toNetwork, toNetwork = value.toNetwork,
toDecimals = value.toDecimals, toDecimals = value.toDecimals.toString(),
toAmount = value.toAmount, toAmount = value.toAmount,
toActualAmount = value.toActualAmount, toActualAmount = value.toActualAmount,
paymentMethod = value.paymentMethod, paymentMethod = value.paymentMethod,
countryCode = value.countryCode, countryCode = value.countryCode,
) )
} }
private fun String.toResponseStatus(): Status = responseStatusAdapter.fromJsonValue(this)
?: error("Unknown onramp status: $this")
} }

View file

@ -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.quotes.OnrampQuotesStore
import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.domain.common.wallets.UserWalletsListRepository
import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.domain.onramp.repositories.* import com.tangem.domain.onramp.repositories.*
@ -56,6 +57,7 @@ internal object OnrampDataModule {
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
dataSignatureVerifier: DataSignatureVerifier, dataSignatureVerifier: DataSignatureVerifier,
onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore, onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore,
expressHistoryDao: ExpressHistoryDao,
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
): OnrampRepository { ): OnrampRepository {
return DefaultOnrampRepository( return DefaultOnrampRepository(
@ -71,6 +73,7 @@ internal object OnrampDataModule {
countriesStore = countriesStore, countriesStore = countriesStore,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
dataSignatureVerifier = dataSignatureVerifier, dataSignatureVerifier = dataSignatureVerifier,
expressHistoryDao = expressHistoryDao,
moshi = moshi, moshi = moshi,
) )
} }

View file

@ -1,6 +1,5 @@
package com.tangem.data.txhistory.repository 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.common.response.getOrThrow
import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.TangemExpressApi
import com.tangem.datasource.api.express.models.response.ExchangeHistoryDeltaResponse 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.OnrampHistoryDeltaResponse
import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse import com.tangem.datasource.api.onramp.models.response.OnrampHistoryResponse
import com.tangem.datasource.api.onramp.models.response.OnrampItemResponse 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.ExpressHistoryDao
import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao import com.tangem.datasource.local.txhistory.db.dao.ExpressSyncStateDao
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity

View file

@ -1,7 +1,7 @@
package com.tangem.data.txhistory.repository package com.tangem.data.txhistory.repository
import com.google.common.truth.Truth.assertThat 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.ApiResponse
import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.TangemExpressApi
@ -305,15 +305,15 @@ internal class ExpressHistoryRepositoryTest {
refundAddress = null, refundAddress = null,
refundExtraId = null, refundExtraId = null,
rateType = "float", rateType = "float",
status = ExchangeItemResponse.Status.FINISHED, status = "finished",
externalTxId = null, externalTxId = null,
externalTxStatus = null,
externalTxUrl = null, externalTxUrl = null,
payinHash = "payin-hash", payinHash = "payin-hash",
payoutHash = "payout-hash", payoutHash = "payout-hash",
refundNetwork = null, refundNetwork = null,
refundContractAddress = null, refundContractAddress = null,
createdAt = "2026-06-01T00:00:00Z", createdAt = "2026-06-01T00:00:00Z",
updatedAt = "2026-06-01T00:05:00Z",
payTill = null, payTill = null,
averageDuration = null, averageDuration = null,
fromContractAddress = "0xfromContract", fromContractAddress = "0xfromContract",
@ -330,33 +330,24 @@ internal class ExpressHistoryRepositoryTest {
private fun createOnrampItem(txId: String = "onramp-tx-1") = OnrampItemResponse( private fun createOnrampItem(txId: String = "onramp-tx-1") = OnrampItemResponse(
txId = txId, txId = txId,
providerId = "mercuryo", providerId = "mercuryo",
fromAddress = "0xfrom",
payinAddress = "0xpayin",
payinExtraId = null,
payoutAddress = "0xpayout", payoutAddress = "0xpayout",
refundAddress = null, status = "finished",
refundExtraId = null, failReason = null,
rateType = "fixed",
status = OnrampItemResponse.Status.FINISHED,
externalTxId = null, externalTxId = null,
externalTxStatus = null,
externalTxUrl = null, externalTxUrl = null,
payinHash = "payin-hash",
payoutHash = "payout-hash", payoutHash = "payout-hash",
refundNetwork = null,
refundContractAddress = null,
createdAt = "2026-06-01T00:00:00Z", createdAt = "2026-06-01T00:00:00Z",
payTill = null, updatedAt = "2026-06-01T00:05:00Z",
averageDuration = null, fromCurrencyCode = "USD",
fromContractAddress = "0xfromContract",
fromNetwork = "usd",
fromDecimals = 2,
fromAmount = "100.0", fromAmount = "100.0",
fromPrecision = 2,
toContractAddress = "0xtoContract", toContractAddress = "0xtoContract",
toNetwork = "bitcoin", toNetwork = "bitcoin",
toDecimals = 8, toDecimals = 8,
toAmount = "0.001", toAmount = "0.001",
toActualAmount = "0.99", toActualAmount = "0.99",
paymentMethod = "card",
countryCode = "US",
) )
private companion object { private companion object {

View file

@ -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"
}
}

View file

@ -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.api.express.models.response.TxDetails
import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.exchangeservice.swap.ExpressUtils 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.AppPreferencesStore
import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.PreferencesKeys
import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull
import com.tangem.datasource.local.preferences.utils.storeObject 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.exchange.RampStateManager
import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.express.models.ExpressOperationType
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
@ -52,13 +54,14 @@ internal class DefaultSwapRepository(
private val dataSignatureVerifier: DataSignatureVerifier, private val dataSignatureVerifier: DataSignatureVerifier,
private val appPreferencesStore: AppPreferencesStore, private val appPreferencesStore: AppPreferencesStore,
private val rampStateManager: RampStateManager, private val rampStateManager: RampStateManager,
private val expressHistoryDao: ExpressHistoryDao,
moshi: Moshi, moshi: Moshi,
) : SwapRepository { ) : SwapRepository {
private val expressDataConverter = ExpressDataConverter() private val expressDataConverter = ExpressDataConverter()
private val leastTokenInfoConverter = LeastTokenInfoConverter() private val leastTokenInfoConverter = LeastTokenInfoConverter()
private val swapPairInfoConverter = SwapPairInfoConverter() private val swapPairInfoConverter = SwapPairInfoConverter()
private val exchangeStatusConverter = ExchangeStatusConverter() private val exchangeStatusConverter = ExchangeStatusConverter(moshi)
private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java)
override suspend fun getPairs( override suspend fun getPairs(
@ -238,18 +241,21 @@ internal class DefaultSwapRepository(
either { either {
catch( catch(
block = { block = {
exchangeStatusConverter.convert( val response = tangemExpressApi
tangemExpressApi .getExchangeStatus(
.getExchangeStatus( userWalletId = userWalletId.stringValue,
userWalletId = userWalletId.stringValue, refCode = ExpressUtils.getRefCode(
refCode = ExpressUtils.getRefCode( userWallet = userWallet,
userWallet = userWallet, appPreferencesStore = appPreferencesStore,
appPreferencesStore = appPreferencesStore, ),
), txId = txId,
txId = txId, )
) .getOrThrow()
.getOrThrow(),
) val entity = response.toEntity(ownerAddress = response.fromAddress.orEmpty())
expressHistoryDao.upsertExchanges(listOf(entity))
exchangeStatusConverter.convert(response)
}, },
catch = { exception -> catch = { exception ->
TangemLogger.e("getExchangeStatus error", exception) TangemLogger.e("getExchangeStatus error", exception)

View file

@ -1,24 +1,33 @@
package com.tangem.feature.swap.converters 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.ExchangeStatus
import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel
import com.tangem.utils.converter.Converter 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<ExchangeStatusResponse, ExchangeStatusModel> { internal class ExchangeStatusConverter(moshi: Moshi) : Converter<ExchangeItemResponse, ExchangeStatusModel> {
override fun convert(value: ExchangeStatusResponse): ExchangeStatusModel {
private val responseStatusAdapter = moshi.adapter(ResponseExchangeStatus::class.java)
override fun convert(value: ExchangeItemResponse): ExchangeStatusModel {
return ExchangeStatusModel( return ExchangeStatusModel(
providerId = value.providerId, providerId = value.providerId,
status = ExchangeStatus.entries.firstOrNull { status = value.status.toExchangeStatus(),
it.name.lowercase() == value.status.name.lowercase()
},
txId = value.externalTxId, txId = value.externalTxId,
txExternalUrl = value.externalTxUrl, txExternalUrl = value.externalTxUrl,
txExternalId = value.externalTxId, txExternalId = value.externalTxId,
refundNetwork = value.refundNetwork, refundNetwork = value.refundNetwork,
refundContractAddress = value.refundContractAddress, refundContractAddress = value.refundContractAddress,
createdAt = value.createdAt, createdAt = runCatching { DateTime.parse(value.createdAt) }.getOrNull(),
averageDuration = value.averageDuration, averageDuration = value.averageDuration?.toInt(),
) )
} }
private fun String.toExchangeStatus(): ExchangeStatus? {
val responseStatus = responseStatusAdapter.fromJsonValue(this) ?: return null
return ExchangeStatus.entries.firstOrNull { it.name == responseStatus.name }
}
} }

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier
import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.environment.EnvironmentConfig
import com.tangem.datasource.local.preferences.AppPreferencesStore 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.account.supplier.SingleAccountListSupplier
import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade
@ -43,6 +44,7 @@ internal class SwapDataModule {
@NetworkMoshi moshi: Moshi, @NetworkMoshi moshi: Moshi,
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
rampStateManager: RampStateManager, rampStateManager: RampStateManager,
expressHistoryDao: ExpressHistoryDao,
): SwapRepository { ): SwapRepository {
return DefaultSwapRepository( return DefaultSwapRepository(
tangemExpressApi = tangemExpressApi, tangemExpressApi = tangemExpressApi,
@ -53,6 +55,7 @@ internal class SwapDataModule {
moshi = moshi, moshi = moshi,
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
rampStateManager = rampStateManager, rampStateManager = rampStateManager,
expressHistoryDao = expressHistoryDao,
) )
} }