From dc5898f561e3c039ba1db7e3b9c68c19c2265b1a Mon Sep 17 00:00:00 2001 From: Vihang Patil Date: Thu, 25 Apr 2024 13:14:20 +0200 Subject: [PATCH] feat(vault): export transactions as csv --- .github/workflows/deploy-endpoints.yaml | 2 +- .../k33/platform/app/vault/VaultEndpoint.kt | 119 +++++++++++++++--- .../k33/platform/app/vault/VaultService.kt | 91 ++++++++++++++ .../src/main/httpx/vault.http | 8 ++ .../src/main/openapi/k33-backend-api.yaml | 64 ++++++++++ .../main/openapi/k33-backend-canary-api.yaml | 64 ++++++++++ .../main/openapi/k33-backend-test-api.yaml | 62 +++++++++ .../platform/fireblocks/service/ApiModel.kt | 59 +++++++-- .../fireblocks/service/FireblocksService.kt | 56 ++++++--- .../service/FireblocksServiceTest.kt | 32 ----- 10 files changed, 480 insertions(+), 77 deletions(-) diff --git a/.github/workflows/deploy-endpoints.yaml b/.github/workflows/deploy-endpoints.yaml index c1326a7..d427fa7 100644 --- a/.github/workflows/deploy-endpoints.yaml +++ b/.github/workflows/deploy-endpoints.yaml @@ -4,7 +4,7 @@ on: env: GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} SERVICE_ID: api.k33.com - ESP_FULL_VERSION: 2.47.0 + ESP_FULL_VERSION: 2.48.0 jobs: deploy-endpoints: runs-on: ubuntu-latest diff --git a/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultEndpoint.kt b/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultEndpoint.kt index 64f35a6..674a74a 100644 --- a/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultEndpoint.kt +++ b/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultEndpoint.kt @@ -1,5 +1,6 @@ package com.k33.platform.app.vault +import com.k33.platform.app.vault.VaultService.getTransactions import com.k33.platform.app.vault.VaultService.getVaultAddresses import com.k33.platform.app.vault.VaultService.getVaultAppSettings import com.k33.platform.app.vault.VaultService.getVaultAssets @@ -8,22 +9,33 @@ import com.k33.platform.app.vault.VaultService.updateVaultAppSettings import com.k33.platform.identity.auth.gcp.UserInfo import com.k33.platform.user.UserId import com.k33.platform.utils.logging.logWithMDC +import io.ktor.http.ContentDisposition +import io.ktor.http.ContentType +import io.ktor.http.HeaderValueParam +import io.ktor.http.HttpHeaders import io.ktor.http.HttpStatusCode import io.ktor.http.Parameters import io.ktor.server.application.Application +import io.ktor.server.application.ApplicationCall import io.ktor.server.application.call import io.ktor.server.auth.authenticate import io.ktor.server.auth.principal import io.ktor.server.plugins.BadRequestException import io.ktor.server.request.receive +import io.ktor.server.response.header import io.ktor.server.response.respond +import io.ktor.server.response.respondBytes import io.ktor.server.routing.get import io.ktor.server.routing.put import io.ktor.server.routing.route import io.ktor.server.routing.routing +import io.ktor.utils.io.core.toByteArray import kotlinx.serialization.Serializable +import java.time.Instant import java.time.LocalDate +import java.time.LocalTime import java.time.ZoneId +import java.time.ZonedDateTime import java.time.format.DateTimeParseException fun Application.module() { @@ -48,6 +60,15 @@ fun Application.module() { } } + get("transactions") { + val userId = UserId(call.principal()!!.userId) + logWithMDC("userId" to userId.value) { + val (afterDate, beforeDate, zoneId) = call.request.queryParameters.getDateRange() + val transactions = userId.getTransactions(afterDate to beforeDate, zoneId) + call.respondTransactionsAsCsv(transactions) + } + } + route("settings") { get { val userId = UserId(call.principal()!!.userId) @@ -76,6 +97,17 @@ fun Application.module() { } } } + + get("/apps/vault/admin/transactions") { + val vaultAccountId = call.request.queryParameters["vaultAccountId"] + ?: throw BadRequestException("Missing query parameter: vaultAccountId") + logWithMDC("vaultAccountId" to vaultAccountId) { + val (afterDate, beforeDate, zoneId) = call.request.queryParameters.getDateRange() + val transactions = getTransactions(vaultAccountId, afterDate to beforeDate, zoneId) + call.respondTransactionsAsCsv(transactions) + } + } + put("/admin/jobs/generate-vault-accounts-balance-reports/{date?}") { val mode = call.request.queryParameters.getEnum("mode") ?: Mode.FETCH_AND_STORE val date = call.parameters.getLocalDate("date") @@ -89,26 +121,64 @@ fun Application.module() { } } -private inline fun > Parameters.getEnum(name: String): E? { - return get(name) - ?.let { - try { - enumValueOf(it) - } catch (e: IllegalArgumentException) { - throw BadRequestException("Invalid enum value: $it of $name") - } +private inline fun > Parameters.getEnum( + name: String +): E? = get(name) + ?.let { + try { + enumValueOf(it) + } catch (e: IllegalArgumentException) { + throw BadRequestException("Invalid enum value: $it of $name") } + } + +private fun Parameters.getLocalDate( + name: String +): LocalDate? = get(name) + ?.let { + try { + LocalDate.parse(it) + } catch (e: DateTimeParseException) { + throw BadRequestException("Invalid local date value: $it of $name") + } + } + +private fun Parameters.getDateRange(): Triple { + val zoneId = this["zoneId"]?.let(ZoneId::of) ?: ZoneId.of("Europe/Oslo") + val afterDate = ZonedDateTime.of(getLocalDate("afterDate"), LocalTime.MIN, zoneId).toInstant() + val beforeDate = ZonedDateTime.of(getLocalDate("beforeDate"), LocalTime.MAX, zoneId).toInstant() + return Triple(afterDate, beforeDate, zoneId) } -private fun Parameters.getLocalDate(name: String): LocalDate? { - return get(name) - ?.let { - try { - LocalDate.parse(it) - } catch (e: DateTimeParseException) { - throw BadRequestException("Invalid local date value: $it of $name") - } +private suspend fun ApplicationCall.respondTransactionsAsCsv( + transactions: List, +) { + val csvBytes = toCsv(transactions).toByteArray(Charsets.UTF_8) + this.response.header( + HttpHeaders.ContentDisposition, + ContentDisposition.Attachment.withParameters( + listOf( + HeaderValueParam(ContentDisposition.Parameters.FileName, "transactions.csv"), + HeaderValueParam(ContentDisposition.Parameters.Size, csvBytes.size.toString()), + ) + ).toString() + ) + this.respondBytes( + bytes = csvBytes, + contentType = ContentType.Text.CSV, + status = HttpStatusCode.OK, + ) +} + +private fun toCsv( + transactions: List +): String = buildString { + appendLine("Id,CreatedAt,Operation,Direction,Asset,Amount,NetAmount,AmountUSD,FeeCurrency,Fee") + transactions.forEach { transaction -> + with(transaction) { + appendLine(""""$id","$createdAt","$operation","$direction","$assetId",$amount,$netAmount,$amountUSD,"$feeCurrency",$fee""") } + } } @Serializable @@ -150,8 +220,10 @@ enum class Mode( ) { // running on time to store a snapshot of balances and generate PDFs FETCH_AND_STORE(fetch = true, store = true), + // testing to generate PDFs only FETCH(fetch = true, store = false), + // running later to regenerate PDFs only LOAD(fetch = false, store = false); @@ -161,4 +233,17 @@ enum class Mode( val useCurrentFxRate: Boolean get() = store -} \ No newline at end of file +} + +data class Transaction( + val id: String, + val createdAt: String, + val operation: String, + val direction: String, + val assetId: String, + val amount: String, + val netAmount: String, + val amountUSD: String, + val feeCurrency: String, + val fee: String, +) \ No newline at end of file diff --git a/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultService.kt b/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultService.kt index 911a15f..ed46ce2 100644 --- a/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultService.kt +++ b/libs/apps/vault/src/main/kotlin/com/k33/platform/app/vault/VaultService.kt @@ -5,7 +5,10 @@ import com.k33.platform.app.vault.coingecko.CoinGeckoClient import com.k33.platform.app.vault.pdf.getBalanceReport import com.k33.platform.app.vault.stripe.StripeService import com.k33.platform.filestore.FileStoreService +import com.k33.platform.fireblocks.service.Destination +import com.k33.platform.fireblocks.service.Destinations import com.k33.platform.fireblocks.service.FireblocksService +import com.k33.platform.fireblocks.service.TxnSrcDest import com.k33.platform.identity.auth.gcp.FirebaseAuthService import com.k33.platform.user.UserId import com.k33.platform.utils.logging.NotifySlack @@ -16,7 +19,11 @@ import io.ktor.server.plugins.NotFoundException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope +import java.math.BigDecimal +import java.time.Instant import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId import java.util.Locale object VaultService { @@ -144,6 +151,90 @@ object VaultService { firestoreClient.put(inVaultAppContext(), vaultApp) } + suspend fun UserId.getTransactions( + dateRange: Pair, + zoneId: ZoneId, + ): List { + val vaultApp = firestoreClient.get(inVaultAppContext()) + ?: throw NotFoundException("Not registered to K33 Vault service") + + val vaultAccountId = vaultApp.vaultAccountId + val vaultAccount = FireblocksService.fetchVaultAccountById( + vaultAccountId = vaultAccountId, + ) + + if (vaultAccount == null) { + logger.warn( + NotifySlack.ALERTS, + "No vault account (id: $vaultAccountId) found in Fireblocks" + ) + return emptyList() + } + + return getTransactions( + vaultAccountId = vaultAccountId, + dateRange = dateRange, + zoneId = zoneId + ) + } + + suspend fun getTransactions( + vaultAccountId: String, + dateRange: Pair, + zoneId: ZoneId + ): List { + val (after, before) = dateRange + return FireblocksService.fetchTransactions( + vaultAccountId = vaultAccountId, + after = after, + before = before, + ).map { transaction -> + val createdAt = LocalDateTime.ofInstant(Instant.ofEpochMilli(transaction.createdAt!!), zoneId) + val fee = transaction.feeInfo?.let { + (it.serviceFee?.toBigDecimalOrNull() ?: BigDecimal.ZERO) + + (it.networkFee?.toBigDecimalOrNull() ?: BigDecimal.ZERO) + + (it.gasPrice?.toBigDecimalOrNull() ?: BigDecimal.ZERO) + } ?: BigDecimal.ZERO + + // this will be overwritten by transaction.destinations.* if transaction.destinations != null + var amount = transaction.amountInfo?.amount ?: "" + var amountUSD = transaction.amountInfo?.amountUSD ?: "" + + fun TxnSrcDest?.isVaultAccountWith(id: String): Boolean = this?.type == "VAULT_ACCOUNT" && this.id == id + + // needed only if transaction source and destination are not the vault account + val destinations by lazy { + transaction.destinations?.find { + it.destination?.type == "VAULT_ACCOUNT" && it.destination?.id == vaultAccountId + } + } + + val direction = when { + transaction.source.isVaultAccountWith(id = vaultAccountId) -> "DEBIT" + transaction.destination.isVaultAccountWith(id = vaultAccountId) -> "CREDIT" + destinations != null -> { + amount = destinations?.amount ?: "" + amountUSD = destinations?.amountUSD ?: "" + "CREDIT" + } + else -> "" + } + + Transaction( + id = transaction.id ?: "", + createdAt = createdAt.toString(), + operation = transaction.operation ?: "", + direction = direction, + assetId = transaction.assetId ?: "", + amount = amount, + netAmount = transaction.amountInfo?.netAmount ?: "", + amountUSD = amountUSD, + feeCurrency = transaction.feeCurrency ?: "", + fee = fee.toPlainString(), + ) + } + } + suspend fun generateVaultAccountBalanceReports( date: LocalDate, mode: Mode, diff --git a/libs/clients/k33-backend-client/src/main/httpx/vault.http b/libs/clients/k33-backend-client/src/main/httpx/vault.http index 7a1b8dc..c874783 100644 --- a/libs/clients/k33-backend-client/src/main/httpx/vault.http +++ b/libs/clients/k33-backend-client/src/main/httpx/vault.http @@ -2,3 +2,11 @@ GET https://api.k33.com/apps/vault/assets Authorization: Bearer {{firebase_id_token}} + +### Get vault transactions by admin + +GET https://api.k33.com/apps/vault/admin/transactions + ?vaultAccountId={{vault_account_id}} + &afterDate=2023-01-01 + &beforeDate=2023-12-31 +X-API-KEY: {{admin_api_key}} \ No newline at end of file diff --git a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-api.yaml b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-api.yaml index ba12717..4d5afd5 100644 --- a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-api.yaml +++ b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-api.yaml @@ -401,6 +401,70 @@ paths: security: - firebase: [ ] + "/apps/vault/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactions" + produces: + - "text/csv" + parameters: + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + security: + - firebase: [ ] + + "/apps/vault/admin/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactionsByAdmin" + produces: + - "text/csv" + parameters: + - name: "vaultAccountId" + in: query + type: string + required: true + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + security: + - api_key: [ ] + "/apps/vault/settings": get: description: Get vault app setting diff --git a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-canary-api.yaml b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-canary-api.yaml index 92b142f..8dce310 100644 --- a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-canary-api.yaml +++ b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-canary-api.yaml @@ -402,6 +402,70 @@ paths: security: - firebase: [ ] + "/apps/vault/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactions" + produces: + - "text/csv" + parameters: + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + security: + - firebase: [ ] + + "/apps/vault/admin/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactionsByAdmin" + produces: + - "text/csv" + parameters: + - name: "vaultAccountId" + in: query + type: string + required: true + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + security: + - api_key: [ ] + "/apps/vault/settings": get: description: Get vault app setting diff --git a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-test-api.yaml b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-test-api.yaml index ff55ed8..718812d 100644 --- a/libs/clients/k33-backend-client/src/main/openapi/k33-backend-test-api.yaml +++ b/libs/clients/k33-backend-client/src/main/openapi/k33-backend-test-api.yaml @@ -395,6 +395,68 @@ paths: security: - firebase: [ ] + "/apps/vault/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactions" + produces: + - "text/csv" + parameters: + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + security: + - firebase: [ ] + + "/apps/vault/admin/transactions": + get: + description: Get vault transactions + operationId: "getVaultTransactionsByAdmin" + produces: + - "text/csv" + parameters: + - name: "vaultAccountId" + in: query + type: string + required: true + - name: "afterDate" + in: query + type: string + format: date + required: true + - name: "beforeDate" + in: query + type: string + format: date + required: true + - name: "zoneId" + in: query + type: string + format: date + required: false + responses: + 200: + description: "CSV file list of transactions" + 404: + description: "User/account not registered/found" + "/apps/vault/settings": get: description: Get vault app setting diff --git a/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/ApiModel.kt b/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/ApiModel.kt index 517929f..42127b6 100644 --- a/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/ApiModel.kt +++ b/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/ApiModel.kt @@ -42,27 +42,53 @@ data class VaultAssetAddress( data class Transaction( val id: String? = null, + // val externalTxId: String? = null, // https://developers.fireblocks.com/reference/primary-transaction-statuses - val status: String? = null, + // val status: String? = null, // https://developers.fireblocks.com/reference/transaction-substatuses - val subStatus: String? = null, + // val subStatus: String? = null, + // val txHash: String? = null, val operation: String? = null, + // val note: String? = null, val assetId: String? = null, val source: Source? = null, - val sourceAddress: String? = null, - val tag: String? = null, + // val sourceAddress: String? = null, + // val tag: String? = null, val destination: Destination? = null, - val destinationAddress: String? = null, - val destinationAddressDescription: String? = null, - val destinationTag: String? = null, + val destinations: List? = null, + // val destinationAddress: String? = null, + // val destinationAddressDescription: String? = null, + // val destinationTag: String? = null, + // contractCallDecodedData val amountInfo: AmountInfo? = null, + // val treatAsGrossAmount: Boolean? = null, val feeInfo: FeeInfo? = null, val feeCurrency: String? = null, + // networkRecords val createdAt: Long? = null, - val lastUpdated: Long? = null, - val createdBy: String? = null, - val signedBy: List = emptyList(), - val rejectedBy: String? = null, + // val lastUpdated: Long? = null, + // val createdBy: String? = null, + // val signedBy: List = emptyList(), + // val rejectedBy: String? = null, + // authorizationInfo + // val exchangeTxId: String? = null, + // val customerRefId: String? = null, + // amlScreeningResult + // extraParameters + // signedMessages + // val numOfConfirmations: Long? = null, + // blockInfo + // val index: Long? = null, + // rewardInfo + // systemMessages + // val addressType: String? = null, + // @Deprecated val requestedAmount: Long? = null, + // @Deprecated val amount: Long? = null, + // @Deprecated val netAmount: Long? = null, + // @Deprecated val amountUSD: Long? = null, + // @Deprecated val serviceFee: Long? = null, + // @Deprecated val fee: Long? = null, + // @Deprecated val networkFee: Long? = null, ) data class TxnSrcDest( @@ -75,6 +101,17 @@ data class TxnSrcDest( typealias Source = TxnSrcDest typealias Destination = TxnSrcDest +data class Destinations( + val destination: Destination? = null, + // val destinationAddress: String? = null, + // val destinationAddressDescription: String? = null, + val amount: String? = null, + val amountUSD: String? = null, + // amlScreeningResult + // val customerRefId: String? = null, + // authorizationInfo +) + data class AmountInfo( val amount: String? = null, val requestedAmount: String? = null, diff --git a/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/FireblocksService.kt b/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/FireblocksService.kt index 9582312..7f3464f 100644 --- a/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/FireblocksService.kt +++ b/libs/utils/fireblocks/src/main/kotlin/com/k33/platform/fireblocks/service/FireblocksService.kt @@ -1,6 +1,7 @@ package com.k33.platform.fireblocks.service import com.k33.platform.fireblocks.client.FireblocksClient +import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import java.time.Instant @@ -37,32 +38,55 @@ object FireblocksService { after: Instant, before: Instant, ): List = coroutineScope { - val sourceTransactions = async { + // page limit. default: 200, max: 500 + val limit = 500 + + // loop to get all pages + suspend fun fetchPaginatedTransactionsAsync( + nextPage: suspend (nextAfter: String) -> List?, + ): Deferred> { + return async { + val transactionsList = mutableListOf() + var nextAfter: String = after.toEpochMilli().toString() + do { + val transactions = nextPage(nextAfter) + if (transactions != null) { + transactionsList.addAll(transactions) + nextAfter = transactions.lastOrNull()?.createdAt?.toString() ?: "" + } + } while (transactions?.size == limit) + transactionsList + } + } + + val commonQueryParams = arrayOf( + "before" to before.toEpochMilli().toString(), + "status" to "COMPLETED", + "orderBy" to "createdAt", + "sort" to "ASC", + "limit" to limit.toString() + ) + // list of transactions where given vault account is source + val sourceTransactions = fetchPaginatedTransactionsAsync { nextAfter -> FireblocksClient.get>( path = "transactions", - "after" to after.toEpochMilli().toString(), - "before" to before.toEpochMilli().toString(), - "status" to "COMPLETED", - "orderBy" to "createdAt", - "sort" to "ASC", + *commonQueryParams, + "after" to nextAfter, "sourceType" to "VAULT_ACCOUNT", "sourceId" to vaultAccountId, - "limit" to "1" - ) ?: emptyList() + ) } - val destinationTransactions = async { + // list of transactions where given vault account is destination + val destinationTransactions = fetchPaginatedTransactionsAsync { nextAfter -> FireblocksClient.get>( path = "transactions", - "after" to after.toEpochMilli().toString(), - "before" to before.toEpochMilli().toString(), - "status" to "COMPLETED", - "orderBy" to "createdAt", - "sort" to "ASC", + *commonQueryParams, + "after" to nextAfter, "destType" to "VAULT_ACCOUNT", "destId" to vaultAccountId, - "limit" to "10" - ) ?: emptyList() + ) } + // merge transactions and sort by created ((sourceTransactions.await()) + (destinationTransactions.await())) .sortedBy { it.createdAt } } diff --git a/libs/utils/fireblocks/src/test/kotlin/com/k33/platform/fireblocks/service/FireblocksServiceTest.kt b/libs/utils/fireblocks/src/test/kotlin/com/k33/platform/fireblocks/service/FireblocksServiceTest.kt index 187cac7..31b1cbb 100644 --- a/libs/utils/fireblocks/src/test/kotlin/com/k33/platform/fireblocks/service/FireblocksServiceTest.kt +++ b/libs/utils/fireblocks/src/test/kotlin/com/k33/platform/fireblocks/service/FireblocksServiceTest.kt @@ -25,36 +25,4 @@ class FireblocksServiceTest : StringSpec({ ) } shouldNotBe emptyList() } - "fetch transactions".config(enabled = false) { - val zoneId = ZoneId.of("Europe/Oslo") - val zoneOffset = ZoneOffset.of("+01:00") - val after = ZonedDateTime.ofLocal(LocalDateTime.parse("2023-01-01T00:00:00"), zoneId, zoneOffset).toInstant() - .minusMillis(1) - println("after: $after") - val before = ZonedDateTime.ofLocal(LocalDateTime.parse("2024-01-01T00:00:00"), zoneId, zoneOffset).toInstant() - println("before: $before") - val transactions = FireblocksService.fetchTransactions( - vaultAccountId = vaultAccountId, - after = after, - before = before, - ) - transactions.forEach(::println) - println("CreatedAt,Operation,Direction,Asset,Amount,Fee") - transactions.forEach { transaction -> - val createdAt = - ZonedDateTime.ofInstant(Instant.ofEpochMilli(transaction.createdAt!!), zoneId).toLocalDateTime() - val amount = transaction.amountInfo?.amountUSD!! - val fee = transaction.feeInfo?.let { - (it.serviceFee?.toBigDecimalOrNull() ?: BigDecimal.ZERO) + - (it.networkFee?.toBigDecimalOrNull() ?: BigDecimal.ZERO) + - (it.gasPrice?.toBigDecimalOrNull() ?: BigDecimal.ZERO) - } ?: BigDecimal.ZERO - val direction = when { - transaction.source?.type == "VAULT_ACCOUNT" && transaction.source?.id == vaultAccountId -> "DEBIT" - transaction.destination?.type == "VAULT_ACCOUNT" && transaction.destination?.id == vaultAccountId -> "CREDIT" - else -> null - } - println("$createdAt,${transaction.operation},$direction,${transaction.assetId},USD $amount,${transaction.feeCurrency} ${fee.toPlainString()}") - } - } }) \ No newline at end of file