|
@@ -1,21 +1,15 @@
|
|
package inn.ocsf.bee.freigeld.core.demo
|
|
package inn.ocsf.bee.freigeld.core.demo
|
|
|
|
|
|
-import com.oblac.nomen.Nomen
|
|
|
|
-import inn.ocsf.bee.freigeld.core.calc.CoinStrategy
|
|
|
|
import inn.ocsf.bee.freigeld.core.calc.CoinUtils
|
|
import inn.ocsf.bee.freigeld.core.calc.CoinUtils
|
|
-import inn.ocsf.bee.freigeld.core.data.CentralBankAccountLevel
|
|
|
|
-import inn.ocsf.bee.freigeld.core.model.*
|
|
|
|
-import inn.ocsf.bee.freigeld.core.model.data.PersonData
|
|
|
|
-import kotlinx.coroutines.*
|
|
|
|
|
|
+import inn.ocsf.bee.freigeld.core.model.BankAccount
|
|
|
|
+import inn.ocsf.bee.freigeld.core.model.CentralBank
|
|
|
|
+import inn.ocsf.bee.freigeld.core.model.GlobalEmitter
|
|
|
|
+import inn.ocsf.bee.freigeld.core.model.GlobalWorld
|
|
import org.slf4j.LoggerFactory
|
|
import org.slf4j.LoggerFactory
|
|
import org.springframework.beans.factory.annotation.Autowired
|
|
import org.springframework.beans.factory.annotation.Autowired
|
|
import org.springframework.scheduling.annotation.Scheduled
|
|
import org.springframework.scheduling.annotation.Scheduled
|
|
import org.springframework.stereotype.Service
|
|
import org.springframework.stereotype.Service
|
|
-import java.util.*
|
|
|
|
-import java.util.concurrent.CompletableFuture
|
|
|
|
import javax.annotation.PostConstruct
|
|
import javax.annotation.PostConstruct
|
|
-import kotlin.math.absoluteValue
|
|
|
|
-import kotlin.math.roundToLong
|
|
|
|
|
|
|
|
@Service
|
|
@Service
|
|
class DemoInMem {
|
|
class DemoInMem {
|
|
@@ -43,7 +37,11 @@ class DemoInMem {
|
|
emitter.emit(25, CoinValue.bi)
|
|
emitter.emit(25, CoinValue.bi)
|
|
emitter.emit(10, CoinValue.mega)
|
|
emitter.emit(10, CoinValue.mega)
|
|
*/
|
|
*/
|
|
- bank.exchange(bank.getSelfAccount(), emitter, 1_300_000)
|
|
|
|
|
|
+ CoinUtils.avaiues.flatMap { it.value }.forEach { c ->
|
|
|
|
+ emitter.emit((10..40L).random(), c.first, c.second)
|
|
|
|
+ }
|
|
|
|
+ bank.exchange(bank.getSelfAccount(), emitter, 2_300_000)
|
|
|
|
+ /*
|
|
(0 until 100).map {
|
|
(0 until 100).map {
|
|
val newHuman: NaturalPerson = PersonData.NaturalPersonImpl(Nomen.randomName())
|
|
val newHuman: NaturalPerson = PersonData.NaturalPersonImpl(Nomen.randomName())
|
|
val oldHuman = world.getPersonByIdentity(PersonIdentityFullName(newHuman.fullName))
|
|
val oldHuman = world.getPersonByIdentity(PersonIdentityFullName(newHuman.fullName))
|
|
@@ -61,12 +59,13 @@ class DemoInMem {
|
|
.thenRun({ log.info("exchange ok") })
|
|
.thenRun({ log.info("exchange ok") })
|
|
.exceptionally({ log.error("exchange error"); null })
|
|
.exceptionally({ log.error("exchange error"); null })
|
|
}
|
|
}
|
|
|
|
+ */
|
|
}
|
|
}
|
|
|
|
|
|
private fun doRand0() {
|
|
private fun doRand0() {
|
|
val personIds = world.getPersonIds()
|
|
val personIds = world.getPersonIds()
|
|
- val fromPerson = personIds.random()
|
|
|
|
- val toPerson = personIds.subtract(setOf(fromPerson)).random()
|
|
|
|
|
|
+ val fromPerson = personIds.subtract(setOf(bank.id)).random()
|
|
|
|
+ val toPerson = personIds.subtract(setOf(bank.id, fromPerson)).random()
|
|
val fromAccount = bank.getAccounts(fromPerson).first()
|
|
val fromAccount = bank.getAccounts(fromPerson).first()
|
|
val toAccout = bank.getAccounts(toPerson).first()
|
|
val toAccout = bank.getAccounts(toPerson).first()
|
|
val dir = if (fromAccount.overall >= toAccout.overall) fromAccount to toAccout else toAccout to fromAccount
|
|
val dir = if (fromAccount.overall >= toAccout.overall) fromAccount to toAccout else toAccout to fromAccount
|
|
@@ -74,9 +73,9 @@ class DemoInMem {
|
|
bank.exchange(dir.second, dir.first, x).thenRun({ log.info("exchange ok") }).exceptionally({ log.error("exchange error ${it.message}"); null })
|
|
bank.exchange(dir.second, dir.first, x).thenRun({ log.info("exchange ok") }).exceptionally({ log.error("exchange error ${it.message}"); null })
|
|
}
|
|
}
|
|
|
|
|
|
- @Scheduled(initialDelay = 5 * 1000L, fixedDelay = 500L)
|
|
|
|
|
|
+ @Scheduled(initialDelay = 30 * 1000L, fixedDelay = 2000L)
|
|
fun tick() {
|
|
fun tick() {
|
|
- doRand0()
|
|
|
|
|
|
+ //doRand0()
|
|
log.info("tick")
|
|
log.info("tick")
|
|
}
|
|
}
|
|
|
|
|
|
@@ -91,378 +90,4 @@ fun CentralBank.getSelfAccount(): BankAccount {
|
|
return this.getAccounts(this.id).first()
|
|
return this.getAccounts(this.id).first()
|
|
}
|
|
}
|
|
|
|
|
|
-@Service
|
|
|
|
-class GlobalBankDemo : CentralBankAccountLevel() {
|
|
|
|
-
|
|
|
|
- @Autowired
|
|
|
|
- private lateinit var emitter: GlobalEmitter
|
|
|
|
-
|
|
|
|
- //val accountMap = mutableMapOf<UUID, BankAccount>()
|
|
|
|
- //val personToAccountTable = mutableSetOf<Pair<UUID, UUID>>()
|
|
|
|
- val globalQueue: Deque<BankEvent> = ArrayDeque()
|
|
|
|
- val globalFutureMap = mutableMapOf<UUID, CompletableDeferred<Any>>()
|
|
|
|
- val coinToAccountMap = mutableMapOf<UUID, UUID>()
|
|
|
|
-
|
|
|
|
- private val log = LoggerFactory.getLogger(javaClass)
|
|
|
|
-
|
|
|
|
- override fun exchange(to: BankAccount, from: BankAccount, amount: Long): CompletableFuture<Any> {
|
|
|
|
- val e = ExchangeStartEvent(UUID.randomUUID(), to, from, amount)
|
|
|
|
- val ret = CompletableFuture<Any>()
|
|
|
|
- globalFutureMap.computeIfAbsent(e.id) { id ->
|
|
|
|
- val def = CompletableDeferred<Any>()
|
|
|
|
- def.invokeOnCompletion { t: Throwable? -> if (t == null) ret.complete(null) else ret.completeExceptionally(t) }
|
|
|
|
- def
|
|
|
|
- }
|
|
|
|
- globalQueue.offerLast(e)
|
|
|
|
- return ret
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- suspend fun pollLater(e: BankEvent): Boolean {
|
|
|
|
- return when (e) {
|
|
|
|
- is ExchangeStartEvent -> {
|
|
|
|
- CoinUtils.makeSomeStrategy(CoinStrategy(e.amount, if (e.from.id != emitter.id) e.from.coins else null, if (e.from.id != selfId) e.to.coins else null)).let { st ->
|
|
|
|
- if (st.res!!.isOk()) {
|
|
|
|
- st
|
|
|
|
- } else if (st.res!!.isErr()) {
|
|
|
|
- globalQueue.offerLast(ExchangeFailedEvent(UUID.randomUUID(), e, "strategy error ${st.res}"))
|
|
|
|
- null
|
|
|
|
- } else {
|
|
|
|
- TODO()
|
|
|
|
- }
|
|
|
|
- }?.let { st ->
|
|
|
|
- log.info("${selfId} do ${st.res?.name} transaction: ${e.from} -> ${e.to} = ${e.amount}")
|
|
|
|
- var fromAmount = e.from.overall
|
|
|
|
- var toAmount = e.to.overall
|
|
|
|
-
|
|
|
|
- val leftExtracted = if (st.leftExtract != null) {
|
|
|
|
- val old = e.from.extract(st.leftExtract?.map { it.id }!!)
|
|
|
|
- //emitter.accept(old) later
|
|
|
|
- old
|
|
|
|
- } else {
|
|
|
|
- listOf()
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (st.leftEmit != null) {
|
|
|
|
- val newIds = emitter.emit(st.leftEmit!!)
|
|
|
|
- val newCoins = emitter.extract(newIds)
|
|
|
|
- e.from.accept(newCoins)
|
|
|
|
- if (e.from.id != selfId) {
|
|
|
|
- emitter.free(newCoins)
|
|
|
|
- } else {
|
|
|
|
- //do nothing
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- val rightExtracted = if (st.rightExtract != null) {
|
|
|
|
- val old = e.to.extract(st.rightExtract?.map { it.id }!!)
|
|
|
|
- //emitter.accept(old) later
|
|
|
|
- old
|
|
|
|
- } else {
|
|
|
|
- listOf()
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (st.rightEmit != null) {
|
|
|
|
- val newIds = emitter.emit(st.rightEmit!!)
|
|
|
|
- if (e.from.id == emitter.id) {
|
|
|
|
- fromAmount = e.from.overall
|
|
|
|
- }
|
|
|
|
- val newCoins = emitter.extract(newIds)
|
|
|
|
- e.to.accept(newCoins)
|
|
|
|
- if (e.to.id != selfId) {
|
|
|
|
- emitter.free(newCoins)
|
|
|
|
- } else {
|
|
|
|
- //do nothing
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (st.rightCashback != null) {
|
|
|
|
- val localCoins = leftExtracted.map { it.id to it }.toMap().toMutableMap()
|
|
|
|
- val externalCoins = emitter.extract(st.rightCashback?.filterNot { localCoins.containsKey(it.id) }?.map { it.id }!!)
|
|
|
|
- val sameCoins = st.rightCashback?.filter { localCoins.containsKey(it.id) } ?: listOf()
|
|
|
|
- val someCoins = sameCoins + externalCoins
|
|
|
|
- e.to.accept(someCoins)
|
|
|
|
- someCoins.forEach { localCoins.remove(it.id) }
|
|
|
|
- emitter.accept(localCoins.values)
|
|
|
|
- if (e.to.id != selfId) {
|
|
|
|
- emitter.free(someCoins)
|
|
|
|
- } else {
|
|
|
|
- //do nothing
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if (st.leftCashback != null) {
|
|
|
|
- val localCoins = rightExtracted.map { it.id to it }.toMap().toMutableMap()
|
|
|
|
- val externalCoins = emitter.extract(st.leftCashback?.filterNot { localCoins.containsKey(it.id) }?.map { it.id }!!)
|
|
|
|
- val sameCoins = st.leftCashback?.filter { localCoins.containsKey(it.id) } ?: listOf()
|
|
|
|
- val someCoins = sameCoins + externalCoins
|
|
|
|
- e.from.accept(someCoins)
|
|
|
|
- someCoins.forEach { localCoins.remove(it.id) }
|
|
|
|
- emitter.accept(localCoins.values)
|
|
|
|
- if (e.from.id != selfId) {
|
|
|
|
- emitter.free(someCoins)
|
|
|
|
- } else {
|
|
|
|
- //do nothing
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- val creditOk = (fromAmount - e.amount == e.from.overall)
|
|
|
|
- val debitOk = (toAmount + e.amount == e.to.overall)
|
|
|
|
- val fromCoins = e.from.coins
|
|
|
|
- val toCoins = e.to.coins
|
|
|
|
- val uniqOk = if (fromCoins != null) {
|
|
|
|
- if (toCoins != null) {
|
|
|
|
- fromCoins.map { it.id }.toSet().intersect(toCoins.map { it.id }.toSet()).isEmpty()
|
|
|
|
- } else {
|
|
|
|
- true
|
|
|
|
- }
|
|
|
|
- } else {
|
|
|
|
- true
|
|
|
|
- }
|
|
|
|
- if (debitOk && creditOk && uniqOk) {
|
|
|
|
- true
|
|
|
|
- } else {
|
|
|
|
- TODO()
|
|
|
|
- false
|
|
|
|
- }
|
|
|
|
- }?.let { ok ->
|
|
|
|
- if (ok) {
|
|
|
|
- globalQueue.offerLast(ExchangeSuccessEvent(UUID.randomUUID(), e))
|
|
|
|
- } else {
|
|
|
|
- globalQueue.offerLast(ExchangeFailedEvent(UUID.randomUUID(), e, "exchange strategy failed"))
|
|
|
|
- }
|
|
|
|
- ok
|
|
|
|
- } ?: true
|
|
|
|
- }
|
|
|
|
- is ExchangeSuccessEvent -> {
|
|
|
|
- globalFutureMap.get(e.parentEvent.id)!!.let {
|
|
|
|
- globalFutureMap.remove(e.parentEvent.id)
|
|
|
|
- it.complete(true)
|
|
|
|
- }
|
|
|
|
- true
|
|
|
|
- }
|
|
|
|
- is ExchangeFailedEvent -> {
|
|
|
|
- globalFutureMap.get(e.parentEvent.id)?.let {
|
|
|
|
- globalFutureMap.remove(e.parentEvent.id)
|
|
|
|
- it.completeExceptionally(ExchangeFailedException(e, e.reason))
|
|
|
|
- }
|
|
|
|
- true
|
|
|
|
- }
|
|
|
|
- is BankPauseOnRecalcEvent -> !e.paused
|
|
|
|
- else -> throw IllegalArgumentException("wrong event type ${e.javaClass.name}")
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- fun poll(e: BankEvent): Boolean {
|
|
|
|
- return runBlocking {
|
|
|
|
- try {
|
|
|
|
- pollLater(e)
|
|
|
|
- } catch (t: Throwable) {
|
|
|
|
- log.error("error in event", t)
|
|
|
|
- true
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- val maxPollCount = 1024
|
|
|
|
-
|
|
|
|
- @PostConstruct
|
|
|
|
- fun start() {
|
|
|
|
- GlobalScope.launch {
|
|
|
|
- do {
|
|
|
|
- tick()
|
|
|
|
- delay(200)
|
|
|
|
- } while (true)
|
|
|
|
- }
|
|
|
|
- emitter.listen { e ->
|
|
|
|
- log.info("emitter event ${e.javaClass.name}")
|
|
|
|
- when (e) {
|
|
|
|
- is EmitterStartRecalculationEvent -> {
|
|
|
|
- globalQueue.offerLast(BankPauseOnRecalcEvent(UUID.randomUUID(), e))
|
|
|
|
- runBlocking {
|
|
|
|
- do {
|
|
|
|
- delay(500)
|
|
|
|
- } while (!(globalQueue.peekFirst() is BankPauseOnRecalcEvent))
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- is EmitterStopRecalculationEvent -> {
|
|
|
|
- accounts.map { it to it.coins?.map { it.id }?.intersect(e.nullCoins) }.filter { it.second?.isNotEmpty() ?: false }.forEach { ap ->
|
|
|
|
- val nullCoins = ap.second?.map {
|
|
|
|
- //(ap.first as DemoAccount).internalCoins.get(it)?.incEra() //TODO убрать!
|
|
|
|
- ap.first.extractOne(it)
|
|
|
|
- }?.toSet() ?: setOf()
|
|
|
|
- emitter.accept(nullCoins)
|
|
|
|
- }
|
|
|
|
- globalQueue.filter { it is BankPauseOnRecalcEvent }.map { it as BankPauseOnRecalcEvent }.first().paused = false
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- fun tick() {
|
|
|
|
- var pollCount = 0
|
|
|
|
- while (globalQueue.isNotEmpty() && pollCount < maxPollCount) {
|
|
|
|
- val e = globalQueue.element()
|
|
|
|
- if (poll(e)) {
|
|
|
|
- globalQueue.remove(e)
|
|
|
|
- }
|
|
|
|
- pollCount++
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- class DemoOwnerAccout(val inner: BankAccount, val owner: GlobalBankDemo) : BankAccount {
|
|
|
|
-
|
|
|
|
- override fun getOverall(): Long {
|
|
|
|
- return inner.overall
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getCoins(): MutableCollection<Coin>? {
|
|
|
|
- return inner.coins
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getOwnerId(): UUID {
|
|
|
|
- TODO("Not yet implemented")
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun acceptOne(coin: Coin) {
|
|
|
|
- if (owner.coinToAccountMap.containsKey(coin.id)) {
|
|
|
|
- throw RuntimeException("already owned")
|
|
|
|
- } else {
|
|
|
|
- owner.coinToAccountMap.put(coin.id, this.id)
|
|
|
|
- }
|
|
|
|
- inner.acceptOne(coin)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getId(): UUID {
|
|
|
|
- return inner.id
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun extractOne(coinId: UUID): Coin? {
|
|
|
|
- if (owner.coinToAccountMap.remove(coinId) == null) {
|
|
|
|
- throw RuntimeException("owner missing")
|
|
|
|
- }
|
|
|
|
- return inner.extractOne(coinId)
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun toString(): String {
|
|
|
|
- return "*${inner}"
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-abstract class AbstractBankEvent(val eventId: UUID) : BankEvent {
|
|
|
|
-
|
|
|
|
- override fun getId(): UUID {
|
|
|
|
- return eventId
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun equals(other: Any?): Boolean {
|
|
|
|
- if (this === other) return true
|
|
|
|
- if (javaClass != other?.javaClass) return false
|
|
|
|
-
|
|
|
|
- other as AbstractBankEvent
|
|
|
|
-
|
|
|
|
- if (eventId != other.eventId) return false
|
|
|
|
-
|
|
|
|
- return true
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun hashCode(): Int {
|
|
|
|
- return eventId.hashCode()
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-abstract class ExchangeBankEvent(eventId: UUID) : AbstractBankEvent(eventId)
|
|
|
|
-
|
|
|
|
-class ExchangeSuccessEvent(eventId: UUID, val parentEvent: ExchangeStartEvent) : ExchangeBankEvent(eventId)
|
|
|
|
-
|
|
|
|
-class ExchangeFailedEvent(eventId: UUID, val parentEvent: ExchangeStartEvent, val reason: String) : ExchangeBankEvent(eventId)
|
|
|
|
-
|
|
|
|
-class ExchangeFailedException(val event: ExchangeBankEvent, message: String = "exchange failed") : Exception(message)
|
|
|
|
-
|
|
|
|
-class ExchangeStartEvent(eventId: UUID, val to: BankAccount, val from: BankAccount, val amount: Long, var retry: Boolean = false) : ExchangeBankEvent(eventId)
|
|
|
|
-
|
|
|
|
-class BankPauseOnRecalcEvent(eventId: UUID, val emitterEvent: EmitterStartRecalculationEvent, var paused: Boolean = true) : AbstractBankEvent(eventId)
|
|
|
|
-
|
|
|
|
-
|
|
|
|
-open class DemoAccount(val accountId: UUID, val personId: UUID) : BankAccount {
|
|
|
|
-
|
|
|
|
- val internalCoins = mutableMapOf<UUID, Coin>()
|
|
|
|
- var _overall: Long? = null
|
|
|
|
-
|
|
|
|
- private val log = LoggerFactory.getLogger(javaClass)
|
|
|
|
-
|
|
|
|
- override fun getId(): UUID {
|
|
|
|
- return accountId
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun extractOne(coinId: UUID): Coin {
|
|
|
|
- return if (internalCoins.containsKey(coinId)) {
|
|
|
|
- val coin = internalCoins.remove(coinId)!!
|
|
|
|
- if (coin.current == 0L) {
|
|
|
|
- _overall = null
|
|
|
|
- log.debug("${this} leave coin ${coin.current}")
|
|
|
|
- } else {
|
|
|
|
- decOverall(coin.current)
|
|
|
|
- log.debug("${this} credit coin ${coin.current}")
|
|
|
|
- }
|
|
|
|
- coin
|
|
|
|
- } else {
|
|
|
|
- throw IllegalArgumentException("no such coin")
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getOverall(): Long {
|
|
|
|
- return if (_overall != null) {
|
|
|
|
- _overall
|
|
|
|
- } else {
|
|
|
|
- _overall = internalCoins.values.map { it.current }.onEach { if (it == 0L) throw RuntimeException("illegal value 0") }.sum()
|
|
|
|
- _overall
|
|
|
|
- } ?: 0L
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getCoins(): MutableCollection<Coin> {
|
|
|
|
- return internalCoins.values
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun getOwnerId(): UUID {
|
|
|
|
- return personId
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private fun decOverall(amount: Long) {
|
|
|
|
- if (amount == 0L) throw IllegalArgumentException("should not be 0")
|
|
|
|
- if (_overall != null) {
|
|
|
|
- _overall = _overall!! - amount
|
|
|
|
- } else {
|
|
|
|
- _overall = -amount
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- private fun incOverall(amount: Long) {
|
|
|
|
- if (amount == 0L) throw IllegalArgumentException("should not be 0")
|
|
|
|
- if (_overall != null) {
|
|
|
|
- _overall = _overall!! + amount
|
|
|
|
- } else {
|
|
|
|
- _overall = amount
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- override fun acceptOne(coin: Coin) {
|
|
|
|
- if (coin.id != null && !internalCoins.containsKey(coin.id)) {
|
|
|
|
- internalCoins.put(coin.id, coin)
|
|
|
|
- incOverall(coin.current)
|
|
|
|
- } else if (coin.id == null) {
|
|
|
|
- throw IllegalArgumentException("fake coin is not acceptable")
|
|
|
|
- } else if (internalCoins.containsKey(coin.id)) {
|
|
|
|
- throw IllegalArgumentException("same coin is not acceptable")
|
|
|
|
- }
|
|
|
|
- log.debug("${this} debit coin ${CoinUtils.sum(listOf(coin))}")
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
-
|
|
|
|
- override fun toString(): String {
|
|
|
|
- return accountId.hashCode().absoluteValue.toString(16)
|
|
|
|
- }
|
|
|
|
-}
|
|
|
|
|
|
|