Skip to content

Commit

Permalink
CU-863h5ec15: Split stream token mint among collaborators
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewWestberg committed Jul 19, 2023
1 parent 98c13ec commit 603c913
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class CollaborationEntity(id: EntityID<UUID>) : UUIDEntity(id) {
songId = songId.value,
email = email,
role = role,
royaltyRate = royaltyRate,
royaltyRate = royaltyRate?.toBigDecimal(),
credited = credited,
featured = featured,
status = status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package io.newm.server.features.collaboration.database

import io.newm.server.features.collaboration.model.CollaborationStatus
import io.newm.server.features.song.database.SongTable
import io.newm.server.features.song.database.SongTable.defaultExpression
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.UUIDTable
import org.jetbrains.exposed.sql.Column
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package io.newm.server.features.collaboration.model

import io.newm.shared.serialization.LocalDateTimeSerializer
import io.newm.shared.serialization.UUIDSerializer
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.time.LocalDateTime
import java.util.UUID

Expand All @@ -16,7 +18,8 @@ data class Collaboration(
val songId: UUID? = null,
val email: String? = null,
var role: String? = null,
val royaltyRate: Float? = null,
@Contextual
val royaltyRate: BigDecimal? = null,
val credited: Boolean? = null,
val featured: Boolean? = null,
val status: CollaborationStatus? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal class CollaborationRepositoryImpl(
this.songId = EntityID(songId, SongTable)
this.email = email
this.role = collaboration.role
this.royaltyRate = collaboration.royaltyRate
this.royaltyRate = collaboration.royaltyRate?.toFloat()
collaboration.credited?.let { this.credited = it }
collaboration.featured?.let { this.featured = it }
collaboration.distributionArtistId?.let { this.distributionArtistId = it }
Expand All @@ -72,7 +72,7 @@ internal class CollaborationRepositoryImpl(
}
collaboration.email?.let { email = it.asValidUniqueEmail(this) }
collaboration.role?.let { role = it }
collaboration.royaltyRate?.let { royaltyRate = it }
collaboration.royaltyRate?.let { royaltyRate = it.toFloat() }
collaboration.credited?.let { credited = it }
collaboration.featured?.let { featured = it }
collaboration.distributionArtistId?.let { distributionArtistId = it }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import io.newm.txbuilder.ktx.toPlutusData
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.parameter.parametersOf
import org.slf4j.Logger
import java.math.BigDecimal
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

Expand All @@ -62,6 +63,22 @@ class MintingRepositoryImpl(
limit = Integer.MAX_VALUE
)
val cip68Metadata = buildStreamTokenMetadata(song, user, collabs)
val streamTokensTotal = 100_000_000L.toBigDecimal()
var streamTokensRemaining = 100_000_000L
val splitCollabs = collabs.filter { it.royaltyRate!! > BigDecimal.ZERO }.sortedByDescending { it.royaltyRate }
require(splitCollabs.sumOf { it.royaltyRate!! } == 100.toBigDecimal()) { "Collaboration royalty rates must sum to 100" }
val streamTokenSplits = splitCollabs.mapIndexed { index, collaboration ->
val collabUser = userRepository.findByEmail(collaboration.email!!)
val splitMultiplier = collaboration.royaltyRate!! / 100.toBigDecimal()
val amount = if (index < splitCollabs.lastIndex) {
// round down to nearest whole token
(streamTokensTotal * splitMultiplier).toLong()
} else {
streamTokensRemaining
}
streamTokensRemaining -= amount
Pair(collabUser.walletAddress!!, amount)
}
val paymentKey = cardanoRepository.getKey(song.paymentKeyId!!)
val mintPriceLovelace = song.mintCostLovelace.toString()
val cip68ScriptAddress = configRepository.getString(CONFIG_KEY_MINT_CIP68_SCRIPT_ADDRESS)
Expand Down Expand Up @@ -101,7 +118,8 @@ class MintingRepositoryImpl(
) { "collateral utxo not found!" }
val starterTokenUtxoReference =
configRepository.getString(CONFIG_KEY_MINT_STARTER_TOKEN_UTXO_REFERENCE).toReferenceUtxo()
val scriptUtxoReference = configRepository.getString(CONFIG_KEY_MINT_SCRIPT_UTXO_REFERENCE).toReferenceUtxo()
val scriptUtxoReference =
configRepository.getString(CONFIG_KEY_MINT_SCRIPT_UTXO_REFERENCE).toReferenceUtxo()

val signingKeys = listOfNotNull(cashRegisterKey, moneyBoxKey, paymentKey, collateralKey)

Expand All @@ -120,7 +138,7 @@ class MintingRepositoryImpl(
cip68Policy = cip68Policy,
refTokenName = refTokenName,
fracTokenName = fracTokenName,
artistWalletAddress = user.walletAddress!!,
streamTokenSplits = streamTokenSplits,
requiredSigners = signingKeys,
starterTokenUtxoReference = starterTokenUtxoReference,
mintScriptUtxoReference = scriptUtxoReference,
Expand All @@ -145,7 +163,7 @@ class MintingRepositoryImpl(
cip68Policy = cip68Policy,
refTokenName = refTokenName,
fracTokenName = fracTokenName,
artistWalletAddress = user.walletAddress,
streamTokenSplits = streamTokenSplits,
requiredSigners = signingKeys,
starterTokenUtxoReference = starterTokenUtxoReference,
mintScriptUtxoReference = scriptUtxoReference,
Expand Down Expand Up @@ -193,7 +211,7 @@ class MintingRepositoryImpl(
cip68Policy: String,
refTokenName: String,
fracTokenName: String,
artistWalletAddress: String,
streamTokenSplits: List<Pair<String, Long>>,
requiredSigners: List<Key>,
starterTokenUtxoReference: Utxo,
mintScriptUtxoReference: Utxo,
Expand Down Expand Up @@ -221,21 +239,22 @@ class MintingRepositoryImpl(
}
)

// fraction SFT output to the artist's wallet
// TODO: CU-863h5ec15 - split among collaborators
add(
outputUtxo {
address = artistWalletAddress
// lovelace = "0" auto-calculated minutxo
nativeAssets.add(
nativeAsset {
policy = cip68Policy
name = fracTokenName
amount = "100000000"
}
)
}
)
// fraction SFT output to each artist's wallet
streamTokenSplits.forEach { (artistWalletAddress, streamTokenAmount) ->
add(
outputUtxo {
address = artistWalletAddress
// lovelace = "0" auto-calculated minutxo
nativeAssets.add(
nativeAsset {
policy = cip68Policy
name = fracTokenName
amount = streamTokenAmount.toString()
}
)
}
)
}

// collect some into the moneyBox
moneyBoxAddress?.let {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import kotlinx.serialization.json.Json
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.core.parameter.parametersOf
import java.math.BigDecimal
import java.net.URL
import java.util.UUID

Expand Down Expand Up @@ -291,7 +292,7 @@ internal class SongRepositoryImpl(
CollaborationFilters(songIds = listOf(song.id!!)),
offset = 0,
limit = Int.MAX_VALUE,
).count { (it.royaltyRate ?: -1.0f) > 0.0f && it.status != CollaborationStatus.Accepted }
).count { (it.royaltyRate ?: BigDecimal.ZERO) > BigDecimal.ZERO && it.status != CollaborationStatus.Accepted }
}

override suspend fun getMintingPaymentAmountCborHex(songId: UUID, requesterId: UUID): String {
Expand All @@ -302,7 +303,7 @@ internal class SongRepositoryImpl(
filters = CollaborationFilters(songIds = listOf(songId)),
offset = 0,
limit = Int.MAX_VALUE,
).count { (it.royaltyRate ?: -1.0f) > 0.0f }
).count { (it.royaltyRate ?: BigDecimal.ZERO) > BigDecimal.ZERO }
val mintCostBase = configRepository.getLong(CONFIG_KEY_MINT_PRICE)
val minUtxo: Long = cardanoRepository.queryStreamTokenMinUtxo()
val mintCostTotal = mintCostBase + (numberOfCollaborators * minUtxo)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
package io.newm.server.serialization

import io.newm.shared.serialization.BigDecimalSerializer
import io.newm.shared.serialization.BigIntegerSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.dsl.module
import java.math.BigDecimal
import java.math.BigInteger

/**
* Anything marked as @Contextual will use the contextualSerializersModule to pick a serializer automatically.
*/
private val contextualSerializersModule = SerializersModule {
contextual(BigInteger::class, BigIntegerSerializer)
contextual(BigDecimal::class, BigDecimalSerializer)
}

val serializationModule = module {
single {
Json {
ignoreUnknownKeys = true
explicitNulls = false
serializersModule = contextualSerializersModule
}
}
}
16 changes: 14 additions & 2 deletions newm-server/src/test/kotlin/io/newm/server/BaseApplicationTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import io.newm.server.features.user.oauth.providers.GoogleUserProvider
import io.newm.server.features.user.oauth.providers.LinkedInUserProvider
import io.newm.server.ktx.asValidUrl
import io.newm.shared.auth.Password
import io.newm.shared.serialization.BigDecimalSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
Expand All @@ -44,6 +47,7 @@ import org.koin.dsl.module
import org.testcontainers.containers.PostgreSQLContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers
import java.math.BigDecimal
import java.util.UUID
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
Expand All @@ -61,7 +65,15 @@ open class BaseApplicationTests {
protected val client: HttpClient by lazy {
application.createClient {
install(ContentNegotiation) {
json()
json(
json = Json {
ignoreUnknownKeys = true
explicitNulls = false
serializersModule = SerializersModule {
contextual(BigDecimal::class, BigDecimalSerializer)
}
}
)
}
install(HttpTimeout) {
requestTimeoutMillis = 2.minutes.inWholeMilliseconds
Expand Down Expand Up @@ -159,7 +171,7 @@ open class BaseApplicationTests {
songId = EntityID(collab.songId!!, SongTable)
role = collab.role
credited = collab.credited!!
royaltyRate = collab.royaltyRate
royaltyRate = collab.royaltyRate?.toFloat()
status = CollaborationStatus.Accepted
}.id.value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class CollaborationRoutesTests : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Role",
royaltyRate = 0.5f,
royaltyRate = 0.5f.toBigDecimal(),
credited = true,
featured = true
)
Expand Down Expand Up @@ -101,7 +101,7 @@ class CollaborationRoutesTests : BaseApplicationTests() {
val collaboration2 = Collaboration(
email = "[email protected]",
role = "Role2",
royaltyRate = 2 * collaboration1.royaltyRate!!,
royaltyRate = 2.toBigDecimal() * collaboration1.royaltyRate!!,
credited = !collaboration1.credited!!,
featured = !collaboration1.featured!!,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Artwork",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -174,7 +174,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Artist",
royaltyRate = 10.0f,
royaltyRate = 10.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -183,7 +183,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Artist",
royaltyRate = 10.0f,
royaltyRate = 10.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -192,7 +192,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -201,7 +201,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -210,7 +210,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -220,7 +220,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -230,7 +230,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -240,7 +240,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -250,7 +250,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -260,7 +260,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Author (Lyrics)",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -270,7 +270,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Synthesizer",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand All @@ -280,7 +280,7 @@ class EvearaDistributionRepositoryTest : BaseApplicationTests() {
songId = songId,
email = "[email protected]",
role = "Producer",
royaltyRate = 0.0f,
royaltyRate = 0.0f.toBigDecimal(),
credited = true,
status = CollaborationStatus.Accepted,
),
Expand Down
Loading

0 comments on commit 603c913

Please sign in to comment.