From b3c619e57ad6572faae275446a34e4d801a91083 Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Mon, 6 Jul 2020 19:04:45 -0400 Subject: [PATCH] solution: track balance of a ERC20 token --- build.gradle | 2 + gradle.properties | 2 +- .../kotlin/io/emeraldpay/dshackle/Config.kt | 10 +- .../emeraldpay/dshackle/rpc/BlockchainRpc.kt | 16 +- .../dshackle/rpc/EthereumAddresses.kt | 28 +++ .../emeraldpay/dshackle/rpc/TrackAddress.kt | 2 +- .../dshackle/rpc/TrackBitcoinAddress.kt | 5 +- .../dshackle/rpc/TrackERC20Address.kt | 132 +++++++++++++ .../dshackle/rpc/TrackEthereumAddress.kt | 18 +- .../upstream/rpcclient/JsonRpcRequest.kt | 4 +- .../dshackle/rpc/EthereumAddressesSpec.groovy | 113 +++++++++++ .../dshackle/rpc/TrackERC20AddressSpec.groovy | 178 ++++++++++++++++++ .../dshackle/test/ReaderMock.groovy | 6 +- 13 files changed, 490 insertions(+), 26 deletions(-) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/EthereumAddresses.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackERC20Address.kt create mode 100644 src/test/groovy/io/emeraldpay/dshackle/rpc/EthereumAddressesSpec.groovy create mode 100644 src/test/groovy/io/emeraldpay/dshackle/rpc/TrackERC20AddressSpec.groovy diff --git a/build.gradle b/build.gradle index 6249a6de..3a1f9473 100644 --- a/build.gradle +++ b/build.gradle @@ -90,6 +90,8 @@ dependencies { implementation "io.infinitape:etherjar-rpc-http:$etherjarVersion" implementation "io.infinitape:etherjar-rpc-ws:$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.yaml:snakeyaml:1.24' diff --git a/gradle.properties b/gradle.properties index 587451be..ca017e75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ springSecurtyVersion=5.3.2.RELEASE reactorVersion=3.3.5.RELEASE nettyVersion=4.1.49.Final # Our Libs -etherjarVersion=0.9.1 +etherjarVersion=0.10.0 # Testing spockVersion=1.3-groovy-2.5 diff --git a/src/main/kotlin/io/emeraldpay/dshackle/Config.kt b/src/main/kotlin/io/emeraldpay/dshackle/Config.kt index 3ec7a475..0479086d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/Config.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/Config.kt @@ -20,10 +20,7 @@ import com.fasterxml.jackson.core.Version import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.module.SimpleModule -import io.emeraldpay.dshackle.config.CacheConfig -import io.emeraldpay.dshackle.config.MainConfig -import io.emeraldpay.dshackle.config.MainConfigReader -import io.emeraldpay.dshackle.config.UpstreamsConfig +import io.emeraldpay.dshackle.config.* import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Qualifier @@ -103,4 +100,9 @@ open class Config( return mainConfig.cache ?: CacheConfig() } + @Bean + open fun tokensConfig(@Autowired mainConfig: MainConfig): TokensConfig { + return mainConfig.tokens ?: TokensConfig(emptyList()) + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt index f05388c8..516b44ad 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/BlockchainRpc.kt @@ -64,9 +64,13 @@ class BlockchainRpc( override fun subscribeBalance(requestMono: Mono): Flux { return requestMono.flatMapMany { request -> val chain = Chain.byId(request.asset.chainValue) + val asset = request.asset.code.toLowerCase() try { - trackAddress.find { it.isSupported(chain) }?.subscribe(request) - ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + trackAddress.find { it.isSupported(chain, asset) }?.subscribe(request) + ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + .doOnSubscribe { + log.error("Balance for $chain:$asset is not supported") + } } catch (t: Throwable) { log.error("Internal error during Balance Subscription", t) Flux.error(IllegalStateException("Internal Error")) @@ -77,9 +81,13 @@ class BlockchainRpc( override fun getBalance(requestMono: Mono): Flux { return requestMono.flatMapMany { request -> val chain = Chain.byId(request.asset.chainValue) + val asset = request.asset.code.toLowerCase() try { - trackAddress.find { it.isSupported(chain) }?.getBalance(request) - ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + trackAddress.find { it.isSupported(chain, asset) }?.getBalance(request) + ?: Flux.error(SilentException.UnsupportedBlockchain(chain)) + .doOnSubscribe { + log.error("Balance for $chain:$asset is not supported") + } } catch (t: Throwable) { log.error("Internal error during Balance Request", t) Flux.error(IllegalStateException("Internal Error")) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/EthereumAddresses.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/EthereumAddresses.kt new file mode 100644 index 00000000..aa51b6d0 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/EthereumAddresses.kt @@ -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
{ + 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() + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt index 8f2f060e..a841626c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackAddress.kt @@ -25,7 +25,7 @@ import reactor.core.publisher.Mono */ interface TrackAddress { - fun isSupported(chain: Chain): Boolean + fun isSupported(chain: Chain, asset: String): Boolean fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux fun subscribe(request: BlockchainOuterClass.BalanceRequest): Flux diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt index 58896632..3ac705b5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackBitcoinAddress.kt @@ -41,8 +41,9 @@ class TrackBitcoinAddress( private val log = LoggerFactory.getLogger(TrackBitcoinAddress::class.java) } - override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) + override fun isSupported(chain: Chain, asset: String): Boolean { + return (asset == "bitcoin" || asset == "btc" || asset == "satoshi") + && BlockchainType.fromBlockchain(chain) == BlockchainType.BITCOIN && multistreamHolder.isAvailable(chain) } fun allAddresses(request: BlockchainOuterClass.BalanceRequest): List? { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackERC20Address.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackERC20Address.kt new file mode 100644 index 00000000..64c9233e --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackERC20Address.kt @@ -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 = 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 { + 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 { + 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 { + 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) + +} \ No newline at end of file diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt index b953d448..c2e37e1d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/TrackEthereumAddress.kt @@ -38,9 +38,11 @@ class TrackEthereumAddress( ) : TrackAddress { private val log = LoggerFactory.getLogger(TrackEthereumAddress::class.java) + private val ethereumAddresses = EthereumAddresses() - override fun isSupported(chain: Chain): Boolean { - return BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain) + override fun isSupported(chain: Chain, asset: String): Boolean { + return asset == "ether" && + BlockchainType.fromBlockchain(chain) == BlockchainType.ETHEREUM && multistreamHolder.isAvailable(chain) } override fun getBalance(request: BlockchainOuterClass.BalanceRequest): Flux { @@ -99,16 +101,8 @@ class TrackEthereumAddress( if (request.asset.code?.toLowerCase() != "ether") { return Flux.error(SilentException("Unsupported asset ${request.asset.code}")) } - return when (request.address.addrTypeCase) { - Common.AnyAddress.AddrTypeCase.ADDRESS_SINGLE -> - 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() - } + return ethereumAddresses.extract(request.address).map { + TrackedAddress(chain, it) } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt index d49e168d..5339468e 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/rpcclient/JsonRpcRequest.kt @@ -48,5 +48,7 @@ class JsonRpcRequest( return result } - + override fun toString(): String { + return String(this.toJson()) + } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/EthereumAddressesSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/EthereumAddressesSpec.groovy new file mode 100644 index 00000000..f6a9ea0a --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/EthereumAddressesSpec.groovy @@ -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)) + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackERC20AddressSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackERC20AddressSpec.groovy new file mode 100644 index 00000000..9e1d2429 --- /dev/null +++ b/src/test/groovy/io/emeraldpay/dshackle/rpc/TrackERC20AddressSpec.groovy @@ -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() + } +} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy index 9cb51595..b1da4d99 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/ReaderMock.groovy @@ -32,6 +32,10 @@ class ReaderMock implements Reader { @Override Mono 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) } }