diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/api/StrigaWalletApi.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/api/StrigaWalletApi.kt index aa87a6d2a4..2dc678d80e 100644 --- a/app/src/main/java/org/p2p/wallet/striga/wallet/api/StrigaWalletApi.kt +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/api/StrigaWalletApi.kt @@ -6,8 +6,10 @@ import org.p2p.wallet.striga.wallet.api.request.StrigaAddWhitelistedAddressReque import org.p2p.wallet.striga.wallet.api.request.StrigaEnrichAccountRequest import org.p2p.wallet.striga.wallet.api.request.StrigaGetWhitelistedAddressesRequest import org.p2p.wallet.striga.wallet.api.request.StrigaInitiateOnchainWithdrawalRequest +import org.p2p.wallet.striga.wallet.api.request.StrigaUserWalletsRequest import org.p2p.wallet.striga.wallet.api.response.StrigaEnrichFiatAccountResponse import org.p2p.wallet.striga.wallet.api.response.StrigaInitiateOnchainWithdrawalResponse +import org.p2p.wallet.striga.wallet.api.response.StrigaUserWalletsResponse import org.p2p.wallet.striga.wallet.api.response.StrigaWhitelistedAddressItemResponse import org.p2p.wallet.striga.wallet.api.response.StrigaWhitelistedAddressesResponse @@ -34,4 +36,7 @@ interface StrigaWalletApi { */ @POST("v1/wallets/account/enrich") suspend fun enrichFiatAccount(@Body body: StrigaEnrichAccountRequest): StrigaEnrichFiatAccountResponse + + @POST("v1/wallets/get/all") + suspend fun getUserWallets(@Body body: StrigaUserWalletsRequest): StrigaUserWalletsResponse } diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/api/request/StrigaUserWalletsRequest.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/api/request/StrigaUserWalletsRequest.kt new file mode 100644 index 0000000000..7205fddb2e --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/api/request/StrigaUserWalletsRequest.kt @@ -0,0 +1,15 @@ +package org.p2p.wallet.striga.wallet.api.request + +import com.google.gson.annotations.SerializedName +import org.p2p.core.utils.MillisSinceEpoch + +class StrigaUserWalletsRequest( + @SerializedName("userId") + val userId: String, + @SerializedName("startDate") + val startDate: MillisSinceEpoch, + @SerializedName("endDate") + val endDate: MillisSinceEpoch, + @SerializedName("page") + val page: Long +) diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletAccountResponse.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletAccountResponse.kt new file mode 100644 index 0000000000..fe31f75990 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletAccountResponse.kt @@ -0,0 +1,39 @@ +package org.p2p.wallet.striga.wallet.api.response + +import com.google.gson.annotations.SerializedName +import java.math.BigInteger + +data class StrigaUserWalletAccountResponse( + @SerializedName("accountId") + val accountId: String, + @SerializedName("parentWalletId") + val parentWalletId: String, + @SerializedName("currency") + val currency: String, + @SerializedName("ownerId") + val ownerId: String, + @SerializedName("rootFiatCurrency") + val rootFiatCurrency: String, + @SerializedName("ownerType") + val ownerType: String, + @SerializedName("createdAt") + val createdAt: String, + @SerializedName("availableBalance") + val availableBalance: AvailableBalanceResponse, + @SerializedName("linkedCardId") + val linkedCardId: String, + @SerializedName("linkedBankAccountId") + val linkedBankAccountId: String, + @SerializedName("status") + val status: String +) { + /** + * @param currencyUnits can be cents, or satoshis or smth else + */ + data class AvailableBalanceResponse( + @SerializedName("amount") + val amount: BigInteger, + @SerializedName("currency") + val currencyUnits: String + ) +} diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletDetailsResponse.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletDetailsResponse.kt new file mode 100644 index 0000000000..89900bc460 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletDetailsResponse.kt @@ -0,0 +1,24 @@ +package org.p2p.wallet.striga.wallet.api.response + +import com.google.gson.annotations.SerializedName +import com.google.gson.internal.LinkedTreeMap + +/** + * @param createdAt - example 2022-07-22T12:39:56.835Z + */ +data class StrigaUserWalletDetailsResponse( + @SerializedName("walletId") + val walletId: String, + @SerializedName("accounts") + val accountCurrencyToDetails: LinkedTreeMap, + @SerializedName("rootFiatCurrency") + val rootFiatCurrency: String, + @SerializedName("syncedOwnerId") + val syncedOwnerId: String, + @SerializedName("ownerType") + val ownerType: String, + @SerializedName("createdAt") + val createdAt: String, + @SerializedName("comment") + val comment: String +) diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletsResponse.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletsResponse.kt new file mode 100644 index 0000000000..15addbe7ca --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/api/response/StrigaUserWalletsResponse.kt @@ -0,0 +1,8 @@ +package org.p2p.wallet.striga.wallet.api.response + +import com.google.gson.annotations.SerializedName + +class StrigaUserWalletsResponse( + @SerializedName("wallets") + val wallets: List +) diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWallet.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWallet.kt new file mode 100644 index 0000000000..782b785710 --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWallet.kt @@ -0,0 +1,21 @@ +package org.p2p.wallet.striga.wallet.models + +import org.p2p.core.utils.isNotZero +import org.p2p.wallet.striga.wallet.models.ids.StrigaWalletId + +/** + * A wallet on the Striga platform contains accounts. + * Accounts are the lowest divisible unit of value storage and are each represented in one currency only. + * When creating a wallet, accounts for each of your configured currencies are created and linked under that wallet. + */ +data class StrigaUserWallet( + val walletId: StrigaWalletId, + val userId: String, + val accounts: List +) { + val hasAvailableBalance: Boolean + get() = accounts.any { it.availableBalance.isNotZero() } + + val eurAccount: StrigaUserWalletAccount? + get() = accounts.firstOrNull { it.accountCurrency == StrigaWalletAccountCurrency.EUR } +} diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWalletAccount.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWalletAccount.kt new file mode 100644 index 0000000000..d8d49d555d --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/models/StrigaUserWalletAccount.kt @@ -0,0 +1,32 @@ +package org.p2p.wallet.striga.wallet.models + +import java.math.BigInteger +import org.p2p.wallet.striga.wallet.models.ids.StrigaAccountId + +class StrigaUserWalletAccount( + val accountId: StrigaAccountId, + // todo (leave comment on PR if you see this): convert to enum when all statuses are found + val accountStatus: String, + val accountCurrency: StrigaWalletAccountCurrency, + val parentWalletId: String, + val ownerId: String, + val rootFiatCurrency: String, + val ownerType: String, + val availableBalance: BigInteger, + val balanceUnit: String, + val linkedBankAccount: StrigaWalletAccountBankLink, +) { + fun availableBalanceWithUnits(): String = "$availableBalance $balanceUnit" +} + +sealed interface StrigaWalletAccountBankLink { + data class Linked(val value: String) : StrigaWalletAccountBankLink + object Unlinked : StrigaWalletAccountBankLink +} + +/** + * Striga provides only 1 wallet with two accounts - USDC and EUR + */ +enum class StrigaWalletAccountCurrency(val currencyName: String) { + USDC("USDC"), EUR("EUR"), OTHER("OTHER") +} diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaUserWalletsMapper.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaUserWalletsMapper.kt new file mode 100644 index 0000000000..30d5a5243c --- /dev/null +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaUserWalletsMapper.kt @@ -0,0 +1,65 @@ +package org.p2p.wallet.striga.wallet.repository + +import org.p2p.wallet.striga.wallet.api.response.StrigaUserWalletAccountResponse +import org.p2p.wallet.striga.wallet.api.response.StrigaUserWalletDetailsResponse +import org.p2p.wallet.striga.wallet.api.response.StrigaUserWalletsResponse +import org.p2p.wallet.striga.wallet.models.StrigaUserWallet +import org.p2p.wallet.striga.wallet.models.StrigaUserWalletAccount +import org.p2p.wallet.striga.wallet.models.StrigaWalletAccountBankLink +import org.p2p.wallet.striga.wallet.models.StrigaWalletAccountCurrency +import org.p2p.wallet.striga.wallet.models.ids.StrigaAccountId +import org.p2p.wallet.striga.wallet.models.ids.StrigaWalletId + +class StrigaUserWalletsMapper { + private companion object { + private const val EUR_ACCOUNT_NAME = "EUR" + private const val USDC_ACCOUNT_NAME = "USDC" + private const val UNLINKED_BANK_ACCOUNT_VALUE = "UNLINKED" + } + + fun fromNetwork(userId: String, response: StrigaUserWalletsResponse): StrigaUserWallet { + require(response.wallets.isNotEmpty()) { + "Wallets should be not empty: they are created when user $userId is created" + } + val activeWallet: StrigaUserWalletDetailsResponse = response.wallets.first() + + return StrigaUserWallet( + userId = userId, + walletId = StrigaWalletId(activeWallet.walletId), + // no support for multiple wallets so we get first + accounts = activeWallet.accountCurrencyToDetails.map(::toDomain) + ) + } + + private fun toDomain(entry: Map.Entry): StrigaUserWalletAccount { + val (accountCurrency, accountDetails) = entry + return StrigaUserWalletAccount( + accountId = StrigaAccountId(accountDetails.accountId), + accountStatus = accountDetails.status, + accountCurrency = mapAccountCurrency(accountCurrency), + parentWalletId = accountDetails.parentWalletId, + ownerId = accountDetails.ownerId, + rootFiatCurrency = accountDetails.rootFiatCurrency, + ownerType = accountDetails.ownerType, + availableBalance = accountDetails.availableBalance.amount, + balanceUnit = accountDetails.availableBalance.currencyUnits, + linkedBankAccount = mapLinkedBankAccount(accountDetails.linkedBankAccountId) + ) + } + + private fun mapAccountCurrency(currency: String): StrigaWalletAccountCurrency { + return when (currency.uppercase()) { + EUR_ACCOUNT_NAME -> StrigaWalletAccountCurrency.EUR + USDC_ACCOUNT_NAME -> StrigaWalletAccountCurrency.USDC + else -> StrigaWalletAccountCurrency.OTHER + } + } + + private fun mapLinkedBankAccount(linkedBankAccountId: String?): StrigaWalletAccountBankLink { + return if (linkedBankAccountId == null || linkedBankAccountId == UNLINKED_BANK_ACCOUNT_VALUE) { + StrigaWalletAccountBankLink.Unlinked + } else { + StrigaWalletAccountBankLink.Linked(linkedBankAccountId) + } + } +} diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRemoteRepository.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRemoteRepository.kt index d374010617..20b295c687 100644 --- a/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRemoteRepository.kt +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRemoteRepository.kt @@ -1,6 +1,8 @@ package org.p2p.wallet.striga.wallet.repository import java.math.BigInteger +import java.util.Calendar +import org.p2p.wallet.striga.StrigaUserIdProvider import org.p2p.wallet.striga.model.StrigaDataLayerError import org.p2p.wallet.striga.model.StrigaDataLayerResult import org.p2p.wallet.striga.model.toSuccessResult @@ -9,9 +11,11 @@ import org.p2p.wallet.striga.wallet.api.request.StrigaAddWhitelistedAddressReque import org.p2p.wallet.striga.wallet.api.request.StrigaEnrichAccountRequest import org.p2p.wallet.striga.wallet.api.request.StrigaGetWhitelistedAddressesRequest import org.p2p.wallet.striga.wallet.api.request.StrigaInitiateOnchainWithdrawalRequest +import org.p2p.wallet.striga.wallet.api.request.StrigaUserWalletsRequest import org.p2p.wallet.striga.wallet.models.StrigaFiatAccountDetails import org.p2p.wallet.striga.wallet.models.StrigaInitiateOnchainWithdrawalDetails import org.p2p.wallet.striga.wallet.models.StrigaNetworkCurrency +import org.p2p.wallet.striga.wallet.models.StrigaUserWallet import org.p2p.wallet.striga.wallet.models.StrigaWhitelistedAddressItem import org.p2p.wallet.striga.wallet.models.ids.StrigaAccountId import org.p2p.wallet.striga.wallet.models.ids.StrigaWhitelistedAddressId @@ -19,6 +23,8 @@ import org.p2p.wallet.striga.wallet.models.ids.StrigaWhitelistedAddressId class StrigaWalletRemoteRepository( private val api: StrigaWalletApi, private val mapper: StrigaWalletRepositoryMapper, + private val walletsMapper: StrigaUserWalletsMapper, + private val strigaUserIdProvider: StrigaUserIdProvider ) : StrigaWalletRepository { override suspend fun initiateOnchainWithdrawal( @@ -105,4 +111,27 @@ class StrigaWalletRemoteRepository( ) } } + + override suspend fun getUserWallet(): StrigaDataLayerResult { + return try { + val hardcodedStartDate = Calendar.getInstance().apply { set(2023, 6, 26) }.timeInMillis + val request = StrigaUserWalletsRequest( + userId = strigaUserIdProvider.getUserIdOrThrow(), + startDate = hardcodedStartDate, + endDate = System.currentTimeMillis(), + page = 1 + ) + val response = api.getUserWallets(request) + + walletsMapper.fromNetwork( + userId = strigaUserIdProvider.getUserIdOrThrow(), + response = response + ).toSuccessResult() + } catch (error: Throwable) { + StrigaDataLayerError.from( + error = error, + default = StrigaDataLayerError.InternalError(error) + ) + } + } } diff --git a/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepository.kt b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepository.kt index 96ef4dfee0..b74e4dd800 100644 --- a/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepository.kt +++ b/app/src/main/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepository.kt @@ -3,11 +3,12 @@ package org.p2p.wallet.striga.wallet.repository import java.math.BigInteger import org.p2p.wallet.striga.model.StrigaDataLayerResult import org.p2p.wallet.striga.wallet.models.StrigaFiatAccountDetails -import org.p2p.wallet.striga.wallet.models.ids.StrigaAccountId import org.p2p.wallet.striga.wallet.models.StrigaInitiateOnchainWithdrawalDetails import org.p2p.wallet.striga.wallet.models.StrigaNetworkCurrency -import org.p2p.wallet.striga.wallet.models.ids.StrigaWhitelistedAddressId +import org.p2p.wallet.striga.wallet.models.StrigaUserWallet import org.p2p.wallet.striga.wallet.models.StrigaWhitelistedAddressItem +import org.p2p.wallet.striga.wallet.models.ids.StrigaAccountId +import org.p2p.wallet.striga.wallet.models.ids.StrigaWhitelistedAddressId interface StrigaWalletRepository { @@ -68,4 +69,6 @@ interface StrigaWalletRepository { userId: String, accountId: StrigaAccountId ): StrigaDataLayerResult + + suspend fun getUserWallet(): StrigaDataLayerResult } diff --git a/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryTest.kt b/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryEnrichAccountTest.kt similarity index 98% rename from app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryTest.kt rename to app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryEnrichAccountTest.kt index 5993e3675f..27eb45befa 100644 --- a/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryTest.kt +++ b/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryEnrichAccountTest.kt @@ -24,7 +24,7 @@ import org.p2p.wallet.striga.wallet.models.ids.StrigaWhitelistedAddressId import org.p2p.wallet.utils.fromJsonReified @OptIn(ExperimentalCoroutinesApi::class) -class StrigaWalletRepositoryTest { +class StrigaWalletRepositoryEnrichAccountTest { private val gson = Gson() private val api: StrigaWalletApi = mockk() @@ -232,6 +232,8 @@ class StrigaWalletRepositoryTest { return StrigaWalletRemoteRepository( api = api, mapper = StrigaWalletRepositoryMapper(), + walletsMapper = mockk(), + strigaUserIdProvider = mockk() ) } } diff --git a/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryGetUserWalletTest.kt b/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryGetUserWalletTest.kt new file mode 100644 index 0000000000..37099b5e82 --- /dev/null +++ b/app/src/test/java/org/p2p/wallet/striga/wallet/repository/StrigaWalletRepositoryGetUserWalletTest.kt @@ -0,0 +1,154 @@ +package org.p2p.wallet.striga.wallet.repository + +import assertk.all +import assertk.assertions.isEqualTo +import assertk.assertions.isInstanceOf +import assertk.assertions.isNotEmpty +import assertk.assertions.isNotNull +import assertk.assertions.isTrue +import assertk.assertions.prop +import com.google.gson.Gson +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import org.intellij.lang.annotations.Language +import org.junit.Before +import org.junit.Test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.p2p.wallet.striga.model.StrigaDataLayerResult +import org.p2p.wallet.striga.wallet.api.StrigaWalletApi +import org.p2p.wallet.striga.wallet.api.response.StrigaUserWalletsResponse +import org.p2p.wallet.striga.wallet.models.StrigaUserWallet +import org.p2p.wallet.striga.wallet.models.ids.StrigaWalletId +import org.p2p.wallet.utils.assertThat +import org.p2p.wallet.utils.createHttpException +import org.p2p.wallet.utils.fromJsonReified +import org.p2p.wallet.utils.stub + +@OptIn(ExperimentalCoroutinesApi::class) +class StrigaWalletRepositoryGetUserWalletTest { + private val gson = Gson() + private val api: StrigaWalletApi = mockk() + private val userId = "65b1c37c-686a-487b-81f4-8d0ea6dd0e53" + + private var repository = StrigaWalletRemoteRepository( + api = api, + mapper = StrigaWalletRepositoryMapper(), + walletsMapper = StrigaUserWalletsMapper(), + strigaUserIdProvider = mockk { + every { getUserIdOrThrow() }.returns(userId) + } + ) + + @Before + fun beforeEach() { + repository = StrigaWalletRemoteRepository( + api = api, + mapper = StrigaWalletRepositoryMapper(), + walletsMapper = StrigaUserWalletsMapper(), + strigaUserIdProvider = mockk { + every { getUserIdOrThrow() }.returns(userId) + } + ) + } + + @Test + fun `GIVEN 200 response WHEN get wallets THEN user wallet is gotten`() = runTest { + // GIVEN + @Language("JSON") val successResponse = """ +{ + "wallets": [ + { + "walletId": "3d57a943-8145-4183-8079-cd86b68d2993", + "syncedOwnerId": "aa3534a1-d13d-4920-b023-97cb00d49bad", + "ownerType": "CONSUMER", + "createdAt": "2023-05-28T19:47:17.094Z", + "comment": "DEFAULT", + "accounts": { + "EUR": { + "accountId": "4dc6ecb29d74198e9e507f8025cad011", + "parentWalletId": "3d57a943-8145-4183-8079-cd86b68d2993", + "currency": "EUR", + "ownerId": "aa3534a1-d13d-4920-b023-97cb00d49bad", + "ownerType": "CONSUMER", + "createdAt": "2023-05-28T19:47:17.077Z", + "availableBalance": { + "amount": "1888383", + "currency": "cents" + }, + "linkedCardId": "UNLINKED", + "linkedBankAccountId": "EUR10112624134233", + "status": "ACTIVE", + "permissions": [ + "CUSTODY", + "TRADE", + "INTER", + "INTRA" + ], + "enriched": true + }, + "USDC": { + "accountId": "140ecf6f979975c8e868d14038004b37", + "parentWalletId": "3d57a943-8145-4183-8079-cd86b68d2993", + "currency": "USDC", + "ownerId": "aa3534a1-d13d-4920-b023-97cb00d49bad", + "ownerType": "CONSUMER", + "createdAt": "2023-05-28T19:47:17.078Z", + "availableBalance": { + "amount": "5889", + "currency": "cents" + }, + "linkedCardId": "UNLINKED", + "blockchainDepositAddress": "0xF13607D9Ab2D98f6734Dc09e4CDE7dA515fe329c", + "blockchainNetwork": { + "name": "USD Coin Test (Goerli)", + "type": "ERC20", + "contractAddress": "0x07865c6E87B9F70255377e024ace6630C1Eaa37F" + }, + "status": "ACTIVE", + "permissions": [ + "CUSTODY", + "TRADE", + "INTER", + "INTRA" + ], + "enriched": true + } + }, + "count": 1, + "total": 1 + } + ] +} + """.trimIndent() + val parsedResponse = gson.fromJsonReified(successResponse)!! + api.stub { + coEvery { getUserWallets(any()) }.returns(parsedResponse) + } + // WHEN + val actualResult: StrigaUserWallet = repository.getUserWallet().unwrap() + + actualResult.assertThat() + .all { + prop(StrigaUserWallet::walletId).isEqualTo(StrigaWalletId("3d57a943-8145-4183-8079-cd86b68d2993")) + prop(StrigaUserWallet::userId).isEqualTo(userId) + prop(StrigaUserWallet::eurAccount).isNotNull() + prop(StrigaUserWallet::hasAvailableBalance).isTrue() + + prop(StrigaUserWallet::accounts).isNotEmpty() + } + } + + @Test + fun `GIVEN 400 response WHEN get wallets THEN user wallet is not gotten`() = runTest { + // GIVEN + api.stub { + coEvery { getUserWallets(any()) }.throws(createHttpException(400, "{}")) + } + // WHEN + val actualResult = repository.getUserWallet() + actualResult.assertThat() + .isInstanceOf(StrigaDataLayerResult.Failure::class) + } +}