solution: basic implementation for Proxy endpoint

This commit is contained in:
Igor Artamonov
2020-03-19 23:34:32 -04:00
parent 7455949541
commit 26f8dd294e
27 changed files with 1309 additions and 135 deletions

View File

@@ -0,0 +1,67 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.grpc.Chain
import spock.lang.Specification
class ProxyConfigReaderSpec extends Specification {
ProxyConfigReader reader = new ProxyConfigReader()
def "Read basic proxy config"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-basic.yaml")
when:
def act = reader.read(config)
then:
act.enabled
act.port == 8080
act.host == '127.0.0.1'
act.routes.size() == 1
with(act.routes[0]) {
id == "ethereum"
blockchain == Chain.ETHEREUM
}
}
def "Read proxy config with two elements"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-two.yaml")
when:
def act = reader.read(config)
then:
act.enabled
act.port == 8080
act.routes.size() == 2
with(act.routes[0]) {
id == "ethereum"
blockchain == Chain.ETHEREUM
}
with(act.routes[1]) {
id == "classic"
blockchain == Chain.ETHEREUM_CLASSIC
}
}
def "Read max proxy config"() {
setup:
def config = this.class.getClassLoader().getResourceAsStream("dshackle-proxy-max.yaml")
when:
def act = reader.read(config)
then:
act.enabled
act.host == '0.0.0.0'
act.port == 8080
act.routes.size() == 2
with(act.routes[0]) {
id == "ethereum"
blockchain == Chain.ETHEREUM
}
with(act.routes[1]) {
id == "classic"
blockchain == Chain.ETHEREUM_CLASSIC
}
}
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2020 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.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall
import io.emeraldpay.dshackle.test.TestingCommons
import reactor.core.publisher.Flux
import reactor.test.StepVerifier
import spock.lang.Specification
import java.time.Duration
import java.util.function.Function
class ProxyServerSpec extends Specification {
def "Uses NativeCall"() {
setup:
NativeCall nativeCall = Mock(NativeCall)
def predefined = { a -> Flux.just("hello") } as Function
WriteRpcJson writeRpcJson = Mock {
1 * toJsons(_) >> predefined
}
ProxyServer server = new ProxyServer(
new ProxyConfig(),
new ReadRpcJson(TestingCommons.objectMapper()),
writeRpcJson,
nativeCall
)
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
call.items.add(
BlockchainOuterClass.NativeCallItem.newBuilder()
.setMethod("eth_hello")
.build()
)
when:
def act = server.execute(Common.ChainRef.CHAIN_ETHEREUM, call)
then:
1 * nativeCall.nativeCall(_) >> Flux.just(BlockchainOuterClass.NativeCallReplyItem.newBuilder().build())
StepVerifier.create(act)
.expectNext("hello")
.expectComplete()
.verify(Duration.ofSeconds(1))
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2020 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.proxy
import io.emeraldpay.dshackle.test.TestingCommons
import io.infinitape.etherjar.rpc.RpcException
import spock.lang.Specification
class ReadRpcJsonSpec extends Specification {
ReadRpcJson reader = new ReadRpcJson(TestingCommons.objectMapper())
def "Get first symbol"() {
expect:
reader.getStartOfJson(input.bytes) == exp.bytes[0]
where:
exp | input
"{" | "{}"
"{" | " { }"
"{" | "\n\n { }"
"[" | " [ { } ] "
}
def "Error for input with many spaces"() {
setup:
def empty = " " * 1000
when:
reader.getStartOfJson((empty + "{}").bytes)
then:
thrown(IllegalArgumentException)
}
def "Error for empty spaces"() {
when:
reader.getStartOfJson("".bytes)
then:
thrown(IllegalArgumentException)
}
def "Get type"() {
expect:
reader.getType(input.bytes) == exp
where:
exp | input
ProxyCall.RpcType.SINGLE | "{}"
ProxyCall.RpcType.SINGLE | " { }"
ProxyCall.RpcType.SINGLE | "\n\n { }"
ProxyCall.RpcType.BATCH | " [ { } ] "
}
def "Error type for invalid input"() {
when:
reader.getType("hello".bytes)
then:
thrown(RpcException)
when:
reader.getType("1".bytes)
then:
thrown(RpcException)
when:
reader.getType("".bytes)
then:
thrown(RpcException)
}
}

View File

@@ -0,0 +1,178 @@
/**
* Copyright (c) 2020 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.proxy
import com.google.protobuf.ByteString
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.dshackle.test.TestingCommons
import reactor.core.publisher.Flux
import spock.lang.Specification
import java.time.Duration
class WriteRpcJsonSpec extends Specification {
WriteRpcJson writer = new WriteRpcJson(TestingCommons.objectMapper())
def "Write empty array"() {
when:
def act = Flux.empty().transform(writer.asArray())
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == "[]"
}
def "Write single item array"() {
when:
def act = Flux.just('{"id": 1}').transform(writer.asArray())
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == '[{"id": 1}]'
}
def "Write two item array"() {
setup:
def data = [
'{"id": 1}',
'{"id": 2}',
]
when:
def act = Flux.fromIterable(data).transform(writer.asArray())
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == '[{"id": 1},{"id": 2}]'
}
def "Write few items array"() {
setup:
def data = [
'{"id": 1}',
'{"id": 2, "foo": "bar"}',
'{"id": 3, "foo": "baz"}',
'{"id": 4}',
'{"id": 5, "x": 5}',
]
when:
def act = Flux.fromIterable(data).transform(writer.asArray())
.collectList()
.block(Duration.ofSeconds(1))
.join("")
then:
act == '[{"id": 1},{"id": 2, "foo": "bar"},{"id": 3, "foo": "baz"},{"id": 4},{"id": 5, "x": 5}]'
}
def "Convert basic to JSON"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = "aaa"
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "result": "0x98dbb1"}', 'UTF-8'))
.build()
]
when:
def act = Flux.fromIterable(data)
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then:
act == ['{"jsonrpc":"2.0","id":"aaa","result":"0x98dbb1"}']
}
def "Convert error to JSON"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "error": {"code": -32001, "message": "oops"}}', 'UTF-8'))
.build()
]
when:
def act = Flux.fromIterable(data)
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then:
act == ['{"jsonrpc":"2.0","id":1,"error":{"code":-32001,"message":"oops"}}']
}
def "Convert gRPC error to JSON"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 1
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(false)
.setErrorMessage("Internal Error")
.build()
]
when:
def act = Flux.fromIterable(data)
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then:
act == ['{"jsonrpc":"2.0","id":1,"error":{"code":-32002,"message":"Internal Error"}}']
}
def "Convert few items to JSON"() {
setup:
def call = new ProxyCall(ProxyCall.RpcType.SINGLE)
call.ids[1] = 10
call.ids[2] = 11
call.ids[3] = 15
def data = [
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(1)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 1, "result": "0x98dbb1"}', 'UTF-8'))
.build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(2)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 2, "error": {"code": -32001, "message": "oops"}}', 'UTF-8'))
.build(),
BlockchainOuterClass.NativeCallReplyItem.newBuilder()
.setId(3)
.setSucceed(true)
.setPayload(ByteString.copyFrom('{"jsonrpc": "2.0", "id": 3, "result": {"hash": "0x2484f459dc"}}', 'UTF-8'))
.build(),
]
when:
def act = Flux.fromIterable(data)
.transform(writer.toJsons(call))
.collectList()
.block(Duration.ofSeconds(1))
then:
act == [
'{"jsonrpc":"2.0","id":10,"result":"0x98dbb1"}',
'{"jsonrpc":"2.0","id":11,"error":{"code":-32001,"message":"oops"}}',
'{"jsonrpc":"2.0","id":15,"result":{"hash":"0x2484f459dc"}}'
]
}
}

View File

@@ -0,0 +1,5 @@
proxy:
port: 8080
routes:
- id: ethereum
blockchain: ethereum

View File

@@ -0,0 +1,9 @@
proxy:
enabled: true
host: 0.0.0.0
port: 8080
routes:
- id: ethereum
blockchain: ethereum
- id: classic
blockchain: etc

View File

@@ -0,0 +1,7 @@
proxy:
port: 8080
routes:
- id: ethereum
blockchain: ethereum
- id: classic
blockchain: etc