From 66b9b004542b6d783f18bff0e0d9f624fe6256d0 Mon Sep 17 00:00:00 2001 From: a10zn8 Date: Thu, 2 Nov 2023 16:18:54 +0300 Subject: [PATCH] add base support of polkadot/substrate/vara chain (#332) --- .../kotlin/chainsconfig.codegen.gradle.kts | 24 +++- emerald-grpc | 2 +- .../io/emeraldpay/dshackle/BlockchainType.kt | 5 + .../org/drpc/chainsconfig/ChainsConfig.kt | 2 + .../drpc/chainsconfig/ChainsConfigReader.kt | 9 ++ foundation/src/main/resources/chains.yaml | 23 ++++ .../test/resources/configs/chains-basic.yaml | 1 + .../io/emeraldpay/dshackle/BlockchainType.kt | 22 --- .../dshackle/config/TokensConfig.kt | 2 +- .../config/context/MultistreamsConfig.kt | 3 +- .../monitoring/accesslog/AccessHandlerGrpc.kt | 47 ------- .../monitoring/accesslog/EventsBuilder.kt | 94 ------------- .../io/emeraldpay/dshackle/rpc/NativeCall.kt | 5 +- .../dshackle/rpc/NativeSubscribe.kt | 2 +- .../io/emeraldpay/dshackle/rpc/StreamHead.kt | 2 +- .../dshackle/startup/ConfiguredUpstreams.kt | 3 +- .../dshackle/upstream/CallTargetsHolder.kt | 17 ++- .../upstream/bitcoin/RemoteUnspentReader.kt | 27 +--- .../upstream/calls/DefaultPolkadotMethods.kt | 130 ++++++++++++++++++ .../ethereum/EthereumChainSpecific.kt | 11 +- .../{EthereumWsHead.kt => GenericWsHead.kt} | 66 ++------- .../upstream/ethereum/WsSubscriptions.kt | 3 +- .../upstream/ethereum/WsSubscriptionsImpl.kt | 9 +- .../subscribe/WebsocketPendingTxes.kt | 3 +- .../upstream/generic/ChainSpecific.kt | 22 ++- .../dshackle/upstream/generic/GenericHead.kt | 4 +- .../upstream/generic/GenericUpstream.kt | 2 +- .../generic/connectors/ConnectorFactory.kt | 1 - .../connectors/GenericConnectorFactory.kt | 3 - .../generic/connectors/GenericRpcConnector.kt | 9 +- .../generic/connectors/GenericWsConnector.kt | 8 +- .../dshackle/upstream/grpc/GrpcUpstreams.kt | 3 +- .../polkadot/PolkadotChainSpecific.kt | 122 ++++++++++++++++ .../starknet/StarknetChainSpecific.kt | 16 ++- .../EventsBuilderSubscribeBalanceSpec.groovy | 83 ----------- .../dshackle/test/ConnectorFactoryMock.groovy | 2 +- .../test/MultistreamHolderMock.groovy | 4 +- ...adSpec.groovy => GenericWsHeadSpec.groovy} | 44 +++--- .../ethereum/WsSubscriptionsImplSpec.groovy | 4 +- .../subscribe/WebsocketPendingTxesSpec.groovy | 3 +- .../polkadot/PolkadotChainSpecificTest.kt | 44 ++++++ .../starknet/StarknetChainSpecificTest.kt | 3 +- 42 files changed, 464 insertions(+), 425 deletions(-) create mode 100644 foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt delete mode 100644 src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultPolkadotMethods.kt rename src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/{EthereumWsHead.kt => GenericWsHead.kt} (60%) create mode 100644 src/main/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecific.kt delete mode 100644 src/test/groovy/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilderSubscribeBalanceSpec.groovy rename src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/{EthereumWsHeadSpec.groovy => GenericWsHeadSpec.groovy} (82%) create mode 100644 src/test/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecificTest.kt diff --git a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts index 2edf5238b..bdb8aab79 100644 --- a/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/chainsconfig.codegen.gradle.kts @@ -1,5 +1,6 @@ import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.config.ChainsConfig import io.emeraldpay.dshackle.config.ChainsConfigReader import io.emeraldpay.dshackle.foundation.ChainOptionsReader @@ -18,7 +19,7 @@ open class CodeGen(private val config: ChainsConfig) { builder.addEnumConstant( "UNSPECIFIED", TypeSpec.anonymousClassBuilder() - .addSuperclassConstructorParameter("%L, %S, %S, %S, %L, %L", 0, "UNSPECIFIED", "Unknown", "0x0", "BigInteger.ZERO", "emptyList()") + .addSuperclassConstructorParameter("%L, %S, %S, %S, %L, %L, %L", 0, "UNSPECIFIED", "Unknown", "0x0", "BigInteger.ZERO", "emptyList()", "BlockchainType.UNKNOWN") .build(), ) for (chain in config) { @@ -27,13 +28,14 @@ open class CodeGen(private val config: ChainsConfig) { .replace(' ', '_'), TypeSpec.anonymousClassBuilder() .addSuperclassConstructorParameter( - "%L, %S, %S, %S, %L, %L", + "%L, %S, %S, %S, %L, %L, %L", chain.grpcId, chain.code, chain.blockchain.replaceFirstChar { it.uppercase() } + " " + chain.id.replaceFirstChar { it.uppercase() }, chain.chainId, "BigInteger(\"" + chain.netVersion + "\")", "listOf(" + chain.shortNames.map { "\"${it}\"" }.joinToString() + ")", + type(chain.type) ) .build(), ) @@ -65,6 +67,7 @@ open class CodeGen(private val config: ChainsConfig) { .addParameter("chainId", String::class) .addParameter("netVersion", BigInteger::class) .addParameter("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName())) + .addParameter("type", BlockchainType::class) .build(), ) .addProperty( @@ -96,12 +99,27 @@ open class CodeGen(private val config: ChainsConfig) { PropertySpec.builder("shortNames", List::class.asClassName().parameterizedBy(String::class.asClassName())) .initializer("shortNames") .build(), - ), + ) + .addProperty( + PropertySpec.builder("type", BlockchainType::class) + .initializer("type") + .build(), + ) ).build() return FileSpec.builder("io.emeraldpay.dshackle", "Chain") .addType(chainType) .build() } + + private fun type(type: String): String { + return when(type) { + "eth" -> "BlockchainType.ETHEREUM" + "bitcoin" -> "BlockchainType.BITCOIN" + "starknet" -> "BlockchainType.STARKNET" + "polkadot" -> "BlockchainType.POLKADOT" + else -> throw IllegalArgumentException("unknown blockchain type $type") + } + } } open class ChainsCodeGenTask : DefaultTask() { diff --git a/emerald-grpc b/emerald-grpc index 931ec0ff1..3a98f870f 160000 --- a/emerald-grpc +++ b/emerald-grpc @@ -1 +1 @@ -Subproject commit 931ec0ff1e3049b40280c66154a3dcc4a51f2373 +Subproject commit 3a98f870ff6098c62297f59761514e1e3c0b7783 diff --git a/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt new file mode 100644 index 000000000..3882ffece --- /dev/null +++ b/foundation/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt @@ -0,0 +1,5 @@ +package io.emeraldpay.dshackle + +enum class BlockchainType { + UNKNOWN, BITCOIN, ETHEREUM, STARKNET, POLKADOT; +} diff --git a/foundation/src/main/kotlin/org/drpc/chainsconfig/ChainsConfig.kt b/foundation/src/main/kotlin/org/drpc/chainsconfig/ChainsConfig.kt index e043a2495..9cca90b0e 100644 --- a/foundation/src/main/kotlin/org/drpc/chainsconfig/ChainsConfig.kt +++ b/foundation/src/main/kotlin/org/drpc/chainsconfig/ChainsConfig.kt @@ -30,6 +30,7 @@ data class ChainsConfig(private val chains: List) : Iterable) : Iterable val blockchain = getValueAsString(protocol, "id") ?: throw IllegalArgumentException("Blockchain id is not defined") + val type = getValueAsString(protocol, "type") + ?: throw IllegalArgumentException("undefined type for $blockchain") val settings = mergeMappingNode(default, getMapping(protocol, "settings")) acc.plus( getList(protocol, "chains")?.let { chains -> @@ -38,6 +40,10 @@ class ChainsConfigReader( ScalarNode(Tag.STR, "blockchain", null, null, DumperOptions.ScalarStyle.LITERAL), ScalarNode(Tag.STR, blockchain, null, null, DumperOptions.ScalarStyle.LITERAL), ), + NodeTuple( + ScalarNode(Tag.STR, "type", null, null, DumperOptions.ScalarStyle.LITERAL), + ScalarNode(Tag.STR, type, null, null, DumperOptions.ScalarStyle.LITERAL), + ), ), chain.flowStyle, ), @@ -78,6 +84,8 @@ class ChainsConfigReader( val netVersion = getValueAsLong(node, "net-version")?.toBigInteger() ?: BigInteger(chainId.drop(2), 16) val shortNames = getListOfString(node, "short-names") ?: throw IllegalArgumentException("undefined shortnames for $blockchain") + val type = getValueAsString(node, "type") + ?: throw IllegalArgumentException("undefined type for $blockchain") return ChainsConfig.ChainConfig( expectedBlockTime = expectedBlockTime, syncingLagSize = lags.first, @@ -91,6 +99,7 @@ class ChainsConfigReader( shortNames = shortNames, id = id, blockchain = blockchain, + type = type ) } diff --git a/foundation/src/main/resources/chains.yaml b/foundation/src/main/resources/chains.yaml index 8b816c1c9..33fe1387d 100644 --- a/foundation/src/main/resources/chains.yaml +++ b/foundation/src/main/resources/chains.yaml @@ -641,3 +641,26 @@ chain-settings: short-names: [ astar-zkatana ] chain-id: 0x133e40 grpcId: 10035 + - id: vara + label: varanet + type: polkadot + settings: + expected-block-time: 3s + options: + validate-peers: false + lags: + syncing: 10 + lagging: 5 + chains: + - id: Mainnet + priority: 1 + code: VARA_MAINNET + short-names: [ vara ] + chain-id: 0x0 + grpcId: 1027 + - id: Testnet + priority: 1 + code: VARA_TESTMET + short-names: [ vara-testnet ] + chain-id: 0x0 + grpcId: 10036 diff --git a/foundation/src/test/resources/configs/chains-basic.yaml b/foundation/src/test/resources/configs/chains-basic.yaml index e40d9b232..8ba3e3af9 100644 --- a/foundation/src/test/resources/configs/chains-basic.yaml +++ b/foundation/src/test/resources/configs/chains-basic.yaml @@ -3,6 +3,7 @@ version: v1 chain-settings: protocols: - id: fantom + type: eth settings: expected-block-time: 10s options: diff --git a/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt b/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt deleted file mode 100644 index 8d3421a82..000000000 --- a/src/main/kotlin/io/emeraldpay/dshackle/BlockchainType.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.emeraldpay.dshackle - -enum class BlockchainType { - BITCOIN, ETHEREUM, STARKNET; - - companion object { - val bitcoin = setOf(Chain.BITCOIN__MAINNET, Chain.BITCOIN__TESTNET) - - val starknet = setOf(Chain.STARKNET__MAINNET, Chain.STARKNET__TESTNET, Chain.STARKNET__TESTNET_2) - - @JvmStatic - fun from(chain: Chain): BlockchainType { - return if (bitcoin.contains(chain)) { - BITCOIN - } else if (starknet.contains(chain)) { - STARKNET - } else { - ETHEREUM - } - } - } -} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/TokensConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/TokensConfig.kt index 4e8faadea..f24b19d3a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/TokensConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/TokensConfig.kt @@ -41,7 +41,7 @@ class TokensConfig( type == null -> type address.isNullOrBlank() -> "address" blockchain != null && - (BlockchainType.from(blockchain!!) == BlockchainType.ETHEREUM) && + (blockchain!!.type == BlockchainType.ETHEREUM) && !Address.isValidAddress(address) -> "address" else -> null } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/config/context/MultistreamsConfig.kt b/src/main/kotlin/io/emeraldpay/dshackle/config/context/MultistreamsConfig.kt index 98c77954b..b3d2afdd0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/config/context/MultistreamsConfig.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/config/context/MultistreamsConfig.kt @@ -1,6 +1,5 @@ package io.emeraldpay.dshackle.config.context -import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.cache.CachesFactory @@ -30,7 +29,7 @@ open class MultistreamsConfig(val beanFactory: ConfigurableListableBeanFactory) return Chain.entries .filterNot { it == Chain.UNSPECIFIED } .map { chain -> - if (BlockchainType.from(chain) == BITCOIN) { + if (chain.type == BITCOIN) { bitcoinMultistream(chain, cachesFactory, headScheduler) } else { genericMultistream(chain, cachesFactory, headScheduler, tracer) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/AccessHandlerGrpc.kt b/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/AccessHandlerGrpc.kt index eef4618b2..3c6a75509 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/AccessHandlerGrpc.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/AccessHandlerGrpc.kt @@ -43,14 +43,10 @@ class AccessHandlerGrpc( ): ServerCall.Listener { return when (val method = call.methodDescriptor.bareMethodName) { "SubscribeHead" -> processSubscribeHead(call, headers, next) - "SubscribeBalance" -> processSubscribeBalance(call, headers, next, true) - "SubscribeTxStatus" -> processSubscribeTxStatus(call, headers, next) - "GetBalance" -> processSubscribeBalance(call, headers, next, false) "NativeCall" -> processNativeCall(call, headers, next) "NativeSubscribe" -> processNativeSubscribe(call, headers, next) "Describe" -> processDescribe(call, headers, next) "SubscribeStatus" -> processStatus(call, headers, next) - "EstimateFee" -> processEstimateFee(call, headers, next) else -> { log.warn("unsupported method `{}`", method) next.startCall(call, headers) @@ -90,35 +86,6 @@ class AccessHandlerGrpc( ) } - @Suppress("UNCHECKED_CAST") - private fun processSubscribeBalance( - call: ServerCall, - headers: Metadata, - next: ServerCallHandler, - subscribe: Boolean, - ): ServerCall.Listener { - return process( - call, - headers, - next, - EventsBuilder.SubscribeBalance(subscribe) as EventsBuilder.RequestReply<*, ReqT, RespT>, - ) - } - - @Suppress("UNCHECKED_CAST") - private fun processSubscribeTxStatus( - call: ServerCall, - headers: Metadata, - next: ServerCallHandler, - ): ServerCall.Listener { - return process( - call, - headers, - next, - EventsBuilder.TxStatus() as EventsBuilder.RequestReply<*, ReqT, RespT>, - ) - } - @Suppress("UNCHECKED_CAST") private fun processNativeCall( call: ServerCall, @@ -175,20 +142,6 @@ class AccessHandlerGrpc( ) } - @Suppress("UNCHECKED_CAST") - private fun processEstimateFee( - call: ServerCall, - headers: Metadata, - next: ServerCallHandler, - ): ServerCall.Listener { - return process( - call, - headers, - next, - EventsBuilder.EstimateFee() as EventsBuilder.RequestReply<*, ReqT, RespT>, - ) - } - open class StdCallListener>( val next: ServerCall.Listener, val builder: EB, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilder.kt b/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilder.kt index 52216a97b..31e3f1b49 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilder.kt @@ -32,7 +32,6 @@ import java.net.InetAddress import java.net.InetSocketAddress import java.time.Duration import java.time.Instant -import java.util.Locale import java.util.UUID class EventsBuilder { @@ -244,69 +243,6 @@ class EventsBuilder { } } - class SubscribeBalance(val subscribe: Boolean) : - Base(), - RequestReply { - - private var index = 0 - private var balanceRequest: Events.BalanceRequest? = null - - override fun getT(): SubscribeBalance { - return this - } - - override fun onRequest(msg: BlockchainOuterClass.BalanceRequest) { - balanceRequest = Events.BalanceRequest( - msg.asset.code.uppercase(Locale.getDefault()), - msg.address.addrTypeCase.name, - ) - } - - override fun onReply(msg: BlockchainOuterClass.AddressBalance): Events.SubscribeBalance { - if (balanceRequest == null) { - throw IllegalStateException("Request is not initialized") - } - val addressBalance = Events.AddressBalance(msg.asset.code, msg.address.address) - val chain = Chain.byId(msg.asset.chain.number) - return Events.SubscribeBalance( - chain, - UUID.randomUUID(), - subscribe, - requestDetails, - balanceRequest!!, - addressBalance, - index++, - ) - } - } - - class TxStatus : - Base(), - RequestReply { - private var index = 0 - private var txStatusRequest: Events.TxStatusRequest? = null - - override fun onRequest(msg: BlockchainOuterClass.TxStatusRequest) { - this.txStatusRequest = Events.TxStatusRequest(msg.txId) - withChain(msg.chainValue) - } - - override fun onReply(msg: BlockchainOuterClass.TxStatus): Events.TxStatus { - return Events.TxStatus( - chain, - UUID.randomUUID(), - requestDetails, - txStatusRequest!!, - Events.TxStatusResponse(msg.confirmations), - index++, - ) - } - - override fun getT(): TxStatus { - return this - } - } - class NativeCall(private val startTs: Instant) : Base(), RequestReply { @@ -496,34 +432,4 @@ class EventsBuilder { ) } } - - class EstimateFee : - Base(), - RequestReply { - - private var mode: String = "UNKNOWN" - private var blocks: Int = 0 - - override fun getT(): EstimateFee { - return this - } - - override fun onRequest(msg: BlockchainOuterClass.EstimateFeeRequest) { - this.chain = Chain.byId(msg.chain.number) - this.mode = msg.mode.name - this.blocks = msg.blocks - } - - override fun onReply(msg: BlockchainOuterClass.EstimateFeeResponse): Events.EstimateFee { - return Events.EstimateFee( - blockchain = chain, - request = requestDetails, - id = UUID.randomUUID(), - estimateFee = Events.EstimateFeeDetails( - mode = mode, - blocks = blocks, - ), - ) - } - } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt index 3b3db8427..29eca620a 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeCall.kt @@ -19,7 +19,6 @@ package io.emeraldpay.dshackle.rpc import com.fasterxml.jackson.databind.ObjectMapper import com.google.protobuf.ByteString import io.emeraldpay.api.proto.BlockchainOuterClass -import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Global @@ -84,7 +83,7 @@ open class NativeCall( @EventListener fun onUpstreamChangeEvent(event: UpstreamChangeEvent) { multistreamHolder.getUpstream(event.chain).let { up -> - if (BlockchainType.from(up.chain) == ETHEREUM) { + if (up.chain.type == ETHEREUM) { ethereumCallSelectors.putIfAbsent( event.chain, EthereumCallSelector(up.caches), @@ -306,7 +305,7 @@ open class NativeCall( } // for ethereum the actual block needed for the call may be specified in the call parameters val callSpecificMatcher: Mono = - if (BlockchainType.from(upstream.chain) == ETHEREUM) { + if (upstream.chain.type == ETHEREUM) { ethereumCallSelectors[chain]?.getMatcher(method, params, upstream.getHead(), passthrough) } else { null diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeSubscribe.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeSubscribe.kt index 93eb16d3e..5657097e3 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeSubscribe.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/NativeSubscribe.kt @@ -57,7 +57,7 @@ open class NativeSubscribe( fun start(request: BlockchainOuterClass.NativeSubscribeRequest): Publisher { val chain = Chain.byId(request.chainValue) - if (BlockchainType.from(chain) != BlockchainType.ETHEREUM) { + if (chain.type != BlockchainType.ETHEREUM) { return Mono.error(UnsupportedOperationException("Native subscribe is not supported for ${chain.chainCode}")) } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt index a8a074372..a573b320c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/rpc/StreamHead.kt @@ -43,7 +43,7 @@ class StreamHead( .getFlux() .map { asProto(chain, it!!) } .onErrorContinue { t, _ -> - log.warn("Head subscription error: ${t.message}") + log.warn("Head subscription error", t) } } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt index 6fff9716e..c59e35427 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/startup/ConfiguredUpstreams.kt @@ -18,7 +18,6 @@ package io.emeraldpay.dshackle.startup import brave.grpc.GrpcTracing import com.google.common.annotations.VisibleForTesting -import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.BlockchainType.ETHEREUM import io.emeraldpay.dshackle.Chain @@ -135,7 +134,7 @@ open class ConfiguredUpstreams( .merge(defaultOptions[chain] ?: ChainOptions.PartialOptions.getDefaults()) .merge(up.options ?: ChainOptions.PartialOptions()) .buildOptions() - val upstream = when (BlockchainType.from(chain)) { + val upstream = when (chain.type) { BITCOIN -> { buildBitcoinUpstream( up.cast(BitcoinConnection::class.java), diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt index 10a8db830..de90454ec 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/CallTargetsHolder.kt @@ -1,10 +1,15 @@ package io.emeraldpay.dshackle.upstream -import io.emeraldpay.dshackle.BlockchainType +import io.emeraldpay.dshackle.BlockchainType.BITCOIN +import io.emeraldpay.dshackle.BlockchainType.ETHEREUM +import io.emeraldpay.dshackle.BlockchainType.POLKADOT +import io.emeraldpay.dshackle.BlockchainType.STARKNET +import io.emeraldpay.dshackle.BlockchainType.UNKNOWN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.calls.DefaultBitcoinMethods import io.emeraldpay.dshackle.upstream.calls.DefaultEthereumMethods +import io.emeraldpay.dshackle.upstream.calls.DefaultPolkadotMethods import io.emeraldpay.dshackle.upstream.calls.DefaultStarknetMethods import org.springframework.stereotype.Component @@ -17,10 +22,12 @@ class CallTargetsHolder { } private fun setupDefaultMethods(chain: Chain): CallMethods { - val created = when (BlockchainType.from(chain)) { - BlockchainType.BITCOIN -> DefaultBitcoinMethods() - BlockchainType.ETHEREUM -> DefaultEthereumMethods(chain) - BlockchainType.STARKNET -> DefaultStarknetMethods(chain) + val created = when (chain.type) { + BITCOIN -> DefaultBitcoinMethods() + ETHEREUM -> DefaultEthereumMethods(chain) + STARKNET -> DefaultStarknetMethods(chain) + POLKADOT -> DefaultPolkadotMethods() + UNKNOWN -> throw IllegalArgumentException("unknown chain") } callTargets[chain] = created return created diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/RemoteUnspentReader.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/RemoteUnspentReader.kt index 609f847bf..696448c45 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/RemoteUnspentReader.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/bitcoin/RemoteUnspentReader.kt @@ -1,22 +1,15 @@ package io.emeraldpay.dshackle.upstream.bitcoin -import io.emeraldpay.api.proto.BlockchainOuterClass.BalanceRequest import io.emeraldpay.dshackle.upstream.Capability import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.bitcoin.data.SimpleUnspent -import io.emeraldpay.dshackle.upstream.grpc.BitcoinGrpcUpstream import org.bitcoinj.core.Address -import org.slf4j.LoggerFactory import reactor.core.publisher.Mono class RemoteUnspentReader( val upstreams: BitcoinMultistream, ) : UnspentReader { - companion object { - private val log = LoggerFactory.getLogger(RemoteUnspentReader::class.java) - } - private val selector = Selector.MultiMatcher( listOf( Selector.GrpcMatcher(), @@ -27,24 +20,6 @@ class RemoteUnspentReader( override fun read(key: Address): Mono> { val apis = upstreams.getApiSource(selector) apis.request(1) - return Mono.from(apis) - .map { up -> - up.cast(BitcoinGrpcUpstream::class.java).remote - } - .flatMapMany { - val request = BalanceRequest.newBuilder() - .build() - it.getBalance(request) - } - .map { resp -> - resp.utxoList.map { utxo -> - SimpleUnspent( - utxo.txId, - utxo.index.toInt(), - utxo.balance.toLong(), - ) - } - } - .reduce(List::plus) + return Mono.empty() } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultPolkadotMethods.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultPolkadotMethods.kt new file mode 100644 index 000000000..274843dd8 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/calls/DefaultPolkadotMethods.kt @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2020 EmeraldPay, Inc + * Copyright (c) 2019 ETCDEV GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.emeraldpay.dshackle.upstream.calls + +import io.emeraldpay.dshackle.quorum.AlwaysQuorum +import io.emeraldpay.dshackle.quorum.BroadcastQuorum +import io.emeraldpay.dshackle.quorum.CallQuorum +import io.emeraldpay.etherjar.rpc.RpcException + +/** + * Default configuration for Ethereum based RPC. Defines optimal Quorum strategies for different methods, and provides + * hardcoded results for base methods, such as `net_version`, `web3_clientVersion` and similar + */ +class DefaultPolkadotMethods : CallMethods { + + private val all = setOf( + "author_pendingExtrinsics", + "author_removeExtrinsic", + "chain_getBlock", + "chain_getBlockHash", + "chain_getFinalisedHead", + "chain_getFinalizedHead", + "chain_getHead", + "chain_getHeader", + "chain_getRuntimeVersion", + "chain_subscribeAllHeads", + "chain_subscribeFinalizedHeads", + "chain_subscribeNewHeads", + "chain_subscribeRuntimeVersion", + "chain_unsubscribeAllHeads", + "chain_unsubscribeFinalisedHeads", + "chain_unsubscribeFinalizedHeads", + "chain_unsubscribeNewHead", + "chain_unsubscribeNewHeads", + "chain_unsubscribeRuntimeVersion", + "childstate_getKeys", + "childstate_getKeysPaged", + "childstate_getKeysPagedAt", + "childstate_getStorage", + "childstate_getStorageEntries", + "childstate_getStorageHash", + "childstate_getStorageSize", + "gear_calculateHandleGas", + "gear_calculateInitCreateGas", + "gear_calculateInitUploadGas", + "gear_calculateReplyGas", + "gear_readMetahash", + "gear_readState", + "gear_readStateBatch", + "gear_readStateUsingWasm", + "gear_readStateUsingWasmBatch", + "grandpa_proveFinality", + "grandpa_roundState", + "payment_queryFeeDetails", + "payment_queryInfo", + "state_call", + "state_callAt", + "state_getChildReadProof", + "state_getKeys", + "state_getKeysPaged", + "state_getKeysPagedAt", + "state_getMetadata", + "state_getPairs", + "state_getReadProof", + "state_getRuntimeVersion", + "state_getStorage", + "state_getStorageAt", + "state_getStorageHash", + "state_getStorageHashAt", + "state_getStorageSize", + "state_getStorageSizeAt", + "state_queryStorage", + "state_queryStorageAt", + "state_traceBlock", + "state_trieMigrationStatus", + "subscribe_newHead", + "system_chain", + "unsubscribe_newHead", + ) + + private val add = setOf( + "author_submitExtrinsic", + ) + + private val allowedMethods: Set = all + add + + override fun createQuorumFor(method: String): CallQuorum { + return when { + add.contains(method) -> BroadcastQuorum() + all.contains(method) -> AlwaysQuorum() + else -> AlwaysQuorum() + } + } + + override fun isCallable(method: String): Boolean { + return allowedMethods.contains(method) + } + + override fun isHardcoded(method: String): Boolean { + return false + } + + override fun executeHardcoded(method: String): ByteArray { + throw RpcException(-32601, "Method not found") + } + + override fun getGroupMethods(groupName: String): Set = + when (groupName) { + "default" -> getSupportedMethods() + else -> emptyList() + }.toSet() + + override fun getSupportedMethods(): Set { + return allowedMethods.toSortedSet() + } +} diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt index f00f0eb20..008329b0d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumChainSpecific.kt @@ -22,17 +22,22 @@ import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.springframework.cloud.sleuth.Tracer import reactor.core.publisher.Mono import reactor.core.scheduler.Scheduler object EthereumChainSpecific : ChainSpecific { - override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer { - return BlockContainer.fromEthereumJson(data.getResult(), upstreamId) + override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer { + return BlockContainer.fromEthereumJson(data, upstreamId) + } + + override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer { + return parseBlock(data, upstreamId) } override fun latestBlockRequest() = JsonRpcRequest("eth_getBlockByNumber", listOf("latest", false)) + override fun listenNewHeadsRequest(): JsonRpcRequest = JsonRpcRequest("eth_subscribe", listOf("newHeads")) + override fun localReaderBuilder( cachingReader: CachingReader, methods: CallMethods, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHead.kt similarity index 60% rename from src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt rename to src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHead.kt index 0b11eed40..fded48743 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHead.kt @@ -16,41 +16,32 @@ */ package io.emeraldpay.dshackle.upstream.ethereum -import io.emeraldpay.dshackle.Global -import io.emeraldpay.dshackle.SilentException -import io.emeraldpay.dshackle.ThrottledLogger import io.emeraldpay.dshackle.data.BlockContainer import io.emeraldpay.dshackle.reader.JsonRpcReader -import io.emeraldpay.dshackle.reader.Reader import io.emeraldpay.dshackle.upstream.BlockValidator import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.UpstreamAvailability -import io.emeraldpay.dshackle.upstream.ethereum.json.BlockJson import io.emeraldpay.dshackle.upstream.forkchoice.ForkChoice import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.GenericHead -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse -import io.emeraldpay.etherjar.domain.BlockHash -import io.emeraldpay.etherjar.rpc.json.TransactionRefJson import reactor.core.Disposable import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.publisher.Sinks import reactor.core.scheduler.Scheduler import java.time.Duration +import java.util.concurrent.atomic.AtomicInteger -class EthereumWsHead( +class GenericWsHead( forkChoice: ForkChoice, blockValidator: BlockValidator, private val api: JsonRpcReader, private val wsSubscriptions: WsSubscriptions, - private val skipEnhance: Boolean, private val wsConnectionResubscribeScheduler: Scheduler, - private val headScheduler: Scheduler, + headScheduler: Scheduler, private val upstream: DefaultUpstream, - chainSpecific: ChainSpecific, + private val chainSpecific: ChainSpecific, ) : GenericHead(upstream.getId(), forkChoice, blockValidator, headScheduler, chainSpecific), Lifecycle { private var connectionId: String? = null @@ -98,50 +89,7 @@ class EthereumWsHead( Flux.concat(it.next().doOnNext { upstream.setStatus(UpstreamAvailability.OK) }, it) } .map { - val block = Global.objectMapper.readValue(it, BlockJson::class.java) as BlockJson - if (!block.checkExtraData() && skipEnhance) { - ThrottledLogger.log(log, "$upstreamId recieved block with empty extradata through ws subscription") - } - return@map block - } - .flatMap { block -> - // newHeads returns incomplete blocks, i.e. without some fields and without transaction hashes, - // so we need to fetch the full block data - if (!skipEnhance && ( - block.difficulty == null || - block.transactions == null || - block.transactions.isEmpty() || - block.totalDifficulty == null - ) - ) { - EthereumBlockEnricher.enrich( - block.hash, - object : - Reader { - override fun read(key: BlockHash): Mono { - return api.read(JsonRpcRequest("eth_getBlockByHash", listOf(block.hash.toHex(), false))) - .flatMap { resp -> - if (resp.isNull()) { - Mono.error(SilentException("Received null for block ${block.hash}")) - } else { - Mono.just(resp) - } - } - .flatMap(JsonRpcResponse::requireResult) - .map { - val parsedBlock = BlockContainer.fromEthereumJson(it, upstreamId) - if (parsedBlock.parsed is BlockJson<*> && !parsedBlock.parsed.checkExtraData() && !skipEnhance) { - ThrottledLogger.log(log, "$upstreamId recieved block with empty extradata from block enrichment") - } - return@map parsedBlock - } - } - }, - headScheduler, - ) - } else { - Mono.just(BlockContainer.from(block)) - } + chainSpecific.parseHeader(it, "unknown") } .timeout(Duration.ofSeconds(60), Mono.error(RuntimeException("No response from subscribe to newHeads"))) .onErrorResume { @@ -158,9 +106,11 @@ class EthereumWsHead( noHeadUpdatesSink.tryEmitComplete() } + private val ids = AtomicInteger(1) + private fun subscribe(): Flux { return try { - wsSubscriptions.subscribe("newHeads") + wsSubscriptions.subscribe(chainSpecific.listenNewHeadsRequest().copy(id = ids.getAndIncrement())) .also { connectionId = it.connectionId if (!connected) { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptions.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptions.kt index 229855f17..ad8cfe235 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptions.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptions.kt @@ -15,6 +15,7 @@ */ package io.emeraldpay.dshackle.upstream.ethereum +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import reactor.core.publisher.Flux /** @@ -37,7 +38,7 @@ interface WsSubscriptions { /** * Subscribe on remote */ - fun subscribe(method: String): SubscribeData + fun subscribe(request: JsonRpcRequest): SubscribeData fun connectionInfoFlux(): Flux diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImpl.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImpl.kt index c9dabe069..8712282e0 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImpl.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImpl.kt @@ -20,7 +20,6 @@ import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import org.slf4j.LoggerFactory import reactor.core.publisher.Flux import reactor.core.publisher.Mono -import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicReference class WsSubscriptionsImpl( @@ -31,9 +30,7 @@ class WsSubscriptionsImpl( private val log = LoggerFactory.getLogger(WsSubscriptionsImpl::class.java) } - private val ids = AtomicLong(1) - - override fun subscribe(method: String): WsSubscriptions.SubscribeData { + override fun subscribe(request: JsonRpcRequest): WsSubscriptions.SubscribeData { val subscriptionId = AtomicReference("") val conn = wsPool.getConnection() val messages = conn.getSubscribeResponses() @@ -41,10 +38,10 @@ class WsSubscriptionsImpl( .filter { it.result != null } // should never happen .map { it.result!! } - val messageFlux = conn.callRpc(JsonRpcRequest("eth_subscribe", listOf(method), ids.incrementAndGet())) + val messageFlux = conn.callRpc(request) .flatMapMany { if (it.hasError()) { - log.warn("Failed to establish ETH Subscription: ${it.error?.message}") + log.warn("Failed to establish subscription: ${it.error?.message}") Mono.error(JsonRpcException(it.id, it.error!!)) } else { subscriptionId.set(it.getResultAsProcessedString()) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxes.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxes.kt index e23fc96d2..a529ddcbc 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxes.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxes.kt @@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum.subscribe import io.emeraldpay.dshackle.upstream.SubscriptionConnect import io.emeraldpay.dshackle.upstream.ethereum.EthereumEgressSubscription import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.etherjar.domain.TransactionId import org.slf4j.LoggerFactory import reactor.core.publisher.Flux @@ -33,7 +34,7 @@ class WebsocketPendingTxes( } override fun createConnection(): Flux { - return wsSubscriptions.subscribe(EthereumEgressSubscription.METHOD_PENDING_TXES) + return wsSubscriptions.subscribe(JsonRpcRequest("eth_subscribe", listOf(EthereumEgressSubscription.METHOD_PENDING_TXES))) .data .timeout(Duration.ofSeconds(60), Mono.empty()) .map { diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt index fd69eb60f..dccab9666 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/ChainSpecific.kt @@ -1,7 +1,10 @@ package io.emeraldpay.dshackle.upstream.generic -import io.emeraldpay.dshackle.BlockchainType +import io.emeraldpay.dshackle.BlockchainType.BITCOIN +import io.emeraldpay.dshackle.BlockchainType.ETHEREUM +import io.emeraldpay.dshackle.BlockchainType.POLKADOT import io.emeraldpay.dshackle.BlockchainType.STARKNET +import io.emeraldpay.dshackle.BlockchainType.UNKNOWN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.cache.Caches import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig @@ -17,8 +20,8 @@ import io.emeraldpay.dshackle.upstream.Upstream import io.emeraldpay.dshackle.upstream.UpstreamValidator import io.emeraldpay.dshackle.upstream.calls.CallMethods import io.emeraldpay.dshackle.upstream.ethereum.EthereumChainSpecific +import io.emeraldpay.dshackle.upstream.polkadot.PolkadotChainSpecific import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import io.emeraldpay.dshackle.upstream.starknet.StarknetChainSpecific import org.apache.commons.collections4.Factory import org.springframework.cloud.sleuth.Tracer @@ -30,10 +33,14 @@ typealias LocalReaderBuilder = (CachingReader, CallMethods, Head) -> Mono) -> CachingReader interface ChainSpecific { - fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer + fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer + + fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer fun latestBlockRequest(): JsonRpcRequest + fun listenNewHeadsRequest(): JsonRpcRequest + fun localReaderBuilder(cachingReader: CachingReader, methods: CallMethods, head: Head): Mono fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription @@ -51,9 +58,12 @@ object ChainSpecificRegistry { @JvmStatic fun resolve(chain: Chain): ChainSpecific { - if (BlockchainType.from(chain) == STARKNET) { - return StarknetChainSpecific + return when (chain.type) { + ETHEREUM -> EthereumChainSpecific + STARKNET -> StarknetChainSpecific + POLKADOT -> PolkadotChainSpecific + BITCOIN -> throw IllegalArgumentException("bitcoin should use custom streams implementation") + UNKNOWN -> throw IllegalArgumentException("unknown chain") } - return EthereumChainSpecific } } diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericHead.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericHead.kt index ca5326847..cb2ef1ea8 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericHead.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericHead.kt @@ -37,7 +37,9 @@ open class GenericHead( return api.read(chainSpecific.latestBlockRequest()) .subscribeOn(headScheduler) .timeout(Defaults.timeout, Mono.error(Exception("Block data not received"))) - .map { chainSpecific.parseBlock(it, upstreamId) } + .map { + chainSpecific.parseBlock(it.getResult(), upstreamId) + } .onErrorResume { err -> log.error("Failed to fetch latest block: ${err.message} $upstreamId", err) Mono.empty() diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt index 3be3e9d9e..bfa0fbb7c 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/GenericUpstream.kt @@ -49,7 +49,7 @@ open class GenericUpstream( private var validationSettingsSubscription: Disposable? = null private val hasLiveSubscriptionHead: AtomicBoolean = AtomicBoolean(false) - protected val connector: GenericConnector = connectorFactory.create(this, chain, true) + protected val connector: GenericConnector = connectorFactory.create(this, chain) private var livenessSubscription: Disposable? = null private val labelsDetector = labelsDetectorBuilder(chain, this.getIngressReader()) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/ConnectorFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/ConnectorFactory.kt index dd90e29e5..a20bfb22d 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/ConnectorFactory.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/ConnectorFactory.kt @@ -7,7 +7,6 @@ interface ConnectorFactory { fun create( upstream: DefaultUpstream, chain: Chain, - skipEnhance: Boolean, ): GenericConnector fun isValid(): Boolean diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericConnectorFactory.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericConnectorFactory.kt index f52640ec5..3d7c447e5 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericConnectorFactory.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericConnectorFactory.kt @@ -48,7 +48,6 @@ open class GenericConnectorFactory( override fun create( upstream: DefaultUpstream, chain: Chain, - skipEnhance: Boolean, ): GenericConnector { val specific = ChainSpecificRegistry.resolve(chain) if (wsFactory != null && connectorType == WS_ONLY) { @@ -57,7 +56,6 @@ open class GenericConnectorFactory( upstream, forkChoice, blockValidator, - skipEnhance, wsConnectionResubscribeScheduler, headScheduler, expectedBlockTime, @@ -74,7 +72,6 @@ open class GenericConnectorFactory( upstream, forkChoice, blockValidator, - skipEnhance, wsConnectionResubscribeScheduler, headScheduler, expectedBlockTime, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericRpcConnector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericRpcConnector.kt index 6b2732f92..165fb2370 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericRpcConnector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericRpcConnector.kt @@ -10,7 +10,7 @@ import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.IngressSubscription import io.emeraldpay.dshackle.upstream.Lifecycle import io.emeraldpay.dshackle.upstream.MergedHead -import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead +import io.emeraldpay.dshackle.upstream.ethereum.GenericWsHead import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.NoEthereumIngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool @@ -37,7 +37,6 @@ class GenericRpcConnector( upstream: DefaultUpstream, forkChoice: ForkChoice, blockValidator: BlockValidator, - skipEnhance: Boolean, wsConnectionResubscribeScheduler: Scheduler, headScheduler: Scheduler, expectedBlockTime: Duration, @@ -71,12 +70,11 @@ class GenericRpcConnector( RPC_REQUESTS_WITH_MIXED_HEAD -> { val wsHead = - EthereumWsHead( + GenericWsHead( AlwaysForkChoice(), blockValidator, getIngressReader(), WsSubscriptionsImpl(pool!!), - skipEnhance, wsConnectionResubscribeScheduler, headScheduler, upstream, @@ -97,12 +95,11 @@ class GenericRpcConnector( } RPC_REQUESTS_WITH_WS_HEAD -> { - EthereumWsHead( + GenericWsHead( AlwaysForkChoice(), blockValidator, getIngressReader(), WsSubscriptionsImpl(pool!!), - skipEnhance, wsConnectionResubscribeScheduler, headScheduler, upstream, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericWsConnector.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericWsConnector.kt index 2caa57b97..ea885a943 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericWsConnector.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/generic/connectors/GenericWsConnector.kt @@ -6,7 +6,7 @@ import io.emeraldpay.dshackle.upstream.DefaultUpstream import io.emeraldpay.dshackle.upstream.Head import io.emeraldpay.dshackle.upstream.IngressSubscription import io.emeraldpay.dshackle.upstream.ethereum.EthereumIngressSubscription -import io.emeraldpay.dshackle.upstream.ethereum.EthereumWsHead +import io.emeraldpay.dshackle.upstream.ethereum.GenericWsHead import io.emeraldpay.dshackle.upstream.ethereum.HeadLivenessValidator import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPool import io.emeraldpay.dshackle.upstream.ethereum.WsConnectionPoolFactory @@ -24,7 +24,6 @@ class GenericWsConnector( upstream: DefaultUpstream, forkChoice: ForkChoice, blockValidator: BlockValidator, - skipEnhance: Boolean, wsConnectionResubscribeScheduler: Scheduler, headScheduler: Scheduler, expectedBlockTime: Duration, @@ -32,19 +31,18 @@ class GenericWsConnector( ) : GenericConnector { private val pool: WsConnectionPool private val reader: JsonRpcReader - private val head: EthereumWsHead + private val head: GenericWsHead private val subscriptions: EthereumIngressSubscription private val liveness: HeadLivenessValidator init { pool = wsFactory.create(upstream) reader = JsonRpcWsClient(pool) val wsSubscriptions = WsSubscriptionsImpl(pool) - head = EthereumWsHead( + head = GenericWsHead( forkChoice, blockValidator, reader, wsSubscriptions, - skipEnhance, wsConnectionResubscribeScheduler, headScheduler, upstream, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt index 1cf32a483..e1f4a5ae9 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/grpc/GrpcUpstreams.kt @@ -24,7 +24,6 @@ import io.emeraldpay.api.proto.Common import io.emeraldpay.api.proto.Common.ChainRef.UNRECOGNIZED import io.emeraldpay.api.proto.ReactorAuthGrpc import io.emeraldpay.api.proto.ReactorBlockchainGrpc -import io.emeraldpay.dshackle.BlockchainType import io.emeraldpay.dshackle.BlockchainType.BITCOIN import io.emeraldpay.dshackle.Chain import io.emeraldpay.dshackle.Defaults @@ -248,7 +247,7 @@ class GrpcUpstreams( private fun getOrCreate(chain: Chain): UpstreamChangeEvent { val metrics = makeMetrics(chain) - val creator = if (BlockchainType.from(chain) != BITCOIN) { + val creator = if (chain.type != BITCOIN) { { ch: Chain, rpcClient: JsonRpcGrpcClient -> GenericGrpcUpstream( id, diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecific.kt new file mode 100644 index 000000000..714d19d34 --- /dev/null +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecific.kt @@ -0,0 +1,122 @@ +package io.emeraldpay.dshackle.upstream.polkadot + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty +import io.emeraldpay.dshackle.Chain +import io.emeraldpay.dshackle.Global +import io.emeraldpay.dshackle.config.ChainsConfig.ChainConfig +import io.emeraldpay.dshackle.data.BlockContainer +import io.emeraldpay.dshackle.data.BlockId +import io.emeraldpay.dshackle.foundation.ChainOptions.Options +import io.emeraldpay.dshackle.reader.JsonRpcReader +import io.emeraldpay.dshackle.upstream.CachingReader +import io.emeraldpay.dshackle.upstream.EgressSubscription +import io.emeraldpay.dshackle.upstream.EmptyEgressSubscription +import io.emeraldpay.dshackle.upstream.Head +import io.emeraldpay.dshackle.upstream.LabelsDetector +import io.emeraldpay.dshackle.upstream.Multistream +import io.emeraldpay.dshackle.upstream.NoopCachingReader +import io.emeraldpay.dshackle.upstream.Upstream +import io.emeraldpay.dshackle.upstream.UpstreamValidator +import io.emeraldpay.dshackle.upstream.calls.CallMethods +import io.emeraldpay.dshackle.upstream.generic.CachingReaderBuilder +import io.emeraldpay.dshackle.upstream.generic.ChainSpecific +import io.emeraldpay.dshackle.upstream.generic.GenericUpstream +import io.emeraldpay.dshackle.upstream.generic.LocalReader +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest +import org.springframework.cloud.sleuth.Tracer +import reactor.core.publisher.Mono +import reactor.core.scheduler.Scheduler +import java.math.BigInteger +import java.time.Instant + +object PolkadotChainSpecific : ChainSpecific { + override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer { + val response = Global.objectMapper.readValue(data, PolkadotBlockResponse::class.java) + + return makeBlock(response.block.header, data, upstreamId) + } + + override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer { + val header = Global.objectMapper.readValue(data, PolkadotHeader::class.java) + + return makeBlock(header, data, upstreamId) + } + + private fun makeBlock(header: PolkadotHeader, data: ByteArray, upstreamId: String): BlockContainer { + return BlockContainer( + height = header.number.substring(2).toLong(16), + hash = BlockId.from(header.parentHash), // todo + difficulty = BigInteger.ZERO, + timestamp = Instant.EPOCH, + full = false, + json = data, + parsed = header, + transactions = emptyList(), + upstreamId = upstreamId, + parentHash = BlockId.from(header.parentHash), + ) + } + + override fun latestBlockRequest(): JsonRpcRequest = + JsonRpcRequest("chain_getBlock", listOf()) + + override fun listenNewHeadsRequest(): JsonRpcRequest = + JsonRpcRequest("chain_subscribeNewHeads", listOf()) + + override fun localReaderBuilder( + cachingReader: CachingReader, + methods: CallMethods, + head: Head, + ): Mono { + return Mono.just(LocalReader(methods)) + } + + override fun subscriptionBuilder(headScheduler: Scheduler): (Multistream) -> EgressSubscription { + return { _ -> EmptyEgressSubscription } + } + + override fun makeCachingReaderBuilder(tracer: Tracer): CachingReaderBuilder { + return { _, _, _ -> NoopCachingReader } + } + + override fun validator( + chain: Chain, + upstream: Upstream, + options: Options, + config: ChainConfig, + ): UpstreamValidator? { + return null + } + + override fun labelDetector(chain: Chain, reader: JsonRpcReader): LabelsDetector? { + return null + } + + override fun subscriptionTopics(upstream: GenericUpstream): List { + return emptyList() + } +} + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PolkadotBlockResponse( + @JsonProperty("block") var block: PolkadotBlock, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PolkadotBlock( + @JsonProperty("header") var header: PolkadotHeader, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class PolkadotHeader( + @JsonProperty("parentHash") var parentHash: String, + @JsonProperty("number") var number: String, + @JsonProperty("stateRoot") var stateRoot: String, + @JsonProperty("extrinsicsRoot") var extrinsicsRoot: String, + @JsonProperty("digest") var digest: PolkadotDigest, +) + +data class PolkadotDigest( + @JsonProperty("logs") var logs: List, +) diff --git a/src/main/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecific.kt b/src/main/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecific.kt index 96ddafe08..fb1178bb1 100644 --- a/src/main/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecific.kt +++ b/src/main/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecific.kt @@ -24,7 +24,6 @@ import io.emeraldpay.dshackle.upstream.generic.ChainSpecific import io.emeraldpay.dshackle.upstream.generic.GenericUpstream import io.emeraldpay.dshackle.upstream.generic.LocalReader import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.springframework.cloud.sleuth.Tracer import reactor.core.publisher.Mono import reactor.core.scheduler.Scheduler @@ -32,9 +31,8 @@ import java.math.BigInteger import java.time.Instant object StarknetChainSpecific : ChainSpecific { - override fun parseBlock(data: JsonRpcResponse, upstreamId: String): BlockContainer { - val raw = data.getResult() - val block = Global.objectMapper.readValue(raw, StarknetBlock::class.java) + override fun parseBlock(data: ByteArray, upstreamId: String): BlockContainer { + val block = Global.objectMapper.readValue(data, StarknetBlock::class.java) return BlockContainer( height = block.number, @@ -42,7 +40,7 @@ object StarknetChainSpecific : ChainSpecific { difficulty = BigInteger.ZERO, timestamp = block.timestamp, full = false, - json = raw, + json = data, parsed = block, transactions = emptyList(), upstreamId = upstreamId, @@ -50,9 +48,17 @@ object StarknetChainSpecific : ChainSpecific { ) } + override fun parseHeader(data: ByteArray, upstreamId: String): BlockContainer { + throw NotImplementedError() + } + override fun latestBlockRequest(): JsonRpcRequest = JsonRpcRequest("starknet_getBlockWithTxHashes", listOf("latest")) + override fun listenNewHeadsRequest(): JsonRpcRequest { + throw NotImplementedError() + } + override fun localReaderBuilder( cachingReader: CachingReader, methods: CallMethods, diff --git a/src/test/groovy/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilderSubscribeBalanceSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilderSubscribeBalanceSpec.groovy deleted file mode 100644 index 79fbf85b7..000000000 --- a/src/test/groovy/io/emeraldpay/dshackle/monitoring/accesslog/EventsBuilderSubscribeBalanceSpec.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package io.emeraldpay.dshackle.monitoring.accesslog - -import io.emeraldpay.api.proto.BlockchainOuterClass -import io.emeraldpay.api.proto.Common -import io.emeraldpay.dshackle.Chain -import spock.lang.Specification - -class EventsBuilderSubscribeBalanceSpec extends Specification { - - def "Basic ethereum event"() { - setup: - def request = BlockchainOuterClass.BalanceRequest.newBuilder() - .setAddress( - Common.AnyAddress.newBuilder() - .setAddressSingle( - Common.SingleAddress.newBuilder() - .setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D") - ) - ) - .setAsset( - Common.Asset.newBuilder() - .setChainValue(100) - .setCode("ETHER") - ) - .build() - def resp = BlockchainOuterClass.AddressBalance.newBuilder() - .setAddress(Common.SingleAddress.newBuilder() - .setAddress("0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D")) - .setAsset(Common.Asset.newBuilder() - .setChainValue(100) - .setCode("ETHER")) - .setBalance("1234560000000000000") - .build() - when: - def act = new EventsBuilder.SubscribeBalance(true).tap { - it.onRequest(request) - }.onReply(resp) - then: - act.index == 0 - act.blockchain == Chain.ETHEREUM__MAINNET - act.balanceRequest.asset == "ETHER" - act.balanceRequest.addressType == "ADDRESS_SINGLE" - act.addressBalance.asset == "ETHER" - act.addressBalance.address == "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" - } - - def "Basic bitcoin event"() { - setup: - def request = BlockchainOuterClass.BalanceRequest.newBuilder() - .setAddress( - Common.AnyAddress.newBuilder() - .setAddressSingle( - Common.SingleAddress.newBuilder() - .setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s") - ) - ) - .setAsset( - Common.Asset.newBuilder() - .setChainValue(1) - .setCode("BTC") - ) - .build() - def resp = BlockchainOuterClass.AddressBalance.newBuilder() - .setAddress(Common.SingleAddress.newBuilder() - .setAddress("1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s")) - .setAsset(Common.Asset.newBuilder() - .setChainValue(1) - .setCode("BTC")) - .setBalance("12345600000000") - .build() - when: - def act = new EventsBuilder.SubscribeBalance(true).tap { - it.onRequest(request) - }.onReply(resp) - then: - act.index == 0 - act.blockchain == Chain.BITCOIN__MAINNET - act.balanceRequest.asset == "BTC" - act.balanceRequest.addressType == "ADDRESS_SINGLE" - act.addressBalance.asset == "BTC" - act.addressBalance.address == "1NDyJtNTjmwk5xPNhjgAMu4HDHigtobu1s" - } -} diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/ConnectorFactoryMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/ConnectorFactoryMock.groovy index 0af719000..598f1eb54 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/ConnectorFactoryMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/ConnectorFactoryMock.groovy @@ -23,7 +23,7 @@ class ConnectorFactoryMock implements ConnectorFactory { return true } - GenericConnector create(DefaultUpstream upstream, Chain chain, boolean skipEnhance) { + GenericConnector create(DefaultUpstream upstream, Chain chain) { return new GenericConnectorMock(api, head) } } \ No newline at end of file diff --git a/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy b/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy index 883e1f209..430aeaf16 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/test/MultistreamHolderMock.groovy @@ -42,7 +42,7 @@ class MultistreamHolderMock implements MultistreamHolder { Multistream addUpstream(@NotNull Chain chain, @NotNull Upstream up) { if (!upstreams.containsKey(chain)) { - if (BlockchainType.from(chain) == BlockchainType.ETHEREUM) { + if (chain.type == BlockchainType.ETHEREUM) { if (up instanceof GenericMultistream) { upstreams[chain] = up } else if (up instanceof GenericUpstream) { @@ -57,7 +57,7 @@ class MultistreamHolderMock implements MultistreamHolder { throw new IllegalArgumentException("Unsupported upstream type ${up.class}") } upstreams[chain].start() - } else if (BlockchainType.from(chain) == BlockchainType.BITCOIN) { + } else if (chain.type == BlockchainType.BITCOIN) { if (up instanceof BitcoinMultistream) { upstreams[chain] = up } else if (up instanceof BitcoinRpcUpstream) { diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHeadSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHeadSpec.groovy similarity index 82% rename from src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHeadSpec.groovy rename to src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHeadSpec.groovy index 1cd1bfc53..23268d8a3 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/EthereumWsHeadSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/GenericWsHeadSpec.groovy @@ -27,6 +27,7 @@ import io.emeraldpay.dshackle.upstream.forkchoice.AlwaysForkChoice import io.emeraldpay.etherjar.domain.BlockHash import io.emeraldpay.etherjar.domain.TransactionId import io.emeraldpay.etherjar.rpc.json.TransactionRefJson +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import reactor.core.publisher.Flux import reactor.core.publisher.Mono import reactor.core.publisher.Sinks @@ -38,7 +39,7 @@ import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit -class EthereumWsHeadSpec extends Specification { +class GenericWsHeadSpec extends Specification { BlockHash parent = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") DefaultUpstream upstream = new GenericUpstreamMock(Chain.ETHEREUM__MAINNET, TestingCommons.api()) @@ -50,38 +51,29 @@ class EthereumWsHeadSpec extends Specification { block.hash = BlockHash.from("0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200") block.parentHash = parent block.timestamp = Instant.now().truncatedTo(ChronoUnit.SECONDS) - block.transactions = [ - new TransactionRefJson(TransactionId.from("0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8")), - new TransactionRefJson(TransactionId.from("0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93")) - ] block.uncles = [] block.totalDifficulty = BigInteger.ONE - def headBlock = block.copy().tap { - it.transactions = null - }.with { + def headBlock = block.copy().with { Global.objectMapper.writeValueAsBytes(it) } def apiMock = TestingCommons.api() - apiMock.answerOnce("eth_getBlockByHash", ["0x3ec2ebf5d0ec474d0ac6bc50d2770d8409ad76e119968e7919f85d5ec8915200", false], block) def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> Flux.empty() } - def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, false, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def res = BlockContainer.from(block) when: def act = head.listenNewHeads().blockFirst() then: - act == BlockContainer.from(block) - act.transactions.size() == 2 - act.transactions[0].toHexWithPrefix() == "0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8" - act.transactions[1].toHexWithPrefix() == "0xebe8f22a55a9e26892a8545b93cbb2bfa4fd81c3184e50e5cf6276025bb42b93" + act == res - 1 * ws.subscribe("newHeads") >> new WsSubscriptions.SubscribeData( + 1 * ws.subscribe(_) >> new WsSubscriptions.SubscribeData( Flux.fromIterable([headBlock]), "id" ) } @@ -99,19 +91,17 @@ class EthereumWsHeadSpec extends Specification { } def apiMock = TestingCommons.api() - apiMock.answerOnce("eth_getBlockByHash", ["0x29229361dc5aa1ec66c323dc7a299e2b61a8c8dd2a3522d41255ec10eca25dd8", false], null) - apiMock.answerOnce("eth_blockNumber", [], Mono.empty()) def connectionInfoSink = Sinks.many().multicast().directBestEffort() def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() - 2 * subscribe("newHeads") >>> [ + 2 * subscribe(_) >>> [ new WsSubscriptions.SubscribeData(Flux.error(new RuntimeException()), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id") ] } - def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) when: def act = head.getFlux() @@ -159,13 +149,13 @@ class EthereumWsHeadSpec extends Specification { def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() - 2 * subscribe("newHeads") >>> [ + 2 * subscribe(_) >>> [ new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id") ] } - def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) when: def act = head.getFlux() @@ -200,12 +190,12 @@ class EthereumWsHeadSpec extends Specification { def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() - 1 * subscribe("newHeads") >>> [ + 1 * subscribe(_) >>> [ new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), ] } - def head = new EthereumWsHead( new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead( new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) when: def act = head.getFlux() @@ -239,12 +229,12 @@ class EthereumWsHeadSpec extends Specification { def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() - 1 * subscribe("newHeads") >>> [ + 1 * subscribe(_) >>> [ new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), ] } - def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) when: def act = head.getFlux() @@ -291,13 +281,13 @@ class EthereumWsHeadSpec extends Specification { def ws = Mock(WsSubscriptions) { 1 * it.connectionInfoFlux() >> connectionInfoSink.asFlux() - 2 * subscribe("newHeads") >>> [ + 2 * subscribe(_) >>> [ new WsSubscriptions.SubscribeData(Flux.fromIterable([firstHeadBlock]), "id"), new WsSubscriptions.SubscribeData(Flux.fromIterable([secondHeadBlock]), "id"), ] } - def head = new EthereumWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, true, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) + def head = new GenericWsHead(new AlwaysForkChoice(), BlockValidator.ALWAYS_VALID, apiMock, ws, Schedulers.boundedElastic(), Schedulers.boundedElastic(), upstream, EthereumChainSpecific.INSTANCE) when: def act = head.getFlux() diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImplSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImplSpec.groovy index ee129c72c..714f17263 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImplSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/WsSubscriptionsImplSpec.groovy @@ -45,7 +45,7 @@ class WsSubscriptionsImplSpec extends Specification { def ws = new WsSubscriptionsImpl(pool) when: - def act = ws.subscribe("foo_bar") + def act = ws.subscribe(new JsonRpcRequest("eth_subscribe", ["foo_bar"])) .data .map { new String(it) } .take(3) @@ -83,7 +83,7 @@ class WsSubscriptionsImplSpec extends Specification { def ws = new WsSubscriptionsImpl(pool) when: - def act = ws.subscribe("foo_bar") + def act = ws.subscribe(new JsonRpcRequest("eth_subscribe", ["foo_bar"])) .data .map { new String(it) } .take(3) diff --git a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxesSpec.groovy b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxesSpec.groovy index 34ebc89e3..4db0450fd 100644 --- a/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxesSpec.groovy +++ b/src/test/groovy/io/emeraldpay/dshackle/upstream/ethereum/subscribe/WebsocketPendingTxesSpec.groovy @@ -15,6 +15,7 @@ */ package io.emeraldpay.dshackle.upstream.ethereum.subscribe +import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcRequest import io.emeraldpay.dshackle.upstream.Selector import io.emeraldpay.dshackle.upstream.ethereum.WsSubscriptions import reactor.core.publisher.Flux @@ -40,7 +41,7 @@ class WebsocketPendingTxesSpec extends Specification { .collectList().block(Duration.ofSeconds(1)) then: - 1 * ws.subscribe("newPendingTransactions") >> new WsSubscriptions.SubscribeData( + 1 * ws.subscribe(new JsonRpcRequest("eth_subscribe", ["newPendingTransactions"])) >> new WsSubscriptions.SubscribeData( Flux.fromIterable(responses), "id" ) txes.collect {it.toHex() } == [ diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecificTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecificTest.kt new file mode 100644 index 000000000..7327b1a85 --- /dev/null +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/polkadot/PolkadotChainSpecificTest.kt @@ -0,0 +1,44 @@ +package io.emeraldpay.dshackle.upstream.polkadot + +import io.emeraldpay.dshackle.data.BlockId +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test + +val example = """ +{ + "block": { + "header": { + "parentHash": "0xb52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53", + "number": "0x1121bbc", + "stateRoot": "0x66835be7bb9d6e698a6785f3c03215d468fa1feddce7b3c8c97e8313df7207ca", + "extrinsicsRoot": "0x0103947815e820645daee747c608219f5745b0de688955a46816a7f016fc661f", + "digest": { + "logs": [ + "0x0642414245b50103f9000000722de01000000000d871116ec87ad9b8b45d73ccb9d1b7ba5fd11507dade591dcfa2559948b8314cbc20a9e0cfeedfafbb01dd9778bf3a57e426c83a668e79917952110fef280b01d452047028f36edb8a2e4983cc1207b06e638db5736a49203dcb678b882f1e0f", + "0x05424142450101a056f81200f7ce9a09ffd3342648c54f8ecdf8045436ad27388947a41f681d33c2da0990c313b04d106b76d515e38f36540fb860927caf6f9f7c7e3d7233a88a" + ] + } + }, + "extrinsics": [ + "0x280403000be01f29868b01", + "0x72550200043600a504ece20ae54b29290000000000005ae4035c2999ca91c8c186d1679a7af442e8246b64cf0ee62f3661340177cf10cc9a1b4a3e4d0798e9f5db590ffb4ef041aaac5a4faadfb34ce71cb5247aec83ece20ae54b29290000010000007eba6f2771cbe2839fcbed5fcb2ea782c70df8746c48a8ff826236fecdbe1c298ffb906751e3cd12c92a00d409b408068fac04bb50357650670b7c7f4858ac87ece20ae54b29290000020000002cb6730d852f12bd00e7f69853e01ea983147ae42d1d7ffbcf97c340b3bf351443bf72566fcfbdd3d5c78e348cab50116b6f09141628f5285b32a6cf75ef0e86ece20ae54b292900000300000000c2607a0eebc621e0117f284f9eae3e7a83d01de8cc8d6d24b75d6141b8231bedd35ee8794b2b5fbda9fe9cbe739f388f29d15a36a8135af42839ca851f9a89ec000000000000000004000000ee9bc14b97debaf90ed50dbdfeecc3cde4bf5fc7db3468c5ec5599073da07c6b6bf801e4f1c5ba4c7d70e6b2ae93a3218e51fe7533caa54b11a50763fb31d687ece20ae54b2929000005000000a805821c8ad4670079633dae9e98414f1b8995ba16825c81f551506c1f65c874e49c0718cbc1446ac6a3936ef4f3435a55b5dbf7463ab0efa788f1f4854dee83ece20ae54b29290000060000000a53d894dc327d6587b5138e9e1af0314cb56d0559abb99a1c0aa6ec617ccd412f2cd0dbf5fc4e82312b10c416099495f2cf553cf5be9d3694b27a4c96eee981ece20ae54b2929000007000000d66762120577e776db428381127b903ace080e158dbfed0a57ab6a4c8262cf088d47be09d3c2fca8a800aa9fae5b6ddcc63bd85288f6a9209243ebbf71b64484ece20ae54b29290000080000004682807d59ff1dc8a45620434d7cdf07998baf83627d2d6c1bc7c1e12a8c1f0c1f41cb3c9f0f28d5d0e6c669926185381f2a53464df3beaa23d85830bae4338cec000000000000000009000000fa1c56a894eee09b8d510aa354cd85e4a9b1a69232b9eb5ae12d71ac4112532c5d03f4e2346adf7548706a4d3234b09a44f352eb44186fa86f4bfa8465e87780ece20ae54b292900000a000000a055de10eb5eacb03daec981b8a163896d67865c874de455647596efe451064c451fa30845291882952157edf337ce90de798151540301e3b3ce10eb6cedc88cece20ae54b292900000b000000f2502e4c5820580f0a3803604f0b5f419566ed97d51c275878aac4f1ddd58776fbd707909f174075169a9c8c0558434823e5a34e8ab1f7977f42e48b94490b88ece20ae54b292900000c000000b09e67470f579b3b569f53d80b0646d335c369a50485d13e5f1e00a169c2d943312c8ee3afbce7f0e202746b5ba3ee2ef0f8bab32de244875a4db7f2ada80b88ece20ae54b292900000d000000b22f43df3e5f6bf728fad820bd9e88c1589b20e0ca121e06656b37129f707f6d2e1228c96d0cca6ff29c477c9b7b3c31ede71d822d7b9bdb17b7072de4d1c38eece20ae54b292900000e000000d2ac176667478b294ed5dcd41b8c0b13678bd5ba50c73dbe04b9de29ab97b27d45030cfef9891778f8b6b4e57502bd43306ff58f3d370e5d148c6e31e0109b8dec02000000000000000f0000005006da590eaaddeefa5a611e117c1bd6baa4a8d8e013c556c756ca02e7497e4d09873f1f77741d985493c142f50d30e3c44b4b17378c279e86c7b72d99ddb985ec0200a0000000000010000000f6cb71632f8a8db5a69967f700263dd1063a7b4c6a2f4cd449cb81861ad9d90a17701d3f13d8175fe924ddc1b17b2e71bbde654cf6d3292726817b1000a02a80ec00000000000000001100000058bf67ad6b000a143d505227700d17ff2b7e12aa2e4accb187f7c9bf9d320d483b3c5a353326eee83fc0cf34698186f01f3aaadad4682e41c1d70b1b111fd78cece20ae54b29290000120000003a69bbc0835107a528d43ae7690efd825e5e634e2143c0723cced78839801a616ebbaa95ed2458e2473ccd7235e66de494b9084efe379012dceb572d794d1a8cece20ae54b2929000013000000b8748a3bdfd8f7aed974a23afe4032aadb930dbfba6bbbf37f303d70b3448953007a4b1ec95113ca05386934dd6601d4c723367d414a12676e363b7dc6ec4d87ece20ae54b2929000014000000d22185ae3b35e4e7e8aecf1a84dc089fa143cc16b1027462ac0a523921a3590c21cf754cd119edc590de68df4e3b98eb510bcb7f5429feceebdaba6ede21058eece20ac54b2929000015000000fcc0c765ec33113c8e642ee6f15ffe4dda7e98a0ccb149f329972c877c56eb0ee338a04c0626dccf4284230955b0ccf29299799f9eb96f4bf53fcf2b661fad8bece20ae54b29290000160000002ac1b420943d0aca653e7526c87c06d7a3d17bd543420627cf47d74d6d44290fb9e7025ffbac4122f19c20528a051b5b5d3cbc4e93ad975e174d128028aaa586ece20ae54b29290000170000000cacaa86d4e6ca391c2a89b062294f9fc983fab1ec8e4bec6a64c8b614d94879dd7cfa289c60d2f9309dc33b15a5922930891a0e77ce660f83d9f5f5f49f5987ece20ae54b2929000018000000baec02444ac17b116f68c54376d7d43f4d74e5ecae4aff99311b9ef2527d8b551df71bc7de79b86126c00db74f04a935d6085a2d09015c3a17355cc675dceb8dece20ae54b2929000019000000a2103c230b8519c9280f03a78f198d1e0e842b369ec5ddf418b6d5b033867a6f6ea4a7f25bd6007d3a6e0a408c2a6db6844c2edd3707eeb915093ecb1602548fece20ae54b292900001a00000058df952db0def7e53f71f11c2c4e4b4d18045e02ec319af158be91437c02864848627d1ccff5732c4fa987557d96a24f2abe3c245ebbd7c3e5bf49b45554db8aece20ae54b292900001b0000003a83a1b63277540e25d5a350250af342c536f6fd156038301756b97100e7a1410c87845ba3ac943e452e25d9de6718f9ae3fcf7c5beb6cb2467d4cb96983cb8dec00000000000000001c000000d6ed12a6ecf1f6053130a1612a69da15504dc464f041393805a74c9bc462893c73b97a5b6ab96add4d16528f956886b3bc31e414909bec275a0617ad4c4d6a81ece20ae54b292900001d00000000137475aa822c700f46a1c6c324c9f8c9169644b25c16b1e3d9db1a4af3f541336ef8a594bf8392413d2fdcc3682ca33c065b5d92d8114bbbc50ffdd194408fec00000000000000001e0000004ea6861831d5c98d0d62766700aa9e83b82237f2a708579c8a43913ada11c172065444ab09a03d7ccc60329d16d4f86d7016460f892517036b7609f63f6d9781ece20ae54b292900001f00000076419170b21a0756ae2b801bcf7de734c5ed8bf9b88dc2fa2c68d2de103a06788360be32de6204b516a44df51fd1af71f9128c6215966a61ccceb75c94a80b80ece20ae54b2929000020000000026f5e93b0f9fd0e61e69343e4300e6691eba037b46a224fce14e4989361786c205a9b08d02d2ad1f81cd0a4040f17cce6afd418dc1709394ce022b7d379b680ece20ae54b2929000021000000e81c937007ca780d68cae29b8e54a082b93c0bb1b262410ee673a71261701e49874d2bf96565adedd255ee4b0bd9d3f90dee887544485e8c6e8db522ba3c7f83ece20ae54b2929000022000000d6f7dcd64d50fcf726630feee305364d01f6d49a69465e17e10e9b67c766117853eb24b88888c47e20cb45eca492ca68da23c9cb1412f98ed7c7c4b75965f781ece20ae54b2929000023000000487085e60aacfe67bd97216bdff15eae8ea0eb94559cf2d17f5397b2a2f97e6e4ac515053a2d5d40eabfc29d016ea2f727ba1bd5349fb8c41584b632dc1a7b86ece20ae44b2929000024000000805dbce128c3c8116af692fe99faf4ca0e0cceb09bc4159bfad69c20131f0960d1c192c76112048d81a7192abc1777baf414151caf8fa693ff33be75602bc982ec40000000000000002500000088186a1a0f94a45e997a5ee33f6792214dc83535d43e58a686224122e0c1800bc18361cb81b84af8dc397bc15c52ae1f6fbd186d0d23b008d086b43f7b43b18aece20ae54b29290000260000003e578b9efa9c17c45e37d4231011e40b8d0fc1d28da24d4ebb4ab4a785d3fd7139f1c6f20041d0db51765e69857c83a8670f901864f4f104ff62a6bafef96685ece20ae54b29290000270000003e06674e4ca9fae9da89fdb3d8988073246212c98777f6a2262db18df549c45a153750fc2493cb3d516640cb8ed5eef44161a037524db94b25f4d154962d6580ece20ae54b2929000028000000b4e80cbf2ccfda4c2e6133e4340e3d82d44487cc5f4de390f1c88c18fac42158d689ae542a62d9c9d04b67428f00eb6772257740b29a1d92d30346c77172728eece20ae54b2929000029000000d82f5200ef05e219d138486889e673566c1aec2a60311bc63bca24dee1512864eeb413de5ceeabd01243db9f7d741a4dca83c64fbf3016f73a544c8874284387ec80000000000000002a000000e8c91fd658752a003c6fb093e634f5ff6dbf92682a3bcae4ef4b4703381f184b3e99d402e3864f84343c6b1ed12a096986a76c78da99191c519d423cfeb71388ec80000000000000002b000000d633208f82b7602a3349b7631bd6984194fe9a231d204578129d9c5ef1ce7256d25d4884352d8e0f9f6a96aaf39ca26dba3b54dee0d8bf7b9169052bf33a8e8bece20ae54b292900002c000000341b4ab2fed7e6f591c6e73ce1cb8b3640cee094f3b89100be14127bb60b63165f2db8d149cde398818a25eead6d34625490b1247965914a4482f2d55832fa8fece20ae54b292900002d0000005cbc9d49cf014c70b30c8585833751d55228b078b125b91ab72676df309e5e391a57bfaea9e9d3f7729adc345787551823a96e921eed781e37453487ed3c9b8dece20ae54b292900002e0000005251107bd5ff56e036780c53718a10f34048c9b36af2202ce600e8c62c75b80f07a6307fbef006a62e1d404e7a5b64852ddc03e45f4d5a5b54f7f6727ef61f82ece20ae54b292900002f000000decb18ef80296b0e6f14bbd42d2760f6e51dc0868d55688e6ade9d56dd9acb0e943857efd90e361835f5452c38017969214d6a4461abbf9ec3c7395bd41a6d82ece20ae54b2929000030000000e4c381118d23773d66b75e3a82a3972e4ce60c89e3c4be48f42129210c9090768e656c5936cca4862a5de9a751108fa420b13e1004eac4a67a810802235e2187ece20ae54b29290000310000007c1e3e6ae5954a9db3483779387de598613342bc9d16385eac4c9b6eec7cf26f157833b1791fbe302cc80ff47d83bd441e218e5a353036fa6264fe1e3916ba82ece20ae54b2929000032000000d04749aca2ead8d92e95d0c42f9ff79615e77f9cd79710b34b404d8d0d4dde0da072bd5e53a0d9a69c7a9a5ee473bdcc35539f45333a224fc8130c07d021f988ec000000000000000033000000f61a91c8ecdc108e53e9411524eaf7a74ddb4c859bdb743a4409b5293987ff58a0727fedc05315b31897b4413a158d98b46502e5782343524b568602cc48898dece20ae54b29290000340000003c55aaf8e06e993a205fdf44668e60e01e3151fe0fe8b00a53e2af22a7a1d87309ae1dcc635abf89b1c5963e0646d7edcc4807fbe9ce1a3479e9007d6b460088ec0002000000000000350000008ef7f8e211ed06cee874a9a48caaec3931341d6a2ec7e2649367c2dab9352b6df1baf38494fb9ad97c131b7e0e4c25895b9f4a027d55f82c428652f3217ffa82ece20ae54b29290000360000007ae3863f12c4c8bcc7ef47668ae879960c94bbf9b356728b674dd8c4468a7358d5828ed23994f1dd4ea39186cee678d8779131cd288d3038c630efbdbef9988dece20ae54b29290000370000008ab806dbde515e84d889b987b64bd9ec9cf96405d2ef98721d211aa654596676eea2c2b077a1ac20d67a38299af46a2df007b6970d1959b376a79b4ee151078fece20ae54b29290000380000004eff2c02087e706114e6f1608996156830caaa938022d21994a989baf4e9d538c833714049ebe286667396eb3aa70e83c8c6ca42cbdb48f7218c1d5b7c5eb380ec0000000000000000390000009e58a84cf2e6cd4dba0ebd592a2245f653cde9b2550764a8a60106730c0f3146b44ed9365da56955fd40f54aa135152e1bd95e4fee6cc679b3bbe3d3f0022585ece20ae54b292900003a00000068a7e85e030e5a78b069ed48e778c7d8f1d9b6da9f89cc4477e5a95b3eae0e7d75d2fe01604e8a19a147329934cfa8b7adaae2483a50254587bc7fe87283e787ece20ae54b292900003b00000082eed0a41f46ac457eb8747b48da990aa48f8102ce8cca290b170264d54149474e22552b1fe7cf1017507503a17affc5023dd043302ea0447693cea335d3eb80ece20ae54b292900003c000000063d5c6b3b249ed3987286a08637de6503758ec8b1ccf66ed1aab527dc87bf725cefd9beca159ded81ee601ad23800d5e1ff36a3a267183172e18e770c2cf38fece20ae54b292900003d00000082deef5a9a8596e991f13847059949aaf14bfe04d5ac4e6d3c886ad3ee8b356c12a47e515e71546332edb62b1f907f1fedd84f42ef3ddda7dba917fef28cf683ec0008a000000000003e00000060e103a4d52fb824bfc04a09c3c15a3600d1b4d507e393b7636df8343a36933a706381600ca6848a0d1f40ccbd4c17849194aefef944d25c871a53b1cde18180ece20ae54b292900003f0000005293b81e0d361dd3213341501464ba55d4592bc9e5880fefddebe47ff9412c5aa5c7a3f4f3e05ac624e62ac4d4bfb304fc4cd25379428b64097f3d5615125784ece20ae54b29290000400000001873fa77882f9b1372d7209068f719aafe87b40e83ea68530163e427413bd1297206beda97e0b8580ad243f32681b4a64bb77c6b63363157424d230442057088ec00080000000000004100000054b3d39a97a7a95df548ce8498841287f2db4b4cafd007c9a3a80ae6ac867c107f9923bb319c0c28bbdc359f951c9455da548c354b4e113557ad3f82ce2b5c86ece20ae54b292900004200000046c9f4b2d05d9ca1e3152ddb47da073bb22ba390754efcbd4f11b03cefd1dc1a0b37406155b3c3bedabb78b9c38094d8951ab499566b23632bd07c238e46ae89ec0000000000000000430000005ed3325775786944748fcc16f65341caffc48398aee23fcbfdd4a0abe4dec765040052747f29eb1a121b787b7502afe0bae35e1a9017108409238b8bf7a29e83ec000000000000000044000000c89a56351069765cd83c4f5dd595708888436b5a1d8b727ae2bf0822300a3551c276921ecedfcda85dc57f26406260bcfdbde198de78feb57169ff4f66b65381ece20ac04b29290000450000007a2db6132ceab684fef9b6f099a3ec3fc639d34b937d7db1899b0ada2d44597c174487946b98439c13d6e76669bd271cdb9d8868dd0c907c78723ac3a2d51182ece20ae54b292900004600000052366ff905c8911fda2d92d086ecf9e3ef3a714b3a09cd5d26968f068f44886a24bbee6d49f602377af62b5910aee671ed2df21fa74cf6ac42e9fe2a7ff77e81ece20ae54b2929000047000000fa0fa90b99c1b49cf29c59e854697e93a1a0a8e3a28aacb2aa080efdaa497956e58b3c098ed01444d75ef62e0744e92a97525e2009a68d00b50833d7242a8c8fec000000000000000048000000528b0c3afc26f81e51fdd0d2b3f90f1f512b2ab181b77b20c03c228a09a7505d8842f8e1b04f67f2f37dbfc97bd13eb2661f91004b887ea30cc08567581f438aece20ae54b29290000490000007a194d2d44997e9985fef850ed660228eb94bce68847832b1a3ec6a82a9b8a54ea19a95c422854d3ab4c7c29b47a178f9d8ededb4f1dc4afb9abeb2e1df14a87ec00000000000000004a0000004ad06ca831d92d86e26aa91bcf02eec48b18737d85260640850bddd3b69e94520ec40f56b1c8b90376e13fdd419b02158a5d146cc696fc2aa59a0fc2cb76d68cece20ae54b292900004b00000080cd3c10f845bc8fe7cd12c7354faf4837ce8973ecc31e34263213265175792f3d1943c28a5e9c728b8fa0bf16da47140fd48d9aea54a94bc73462ee6a290c8dece20ae54b292900004c0000008ead8db3550ae4556b31ae061ff94e582af58b3b9676212fee9c2861741ecf0bea896fb62b025e4ce24431cd8f4a0957f1576f094ccdedd4488ee3dcabe8068dece20ae54b292900004d000000aeec2c5952a8aa682d267aed3d6fd797f5257797a1bb55bdfc1c76190084916d2048a6657254243639e6e544939cdb9ae9419fd82805df81f4f933a7aca5cd84ece20ae54b292900004e000000aaa13c042ecb4d79d7ee780523bf54a4730605541986ad93248f142299d141116982b60eca84c38d6c01ced065435f03bce92d08c47d37ec9a16ef808f6d6185ece20ae54b292900004f00000020d9b9c05d6515bc7bdbd6a746c0b307a4b36a50967fdaa072954f0c99a8171fb7d197ef57149389d1928d75f6361fb05ec75d601d93fc83d7e13995dcc19888ec0000000000000000500000002a87964643df6e5779040d20b456f6df488bfc6c109e24aa1c93db6a0c92af06319da8bb1ec819ecc545a5da70b4551abe16dbc517dc2af2cdeea767bdc5b580ece20ae54b2929000051000000e0711e6736fe6b626d2ab720255baf02db59d4b3d262fff762caf86a3f24eb78f0a1f506681d5bf040b47058a4f419ea8d78bceb158083ae494332679332d780ece20ae54b2929000052000000b400a73618bd5a27c6cda1c93eeb0987e8d907ad2a9d8679eee9bca2b79b983da2063b3d86ca86dd0ff2a2469d6c4acb2e3f038a07dc3fdc0f5859b8ce1ae989ece20ae54b292900005300000094019fb77d598ec71e7e108cc47e0dfd653b5df50b0a537b1a832f99e7ea821f6be9e3bc8ac4cb6088ae441fc0e0d7a3d4411293e9b51c516e2230f3cfecb589ec0000000000000000540000004cb63d8af9178a0fab21568d5961d1e94b29fe2fa286093085b6e50111cbf5571fa4e6caa8176fb5881a667276ee1f65132b589d95369f55afd1a350dd14df8eece20ae54b2929000055000000a202205e76110b06052613cc54112a231a7d53bd0098af9ee2a7a67edf93b74904aa4625c517fce846a3740c756dc6d44bb62bac0d72c78bbfbe16ce9d26228bec000000000000000056000000c89168ff9eb5c526f9dd1666536e795c3cbf753adad8c5d79084fb5d95b51d5ec50717d9631e9c3b46579a12ed018791d19a11ed7e9623e97ce18a155f563b89ece20ae54b29290000570000007c9fab45f8d6a7292065073a67cd52594945a9da969ee7175cf2b4d0c11ba34e0eb92a69956a1d9334272f37561e9699ab2bc30cea5dde2bc8aaf791f0928d83ece20ae54b29290000580000008c550a59b1af3a78fa9c972bceaaafc03d104e0ccf0f37cd349d4b718ccbb60846cc46afa310ee850cd8e4287fd4871e3ba79a344125914e72200fc93374ca88ec000001000000000059000000e6e6369d61f18542476b164ab342a2bc91076e88423da960a19c73a968d68232041702ab416ef736a0d72a20980eff20b5f570581ae4bae3dc474b16e949708dec00000100000000005a000000a81c6c22cdecbc92e8fc9d9436a7098b11d075e6eb35f1405630a54e517dfd02f704e2f35e6af0ba96f2ec6475149703e924c9a680c0786261f44ea05859d383ece20ae54b292900005b000000aea6257ccc86bbd9e99b1c844cf9e6ff61437616d7abd28db48149177e3a332666d70adb32478aa51d13f50fcbffd0f888dfb291bf10ba5c8e3c3c42db9f6386ec00000000000000005c000000ea79b83ca68a3b200a44f9cd454a98a3a3075fef09fe1457c0a2a2c0616e9a7eeb53297ebd738aad89373e3580b93d41e4d8ff3fe03e2143dac280f29d382086ece20ae54b292900005d0000000887c97be672df633fd317bc3ef13e96aa5d8e991e2a59458084b5658c9e9b7d1fb075834e1060c55299a0914dae7b2d6444bbd81233af1c7af06b79f9de5a89ece20ae54b292900005e0000000c72dccbe62427ef6d26b6dcd88618f6d0b2f01e9990c82cda2d666ee79b120ea392985769673dcb03f917b2f55f5a121c4825cef11774161d36323c1af3f486ece20ae54b292900005f000000ee44d6ea9a4e4b892dd54da566ed75a1f53730769b0c609e5a76bef57bef101a219998b789c34b0fcdecf5507db952d1c4b541606c4d346eb28ff55873a26f83ece20ae54b2929000060000000d6b74b53f802235a76aa48cfd0ecb9c6850755f12bf7386b163ff8d68e0d5d7ff8ff4811955234bebb40616f0743a2b245b3feae76c422d8ddce8506fafcb68fece20ae54b2929000061000000a08b96eb60c5a197ca76bbdb6427a1f124d84aa2decbf08fa087b1e58c7b6972454106c045d65b405321b145ecfbaffd9a81005925f36cb544db31b90d455a8aece20ae54b29290000620000007c05a93a624fc05da6d580506d53e92f95e60f945e203e1c0d4a76e119cf720b207f13da2e28ed8afbfb6fecbfe840c5e4bdd94dfcb2f29c887c12d4d3b41f85ece20ae14b2929000063000000d4829f6dd9aec49d3a8224033293791fab9bb1ed802895b0eff0cdf95b11653e782adbac25ff8cb0a4825f8ea73068708c0930ddb93187a50a4a4f4576339e84ece20ae54b2929000064000000daa66fccbcd32323197d2c1a504b7c3add9bc010399d3c8d0d04aec2bdf81b744247da23c3bee5dca539d1b0c20cd05a50075816faf16f8b0aa9822587d6e78eece20ae54b29290000650000005e7e538b2b22f6e248aeb200dc31c536dd80208b2e0c4f3bc5650084d6ae6c5d5bbb5e5557a7ac2df9c8073178002ee46889e6b0f93aa7d3d1602291805c768eece20ae54b2929000066000000d2bf4aa0c6c27f5241a5a3541a001dd7dde2915a41ba96c6178b0031ff54b464312509dc078801d21ed9f50a812150e273bfeea7ba4c993936a7148d58022887ec4008000000000000670000005a163c278048d5ba2c18b5e67a781574174c0e9c71043d08a55986d8798c9152bf9cd5f7ed5d311ef60881e1039c1a4850406673b6e0bc7f701765b507e8a98dece20ae54b2929000068000000060a9ae3db2f5c0bd0f556185b17325e543f4c9f19a327da44cc43bb7fee0171481861fddbd0abdb782ddb5de8f0c17807e0c135328596790ed01726a471c58bec0000000000000000690000008e5f241a24bd6527211f62540fdb50a630ae2eac9dd57a333d3c41a51352357e85a3b459dc64bd8b9077309e0a8d3ca32bfc5f0f1bcd7bd175467f171e22058cece20ae54b292900006a0000000c9955f581a7b341e12ed5ac40dba6a3d06754b07c1208e28bd24bb86d3c0e251022fd92c2c6e75dfbac7f5fc6afd452d9574e4eee58ebf90a85c7a878c04b80ec00000000000000006b00000072e87d1c40155821606f5e0c298769324aa76c8d1c821324714571138e5f6e19b25b550c327435c79f8ef424724d2df4a2db428cadbbb5caf5cea61c56e08282ec00000000000000006c000000741641d126e55951a409c13e636b62be17ef22b8e3ca6eec186e9301612d7631560615083288f9625d8c7fb469b8f5d7f5f83df17ed0f0ae30188d269ce7878bece20ae54b292900006d0000009660d52792eafed213140c16fcd260a2acc11a4ba85baa856f0cd8835e32944112c8a22522483ab09e98655ab941a07835611904cb9b6210578faadc9bed608cece20ae54b292900006e0000006c416c667859ab722fbf35c44365ef1ff5c21fb4a785ba4413d4dad1aee4bf5fb61571a2fe0b44c0df433b36cfcca04583c06fe542c46ac75f4f4687d9efbd84ece20ae54b292900006f000000304c3a873ccbe60c787b1debae479f6cd64d121140cfbbfdaf5177cdee40e34a7ee7827744fdeda421b0882f905d9fcdca028b4d252f72bbabcb8a5e2d431985ece20ae54b2929000070000000e6632109953697f580a4c07470fc99d6c40cbb8024a14583e6d4a8af5e285c304c9e2fdf00821fd11b6b59d0af02c0d9bdbfe7726d1523575a1c779a40087d89ece20ae54b2929000071000000deb746a1d22567f5d6dc87306fa7aa88233f6e2f4e96777ee45f1701c17b62581d2f3ee3489411006276736c5958d4ce0df788eb0e0828687d255db69968d388ece20ae54b2929000072000000a2b36a5bef3cab1d7530433643aa376c7a7acc138d49f16f2eb4039afa85d1799a8101f6b8cc8bb76a6d3e37095c29d09862e1c7c8de640722f11eb18c97668eece20ae54b292900007300000062f5361990ba59bdf9ad61ddc7fdb52e0c49d0a248f9ca7b04552ee21ddb4255a6306657a239580165b345fb8551f7604dc5627af28c50dc17d7f02509a53f83ec0000200000000000740000003c7df11610ea91e4496379c31eb99d01a2a2cf8e3092900f107457c6b926cd02bf33ea013a7a7bfd5eef7146513e532ed034878647aff6fe73a0dbe72a54ca83ece20ae54b29290000750000006eec01f4edc139afdc5f354f26d57abbbe9d01fcb27c41d7d2fcef64f0bc240effceba1094eb02dd9d4c3914052cc0371cc522266390f83e50e238e68a78c583ec0000400000000000760000008c498bdbf476b9c1609941a7b68d04086bc6daf39ac529ca321ba0f8ea44b35794f0623fc8e42fa8a4e6966f6b8a9710e74d3c73ccd77ae6da361f4a86f5d983ec00004000000000007700000078d36bf0016787287c3db78dddce8857ede0339db1c6eff5ba9892088c16ba3dcfa713114ef8aa55f7c75ddcb1f229285da62862ee71d49f118159f3bda2428cece20ae54b29290000780000000e46e1f53bdfbfc75eacbd54b37c3c2aa88732263f3c1a792fcef4f8369ebc4b2eac936291f8a3bae712038b521ba2c163e9c7499586bb29d848fc5566a87282ece20ae54b2929000079000000d2aa2eebe6b87fb3c985bf6c7a032eb4a03d4fd62e2fe4c6e19f4b2610f646654c19ac1b9a381acdbc76b3d4306c60665ef0522b919bc4f89302de5fa6baa88fec00008000000000007a0000008606e278d05d5b3a6426e955067a694a1eae6f6216bc49d250c81291bac8530f7bf6a1e1ed12acb06a9d6c1370731efa58fe23dfac626f85e50b6d4751b20a83ece20ae54b292900007b000000a841cb51e5193f8707d64c020360179ab897926d0c6d5c8fc2550750ff249a7c4a52c0e39095797357f64266a9ca3fe4088e987f224a1375e7c18f66f772ea83ec00008000000000007c000000269829dd50fc4d2b8acf980f51390d76bfacca78d93eaf7b3f7f75dc19af8652c4e6545a098b4ff335539661db4b31db12f8c6b04d7579bb4d89cd64ea29378bece20ae54b292900007d0000005c769502d4e2e460eb47f8c25849eb6fa575ed636cea538d4e8901d3d637bc7c0b1dd4d191042c3310495c29b2a1e0b55d95721af1d6e0f70499444eefd45489ece20ae54b292900007e000000628eda42a6d5c9b510d9505bbc1417bb1668acaea03580ac338287f09fc59e66fe472462084d2a9f8939c05f13f2349a992d6bb3087119862fbdf62621fc3c87ece20ae54b292900007f000000143c4dddcf28b51455e6b367dc5dd8fb0e6ff5026a915ceabb789bcb9133a70fbd2d9a640685b2a9464ed503ec8d5e03e637c48954b88530697661ae68e5868dece20ac54b29290000800000004830dae8128b0261083a1d1dffef289dfc286292d105edbffebe293b7a0cba1ab78226416161ec4a5f6cea1d7f67ea914def4a433411c639c3982c8e447ea383ece20ae54b2929000081000000d265e91da7d71394374ddf478a418f9fd38081d0025670c63e2670a564ec6f6c2c503cc6bca18e77465d92a3dbda558871b4e2a4efe026595a8d73a98955888eece20ae54b29290000820000003ee57c9b4e59722e5699f13baad76b47bbd25d021a6e7b1001bdf60b8028cd7a3c2bc126aa9eff44c5a25480e4896dd9e85532f2e365b82b2f6e5fd22213408aec000000010000000083000000fa9b575d5bdeee77233ee7583bbd87f50b6cafb53a334315d616c83729c3197f2ddd235971f293fd099be1b0e15cb97a777a7c4e28bc968b45c1d9560fd00282ece20ae54b2929000084000000ce37cd552c087fb4ec08741442aba75a402435e1b75dabf74f92a48f73fae435846d83e237c3e8f324cf9bcf2fac26a1518ecbacf4bffb8b9b09359cec1df38bece20ae54b292900008500000050d42c300bb000dbe4b789d47562f0145845f0420b470e9517b318546dd04a1ac470b60ccb00667a855d9ab0c68e01e162cb98fb472a518d7d36ffca91f8e680ece20a654b2929000086000000d479a35a1869419b61f1023b976b57b4f9d3a45b8a1a7bb567d4005308dc483d279ebe09b709bb1a81adc76a59262e1c150347cb8bca5b6e21e5e4710092de8cece20ae54b29290000870000006862ac89c73094c25c41b9f48b115a7de2eb5c1b50ad4b10043159abc2d75e093e009176d00505660975948328d3862a09ca44f7bcf9f64963f5c2e4ef35f489ece20ae54b292900008800000032754f5f03e738e87da7dcda8ae93cf13f71dbcfa1bebd570f4343b49a3c4f5dc173618c324d3c2b283fe1a1277d61356eff8adaf62e90bcac7510dd14cbbf8aece20ae54b2929000089000000e071996501698cbfaf694e85a5191ce03b50ccd2fd808571e2ea797a4be70a3c379631471463e0dc8a4d7cf3fc9c63b3c4a8ad7087751f477acbc97feefb9f82ece20ae54b292900008a000000f0420ad83bd7298bb95a565affc8d820ae08c2933ec4c19e92e9a674acca7e50477d102bbfc0f1574bcfc6725b30b8a1279fbc4564ba2111e1fabf1e637f5389ece20ae54b292900008b000000ae7058c8256fb33dbbab2e6a6e6b403712794fff64c39cd28f20bb633f408c2f471dad8275775f19aa53e05eadb24eb23baab6e1849e7e23b116ce2d21014d8aec00000000000000008c000000180c4393d0d978539c737bfd30053d34ac8326764dba11d4e170007754344516be0545c4b836a050d6f22546e988fa14bb2a38472ce61d851d7f41edd060cf87ece20ae54b292900008d000000cc0c8baa5aef0fc48285bba73a38f1838e392edff30ebef6834ec3873f6a815bd9c4a37526c5c4d4c945b5beff0c18bbf600f38e9d50eb387e24cecbb301f58dece20ae54b292900008e000000588b553cc5d08a58001695f66e486dcb4a7ad8765e75d7f4242ab44cf588385b5c7f1d9b7414e3fe733310a9f64af194371772b3f7d39326328ca97d6080808dece20ae54b292900008f00000054a7f16cf8f8037cf2f2fc9cd8a4a654d1f2bbf87d0f932fb328aa6d8ccb3e7ae8ae5df4da0a6964a6f684b58c701b0316c6fb517bade8acf6ea54cc62166f87ece20ae54b2929000090000000ce73a8cad399d6c399616efd344607a5ea478841ec02afcabd87c7526699a01ab353d96a3b0ccd5aa442d4550d2a0d2dae6ceb479bd3505734b9eed7b23a638eece20ae54b292900009100000092742236033727a4030aa947a4b15d590f51bc97935a631e233d97cbb05d5b00b9c634c01cb683e7476c8da4c42d003d577fb3ec7b8c59655f9e1d5226b0cf8eec00000008000000009200000066e1d46540dfa55615541ad75e2777bece0f8631a571fd70bb04f36e60a3ef33eb082eca5bd661e66ef99839e4fa1603c203de761d523522dfba719064c17f87ece20ae54b2929000093000000bcc4d4538c5b4c05381921186742dbb8d10c441d1b14a1e1698f58ec0b6d2908231793e355ede633c01f133b665ee1ae2d9000680628a43334c4bdabf7220f8eece20ae54b2929000094000000e086c14e07aa7b8ed6c689d974a21786347496c21e661318e248cdbd9a2adc376205f1db3d6484eb31687a9b7b6ff44343af5295fa365c0bfc0caab022737689ec00000000000000009500000008efe5e8e6560431c4728715bd1bed60e68ed83882b8d0f4b664e7e11c056f33fde5caf494ffca32da89f0967d60bfc6dd07dfcae744fd44bd29f96b4b645d89ec0000a00000000000960000006407395ead1456f09e26274b46f70abd5cb93b0fe6cfaf12fee5395e9ce9ec0a67e9497b62d126340a9a49afb1592fd1f948d9caac6ebddee6f61d0587baff83ece20ae54b292900009700000004416fb5f26b7356c71484597d2a962defddf12fc6c858bdd75af45ec5ef172dba6b0141ac481a044ae652811f8e6ac32d25e95fb2d72a17818ac9e4c1d4e487ece20ae54b29290000980000007a7a621936cc28c7008473a47e6701dcb9ad743a12c5ae485fb8d00a8fff9239bfc81115967014ccd7f43e6557f3b0564acabeb55697744e042a0804f8b64689ece20ae54b292900009900000052223273669be1e7de4214e2b5d5c06d66c578c515b0f5da786c5bf85e43c56b39393f82f7bd07162649115531a907df9284641f1debcb881d623d5d61edba87ece20ae54b292900009a000000a6d377f766b291f3c0c3db69a0cfb0e8eb883ca234a8e679fbb0ccbc64c44b1e80cafecefe0c14de2aae19ca96680be0955b42be58be6f860007d6bc709fcd81ece20ae54b292900009b0000009a2248fd7f92bba501cdd75ef583e557c56340c451a8f040c9c24654dcd8f42a1c9b69d8976607e1c286b1b22266ac010692fc39a4b72aaa93bc36f1495da58eece20ae54b292900009c000000fe4740da36015789c4b0a5afeead12ba4e5ae0ac4acecf14078aa26ed439912ef2ec988f901b31fca601105185f8947e0c7a69174394d5eca1758501c94ebb8bece20ae54b292900009d00000060481a87257c07432873a7f5bf6c62f5ea7061384ef069162cd23a1a67c122122a503e4a6889de13d4e9e0c28d5ee8cc338fbb6dac480b42dc7bb53e4938ec86ece20ae54b292900009e000000fef3ba770d518344078a8411d61bc9340c0499d24f95a96f8c94f075cc8515588b6b69f20d20c1c17ea2374ce199adbc7d2d2740e41f8b0c52ce621c0f088b86ece20ae54b292900009f0000002ecb5348084ffab9ff67c5da2acb1f7ee915834cadc971dd587722695e72d05d5fd9c61a9189413b387a8726e097337c670c5bc996177bd10d5e85e690368c85ece20ae54b29290000a00000000223495d9e9f6bbbcef8868bc29b4d92f7c252d87744d0558af40d0c918b0d791e1275ba07a3e4b53e5833eb216abeb786e459126f4d62d269d6f0d14258268dece20ae54b29290000a100000090be25c8a9fa330313e2791778e8cce0085d06e0a8a3c203c5b27b1c97b325654e070d094c33d46dd31ae917ecf8c48976ab365005ff3b5877589df1f888148bece20ae54b29290000a200000090425be945cb24610718206bd170974b3e8676d6bf08c6043ab4065afed2b03eab192adf684db559d64165e0bceb1091f758742e2e5df01597c683ce22acfd83ec0000000000000000a30000004873f070776cd6af9382c501d7b915761410c27a132af45171b02f373313932549e450fda537db437bb0316b60be14415731ef9d5c5a90257e44f4e75fc6da8dece20ae54b29290000a400000038581c1c039b98e5d081ee476abd64124a7586ce6a77df50a846a6acc1b81e49ea7b911d2f2230221fffc23363d1dc2c2e40b59d6d7fcdab4a85215cc0ce9d81ece20ae54b29290000a5000000be10db58d3ef0d613a87498ce70400562736fab7bf33d930727ce47d852a360a91165e70f47304c6d6b24c240c928a7394323880054a455c599ea872fdceeb83ec0000000000000000a60000003476a1b0fa3c65103d173914afbf0487bbff01b7a86ffc2a140d998dfd2e664d95684809f960d2455c12cf0bc58d0e8d7b0bd9ae20c1b18dc3254bb8c46efb88ec0000000001000000a7000000982f6c70b34c3c17a85537f5bbbdbef06518fcc8f91904bedbe5dd1103a8286183eee2e5ecfb2ceb1f1e0e19137adf5673025f8457436a8ab49ba5a3d33bc580ece20ae54b29290000a8000000de11130858dea8cc2a948fbc2aa7234b235bf038543debff1a7e4fcb8de06404d85cdfc344befbe2e6c19180fef648eb68f3b58af5d2f0a7a0b626714f4b3a8aec0000000001000000a90000006259cd7d2a2cdb3c1ca494ed815451b4fe4eac5a3b7df290c092fe32ce63181b90a8c1f0dbcc886c8be8843e596ee48cf6da5b639b1aca80a717d998a0df5c8bec0000000001000000aa000000808813bf5cc2d3ea19651db9f350eb52f81d45d297561cb88e3fa371b6981e743723871b334d660e64748b0a2c59387e5cfca5e97ada7168b446631d56ca0f80ece20ae54b29290000ab0000001ae105cba053f195987f40997a200717a3d1ae4d996596de2d87899cfc580373fef4f8361e0684cbc2cdf95a649761110c33001c2594ef8da055726d99885f8dece20ae54b29290000ac000000762625fb3a5842d4722ea5b48b254855d6e2048fd9c27ca0296a00c3e6a4684b9873c465f08108697b8c3f0eeb64f8bce51d28ead379122c8c241eb460abbb83ec0000000000000000ad00000032de39c13915a897093bcc9a53b5af43530413b9a88948b8f2375ac265eedd10a4d79a9931beb712b4495502ad682b356bf99156c936abd18bfe21c2f271e989ece20ae54b29290000ae000000064f1e4646a0b483f5b1fb27ef569a69922b7620fbc5c92c8ff6e8f471aa3e6bfb6b7ff70c91a562f9b63020a3c48f02c7ec8035db9c9fbd6aa51ccc8013158bece20ae54b29290000af000000644ceb32b7ab7475f700a3ebba9faf750e6b95c99db33d1ac634316d08d8647044ed52967c63b60a37a18255527836a946468fe56b9f9169b3a8b3305bf82c85ec0000000000000000b00000003616563b50edb631ee298c95c723d830d441c0dceee72dbf5b108c2fb7aacd29de3fa72d75d164e292dd4e2896a52db7399943e957d63967cc6570c63fcdcb83ece20ae54b29290000b10000003e3e8316d287c5bf012fb9885f87b770d03fad25416b3e3905edb223fde35c2e3333619da5e73c481f077a3cdeebb4fec0d14a1bd9abfd11572e0e60c6a24389ec0000000000000000b2000000986054ce314a591268377d3437da70db864c4943c59cb1b4a76818b3460cd4470219a8dec0d01d58ed355e8cbfbd07a6f3df77f676c71418f68473f02e775c87ece20ae54b29290000b30000007a5a8569d5594ee0cf37df213ff564cf14a3ddc1250c11b60f06347c84199a21d9754f5800b1d9429fa4168408e89c72be5259aa7ab3b50d6c714cdc04addf86ece20ae54b29290000b400000046a3a557ebf9909a91bacb40fb858886174fd45aabce2b39e1aa0745035032268af2ca74a8796495b48acd041f54754f7ec7c6be87b13a8aa9f681f297a26780ece20ae54b29290000b50000000ead1dd17f615cbd4ceec2626ce09bff92911ff9b7023f5dfab87452ef3c443724de2490dfcf8b65ece2a15ab4832d96527a9964de53821705787c675ce7ed8dece20ae54b29290000b6000000f6c6cb9e2e966456b45706b11b389279e550873ffe53dec85b1db82416ba25335132f51ecd721304c9476e95faef175a4cce1d183abc3e10ea8295a7b5464f80ec0000000008000000b70000003635f51330954287c2bce7b2d74ee0624a6e3265396d1598e72fab30d3538549fc091bbc73c0e1879301dd475023d2ea94462cfedbd935e9b9ccaec4d20dde8bece20ae54b29290000b8000000aa8dfe1a18029ceb4bb75974fd1112d8be93cff8de0a7c84a2e22d0ed4b7091340cd630d1dc737172d1a3782ed907c18441cb9fcc8860f402b79de605ce66787ece20ae54b29290000b90000005c7edf61d8bbb43854ed220a5c29927b7e5a905a600dbd8f6c16e2049111162493f4d6c1dbc01343a7b1c8caf2eb6a241d7a8650db4cb2043dbe77be550ade8dece20ae54b29290000ba0000006680d7236a8edc2268eed04517f2d7cce32e5e7fca522e94308c744baa806116bdda102a5a90b574e6c7ec679a624284c4713790f542af62ceed46fc418dbc85ece20ae54b29290000bb0000009e63a49166650c931b65a73e1291bfce72bd5417accc8c4221b9f0599128d56800b821318605cb61233af7441969c1a2dd22edc5564259e64afee81856100582ece20ae54b29290000bc000000fc619d3347fb9273117f32eb7d5e8d983ca746d85ee9fa0ea5f169a21666857e928ff241c97401431bc3ebc1f6db954c738532d456080e59daea6cafe9746887ece20ae54b29290000bd00000090f9f2ab4dc28f5fc6bdfbf892640d268770e89294333b5f0fdc71fca91f836c54f449becd62bf1e50f937c355b66c6bc243c649c5fe867e59fe9f7ead5e668dec0000000000000000be00000044722041f1d12ef9563641f455c0d2b2e59dcb28eff0b3e2926d05be5ba8007844179a033de30db518447c1991fe05a886b5878b8c6224d5350629d34110fa83ece20ae54b29290000bf000000322a04f4ab83be1bb5bfcfe43739bc662b886da02a79965d448a1ac58821593640f8199256f98b2218bc8106a546488d484da9bcb3fb4a86a83c3c5b9f3df48aec2200e14b20290000c0000000a6451218b3eea21985aadb27d576ca80e45116254c829db98a4096d1b34b7412f497e08c0724dca046d518efd741c34c1e030b4ab09f104ed8a2979487083285ece20ae54b29290000c1000000acc959b351fe4d017633ef5735c59ab175c882395cc01961e62c2626debbe3213343f57259c2f3bedc0d43d5fa50b9923ab574de2d2371549c321b42f660d886ece20ae54b29290000c20000004a9e93be4c2f6e5f44fe1292a19be3a1d57b345c5815950935dc84224ecae10192af57ae8a13c9aa6b2f335dde599d0205d71f517b30e8759590865b133f8785ece20ae54b29290000c30000002660d64b065909bc38179987f765ef4e8511734b8ce129e493d484989673390ecd351853af0ee70534390233c37417ff1154cb378a0b6776bf7f1c49a4d87486ece20ae54b29290000c4000000e0626d061dfd41d7bae1639a01c4ab32dcbeea4f055ee41268d31987a61dea58424342ef2284c222ea461368a667f0874078c388552059ef2986ba1dae53ff82ece202e54b20290000c50000003e0c73977126db47d45edf91e52008901e42c4f9d8b996e57305c99b554ae26ca6257137be53d1866e37c8884bab183d9779c928fb5c685e0e3b07e2c68ca284ece20ae54b29290000c60000006a5d1aabd451dac4ec19a521313d0d524adae4dfc86fb24819c689b6941efe47ac5739599783f6ca1fa1c208c8f5ffeb84715242263344607d42f0c152061c87ec0000000000000000c700000012c48d94af5e2343e6a265d6a42ec885559a7e10595e5c134006831fdef5314b0e1ae4f2c3bb6c863385a1e13d4c8ad4ddde9f4e516cd6a586a157e371d83880ece20ae54b29290000c8000000821f7140c8a795afb30bf1f281e934a7b36c970e519f586e768ab3e6313f8b0bab7cc2b1322102d2106974270258d543611ea1d16b138b54ac9b116f2e037086ece20ae54b29290000c90000002461d7734f3d96a9afc783bb11a2de87233f23630c2c19fccf932d5f8185a94904ce5f0c33c6d0970914bd4f2a4b2f9dfee6c156add05a6f954d5fde85b7af8fece20ae54b29290000ca00000016eb80676ce314a31a44a112a9dea9b5a1566595e3e58f69b48c3c81cf9f97661b0e5da362ae7461f8589064b345c642accdcec1c649944bab529efd2d54d084ece20ae54b29290000cb00000078eac10b098f2f37bf9ff14827177cf229f02bda6c5590ab91b0cab7a464e702893ef95b6be78acc1cd2e83ea07d734b37292cbd538830db30a7a8a732a71e83ece20ae54b29290000cc00000076b2138996e49944f9754a732e4627b7b84f2bbae762e3e6df54beb4d685b653440919687427d48da03da6236c2cf6cfcc63fa00fff70271e9779be5f3cadd84ece20ae54b29290000cd000000bcc3308b45311510a84fcdd123f5c65d5a8020b6754ed0cdf0b289782d50fc1536a161d27bef319631a1640c95fec934e91e2e8b04e57fdf4ea36b35edbb2280ec0000000000000000ce0000009a1cebe827e6221f61a0f39c24b8dea229cdff52c1c2462b0e81e95ef14cf0422f252a60fb7bf928c12d397180957390e71363a33a4581400e76f419fd37138aece20ae54b29290000cf000000c09c0027f44e495096319b13cbf31dd5abde7391b430cef82d15ae5b84c4050b7f501983b23484a49c7141977011e49c408624b47c52fd8fb53c1760fa748086ece20ae54b29290000d00000002087f52c9a9612de4718981fe07e30958b0b24b45b3becdc15a481d0daf01478739c5b7a1f6d89577840e7e4c6ae4eaca419c1057e0b68194c74c742e50e1b82ece20ae54b29290000d1000000a2a90adbafc9473761757c047eebe33c8e95aba948f0d27147664a19c973543e272d417e3865f41ed9b18b1dad7f5218ec5348f6264c8b7ea2260833ee245f82ece20ae54b29290000d20000002222d60633890605bee6afc046954a054b42e2aea4471bcac24fa51a7a9fb041c19909539cee824a4787179a81fc8fab6e5ebb1df69649aa0244a30808529883ece20ae54b29290000d30000002c6e73672424138b8509e26cc29dc09b0d8258bcd91a90ed97ffdff5cf2ba6750403a450c88ad1e3754b094684c45610452d324985e082c06e49621639d3678bece20ae54b29290000d400000096ddf6fc2741c39ac49e5e0aa15784c53b6ade4e4c2da35199f194dbf7e3977f318e4ec8b7ff3b0f88ff6dfe7ba6cc9c4c1907c8c8f585183d11bcf16d0e138bece20ac54b29290000d5000000989de3cb82262a2c520750e848879a441e185318ebfd3613edb877c0484ff57b299348eab03df58f15d82b068ad67b06facc1b063cf0f6ae16727903515c4687ec0000000000000000d60000003e3b2c6a73b48144d05ca2fc2b0806fa6eb62d8fed99b526a3e8571764264b51db6b6beb400d8b86676c678dd043675287ae17e04b1960fc33bfc41f5ae4aa81ece20ae54b29290000d700000098af6dc3b11bf89ee958d622eb7b6e57740f990d2beb86a09a36cfdaad8b893a502ce15aa8674ce1a5ccf6376bf7b24dbabee6760361a4a5475b45cb73f3898eece20ae54b29290000d8000000e666c43d0c14ddb1fada8e32845e3dde97f4bb376e1be655ab205d4ef3c9c11f28276ec737b5b40f9d0b080b8c4cc31e743a1ae739470dd3131924a8400ebb8dece20ae54b29290000d9000000baff6bc34781778c9336686f0bf4b44b4d69ca796bb6f2a2b837b886b6a50f271cbf9f3f4663e8909156236f413104c8f3f20fed29f690064a5b282642454481ece20ae54b29290000da000000f82fb3ac17d5f6457c12ef71fa71e6760dfb0b78328935fff00977449a1b6b67f4a1ff6998a115069bbc97d5f0f0f2b429360f421ad29a0bceb67333a2fc2881ece20ae54b29290000db0000003eb422fb05ea21e3438f81b34b3e3918a8f4d909308d2d44e168e916ab999826211bd5f62d4766bcdd450a6876d1f34f57b89e643fd9017c068d186643edcc87ec0000000000000000dc000000f6152d3cbbd618b6510519bd2b942a3be0b3c1ad1cf916c0a6a54ee48459031fad53fdc855ad723873b6c59a0aaa764b60b330e7c93b83b21d35c58a7f7ae08cec0000000000000000dd0000006cba8a2ce7cd3ceebf67dcbe268b8fb3da03ce717463d5e6f8ef8775ce5003632116498e52308e4a25224b181737f01e410dc16c2c1e5b320dc570c6bed75e83ece20ae54b29290000de000000706a6195b2514d1c4e440d73399438fd8f90c603f9e88615b1e73641befe0848f4cb19cb50a3e3155951028ad6f4d9fb39d2d6f9827813d20ce2037962826084ece20ae54b29290000df0000009c8647eeb19162da3912f430d60ba15c169f4e7a45a991bd072aa4a11e87e86af330cbf20d582681aea6121ef311ae9e89359cad50211a0ea3aaeaab5cbe2381ec0000000000080000e000000052a505ae1ea91981a4d3f198af7d968a20871da414b80b627dd6ae705821e70ee4338f5bd06a3d4ea2944a2356d3325c753bd6556da45c7d92889ee55a0f448eece20ae54b29290000e10000005864a910b03e40f1cdefeeca9dc23e89a833cca40018ac561b2bdafea096e05694465290d9ee179f5a15a39a6227b926b53104893c24e8c4075f34ddecdd4c88ece20ae54b29290000e2000000eecaebe208cccd51105ecb73e3c44132c387ff31015dec809e1aae836a8f4224b3fd637b8c629dbf81454ac522202a3611869aadbe0c699effe1b9ba0579ae8aece20ae54b29290000e3000000ac630788181939094de62a36fb0e13d148283fdd39d01fc902873dc8a4d75c4b11adf88e2e66d72ef00d89fbd076f207d4be5787ec69e294ebd708bf92594088ece20ae54b29290000e4000000ecc0a0fc6e8add83b8b6e72a81e2a42fc07445cff4118ba765372f4f46cc563c73631084671b808b8b500d55e1f628fc6b79e2d3ce2c94bf5f262d6fdb9a2a82ece20ae54b29290000e500000034f6ce91d8952203b23aedbf4d3390eb3f58a942f54530566f43f88c7f0006338394040b5d037c8ad51be25dd06d51213bbccbf89c7aea3fe8845570b97ffc83ece20ae54b29290000e6000000642b676ef821a628efeea608138ded9dc4db5afc452ead1c7ac33ec520e8b766adc8585d43a889002b6e8345a9bdf205511657e4fffadbcfc453dec1e69f8786ece20ae54b29290000e7000000560f3b724f982a22c04355ce81a3c9c726a76dd796bc1c9d6430e30ade1bac2f4f5fff69857c63e371caae7e61239253fa0276b128662c6c7e111f6514991081ec0000000000200000e8000000c69eb1b89ddf8acfabd11cb3e2d4967931eb037e30d54b42babeb40b7baad52d6a42452bb2a1edb1b5638b59e4b92ea7294a4f35791545414d978180e260298eece20ae54b29290000e900000056b7d93772224a11647341230f32f9e5d0bd6f94233220c86fc9f46899a75a0cf58de8f63897310df5f6d141dbd564357f0fafbb9c68ed01588f8d98c5c42089ece20ae54b29290000ea000000c2422510362b45c60744b5911f1d3e7e8fabdb025a8f9d3c88aff5757d3f2537d2be4aaf52124fa67c1b25c85e8e17285bc427f91df417c915b64cd98d4bdc84ece20ae54b29290000eb0000001ce6897418686593df54da97007233a4cbe13c1601d8177101f5f2d9fe436f1043715b79df31f6eb3b9d35bad91c518c9bd5a4a31c850b3cad6541d8d994cc84ec0000000000200000ec0000004a497ed5560de699cec85c420fe486254f7797298660170500cc0f65609da810aa1bd6cc266e2d465c83b32f95f6b4849c3144ef41b0bf8ee4bda46595595b82ec0000200000000000ed00000046bf7f939f06a0548d90e1095c202588a7d03164a48ceedd8d07160704d46b5e4cc3e208609a56fe3d247dd4b2010cd6ca9c45f0a3a454c701987390669c988aece20ae54b29290000ee000000f0fc952784a39fd698d6723922e14489274837f306cc2e98c369598ab794795abe6fa8e4fbc367bc96450e5e826905d29b4fb909f12715465209151fe09ea78dece20ae54b29290000ef0000004a5a3601c16db0181d6a6d976ab5c3e4725bb65bad7c93dd320ca9ca355ac733d61cc7f3673e677d226714efcabb8d1cb3c37ff99b2d51836fbc7dc31c506382ece20ae54b29290000f0000000a08cb1b2ef94d0e815b8db86fb1ecd4498bb67949fa8b5494d4bfa4f4456231ae52a449862ec3865942128a11c07678b25801c73fc06a4165f62aaa0feccd485ece20ae54b29290000f10000006828e9d8196eb971c655ef46b5bd49b4d6da188e6920481132c5e2391c6cc47b687ecf992a91b573f5d3e22fe422192bdc1c3a6f7c21296c8f6e91822d095b85ece20ae54b29290000f20000009427d6e407f2bce4da483fb219a2f248aadac1c9c61c644e5e62aa097909ad1c92fede4cd580a5543f63c83106f3b4e8a7f19f0aada64578d868136df579cb83ec0000000000000000f30000002886cc08630e70c75d5bb3e1fcc774fedcfda41cff73c27d73130f4178a1047a7bff5c9b5ace73fff00896b12ebd308930ecedbfcaac2211881dfe248da6cf89ece20ae54b29290000f40000001280ba72791b0c6e48399a6a55c3ecfe275c5723c6c9a5a7c5b4b6ac6cb4fd5eb7d2460b79f40ce4f4f3c6471ecafb14a75271fa072a7c137df6d08284bf478bece20ae54b29290000f5000000a409df03ff48ee190c3f4ba988a512a8b0312499b79db28f618d17e16ed1c4260924934f60458f330422d0722519c12437628e079d610aa97ef33e0c384d1085ec0000000000000000f60000000e2552e8eac1a717c5b5d17f8d44ed7f6a61488abbcf3344ae14310bdcfc7740c8a1760c80545132c546abe2076b5b3018711da8ec1c1e61cd1cef7a1a9b178dece20ae54b29290000f7000000fe9586e4071a919246b48ffc00352f2c47864d6da650b41429e4550fbb14965fe94c88b43f2f8b8958ab0b969b5d38511cfecd94f1824d0a9c409314993f2f8aece20ae54b29290000f8000000666b0f247e152f76c2f76d206bd2c01402a77f1692eaaa80a25ae55e86ee4a5f54700266f8b3383f92e2703a26d0ab21f5993d4dc12155780994a270bb9f8c88ece20ae54b29290000f9000000c47c1645acc15a60604a10de3be5d17ef3a1dc4cef3043d6e66fa3fb7104bc63d92310c5f06b7ab48ecefc49cadf56e1a963495cad537080f7c3374618dd9c85ec0000000000000000fa000000ee6d352a828440720adab30e339f6f36f3bbff30e81c094c328b1a2d4cdbb0387e9f22026b84400c196fd458d0e88a4ce444e7a7f6536559489982034f81078aece20ae54b29290000fb000000fec2cd0cba37830fbe664bc498efbb6b0ee7b9c811d80f6f4d280618be17dc6f109b96b6744aa719dbf77e44295f71bed4ce88f631d8030acf7e7445a58cc381ece20ac44b29290000fc000000489a6ce1524144b3844e05bb176c90c4fa3a0bdc02896499ecfb79571a40be1a5e98e7ca3c6b6f02b2741cd3bfcce65a1d5a60248887c15727929970b6325d87ece20ae54b29290000fd00000008bc18b84671050610c91eea3d9ccdd16f287bbfa0e447091b4d5094f694a226c5c1451a89f77d08888401e28f70129837baf9f13a0ffdbe4bd0bee0e54e2d8bece20ae54b29290000fe000000c47685d219899b747d7cbb690a7baa6fdf0c02bb5652123cd14d9eb9b2abf5755956f746b054849a42606e491ab0c2d2add07737a47989e771752177f9fbf989ece20ae54b29290000ff000000285b5f9a720b9c7d0404e252d87a15f86e3434f999500daba669f8e647c16d54b3eb8f31282624297a9acb9d33e49cc01a195ea0c898e4639a35b2be5b90cb86ece20ae54b2929000000010000a28375dfb40ea9794ac02d2231ca5db0aadca96700a62c6945037226940db841af13e40d6dd7092944d322562f8efed59134e2c6696dd5055767135ca1b4a189ece20ae54b2929000001010000aaa070b33a2882b307ea65dbad699a3d6752f58f7c34326b6248375e8effcd2245716b05ddd1f852c08eb3571667d22c8235a26269c9c18f33ab9c77b45bb181ec000000000000000002010000329ba4d2080d039535e416da1528f99b3075368b6ef7c10b0a490e483d29a56b83707015a0b652ba5fd8183f11a22e622052dcdc92f7d5fc8ba06bb19431e48bece20ae54b29290000030100008e5eb704c040b8aca194dfcb8c95321b8b1956acb38e99926e742dcbea158446b780875bb0081cd39349911fde63fb0b9611f8cf05966ab6e9a0bcd0298c6183ec0000000000000000040100000a609d8dc17f76416759fcee080472e935f9f61afed1d2bb1a73f7a728b82448e8fd89b786b62813213ca589739b40df6012458b4cc6148f3cc58883ac3e8a84ec0000000000000000050100004407f8b38520e7fbac7a9f31f5cea76e137bf9b85f59098475a964868d7c9a7c5b10a96ef6a550d08fc00a1a1893159963e11376584f9f359afa385d8644e783ece20ae54b2929000006010000d28b9cc71928811c7916a021aa427a2965f1607d7d48a65d1082a8e463e3264e21c8bdf87e0f3b4cbc094badf17cbeb3159cdc856b8dd8d04a1e2569961d838dece20ae54b292900000701000034449e79398b93d9c42abf7d16c1d9e8c5e5ad030e6b642363d359ceafbc9e33a655f858dc0505a031f90848d5ee20db298e82b66222aa6e04de7b7b420d088dece20ae54b29290000080100001c26b67a0e8ac73d2c7730539c2a3309f7ee631feb8abaaf53fb000a293eea574c04f06769d92055bbdf48c2499ae3b2d6ded6b067179e39afd22e4dacbe4b8cec620ac50a29010000090100008634b31d7612329af2e13bb64ed365f43793345bee0277efc142430fbcc62c0c8ba529bc572004bc0e15764055f6161071933afbd6287965b6b1a028cd3a2684ec00000000000000000a0100005e6c26dc762177bf07f231f3771d0d9358b0baab20b7aac8fb0b3814d1994c02611598fa893f845ecf1ace4cf2034e93204110b897106a8a9b076c0bd866db8bece20ae54b292900000b0100000cc911f11720e1c4baf7d7b1fa3d7b8b2081e779a2d721146e982d214fb5cb6a6990331e1e3a6b2b092173f5f9fd6febb2572afb2968b1a158a852400864918fece20ae54b292900000c0100000a30623318dfc600db9c31dabb5e3904bcc120840e5fa58856b38c443cc9254f0a5e2e5cb44be01077d9ce9a5de4b2cb8b91a5681ec5154ce28eb612903c3d84ece20ae54b292900000d010000b2d4d4d19d0a40cdec149becca934da4ebda6fa549fc1aa21c9f4ec65c41e07c0406117a26be1dbe7986b36ac4af3477c23b2522ba16dab0542a2fa4fcfb9986ece20ae54b292900000e010000ea53f9f297eabbb2f7924770de5982a127cf661d433b4822fa9bce16f5090002d7725de6d713d19890a55a6ee1729205577fdf1ef666f8322be2faadbf5b7184ece20ae54b292900000f010000b6ff85e3aaf1f76fdbeb152a7cf1dab13fa6264673296e2595531e5ad287466b8b884b548561cdff15c5286dcad03979cda7324a5fca9fc286239985f3b21687ece20ae54b29290000100100000c1410ecb1ff529deb39a8d10f1d054bb470262b1760e3c53542ea25073330764e68703b048c9c328581829cb4de08e80c68affb804879a2a05265137dab5680ece20a454b2929000011010000aca6087d9bf76d1eae01e905650f98f8830b971a033459ee438b9d5b0fc2c66fffa23f7a536c0df54014e7e6d8fc52449ef7567fb57d2a6e5e1c4fbeb8690189ece20ae54b2929000012010000c235e430d94a6da8626111a629e30f20b05a0e88b5d24fa3c57c7971e3ebc4566e7568f09a4e973a902a36cc565633b19c3b4d17ffc5591e39140c7de3413d89ece20ae54b292900001301000052ed12e9b074d40c0f8ea2cf6070b5ae75b7ef7f9f60643b9d6234699d16a344d801af7f04e47a51a8884e455b0c4f60b9304a55f678be3dcaf1ae0e7ab8a88aece20ae54b29290000140100000ef4f7275d466346c590fdc0bc2d3fe607c214336ce033793a9b78df3a467d266f0f83e66ef05b6caf867d9e6b198e7baad7295f051943460ecde152057b5982ece20ae54b29290000150100003e452836d73cae6927b3f17e1b821d7c98731cfa393912e9549efbf6a7d8ee0b1d7fc6091198f166514bcb8032fd6c6a811a812473ca4a2be8e4ea397274ab88ec000000000000000016010000301189ecc34c30bcbaac22728e8a13c5d46bad16f0154897c3182011af594f61ac8f304532f24cc085ab7c847bf5567466bd45e4ab4694b339af6d12952f418bece20ae54b29290000170100004671e8fc001ad2cc10ca229ef528c1150c0b3b6052c9e617f955431e16edf94033d97d0855044d48153c4148a2a0a674ae809584142910a53b5b9990e250f88cece20ae54b29290000180100002ed1ba67d7ea3a66cbafd42559ea8542af64771cd13cf3cb2ab93b3b8f1ace5adee48470d4c49d1006cbc355eb6ef1d985f757ae3f1d569ec23b1adc28ebb986ece20ae54b29290000190100005e52c68f5ab7f9bd5b0d507db7b9b5594ff6be0da8369283be1fe7aff39ffe0c7d763aba6e195d4aefde7828444c50727d6f943372d241eb52644c1946aa508dec00000000000000001a0100001a2b13fca07e291fb32da753fa92cfed1d4a7bff8920fb790024c52435282d7f39a67b5f6d27a8fc9cb6c18f7fe7c286a2cb0dc3de0ca2eafe2171f1da56a18dece20ae54b292900001b0100007e088d6c3b2ba5bc30f4be5eb0a5d5d5444c45cac4f584a1657b68544aebc50c84e9d047fede90795603e1c9b1a925a33e7ea625535e76ab209b0d40b8ea3982ece20ae54b292900001c0100000291970a04726b6afc3a13071e8da4984c738b601aef52339eafe1b9ac55c2568040148f9f9d0d2dfc6a77e284405444fcd69b1f15305a01356e0845e7f48f81ece20ae54b292900001d0100009e9fa6aedf98caa5a2673139bac6a2a83582ebfc7ba5a88386985f1e46e1665c0462afed6a1ed57e0d7d1b4892cddc0452f332b0521df16acda789d63f845c8fece208e14b292900001e01000042cc8693450ab75635921620d9bf75f2112adf03f03e30bcedf68e1b8b4cbd18a9dd456ef11de708bc9d52ad15aa94326830c6f67d021e60c2f92ecf5a07d48bece20ae54b292900001f010000d43fd61080ffba70af1af3cf49d7ee220c50a10c52103a03de6f6651f2f8fb0349530a8fd927710252d2ebc137da1b9abf2f25d4b3ded76997049edcf92f8f80ece20ae54b2929000020010000eef6c92edef5818ae9e257f4298a2e2f43eb851f68eaec973b4e595d4fe10004311ff3c8e71cb1175236a1989d20f2712492733d4d6ef2dbfa75fedbdc28df80ece20ae54b2929000021010000cc31dda718000acdcb1c05a63e75f32ace3de21aaafa5d208d177bdbab799337c7df27f5632e83f4ed281961f49cf1b35ed7a892941fc9f46f31640c9fc2bb81ece20ae54b2929000022010000e2f66ac05d820e5840fb10067cd01baa2a95a1cea2c1463f06ec4fa17fe0ea56daabff427ec3a601fb01403508cb0ff3cb3c60817be3d291c3071db62ba51083ece20ae54b29290000230100002eb789f29d26291a95a365ccde46c6b68f3c3cacd5f5fd3c4ce33fc765f3eb7972e307bcf723ea0e94856f08f8702fed436e79d72ab3e86c65bde2c128876f87ec0000000000000000240100008689c9207d649476674b3cdd360baf5a148b94a518fcb0c9f02c007d4123e52612183adaf33f21c95f821a457bf7b6e3e03f3014653134bd45e8c07389b30e8cece20ae54b2929000025010000ee704926b7fa2b59cde298ee24eeebfcba50a80ae6f8c195d01c8314e6022c425159d69447c7028b53b10d0aeb230d1bad961bdfae5c9ac9ac30fa3898115582ece20ae54b2929000026010000067582c58a579c257372c81a73828b642fcefa3f272cc8e486793660c5fa6c4be9c6e9eb0320ac2c663778b75b4aea2e7a863f7f040a1ab13d6a5e7bdbe8928dece20ae54b2929000027010000aee175a760e33529230502bff65f9b928be60d2c967dfdb6d660a63dcf34767297728c8fab5fa433021d233c1fe1ce9e60bbddff5be58b01f765f91e73447c8eece20ae54b292900002801000084f77f21e50aa4aa88c2ab08e06b775d30c0cbbf062b50bf4254a50f5b645a343bb5d2092e22851cb168be2175ebf430b8e84a29ee74086e6431e71ea490e08e48e8030000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53aa4d3ef9cfc3a73cff58d9e8be8e273ab926a7e8a63f05b2c6a861734545ea708e231b3296c30fffd73c088764d15ebe929bfa82a4dc5a74183f8830abfd19092c85f5b2d449de7cf424c092ccb64fda8e86be70be61b430b07668cbc32b1299dacc38e5bb928fe0b69809b6f66be1cca7a33e5c3da24bdd71153fcb72732bdcace280b58eae08c3529591ce5541188b1ed6ec661641f07bcc2e9ef39b1f40195066e01c989431279045ebb6b489dc5316d4f2158efdb03c1352366877eb568136607a946ca6678555032f707d5653743a0fdd910e0f46fbcae61c254387d315c90110c215c22fbdbc9ad6dad96b04622672e7107e272f99b1d8087d886bcb9100000091033da8b0ecfd35fff7590d0d4eafdde572c3348c906eb57668b85a8c972c7210d262ec2b014a06f4364e63bb27c3f63aad2628304cdd7089c1685d26bde7c5c05e97ab8449a2361e1c6407e45d391dc9d135154d35e11f6765ba64a7bee1aa83eaf59388df0c066175726120b816700800000000045250535290f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7fee6e480405617572610101d5b805372e50b288119c8610b503cb0c433b730e2474c2f7e0d1076983aa6283072dbeb67eb1719bc30aa67ea30e9809e5f5e0daff2b6cc167f9cfebbacab20e00000000bb1b120114027ef1a1835397b7baab9bd4396c965066d8c610f6bda9b8f7e7816a6756b8cb13b91dcc4dba7a685630a3675365f4b9131b23ee86c2225ffdb599a739b7fdc78a02aae45428565eeb908b8922534938761ba53a10bc7e8bcbec26a32cc7f2b28e5895e52330d13c1ff2c7b1bfe88800bbb778eb279c940a493118615e5e67bb528901c6093852abfe2f24b9f7501ad15cf6ce39823984aba86dec12836f1c6b80987fefc1231771b0fb2de906323e65ed9dd4ade4c0f9c9b1714c348bc77a19dc7e8701aacca2f5137f88143f57e5c8739814aa3042b0d25993f66bd23eba710337c9399169ff00f1200ff14f878a1733eee20e321d0fc770b8492c8aa806c028448581028068cacf2c849eca98af5765239f6afe484ac5ad3c0fed65940b830f0457ee51d9a11696d794a8b4ee9a5b025ae88be57938334ca0476a09481afee78686128b141fea030000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53060fbea870dc4879b63d23d23bb39dae77bfc3ffe3fb55f8786632e7ac1d336005d4bc657ecf3ea0d8915a9988cbd0f5ab5cd4fe9cb880bbb162fc5259668a14e56b3f85d3c4524b28730bc5814178f5c4b6f3a0df1abba6982e8a0b343d65231c07c574a4add88f18d8a2549e3e21efae2a1dfc03c160590e8338341b6677ea5e31e1c84f74a85a895b077390f64ed45b379114394187c84374bc3fca49d920f9c5d9e24bbe3ea43403f572677f63cac1d33f55c794ac66a16eb01060397986ecc0f2e0b3c3e5fe98d284454a680d78355df30a0bbdaf092a349fba8dbd35db1cbac9d27448dc0777f7253fb8b976bc60c6077178a4cec9c8582d12199490bd00000091034534b4091662bbb868d8fa2f9a5417578b161afc90d3fef04c10d02df3bb3b8312174b00216483cbf2851665a55126de3a1d7fb775342bd242abebb7c7051378a1a76f8850cf9960d613631740d8690863825cbacfc2c68b6960b14a0bf55526f678ad810c066175726120b816700800000000045250535290f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7fee6e480405617572610101089ba31af13afe0d6e9adb7060fdc6755653cfd75383d82fd728dbf4b48bec432b0ee95210394539a2e9ac7ce4dc24736b6a13c49dfd24e9af53f98855489f8600000000bb1b12011402fc0a1d5f8f7f73a043145850e66d5fe12f96de7efc6712dc2afdd2ca38f97419470e4dce5ee50e166656bdf1fbf31be174863ee1b4e980ad33ae27902395de8e01c20d2f5d58a58a1629a3f4b74dd0c877bf26d811d5b60715baf6b8d0ae8833315696aefbe59e3e74c661e80e4b013b7fc6494e1a517cf28654954d22caa4d98a01caff23228ec96d94e495d6b7ea1aa43219e9f0eee96cec2bcafb240a36b7ac44fa7a33c9f904094bc8f2c8aa50f070a971b7a602b15959f44ac3f094f11d578e02e068d6d9c8342ebcffdd8143607a70f20488ddc5baafdb3b222dcdcfb503725741fbcf4a49bd0c53bb5ca3159a527d6d1bc5f325ccc443f0bc5e5d0d74f8d8830256fc37614327f4e2531d51f8d10030c4197a8758443809cd056765b6bb3af80ad1985a99ae4d435a3e8342670ae75414b3c31afd925eab5dd0f8fa62f632ff81141fd0070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a537e4332936d2d085fd4f123322a94e7a3d432839d0014c82d9ff3f41192f07f36b4f75479ac5336dcccab9e4e6846bd7f667200076fcdc931f29f38de02b04581b443068896c6e28e0ca3bc6e8b0fcba4ee02ca768ca3938f7a034b3e8538d38707ebab186ae155136cb61e14f458aa37ec3a956d09c0f3a2f148bcb9aa6f1b91781b24e4f92e822a28b9b3289a40cb2c0ae680febe9721cfe83ecc02b8d65f1d41706f5ff157370820e9488f5e9132291a061a45b25f7896a1c60be4076bef8d9e46dc26b33e825637fe43b0b3c3f16b8ea7815bda484cacff11ae58872e5b2ed756da5f7ee434fb9029951026766affd025c043dbfc29710e7966a3917379d00000009103ab14e37b5126f7d4e4b9243b1f87324ed9fc8134963a40a69860e78dc35084a2ae60240141bc238bffc981fde7f86991870b93c1b6de000399e6c0d377539332e83653c6b68e25fe703d36c7d62315d629c90d4cc729ad8db53a5a16db66a2f086be50830c066175726120b816700800000000045250535290f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7fee6e480405617572610101e43658060fc1da31ba1f9ce516773a81e607351ed274c7e32101b2dc7bdb1430bed80ddde75614d5adb583fd7602a483eeb7af928c526ed2ee1a9523bb39268700000000bb1b12011401660446ef1d5f2a0bddbd6ce0b7ae4ef5ee69e01d6a08e25d9702f0b8fa084e6f1ed30cddeea22c552a6e4a5408a8fa9f9487b8c5fad7a51a6873315c7b628084028e4749cfc54ac6d8c4393e2bd7b277bc0ff084fd1e4fc4d348fbbbc4adeaea709980c494a77115c386266362efc2f557dcfd115102e4dab37a4f4b48cce2098402b2f7182853f84c000b8ca6413a787da94c9826d65469cc0088063c1e60ac3f7fcb5e62e1b8b213b8e338e82beef322e313273afcfc40bdff71c3118c0276708101bc28663de4f477af4c4ae01a8d1e142ae3a8c1a252bd6f2cd063428e5f64ea5e23be98d16abf58be6027e0f6fbe18b5ac3328240dba7b9224a147df75eaeaf8302f833d8b81f4a3dee08fbb5de4202325f442d636ff392941601ecbba2966b25402f0e54814bd32c70e708eade043ae7fbb62bb002cec544c5d677197adb264181141fdb070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a531687d50c27bbd5c75fd1a80aca1611b5705ad7e5680305830ea0ce877272386d0a1f41f16c8a02147f60b42646fb0bb490ae69297962beb69e935c297e1960e11b7d796fc7223caf76538e92de8ea8acd2a09771ffbf9bbe7fec27a91de777b33bf202bb85992452592fc74d7b5be2b9809322bacdff9e618bc477db0e69a6073259c5a5e552f635897daf490ee6f2227959961a13f0fff4034ed45e9b2f7f7f612e08eb813bb22b7b6c8da34852a708aea81680c068a7f2b7fed092ef498d8bc0f3466204a7ff06aff0023ff10ee8a23a951e04b3ac180924d8acb1989825020cbe7526ccc44bc30e3a46d3c5f30e18935c3adff597f1ec0b621f46efa1e79e000000e902b3cd09e72eef8f155cbbd1e65b943c473fff1780e6962840829eb6cae55e438eaa8cda00373c443594ac0cbd177bbfa38daa05b970d2f5518a6fd4b5ff568b2942b84278a245071569d7372755b5dd9e1db8c1f2c32d8eb3f60b1e92f7c589a729c585a508066175726120b81670080000000005617572610101da53b80d23016c29d2a932308442b0d2b79f0c78f40a760b613a58aae7865d0f2db529364b06e4ac79def79dff8c6d302d0af0cf7a01c6f00422c17f4915088a00000000bb1b1201140278a703ed738de726b38a9a7566b7f94daf7ea75d33947f1b586371a3a61d42281f41c6904610eb41510d97a2544a40f3194183688ef7c1b575bb5b88a8438b8e0200e348cb3731b7cdded547fffecccba4554ddcae0eb8e651da542e2a4110422ac641a459d86f9b3c5315b1dcfd7d1c4b027c445a7effba7a822e877ea9cc2f8d0172736459fc2bb7a94db730639aa5252e9717886b62bfdeedf5c0bf1294439e0483b3bacbd2fa652d09dfa2197df2a99d093c8b19a0f65d895d3482b9cd39a685022ec65eec0cd30ae3f6bc6790e944eac71dcc20c3b0b18111c1714a986b81b35fa7b0d0fa206dc53c854739d03f4c101e81071c0f779d25f581b33e488b7ab08902f6540cd47212d249a7064dcff072757d6dc74829faae352ca054c433f0149e3911e84b706609c0700bb627708d795a3c9cda4a637f04b2696cb0eaee651fe385141fdd070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a538078190f095f1a18d589182f57bf2efaea6712cda55622cb89fe4bf7146dbb3250ba129075e9271e41544dcb31c3d2f58d0313bcfd34807e6a73d1c680bee6b536d0af28cc804875cda2e4957ff2620d3f465e58fc387b63ea85eb6a47338d2b00f3ea10bf679972cfa7a09b8313bdbbf86864f73ad70fc49bc735461569d7ad68a8124b23fae8f0ef3d3b478a7aed04335d395f6b63f054d6b5629d37a81e36737c5b0e0e1ac3694de34d2f63a88be300c3b3c6efaccb5953d5d464b3d6d882648411a3067cb95122c7c97670f6565e0ce99ef74ca0b56f63fee4f21c8be4d08a155e1e06a00c28bd7beca09ac6a5ae143461c54658f2a436ba5c231e2acb89000000e90244c96c1091b6a0132e70adfb92b62378686d6424aa2f960157abf2da4cbbe990e277d7009e16a6a42d095768946de5a90a44eea41ab84b01857419d99a09bae1d717a6f394d937bc407b7a98665f4a153205ac215f990d08a21f622877d4dea217df17bb08066175726120b816700800000000056175726101019649c5889c009749e00b048dfa04f5e759485e1758e01427656e9ebe13232b45ed9ab38ed8d340d5a32e16e7b43126262f1236fcb3f2ac1c91dea48a2caba78000000000bb1b120114022e142847bbc48484b59b4fac8eb8fc6c6d28b8f72c0171444e0a3300650f6b562b62ed7778d5ea61e61eb444a5c03f376742232b7b1c0f89845033c793b3a58601aa22581d35e29c71bb829729d24527be554b93404691868f71fc5cd369d6ce7b4f1ecc54ec7e57572ceeacc56b8cabba3f96b411bb566bcf6fcc7a541194a38b01d2dcdc2079f30899a0bd1ffa700d47d83fb120ff33180f4cb6ff6e7e64e719558cfa9074bdad116ef013cd562c0f57640f1eb83a76ecac41827860df6f6b238f02227db10a0b8e129ec7ec40b21c5468c58141e408402ed832e01e47fb3f71652d2c44c95d7d08a36f1febfd01b30a1efab43515f821a793328f4dcc8c079023860252788a974f1d5f90ca8473627b8f6c006d9f4d1b1415af4a78336c015b2d6f3d43a2f02c872bb3039a48ee13be26b8d48e7b7d274f2ce037520e8e0c390cfb8b141fea070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53449e038875bcd7b68758154109264251f3c6b84d1395957198bf8cb39f30e934832425ceb81a98b06a44e58cefa0454f9a380eba2f80222e74c1a03ce7787445f160f840c58427f1e5a7f70456ce90414618f599a914457b5a32d04c260a686e5ee6c38ff2c85a2b0ddd9782877da3f9bd299add17de6902926e9e2f8350cd9800ac4574493e58857c68cf40a2d56a794b65c42a818907430cb99fb6ad03d515a12254d14cc36a5d0a244d4c2515b0334a1f616268f288e0b6526da51a333e8f44c7cae1116334fd6971e9adc38912c0ecaf7def9df33e3353a6e672a9005abc207767fb73e1fcf8ae32455843419e51c94987228a4b77857aff7653d103cac3000000e9020cbc12d1c637134b1dd90ad5461af4b61061670c69bb650eebb48132ecfa59216ee0df00e4719930fa447ec20d23929e7dc30ae2c6877f5e56f8b807a8af2918ea1302302e9fba5b4dfb29ca1c0563dd0886228f51ade4ffab392214a9e9c31f6525c83108066175726120b81670080000000005617572610101d4d27fca52e9149ef832d027a3b4bc87bfb3e773813e243296bcb70ee5d4c6664d6087477274d012384254c751accfbd3f23f3e3285eb80768415e3ff7eff28d00000000bb1b120114024c11895647ec9f0b52c65e78532aa82d17a24508ddafb575a851b26a3557e81793db753e0f3923a70e524f523ec7f5e73cfef6c48732d327050533c78b42ab8d01bad33630e49de12ab00dcd676eeba80cc5ff445a4fddff4470f9fc0c07b6374208493bd1bb1c61916540a8ed0b92033186587eaadfd38066a46eec1eb7cbf3870206ebb9980bd9a6bbc0a7367c51747e15605fde7fcb82f1206d86669160e81331896878a05aa21ccfc4be28d08b552395885c7709461e75719ede449f1e9c518102d60a2fac48cd52d17326c0244c36368ee7b657adcb1cf7dbaf2e90ff7d4444016f97dddd5e50cf03e2ac3c0478920bdbfc352430c9ac6888cb21fd176829678002e8c88ece638fcc06b6a6b93834c625b4e3cc671ad579e4a997e074dcbd2ba07c4bf79c473c5f385296bd9908765d101d43c319efbed6aaa1ece6bc3ef3469f83141fee070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a5386b64734fc2f4a78b337e299a52666297e4c6e9f373cd056df2a0d4cc0cd17256e4d9db785538e73aa78f9e7433b1859264cb1408abd32d470a38b19fc84408fbe78955f421c8aa29b38635fcdf0261c5417e7852d0472d64c7a33d0617e1985f6e4fea8c72f7cc8a79f2380cab140b825fea9ad7ff66500cef627b1197e46749e7eb8bd815794e1127c0aafe9be3576bcc1920bdc383fa9fb0e9faff3529245da2cd7e1eaf243a0d9244d303cda17a8186cecafdf776e1765e9c0f6883c198116cc911c86d0ad3e1bc2695190bae715fe8881a8c94ba703e3ddfc2a4a2581289b0320f665e103a6b614b812c6fa1569e2c7b53c4c98c5eee9e0fd126afef95a000000e902168d1bd382c0931d8ebb8b7b623d37eb4bd566d9e6ec80a471d019aeef73551b965ecd009db03652c109e9101316d6e2cbdf3d7f5d59a1da301eafa06f3c23bcd3905799ef3e24704df593fdfce28c6b31748bf98680b18866a7452d66d4399e243d305708066175726120b816700800000000056175726101011a3e3ec949ce0e316fc1d468808638ffa6e9ec33f07a63ebcccbfdcc8a674646f479868bb774bec8f6cc27a8ff53bf792e28eabbc2077cca7964e5909f93cc8500000000bb1b12011401309ce33b9c1403e75086093f0238fd0389a8ae3624067e32d6977c00f8fc9f2ad04858e9d30cfd081876e4a8d3dac47f976b2a3769d3eb7b618f90ac8b44d48302149ca7a0f13b5dc1df673caf4fe365aa466f2ef1a17f8845a188b70e4d16e22d00b0c424ab4c56c3d30bda0632fb2f0670fc3e0fd080eb3a174fac843c97538f02681acbd4976a098fae92f8fb165735073b41c6dc84a7a513e12021d5fd800145bd8dd90a048f3bdc3482397e0adf7c01a3227b5ca5241f8a26bf6c5833da958c02325c824588f075b440dffa96103df3bf6e8aef9f1718c9017680be2df294a05f5e1a716225875d6761ebfbb48bf56047266fdbbf353065195bdf0ee5296f6d8b02ec901bf7e5ae0cb6b5e8a3a3722c117c5c0363fcdbc8e75d9f86d1cf7a855946ea98efbd1243621b5520cdd0c5c8806dc080af780413fd62e31ac1fed9f8d18c141ff0070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a536469ce5beb2dfb3ac648b112012fd1aa9a8d3083296784f635080708f987af4b0a7db17402c36206ca7a903432b2f937cde95499720ee998333b6ab0df74f47f223cb59242effdeec4be62fe5621a3591897649c88697c278d2b3f8bb56aedfb4658a9416b968ee59c97d8d534ac6d04d8b83d5fbfda72076d5eb399c8a4649518f9e3371585fa2d4ceebaee6e8bebd754c5ac5bcaa3ee703d89b64fb3dbf2478828e48798c6530e176dd604317a6200c0980f4d941839d0df00fe40c0e3548cbd9a25be65307294d3ec4c33f992c682e4c178bb9e471daf205c510e1c914e1bb9f8cb5040d1bbf541e0909a87a87b7101a8a43ad43eee2a62f712a8a2fad7070000008903c13c40e05a7fe6f25765c8bdf70df04835918551059f640fc9a0f1be412cd94a12d2eb00e111105d2a738c5b7547f971fe61e3e91926872569f50080cc107bfe01bf66930244d3ec63cfb049a9ec60fd808d7efa17e93a07e7196948c50ca6b7cc63500e0c066175726120b8167008000000000466726f6e88011125c4f85aada74372de80c8de5886135b5b3f061438d5901d457c3a2834dfe800056175726101017c99a96bac36d77d390673c34c180bcdc2eab66f9f2b714e354d4b97ebaf0e647f62c6907221002f6eccbb2df11448b6328385f2e8a85d74a15f761c66e4fc8a00000000bb1b120114010e645ab34ec7a96bf63f7e1c25b08906968142dd14e047f751a0de16839a1c322e908090f5461c9b4f0ea109b9d02ce2eaefde5c3031c375e311d62673464a8c02cea89063d18cea74085eaa3f4fd2739fa887de663fe40ff36b8931fb12dbad153fd2917b98d49f56c77b396857661797b25efb36d33aa20aab53a07c152cd88802847571b93aa74868006a008b854dc6bf1819c58540914aafd60b32786bd8094a9294517c3358f7547c867453d550eedd974bdbae7114701682f0773c4d3b378b0266e894a407d3e6101c6c1cc3569bacb6a5a58a35c156747526895a292f3c5c114a08507f6d28f3b4bb5e8506a6d3c89e936f8ef4cb1283e701e4044ba0b8d9890252bc7c7080360d62d343c6a0a52f3b446bf655564d4fbd4ece3f88997c50163369e0be0568a7dfbf00b653784597a6205b5e4c461a76b2a773865340d3818680141ff3070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53b2333f8e77e549f9c3253efad8a1ca915f92cf175db3e5c6227c663b81271f58510addf322a269d537bd11d42bfbc1ea800969eef5bdfb0b1603c53b86050eeb4e0bae6fbe63291a3707666ed61bf7bb8aa8074da204e7976630dc731675a33a0ad56bb4f660a6b78a706f23e036983b6249f2c919dfee1e178e38803ff6f432f4c64603cc8acc1eb3a4917f4998b14f4ef174bab0b3651a004d128df9f6d13676fcc04a17329ae89e93bbffda8eb8199c768f413be1db9acfb9703627048980a485fe1741cd894748d231f109726c1c2c3f016a047c06701420d1f8de5fc36d67564ba63b2ccb473407246b5c770a4c488bd2ccefda43c9f8a9bccf1bcdfe45000000e90257a5f23ef1b03a93419998203a32f6df9693180e2d8e2ddd741ba7f2ccd4efd9c2bbe400109f64403ffd5b54ee09c3e4bde230afec5995bec724748dff8ce22412a68d55bc7d44805213ea1aae41fdd6ab54b4e187a96732f07094f95d14d7db8d0b669008066175726120b81670080000000005617572610101e4747d6f1f49413b4b3a354ad0249acf90888bc6e968fdefa61bf76f5bb8ef0fe30a79be40de69b9bd1d2a8409474c9f6143e85e5a1382822db4fd625845698800000000bb1b12011401dc68fe050217f7e843a7afb4abc52e8e9f3143e50107e2464539c309b561c40791ec3ac2e6a0c67f143668ecbb7baa1307699caf07b71ba2a03a7b5fc854658302948af828d4c034fae816e7eb9e6d93985b372ce08f97f40d04d93ddcdcf01369fa07ac3df08f444f012f36fcdadeb4d959596bacb17f9807ab152a3a30ff678b02ae01782d5798bd3e4c2e35b9de8b9e71d8160014c3878b12694a3d1bb5abf9049fa08338ec17da41b8e4c22fe7d78f8582e52600d64c58b2213701fd79933d8402e051285bb93d493d54cea53e149a7a46720cdf7234c17b54c27038ffa12ace215caa85c31664a595e5d8a95fac2642498ed74c38cfd4b22c5ea148dee8fb39820108d0bddd14535af4a1d92f279b0cac5f99c83e4679d02af0af9f5e05ae3aed07325cce03c905a008a7cfb4155c79cc8ba0e71b8f124737476f84a4feb8774f80141ff5070000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53dc6686ec41cff4a56c507427f42516974a7872e67c2dd72f70900f054bb5f0146ce389aa0364dd11c6be70d1016b3f6c24983fca3f213cd7eda578fcfd41bcb21aa4a6aa57b651ea361d3f0305b2bd0430ee8eeb4e2f4f3dfc06e0e1d5392cddcc783b551eaee5d171b21218b1eebbec37df3721a8a783aacade1f13ca325c81da5ac567ce15b57388eb542e9964e04decb1f4b976560c0e53c247860db41e309254e1548f6c11bd146102d64e9675336a3aa248a41781334321b5c66d85b98fa4dfafec1216d7e83dbe47d352ac1c32b0dea02eb81cca65f0c4598fcc56f2a94ef714d8f4e59f1a17e129d4db340be93da7c73c32b190a247dcd5f79586c3e80000003104c3044f5d4c7487e09bd263beb225ac2478f09f52e5e804c73f6d1f469e91436b7eb1d900549675565ea848bd53cf799de8e7db5370d53854f720f0af313ea14826278c52cef8622a592021fc0906461727eb8e11be08f94b40cdae05775dd425e045ad3210066175726120b816700800000000045250535290f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7fee6e48040466726f6e88016a85aa5e08343c4811f362b0c91535184044de582d8946bcdb9f06811d4effc30005617572610101f677e8cb1a29ae6826adc1e6cab8c9fd2eb4c7c192e2deb318b3de60bb60ff78c68a31bc1cafc990ed8511c34c9f9d02f5fccf58519e2c24d7847b0d11ac2b8000000000bb1b1201140122c781a15358b583ce23f3054f4ade939fd2b03ba5536099bd3956fa1debd01b3fd947f3464c61c7553c805f34643b6d235e5637ef31043b7a392ad2a7145b840240cfecf529d19aec53ec8e146960b49481ff7d30c7d8aeb1dc3232d15337bd115406be54330f1cdfbab93730f42ba3323d1ef0e4941a2bbdda42d7f9f5eda789022ef09eb50d5c1719fad91aa2658745df88d64ab90f2cf0b3c154786eb554b94a3fabbdd2cf484f981127a302ed07e331dcdf632d38f18864816f2d5376868d8c02b8e62c680475c06665eff6b941d65e98e417e21460da5e04fe9cf805e11a8031fed0b942530b2751c1839374c1d8268137f65a9865bd8a9ff48a0a493636b78502dcad02788219b047eac660e97ff5e3f727ddff06e24ab101fc65a229a6396e5b3524527b735b79d7c7132707852957244352de95d13528b6f1fb1fb312ed9987141f03080000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53ae9e524e77df403ced99f20c997452a7bbfe72c3751ed39c8b14431d8d56e82e941cd69f5aab0bf9f02afac6a78ab984093570a53229abd2785da7826734a2648d80b0af1fd697343d33d1df8d71b9fc6a66d1a77953d52aec6984ae633bec15d529386130aa56b522cde6862ac3a541dadbfbfc5b648e3b9bcc773e7f13361bbac05975b4267885d3731ce6f6ac62c32b73e337b7c453f055e4537ed8388b18a3d1cfb61862aef1510c11bdb18213b222c7260f53cfdea7483d67f28717d68dc7e2201b509b1a39094da633e88130dd656d4eea3638b6b92668b76e3427a7eef24b1c1afe56fbfcc4df921379be93d6320632d3d5cb60e54eab05a43d397434000000e90254cb569f236bc0fab0f8e008a357790281bd3402499fccdd280eb969de91dd509e708f00844c3b912423cff594c643cb7ca17454f845aa94c4557b1f2fca62ab0aa1ba97387b6c6104aef90d2bf23720799a4dea8038b1fa773740d23e8750e2c1397a4708066175726120b81670080000000005617572610101828c2a93b966d6a703f914bff67841517f078637802fa87868c66a21e7bb187d3da7feac169ff3a43f18bc16e93fa0eea9c47e4d1868a02c851e661e2051858d00000000bb1b12011402a6f8758bc3ab28790c617b88aab1592f164a5fe1711d95f7fabdcb30c3da9f06478a194c5d36bbc8463a8002aa2fbc3fd0515eba728c7ca76b57f19edd3cbe860232e7d6defe6e44cdfc9275ced29c56ed08b81c7f74db4821edc8aab21793305586127994cefa98a5032d24e03d1c1c482676f1cc9ada594253f68a62168b798301deadc9359074cbf964c56345da6f170b0418165f7ebd84a8cfd5cc49973994265c57d94c1ed6707a9d78127e18d8ebeca57187f6fc1feb23ef4b160ff8702184028e200b04144d49c8306ec5ba0cf6bb2dae1822732eddfae93fcaf271ce4225473020ddd90b9f110ef8d4cc0033b8c4434fb5fe5593ac89e8d626485c9dadf5880212167c5f7e2e643ba0b35f4234e67c96ebbf3cc85e933e2ca1531bbfc1683f2f2d364fc0676a9141fd971f3efc1219f0cf9c84de707c842ceb8690b90cc4798f141f0a080000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53ec204e8737a4a6c7b1aa054e166f77540d2851b2be436ade94fb500d54aebd4a8457c39ca6784cc8bd5a2b64cf54ce97385b33f3adb976b7ba0d575712ba189ffc2854c82dc5e52f76fe34acedf808ba9d6eda8ea388a8efdd6b446ca56e8e86ad9c18ef81d09f3b09574772569c2e6a2b8ea720fe66eb31f841ea99f4715a4f14d043b615cac82295a68e60aabfda5e79b4ab0e213fab796481e7d4fc7baf11aa54587ae52cc5dfd21938b7133e5e30cf6908f545e6f5b98063936ab124cb8cb7fac0bb039ad73ccbeeaf16c14cc7e0ec5fed28f42a816180de22981e9abf2f349bac4ca39f6f77e5d4dc7b8435177212609badfe18bd85c4ae6bf2d1dbc89a0000003104a6323c455f780ce0ebd47a24c53c115769450c4dc32514e07b0e2e689b74aa758a340200c23a87d314203e83d0d5265b820de7d6c3ea47b7e4a18e8f0601b625ea8d627c05d2bda719ee4661cfe8c4e33bed2ff60b6e03ba9abf78859ca0ac44487ba52d10066175726120b816700800000000045250535290f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7fee6e48040466726f6e880185d08b836356bcf9e43fd54d6b30d2a08f451707c2d32d0fedd6c66c5471b15f000561757261010140f971209efcc27849484cbbb1064a291bf78f265efff46e843b6fbbc999f23d282bc94541e02c9eb28271574448d227a63100cd2c33b17972d435b0d254dc8300000000bb1b12011401408340871ea5089f46ab24504d90ec523db40ae4e47a620ceb9582f94f418d00729332feef4368d984315574681ef240ea93e7ee2b9f5c894633ff25f184348a02aa1b8988bb8af4948b472a0f1d95bb7594bc5d722a708547987e7dde6120c17413125b15218bce469faa159b322679877ad957c1220796a615bb3a64d335158e02ccbf990fb354280e498dfbaf746843009726673e241109f84b8e3496b55a9a0b1e24b33bebc7a85550cdab19c743c687f69fc5a46942fa4c939022e16aa1f58b028ce4cd59675074f661f3ac9ec08d00a33141f18b30adcdcce3930da235e6e60f1a477ca4651aa442ee870f84093729d339a9ab4275dde5ca9c82ae3521e71f8f013e563b35dadcd981d1fbad92ff67ecf92f1aaeb0d1b1064383f2f37797661d10294f3f265583dfc7d51947d12e5e977329a91f2d6f5e7d96d05911011a541488141f2b080000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a537efc3d98f8086a3fda58c7151510085c92e1dfc75e9b7624ea07054680bbe471b1db60203239e02b7c18bcd3f5e75407853b3a5940c42ded7aa0c76d7a15c2d474d5984c5c368098f582e80cd74e13cd8236e6e964c88895fcb4cfefdb836083f3d6d453d0ea9bc3a99368e1c29ee416f8ebc0dca0802f0abf19eddbf5f2f4f88818571eb61381d00751d8ef81c40d51ae8972c9b00e100761601e212b618f01f11a39c8e2fb4df11faa39554f43c6e3ce17f9ad65addd8ca0592d6470163884f764cfc2cc18abcd39a458fbbf1c6fafadc482c46956a474dbd8394225ac3158968681974762c4dbaf591c05c4cd50d15e94f876bda8d5582e4de4fd361fbf63000000e902f9f7d09c1fe4292afec64262438664d73dbe7be7b8ba3f4eba1ee0e0d5e4d8bba69a91005101c2a5f88605b351a37e0156facc867f0d87a2872ec0db65a3406a5cbd46c6c801b537a9917c39574e4f436f2fc8d7ccaa956a9635ad8d99ed71d2ad217fe708066175726120b816700800000000056175726101017c64073519419b519cf04d04a7113807979427c6d9dc8a963380b2eb84e13379a5a86263e8b3db50d5a3ef2592e1507e67857d3b1dacfc7f7289a666a245e78b00000000bb1b12011402dce883d99fc13786638680a562e92053f0d9b0bcb24d6394c3421190679248151a90fab993c5abb88e535e2c6973545b3177308dcefd6edcd110dfbec9becd85014e2be754a445b770fe19c2a18aaa95ec2fa2f6bef2fcc9bab127abcc55f80d029dd6b03201f131eda390b41f88b1fc52308e6f81d66f5f6c58b014484b67cb8602d83f0b83872d7ad818722204403099f1f6ab2e026e344207c1c43eeaff707f1f3f7cdc684fc2170633de230de13f788ee7e2b0f96474c8c3acf1bc1aaac4f58e02a622a24380a63c82060437fa7e5f274033eada6b48d2d5b256b406ddde81c05b6cabc4ede8a839fa7c4389e5f5666b557bf6c58565a0e02fc7ba170abf03588f0240b65b473bb04e23662e8978861a8f7bfd1d7f09709909091c0ccb8e644b450b07ebbbe918f1f9aeb43de95c6e71218b2d8deb8a5a5ef8d8b0977c6525518282141f2d080000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a532ad2b4d382b5b19d798bbb24dc8274668c5d144d8cb6bc99d3202e89da0cfc62e48d3330f2488cebd7d88dc305a2e6198a7fcb3263813f1aee62a427f047e7c9b03c329557c969002d3c354fa3611fab7657ac1e62734d5e84c96410420331e5c687c051892c12957632e300f1e11feaf2825457e49de98a86e381c02e1aebb0eea625d7f5f1e4841d3b72cc569863665a8f75f4268ea27a89e6487001036a2321a4a620b324e66b36584b4d57f28b35e757c535054f2fd02bf23087f38110857f58c03fe7cc8f5c6d2c13c8e1d517c45c84b7571d3749bec7c1f8c76992273daaf8b111859be1b3d33934ee9bc8a6fdc2bd674f5236099d42bf469cae8e9dde000000e9026641c6f7dd5e302baa0ab89581d661bb160c5dbd73cf37896031ce136cb3ae5eda1c6b007118dbc423bb717aaec75935ad90704e2b6d7c44dc524f2913961239c7e7fff7885806aaf47b5d753fd543dcc80f9d555341c36f83899e662ad54da963b166b208066175726120b81670080000000005617572610101fe1a8715c01c39141bf1bb5b285a48b40bc801e0e6097344b66cf16b471c30065b8c6375de0415b3f622542b199fb47e23d9026d30712bcb5251d7cca7601c8a00000000bb1b120114025cc08e0bb092c1a31c04841a9c325a23373d529662647f3ab20ce8cce4cbbb1767999dd49cffc69f880fa401910dcb6282052bd67a727dce98365e1ae4167b8802ead82d5dc1bd6f9e1a052e13e466ae365af912f1242cb568787ee0ed48d8c51817fd0caa95dc54d2a15a16cf1d7be97c7fcc692ad0d3352c91eb63de5b3ded8b0180167f43f35c14831d28ee819853d5d84a7f4772d1adff9a5a0fdc227fe96a6ed498d83e959f14dc713303931eaf5fff331b31c54f8936519b18f314a9defa8c0164a736c00fc0660186ee46c65e2efdeaab5d24c5d72c458e91e2af32103b724a14576a7b0fb759e242758978521dfda77d9ac1d487f02e8af34cb6333f7c58830130d4c8cac61fc5b04f2a20f290f1759d15c0bae7037c9a442972e8b823338c479510506d603e7c26cf895385b6e519b9a84c031ed7684b31db1ad82a520fcb87141f35080000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53e6271a585fb85d43626bcc811ff8e3c9c9c05cd5777f5ed557b49f7f7a86881d3211bce27d9df3d797624979f14c52289392fb645c082a43a2b3bd6a6daf5aed00d5e9450862cc3fdcd61b277453e4f82d40c8dc16ecdc3dbf239ecd6c11541067ec7d37e15fb84f82533a065d4f546ab637cfe4b60750da433e4b6332d616ad305d4a8c3370d6b60ce51d35a5bc21df1492d72c694eeeee50fb2384ff5c333f635972b02fd79f7f54d623dff79aa78b106a6f80a2f8b0bb3cf0b1b52b77468e50a30bbcd434f068eb381a33236aa44b4a3d671eca4b3d8e99aeb94853fc1a0cb25b6ad33d86e31a5f81ff4c731b7c5505850c0a5970a522c41045ec52dffc3b000000e9028ba4aef9760e30aaf6e34dc7cf62fbcc24d9e6f8d53ce8e2e7e9f87b3dc82b3ec69ff70082c28e8faf03b1cfaca464901991135e28698ebb642e6f8b86ce143aa62baf1747628a8c7939bfe9cecde9da6c93638d776a1173140b1bf3e63348492b57f07c08066175726120b8167008000000000561757261010118c60424c9be708502d8114c3d148a8d20f12970f232449b4523e830cee9e0502e9659f6f116fbafb79c4b15ca2ce2ba8f00f69d022a8d9d5e56f72a50600e8900000000bb1b1201140256655ec9ce0bd76ac7a0f8e819b8445a7af9bcba2dba7832e0ad5a034ffb704ac1d9d49463104707fcc4c178b09981a7459a03c21303b5c35854c1834c00e78d022e36c06a0d37f5b6885591bfe035671b24dd62f25d11a48c686c15927379bd65e112bf32ebdd462636c3fe7b64022eaa40dccec101ed3c24a73ac5b9c17891860274defe140bf870b07303cd579e236c022d305f4db8fd41efe7e675be50496d58522c8094cdb1ced8ba8ee0b343fa2b80e31cf534bca3b3cd036c9e17f68d2684010c3d78003f7b811c9491e10c06afdc6c524445fb9e38b358ea5c8d337cef146c086715ba0b928553a53d2e24d646e483b069d0092fb6ae834cd5f76e9c543a810204956733ba8da9815a45b26b1932460015d1760def246937b06cbdfd7196a57a5ac4326cf08ce8bb15eac3c48f036dc81b2b82d57544544c2fd7003420060186141f050d0000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53acbb98d75520f180f26436807b1cb5ae68a0be25deb573330a90607f94846055d0e6b96591c8e0b2ddd7491b3c369039a6c286584395cf16e6815892dcccf5d5d1d113d6b0e4657a4123f00648361e428981083e0a8cfb7036adbfca6d0fe19d4c4245567f5e25c66b238ef0e8e24be2d1c2831e07596a1e9b56992fcaca665508ab88926c0e6d6910718d245861f9a0a20f7bf967ed340751c531bde5c97a75158b170fc46017e4c8fce430eeb00271834807e7fefd8a2de5ab21b6d87af082af112593dbeb6cf625fac54550d8d7b5713d7ad79804bd73547608088ac64df26e25771c2505f549b5af7734c84213bd1fd39278fa8f07298ba69da55a5c7089000000e902e46dc12278089308b06b1f0bc736dad63c8e38fb0a9eca347f5a3354afdee9df72ac6700ebf3d9e4efbadd73b5730f95fc16e3d4d2f01dd50977e01ab806c7db33c04823cc0da8ba406c53224a4ebddff1e4961d0e2863a3526914ff98bc41b99ea65de108066175726120b816700800000000056175726101011e91caab14149aa7d02c43a98d85bcf3356228799fa7acc886b63eb5d65f89529c2b13dae24aae8bd01744d9e9d699599059f9b90a573b1da3c25ecbd4ee4f8200000000bb1b120114012a7403b45235d2980c45980f3c163688b37fb98a6e86079cb7268bd86c452f120d86c04e0ec46d77d68c78bc51078f5acc83973d79a3434cc353255bb677cb8302b00e1f01bca5d3383fcf86018aa931ecbb1f96f1f01bbd07aadbd4ce0df2807dfd41bee5d8761b97f5943262ffd414c12ff2da8018c8310f87b9f465f7ca7f850212e1873116dbec2ea47348eeacbcee4e960ce8d89d69c6d15103b47373ffef75b42dc113163c36506ee5f74b1aa85c42458e49aed167d1d696e39bb1c81c798101a8bb77d267585a1cea2763a688b5f9964dfd1088e3082f188c36859796b71d37950961df112deef2bfc5249dc52e322417694c9956481625acf06aac9330988e02c6e851363abc1af7d3b0949b0824a516e2af103183009e9a3a760be6b8c929506cb50c0d5e821597bcebc9d792248fddc10753674b966e9441238fc1d17d788b141f110d0000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53ea674fcd7b7a215d5a4d8452cfbaccaa8152bce4e634cc210691df3b8f65674b16e4876f64f4e3d22c90b72de04f99307ba9f9e1f6fc10223471c8272f38effeeffe7f07bb593d26b7456fc947dac7496065f2d05f00607a3a165eafebc16e44ecbff66630037f511d2b8630aa486a575d649c8f2251ad73d7dc843078406620f4968fc74f54faab8d9766d7e8d32ed47c271fe7f746a03bf1a2f64b96869171092d34a7db51d2a1e6ce79ddbc3c38b420675020de310552fbc8b02eca6ff0879f9a90f7bd8f898025b86d8b8abdc92625f0815893307e4bdeb942c9d7e6d84073f790417fb85f65160f31d6d8283f3685b9f6027d7516cabd0640f0750b32de000000e902e69d93a71db604b1b17f23cf6b4b00f4437cf0cfd4cf685fa1476eb8a4888d26b6300300de195009a2eb39a403069906d86535b06a776c1cf96b5c4bd10c9fafa6e436fe73eabc4d38b711c40d856a3e16369045d9162831f02a21627e3b87a4fda0711808066175726120b816700800000000056175726101015e1377a508a295fdeeb6cb9c9dd0e2f8c496dd0d79b7d58b1edd1ff9a6abe6143bb5a913033195c82f53f71c63052b3c87fec231b0019380cf8eeda4f6874f8700000000bb1b12011401b40de399cd5c80bb8d2f1ac6a5969477a758c89e14747e03ea4de230aa8df60d0536554bdde6f58fb6d8e4cfd5f500d89c2a6209a088d7e27fa7bfa91eb7c685026cda79fea5cb5638b97357d67d490f256a0aabc3c9255e1b47548b2eb46f0f608b36396f42aff964126e4d6a29db57ea8f934ad89f50f22e4e995fe01ea76e8102026af1d1f8312fd0213376c325dc219cdb4b604a3a6e50cf4da23f690aa7210bb55930cc1fd22f8e0369294b37d7007c861b2e9361bcb4f160c893f340c3b98e022a53cda3ac37f5ff9f51b3ae352cd03e7641516fc20b3cf79eeac2550bced1021a99fd29a969d8847a692caff568922e3193f6cb62fc655eca2686762c62fb8402f015dada69267a0dab79b5b9d0918cde3d092914321094ff3ebcf7e3a2ed920520dae001da70d5d696747ee46accb3772411defdce2151a6efa10e8f162b668c141f120d0000b52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a536ea0133290020cca710f4648ef73cdd380da6da57d492cbb96348e946e29172a1fd0eaeb258c256c013d32a63ee5498b4fc772ba60568678ccabb6a07e1cbb68faf620e5f132f532749f244c91898c1a0321184089ff44fab8ad82b6cf5bce51a02a48fef776eae1a16257150bf5eb564b7deb240e659d5718bce95d698e849b9a937205e9d2ad0bb2010ade6a56953743ad222589f6f89e33581dc58d895a5dd8285c41ac6dd1e336d8a35b090d0b44a34d87d5d320f674a43c2638b196e083213b7416c3b7e381d428250a1cfc6b4969be2223376a651f0766dcd5e003e1941161fa8c2d2563429af767be4b1587b806118dcf2c16a881bb0f5f278f7fbe91000000e902646ac9acbc3728dad92b88262890e6920c4de391924e8d7b2111cc2615ee39bc6e280300636b52cd519f59b66c1c716518682b349f95820a3f676a7ec6ac927e37c2c6db7a79cb4ed03e614cc90bf2a69efdef1a9aceff538a4a5fcfb5d0fb7c4477c23a08066175726120b8167008000000000561757261010178cc22b3c30771d3fb6fb113c6a96248ea57cbf6bee1f72859fa86ccc7b73847ed81495defb291f05a45e7e822a1a69d27315da9453334424a9869d71fcac18b00000000bb1b1201140256b937354d181fa5bb2cd3034819311c0ffacf63851e2eb7fd9647414a08df3d6ae552c3ab6358ec0680a89d5dd70672f4486893a575d5f7bfb6a7bb73f4298402b05388ed2d16849fd0da144b1030a9498aa44ef5f553afae867cc0c4136636159e538adbeef60311f2fedeb18ec882493edffe3e64986784c152c63087d93b8602b8d08b07d1ed4b22d5dac111e4f64be30c4a141c37c46f1e7bf5881c36914516ae6884fc50f8cfa7d55f7ab9cbe29e95ec417b61851482c145f7527ec097f8850112011f2d7f1a6d21d2750b38939b0a95875f5f05d23c79c4622ae76cc24e8b1bf990c3e00507c9ad079f6768eec1f045b7bee9e2b5e8c8201b8d3174503850830222b1956b3aa6e5dc2b81049e2a0ec280152c887f4a2f4942de9cd2a0261f3512463a7a7a7c15f5711267d51e93635c77d994d7522fb7e7479d14c866a3bb9986141f0068e3c41796c61ec5fd3e717c95021d8987a4918867834561de7503d0a3b91e11ee6e4804f6274f5f47fe2a92220909fdf55cf1492b2aa41baef92cdea8de6e21387ecb7f57a232d553c0d86cfff9c2c2637949de6c3bc85be8bd4ffe8e6d6ba620cb8e15080642414245b50103ab000000712de010000000009c32370b1704e46de83459317641723d2ac4dc171ba4c5d79bc7e4015af02711312837871e6b12ef898ecec5efaa078c4008f7224f4357a410dc23b52693c809f1b8c91b93dfbddfba8b5fc583cb4455968ba6e5994b40a657365c02989c240405424142450101f67560c019a2a44892fd1660ecafbaf0b2c1b9e7222cfd538710c2edcdac67228861e92e87cf8f09a80f10b12c8a14e422fd91df2638180f0e740a26cc175a8b", + "0x4102840088e9f05bf9ce1ae5cf584ba3d43982bccf957db3e97d781220e5ddbdb5de6a1501e2775e3339265fab8c47f900e1a3d627e77d5ab36b8f3ff7de4ae6c7c9a37769201dc94a19d7fa74afea15e802ea7285b0919d95b45c00c295d50f71adb6788c45011400050300066a8fcd5add9fdc97485581b00862bddc2f5c7fef7d0a64bb9052d8534ecd4507001ea6a752", + "0x450284001c273165b790274c4232543b81ac6510525abe9ee8cb47b36544f371fb0cae49019c5677183aa6e23da0f9310ca90f7f6f0798a55d329cbccf36be7c64025f1407eafa798c5274203f66b604ced0c0bb4dc68e0e71db4d3f1d0599f29d4563b28945030c000503009cda5b3931d308d24e236f6e3ab5c50a99fd4922bd80f3a3b9bad5b0c8d93a350b009c62337201", + "0xb9018400c0d998fb9f36dc65132dd06a845548d564e1db500fec59a8926a8fc75a8ba4460138f470ba7cdee1cd52271bf24456deb6ac041c33a081ec719704a578c1dd2d5feb99322b9de9276f683b70e7f592b4ff84c635e50fdd57f29e8ef6317f8621887503ac0007010300286bee" + ] + }, + "justifications": null + } +""".trimIndent() + +class PolkadotChainSpecificTest { + @Test + fun parseResponse() { + val result = PolkadotChainSpecific.parseBlock(example.toByteArray(), "1") + + Assertions.assertThat(result.height).isEqualTo(17963964) + Assertions.assertThat(result.hash).isEqualTo(BlockId.from("0xb52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53")) + Assertions.assertThat(result.upstreamId).isEqualTo("1") + Assertions.assertThat(result.parentHash).isEqualTo(BlockId.from("0xb52a9b51fb698a891cf378b990b0b6a5743e52fa5175b44a8a6d4e0b2cfd0a53")) + } +} diff --git a/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecificTest.kt b/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecificTest.kt index f48122f56..5b5c66bc4 100644 --- a/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecificTest.kt +++ b/src/test/kotlin/io/emeraldpay/dshackle/upstream/starknet/StarknetChainSpecificTest.kt @@ -1,7 +1,6 @@ package io.emeraldpay.dshackle.upstream.starknet import io.emeraldpay.dshackle.data.BlockId -import io.emeraldpay.dshackle.upstream.rpcclient.JsonRpcResponse import org.assertj.core.api.Assertions import org.junit.jupiter.api.Test @@ -23,7 +22,7 @@ val example = """ class StarknetChainSpecificTest { @Test fun parseResponse() { - val result = StarknetChainSpecific.parseBlock(JsonRpcResponse.ok(example), "1") + val result = StarknetChainSpecific.parseBlock(example.toByteArray(), "1") Assertions.assertThat(result.height).isEqualTo(304789) Assertions.assertThat(result.hash).isEqualTo(BlockId.from("046fa6638dc7fae06cece980ce4195436a79ef314ca49d99e0cef552d6f13c4e"))