Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.newm.server.features.cardano.model

import io.newm.chain.grpc.NativeAsset
import java.util.UUID

data class AllocatedNativeAsset(
val asset: NativeAsset,
val allocations: Map<UUID, Long>
)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ data class CardanoNftSong(
val assetName: String,
val isStreamToken: Boolean,
val amount: Long,
val allocations: Map<@Contextual UUID, Long>,
val title: String,
val imageUrl: String,
val audioUrl: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ private val legacyPrefixRegex = "^\\d+\\.\\s*".toRegex()

fun List<LedgerAssetMetadataItem>.toNFTSongs(
asset: NativeAsset,
allocations: Map<UUID, Long>,
isStreamToken: Boolean,
isNftCdnEnabled: Boolean
): List<CardanoNftSong> {
Expand Down Expand Up @@ -108,7 +109,8 @@ fun List<LedgerAssetMetadataItem>.toNFTSongs(
policyId = asset.policy,
assetName = assetName,
isStreamToken = isStreamToken,
amount = asset.amount.toLong(),
amount = allocations.values.sum(),
allocations = allocations,
title = title,
audioUrl =
file.src.takeUnless { isNftCdnEnabled }?.toResourceUrl() ?: nftCdnRepository.generateFileUrl(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import io.newm.server.config.repo.ConfigRepository.Companion.CONFIG_KEY_NFTCDN_E
import io.newm.server.features.cardano.database.KeyEntity
import io.newm.server.features.cardano.database.KeyTable
import io.newm.server.features.cardano.database.ScriptAddressWhitelistEntity
import io.newm.server.features.cardano.model.AllocatedNativeAsset
import io.newm.server.features.cardano.model.CardanoNftSong
import io.newm.server.features.cardano.model.EncryptionRequest
import io.newm.server.features.cardano.model.GetWalletSongsResponse
Expand Down Expand Up @@ -89,12 +90,12 @@ import io.newm.shared.koin.inject
import io.newm.shared.ktx.isValidHex
import io.newm.shared.ktx.isValidPassword
import io.newm.txbuilder.ktx.fingerprint
import io.newm.txbuilder.ktx.mergeAmounts
import io.newm.txbuilder.ktx.toCborObject
import io.newm.txbuilder.ktx.toNativeAssetMap
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
import org.springframework.security.crypto.encrypt.BytesEncryptor
import org.springframework.security.crypto.encrypt.Encryptors
Expand Down Expand Up @@ -597,22 +598,22 @@ internal class CardanoRepositoryImpl(
includeLegacy: Boolean,
useDripDropz: Boolean
): List<CardanoNftSong> {
val assets = if (useDripDropz) getDripDropzAssets(userId) else getWalletAssets(userId)
val allocatedAssets = if (useDripDropz) getDripDropzAssets(userId) else getWalletAssets(userId)
val nftSongs = mutableListOf<CardanoNftSong>()
val streamTokenPolicyIds = configRepository.getStrings(CONFIG_KEY_MINT_ALL_POLICY_IDS)
val isNftCdnEnabled = configRepository.getBoolean(CONFIG_KEY_NFTCDN_ENABLED)
for (asset in assets) {
for ((asset, allocation) in allocatedAssets) {
val metadata = getAssetMetadata(asset)
if (includeLegacy || metadata.any { it.key.equals("music_metadata_version", true) }) {
val isStreamToken = asset.policy in streamTokenPolicyIds
nftSongs += metadata.toNFTSongs(asset, isStreamToken, isNftCdnEnabled)
nftSongs += metadata.toNFTSongs(asset, allocation, isStreamToken, isNftCdnEnabled)
}
}
return nftSongs
}

override suspend fun getWalletImages(userId: UserId): List<String> {
val assets = getWalletAssets(userId)
val assets = getWalletAssets(userId).map { it.asset }
return if (configRepository.getBoolean(CONFIG_KEY_NFTCDN_ENABLED)) {
assets.map { nftCdnRepository.generateImageUrl(it.fingerprint()) }
} else {
Expand All @@ -622,36 +623,52 @@ internal class CardanoRepositoryImpl(
}
}

private fun getStakeAddressesByUserId(userId: UserId): List<String> =
transaction {
WalletConnectionEntity.getAllByUserIdAndWalletChain(userId, WalletChain.Cardano).map { it.address }
private suspend fun getWalletAssets(userId: UserId): List<AllocatedNativeAsset> =
getAssetsByUserId(userId) { address ->
client
.queryUtxosByStakeAddress(queryUtxosRequest { this.address = address })
.utxosList
.flatMap { it.nativeAssetsList }
}

private suspend fun getWalletAssets(userId: UserId): List<NativeAsset> =
getStakeAddressesByUserId(userId)
.flatMap {
client
.queryUtxosByStakeAddress(
queryUtxosRequest {
address = it
}
).utxosList
.flatMap { it.nativeAssetsList }
}.mergeAmounts()

private suspend fun getDripDropzAssets(userId: UserId): List<NativeAsset> =
getStakeAddressesByUserId(userId)
.flatMap { dripDropzRepository.checkAvailableTokens(it) }
.map {
nativeAsset {
policy = it.tokenPolicy
name = it.tokenAssetId
amount = it.availableQuantity
.movePointRight(6)
.toBigInteger()
.toString()
private suspend fun getDripDropzAssets(userId: UserId): List<AllocatedNativeAsset> =
getAssetsByUserId(userId) { address ->
dripDropzRepository
.checkAvailableTokens(address)
.map {
nativeAsset {
policy = it.tokenPolicy
name = it.tokenAssetId
amount = it.availableQuantity
.movePointRight(6)
.toBigInteger()
.toString()
}
}
}.mergeAmounts()
}

private suspend fun getAssetsByUserId(
userId: UserId,
getAssetsByAddress: suspend (String) -> List<NativeAsset>
): List<AllocatedNativeAsset> =
newSuspendedTransaction {
WalletConnectionEntity.getAllByUserIdAndWalletChain(userId, WalletChain.Cardano).toList()
}.map { connection ->
connection.id.value to getAssetsByAddress(connection.address)
}.flatMap { (connectionId, assets) ->
assets.map { asset ->
Triple(asset.policy + asset.name, asset, connectionId to asset.amount.toLong())
}
}.groupBy { it.first }
.map { (_, group) ->
AllocatedNativeAsset(
asset = group.first().second,
allocations = group
.map { it.third }
.groupBy({ it.first }, { it.second })
.mapValues { (_, amounts) -> amounts.sum() }
)
}

private suspend fun getAssetMetadata(asset: NativeAsset): List<LedgerAssetMetadataItem> =
client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class EthereumNftSong(
val tokenType: String,
val tokenId: String,
val amount: Long,
val allocations: Map<@Contextual UUID, Long>,
val title: String,
val imageUrl: String,
val audioUrl: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,16 @@ private val MOOD_TRAIT_TYPES = listOf(

private val AUDIO_URL_REGEX = "\\.(mp3|mp4|wav|flac|aiff|aac)$".toRegex(RegexOption.IGNORE_CASE)

fun EthereumNft.parseSong(): EthereumNftSong? {
fun EthereumNft.parseSong(allocations: Map<UUID, Long>): EthereumNftSong? {
val audioUrl = (raw?.metadata?.animationUrl ?: animation?.cachedUrl ?: animation?.originalUrl)?.asHttpsUrl()
return if (audioUrl != null && (audioUrl.matches(AUDIO_URL_REGEX) || animation?.contentType?.startsWith("audio/") == true)) {
EthereumNftSong(
id = UUID.nameUUIDFromBytes(contract.address.toByteArray() + tokenId.toByteArray()),
contractAddress = contract.address,
tokenType = tokenType,
tokenId = tokenId,
amount = balance,
amount = allocations.values.sum(),
allocations = allocations,
title = trait(TITLE_TRAIT_TYPES) ?: raw?.metadata?.name ?: contract.name ?: name.orEmpty(),
imageUrl = (raw?.metadata?.image ?: image?.cachedUrl ?: image?.originalUrl)?.asHttpsUrl().orEmpty(),
audioUrl = audioUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.http.ContentType
import io.ktor.server.application.ApplicationEnvironment
import io.newm.server.features.ethereum.model.EthereumNft
import io.newm.server.features.ethereum.model.EthereumNftSong
import io.newm.server.features.ethereum.model.GetNftsByOwnerResponse
import io.newm.server.features.ethereum.parser.parseSong
Expand All @@ -18,7 +17,7 @@ import io.newm.server.ktx.checkedBody
import io.newm.server.ktx.getSecureConfigString
import io.newm.server.typealiases.UserId
import io.newm.shared.ktx.getConfigString
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

internal class EthereumRepositoryImpl(
private val client: HttpClient,
Expand All @@ -29,24 +28,36 @@ internal class EthereumRepositoryImpl(
override suspend fun getWalletNftSongs(userId: UserId): List<EthereumNftSong> {
logger.debug { "getWalletNftSongs: userId = $userId" }

val addresses = transaction {
WalletConnectionEntity.getAllByUserIdAndWalletChain(userId, WalletChain.Ethereum).map { it.address }
val connections = newSuspendedTransaction {
WalletConnectionEntity.getAllByUserIdAndWalletChain(userId, WalletChain.Ethereum).toList()
}
val apiUrl = environment.getConfigString("alchemy.apiUrl")
val apiKey = environment.getSecureConfigString("alchemy.apiKey")
val endpointUrl = "$apiUrl/nft/v3/$apiKey/getNFTsForOwner"
return addresses.flatMap { address ->
client
.get(endpointUrl) {
retry {
maxRetries = 2
delayMillis { 500L }
}
accept(ContentType.Application.Json)
parameter("owner", address)
}.checkedBody<GetNftsByOwnerResponse>()
.ownedNfts
.mapNotNull(EthereumNft::parseSong)
}
return connections
.map { connection ->
connection.id.value to client
.get(endpointUrl) {
retry {
maxRetries = 2
delayMillis { 500L }
}
accept(ContentType.Application.Json)
parameter("owner", connection.address)
}.checkedBody<GetNftsByOwnerResponse>()
.ownedNfts
}.flatMap { (connectionId, nfts) ->
nfts.map { nft ->
Triple(nft.contract.address + nft.tokenId, nft, connectionId to nft.balance)
}
}.groupBy { it.first }
.mapNotNull { (_, group) ->
val nft = group.first().second
val allocations = group
.map { it.third }
.groupBy({ it.first }, { it.second })
.mapValues { (_, balances) -> balances.sum() }
nft.parseSong(allocations)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ data class NftSong(
val genres: List<String>,
val moods: List<String>,
val amount: Long,
val allocations: List<NftWalletAllocation>,
val chainMetadata: NftChainMetadata
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.newm.server.features.nftsong.model

import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.util.UUID

@Serializable
data class NftWalletAllocation(
@Contextual
val id: UUID,
val amount: Long
)
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import io.newm.server.features.ethereum.model.EthereumNftSong
import io.newm.server.features.ethereum.repo.EthereumRepository
import io.newm.server.features.nftsong.model.NftChainMetadata
import io.newm.server.features.nftsong.model.NftSong
import io.newm.server.features.nftsong.model.NftWalletAllocation
import io.newm.server.typealiases.UserId
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import java.util.UUID

internal class NftSongRepositoryImpl(
private val cardanoRepository: CardanoRepository,
Expand Down Expand Up @@ -40,6 +42,7 @@ internal class NftSongRepositoryImpl(
genres = genres,
moods = moods,
amount = amount,
allocations = allocations.mapAllocations(),
chainMetadata = NftChainMetadata.Cardano(
fingerprint = fingerprint,
policyId = policyId,
Expand All @@ -59,10 +62,19 @@ internal class NftSongRepositoryImpl(
genres = genres,
moods = moods,
amount = amount,
allocations = allocations.mapAllocations(),
chainMetadata = NftChainMetadata.Ethereum(
contractAddress = contractAddress,
tokenType = tokenType,
tokenId = tokenId
)
)

private fun Map<UUID, Long>.mapAllocations(): List<NftWalletAllocation> =
this.map {
NftWalletAllocation(
id = it.key,
amount = it.value
)
}
}
Loading