solution: track balance of a ERC20 token

This commit is contained in:
Igor Artamonov
2020-07-06 19:04:45 -04:00
parent 6dbe8b96ec
commit b3c619e57a
13 changed files with 490 additions and 26 deletions

View File

@@ -90,6 +90,8 @@ dependencies {
implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion"
implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-ws:$etherjarVersion"
implementation "io.infinitape:etherjar-tx:$etherjarVersion" implementation "io.infinitape:etherjar-tx:$etherjarVersion"
implementation "io.infinitape:etherjar-contract:$etherjarVersion"
implementation "io.infinitape:etherjar-erc20:$etherjarVersion"
implementation 'org.bitcoinj:bitcoinj-core:0.15.8' implementation 'org.bitcoinj:bitcoinj-core:0.15.8'
implementation 'org.yaml:snakeyaml:1.24' implementation 'org.yaml:snakeyaml:1.24'

View File

@@ -13,7 +13,7 @@ springSecurtyVersion=5.3.2.RELEASE
reactorVersion=3.3.5.RELEASE reactorVersion=3.3.5.RELEASE
nettyVersion=4.1.49.Final nettyVersion=4.1.49.Final
# Our Libs # Our Libs
etherjarVersion=0.9.1 etherjarVersion=0.10.0
# Testing # Testing
spockVersion=1.3-groovy-2.5 spockVersion=1.3-groovy-2.5

View File

@@ -20,10 +20,7 @@ import com.fasterxml.jackson.core.Version
import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.module.SimpleModule
import io.emeraldpay.dshackle.config.CacheConfig import io.emeraldpay.dshackle.config.*
import io.emeraldpay.dshackle.config.MainConfig
import io.emeraldpay.dshackle.config.MainConfigReader
import io.emeraldpay.dshackle.config.UpstreamsConfig
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.beans.factory.annotation.Qualifier import org.springframework.beans.factory.annotation.Qualifier
@@ -103,4 +100,9 @@ open class Config(
return mainConfig.cache ?: CacheConfig() return mainConfig.cache ?: CacheConfig()
} }
@Bean
open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig {
return mainConfig.tokens ?: TokensConfig(emptyList())
}
} }

View File

@@ -64,9 +64,13 @@ class BlockchainRpc(
override fun subscribeBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> { override fun subscribeBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request -> return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.toLowerCase()
try { try {
trackAddress.find { it.isSupported(chain) }?.subscribe(request) trackAddress.find { it.isSupported(chain, asset) }?.subscribe(request)
?: Flux.error(SilentException.UnsupportedBlockchain(chain)) ?: Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(chain))
.doOnSubscribe {
log.error("Balance for $chain:$asset is not supported")
}
} catch (t: Throwable) { } catch (t: Throwable) {
log.error("Internal error during Balance Subscription", t) log.error("Internal error during Balance Subscription", t)
Flux.error<BlockchainOuterClass.AddressBalance>(IllegalStateException("Internal Error")) Flux.error<BlockchainOuterClass.AddressBalance>(IllegalStateException("Internal Error"))
@@ -77,9 +81,13 @@ class BlockchainRpc(
override fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> { override fun getBalance(requestMono: Mono<BlockchainOuterClass.BalanceRequest>): Flux<BlockchainOuterClass.AddressBalance> {
return requestMono.flatMapMany { request -> return requestMono.flatMapMany { request ->
val chain = Chain.byId(request.asset.chainValue) val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.toLowerCase()
try { try {
trackAddress.find { it.isSupported(chain) }?.getBalance(request) trackAddress.find { it.isSupported(chain, asset) }?.getBalance(request)
?: Flux.error(SilentException.UnsupportedBlockchain(chain)) ?: Flux.error<BlockchainOuterClass.AddressBalance>(SilentException.UnsupportedBlockchain(chain))
.doOnSubscribe {
log.error("Balance for $chain:$asset is not supported")
}
} catch (t: Throwable) { } catch (t: Throwable) {
log.error("Internal error during Balance Request", t) log.error("Internal error during Balance Request", t)
Flux.error<BlockchainOuterClass.AddressBalance>(IllegalStateException("Internal Error")) Flux.error<BlockchainOuterClass.AddressBalance>(IllegalStateException("Internal Error"))

View File

@@ -0,0 +1,28 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.Common
import io.infinitape.etherjar.domain.Address
import org.slf4j.LoggerFactory
import reactor.core.publisher.Flux
class EthereumAddresses {
companion object {
private val log = LoggerFactory.getLogger(EthereumAddresses::class.java)
}
fun extract(addresses: Common.AnyAddress): Flux<Address> {
return when (addresses.addrTypeCase) {
Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE ->
Flux.just(Address.from(addresses.addressSingle.address))
Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI ->
Flux.fromIterable(addresses.addressMulti.addressesList)
.map { Address.from(it.address) }
else -> {
log.error("Unsupported address type: ${addresses.addrTypeCase}")
Flux.empty()
}
}
}
}

View File

@@ -25,7 +25,7 @@ import reactor.core.publisher.Mono
*/ */
interface TrackAddress { interface TrackAddress {
fun isSupported(chain: Chain): Boolean fun isSupported(chain: Chain, asset: String): Boolean
fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>
fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance>

View File

@@ -41,8 +41,9 @@ class TrackBitcoinAddress(
private val log = LoggerFactory.getLogger(TrackBitcoinAddress::class.java) private val log = LoggerFactory.getLogger(TrackBitcoinAddress::class.java)
} }
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain, asset: String): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) return (asset == "bitcoin" || asset == "btc" || asset == "satoshi")
&& BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain)
} }
fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? { fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List<String>? {

View File

@@ -0,0 +1,132 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.BlockchainType
import io.emeraldpay.dshackle.SilentException
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.upstream.Selector
import io.emeraldpay.dshackle.upstream.ethereum.EthereumMultistream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.erc20.ERC20Token
import io.infinitape.etherjar.hex.Hex32
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.math.BigInteger
import javax.annotation.PostConstruct
@Service
class TrackERC20Address(
@Autowired private val multistreamHolder: MultistreamHolder,
@Autowired private val tokensConfig: TokensConfig
) : TrackAddress {
companion object {
private val log = LoggerFactory.getLogger(TrackERC20Address::class.java)
}
private val ethereumAddresses = EthereumAddresses()
private val tokens: MutableMap<TokenId, TokenDefinition> = HashMap()
@PostConstruct
fun init() {
tokensConfig.tokens.forEach { token ->
val chain = token.blockchain!!
val asset = token.name!!.toLowerCase()
val id = TokenId(chain, asset)
val definition = TokenDefinition(
chain, asset,
ERC20Token(Address.from(token.address))
)
tokens[id] = definition
log.info("Enable ERC20 balance for $chain:$asset")
}
}
override fun isSupported(chain: Chain, asset: String): Boolean {
return tokens.containsKey(TokenId(chain, asset.toLowerCase())) &&
BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
}
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.toLowerCase()
val tokenDefinition = tokens[TokenId(chain, asset)] ?: return Flux.empty()
return ethereumAddresses.extract(request.address)
.map { TrackedAddress(chain, it, tokenDefinition.token, tokenDefinition.name) }
.flatMap { addr -> getBalance(addr).map(addr::withBalance) }
.map { buildResponse(it) }
}
override fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
val chain = Chain.byId(request.asset.chainValue)
val asset = request.asset.code.toLowerCase()
val tokenDefinition = tokens[TokenId(chain, asset)] ?: return Flux.empty()
val head = multistreamHolder.getUpstream(chain)?.getHead()?.getFlux() ?: Flux.empty()
return ethereumAddresses.extract(request.address)
.map { TrackedAddress(chain, it, tokenDefinition.token, tokenDefinition.name) }
.flatMap { addr ->
val current = getBalance(addr)
val updates = head.flatMap { getBalance(addr) }
Flux.concat(current, updates)
.distinctUntilChanged()
.map { addr.withBalance(it) }
}
.map { buildResponse(it) }
}
fun getBalance(addr: TrackedAddress): Mono<BigInteger> {
return getUpstream(addr.chain)
.getDirectApi(Selector.empty)
.flatMap { api ->
api.read(prepareEthCall(addr.token, addr.address))
.flatMap(JsonRpcResponse::requireStringResult)
.map {
Hex32.from(it).asQuantity().value
}
}
}
fun prepareEthCall(token: ERC20Token, target: Address): JsonRpcRequest {
val call = token
.readBalanceOf(target)
.toJson()
return JsonRpcRequest("eth_call", listOf(call, "latest"))
}
fun getUpstream(chain: Chain): EthereumMultistream {
return multistreamHolder.getUpstream(chain)?.cast(EthereumMultistream::class.java)
?: throw SilentException.UnsupportedBlockchain(chain)
}
private fun buildResponse(address: TrackedAddress): BlockchainOuterClass.AddressBalance {
return BlockchainOuterClass.AddressBalance.newBuilder()
.setBalance(address.balance!!.toString(10))
.setAsset(Common.Asset.newBuilder()
.setChainValue(address.chain.id)
.setCode(address.tokenName.toUpperCase()))
.setAddress(Common.SingleAddress.newBuilder().setAddress(address.address.toHex()))
.build()
}
class TrackedAddress(val chain: Chain,
val address: Address,
val token: ERC20Token,
val tokenName: String,
val balance: BigInteger? = null
) {
fun withBalance(balance: BigInteger) = TrackedAddress(chain, address, token, tokenName, balance)
}
data class TokenId(val chain: Chain, val name: String)
data class TokenDefinition(val chain: Chain, val name: String, val token: ERC20Token)
}

View File

@@ -38,9 +38,11 @@ class TrackEthereumAddress(
) : TrackAddress { ) : TrackAddress {
private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java)
private val ethereumAddresses = EthereumAddresses()
override fun isSupported(chain: Chain): Boolean { override fun isSupported(chain: Chain, asset: String): Boolean {
return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain) return asset == "ether" &&
BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain)
} }
override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> { override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux<BlockchainOuterClass.AddressBalance> {
@@ -99,16 +101,8 @@ class TrackEthereumAddress(
if (request.asset.code?.toLowerCase() != "ether") { if (request.asset.code?.toLowerCase() != "ether") {
return Flux.error(SilentException("Unsupported asset ${request.asset.code}")) return Flux.error(SilentException("Unsupported asset ${request.asset.code}"))
} }
return when (request.address.addrTypeCase) { return ethereumAddresses.extract(request.address).map {
Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE -> TrackedAddress(chain, it)
Flux.just(createAddress(request.address.addressSingle, chain))
Common.AnyAddress.AddrTypeCase.ADDRESS_MULTI ->
Flux.fromIterable(request.address.addressMulti.addressesList)
.map { createAddress(it, chain) }
else -> {
log.error("Unsupported address type: ${request.address.addrTypeCase}")
Flux.empty()
}
} }
} }

View File

@@ -48,5 +48,7 @@ class JsonRpcRequest(
return result return result
} }
override fun toString(): String {
return String(this.toJson())
}
} }

View File

@@ -0,0 +1,113 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.Common
import io.infinitape.etherjar.domain.Address
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
class EthereumAddressesSpec extends Specification {
EthereumAddresses ethereumAddresses = new EthereumAddresses()
def "Extract single address"() {
setup:
def address = Common.AnyAddress.newBuilder()
.setAddressSingle(
Common.SingleAddress.newBuilder()
.setAddress("0xab5c66752a9e8167967685f1450532fb96d5d24f")
)
.build()
when:
def act = ethereumAddresses.extract(address)
then:
StepVerifier.create(act)
.expectNext(Address.from("0xab5c66752a9e8167967685f1450532fb96d5d24f"))
.expectComplete().verify(Duration.ofSeconds(1))
}
def "Extract single addresses passed as multi"() {
setup:
def address = Common.AnyAddress.newBuilder()
.setAddressMulti(
Common.MultiAddress.newBuilder()
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xab5c66752a9e8167967685f1450532fb96d5d24f")
)
)
.build()
when:
def act = ethereumAddresses.extract(address)
then:
StepVerifier.create(act)
.expectNext(Address.from("0xab5c66752a9e8167967685f1450532fb96d5d24f"))
.expectComplete().verify(Duration.ofSeconds(1))
}
def "Extract two addresses"() {
setup:
def address = Common.AnyAddress.newBuilder()
.setAddressMulti(
Common.MultiAddress.newBuilder()
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xab5c66752a9e8167967685f1450532fb96d5d24f")
)
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xfdb16996831753d5331ff813c29a93c76834a0ad")
)
)
.build()
when:
def act = ethereumAddresses.extract(address)
then:
StepVerifier.create(act)
.expectNext(Address.from("0xab5c66752a9e8167967685f1450532fb96d5d24f"))
.expectNext(Address.from("0xfdb16996831753d5331ff813c29a93c76834a0ad"))
.expectComplete().verify(Duration.ofSeconds(1))
}
def "Extract fes addresses"() {
setup:
def address = Common.AnyAddress.newBuilder()
.setAddressMulti(
Common.MultiAddress.newBuilder()
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xab5c66752a9e8167967685f1450532fb96d5d24f")
)
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xfdb16996831753d5331ff813c29a93c76834a0ad")
)
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0x46705dfff24256421a05d056c29e81bdc09723b8")
)
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0xadb2b42f6bd96f5c65920b9ac88619dce4166f94")
)
.addAddresses(
Common.SingleAddress.newBuilder()
.setAddress("0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852")
)
)
.build()
when:
def act = ethereumAddresses.extract(address)
then:
StepVerifier.create(act)
.expectNext(Address.from("0xab5c66752a9e8167967685f1450532fb96d5d24f"))
.expectNext(Address.from("0xfdb16996831753d5331ff813c29a93c76834a0ad"))
.expectNext(Address.from("0x46705dfff24256421a05d056c29e81bdc09723b8"))
.expectNext(Address.from("0xadb2b42f6bd96f5c65920b9ac88619dce4166f94"))
.expectNext(Address.from("0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852"))
.expectComplete().verify(Duration.ofSeconds(1))
}
}

View File

@@ -0,0 +1,178 @@
package io.emeraldpay.dshackle.rpc
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.config.TokensConfig
import io.emeraldpay.dshackle.test.EthereumUpstreamMock
import io.emeraldpay.dshackle.test.MultistreamHolderMock
import io.emeraldpay.dshackle.test.ReaderMock
import io.emeraldpay.dshackle.upstream.Multistream
import io.emeraldpay.dshackle.upstream.MultistreamHolder
import io.emeraldpay.dshackle.reader.Reader
import io.emeraldpay.dshackle.upstream.ethereum.EthereumUpstream
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest
import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse
import io.emeraldpay.grpc.Chain
import io.infinitape.etherjar.domain.Address
import io.infinitape.etherjar.erc20.ERC20Token
import io.infinitape.etherjar.hex.HexData
import io.infinitape.etherjar.rpc.json.TransactionCallJson
import reactor.core.publisher.Mono
import spock.lang.Specification
class TrackERC20AddressSpec extends Specification {
def "Init with single token"() {
setup:
MultistreamHolder ups = Mock(MultistreamHolder) {
_ * isAvailable(Chain.ETHEREUM) >> true
}
TokensConfig tokens = new TokensConfig([
new TokensConfig.Token().tap {
id = "dai"
blockchain = Chain.ETHEREUM
name = "DAI"
type = TokensConfig.Type.ERC20
address = Address.from("0x6B175474E89094C44Da98b954EedeAC495271d0F")
}
])
TrackERC20Address track = new TrackERC20Address(ups, tokens)
when:
track.init()
def act = track.tokens
def supportDai = track.isSupported(Chain.ETHEREUM, "DAI")
def supportSai = track.isSupported(Chain.ETHEREUM, "SAI")
then:
act.size() == 1
with(act.keySet().first()) {
chain == Chain.ETHEREUM
name == "dai"
}
with(act.values().first()) {
chain == Chain.ETHEREUM
name == "dai"
token != null
}
supportDai
!supportSai
}
def "Init without tokens"() {
setup:
MultistreamHolder ups = Mock(MultistreamHolder) {
_ * isAvailable(Chain.ETHEREUM) >> true
}
TokensConfig tokens = new TokensConfig([])
TrackERC20Address track = new TrackERC20Address(ups, tokens)
when:
track.init()
def act = track.tokens
def supportDai = track.isSupported(Chain.ETHEREUM, "dai")
def supportSai = track.isSupported(Chain.ETHEREUM, "sai")
then:
act.size() == 0
!supportDai
!supportSai
}
def "Init with two tokens"() {
setup:
MultistreamHolder ups = Mock(MultistreamHolder) {
_ * isAvailable(Chain.ETHEREUM) >> true
}
TokensConfig tokens = new TokensConfig([
new TokensConfig.Token().tap {
id = "dai"
blockchain = Chain.ETHEREUM
name = "DAI"
type = TokensConfig.Type.ERC20
address = Address.from("0x6B175474E89094C44Da98b954EedeAC495271d0F")
},
new TokensConfig.Token().tap {
id = "sai"
blockchain = Chain.ETHEREUM
name = "SAI"
type = TokensConfig.Type.ERC20
address = Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9")
}
])
TrackERC20Address track = new TrackERC20Address(ups, tokens)
when:
track.init()
def act = track.tokens
def supportDai = track.isSupported(Chain.ETHEREUM, "dai")
def supportSai = track.isSupported(Chain.ETHEREUM, "sai")
then:
act.size() == 2
with(act[act.keySet().find { it.name == "dai" }]) {
chain == Chain.ETHEREUM
name == "dai"
}
with(act[act.keySet().find { it.name == "sai" }]) {
chain == Chain.ETHEREUM
name == "sai"
}
supportDai
supportSai
}
def "Gets balance from upstream"() {
setup:
ReaderMock api = new ReaderMock()
.with(
new JsonRpcRequest("eth_call", [
new TransactionCallJson().tap { json ->
json.setTo(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9"))
json.setData(HexData.from("0x70a0823100000000000000000000000016c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
},
"latest"
]),
JsonRpcResponse.ok('"0x0000000000000000000000000000000000000000000000000000001f28d72868"')
)
EthereumUpstream upstream = new EthereumUpstreamMock(Chain.ETHEREUM, api)
MultistreamHolder ups = new MultistreamHolderMock(Chain.ETHEREUM, upstream)
TrackERC20Address track = new TrackERC20Address(ups, new TokensConfig([]))
TrackERC20Address.TrackedAddress address = new TrackERC20Address.TrackedAddress(
Chain.ETHEREUM,
Address.from("0x16c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"),
new ERC20Token(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9")),
"test",
BigInteger.valueOf(1234)
)
when:
def act = track.getBalance(address).block()
then:
act.toLong() == 0x1f28d72868
}
def "Builds response"() {
setup:
TrackERC20Address track = new TrackERC20Address(Stub(MultistreamHolder), new TokensConfig([]))
TrackERC20Address.TrackedAddress address = new TrackERC20Address.TrackedAddress(
Chain.ETHEREUM,
Address.from("0x16c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"),
new ERC20Token(Address.from("0x54EedeAC495271d0F6B175474E89094C44Da98b9")),
"test",
BigInteger.valueOf(1234)
)
when:
def act = track.buildResponse(address)
then:
act == BlockchainOuterClass.AddressBalance.newBuilder()
.setAddress(Common.SingleAddress.newBuilder().setAddress("0x16c15c65ad00b6dfbcc2cb8a7b6c2d0103a3883b"))
.setAsset(Common.Asset.newBuilder()
.setChain(Common.ChainRef.CHAIN_ETHEREUM)
.setCode("TEST")
)
.setBalance("1234")
.build()
}
}

View File

@@ -32,6 +32,10 @@ class ReaderMock<K, D> implements Reader<K, D> {
@Override @Override
Mono<D> read(K key) { Mono<D> read(K key) {
return Mono.justOrEmpty(mapping.get(key)) def value = mapping.get(key)
if (value == null) {
println("No mocked value for request: $key")
}
return Mono.justOrEmpty(value)
} }
} }