Updated on 2026-08-14
This commit is contained in:
commit
92849d43c2
1407 changed files with 64092 additions and 11990 deletions
|
|
@ -60,70 +60,80 @@ androidComponents {
|
|||
}
|
||||
dependencies {
|
||||
|
||||
/** Project */
|
||||
implementation(projects.core.analytics)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.core.res)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.wallets.models)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.txhistory.models)
|
||||
implementation(projects.domain.staking.models)
|
||||
implementation(projects.domain.onramp.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.nft.models)
|
||||
implementation(projects.domain.walletConnect.models)
|
||||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.visa.models)
|
||||
|
||||
/** Tangem libraries */
|
||||
implementation(tangemDeps.blockchain)
|
||||
implementation(tangemDeps.card.core)
|
||||
|
||||
/** DI */
|
||||
// region DI
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
// endregion
|
||||
|
||||
/** Coroutines */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.kotlin.coroutines.rx2)
|
||||
// region Kotlin
|
||||
api(deps.kotlin.coroutines)
|
||||
api(deps.kotlin.datetime)
|
||||
api(deps.kotlin.serialization)
|
||||
// endregion
|
||||
|
||||
/** Logging */
|
||||
// region AndroidX
|
||||
implementation(deps.androidx.core)
|
||||
api(deps.androidx.datastore)
|
||||
// endregion
|
||||
|
||||
/** Network */
|
||||
// region Network
|
||||
api(deps.moshi)
|
||||
implementation(deps.moshi.kotlin)
|
||||
implementation(deps.moshi.adapters)
|
||||
implementation(deps.moshi.adapters.ext)
|
||||
implementation(deps.moshi.kotlin)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
api(deps.okHttp)
|
||||
implementation(deps.okHttp.prettyLogging)
|
||||
implementation(deps.okio)
|
||||
api(deps.retrofit)
|
||||
implementation(deps.retrofit.moshi)
|
||||
ksp(deps.moshi.kotlin.codegen)
|
||||
api(deps.retrofit.moshi)
|
||||
kaptForObfuscatingVariants(deps.retrofit.response.type.keeper)
|
||||
// endregion
|
||||
|
||||
/** Time */
|
||||
implementation(deps.jodatime)
|
||||
// region Room
|
||||
api(deps.room.runtime)
|
||||
api(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
// endregion
|
||||
|
||||
/** Security */
|
||||
implementation(deps.spongecastle.core)
|
||||
// region Other libraries
|
||||
api(deps.jodatime)
|
||||
runtimeOnly(deps.spongecastle.core)
|
||||
// endregion
|
||||
|
||||
/** Chucker */
|
||||
// region Chucker
|
||||
debugImplementation(deps.chucker)
|
||||
mockedImplementation(deps.chucker)
|
||||
externalImplementation(deps.chuckerStub)
|
||||
internalImplementation(deps.chuckerStub)
|
||||
releaseImplementation(deps.chuckerStub)
|
||||
// endregion
|
||||
|
||||
/** Local storages */
|
||||
api(deps.androidx.datastore)
|
||||
implementation(deps.room.runtime)
|
||||
implementation(deps.room.ktx)
|
||||
ksp(deps.room.compiler)
|
||||
// region Tangem
|
||||
api(tangemDeps.blockchain)
|
||||
api(tangemDeps.card.core)
|
||||
// endregion
|
||||
|
||||
// region Core modules
|
||||
api(projects.core.analytics)
|
||||
implementation(projects.core.analytics.models)
|
||||
api(projects.core.utils)
|
||||
implementation(projects.core.res)
|
||||
// endregion
|
||||
|
||||
// region Domain models
|
||||
api(projects.domain.models)
|
||||
api(projects.domain.nft.models)
|
||||
api(projects.domain.onramp.models)
|
||||
api(projects.domain.staking.models)
|
||||
api(projects.domain.txhistory.models)
|
||||
api(projects.domain.visa.models)
|
||||
api(projects.domain.walletConnect.models)
|
||||
api(projects.domain.wallets.models)
|
||||
api(projects.domain.yieldSupply.models)
|
||||
// endregion
|
||||
|
||||
// region Tests
|
||||
testImplementation(projects.test.core)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "442ac578743a8b624777711cf49c77e2",
|
||||
"identityHash": "c0f8e7d958a8852440a5bdec5a989f0e",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "express_provider",
|
||||
|
|
@ -81,7 +81,7 @@
|
|||
},
|
||||
{
|
||||
"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, `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`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` 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_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",
|
||||
|
|
@ -89,12 +89,6 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerAddress",
|
||||
"columnName": "owner_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "providerId",
|
||||
"columnName": "provider_id",
|
||||
|
|
@ -104,7 +98,8 @@
|
|||
{
|
||||
"fieldPath": "fromAddress",
|
||||
"columnName": "from_address",
|
||||
"affinity": "TEXT"
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "payinAddress",
|
||||
|
|
@ -264,16 +259,16 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_express_exchange_owner_address_from_network_from_contract_address_created_at",
|
||||
"name": "index_express_exchange_from_address_from_network_from_contract_address_created_at",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"owner_address",
|
||||
"from_address",
|
||||
"from_network",
|
||||
"from_contract_address",
|
||||
"created_at"
|
||||
],
|
||||
"orders": [],
|
||||
"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`)"
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_exchange_from_address_from_network_from_contract_address_created_at` ON `${TABLE_NAME}` (`from_address`, `from_network`, `from_contract_address`, `created_at`)"
|
||||
},
|
||||
{
|
||||
"name": "index_express_exchange_to_network_to_contract_address_created_at",
|
||||
|
|
@ -290,7 +285,7 @@
|
|||
},
|
||||
{
|
||||
"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, `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`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tx_id` 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",
|
||||
|
|
@ -298,12 +293,6 @@
|
|||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerAddress",
|
||||
"columnName": "owner_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "providerId",
|
||||
"columnName": "provider_id",
|
||||
|
|
@ -421,16 +410,16 @@
|
|||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_express_onramp_owner_address_to_network_to_contract_address_created_at",
|
||||
"name": "index_express_onramp_payout_address_to_network_to_contract_address_created_at",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"owner_address",
|
||||
"payout_address",
|
||||
"to_network",
|
||||
"to_contract_address",
|
||||
"created_at"
|
||||
],
|
||||
"orders": [],
|
||||
"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`)"
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_express_onramp_payout_address_to_network_to_contract_address_created_at` ON `${TABLE_NAME}` (`payout_address`, `to_network`, `to_contract_address`, `created_at`)"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
@ -474,11 +463,194 @@
|
|||
"address"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "onramp_country",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`code` TEXT NOT NULL, `name` TEXT NOT NULL, `image` TEXT NOT NULL, `alpha3` TEXT NOT NULL, `continent` TEXT NOT NULL, `onramp_available` INTEGER NOT NULL, `currency_name` TEXT NOT NULL, `currency_code` TEXT NOT NULL, `currency_image` TEXT, `currency_precision` INTEGER NOT NULL, `currency_unit` TEXT NOT NULL, PRIMARY KEY(`code`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "code",
|
||||
"columnName": "code",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "image",
|
||||
"columnName": "image",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "alpha3",
|
||||
"columnName": "alpha3",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "continent",
|
||||
"columnName": "continent",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOnrampAvailable",
|
||||
"columnName": "onramp_available",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.name",
|
||||
"columnName": "currency_name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.code",
|
||||
"columnName": "currency_code",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.image",
|
||||
"columnName": "currency_image",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.precision",
|
||||
"columnName": "currency_precision",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "defaultCurrency.unit",
|
||||
"columnName": "currency_unit",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "token_info",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`network_id` TEXT NOT NULL, `contract_address` TEXT NOT NULL, `coin_id` TEXT NOT NULL, `name` TEXT NOT NULL, `symbol` TEXT NOT NULL, `decimals` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, PRIMARY KEY(`network_id`, `contract_address`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "networkId",
|
||||
"columnName": "network_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "contractAddress",
|
||||
"columnName": "contract_address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "coinId",
|
||||
"columnName": "coin_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "symbol",
|
||||
"columnName": "symbol",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "decimals",
|
||||
"columnName": "decimals",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "updatedAt",
|
||||
"columnName": "updated_at",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"network_id",
|
||||
"contract_address"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "history_index",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `entity_id` TEXT NOT NULL, `address` TEXT NOT NULL, `sort_time_millis` INTEGER NOT NULL, PRIMARY KEY(`type`, `entity_id`, `address`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "type",
|
||||
"columnName": "type",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "entityId",
|
||||
"columnName": "entity_id",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sortTimeMillis",
|
||||
"columnName": "sort_time_millis",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"type",
|
||||
"entity_id",
|
||||
"address"
|
||||
]
|
||||
},
|
||||
"indices": [
|
||||
{
|
||||
"name": "index_history_index_address_sort_time_millis_entity_id",
|
||||
"unique": false,
|
||||
"columnNames": [
|
||||
"address",
|
||||
"sort_time_millis",
|
||||
"entity_id"
|
||||
],
|
||||
"orders": [],
|
||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_history_index_address_sort_time_millis_entity_id` ON `${TABLE_NAME}` (`address`, `sort_time_millis`, `entity_id`)"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"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, '442ac578743a8b624777711cf49c77e2')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c0f8e7d958a8852440a5bdec5a989f0e')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.datasource.api.addressbook
|
||||
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksRequest
|
||||
import com.tangem.datasource.api.addressbook.models.SyncAddressBooksResponse
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookRequest
|
||||
import com.tangem.datasource.api.addressbook.models.UpdateAddressBookResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Path
|
||||
|
||||
interface AddressBookApi {
|
||||
|
||||
@POST("v1/address-books/sync")
|
||||
suspend fun syncAddressBooks(@Body body: SyncAddressBooksRequest): ApiResponse<SyncAddressBooksResponse>
|
||||
|
||||
@PUT("v1/address-books/{walletId}")
|
||||
suspend fun updateAddressBook(
|
||||
@Path("walletId") walletId: String,
|
||||
@Header("If-Match") eTag: String?,
|
||||
@Body body: UpdateAddressBookRequest,
|
||||
): ApiResponse<UpdateAddressBookResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for `POST /address-books/sync`.
|
||||
*
|
||||
* Each [Wallet.etag] is optional: when it matches the backend's etag, that wallet is omitted from the
|
||||
* response and the local copy is kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksRequest(
|
||||
@Json(name = "wallets") val wallets: List<Wallet>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Wallet(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String? = null,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response body for `POST /address-books/sync`.
|
||||
*
|
||||
* [items] contains only the wallets whose backend etag differs from the one sent in the request; wallets
|
||||
* with a matching etag are omitted and their local copy must be kept.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class SyncAddressBooksResponse(
|
||||
@Json(name = "items") val items: List<Item>,
|
||||
) {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Item(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Request body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookRequest(
|
||||
@Json(name = "version") val version: String,
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
@Json(name = "ciphertext") val ciphertext: String,
|
||||
@Json(name = "authTag") val authTag: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.api.addressbook.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Response body for `PUT /address-books/{walletId}`. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class UpdateAddressBookResponse(
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
@Json(name = "etag") val etag: String,
|
||||
@Json(name = "updatedAt") val updatedAt: String,
|
||||
)
|
||||
|
|
@ -4,6 +4,7 @@ import com.tangem.datasource.api.auth.models.request.AuthApiRequest
|
|||
import com.tangem.datasource.api.auth.models.request.NonceApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.RefreshApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.RegisterApiRequest
|
||||
import com.tangem.datasource.api.auth.models.request.WalletRegistrationRequest
|
||||
import com.tangem.datasource.api.auth.models.response.NonceApiResponse
|
||||
import com.tangem.datasource.api.auth.models.response.TokenApiResponse
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
|
|
@ -30,8 +31,7 @@ interface AuthApi {
|
|||
* session token pair. Called once per app install.
|
||||
*/
|
||||
@POST("api/v1/auth/register")
|
||||
@RequiresDpopProof
|
||||
suspend fun register(@Body request: RegisterApiRequest): ApiResponse<TokenApiResponse>
|
||||
suspend fun registerDevice(@Body request: RegisterApiRequest): ApiResponse<TokenApiResponse>
|
||||
|
||||
/**
|
||||
* Request authentication nonce.
|
||||
|
|
@ -61,4 +61,23 @@ interface AuthApi {
|
|||
@POST("api/v1/auth/refresh")
|
||||
@RequiresDpopProof
|
||||
suspend fun refresh(@Body request: RefreshApiRequest): ApiResponse<TokenApiResponse>
|
||||
|
||||
/**
|
||||
* Request wallet registration nonce.
|
||||
*
|
||||
* Generates a nonce bound to the device public key for the wallet registration flow.
|
||||
*/
|
||||
@POST("api/v1/auth/nonce/wallet")
|
||||
suspend fun requestWalletNonce(@Body request: NonceApiRequest): ApiResponse<NonceApiResponse>
|
||||
|
||||
/**
|
||||
* Register a wallet.
|
||||
*
|
||||
* Binds a new wallet to an already-registered device. When a card signature is provided the
|
||||
* wallet is bound as COLD (card-backed); otherwise it is registered as a MOBILE (hot) wallet.
|
||||
* Returns refreshed session tokens reflecting the updated wallet list.
|
||||
*/
|
||||
@POST("api/v1/auth/wallet")
|
||||
@RequiresDpopProof
|
||||
suspend fun registerWallet(@Body request: WalletRegistrationRequest): ApiResponse<TokenApiResponse>
|
||||
}
|
||||
|
|
@ -10,17 +10,17 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class DeviceMetadata(
|
||||
/** Device hardware model (e.g. `iPhone 15 Pro`). */
|
||||
@Json(name = "deviceModel") val deviceModel: String?,
|
||||
@Json(name = "deviceModel") val deviceModel: String,
|
||||
/** Operating system (`android` / `ios`). */
|
||||
@Json(name = "os") val os: String,
|
||||
/** OS version string (e.g. `17.4.1`). */
|
||||
@Json(name = "osVersion") val osVersion: String?,
|
||||
@Json(name = "osVersion") val osVersion: String,
|
||||
/** Application version (e.g. `5.8.0`). */
|
||||
@Json(name = "appVersion") val appVersion: String?,
|
||||
@Json(name = "appVersion") val appVersion: String,
|
||||
/** User-Agent header (e.g. `Tangem/5.8.0 (iPhone; iOS 17.4.1; Scale/3.00)`). */
|
||||
@Json(name = "userAgent") val userAgent: String?,
|
||||
@Json(name = "userAgent") val userAgent: String,
|
||||
/** Client locale (e.g. `en-US`). */
|
||||
@Json(name = "locale") val locale: String?,
|
||||
@Json(name = "locale") val locale: String,
|
||||
/** Client timezone (e.g. `Europe/Moscow`). */
|
||||
@Json(name = "timezone") val timezone: String?,
|
||||
@Json(name = "timezone") val timezone: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package com.tangem.datasource.api.auth.models.request
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Wallet registration request — binds a new wallet to an already-registered device.
|
||||
*
|
||||
* When [cardSignature] (and the accompanying [cardSignatureSalt] / [walletStatus]) is provided the
|
||||
* wallet is bound as COLD (card-backed); otherwise it is registered as a MOBILE (hot) wallet.
|
||||
* Mirrors the `WalletRegistrationRequest` schema in the backend OpenAPI contract.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class WalletRegistrationRequest(
|
||||
/** Deciphered nonce value from `/api/v1/auth/nonce/wallet`. */
|
||||
@Json(name = "nonce") val nonce: String,
|
||||
/**
|
||||
* Wallet identifier — Base64-encoded
|
||||
* `HMAC-SHA256(key = SHA-256(walletPublicKey), data = "UserWalletID")`.
|
||||
*/
|
||||
@Json(name = "walletId") val walletId: String,
|
||||
/**
|
||||
* Base64-encoded secp256k1 RSV signature (65 bytes) over `sha256(nonce || walletSignatureSalt)`.
|
||||
* The server recovers `walletPublicKey` from this signature.
|
||||
*/
|
||||
@Json(name = "walletSignature") val walletSignature: String,
|
||||
/** Base64-encoded salt used in the wallet signature hash. */
|
||||
@Json(name = "walletSignatureSalt") val walletSignatureSalt: String,
|
||||
/**
|
||||
* Base64-encoded secp256k1 RSV signature (65 bytes) over
|
||||
* `sha256(walletPublicKey || nonce || cardSignatureSalt || walletStatus)`. Required for
|
||||
* cold-wallet registration; `null` for mobile (hot) wallets.
|
||||
*/
|
||||
@Json(name = "cardSignature") val cardSignature: String?,
|
||||
/** Base64-encoded salt used in the card signature hash. Required for cold-wallet registration. */
|
||||
@Json(name = "cardSignatureSalt") val cardSignatureSalt: String?,
|
||||
/**
|
||||
* Base64-encoded single byte describing wallet provenance on the card
|
||||
* (`0x82` = generated on card, `0xC2` = SEED imported). Required for cold-wallet registration.
|
||||
*/
|
||||
@Json(name = "walletStatus") val walletStatus: String?,
|
||||
/** Platform attestation token (Play Integrity / App Attest). */
|
||||
@Json(name = "attestationToken") val attestationToken: String?,
|
||||
/** Client-reported device metadata. */
|
||||
@Json(name = "metadata") val metadata: DeviceMetadata,
|
||||
)
|
||||
|
|
@ -55,6 +55,7 @@ internal sealed class TangemPay(
|
|||
"version" to ProviderSuspend { appInfoProvider.appVersion },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { getBffStaticToken(apiEnvironment) },
|
||||
"X-Device-Scale" to ProviderSuspend { appInfoProvider.deviceScale.toString() },
|
||||
)
|
||||
|
||||
private fun getBffStaticToken(apiEnvironment: ApiEnvironment): String {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
package com.tangem.datasource.api.gasless
|
||||
|
||||
import com.tangem.datasource.api.common.response.ApiResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessBatchTransactionRequest
|
||||
import com.tangem.datasource.api.gasless.models.GaslessServiceResponse
|
||||
import com.tangem.datasource.api.gasless.models.GaslessSignedTransactionResultDTO
|
||||
import com.tangem.datasource.api.gasless.models.GaslessTransactionRequest
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface GaslessTxServiceApiV2 {
|
||||
@POST("api/v2/transaction/sign")
|
||||
suspend fun signGaslessTransaction(
|
||||
@Body transaction: GaslessTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
|
||||
@POST("api/v2/transaction/batch-sign")
|
||||
suspend fun signGaslessBatchTransaction(
|
||||
@Body transaction: GaslessBatchTransactionRequest,
|
||||
): ApiResponse<GaslessServiceResponse<GaslessSignedTransactionResultDTO>>
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.datasource.api.gasless.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Request body for gasless batch transaction submission (v2 `POST /api/v2/transaction/batch-sign`).
|
||||
* Represents a batch of transactions with fee delegation metadata.
|
||||
*
|
||||
* The top-level payload field is `gaslessTransaction` (shared shape with single sign — see
|
||||
* gasless-service `BatchSignRequestDto`), carrying `transactions[]`, `fee`, `nonce`.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionRequest(
|
||||
@Json(name = "gaslessTransaction")
|
||||
val gaslessTransaction: GaslessBatchTransactionDataDTO,
|
||||
|
||||
@Json(name = "signature")
|
||||
val signature: String,
|
||||
|
||||
@Json(name = "userAddress")
|
||||
val userAddress: String,
|
||||
|
||||
@Json(name = "chainId")
|
||||
val chainId: Int,
|
||||
|
||||
@Json(name = "eip7702auth")
|
||||
val eip7702Auth: Eip7702AuthorizationDTO? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class GaslessBatchTransactionDataDTO(
|
||||
@Json(name = "transactions")
|
||||
val transactions: List<TransactionData>,
|
||||
|
||||
@Json(name = "fee")
|
||||
val fee: FeeData,
|
||||
|
||||
@Json(name = "nonce")
|
||||
val nonce: String,
|
||||
)
|
||||
|
|
@ -45,6 +45,9 @@ data class TransactionData(
|
|||
@Json(name = "value")
|
||||
val value: String,
|
||||
|
||||
@Json(name = "gasLimit")
|
||||
val gasLimit: String? = null,
|
||||
|
||||
@Json(name = "data")
|
||||
val data: String,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,12 +17,30 @@ interface TangemPayApi {
|
|||
@Query("limit") limit: Int = TX_HISTORY_PAGING_DEFAULT_LIMIT,
|
||||
): ApiResponse<TangemPayTxHistoryResponse>
|
||||
|
||||
@GET("v1/customer/transactions/{transaction_id}")
|
||||
suspend fun getCustomerTransaction(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("transaction_id") transactionId: String,
|
||||
): ApiResponse<TangemPayTransactionResponse>
|
||||
|
||||
@GET("v1/customer/kyc")
|
||||
suspend fun getKycAccess(@Header("Authorization") authHeader: String): ApiResponse<KycAccessInfoResponse>
|
||||
|
||||
@GET("v1/customer/me")
|
||||
suspend fun getCustomerMe(@Header("Authorization") authHeader: String): ApiResponse<CustomerMeResponse>
|
||||
|
||||
@GET("v1/customer/tariff-plan/transitions")
|
||||
suspend fun getTariffPlanTransitions(
|
||||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TariffPlanTransitionsResponse>
|
||||
|
||||
/** Fiat bank requisites for the Virtual Account on-ramp (VA MVP0, TWI-1638). */
|
||||
@GET("v1/account/bank-credentials/{product_instance_id}")
|
||||
suspend fun getBankCredentials(
|
||||
@Header("Authorization") authHeader: String,
|
||||
@Path("product_instance_id") productInstanceId: String,
|
||||
): ApiResponse<BankCredentialsResponse>
|
||||
|
||||
@GET("v1/customer/wallets/{customer_wallet_id}")
|
||||
suspend fun checkCustomerWalletId(
|
||||
@Path("customer_wallet_id") customerWalletId: String,
|
||||
|
|
@ -40,6 +58,12 @@ interface TangemPayApi {
|
|||
@GET("v1/eligibility/channels")
|
||||
suspend fun getEligibilityChannels(): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
/** Eligibility channels fetched with the user (customer-wallet) token (VA MVP0, TWI-1638). */
|
||||
@GET("v1/eligibility/channels")
|
||||
suspend fun getUserEligibilityChannels(
|
||||
@Header("Authorization") authHeader: String,
|
||||
): ApiResponse<TangemPayEligibilityChannels>
|
||||
|
||||
@GET("v1/order/{order_id}")
|
||||
suspend fun getOrder(
|
||||
@Header("Authorization") authHeader: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `bff-v2/v1/account/bank-credentials/{product_instance_id}` — fiat bank requisites for the
|
||||
* Virtual Account on-ramp (VA MVP0, TWI-1638).
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class BankCredentialsResponse(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "beneficiary_name") val beneficiaryName: String?,
|
||||
@Json(name = "beneficiary_address") val beneficiaryAddress: String?,
|
||||
@Json(name = "beneficiary_bank_name") val beneficiaryBankName: String?,
|
||||
@Json(name = "beneficiary_bank_address") val beneficiaryBankAddress: String?,
|
||||
@Json(name = "account_number") val accountNumber: String?,
|
||||
@Json(name = "routing_number") val routingNumber: String?,
|
||||
)
|
||||
|
|
@ -21,6 +21,39 @@ data class CustomerMeResponse(
|
|||
@Json(name = "balance") val balance: BalanceResponse?,
|
||||
@Json(name = "product_instances") val productInstances: List<ProductInstance>,
|
||||
@Json(name = "cards") val cards: List<Card>,
|
||||
@Json(name = "customer_tariff_plan") val customerTariffPlan: CustomerTariffPlan? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CustomerTariffPlan(
|
||||
@Json(name = "status") val status: String?,
|
||||
@Json(name = "next_billing_at") val nextBillingAt: String?,
|
||||
@Json(name = "pending_transition_at") val pendingTransitionAt: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: TariffPlan?,
|
||||
@Json(name = "pending_tariff_plan") val pendingTariffPlan: TariffPlan?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlan(
|
||||
@Json(name = "id") val id: String?,
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "name") val name: String?,
|
||||
@Json(name = "description_items") val descriptionItems: List<DescriptionItem>?,
|
||||
@Json(name = "images") val images: List<Image>? = null,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Image(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "url") val url: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class DescriptionItem(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "order") val order: Int?,
|
||||
@Json(name = "title") val title: String?,
|
||||
@Json(name = "body") val body: String?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
@ -35,7 +68,17 @@ data class CustomerMeResponse(
|
|||
@Json(name = "display_name") val displayName: String?,
|
||||
@Json(name = "actual_card_limit") val actualCardLimit: CardLimit?,
|
||||
@Json(name = "admin_card_limit") val adminCardLimit: CardLimit?,
|
||||
@Json(name = "product_specification_data_type") val specificationDataType: SpecificationDataType,
|
||||
) {
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class SpecificationDataType {
|
||||
@Json(name = "ACCOUNT")
|
||||
ACCOUNT,
|
||||
|
||||
@Json(name = "CARD")
|
||||
CARD,
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = false)
|
||||
enum class Status {
|
||||
@Json(name = "NEW")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/** Response of `GET v1/customer/transactions/{transaction_id}` — a single transaction by its id. */
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TangemPayTransactionResponse(
|
||||
@Json(name = "result") val result: TangemPayTxHistoryResponse.Transaction,
|
||||
)
|
||||
|
|
@ -44,6 +44,8 @@ data class TangemPayTxHistoryResponse(
|
|||
@Json(name = "enriched_merchant_category") val enrichedMerchantCategory: String? = null,
|
||||
@Json(name = "card_id") val cardId: String? = null,
|
||||
@Json(name = "card_type") val cardType: String? = null,
|
||||
@Json(name = "card_display_name") val cardDisplayName: String? = null,
|
||||
@Json(name = "card_number_end") val cardNumberEnd: String? = null,
|
||||
@Json(name = "status") val status: String,
|
||||
@Json(name = "declined_reason") val declinedReason: String? = null,
|
||||
@Json(name = "authorized_at") val authorizedAt: DateTime,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.datasource.api.pay.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlanTransitionsResponse(
|
||||
@Json(name = "result") val result: List<TariffPlanTransitionResponse>?,
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TariffPlanTransitionResponse(
|
||||
@Json(name = "type") val type: String?,
|
||||
@Json(name = "tariff_plan") val tariffPlan: CustomerMeResponse.TariffPlan?,
|
||||
)
|
||||
|
|
@ -46,22 +46,25 @@ interface TangemTechApi {
|
|||
@GET("v1/geo")
|
||||
suspend fun getUserCountryCode(): GeoResponse
|
||||
|
||||
@GET("v1/application/versions")
|
||||
suspend fun getApplicationVersions(): ApiResponse<ApplicationVersionsResponse>
|
||||
|
||||
@PUT("/v1/wallets/{walletId}/tokens")
|
||||
suspend fun saveTokens(
|
||||
@Path(value = "walletId") userId: String,
|
||||
@Body userTokens: UserTokensResponse,
|
||||
): ApiResponse<Unit>
|
||||
|
||||
@GET("/v1/wallets/{wallet_id}/notification-preferences")
|
||||
@GET("/api/v1/notification-preferences/{wallet_id}")
|
||||
suspend fun getPushNotificationPreferences(
|
||||
@Path("wallet_id") walletId: String,
|
||||
): ApiResponse<PushNotificationPreferencesResponse>
|
||||
|
||||
@PUT("/v1/wallets/{wallet_id}/notification-preferences")
|
||||
@PUT("/api/v1/notification-preferences/{wallet_id}")
|
||||
suspend fun updatePushNotificationPreferences(
|
||||
@Path("wallet_id") walletId: String,
|
||||
@Body body: PushNotificationPreferencesBody,
|
||||
): ApiResponse<Unit>
|
||||
): ApiResponse<PushNotificationPreferencesResponse>
|
||||
|
||||
// region Referral
|
||||
/** Returns referral status by [walletId] */
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* Response of `GET v1/application/versions`. All fields are nullable — the backend may omit any, in
|
||||
* which case the corresponding check is skipped.
|
||||
*
|
||||
* @property minSupportedVersion app version threshold for a mandatory update: if
|
||||
* `installedVersion <= minSupportedVersion` the app must force-update (or show "update your OS"
|
||||
* when [minSupportedOSVersion] is not met). Inclusive. E.g. "5.30".
|
||||
* @property minSupportedOSVersion minimal device OS version required to install the update for the
|
||||
* [minSupportedVersion] case: if `deviceOsVersion < minSupportedOSVersion` the device can't update
|
||||
* and the "OS too old" screen is shown. Exclusive.
|
||||
* @property criticalVersion app version threshold for a critical mandatory update: if
|
||||
* `installedVersion <= criticalVersion` the app must force-update (or permanently "brick" when
|
||||
* [criticalOSVersion] is not met). Inclusive.
|
||||
* @property criticalOSVersion minimal device OS version required to install the critical update:
|
||||
* if `deviceOsVersion < criticalOSVersion` the app is bricked (update impossible). Exclusive.
|
||||
* @property latestVersion latest available app version: if `installedVersion < latestVersion`
|
||||
* an optional update is offered. Exclusive. E.g. "5.40".
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ApplicationVersionsResponse(
|
||||
@Json(name = "minSupportedVersion") val minSupportedVersion: String?,
|
||||
@Json(name = "minSupportedOSVersion") val minSupportedOSVersion: String?,
|
||||
@Json(name = "criticalVersion") val criticalVersion: String?,
|
||||
@Json(name = "criticalOSVersion") val criticalOSVersion: String?,
|
||||
@Json(name = "latestVersion") val latestVersion: String?,
|
||||
)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
package com.tangem.datasource.api.tangemTech.models
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferenceState(
|
||||
@Json(name = "isEnabled") val isEnabled: Boolean,
|
||||
@Json(name = "isVisible") val isVisible: Boolean,
|
||||
)
|
||||
|
|
@ -5,10 +5,10 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferencesBody(
|
||||
@Json(name = "transactionAlerts")
|
||||
val areTransactionAlertsEnabled: Boolean,
|
||||
@Json(name = "offersUpdates")
|
||||
val areOffersUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlerts")
|
||||
@Json(name = "transactionEventsEnabled")
|
||||
val areTransactionEventsEnabled: Boolean,
|
||||
@Json(name = "offerUpdatesEnabled")
|
||||
val areOfferUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlertsEnabled")
|
||||
val arePriceAlertsEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
|
|||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class PushNotificationPreferencesResponse(
|
||||
@Json(name = "transactionAlerts") val transactionAlerts: PushNotificationPreferenceState,
|
||||
@Json(name = "offersUpdates") val offersUpdates: PushNotificationPreferenceState,
|
||||
@Json(name = "priceAlerts") val priceAlerts: PushNotificationPreferenceState,
|
||||
@Json(name = "transactionEventsEnabled") val areTransactionEventsEnabled: Boolean,
|
||||
@Json(name = "offerUpdatesEnabled") val areOfferUpdatesEnabled: Boolean,
|
||||
@Json(name = "priceAlertsEnabled") val arePriceAlertsEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.addressbook.AddressBookApi
|
||||
import com.tangem.datasource.api.auth.AuthApi
|
||||
import com.tangem.datasource.api.common.blockaid.BlockAidApi
|
||||
import com.tangem.datasource.api.surveysparrow.SurveySparrowApi
|
||||
|
|
@ -18,6 +19,7 @@ import com.tangem.datasource.api.news.NewsApi
|
|||
import com.tangem.datasource.api.onramp.OnrampApi
|
||||
import com.tangem.datasource.api.ethpool.P2PEthPoolApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApi
|
||||
import com.tangem.datasource.api.gasless.GaslessTxServiceApiV2
|
||||
import com.tangem.datasource.api.pay.TangemPayApi
|
||||
import com.tangem.datasource.api.pay.TangemPayAuthApi
|
||||
import com.tangem.datasource.api.stakekit.StakeKitApi
|
||||
|
|
@ -117,6 +119,16 @@ internal object NetworkModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddressBookApi(retrofitApiBuilder: RetrofitApiBuilder): AddressBookApi {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.TangemTech,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideYieldSupplyApi(retrofitApiBuilder: RetrofitApiBuilder): YieldSupplyApi {
|
||||
|
|
@ -252,4 +264,20 @@ internal object NetworkModule {
|
|||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGaslessTxServiceApiV2(retrofitApiBuilder: RetrofitApiBuilder): GaslessTxServiceApiV2 {
|
||||
return retrofitApiBuilder.build(
|
||||
apiConfigId = ApiConfig.ID.GaslessTxService,
|
||||
applyTimeoutAnnotations = false,
|
||||
sessionAuth = false,
|
||||
timeouts = Timeouts(
|
||||
callTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
connectTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
readTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
writeTimeoutSeconds = TIMEOUT_60_SECONDS,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,27 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStoreFactory
|
||||
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
|
||||
import androidx.datastore.dataStoreFile
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.txhistory.DefaultTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.txhistory.TxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.DefaultTangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.TangemPayTxHistoryItemsStore
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -25,9 +38,34 @@ internal object TxHistoryItemsStoreModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTangemPayTxHistoryItemsStore(): TangemPayTxHistoryItemsStore {
|
||||
fun provideTangemPayTxHistoryItemsStore(
|
||||
@ApplicationContext context: Context,
|
||||
appScope: AppCoroutineScope,
|
||||
toDMConverter: TangemPayTxHistoryItemToDMConverter,
|
||||
toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
|
||||
): TangemPayTxHistoryItemsStore {
|
||||
return DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
dataStore = DataStoreFactory.create(
|
||||
serializer = KotlinxDataStoreSerializer(
|
||||
defaultValue = emptyMap(),
|
||||
serializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
|
||||
),
|
||||
),
|
||||
// TangemPayTxHistoryItemDM.Collateral declares a `type` field that would clash with the
|
||||
// default kotlinx polymorphic class discriminator ("type"), so persist sealed subtypes
|
||||
// under a non-conflicting discriminator key.
|
||||
json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" },
|
||||
),
|
||||
corruptionHandler = ReplaceFileCorruptionHandler { emptyMap() },
|
||||
produceFile = { context.dataStoreFile(fileName = "tangem_pay_tx_history") },
|
||||
scope = appScope,
|
||||
),
|
||||
toDMConverter = toDMConverter,
|
||||
toDomainConverter = toDomainConverter,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ import androidx.room.Room
|
|||
import com.tangem.datasource.local.txhistory.db.TxHistoryDatabase
|
||||
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.HistoryIndexDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.TokenInfoDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -37,5 +39,11 @@ internal interface TxHistoryModule {
|
|||
|
||||
@Provides
|
||||
fun provideSyncStateDao(database: TxHistoryDatabase): ExpressSyncStateDao = database.syncStateDao()
|
||||
|
||||
@Provides
|
||||
fun provideTokenInfoDao(database: TxHistoryDatabase): TokenInfoDao = database.tokenInfoDao()
|
||||
|
||||
@Provides
|
||||
fun provideHistoryIndexDao(database: TxHistoryDatabase): HistoryIndexDao = database.historyIndexDao()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,13 @@ import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEnti
|
|||
|
||||
/**
|
||||
* Maps API history items into their persisted [androidx.room.Entity] representations.
|
||||
*
|
||||
* @param ownerAddress address the history was requested for. Stored as the query key.
|
||||
*/
|
||||
fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
|
||||
fun ExchangeItemResponse.toEntity(): ExpressExchangeEntity? {
|
||||
// Items with no fromAddress (very old app versions didn't send it) can't be found by the outgoing-swap
|
||||
// lookup, which keys on from_address — drop them. Such items are effectively nonexistent nowadays.
|
||||
if (fromAddress == null) return null
|
||||
return ExpressExchangeEntity(
|
||||
txId = txId,
|
||||
ownerAddress = ownerAddress,
|
||||
providerId = providerId,
|
||||
fromAddress = fromAddress,
|
||||
payinAddress = payinAddress,
|
||||
|
|
@ -50,10 +50,9 @@ fun ExchangeItemResponse.toEntity(ownerAddress: String): ExpressExchangeEntity {
|
|||
)
|
||||
}
|
||||
|
||||
fun OnrampItemResponse.toEntity(ownerAddress: String): ExpressOnrampEntity {
|
||||
fun OnrampItemResponse.toEntity(): ExpressOnrampEntity {
|
||||
return ExpressOnrampEntity(
|
||||
txId = txId,
|
||||
ownerAddress = ownerAddress,
|
||||
providerId = providerId,
|
||||
payoutAddress = payoutAddress,
|
||||
status = status,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.local.converter
|
||||
|
||||
import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
|
||||
/** Maps an [OnrampCountryDTO] API response into its persisted [OnrampCountryEntity]. */
|
||||
fun OnrampCountryDTO.toEntity(): OnrampCountryEntity {
|
||||
return OnrampCountryEntity(
|
||||
code = code,
|
||||
name = name,
|
||||
image = image,
|
||||
alpha3 = alpha3,
|
||||
continent = continent,
|
||||
isOnrampAvailable = onrampAvailable,
|
||||
defaultCurrency = OnrampCountryEntity.CurrencyEmbedded(
|
||||
name = defaultCurrency.name,
|
||||
code = defaultCurrency.code,
|
||||
image = defaultCurrency.image,
|
||||
precision = defaultCurrency.precision,
|
||||
unit = defaultCurrency.unit ?: defaultCurrency.code,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -33,6 +33,16 @@ object PreferencesKeys {
|
|||
|
||||
val APP_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "launchCount") }
|
||||
|
||||
val LAST_OPTIONAL_UPDATE_SHOWN_VERSION_KEY by lazy {
|
||||
stringPreferencesKey(name = "lastOptionalUpdateShownVersion")
|
||||
}
|
||||
|
||||
val LAST_OPTIONAL_UPDATE_SHOWN_AT_KEY by lazy { longPreferencesKey(name = "lastOptionalUpdateShownAt") }
|
||||
|
||||
val CACHED_APP_VERSIONS_KEY by lazy { stringPreferencesKey(name = "cachedApplicationVersions") }
|
||||
|
||||
val CACHED_APP_VERSIONS_AT_KEY by lazy { longPreferencesKey(name = "cachedApplicationVersionsAt") }
|
||||
|
||||
val SHOW_RATING_DIALOG_AT_LAUNCH_COUNT_KEY by lazy { intPreferencesKey(name = "showRatingDialogAtLaunchCount") }
|
||||
|
||||
val FUNDS_FOUND_DATE_KEY by lazy { longPreferencesKey(name = "fundsFoundDate") }
|
||||
|
|
@ -41,6 +51,8 @@ object PreferencesKeys {
|
|||
|
||||
val USED_CARDS_INFO_KEY by lazy { stringPreferencesKey(name = "usedCardsInfo_v2") }
|
||||
|
||||
val USEDESK_CLIENT_ID_KEY by lazy { stringPreferencesKey(name = "usedeskClientId") }
|
||||
|
||||
val APP_THEME_MODE_KEY by lazy { stringPreferencesKey(name = "appThemeMode") }
|
||||
|
||||
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
|
||||
|
|
@ -154,7 +166,7 @@ object PreferencesKeys {
|
|||
val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy {
|
||||
stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey")
|
||||
}
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityList") }
|
||||
val TANGEM_PAY_ELIGIBILITY_KEY by lazy { stringSetPreferencesKey(name = "tangemPayEligibilityListV2") }
|
||||
|
||||
fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key")
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -4,10 +4,15 @@ import androidx.room.Database
|
|||
import androidx.room.RoomDatabase
|
||||
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.HistoryIndexDao
|
||||
import com.tangem.datasource.local.txhistory.db.dao.TokenInfoDao
|
||||
import com.tangem.datasource.local.txhistory.db.entity.HistoryIndexEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressSyncStateEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.TokenInfoEntity
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
|
|
@ -16,6 +21,9 @@ import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEn
|
|||
ExpressExchangeEntity::class,
|
||||
ExpressOnrampEntity::class,
|
||||
ExpressSyncStateEntity::class,
|
||||
OnrampCountryEntity::class,
|
||||
TokenInfoEntity::class,
|
||||
HistoryIndexEntity::class,
|
||||
],
|
||||
)
|
||||
abstract class TxHistoryDatabase : RoomDatabase() {
|
||||
|
|
@ -23,4 +31,8 @@ abstract class TxHistoryDatabase : RoomDatabase() {
|
|||
abstract fun expressHistoryDao(): ExpressHistoryDao
|
||||
|
||||
abstract fun syncStateDao(): ExpressSyncStateDao
|
||||
|
||||
abstract fun tokenInfoDao(): TokenInfoDao
|
||||
|
||||
abstract fun historyIndexDao(): HistoryIndexDao
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import androidx.room.Query
|
|||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressExchangeEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressOnrampEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.ExpressProviderEntity
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.OnrampCountryEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
|
@ -22,15 +23,22 @@ interface ExpressHistoryDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertOnramps(items: List<ExpressOnrampEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsertCountries(items: List<OnrampCountryEntity>)
|
||||
|
||||
/**
|
||||
* All persisted providers keyed by [ExpressProviderEntity.id]
|
||||
*/
|
||||
@Query("SELECT * FROM express_provider")
|
||||
fun getProvidersById(): Flow<Map<@MapColumn(columnName = "id") String, ExpressProviderEntity>>
|
||||
|
||||
/** All persisted onramp countries keyed by [OnrampCountryEntity.code]. */
|
||||
@Query("SELECT * FROM onramp_country")
|
||||
fun getCountriesByCode(): Flow<Map<@MapColumn(columnName = "code") String, OnrampCountryEntity>>
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
* Outgoing swaps: the viewed currency is the swap's `from` side, so the row is looked up by its `from_address`.
|
||||
* Join to on-chain by `payin_hash`.
|
||||
*
|
||||
|
||||
* loading the whole table; [activeStatuses] keeps in-progress deals visible even outside the window.
|
||||
|
|
@ -38,7 +46,7 @@ interface ExpressHistoryDao {
|
|||
@Query(
|
||||
"""
|
||||
SELECT * FROM express_exchange
|
||||
WHERE owner_address = :ownerAddress
|
||||
WHERE from_address = :fromAddress
|
||||
AND from_network = :network
|
||||
AND from_contract_address = :contract
|
||||
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
|
||||
|
|
@ -46,7 +54,7 @@ interface ExpressHistoryDao {
|
|||
""",
|
||||
)
|
||||
fun observeOutgoingSwaps(
|
||||
ownerAddress: String,
|
||||
fromAddress: String,
|
||||
network: String,
|
||||
contract: String,
|
||||
fromCreatedAtIso: String,
|
||||
|
|
@ -55,8 +63,8 @@ interface ExpressHistoryDao {
|
|||
|
||||
/**
|
||||
* 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`.
|
||||
* different coin, so the row is stored under that coin's `from_address` — hence this query is
|
||||
* cross-address, matched by the `to` asset. Join to on-chain by `payout_hash`.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
|
|
@ -75,12 +83,12 @@ interface ExpressHistoryDao {
|
|||
): Flow<List<ExpressExchangeEntity>>
|
||||
|
||||
/**
|
||||
* Onramp is always incoming: [ExpressOnrampEntity.ownerAddress] == payoutAddress. Join by `payout_hash`.
|
||||
* Onramp is always incoming, looked up by its `payout_address`. Join by `payout_hash`.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM express_onramp
|
||||
WHERE owner_address = :ownerAddress
|
||||
WHERE payout_address = :payoutAddress
|
||||
AND to_network = :network
|
||||
AND to_contract_address = :contract
|
||||
AND (created_at >= :fromCreatedAtIso OR status IN (:activeStatuses))
|
||||
|
|
@ -88,7 +96,7 @@ interface ExpressHistoryDao {
|
|||
""",
|
||||
)
|
||||
fun observeIncomingOnramps(
|
||||
ownerAddress: String,
|
||||
payoutAddress: String,
|
||||
network: String,
|
||||
contract: String,
|
||||
fromCreatedAtIso: String,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package com.tangem.datasource.local.txhistory.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.tangem.datasource.local.txhistory.db.entity.HistoryIndexEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface HistoryIndexDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(items: List<HistoryIndexEntity>)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(item: HistoryIndexEntity)
|
||||
|
||||
/**
|
||||
* One page of the unified timeline for [addresses] (usually one, but some token-details screens span several),
|
||||
* newest first, **one row per operation**. An operation indexed under several of the queried [addresses] (e.g. a
|
||||
* swap under both its from- and payout-address) is collapsed via `GROUP BY (type, entity_id)`, keeping its
|
||||
* most-recent occurrence (SQLite bare-column rule under a single `MAX`). This both de-duplicates the timeline and
|
||||
* makes the keyset key (`sort_time_millis`, `entity_id`) unique, so rows are neither skipped nor duplicated across
|
||||
* page boundaries even when several addresses are queried.
|
||||
*
|
||||
* The cursor is the (`sort_time_millis`, `entity_id`) of the last (oldest) row of the previous page — pass both
|
||||
* [cursorSortTimeMillis] and [cursorEntityId], or `null` for the first page.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT type, entity_id, address, MAX(sort_time_millis) AS sort_time_millis FROM history_index
|
||||
WHERE address IN (:addresses)
|
||||
GROUP BY type, entity_id
|
||||
HAVING (
|
||||
:cursorSortTimeMillis IS NULL
|
||||
OR MAX(sort_time_millis) < :cursorSortTimeMillis
|
||||
OR (MAX(sort_time_millis) = :cursorSortTimeMillis AND entity_id < :cursorEntityId)
|
||||
)
|
||||
ORDER BY sort_time_millis DESC, entity_id DESC
|
||||
LIMIT :limit
|
||||
""",
|
||||
)
|
||||
fun observePage(
|
||||
addresses: List<String>,
|
||||
cursorSortTimeMillis: Long?,
|
||||
cursorEntityId: String?,
|
||||
limit: Int,
|
||||
): Flow<List<HistoryIndexEntity>>
|
||||
|
||||
fun observePage(addresses: List<String>, cursor: Cursor?, limit: Int): Flow<List<HistoryIndexEntity>> = observePage(
|
||||
addresses = addresses,
|
||||
cursorSortTimeMillis = cursor?.sortTimeMillis,
|
||||
cursorEntityId = cursor?.entityId,
|
||||
limit = limit,
|
||||
)
|
||||
|
||||
/**
|
||||
* Keyset cursor for [observePage]: the (sortTimeMillis, entityId) of the last (oldest) row of a page. Build it from
|
||||
* the previous page's last row to fetch the next page; a `null` cursor requests the first page.
|
||||
*/
|
||||
data class Cursor(
|
||||
val sortTimeMillis: Long,
|
||||
val entityId: String,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun from(lastRow: HistoryIndexEntity): Cursor = Cursor(
|
||||
sortTimeMillis = lastRow.sortTimeMillis,
|
||||
entityId = lastRow.entityId,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.tangem.datasource.local.txhistory.db.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import com.tangem.datasource.local.txhistory.db.entity.express.TokenInfoEntity
|
||||
|
||||
@Dao
|
||||
interface TokenInfoDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun upsert(items: List<TokenInfoEntity>)
|
||||
|
||||
/**
|
||||
* Cached tokens for the given networks/contracts. Filters each column independently, so the result is a
|
||||
* cross-product superset of the `(networkId, contractAddress)` pairs — the caller must match exact pairs.
|
||||
* Contract match is case-insensitive; pass [minUpdatedAt] = `now - ttl` to drop stale rows.
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM token_info
|
||||
WHERE network_id IN (:networkIds)
|
||||
AND contract_address COLLATE NOCASE IN (:contractAddresses)
|
||||
AND updated_at >= :minUpdatedAt
|
||||
""",
|
||||
)
|
||||
suspend fun getCached(
|
||||
networkIds: Collection<String>,
|
||||
contractAddresses: Collection<String>,
|
||||
minUpdatedAt: Long = 0,
|
||||
): List<TokenInfoEntity>
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
|
||||
/**
|
||||
* Unified pagination index over the local history sources.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "history_index",
|
||||
// A single row (type + entity_id) may be shown under more than one address (e.g. a swap between two owned tokens
|
||||
// appears under both), so the address is part of the identity.
|
||||
primaryKeys = ["type", "entity_id", "address"],
|
||||
indices = [
|
||||
// Newest-first cursor scan within an address: address filter + sort-time ordering + entity_id tie-break.
|
||||
Index(value = ["address", "sort_time_millis", "entity_id"]),
|
||||
],
|
||||
)
|
||||
data class HistoryIndexEntity(
|
||||
|
||||
@ColumnInfo(name = "type")
|
||||
val type: String,
|
||||
|
||||
/** ID of the row in its own table. */
|
||||
@ColumnInfo(name = "entity_id")
|
||||
val entityId: String,
|
||||
|
||||
/** Address the row is loaded under on the token-details screen. */
|
||||
@ColumnInfo(name = "address")
|
||||
val address: String,
|
||||
|
||||
/** Time the unified timeline is sorted by (newest first). */
|
||||
@ColumnInfo(name = "sort_time_millis")
|
||||
val sortTimeMillis: Long,
|
||||
) {
|
||||
|
||||
enum class Type(val value: String) {
|
||||
EXCHANGE(value = "EXCHANGE"),
|
||||
ONRAMP(value = "ONRAMP"),
|
||||
}
|
||||
}
|
||||
|
|
@ -10,8 +10,8 @@ import androidx.room.*
|
|||
@Entity(
|
||||
tableName = "express_exchange",
|
||||
indices = [
|
||||
// Outgoing swaps lookup (observeOutgoingSwaps): owner + from-asset equality, created_at range/sort.
|
||||
Index(value = ["owner_address", "from_network", "from_contract_address", "created_at"]),
|
||||
// Outgoing swaps lookup (observeOutgoingSwaps): from-address + from-asset equality, created_at range/sort.
|
||||
Index(value = ["from_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"]),
|
||||
|
|
@ -23,21 +23,16 @@ data class ExpressExchangeEntity(
|
|||
@ColumnInfo(name = "tx_id")
|
||||
val txId: String,
|
||||
|
||||
/**
|
||||
* Address used to query the history. For exchange it matches [fromAddress].
|
||||
*/
|
||||
@ColumnInfo(name = "owner_address")
|
||||
val ownerAddress: String,
|
||||
|
||||
@ColumnInfo(name = "provider_id")
|
||||
val providerId: String,
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Address from which the `from` assets were taken — the key outgoing swaps are looked up by. The API may omit it
|
||||
* (the very first app versions did not send it), but such items are filtered out before persisting, so the stored
|
||||
* value is always present.
|
||||
*/
|
||||
@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")
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import androidx.room.*
|
|||
@Entity(
|
||||
tableName = "express_onramp",
|
||||
indices = [
|
||||
// Incoming onramp lookup (observeIncomingOnramps): owner + to-asset equality, created_at range/sort.
|
||||
Index(value = ["owner_address", "to_network", "to_contract_address", "created_at"]),
|
||||
// Incoming onramp lookup (observeIncomingOnramps): payout-address + to-asset equality, created_at range/sort.
|
||||
Index(value = ["payout_address", "to_network", "to_contract_address", "created_at"]),
|
||||
],
|
||||
)
|
||||
data class ExpressOnrampEntity(
|
||||
|
|
@ -20,16 +20,10 @@ data class ExpressOnrampEntity(
|
|||
@ColumnInfo(name = "tx_id")
|
||||
val txId: String,
|
||||
|
||||
/**
|
||||
* Address used to query the history. For onramp it matches [payoutAddress].
|
||||
*/
|
||||
@ColumnInfo(name = "owner_address")
|
||||
val ownerAddress: String,
|
||||
|
||||
@ColumnInfo(name = "provider_id")
|
||||
val providerId: String,
|
||||
|
||||
/** Address that received the target assets */
|
||||
/** Address that received the target assets — the key incoming onramps are looked up by. */
|
||||
@ColumnInfo(name = "payout_address")
|
||||
val payoutAddress: String,
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity.express
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
/** Persisted onramp country, matched to a transaction by [code] == [ExpressOnrampEntity.countryCode]. */
|
||||
@Entity(tableName = "onramp_country")
|
||||
data class OnrampCountryEntity(
|
||||
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "code")
|
||||
val code: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "image")
|
||||
val image: String,
|
||||
|
||||
@ColumnInfo(name = "alpha3")
|
||||
val alpha3: String,
|
||||
|
||||
@ColumnInfo(name = "continent")
|
||||
val continent: String,
|
||||
|
||||
@ColumnInfo(name = "onramp_available")
|
||||
val isOnrampAvailable: Boolean,
|
||||
|
||||
@Embedded(prefix = "currency_")
|
||||
val defaultCurrency: CurrencyEmbedded,
|
||||
) {
|
||||
|
||||
data class CurrencyEmbedded(
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "code")
|
||||
val code: String,
|
||||
|
||||
@ColumnInfo(name = "image")
|
||||
val image: String?,
|
||||
|
||||
@ColumnInfo(name = "precision")
|
||||
val precision: Int,
|
||||
|
||||
@ColumnInfo(name = "unit")
|
||||
val unit: String,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.local.txhistory.db.entity.express
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
|
||||
/**
|
||||
* Cached token data fetched from [TangemTechApi.getCoins], keyed by network id + contract address.
|
||||
*/
|
||||
@Entity(
|
||||
tableName = "token_info",
|
||||
primaryKeys = ["network_id", "contract_address"],
|
||||
)
|
||||
data class TokenInfoEntity(
|
||||
|
||||
@ColumnInfo(name = "network_id")
|
||||
val networkId: String,
|
||||
|
||||
@ColumnInfo(name = "contract_address")
|
||||
val contractAddress: String,
|
||||
|
||||
/** Coin id from the backend (used as the token's raw currency id and for the icon URL). */
|
||||
@ColumnInfo(name = "coin_id")
|
||||
val coinId: String,
|
||||
|
||||
@ColumnInfo(name = "name")
|
||||
val name: String,
|
||||
|
||||
@ColumnInfo(name = "symbol")
|
||||
val symbol: String,
|
||||
|
||||
@ColumnInfo(name = "decimals")
|
||||
val decimals: Int,
|
||||
|
||||
/** Last refresh timestamp in epoch milliseconds, used to evict stale entries. */
|
||||
@ColumnInfo(name = "updated_at")
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
|
@ -10,15 +10,15 @@ internal class DefaultTangemPayCloseCardStore(
|
|||
private val prefs: AppPreferencesStore,
|
||||
) : TangemPayCloseCardStore {
|
||||
|
||||
override suspend fun setCloseOrderId(cardId: String, orderId: String?) {
|
||||
if (orderId == null) {
|
||||
prefs.edit { it.remove(getCloseKey(cardId)) }
|
||||
} else {
|
||||
prefs.store(
|
||||
key = getCloseKey(cardId),
|
||||
value = orderId,
|
||||
)
|
||||
}
|
||||
override suspend fun storeCloseOrderId(cardId: String, orderId: String) {
|
||||
prefs.store(
|
||||
key = getCloseKey(cardId),
|
||||
value = orderId,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun removeCloseOrderId(cardId: String) {
|
||||
prefs.edit { it.remove(getCloseKey(cardId)) }
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(cardId: String): String? {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
|
|
@ -31,6 +32,10 @@ internal class DefaultTangemPayReissueCardStore(
|
|||
)
|
||||
}
|
||||
|
||||
override suspend fun removeReissueOrderId(cardId: String) {
|
||||
prefs.edit { it.remove(getReissueKey(cardId)) }
|
||||
}
|
||||
|
||||
override suspend fun getOrderId(cardId: String): String? {
|
||||
return prefs.getSyncOrNull(key = getReissueKey(cardId))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,23 +1,37 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStoreDecorator
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
internal typealias StoredTangemPayTxHistory = Map<String, Map<String, List<TangemPayTxHistoryItemDM>>>
|
||||
|
||||
internal class DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore: StringKeyDataStore<Map<String, List<TangemPayTxHistoryItem>>>,
|
||||
) : TangemPayTxHistoryItemsStore,
|
||||
StringKeyDataStoreDecorator<String, Map<String, List<TangemPayTxHistoryItem>>>(dataStore) {
|
||||
override fun provideStringKey(key: String): String = key
|
||||
private val dataStore: DataStore<StoredTangemPayTxHistory>,
|
||||
private val toDMConverter: TangemPayTxHistoryItemToDMConverter,
|
||||
private val toDomainConverter: TangemPayTxHistoryItemToDomainConverter,
|
||||
) : TangemPayTxHistoryItemsStore {
|
||||
|
||||
override suspend fun getSyncOrNull(key: String, cursor: String): List<TangemPayTxHistoryItem>? {
|
||||
val storedValue = getSyncOrNull(key)
|
||||
return storedValue?.get(cursor)
|
||||
return dataStore.data.first()[key]?.get(cursor)?.let { toDomainConverter.convertList(it) }
|
||||
}
|
||||
|
||||
override suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>) {
|
||||
val oldValue = getSyncOrNull(key).orEmpty()
|
||||
val newValue = oldValue.toMutableMap().apply { put(cursor, value) }
|
||||
store(key, newValue)
|
||||
val page = toDMConverter.convertList(value)
|
||||
dataStore.updateData { stored ->
|
||||
val walletPages = stored[key].orEmpty()
|
||||
stored + (key to walletPages + (cursor to page))
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remove(key: String) {
|
||||
remove(keys = listOf(key))
|
||||
}
|
||||
|
||||
override suspend fun remove(keys: List<String>) {
|
||||
dataStore.updateData { stored -> stored - keys.toSet() }
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package com.tangem.datasource.local.visa
|
|||
|
||||
interface TangemPayCloseCardStore {
|
||||
|
||||
suspend fun setCloseOrderId(cardId: String, orderId: String?)
|
||||
suspend fun storeCloseOrderId(cardId: String, orderId: String)
|
||||
|
||||
suspend fun removeCloseOrderId(cardId: String)
|
||||
|
||||
suspend fun getOrderId(cardId: String): String?
|
||||
}
|
||||
|
|
@ -11,5 +11,7 @@ interface TangemPayReissueCardStore {
|
|||
|
||||
suspend fun storeReissueOrderId(cardId: String, orderId: String)
|
||||
|
||||
suspend fun removeReissueOrderId(cardId: String)
|
||||
|
||||
suspend fun getOrderId(cardId: String): String?
|
||||
}
|
||||
|
|
@ -8,5 +8,7 @@ interface TangemPayTxHistoryItemsStore {
|
|||
|
||||
suspend fun remove(key: String)
|
||||
|
||||
suspend fun remove(keys: List<String>)
|
||||
|
||||
suspend fun store(key: String, cursor: String, value: List<TangemPayTxHistoryItem>)
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.tangem.domain.models.serialization.SerializedBigDecimal
|
||||
import com.tangem.domain.models.serialization.SerializedCurrency
|
||||
import com.tangem.domain.models.serialization.SerializedDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class TangemPayTxHistoryItemDM {
|
||||
|
||||
abstract val id: String
|
||||
abstract val date: SerializedDateTime
|
||||
abstract val amount: SerializedBigDecimal
|
||||
abstract val currency: SerializedCurrency
|
||||
abstract val jsonRepresentation: String
|
||||
|
||||
@Serializable
|
||||
@SerialName("spend")
|
||||
data class Spend(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("authorized_amount") val authorizedAmount: SerializedBigDecimal,
|
||||
@SerialName("local_amount") val localAmount: SerializedBigDecimal?,
|
||||
@SerialName("local_currency") val localCurrency: SerializedCurrency?,
|
||||
@SerialName("enriched_merchant_name") val enrichedMerchantName: String?,
|
||||
@SerialName("merchant_name") val merchantName: String,
|
||||
@SerialName("enriched_merchant_category") val enrichedMerchantCategory: String?,
|
||||
@SerialName("merchant_category_code") val merchantCategoryCode: String?,
|
||||
@SerialName("merchant_category") val merchantCategory: String?,
|
||||
@SerialName("status") val status: Status,
|
||||
@SerialName("enriched_merchant_icon_url") val enrichedMerchantIconUrl: String?,
|
||||
@SerialName("declined_reason") val declinedReason: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("payment")
|
||||
data class Payment(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("transaction_hash") val transactionHash: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("fee")
|
||||
data class Fee(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("description") val description: String?,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
@SerialName("collateral")
|
||||
data class Collateral(
|
||||
@SerialName("id") override val id: String,
|
||||
@SerialName("json_representation") override val jsonRepresentation: String,
|
||||
@SerialName("date") override val date: SerializedDateTime,
|
||||
@SerialName("amount") override val amount: SerializedBigDecimal,
|
||||
@SerialName("currency") override val currency: SerializedCurrency,
|
||||
@SerialName("transaction_hash") val transactionHash: String,
|
||||
@SerialName("type") val type: Type,
|
||||
) : TangemPayTxHistoryItemDM()
|
||||
|
||||
@Serializable
|
||||
enum class Type {
|
||||
@SerialName("deposit")
|
||||
Deposit,
|
||||
|
||||
@SerialName("withdrawal")
|
||||
Withdrawal,
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class Status {
|
||||
@SerialName("pending")
|
||||
PENDING,
|
||||
|
||||
@SerialName("reserved")
|
||||
RESERVED,
|
||||
|
||||
@SerialName("completed")
|
||||
COMPLETED,
|
||||
|
||||
@SerialName("declined")
|
||||
DECLINED,
|
||||
|
||||
@SerialName("reversed")
|
||||
REVERSED,
|
||||
|
||||
@SerialName("unknown")
|
||||
UNKNOWN,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.utils.converter.Converter
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class TangemPayTxHistoryItemToDMConverter @Inject constructor() :
|
||||
Converter<TangemPayTxHistoryItem, TangemPayTxHistoryItemDM> {
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryItem): TangemPayTxHistoryItemDM = when (value) {
|
||||
is TangemPayTxHistoryItem.Spend -> TangemPayTxHistoryItemDM.Spend(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
authorizedAmount = value.authorizedAmount,
|
||||
localAmount = value.localAmount,
|
||||
localCurrency = value.localCurrency,
|
||||
enrichedMerchantName = value.enrichedMerchantName,
|
||||
merchantName = value.merchantName,
|
||||
enrichedMerchantCategory = value.enrichedMerchantCategory,
|
||||
merchantCategoryCode = value.merchantCategoryCode,
|
||||
merchantCategory = value.merchantCategory,
|
||||
status = value.status.toDM(),
|
||||
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
|
||||
declinedReason = value.declinedReason,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Payment -> TangemPayTxHistoryItemDM.Payment(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Fee -> TangemPayTxHistoryItemDM.Fee(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
description = value.description,
|
||||
)
|
||||
is TangemPayTxHistoryItem.Collateral -> TangemPayTxHistoryItemDM.Collateral(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
type = value.type.toDM(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal class TangemPayTxHistoryItemToDomainConverter @Inject constructor() :
|
||||
Converter<TangemPayTxHistoryItemDM, TangemPayTxHistoryItem> {
|
||||
|
||||
override fun convert(value: TangemPayTxHistoryItemDM): TangemPayTxHistoryItem = when (value) {
|
||||
is TangemPayTxHistoryItemDM.Spend -> TangemPayTxHistoryItem.Spend(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
authorizedAmount = value.authorizedAmount,
|
||||
localAmount = value.localAmount,
|
||||
localCurrency = value.localCurrency,
|
||||
enrichedMerchantName = value.enrichedMerchantName,
|
||||
merchantName = value.merchantName,
|
||||
enrichedMerchantCategory = value.enrichedMerchantCategory,
|
||||
merchantCategoryCode = value.merchantCategoryCode,
|
||||
merchantCategory = value.merchantCategory,
|
||||
status = value.status.toDomain(),
|
||||
enrichedMerchantIconUrl = value.enrichedMerchantIconUrl,
|
||||
declinedReason = value.declinedReason,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Payment -> TangemPayTxHistoryItem.Payment(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Fee -> TangemPayTxHistoryItem.Fee(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
description = value.description,
|
||||
)
|
||||
is TangemPayTxHistoryItemDM.Collateral -> TangemPayTxHistoryItem.Collateral(
|
||||
id = value.id,
|
||||
jsonRepresentation = value.jsonRepresentation,
|
||||
date = value.date,
|
||||
amount = value.amount,
|
||||
currency = value.currency,
|
||||
transactionHash = value.transactionHash,
|
||||
type = value.type.toDomain(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.Status.toDM(): TangemPayTxHistoryItemDM.Status = when (this) {
|
||||
TangemPayTxHistoryItem.Status.PENDING -> TangemPayTxHistoryItemDM.Status.PENDING
|
||||
TangemPayTxHistoryItem.Status.RESERVED -> TangemPayTxHistoryItemDM.Status.RESERVED
|
||||
TangemPayTxHistoryItem.Status.COMPLETED -> TangemPayTxHistoryItemDM.Status.COMPLETED
|
||||
TangemPayTxHistoryItem.Status.DECLINED -> TangemPayTxHistoryItemDM.Status.DECLINED
|
||||
TangemPayTxHistoryItem.Status.REVERSED -> TangemPayTxHistoryItemDM.Status.REVERSED
|
||||
TangemPayTxHistoryItem.Status.UNKNOWN -> TangemPayTxHistoryItemDM.Status.UNKNOWN
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItemDM.Status.toDomain(): TangemPayTxHistoryItem.Status = when (this) {
|
||||
TangemPayTxHistoryItemDM.Status.PENDING -> TangemPayTxHistoryItem.Status.PENDING
|
||||
TangemPayTxHistoryItemDM.Status.RESERVED -> TangemPayTxHistoryItem.Status.RESERVED
|
||||
TangemPayTxHistoryItemDM.Status.COMPLETED -> TangemPayTxHistoryItem.Status.COMPLETED
|
||||
TangemPayTxHistoryItemDM.Status.DECLINED -> TangemPayTxHistoryItem.Status.DECLINED
|
||||
TangemPayTxHistoryItemDM.Status.REVERSED -> TangemPayTxHistoryItem.Status.REVERSED
|
||||
TangemPayTxHistoryItemDM.Status.UNKNOWN -> TangemPayTxHistoryItem.Status.UNKNOWN
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItem.Type.toDM(): TangemPayTxHistoryItemDM.Type = when (this) {
|
||||
TangemPayTxHistoryItem.Type.Deposit -> TangemPayTxHistoryItemDM.Type.Deposit
|
||||
TangemPayTxHistoryItem.Type.Withdrawal -> TangemPayTxHistoryItemDM.Type.Withdrawal
|
||||
}
|
||||
|
||||
private fun TangemPayTxHistoryItemDM.Type.toDomain(): TangemPayTxHistoryItem.Type = when (this) {
|
||||
TangemPayTxHistoryItemDM.Type.Deposit -> TangemPayTxHistoryItem.Type.Deposit
|
||||
TangemPayTxHistoryItemDM.Type.Withdrawal -> TangemPayTxHistoryItem.Type.Withdrawal
|
||||
}
|
||||
|
|
@ -70,6 +70,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
every { appInfoProvider.osVersion } returns "Android 16"
|
||||
every { appInfoProvider.language } returns Locale.getDefault().toLanguageTag()
|
||||
every { appInfoProvider.device } returns "${Build.MANUFACTURER} ${Build.MODEL}"
|
||||
every { appInfoProvider.deviceScale } returns DEVICE_SCALE
|
||||
|
||||
manager = ProdApiConfigsManager(apiConfigs = createApiConfigs())
|
||||
}
|
||||
|
|
@ -298,6 +299,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -313,6 +315,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
"version" to ProviderSuspend { VERSION_NAME },
|
||||
"platform" to ProviderSuspend { "Android" },
|
||||
"X-API-KEY" to ProviderSuspend { TANGEM_PAY_BFF_KEY_DEV },
|
||||
"X-Device-Scale" to ProviderSuspend { DEVICE_SCALE.toString() },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -457,6 +460,7 @@ internal class ProdApiConfigsManagerTest {
|
|||
private companion object {
|
||||
|
||||
const val VERSION_NAME = "debug"
|
||||
const val DEVICE_SCALE = 3f
|
||||
const val EXPRESS_SESSION_ID = "express_session_id"
|
||||
const val STAKE_KIT_API_KEY = "stake_kit_api_key"
|
||||
const val P2P_API_KEY = "p2p_api_key"
|
||||
|
|
|
|||
|
|
@ -15,11 +15,10 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// 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)
|
||||
|
|
@ -47,7 +46,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem(status = "finished")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("finished")
|
||||
|
|
@ -59,7 +58,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createExchangeItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.from.contractAddress).isEqualTo(item.fromContractAddress)
|
||||
|
|
@ -95,7 +94,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = requireNotNull(item.toEntity())
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.payinExtraId).isNull()
|
||||
|
|
@ -112,17 +111,28 @@ internal class ExpressHistoryConverterTest {
|
|||
Truth.assertThat(entity.to.actualAmount).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN exchange item with null fromAddress WHEN toEntity THEN returns null`() {
|
||||
// GIVEN
|
||||
val item = createExchangeItem().copy(fromAddress = null)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity).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)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// 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)
|
||||
|
|
@ -145,7 +155,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem(status = "waiting-for-payment")
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.status).isEqualTo("waiting-for-payment")
|
||||
|
|
@ -157,7 +167,7 @@ internal class ExpressHistoryConverterTest {
|
|||
val item = createOnrampItem()
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.to.contractAddress).isEqualTo(item.toContractAddress)
|
||||
|
|
@ -180,7 +190,7 @@ internal class ExpressHistoryConverterTest {
|
|||
)
|
||||
|
||||
// WHEN
|
||||
val entity = item.toEntity(ownerAddress = OWNER_ADDRESS)
|
||||
val entity = item.toEntity()
|
||||
|
||||
// THEN
|
||||
Truth.assertThat(entity.failReason).isNull()
|
||||
|
|
@ -269,8 +279,4 @@ internal class ExpressHistoryConverterTest {
|
|||
paymentMethod = "card",
|
||||
countryCode = "US",
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val OWNER_ADDRESS = "0xowner"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.models.pay.TangemPayReissueCardFee
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
/**
|
||||
* Tests for [DefaultTangemPayReissueCardStore], focused on the reissue order-id lifecycle.
|
||||
*
|
||||
* Uses a real [AppPreferencesStore] backed by an in-memory [MockStateDataStore] so the preferences
|
||||
* round-trip (store / read / remove) is exercised end-to-end. The remove path backs the [REDACTED_TASK_KEY] fix:
|
||||
* a terminal reissue order must be forgotten so the payment-account refresh stops re-polling
|
||||
* `GET /order/{id}` for it.
|
||||
*/
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayReissueCardStoreTest {
|
||||
|
||||
private val dataStore = MockStateDataStore(default = emptyPreferences())
|
||||
private val prefs = AppPreferencesStore(
|
||||
moshi = Moshi.Builder().build(),
|
||||
dispatchers = TestingCoroutineDispatcherProvider(),
|
||||
preferencesDataStore = dataStore,
|
||||
)
|
||||
private val feeStore: RuntimeDataStore<TangemPayReissueCardFee> = mockk(relaxed = true)
|
||||
|
||||
private val store = DefaultTangemPayReissueCardStore(feeStore = feeStore, prefs = prefs)
|
||||
|
||||
@BeforeEach
|
||||
fun resetStore() {
|
||||
runBlocking { dataStore.updateData { emptyPreferences() } }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN getOrderId THEN returns stored id`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
val result = store.getOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(ORDER_ID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN order id stored WHEN removeReissueOrderId THEN order id is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN clearing one card WHEN another card has an order THEN the other is untouched`() = runTest {
|
||||
// Arrange
|
||||
store.storeReissueOrderId(CARD_ID, ORDER_ID)
|
||||
store.storeReissueOrderId(OTHER_CARD_ID, OTHER_ORDER_ID)
|
||||
|
||||
// Act
|
||||
store.removeReissueOrderId(CARD_ID)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getOrderId(CARD_ID)).isNull()
|
||||
assertThat(store.getOrderId(OTHER_CARD_ID)).isEqualTo(OTHER_ORDER_ID)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CARD_ID = "card-1"
|
||||
const val OTHER_CARD_ID = "card-2"
|
||||
const val ORDER_ID = "reissue-order-1"
|
||||
const val OTHER_ORDER_ID = "reissue-order-2"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDMConverter
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemToDomainConverter
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.datastore.MockStateDataStore
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class DefaultTangemPayTxHistoryItemsStoreTest {
|
||||
|
||||
private lateinit var store: DefaultTangemPayTxHistoryItemsStore
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
store = DefaultTangemPayTxHistoryItemsStore(
|
||||
dataStore = MockStateDataStore(default = emptyMap()),
|
||||
toDMConverter = TangemPayTxHistoryItemToDMConverter(),
|
||||
toDomainConverter = TangemPayTxHistoryItemToDomainConverter(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN empty store WHEN getSyncOrNull THEN returns null`() = runTest {
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN stored page WHEN getSyncOrNull with same key and cursor THEN returns page`() = runTest {
|
||||
// Arrange
|
||||
val page = listOf(payment("1"), payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page)
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_1)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isEqualTo(page)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN page stored under one cursor WHEN getSyncOrNull with another cursor THEN returns null`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("1")))
|
||||
|
||||
// Act
|
||||
val result = store.getSyncOrNull(key = WALLET_A, cursor = CURSOR_2)
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN two cursors stored for one wallet WHEN getSyncOrNull THEN both pages are kept`() = runTest {
|
||||
// Arrange
|
||||
val page1 = listOf(payment("1"))
|
||||
val page2 = listOf(payment("2"))
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = page1)
|
||||
store.store(key = WALLET_A, cursor = CURSOR_2, value = page2)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isEqualTo(page1)
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_2)).isEqualTo(page2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove one wallet THEN only that wallet is cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(WALLET_A)
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isEqualTo(listOf(payment("b")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN entries for two wallets WHEN remove both in one call THEN both are cleared`() = runTest {
|
||||
// Arrange
|
||||
store.store(key = WALLET_A, cursor = CURSOR_1, value = listOf(payment("a")))
|
||||
store.store(key = WALLET_B, cursor = CURSOR_1, value = listOf(payment("b")))
|
||||
|
||||
// Act
|
||||
store.remove(listOf(WALLET_A, WALLET_B))
|
||||
|
||||
// Assert
|
||||
assertThat(store.getSyncOrNull(WALLET_A, CURSOR_1)).isNull()
|
||||
assertThat(store.getSyncOrNull(WALLET_B, CURSOR_1)).isNull()
|
||||
}
|
||||
|
||||
private fun payment(id: String) = TangemPayTxHistoryItem.Payment(
|
||||
id = id,
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("1.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = null,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val WALLET_A = "wallet-a"
|
||||
const val WALLET_B = "wallet-b"
|
||||
const val CURSOR_1 = "cursor-1"
|
||||
const val CURSOR_2 = "cursor-2"
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
package com.tangem.datasource.local.visa
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.datasource.local.visa.entity.TangemPayTxHistoryItemDM
|
||||
import com.tangem.datasource.utils.KotlinxDataStoreSerializer
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryStoreSerializationTest {
|
||||
|
||||
private val json = KotlinxDataStoreSerializer.jsonBuilder { classDiscriminator = "__type" }
|
||||
|
||||
private val serializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = MapSerializer(
|
||||
keySerializer = String.serializer(),
|
||||
valueSerializer = ListSerializer(TangemPayTxHistoryItemDM.serializer()),
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN all tx history item types WHEN serialized and deserialized THEN value is preserved`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf(
|
||||
"initial_cursor_key" to listOf(spend(), payment(), fee(), collateral()),
|
||||
),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Collateral with type field WHEN serialized and deserialized THEN discriminator does not clash`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val restored = json.decodeFromString(serializer, json.encodeToString(serializer, original))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(original)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN tx history item WHEN serialized THEN uses stable SerialName under non-clashing discriminator`() {
|
||||
// Arrange
|
||||
val original: Map<String, Map<String, List<TangemPayTxHistoryItemDM>>> = mapOf(
|
||||
"wallet-1" to mapOf("cursor" to listOf(collateral())),
|
||||
)
|
||||
|
||||
// Act
|
||||
val encoded = json.encodeToString(serializer, original)
|
||||
|
||||
// Assert
|
||||
assertThat(encoded).contains("\"__type\":\"collateral\"")
|
||||
}
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItemDM.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItemDM.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItemDM.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItemDM.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItemDM.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItemDM.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
package com.tangem.datasource.local.visa.entity
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
|
||||
import com.tangem.test.core.ProvideTestModels
|
||||
import org.joda.time.DateTime
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.EnumSource
|
||||
import java.math.BigDecimal
|
||||
import java.util.Currency
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class TangemPayTxHistoryItemDMConverterTest {
|
||||
|
||||
private val toDMConverter = TangemPayTxHistoryItemToDMConverter()
|
||||
private val toDomainConverter = TangemPayTxHistoryItemToDomainConverter()
|
||||
|
||||
@ParameterizedTest
|
||||
@ProvideTestModels
|
||||
fun `GIVEN every item subtype WHEN converted to DM and back THEN all fields preserved`(
|
||||
item: TangemPayTxHistoryItem,
|
||||
) {
|
||||
assertRoundTrip(item)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Status::class)
|
||||
fun `GIVEN every Spend status WHEN converted to DM and back THEN status preserved`(
|
||||
status: TangemPayTxHistoryItem.Status,
|
||||
) {
|
||||
assertRoundTrip(spend().copy(status = status))
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(TangemPayTxHistoryItem.Type::class)
|
||||
fun `GIVEN every Collateral type WHEN converted to DM and back THEN type preserved`(
|
||||
type: TangemPayTxHistoryItem.Type,
|
||||
) {
|
||||
assertRoundTrip(collateral().copy(type = type))
|
||||
}
|
||||
|
||||
private fun assertRoundTrip(item: TangemPayTxHistoryItem) {
|
||||
// Act
|
||||
val restored = toDomainConverter.convert(toDMConverter.convert(item))
|
||||
|
||||
// Assert
|
||||
assertThat(restored).isEqualTo(item)
|
||||
}
|
||||
|
||||
private fun provideTestModels() = listOf(spend(), payment(), fee(), collateral())
|
||||
|
||||
private fun spend() = TangemPayTxHistoryItem.Spend(
|
||||
id = "spend-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("12.34"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
authorizedAmount = BigDecimal("12.34"),
|
||||
localAmount = BigDecimal("11.00"),
|
||||
localCurrency = Currency.getInstance("EUR"),
|
||||
enrichedMerchantName = "Coffee Co",
|
||||
merchantName = "COFFEE CO",
|
||||
enrichedMerchantCategory = "Food",
|
||||
merchantCategoryCode = "5814",
|
||||
merchantCategory = "restaurants",
|
||||
status = TangemPayTxHistoryItem.Status.COMPLETED,
|
||||
enrichedMerchantIconUrl = "https://example.com/icon.png",
|
||||
declinedReason = null,
|
||||
)
|
||||
|
||||
private fun payment() = TangemPayTxHistoryItem.Payment(
|
||||
id = "payment-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("100.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xabc",
|
||||
)
|
||||
|
||||
private fun fee() = TangemPayTxHistoryItem.Fee(
|
||||
id = "fee-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("0.50"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
description = "network fee",
|
||||
)
|
||||
|
||||
private fun collateral() = TangemPayTxHistoryItem.Collateral(
|
||||
id = "collateral-1",
|
||||
jsonRepresentation = "{}",
|
||||
date = DateTime(DATE_MILLIS),
|
||||
amount = BigDecimal("250.00"),
|
||||
currency = Currency.getInstance("USD"),
|
||||
transactionHash = "0xdef",
|
||||
type = TangemPayTxHistoryItem.Type.Deposit,
|
||||
)
|
||||
|
||||
private companion object {
|
||||
const val DATE_MILLIS = 1_700_000_000_000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue